From 262d9e233fe2facc01d4ef170a2d48553dcdefbf Mon Sep 17 00:00:00 2001 From: Shohrat Date: Fri, 3 Nov 2023 23:53:36 +0500 Subject: [PATCH] backup and dashboard, user id --- plugins/panakour/backup/Dropbox.php | 40 + .../backup/DropboxServiceProvider.php | 34 + plugins/panakour/backup/LICENCE.md | 19 + plugins/panakour/backup/Plugin.php | 94 + plugins/panakour/backup/README.md | 104 + plugins/panakour/backup/Repository.php | 89 + .../panakour/backup/WebdavServiceProvider.php | 79 + plugins/panakour/backup/assets/css/main.css | 5 + .../backup/assets/images/backup-icon.svg | 1 + .../panakour/backup/assets/js/backups-page.js | 10 + plugins/panakour/backup/composer.json | 8 + plugins/panakour/backup/config/config.php | 141 + .../panakour/backup/controllers/Backups.php | 84 + .../backup/controllers/backups/_list.htm | 77 + .../backup/controllers/backups/_toolbar.htm | 49 + .../backup/controllers/backups/index.htm | 2 + .../backup/docs/images/oc_backup_config.png | Bin 0 -> 164471 bytes .../backup/docs/images/oc_backup_config_1.png | Bin 0 -> 135347 bytes .../backup/docs/images/oc_backups.png | Bin 0 -> 66782 bytes plugins/panakour/backup/models/Settings.php | 97 + .../backup/models/settings/fields.yaml | 70 + .../backup/updates/seed_default_settings.php | 22 + plugins/panakour/backup/updates/version.yaml | 13 + plugins/panakour/backup/vendor/autoload.php | 7 + .../backup/vendor/bin/generate_vcards | 241 ++ .../backup/vendor/bin/naturalselection | 140 + plugins/panakour/backup/vendor/bin/sabredav | 2 + plugins/panakour/backup/vendor/bin/vobject | 27 + .../backup/vendor/composer/ClassLoader.php | 445 +++ .../panakour/backup/vendor/composer/LICENSE | 21 + .../vendor/composer/autoload_classmap.php | 9 + .../backup/vendor/composer/autoload_files.php | 24 + .../vendor/composer/autoload_namespaces.php | 9 + .../backup/vendor/composer/autoload_psr4.php | 36 + .../backup/vendor/composer/autoload_real.php | 70 + .../vendor/composer/autoload_static.php | 188 ++ .../backup/vendor/composer/installed.json | 1524 +++++++++ .../graham-campbell/guzzle-factory/LICENSE | 21 + .../guzzle-factory/composer.json | 40 + .../guzzle-factory/src/GuzzleFactory.php | 97 + .../vendor/guzzlehttp/guzzle/CHANGELOG.md | 1327 ++++++++ .../vendor/guzzlehttp/guzzle/Dockerfile | 18 + .../backup/vendor/guzzlehttp/guzzle/LICENSE | 19 + .../backup/vendor/guzzlehttp/guzzle/README.md | 90 + .../vendor/guzzlehttp/guzzle/UPGRADING.md | 1203 +++++++ .../vendor/guzzlehttp/guzzle/composer.json | 59 + .../vendor/guzzlehttp/guzzle/src/Client.php | 502 +++ .../guzzlehttp/guzzle/src/ClientInterface.php | 87 + .../guzzle/src/Cookie/CookieJar.php | 316 ++ .../guzzle/src/Cookie/CookieJarInterface.php | 84 + .../guzzle/src/Cookie/FileCookieJar.php | 91 + .../guzzle/src/Cookie/SessionCookieJar.php | 72 + .../guzzle/src/Cookie/SetCookie.php | 403 +++ .../src/Exception/BadResponseException.php | 27 + .../guzzle/src/Exception/ClientException.php | 9 + .../guzzle/src/Exception/ConnectException.php | 37 + .../guzzle/src/Exception/GuzzleException.php | 23 + .../Exception/InvalidArgumentException.php | 7 + .../guzzle/src/Exception/RequestException.php | 192 ++ .../guzzle/src/Exception/SeekException.php | 27 + .../guzzle/src/Exception/ServerException.php | 9 + .../Exception/TooManyRedirectsException.php | 6 + .../src/Exception/TransferException.php | 6 + .../guzzle/src/Handler/CurlFactory.php | 585 ++++ .../src/Handler/CurlFactoryInterface.php | 27 + .../guzzle/src/Handler/CurlHandler.php | 45 + .../guzzle/src/Handler/CurlMultiHandler.php | 220 ++ .../guzzle/src/Handler/EasyHandle.php | 92 + .../guzzle/src/Handler/MockHandler.php | 195 ++ .../guzzlehttp/guzzle/src/Handler/Proxy.php | 55 + .../guzzle/src/Handler/StreamHandler.php | 545 ++++ .../guzzlehttp/guzzle/src/HandlerStack.php | 277 ++ .../guzzle/src/MessageFormatter.php | 185 ++ .../guzzlehttp/guzzle/src/Middleware.php | 254 ++ .../vendor/guzzlehttp/guzzle/src/Pool.php | 134 + .../guzzle/src/PrepareBodyMiddleware.php | 111 + .../guzzle/src/RedirectMiddleware.php | 255 ++ .../guzzlehttp/guzzle/src/RequestOptions.php | 263 ++ .../guzzlehttp/guzzle/src/RetryMiddleware.php | 128 + .../guzzlehttp/guzzle/src/TransferStats.php | 126 + .../guzzlehttp/guzzle/src/UriTemplate.php | 237 ++ .../vendor/guzzlehttp/guzzle/src/Utils.php | 67 + .../guzzlehttp/guzzle/src/functions.php | 334 ++ .../guzzle/src/functions_include.php | 6 + .../vendor/guzzlehttp/promises/CHANGELOG.md | 65 + .../backup/vendor/guzzlehttp/promises/LICENSE | 19 + .../vendor/guzzlehttp/promises/Makefile | 13 + .../vendor/guzzlehttp/promises/README.md | 504 +++ .../vendor/guzzlehttp/promises/composer.json | 34 + .../promises/src/AggregateException.php | 16 + .../promises/src/CancellationException.php | 9 + .../guzzlehttp/promises/src/Coroutine.php | 151 + .../guzzlehttp/promises/src/EachPromise.php | 229 ++ .../promises/src/FulfilledPromise.php | 82 + .../guzzlehttp/promises/src/Promise.php | 280 ++ .../promises/src/PromiseInterface.php | 93 + .../promises/src/PromisorInterface.php | 15 + .../promises/src/RejectedPromise.php | 87 + .../promises/src/RejectionException.php | 47 + .../guzzlehttp/promises/src/TaskQueue.php | 66 + .../promises/src/TaskQueueInterface.php | 25 + .../guzzlehttp/promises/src/functions.php | 457 +++ .../promises/src/functions_include.php | 6 + .../vendor/guzzlehttp/psr7/CHANGELOG.md | 246 ++ .../backup/vendor/guzzlehttp/psr7/LICENSE | 19 + .../backup/vendor/guzzlehttp/psr7/README.md | 745 +++++ .../vendor/guzzlehttp/psr7/composer.json | 49 + .../guzzlehttp/psr7/src/AppendStream.php | 241 ++ .../guzzlehttp/psr7/src/BufferStream.php | 137 + .../guzzlehttp/psr7/src/CachingStream.php | 138 + .../guzzlehttp/psr7/src/DroppingStream.php | 42 + .../vendor/guzzlehttp/psr7/src/FnStream.php | 158 + .../guzzlehttp/psr7/src/InflateStream.php | 52 + .../guzzlehttp/psr7/src/LazyOpenStream.php | 39 + .../guzzlehttp/psr7/src/LimitStream.php | 155 + .../guzzlehttp/psr7/src/MessageTrait.php | 213 ++ .../guzzlehttp/psr7/src/MultipartStream.php | 153 + .../guzzlehttp/psr7/src/NoSeekStream.php | 22 + .../vendor/guzzlehttp/psr7/src/PumpStream.php | 165 + .../vendor/guzzlehttp/psr7/src/Request.php | 151 + .../vendor/guzzlehttp/psr7/src/Response.php | 154 + .../vendor/guzzlehttp/psr7/src/Rfc7230.php | 18 + .../guzzlehttp/psr7/src/ServerRequest.php | 376 +++ .../vendor/guzzlehttp/psr7/src/Stream.php | 267 ++ .../psr7/src/StreamDecoratorTrait.php | 149 + .../guzzlehttp/psr7/src/StreamWrapper.php | 161 + .../guzzlehttp/psr7/src/UploadedFile.php | 316 ++ .../backup/vendor/guzzlehttp/psr7/src/Uri.php | 760 +++++ .../guzzlehttp/psr7/src/UriNormalizer.php | 216 ++ .../guzzlehttp/psr7/src/UriResolver.php | 219 ++ .../vendor/guzzlehttp/psr7/src/functions.php | 899 ++++++ .../guzzlehttp/psr7/src/functions_include.php | 6 + .../vendor/league/flysystem-webdav/LICENCE | 19 + .../league/flysystem-webdav/changelog.md | 56 + .../league/flysystem-webdav/composer.json | 33 + .../flysystem-webdav/src/WebDAVAdapter.php | 419 +++ .../backup/vendor/league/flysystem/LICENSE | 19 + .../vendor/league/flysystem/SECURITY.md | 16 + .../vendor/league/flysystem/composer.json | 71 + .../vendor/league/flysystem/deprecations.md | 19 + .../flysystem/src/Adapter/AbstractAdapter.php | 72 + .../src/Adapter/AbstractFtpAdapter.php | 697 ++++ .../src/Adapter/CanOverwriteFiles.php | 12 + .../league/flysystem/src/Adapter/Ftp.php | 574 ++++ .../league/flysystem/src/Adapter/Ftpd.php | 45 + .../league/flysystem/src/Adapter/Local.php | 532 ++++ .../flysystem/src/Adapter/NullAdapter.php | 144 + .../Polyfill/NotSupportingVisibilityTrait.php | 33 + .../Adapter/Polyfill/StreamedCopyTrait.php | 51 + .../Adapter/Polyfill/StreamedReadingTrait.php | 44 + .../src/Adapter/Polyfill/StreamedTrait.php | 9 + .../Adapter/Polyfill/StreamedWritingTrait.php | 60 + .../flysystem/src/Adapter/SynologyFtp.php | 8 + .../league/flysystem/src/AdapterInterface.php | 118 + .../vendor/league/flysystem/src/Config.php | 107 + .../league/flysystem/src/ConfigAwareTrait.php | 49 + .../src/ConnectionErrorException.php | 9 + .../src/ConnectionRuntimeException.php | 9 + .../vendor/league/flysystem/src/Directory.php | 31 + .../vendor/league/flysystem/src/Exception.php | 8 + .../vendor/league/flysystem/src/File.php | 205 ++ .../flysystem/src/FileExistsException.php | 37 + .../flysystem/src/FileNotFoundException.php | 37 + .../league/flysystem/src/Filesystem.php | 408 +++ .../flysystem/src/FilesystemException.php | 7 + .../flysystem/src/FilesystemInterface.php | 284 ++ .../src/FilesystemNotFoundException.php | 12 + .../vendor/league/flysystem/src/Handler.php | 137 + .../flysystem/src/InvalidRootException.php | 9 + .../league/flysystem/src/MountManager.php | 648 ++++ .../flysystem/src/NotSupportedException.php | 37 + .../flysystem/src/Plugin/AbstractPlugin.php | 24 + .../league/flysystem/src/Plugin/EmptyDir.php | 34 + .../flysystem/src/Plugin/ForcedCopy.php | 44 + .../flysystem/src/Plugin/ForcedRename.php | 44 + .../flysystem/src/Plugin/GetWithMetadata.php | 51 + .../league/flysystem/src/Plugin/ListFiles.php | 35 + .../league/flysystem/src/Plugin/ListPaths.php | 36 + .../league/flysystem/src/Plugin/ListWith.php | 60 + .../flysystem/src/Plugin/PluggableTrait.php | 97 + .../src/Plugin/PluginNotFoundException.php | 10 + .../league/flysystem/src/PluginInterface.php | 20 + .../league/flysystem/src/ReadInterface.php | 88 + .../flysystem/src/RootViolationException.php | 10 + .../league/flysystem/src/SafeStorage.php | 39 + .../flysystem/src/UnreadableFileException.php | 18 + .../vendor/league/flysystem/src/Util.php | 353 +++ .../src/Util/ContentListingFormatter.php | 122 + .../league/flysystem/src/Util/MimeType.php | 248 ++ .../flysystem/src/Util/StreamHasher.php | 36 + .../vendor/psr/http-message/CHANGELOG.md | 36 + .../backup/vendor/psr/http-message/LICENSE | 19 + .../backup/vendor/psr/http-message/README.md | 13 + .../vendor/psr/http-message/composer.json | 26 + .../psr/http-message/src/MessageInterface.php | 187 ++ .../psr/http-message/src/RequestInterface.php | 129 + .../http-message/src/ResponseInterface.php | 68 + .../src/ServerRequestInterface.php | 261 ++ .../psr/http-message/src/StreamInterface.php | 158 + .../src/UploadedFileInterface.php | 123 + .../psr/http-message/src/UriInterface.php | 323 ++ .../panakour/backup/vendor/psr/log/LICENSE | 19 + .../vendor/psr/log/Psr/Log/AbstractLogger.php | 128 + .../log/Psr/Log/InvalidArgumentException.php | 7 + .../vendor/psr/log/Psr/Log/LogLevel.php | 18 + .../psr/log/Psr/Log/LoggerAwareInterface.php | 18 + .../psr/log/Psr/Log/LoggerAwareTrait.php | 26 + .../psr/log/Psr/Log/LoggerInterface.php | 125 + .../vendor/psr/log/Psr/Log/LoggerTrait.php | 142 + .../vendor/psr/log/Psr/Log/NullLogger.php | 30 + .../vendor/psr/log/Psr/Log/Test/DummyTest.php | 18 + .../log/Psr/Log/Test/LoggerInterfaceTest.php | 138 + .../psr/log/Psr/Log/Test/TestLogger.php | 147 + .../panakour/backup/vendor/psr/log/README.md | 58 + .../backup/vendor/psr/log/composer.json | 26 + .../vendor/ralouphie/getallheaders/LICENSE | 21 + .../vendor/ralouphie/getallheaders/README.md | 27 + .../ralouphie/getallheaders/composer.json | 26 + .../getallheaders/src/getallheaders.php | 46 + .../backup/vendor/sabre/dav/CHANGELOG.md | 2512 +++++++++++++++ .../backup/vendor/sabre/dav/CONTRIBUTING.md | 109 + .../panakour/backup/vendor/sabre/dav/LICENSE | 27 + .../backup/vendor/sabre/dav/README.md | 38 + .../backup/vendor/sabre/dav/bin/build.php | 169 + .../vendor/sabre/dav/bin/googlecode_upload.py | 248 ++ .../vendor/sabre/dav/bin/migrateto20.php | 417 +++ .../vendor/sabre/dav/bin/migrateto21.php | 166 + .../vendor/sabre/dav/bin/migrateto30.php | 161 + .../vendor/sabre/dav/bin/migrateto32.php | 258 ++ .../vendor/sabre/dav/bin/naturalselection | 140 + .../backup/vendor/sabre/dav/bin/sabredav | 2 + .../backup/vendor/sabre/dav/bin/sabredav.php | 51 + .../backup/vendor/sabre/dav/composer.json | 90 + .../sabre/dav/examples/addressbookserver.php | 51 + .../sabre/dav/examples/calendarserver.php | 75 + .../vendor/sabre/dav/examples/fileserver.php | 56 + .../sabre/dav/examples/groupwareserver.php | 91 + .../vendor/sabre/dav/examples/minimal.php | 20 + .../dav/examples/sql/mysql.addressbooks.sql | 28 + .../dav/examples/sql/mysql.calendars.sql | 76 + .../sabre/dav/examples/sql/mysql.locks.sql | 12 + .../dav/examples/sql/mysql.principals.sql | 20 + .../examples/sql/mysql.propertystorage.sql | 9 + .../sabre/dav/examples/sql/mysql.users.sql | 9 + .../dav/examples/sql/pgsql.addressbooks.sql | 44 + .../dav/examples/sql/pgsql.calendars.sql | 105 + .../sabre/dav/examples/sql/pgsql.locks.sql | 19 + .../dav/examples/sql/pgsql.principals.sql | 30 + .../examples/sql/pgsql.propertystorage.sql | 13 + .../sabre/dav/examples/sql/pgsql.users.sql | 14 + .../dav/examples/sql/sqlite.addressbooks.sql | 31 + .../dav/examples/sql/sqlite.calendars.sql | 78 + .../sabre/dav/examples/sql/sqlite.locks.sql | 11 + .../dav/examples/sql/sqlite.principals.sql | 20 + .../examples/sql/sqlite.propertystorage.sql | 9 + .../sabre/dav/examples/sql/sqlite.users.sql | 9 + .../examples/webserver/apache2_htaccess.conf | 16 + .../dav/examples/webserver/apache2_vhost.conf | 29 + .../examples/webserver/apache2_vhost_cgi.conf | 21 + .../lib/CalDAV/Backend/AbstractBackend.php | 216 ++ .../lib/CalDAV/Backend/BackendInterface.php | 273 ++ .../CalDAV/Backend/NotificationSupport.php | 62 + .../sabre/dav/lib/CalDAV/Backend/PDO.php | 1463 +++++++++ .../lib/CalDAV/Backend/SchedulingSupport.php | 66 + .../dav/lib/CalDAV/Backend/SharingSupport.php | 60 + .../dav/lib/CalDAV/Backend/SimplePDO.php | 289 ++ .../CalDAV/Backend/SubscriptionSupport.php | 89 + .../dav/lib/CalDAV/Backend/SyncSupport.php | 83 + .../vendor/sabre/dav/lib/CalDAV/Calendar.php | 451 +++ .../sabre/dav/lib/CalDAV/CalendarHome.php | 356 +++ .../sabre/dav/lib/CalDAV/CalendarObject.php | 223 ++ .../dav/lib/CalDAV/CalendarQueryValidator.php | 340 ++ .../sabre/dav/lib/CalDAV/CalendarRoot.php | 75 + .../CalDAV/Exception/InvalidComponentType.php | 31 + .../sabre/dav/lib/CalDAV/ICSExportPlugin.php | 377 +++ .../vendor/sabre/dav/lib/CalDAV/ICalendar.php | 20 + .../sabre/dav/lib/CalDAV/ICalendarObject.php | 23 + .../lib/CalDAV/ICalendarObjectContainer.php | 39 + .../sabre/dav/lib/CalDAV/ISharedCalendar.php | 27 + .../lib/CalDAV/Notifications/Collection.php | 96 + .../lib/CalDAV/Notifications/ICollection.php | 25 + .../dav/lib/CalDAV/Notifications/INode.php | 41 + .../dav/lib/CalDAV/Notifications/Node.php | 112 + .../dav/lib/CalDAV/Notifications/Plugin.php | 161 + .../vendor/sabre/dav/lib/CalDAV/Plugin.php | 1011 ++++++ .../dav/lib/CalDAV/Principal/Collection.php | 32 + .../dav/lib/CalDAV/Principal/IProxyRead.php | 21 + .../dav/lib/CalDAV/Principal/IProxyWrite.php | 21 + .../dav/lib/CalDAV/Principal/ProxyRead.php | 161 + .../dav/lib/CalDAV/Principal/ProxyWrite.php | 161 + .../sabre/dav/lib/CalDAV/Principal/User.php | 136 + .../sabre/dav/lib/CalDAV/Schedule/IInbox.php | 17 + .../dav/lib/CalDAV/Schedule/IMipPlugin.php | 185 ++ .../sabre/dav/lib/CalDAV/Schedule/IOutbox.php | 17 + .../lib/CalDAV/Schedule/ISchedulingObject.php | 15 + .../sabre/dav/lib/CalDAV/Schedule/Inbox.php | 198 ++ .../sabre/dav/lib/CalDAV/Schedule/Outbox.php | 119 + .../sabre/dav/lib/CalDAV/Schedule/Plugin.php | 999 ++++++ .../lib/CalDAV/Schedule/SchedulingObject.php | 130 + .../sabre/dav/lib/CalDAV/SharedCalendar.php | 219 ++ .../sabre/dav/lib/CalDAV/SharingPlugin.php | 350 ++ .../CalDAV/Subscriptions/ISubscription.php | 41 + .../dav/lib/CalDAV/Subscriptions/Plugin.php | 108 + .../lib/CalDAV/Subscriptions/Subscription.php | 204 ++ .../lib/CalDAV/Xml/Filter/CalendarData.php | 81 + .../dav/lib/CalDAV/Xml/Filter/CompFilter.php | 94 + .../dav/lib/CalDAV/Xml/Filter/ParamFilter.php | 79 + .../dav/lib/CalDAV/Xml/Filter/PropFilter.php | 95 + .../lib/CalDAV/Xml/Notification/Invite.php | 290 ++ .../CalDAV/Xml/Notification/InviteReply.php | 199 ++ .../Notification/NotificationInterface.php | 43 + .../CalDAV/Xml/Notification/SystemStatus.php | 171 + .../Xml/Property/AllowedSharingModes.php | 81 + .../CalDAV/Xml/Property/EmailAddressSet.php | 71 + .../dav/lib/CalDAV/Xml/Property/Invite.php | 120 + .../Xml/Property/ScheduleCalendarTransp.php | 124 + .../SupportedCalendarComponentSet.php | 118 + .../Xml/Property/SupportedCalendarData.php | 57 + .../Xml/Property/SupportedCollationSet.php | 54 + .../Xml/Request/CalendarMultiGetReport.php | 119 + .../Xml/Request/CalendarQueryReport.php | 137 + .../Xml/Request/FreeBusyQueryReport.php | 90 + .../lib/CalDAV/Xml/Request/InviteReply.php | 145 + .../dav/lib/CalDAV/Xml/Request/MkCalendar.php | 77 + .../dav/lib/CalDAV/Xml/Request/Share.php | 107 + .../sabre/dav/lib/CardDAV/AddressBook.php | 335 ++ .../sabre/dav/lib/CardDAV/AddressBookHome.php | 178 ++ .../sabre/dav/lib/CardDAV/AddressBookRoot.php | 75 + .../lib/CardDAV/Backend/AbstractBackend.php | 38 + .../lib/CardDAV/Backend/BackendInterface.php | 194 ++ .../sabre/dav/lib/CardDAV/Backend/PDO.php | 538 ++++ .../dav/lib/CardDAV/Backend/SyncSupport.php | 83 + .../vendor/sabre/dav/lib/CardDAV/Card.php | 202 ++ .../sabre/dav/lib/CardDAV/IAddressBook.php | 20 + .../vendor/sabre/dav/lib/CardDAV/ICard.php | 21 + .../sabre/dav/lib/CardDAV/IDirectory.php | 22 + .../vendor/sabre/dav/lib/CardDAV/Plugin.php | 879 ++++++ .../sabre/dav/lib/CardDAV/VCFExportPlugin.php | 165 + .../lib/CardDAV/Xml/Filter/AddressData.php | 66 + .../lib/CardDAV/Xml/Filter/ParamFilter.php | 86 + .../dav/lib/CardDAV/Xml/Filter/PropFilter.php | 95 + .../Xml/Property/SupportedAddressData.php | 77 + .../Xml/Property/SupportedCollationSet.php | 44 + .../Xml/Request/AddressBookMultiGetReport.php | 109 + .../Xml/Request/AddressBookQueryReport.php | 194 ++ .../lib/DAV/Auth/Backend/AbstractBasic.php | 136 + .../lib/DAV/Auth/Backend/AbstractBearer.php | 130 + .../lib/DAV/Auth/Backend/AbstractDigest.php | 160 + .../sabre/dav/lib/DAV/Auth/Backend/Apache.php | 93 + .../lib/DAV/Auth/Backend/BackendInterface.php | 65 + .../lib/DAV/Auth/Backend/BasicCallBack.php | 56 + .../sabre/dav/lib/DAV/Auth/Backend/File.php | 74 + .../sabre/dav/lib/DAV/Auth/Backend/IMAP.php | 82 + .../sabre/dav/lib/DAV/Auth/Backend/PDO.php | 55 + .../vendor/sabre/dav/lib/DAV/Auth/Plugin.php | 259 ++ .../dav/lib/DAV/Browser/GuessContentType.php | 93 + .../sabre/dav/lib/DAV/Browser/HtmlOutput.php | 34 + .../dav/lib/DAV/Browser/HtmlOutputHelper.php | 118 + .../dav/lib/DAV/Browser/MapGetToPropFind.php | 58 + .../sabre/dav/lib/DAV/Browser/Plugin.php | 789 +++++ .../sabre/dav/lib/DAV/Browser/PropFindAll.php | 128 + .../dav/lib/DAV/Browser/assets/favicon.ico | Bin 0 -> 4286 bytes .../Browser/assets/openiconic/ICON-LICENSE | 21 + .../Browser/assets/openiconic/open-iconic.css | 510 +++ .../Browser/assets/openiconic/open-iconic.eot | Bin 0 -> 23144 bytes .../Browser/assets/openiconic/open-iconic.otf | Bin 0 -> 21048 bytes .../Browser/assets/openiconic/open-iconic.svg | 543 ++++ .../Browser/assets/openiconic/open-iconic.ttf | Bin 0 -> 25568 bytes .../assets/openiconic/open-iconic.woff | Bin 0 -> 12404 bytes .../dav/lib/DAV/Browser/assets/sabredav.css | 228 ++ .../dav/lib/DAV/Browser/assets/sabredav.png | Bin 0 -> 2825 bytes .../vendor/sabre/dav/lib/DAV/Client.php | 429 +++ .../vendor/sabre/dav/lib/DAV/Collection.php | 106 + .../vendor/sabre/dav/lib/DAV/CorePlugin.php | 902 ++++++ .../vendor/sabre/dav/lib/DAV/Exception.php | 50 + .../dav/lib/DAV/Exception/BadRequest.php | 30 + .../sabre/dav/lib/DAV/Exception/Conflict.php | 30 + .../dav/lib/DAV/Exception/ConflictingLock.php | 32 + .../sabre/dav/lib/DAV/Exception/Forbidden.php | 29 + .../lib/DAV/Exception/InsufficientStorage.php | 29 + .../lib/DAV/Exception/InvalidResourceType.php | 29 + .../lib/DAV/Exception/InvalidSyncToken.php | 34 + .../dav/lib/DAV/Exception/LengthRequired.php | 30 + .../Exception/LockTokenMatchesRequestUri.php | 36 + .../sabre/dav/lib/DAV/Exception/Locked.php | 68 + .../lib/DAV/Exception/MethodNotAllowed.php | 45 + .../lib/DAV/Exception/NotAuthenticated.php | 30 + .../sabre/dav/lib/DAV/Exception/NotFound.php | 29 + .../dav/lib/DAV/Exception/NotImplemented.php | 29 + .../dav/lib/DAV/Exception/PaymentRequired.php | 30 + .../lib/DAV/Exception/PreconditionFailed.php | 65 + .../lib/DAV/Exception/ReportNotSupported.php | 28 + .../RequestedRangeNotSatisfiable.php | 30 + .../lib/DAV/Exception/ServiceUnavailable.php | 30 + .../dav/lib/DAV/Exception/TooManyMatches.php | 34 + .../DAV/Exception/UnsupportedMediaType.php | 30 + .../vendor/sabre/dav/lib/DAV/FS/Directory.php | 147 + .../vendor/sabre/dav/lib/DAV/FS/File.php | 87 + .../vendor/sabre/dav/lib/DAV/FS/Node.php | 96 + .../sabre/dav/lib/DAV/FSExt/Directory.php | 212 ++ .../vendor/sabre/dav/lib/DAV/FSExt/File.php | 150 + .../backup/vendor/sabre/dav/lib/DAV/File.php | 93 + .../vendor/sabre/dav/lib/DAV/ICollection.php | 79 + .../vendor/sabre/dav/lib/DAV/ICopyTarget.php | 38 + .../sabre/dav/lib/DAV/IExtendedCollection.php | 43 + .../backup/vendor/sabre/dav/lib/DAV/IFile.php | 83 + .../vendor/sabre/dav/lib/DAV/IMoveTarget.php | 46 + .../vendor/sabre/dav/lib/DAV/IMultiGet.php | 38 + .../backup/vendor/sabre/dav/lib/DAV/INode.php | 44 + .../vendor/sabre/dav/lib/DAV/IProperties.php | 46 + .../vendor/sabre/dav/lib/DAV/IQuota.php | 27 + .../lib/DAV/Locks/Backend/AbstractBackend.php | 20 + .../DAV/Locks/Backend/BackendInterface.php | 52 + .../sabre/dav/lib/DAV/Locks/Backend/File.php | 182 ++ .../sabre/dav/lib/DAV/Locks/Backend/PDO.php | 172 + .../sabre/dav/lib/DAV/Locks/LockInfo.php | 82 + .../vendor/sabre/dav/lib/DAV/Locks/Plugin.php | 546 ++++ .../backup/vendor/sabre/dav/lib/DAV/MkCol.php | 71 + .../vendor/sabre/dav/lib/DAV/Mount/Plugin.php | 78 + .../backup/vendor/sabre/dav/lib/DAV/Node.php | 51 + .../lib/DAV/PartialUpdate/IPatchSupport.php | 49 + .../dav/lib/DAV/PartialUpdate/Plugin.php | 212 ++ .../vendor/sabre/dav/lib/DAV/PropFind.php | 335 ++ .../vendor/sabre/dav/lib/DAV/PropPatch.php | 337 ++ .../Backend/BackendInterface.php | 75 + .../lib/DAV/PropertyStorage/Backend/PDO.php | 224 ++ .../dav/lib/DAV/PropertyStorage/Plugin.php | 176 ++ .../vendor/sabre/dav/lib/DAV/Server.php | 1672 ++++++++++ .../vendor/sabre/dav/lib/DAV/ServerPlugin.php | 105 + .../sabre/dav/lib/DAV/Sharing/ISharedNode.php | 69 + .../sabre/dav/lib/DAV/Sharing/Plugin.php | 313 ++ .../sabre/dav/lib/DAV/SimpleCollection.php | 109 + .../vendor/sabre/dav/lib/DAV/SimpleFile.php | 118 + .../vendor/sabre/dav/lib/DAV/StringUtil.php | 88 + .../dav/lib/DAV/Sync/ISyncCollection.php | 90 + .../vendor/sabre/dav/lib/DAV/Sync/Plugin.php | 240 ++ .../dav/lib/DAV/TemporaryFileFilterPlugin.php | 298 ++ .../backup/vendor/sabre/dav/lib/DAV/Tree.php | 324 ++ .../vendor/sabre/dav/lib/DAV/UUIDUtil.php | 66 + .../vendor/sabre/dav/lib/DAV/Version.php | 20 + .../sabre/dav/lib/DAV/Xml/Element/Prop.php | 110 + .../dav/lib/DAV/Xml/Element/Response.php | 237 ++ .../sabre/dav/lib/DAV/Xml/Element/Sharee.php | 189 ++ .../dav/lib/DAV/Xml/Property/Complex.php | 87 + .../lib/DAV/Xml/Property/GetLastModified.php | 104 + .../sabre/dav/lib/DAV/Xml/Property/Href.php | 156 + .../sabre/dav/lib/DAV/Xml/Property/Invite.php | 66 + .../dav/lib/DAV/Xml/Property/LocalHref.php | 48 + .../lib/DAV/Xml/Property/LockDiscovery.php | 105 + .../dav/lib/DAV/Xml/Property/ResourceType.php | 121 + .../dav/lib/DAV/Xml/Property/ShareAccess.php | 135 + .../lib/DAV/Xml/Property/SupportedLock.php | 52 + .../DAV/Xml/Property/SupportedMethodSet.php | 114 + .../DAV/Xml/Property/SupportedReportSet.php | 144 + .../sabre/dav/lib/DAV/Xml/Request/Lock.php | 84 + .../sabre/dav/lib/DAV/Xml/Request/MkCol.php | 80 + .../dav/lib/DAV/Xml/Request/PropFind.php | 79 + .../dav/lib/DAV/Xml/Request/PropPatch.php | 109 + .../dav/lib/DAV/Xml/Request/ShareResource.php | 80 + .../DAV/Xml/Request/SyncCollectionReport.php | 118 + .../dav/lib/DAV/Xml/Response/MultiStatus.php | 136 + .../vendor/sabre/dav/lib/DAV/Xml/Service.php | 47 + .../vendor/sabre/dav/lib/DAVACL/ACLTrait.php | 94 + .../DAVACL/AbstractPrincipalCollection.php | 178 ++ .../dav/lib/DAVACL/Exception/AceConflict.php | 31 + .../lib/DAVACL/Exception/NeedPrivileges.php | 73 + .../dav/lib/DAVACL/Exception/NoAbstract.php | 31 + .../Exception/NotRecognizedPrincipal.php | 31 + .../Exception/NotSupportedPrivilege.php | 31 + .../sabre/dav/lib/DAVACL/FS/Collection.php | 109 + .../vendor/sabre/dav/lib/DAVACL/FS/File.php | 78 + .../dav/lib/DAVACL/FS/HomeCollection.php | 123 + .../vendor/sabre/dav/lib/DAVACL/IACL.php | 72 + .../sabre/dav/lib/DAVACL/IPrincipal.php | 75 + .../dav/lib/DAVACL/IPrincipalCollection.php | 64 + .../vendor/sabre/dav/lib/DAVACL/Plugin.php | 1549 +++++++++ .../vendor/sabre/dav/lib/DAVACL/Principal.php | 199 ++ .../PrincipalBackend/AbstractBackend.php | 54 + .../PrincipalBackend/BackendInterface.php | 143 + .../CreatePrincipalSupport.php | 29 + .../dav/lib/DAVACL/PrincipalBackend/PDO.php | 443 +++ .../dav/lib/DAVACL/PrincipalCollection.php | 96 + .../sabre/dav/lib/DAVACL/Xml/Property/Acl.php | 257 ++ .../DAVACL/Xml/Property/AclRestrictions.php | 42 + .../Xml/Property/CurrentUserPrivilegeSet.php | 145 + .../dav/lib/DAVACL/Xml/Property/Principal.php | 184 ++ .../Xml/Property/SupportedPrivilegeSet.php | 146 + .../Xml/Request/AclPrincipalPropSetReport.php | 66 + .../Xml/Request/ExpandPropertyReport.php | 100 + .../Xml/Request/PrincipalMatchReport.php | 106 + .../Request/PrincipalPropertySearchReport.php | 122 + .../PrincipalSearchPropertySetReport.php | 58 + .../backup/vendor/sabre/dav/phpstan.neon | 2 + .../Sabre/CalDAV/Backend/AbstractPDOTest.php | 1397 ++++++++ .../Sabre/CalDAV/Backend/AbstractTest.php | 184 ++ .../dav/tests/Sabre/CalDAV/Backend/Mock.php | 247 ++ .../Sabre/CalDAV/Backend/MockScheduling.php | 89 + .../Sabre/CalDAV/Backend/MockSharing.php | 195 ++ .../Backend/MockSubscriptionSupport.php | 153 + .../Sabre/CalDAV/Backend/PDOMySQLTest.php | 10 + .../Sabre/CalDAV/Backend/PDOPgSqlTest.php | 10 + .../Sabre/CalDAV/Backend/PDOSqliteTest.php | 10 + .../Sabre/CalDAV/Backend/SimplePDOTest.php | 424 +++ .../CalDAV/CalendarHomeNotificationsTest.php | 44 + .../CalendarHomeSharedCalendarsTest.php | 75 + .../CalDAV/CalendarHomeSubscriptionsTest.php | 79 + .../tests/Sabre/CalDAV/CalendarHomeTest.php | 193 ++ .../tests/Sabre/CalDAV/CalendarObjectTest.php | 351 +++ .../Sabre/CalDAV/CalendarQueryVAlarmTest.php | 121 + .../CalDAV/CalendarQueryValidatorTest.php | 823 +++++ .../dav/tests/Sabre/CalDAV/CalendarTest.php | 229 ++ .../ExpandEventsDTSTARTandDTENDTest.php | 114 + .../ExpandEventsDTSTARTandDTENDbyDayTest.php | 104 + .../CalDAV/ExpandEventsDoubleEventsTest.php | 104 + .../CalDAV/ExpandEventsFloatingTimeTest.php | 210 ++ .../tests/Sabre/CalDAV/FreeBusyReportTest.php | 158 + .../Sabre/CalDAV/GetEventsByTimerangeTest.php | 82 + .../Sabre/CalDAV/ICSExportPluginTest.php | 366 +++ .../dav/tests/Sabre/CalDAV/Issue166Test.php | 63 + .../dav/tests/Sabre/CalDAV/Issue172Test.php | 140 + .../dav/tests/Sabre/CalDAV/Issue203Test.php | 138 + .../dav/tests/Sabre/CalDAV/Issue205Test.php | 99 + .../dav/tests/Sabre/CalDAV/Issue211Test.php | 90 + .../dav/tests/Sabre/CalDAV/Issue220Test.php | 101 + .../dav/tests/Sabre/CalDAV/Issue228Test.php | 80 + .../tests/Sabre/CalDAV/JCalTransformTest.php | 254 ++ .../CalDAV/Notifications/CollectionTest.php | 78 + .../Sabre/CalDAV/Notifications/NodeTest.php | 88 + .../Sabre/CalDAV/Notifications/PluginTest.php | 161 + .../dav/tests/Sabre/CalDAV/PluginTest.php | 1071 +++++++ .../Sabre/CalDAV/Principal/CollectionTest.php | 20 + .../Sabre/CalDAV/Principal/ProxyReadTest.php | 91 + .../Sabre/CalDAV/Principal/ProxyWriteTest.php | 39 + .../tests/Sabre/CalDAV/Principal/UserTest.php | 111 + .../CalDAV/Schedule/DeliverNewEventTest.php | 89 + .../CalDAV/Schedule/FreeBusyRequestTest.php | 534 ++++ .../Sabre/CalDAV/Schedule/IMip/MockPlugin.php | 47 + .../Sabre/CalDAV/Schedule/IMipPluginTest.php | 239 ++ .../tests/Sabre/CalDAV/Schedule/InboxTest.php | 133 + .../Sabre/CalDAV/Schedule/OutboxPostTest.php | 128 + .../Sabre/CalDAV/Schedule/OutboxTest.php | 47 + .../Sabre/CalDAV/Schedule/PluginBasicTest.php | 37 + .../CalDAV/Schedule/PluginPropertiesTest.php | 143 + ...PluginPropertiesWithSharedCalendarTest.php | 69 + .../CalDAV/Schedule/ScheduleDeliverTest.php | 644 ++++ .../CalDAV/Schedule/SchedulingObjectTest.php | 356 +++ .../tests/Sabre/CalDAV/SharedCalendarTest.php | 172 + .../tests/Sabre/CalDAV/SharingPluginTest.php | 383 +++ .../Subscriptions/CreateSubscriptionTest.php | 119 + .../Sabre/CalDAV/Subscriptions/PluginTest.php | 49 + .../CalDAV/Subscriptions/SubscriptionTest.php | 121 + .../sabre/dav/tests/Sabre/CalDAV/TestUtil.php | 102 + .../tests/Sabre/CalDAV/ValidateICalTest.php | 392 +++ .../Xml/Notification/InviteReplyTest.php | 138 + .../CalDAV/Xml/Notification/InviteTest.php | 157 + .../Xml/Notification/SystemStatusTest.php | 66 + .../Xml/Property/AllowedSharingModesTest.php | 37 + .../Xml/Property/EmailAddressSetTest.php | 39 + .../Sabre/CalDAV/Xml/Property/InviteTest.php | 109 + .../Property/ScheduleCalendarTranspTest.php | 110 + .../SupportedCalendarComponentSetTest.php | 95 + .../Property/SupportedCalendarDataTest.php | 35 + .../Property/SupportedCollationSetTest.php | 36 + .../Xml/Request/CalendarQueryReportTest.php | 348 ++ .../CalDAV/Xml/Request/InviteReplyTest.php | 75 + .../Sabre/CalDAV/Xml/Request/ShareTest.php | 82 + .../Sabre/CardDAV/AbstractPluginTest.php | 43 + .../Sabre/CardDAV/AddressBookHomeTest.php | 131 + .../Sabre/CardDAV/AddressBookQueryTest.php | 351 +++ .../Sabre/CardDAV/AddressBookRootTest.php | 31 + .../tests/Sabre/CardDAV/AddressBookTest.php | 171 + .../Sabre/CardDAV/Backend/AbstractPDOTest.php | 351 +++ .../dav/tests/Sabre/CardDAV/Backend/Mock.php | 257 ++ .../Sabre/CardDAV/Backend/PDOMySQLTest.php | 10 + .../Sabre/CardDAV/Backend/PDOPgSqlTest.php | 10 + .../Sabre/CardDAV/Backend/PDOSqliteTest.php | 10 + .../dav/tests/Sabre/CardDAV/CardTest.php | 194 ++ .../tests/Sabre/CardDAV/IDirectoryTest.php | 28 + .../dav/tests/Sabre/CardDAV/MultiGetTest.php | 99 + .../dav/tests/Sabre/CardDAV/PluginTest.php | 101 + .../CardDAV/SogoStripContentTypeTest.php | 67 + .../dav/tests/Sabre/CardDAV/VCFExportTest.php | 130 + .../Sabre/CardDAV/ValidateFilterTest.php | 204 ++ .../tests/Sabre/CardDAV/ValidateVCardTest.php | 293 ++ .../Xml/Property/SupportedAddressDataTest.php | 37 + .../Property/SupportedCollationSetTest.php | 37 + .../Xml/Request/AddressBookMultiGetTest.php | 89 + .../Request/AddressBookQueryReportTest.php | 333 ++ .../dav/tests/Sabre/DAV/AbstractServer.php | 62 + .../DAV/Auth/Backend/AbstractBasicTest.php | 90 + .../DAV/Auth/Backend/AbstractBearerTest.php | 83 + .../DAV/Auth/Backend/AbstractDigestTest.php | 134 + .../DAV/Auth/Backend/AbstractPDOTest.php | 42 + .../Sabre/DAV/Auth/Backend/ApacheTest.php | 72 + .../DAV/Auth/Backend/BasicCallBackTest.php | 34 + .../tests/Sabre/DAV/Auth/Backend/FileTest.php | 38 + .../tests/Sabre/DAV/Auth/Backend/IMAPTest.php | 52 + .../dav/tests/Sabre/DAV/Auth/Backend/Mock.php | 81 + .../Sabre/DAV/Auth/Backend/PDOMySQLTest.php | 10 + .../Sabre/DAV/Auth/Backend/PDOPgSqlTest.php | 10 + .../Sabre/DAV/Auth/Backend/PDOSqliteTest.php | 10 + .../dav/tests/Sabre/DAV/Auth/PluginTest.php | 127 + .../dav/tests/Sabre/DAV/BasicNodeTest.php | 124 + .../DAV/Browser/GuessContentTypeTest.php | 67 + .../DAV/Browser/MapGetToPropFindTest.php | 40 + .../tests/Sabre/DAV/Browser/PluginTest.php | 176 ++ .../Sabre/DAV/Browser/PropFindAllTest.php | 64 + .../sabre/dav/tests/Sabre/DAV/ClientMock.php | 36 + .../sabre/dav/tests/Sabre/DAV/ClientTest.php | 285 ++ .../dav/tests/Sabre/DAV/CorePluginTest.php | 14 + .../dav/tests/Sabre/DAV/DbTestHelperTrait.php | 126 + .../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 | 27 + .../sabre/dav/tests/Sabre/DAV/FS/NodeTest.php | 30 + .../tests/Sabre/DAV/FSExt/DirectoryTest.php | 26 + .../dav/tests/Sabre/DAV/FSExt/FileTest.php | 99 + .../dav/tests/Sabre/DAV/FSExt/ServerTest.php | 252 ++ .../tests/Sabre/DAV/GetIfConditionsTest.php | 289 ++ .../tests/Sabre/DAV/HTTPPreferParsingTest.php | 175 + .../dav/tests/Sabre/DAV/HttpCopyTest.php | 180 ++ .../dav/tests/Sabre/DAV/HttpDeleteTest.php | 131 + .../sabre/dav/tests/Sabre/DAV/HttpGetTest.php | 150 + .../dav/tests/Sabre/DAV/HttpHeadTest.php | 93 + .../dav/tests/Sabre/DAV/HttpMoveTest.php | 110 + .../sabre/dav/tests/Sabre/DAV/HttpPutTest.php | 336 ++ .../sabre/dav/tests/Sabre/DAV/Issue33Test.php | 93 + .../Sabre/DAV/Locks/Backend/AbstractTest.php | 189 ++ .../Sabre/DAV/Locks/Backend/FileTest.php | 21 + .../tests/Sabre/DAV/Locks/Backend/Mock.php | 132 + .../Sabre/DAV/Locks/Backend/PDOMySQLTest.php | 10 + .../Sabre/DAV/Locks/Backend/PDOPgSqlTest.php | 10 + .../Sabre/DAV/Locks/Backend/PDOSqliteTest.php | 10 + .../tests/Sabre/DAV/Locks/Backend/PDOTest.php | 20 + .../dav/tests/Sabre/DAV/Locks/MSWordTest.php | 119 + .../dav/tests/Sabre/DAV/Locks/Plugin2Test.php | 68 + .../dav/tests/Sabre/DAV/Locks/PluginTest.php | 860 +++++ .../dav/tests/Sabre/DAV/Mock/Collection.php | 157 + .../sabre/dav/tests/Sabre/DAV/Mock/File.php | 151 + .../Sabre/DAV/Mock/PropertiesCollection.php | 91 + .../dav/tests/Sabre/DAV/Mock/SharedNode.php | 113 + .../tests/Sabre/DAV/Mock/StreamingFile.php | 96 + .../sabre/dav/tests/Sabre/DAV/MockLogger.php | 35 + .../dav/tests/Sabre/DAV/Mount/PluginTest.php | 54 + .../dav/tests/Sabre/DAV/ObjectTreeTest.php | 90 + .../sabre/dav/tests/Sabre/DAV/PSR3Test.php | 84 + .../Sabre/DAV/PartialUpdate/FileMock.php | 111 + .../Sabre/DAV/PartialUpdate/PluginTest.php | 122 + .../DAV/PartialUpdate/SpecificationTest.php | 90 + .../dav/tests/Sabre/DAV/PropFindTest.php | 72 + .../dav/tests/Sabre/DAV/PropPatchTest.php | 338 ++ .../Backend/AbstractPDOTest.php | 185 ++ .../DAV/PropertyStorage/Backend/Mock.php | 103 + .../PropertyStorage/Backend/PDOMysqlTest.php | 10 + .../PropertyStorage/Backend/PDOPgSqlTest.php | 10 + .../PropertyStorage/Backend/PDOSqliteTest.php | 10 + .../Sabre/DAV/PropertyStorage/PluginTest.php | 109 + .../dav/tests/Sabre/DAV/ServerEventsTest.php | 114 + .../dav/tests/Sabre/DAV/ServerMKCOLTest.php | 354 +++ .../dav/tests/Sabre/DAV/ServerPluginTest.php | 96 + .../Sabre/DAV/ServerPreconditionsTest.php | 269 ++ .../DAV/ServerPropsInfiniteDepthTest.php | 213 ++ .../dav/tests/Sabre/DAV/ServerPropsTest.php | 194 ++ .../dav/tests/Sabre/DAV/ServerRangeTest.php | 252 ++ .../dav/tests/Sabre/DAV/ServerSimpleTest.php | 433 +++ .../Sabre/DAV/ServerUpdatePropertiesTest.php | 97 + .../tests/Sabre/DAV/Sharing/PluginTest.php | 180 ++ .../Sabre/DAV/Sharing/ShareResourceTest.php | 203 ++ .../tests/Sabre/DAV/SimpleCollectionTest.php | 49 + .../dav/tests/Sabre/DAV/SimpleFileTest.php | 19 + .../dav/tests/Sabre/DAV/StringUtilTest.php | 119 + .../Sabre/DAV/Sync/MockSyncCollection.php | 168 + .../dav/tests/Sabre/DAV/Sync/PluginTest.php | 507 +++ .../tests/Sabre/DAV/SyncTokenPropertyTest.php | 103 + .../Sabre/DAV/TemporaryFileFilterTest.php | 204 ++ .../sabre/dav/tests/Sabre/DAV/TestPlugin.php | 35 + .../sabre/dav/tests/Sabre/DAV/TreeTest.php | 238 ++ .../dav/tests/Sabre/DAV/UUIDUtilTest.php | 24 + .../tests/Sabre/DAV/Xml/Element/PropTest.php | 148 + .../Sabre/DAV/Xml/Element/ResponseTest.php | 304 ++ .../Sabre/DAV/Xml/Element/ShareeTest.php | 89 + .../tests/Sabre/DAV/Xml/Property/HrefTest.php | 103 + .../Sabre/DAV/Xml/Property/InviteTest.php | 76 + .../DAV/Xml/Property/LastModifiedTest.php | 57 + .../Sabre/DAV/Xml/Property/LocalHrefTest.php | 67 + .../DAV/Xml/Property/LockDiscoveryTest.php | 167 + .../DAV/Xml/Property/ShareAccessTest.php | 116 + .../Xml/Property/SupportedMethodSetTest.php | 43 + .../Xml/Property/SupportedReportSetTest.php | 110 + .../Sabre/DAV/Xml/Request/PropFindTest.php | 44 + .../Sabre/DAV/Xml/Request/PropPatchTest.php | 53 + .../DAV/Xml/Request/ShareResourceTest.php | 74 + .../DAV/Xml/Request/SyncCollectionTest.php | 87 + .../dav/tests/Sabre/DAV/Xml/ServiceTest.php | 18 + .../sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php | 48 + .../dav/tests/Sabre/DAVACL/ACLMethodTest.php | 311 ++ .../DAVACL/AclPrincipalPropSetReportTest.php | 68 + .../tests/Sabre/DAVACL/AllowAccessTest.php | 120 + .../tests/Sabre/DAVACL/BlockAccessTest.php | 180 ++ .../DAVACL/Exception/AceConflictTest.php | 37 + .../Exception/NeedPrivilegesExceptionTest.php | 47 + .../Sabre/DAVACL/Exception/NoAbstractTest.php | 37 + .../Exception/NotRecognizedPrincipalTest.php | 37 + .../Exception/NotSupportedPrivilegeTest.php | 37 + .../Sabre/DAVACL/ExpandPropertiesTest.php | 308 ++ .../tests/Sabre/DAVACL/FS/CollectionTest.php | 41 + .../dav/tests/Sabre/DAVACL/FS/FileTest.php | 66 + .../Sabre/DAVACL/FS/HomeCollectionTest.php | 105 + .../dav/tests/Sabre/DAVACL/MockACLNode.php | 49 + .../dav/tests/Sabre/DAVACL/MockPrincipal.php | 58 + .../tests/Sabre/DAVACL/PluginAdminTest.php | 76 + .../Sabre/DAVACL/PluginPropertiesTest.php | 399 +++ .../DAVACL/PluginUpdatePropertiesTest.php | 111 + .../PrincipalBackend/AbstractPDOTest.php | 219 ++ .../Sabre/DAVACL/PrincipalBackend/Mock.php | 158 + .../DAVACL/PrincipalBackend/PDOMySQLTest.php | 10 + .../DAVACL/PrincipalBackend/PDOPgSqlTest.php | 10 + .../DAVACL/PrincipalBackend/PDOSqliteTest.php | 10 + .../Sabre/DAVACL/PrincipalCollectionTest.php | 55 + .../tests/Sabre/DAVACL/PrincipalMatchTest.php | 121 + .../DAVACL/PrincipalPropertySearchTest.php | 389 +++ .../DAVACL/PrincipalSearchPropertySetTest.php | 135 + .../dav/tests/Sabre/DAVACL/PrincipalTest.php | 192 ++ .../tests/Sabre/DAVACL/SimplePluginTest.php | 302 ++ .../Sabre/DAVACL/Xml/Property/ACLTest.php | 326 ++ .../Xml/Property/AclRestrictionsTest.php | 27 + .../Property/CurrentUserPrivilegeSetTest.php | 82 + .../DAVACL/Xml/Property/PrincipalTest.php | 175 + .../Property/SupportedPrivilegeSetTest.php | 99 + .../Request/AclPrincipalPropSetReportTest.php | 28 + .../Xml/Request/PrincipalMatchReportTest.php | 48 + .../sabre/dav/tests/Sabre/DAVServerTest.php | 305 ++ .../dav/tests/Sabre/HTTP/ResponseMock.php | 23 + .../sabre/dav/tests/Sabre/HTTP/SapiMock.php | 27 + .../vendor/sabre/dav/tests/Sabre/TestUtil.php | 66 + .../vendor/sabre/dav/tests/bootstrap.php | 65 + .../backup/vendor/sabre/dav/tests/phpunit.xml | 48 + .../backup/vendor/sabre/event/LICENSE | 27 + .../backup/vendor/sabre/event/composer.json | 50 + .../backup/vendor/sabre/event/lib/Emitter.php | 19 + .../sabre/event/lib/EmitterInterface.php | 78 + .../vendor/sabre/event/lib/EmitterTrait.php | 178 ++ .../vendor/sabre/event/lib/EventEmitter.php | 20 + .../vendor/sabre/event/lib/Loop/Loop.php | 341 ++ .../vendor/sabre/event/lib/Loop/functions.php | 143 + .../backup/vendor/sabre/event/lib/Promise.php | 258 ++ .../sabre/event/lib/Promise/functions.php | 128 + .../lib/PromiseAlreadyResolvedException.php | 17 + .../backup/vendor/sabre/event/lib/Version.php | 20 + .../sabre/event/lib/WildcardEmitter.php | 36 + .../sabre/event/lib/WildcardEmitterTrait.php | 233 ++ .../vendor/sabre/event/lib/coroutine.php | 119 + .../backup/vendor/sabre/event/phpstan.neon | 2 + .../backup/vendor/sabre/http/CHANGELOG.md | 333 ++ .../panakour/backup/vendor/sabre/http/LICENSE | 27 + .../backup/vendor/sabre/http/README.md | 747 +++++ .../backup/vendor/sabre/http/composer.json | 50 + .../sabre/http/examples/asyncclient.php | 62 + .../vendor/sabre/http/examples/basicauth.php | 50 + .../vendor/sabre/http/examples/client.php | 37 + .../vendor/sabre/http/examples/digestauth.php | 51 + .../sabre/http/examples/reverseproxy.php | 48 + .../vendor/sabre/http/examples/stringify.php | 50 + .../backup/vendor/sabre/http/lib/Auth/AWS.php | 220 ++ .../sabre/http/lib/Auth/AbstractAuth.php | 65 + .../vendor/sabre/http/lib/Auth/Basic.php | 60 + .../vendor/sabre/http/lib/Auth/Bearer.php | 53 + .../vendor/sabre/http/lib/Auth/Digest.php | 210 ++ .../backup/vendor/sabre/http/lib/Client.php | 614 ++++ .../vendor/sabre/http/lib/ClientException.php | 17 + .../sabre/http/lib/ClientHttpException.php | 50 + .../vendor/sabre/http/lib/HttpException.php | 31 + .../backup/vendor/sabre/http/lib/Message.php | 291 ++ .../sabre/http/lib/MessageDecoratorTrait.php | 206 ++ .../sabre/http/lib/MessageInterface.php | 151 + .../backup/vendor/sabre/http/lib/Request.php | 267 ++ .../sabre/http/lib/RequestDecorator.php | 179 ++ .../sabre/http/lib/RequestInterface.php | 114 + .../backup/vendor/sabre/http/lib/Response.php | 188 ++ .../sabre/http/lib/ResponseDecorator.php | 72 + .../sabre/http/lib/ResponseInterface.php | 42 + .../backup/vendor/sabre/http/lib/Sapi.php | 243 ++ .../backup/vendor/sabre/http/lib/Version.php | 20 + .../vendor/sabre/http/lib/functions.php | 415 +++ .../backup/vendor/sabre/http/phpstan.neon | 2 + .../sabre/http/tests/HTTP/Auth/AWSTest.php | 228 ++ .../sabre/http/tests/HTTP/Auth/BasicTest.php | 67 + .../sabre/http/tests/HTTP/Auth/BearerTest.php | 55 + .../sabre/http/tests/HTTP/Auth/DigestTest.php | 182 ++ .../sabre/http/tests/HTTP/ClientTest.php | 564 ++++ .../sabre/http/tests/HTTP/FunctionsTest.php | 177 ++ .../http/tests/HTTP/MessageDecoratorTest.php | 91 + .../sabre/http/tests/HTTP/MessageTest.php | 281 ++ .../sabre/http/tests/HTTP/NegotiateTest.php | 135 + .../http/tests/HTTP/RequestDecoratorTest.php | 103 + .../sabre/http/tests/HTTP/RequestTest.php | 137 + .../http/tests/HTTP/ResponseDecoratorTest.php | 35 + .../sabre/http/tests/HTTP/ResponseTest.php | 41 + .../vendor/sabre/http/tests/HTTP/SapiTest.php | 262 ++ .../sabre/http/tests/HTTP/URLUtilTest.php | 95 + .../vendor/sabre/http/tests/bootstrap.php | 10 + .../vendor/sabre/http/tests/phpcs/ruleset.xml | 57 + .../vendor/sabre/http/tests/phpunit.xml | 27 + .../vendor/sabre/http/tests/www/bar.php | 5 + .../backup/vendor/sabre/http/tests/www/foo | 1 + .../vendor/sabre/http/tests/www/large.php | 7 + .../panakour/backup/vendor/sabre/uri/LICENSE | 27 + .../backup/vendor/sabre/uri/composer.json | 46 + .../sabre/uri/lib/InvalidUriException.php | 19 + .../backup/vendor/sabre/uri/lib/Version.php | 20 + .../backup/vendor/sabre/uri/lib/functions.php | 376 +++ .../backup/vendor/sabre/uri/phpstan.neon | 2 + .../backup/vendor/sabre/vobject/CHANGELOG.md | 850 +++++ .../backup/vendor/sabre/vobject/LICENSE | 27 + .../backup/vendor/sabre/vobject/README.md | 55 + .../backup/vendor/sabre/vobject/bin/bench.php | 12 + .../vobject/bin/bench_freebusygenerator.php | 53 + .../vobject/bin/bench_manipulatevcard.php | 64 + .../sabre/vobject/bin/fetch_windows_zones.php | 49 + .../vendor/sabre/vobject/bin/generate_vcards | 241 ++ .../vobject/bin/generateicalendardata.php | 87 + .../sabre/vobject/bin/mergeduplicates.php | 160 + .../vendor/sabre/vobject/bin/rrulebench.php | 32 + .../backup/vendor/sabre/vobject/bin/vobject | 27 + .../backup/vendor/sabre/vobject/composer.json | 92 + .../vobject/lib/BirthdayCalendarGenerator.php | 172 + .../backup/vendor/sabre/vobject/lib/Cli.php | 712 +++++ .../vendor/sabre/vobject/lib/Component.php | 671 ++++ .../sabre/vobject/lib/Component/Available.php | 123 + .../sabre/vobject/lib/Component/VAlarm.php | 138 + .../vobject/lib/Component/VAvailability.php | 149 + .../sabre/vobject/lib/Component/VCalendar.php | 528 ++++ .../sabre/vobject/lib/Component/VCard.php | 535 ++++ .../sabre/vobject/lib/Component/VEvent.php | 140 + .../sabre/vobject/lib/Component/VFreeBusy.php | 93 + .../sabre/vobject/lib/Component/VJournal.php | 101 + .../sabre/vobject/lib/Component/VTimeZone.php | 63 + .../sabre/vobject/lib/Component/VTodo.php | 181 ++ .../sabre/vobject/lib/DateTimeParser.php | 560 ++++ .../vendor/sabre/vobject/lib/Document.php | 264 ++ .../vendor/sabre/vobject/lib/ElementList.php | 46 + .../vendor/sabre/vobject/lib/EofException.php | 15 + .../vendor/sabre/vobject/lib/FreeBusyData.php | 185 ++ .../sabre/vobject/lib/FreeBusyGenerator.php | 550 ++++ .../vendor/sabre/vobject/lib/ITip/Broker.php | 964 ++++++ .../sabre/vobject/lib/ITip/ITipException.php | 16 + .../vendor/sabre/vobject/lib/ITip/Message.php | 136 + ...SameOrganizerForAllComponentsException.php | 18 + .../vobject/lib/InvalidDataException.php | 15 + .../backup/vendor/sabre/vobject/lib/Node.php | 245 ++ .../sabre/vobject/lib/PHPUnitAssertions.php | 75 + .../vendor/sabre/vobject/lib/Parameter.php | 371 +++ .../sabre/vobject/lib/ParseException.php | 14 + .../vendor/sabre/vobject/lib/Parser/Json.php | 190 ++ .../sabre/vobject/lib/Parser/MimeDir.php | 671 ++++ .../sabre/vobject/lib/Parser/Parser.php | 75 + .../vendor/sabre/vobject/lib/Parser/XML.php | 377 +++ .../lib/Parser/XML/Element/KeyValue.php | 65 + .../vendor/sabre/vobject/lib/Property.php | 615 ++++ .../sabre/vobject/lib/Property/Binary.php | 109 + .../sabre/vobject/lib/Property/Boolean.php | 73 + .../sabre/vobject/lib/Property/FlatText.php | 46 + .../sabre/vobject/lib/Property/FloatValue.php | 124 + .../lib/Property/ICalendar/CalAddress.php | 60 + .../vobject/lib/Property/ICalendar/Date.php | 18 + .../lib/Property/ICalendar/DateTime.php | 363 +++ .../lib/Property/ICalendar/Duration.php | 79 + .../vobject/lib/Property/ICalendar/Period.php | 135 + .../vobject/lib/Property/ICalendar/Recur.php | 336 ++ .../vobject/lib/Property/IntegerValue.php | 77 + .../sabre/vobject/lib/Property/Text.php | 390 +++ .../sabre/vobject/lib/Property/Time.php | 131 + .../sabre/vobject/lib/Property/Unknown.php | 41 + .../vendor/sabre/vobject/lib/Property/Uri.php | 116 + .../sabre/vobject/lib/Property/UtcOffset.php | 70 + .../sabre/vobject/lib/Property/VCard/Date.php | 36 + .../lib/Property/VCard/DateAndOrTime.php | 367 +++ .../vobject/lib/Property/VCard/DateTime.php | 28 + .../lib/Property/VCard/LanguageTag.php | 54 + .../lib/Property/VCard/PhoneNumber.php | 30 + .../vobject/lib/Property/VCard/TimeStamp.php | 81 + .../vendor/sabre/vobject/lib/Reader.php | 95 + .../sabre/vobject/lib/Recur/EventIterator.php | 484 +++ .../Recur/MaxInstancesExceededException.php | 17 + .../lib/Recur/NoInstancesException.php | 18 + .../sabre/vobject/lib/Recur/RDateIterator.php | 166 + .../sabre/vobject/lib/Recur/RRuleIterator.php | 973 ++++++ .../vendor/sabre/vobject/lib/Settings.php | 55 + .../sabre/vobject/lib/Splitter/ICalendar.php | 106 + .../lib/Splitter/SplitterInterface.php | 38 + .../sabre/vobject/lib/Splitter/VCard.php | 74 + .../vendor/sabre/vobject/lib/StringUtil.php | 62 + .../vendor/sabre/vobject/lib/TimeZoneUtil.php | 265 ++ .../vendor/sabre/vobject/lib/UUIDUtil.php | 66 + .../sabre/vobject/lib/VCardConverter.php | 416 +++ .../vendor/sabre/vobject/lib/Version.php | 18 + .../vendor/sabre/vobject/lib/Writer.php | 68 + .../lib/timezonedata/exchangezones.php | 94 + .../vobject/lib/timezonedata/lotuszones.php | 101 + .../sabre/vobject/lib/timezonedata/php-bc.php | 153 + .../lib/timezonedata/php-workaround.php | 46 + .../vobject/lib/timezonedata/windowszones.php | 143 + .../backup/vendor/sabre/vobject/phpstan.neon | 4 + .../sabre/vobject/resources/schema/xcal.rng | 1192 +++++++ .../sabre/vobject/resources/schema/xcard.rng | 388 +++ .../vobject/tests/VObject/AttachIssueTest.php | 22 + .../VObject/BirthdayCalendarGeneratorTest.php | 542 ++++ .../sabre/vobject/tests/VObject/CliTest.php | 626 ++++ .../tests/VObject/Component/AvailableTest.php | 71 + .../tests/VObject/Component/VAlarmTest.php | 171 + .../VObject/Component/VAvailabilityTest.php | 474 +++ .../tests/VObject/Component/VCalendarTest.php | 757 +++++ .../tests/VObject/Component/VCardTest.php | 302 ++ .../tests/VObject/Component/VEventTest.php | 94 + .../tests/VObject/Component/VFreeBusyTest.php | 64 + .../tests/VObject/Component/VJournalTest.php | 94 + .../tests/VObject/Component/VTimeZoneTest.php | 54 + .../tests/VObject/Component/VTodoTest.php | 171 + .../vobject/tests/VObject/ComponentTest.php | 542 ++++ .../tests/VObject/DateTimeParserTest.php | 654 ++++ .../vobject/tests/VObject/DocumentTest.php | 84 + .../vobject/tests/VObject/ElementListTest.php | 33 + .../vobject/tests/VObject/EmClientTest.php | 55 + .../tests/VObject/EmptyParameterTest.php | 69 + .../tests/VObject/EmptyValueIssueTest.php | 30 + .../tests/VObject/FreeBusyDataTest.php | 311 ++ .../tests/VObject/FreeBusyGeneratorTest.php | 719 +++++ .../tests/VObject/GoogleColonEscapingTest.php | 31 + .../VObject/ICalendar/AttachParseTest.php | 30 + .../VObject/ITip/BrokerAttendeeReplyTest.php | 1213 +++++++ .../VObject/ITip/BrokerDeleteEventTest.php | 326 ++ .../tests/VObject/ITip/BrokerNewEventTest.php | 578 ++++ .../VObject/ITip/BrokerProcessMessageTest.php | 157 + .../VObject/ITip/BrokerProcessReplyTest.php | 583 ++++ .../ITip/BrokerSignificantChangesTest.php | 108 + .../tests/VObject/ITip/BrokerTester.php | 93 + ...ezoneInParseEventInfoWithoutMasterTest.php | 78 + .../VObject/ITip/BrokerUpdateEventTest.php | 817 +++++ .../tests/VObject/ITip/EvolutionTest.php | 2647 ++++++++++++++++ .../tests/VObject/ITip/MessageTest.php | 30 + .../vobject/tests/VObject/Issue153Test.php | 14 + .../vobject/tests/VObject/Issue259Test.php | 23 + .../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 | 27 + .../sabre/vobject/tests/VObject/JCalTest.php | 149 + .../sabre/vobject/tests/VObject/JCardTest.php | 194 ++ .../tests/VObject/LineFoldingIssueTest.php | 23 + .../vobject/tests/VObject/ParameterTest.php | 124 + .../vobject/tests/VObject/Parser/JsonTest.php | 390 +++ .../tests/VObject/Parser/MimeDirTest.php | 148 + .../VObject/Parser/QuotedPrintableTest.php | 102 + .../vobject/tests/VObject/Parser/XmlTest.php | 2808 +++++++++++++++++ .../tests/VObject/Property/BinaryTest.php | 16 + .../tests/VObject/Property/BooleanTest.php | 21 + .../tests/VObject/Property/CompoundTest.php | 48 + .../tests/VObject/Property/FloatTest.php | 29 + .../Property/ICalendar/CalAddressTest.php | 31 + .../Property/ICalendar/DateTimeTest.php | 347 ++ .../Property/ICalendar/DurationTest.php | 20 + .../VObject/Property/ICalendar/RecurTest.php | 430 +++ .../tests/VObject/Property/TextTest.php | 87 + .../tests/VObject/Property/UriTest.php | 26 + .../Property/VCard/DateAndOrTimeTest.php | 253 ++ .../Property/VCard/LanguageTagTest.php | 47 + .../Property/VCard/PhoneNumberTest.php | 19 + .../vobject/tests/VObject/PropertyTest.php | 388 +++ .../vobject/tests/VObject/ReaderTest.php | 453 +++ .../EventIterator/ByMonthInDailyTest.php | 60 + .../Recur/EventIterator/BySetPosHangTest.php | 62 + .../EventIterator/ExpandFloatingTimesTest.php | 121 + .../EventIterator/FifthTuesdayProblemTest.php | 53 + .../EventIterator/HandleRDateExpandTest.php | 60 + .../EventIterator/IncorrectExpandTest.php | 62 + .../EventIterator/InfiniteLoopProblemTest.php | 95 + .../Recur/EventIterator/Issue26Test.php | 33 + .../Recur/EventIterator/Issue48Test.php | 50 + .../Recur/EventIterator/Issue50Test.php | 127 + .../VObject/Recur/EventIterator/MainTest.php | 1411 +++++++++ .../Recur/EventIterator/MaxInstancesTest.php | 37 + .../EventIterator/MissingOverriddenTest.php | 62 + .../Recur/EventIterator/NoInstancesTest.php | 39 + .../EventIterator/OverrideFirstEventTest.php | 119 + .../SameDateForRecurringEventsTest.php | 56 + .../tests/VObject/Recur/RDateIteratorTest.php | 74 + .../tests/VObject/Recur/RRuleIteratorTest.php | 956 ++++++ .../UntilRespectsTimezoneTest.ics | 39 + .../vobject/tests/VObject/SlashRTest.php | 19 + .../tests/VObject/Splitter/ICalendarTest.php | 320 ++ .../tests/VObject/Splitter/VCardTest.php | 237 ++ .../vobject/tests/VObject/StringUtilTest.php | 50 + .../tests/VObject/TimeZoneUtilTest.php | 357 +++ .../vobject/tests/VObject/UUIDUtilTest.php | 36 + .../vobject/tests/VObject/VCard21Test.php | 51 + .../tests/VObject/VCardConverterTest.php | 547 ++++ .../vobject/tests/VObject/VersionTest.php | 14 + .../vobject/tests/VObject/WriterTest.php | 39 + .../sabre/vobject/tests/VObject/issue153.vcf | 352 +++ .../sabre/vobject/tests/VObject/issue64.vcf | 351 +++ .../vendor/sabre/vobject/tests/bootstrap.php | 15 + .../vendor/sabre/vobject/tests/phpunit.xml | 24 + .../backup/vendor/sabre/xml/CHANGELOG.md | 273 ++ .../panakour/backup/vendor/sabre/xml/LICENSE | 27 + .../backup/vendor/sabre/xml/README.md | 25 + .../backup/vendor/sabre/xml/composer.json | 53 + .../sabre/xml/lib/ContextStackTrait.php | 118 + .../sabre/xml/lib/Deserializer/functions.php | 359 +++ .../backup/vendor/sabre/xml/lib/Element.php | 22 + .../vendor/sabre/xml/lib/Element/Base.php | 84 + .../vendor/sabre/xml/lib/Element/Cdata.php | 59 + .../vendor/sabre/xml/lib/Element/Elements.php | 100 + .../vendor/sabre/xml/lib/Element/KeyValue.php | 100 + .../vendor/sabre/xml/lib/Element/Uri.php | 99 + .../sabre/xml/lib/Element/XmlFragment.php | 148 + .../vendor/sabre/xml/lib/LibXMLException.php | 49 + .../vendor/sabre/xml/lib/ParseException.php | 19 + .../backup/vendor/sabre/xml/lib/Reader.php | 301 ++ .../sabre/xml/lib/Serializer/functions.php | 208 ++ .../backup/vendor/sabre/xml/lib/Service.php | 310 ++ .../backup/vendor/sabre/xml/lib/Version.php | 20 + .../backup/vendor/sabre/xml/lib/Writer.php | 257 ++ .../sabre/xml/lib/XmlDeserializable.php | 38 + .../vendor/sabre/xml/lib/XmlSerializable.php | 34 + .../backup/vendor/sabre/xml/phpstan.neon | 6 + .../xml/tests/Sabre/Xml/ContextStackTest.php | 43 + .../tests/Sabre/Xml/Deserializer/EnumTest.php | 85 + .../Xml/Deserializer/FunctionCallerTest.php | 227 ++ .../Sabre/Xml/Deserializer/KeyValueTest.php | 151 + .../Xml/Deserializer/MixedContentTest.php | 35 + .../Deserializer/RepeatingElementsTest.php | 35 + .../Xml/Deserializer/ValueObjectTest.php | 165 + .../xml/tests/Sabre/Xml/Element/CDataTest.php | 54 + .../xml/tests/Sabre/Xml/Element/Eater.php | 71 + .../tests/Sabre/Xml/Element/ElementsTest.php | 127 + .../tests/Sabre/Xml/Element/KeyValueTest.php | 206 ++ .../xml/tests/Sabre/Xml/Element/Mock.php | 56 + .../xml/tests/Sabre/Xml/Element/UriTest.php | 74 + .../Sabre/Xml/Element/XmlFragmentTest.php | 139 + .../xml/tests/Sabre/Xml/InfiteLoopTest.php | 50 + .../sabre/xml/tests/Sabre/Xml/ReaderTest.php | 550 ++++ .../tests/Sabre/Xml/Serializer/EnumTest.php | 33 + .../Xml/Serializer/RepeatingElementsTest.php | 32 + .../sabre/xml/tests/Sabre/Xml/ServiceTest.php | 418 +++ .../sabre/xml/tests/Sabre/Xml/WriterTest.php | 412 +++ .../backup/vendor/sabre/xml/tests/phpunit.xml | 21 + .../vendor/spatie/db-dumper/CHANGELOG.md | 174 + .../vendor/spatie/db-dumper/CONTRIBUTING.md | 32 + .../backup/vendor/spatie/db-dumper/LICENSE.md | 21 + .../backup/vendor/spatie/db-dumper/README.md | 285 ++ .../vendor/spatie/db-dumper/composer.json | 41 + .../db-dumper/src/Compressors/Compressor.php | 10 + .../src/Compressors/GzipCompressor.php | 16 + .../db-dumper/src/Databases/MongoDb.php | 124 + .../spatie/db-dumper/src/Databases/MySql.php | 269 ++ .../db-dumper/src/Databases/PostgreSql.php | 122 + .../spatie/db-dumper/src/Databases/Sqlite.php | 49 + .../vendor/spatie/db-dumper/src/DbDumper.php | 268 ++ .../src/Exceptions/CannotSetParameter.php | 19 + .../src/Exceptions/CannotStartDump.php | 18 + .../db-dumper/src/Exceptions/DumpFailed.php | 35 + .../vendor/spatie/dropbox-api/CHANGELOG.md | 96 + .../vendor/spatie/dropbox-api/CONTRIBUTING.md | 55 + .../vendor/spatie/dropbox-api/LICENSE.md | 21 + .../vendor/spatie/dropbox-api/README.md | 108 + .../vendor/spatie/dropbox-api/composer.json | 51 + .../vendor/spatie/dropbox-api/src/Client.php | 671 ++++ .../dropbox-api/src/Exceptions/BadRequest.php | 36 + .../dropbox-api/src/UploadSessionCursor.php | 26 + .../spatie/flysystem-dropbox/CHANGELOG.md | 48 + .../spatie/flysystem-dropbox/CONTRIBUTING.md | 55 + .../spatie/flysystem-dropbox/LICENSE.md | 21 + .../vendor/spatie/flysystem-dropbox/README.md | 85 + .../spatie/flysystem-dropbox/composer.json | 46 + .../flysystem-dropbox/src/DropboxAdapter.php | 323 ++ .../vendor/spatie/laravel-backup/CHANGELOG.md | 597 ++++ .../spatie/laravel-backup/CONTRIBUTING.md | 32 + .../vendor/spatie/laravel-backup/LICENSE.md | 21 + .../vendor/spatie/laravel-backup/README.md | 77 + .../vendor/spatie/laravel-backup/UPGRADING.md | 3 + .../spatie/laravel-backup/composer.json | 67 + .../spatie/laravel-backup/config/backup.php | 210 ++ .../resources/lang/ar/notifications.php | 35 + .../resources/lang/da/notifications.php | 35 + .../resources/lang/de/notifications.php | 35 + .../resources/lang/en/notifications.php | 35 + .../resources/lang/es/notifications.php | 35 + .../resources/lang/fa/notifications.php | 35 + .../resources/lang/fr/notifications.php | 35 + .../resources/lang/hi/notifications.php | 35 + .../resources/lang/id/notifications.php | 35 + .../resources/lang/it/notifications.php | 35 + .../resources/lang/pl/notifications.php | 35 + .../resources/lang/pt-BR/notifications.php | 35 + .../resources/lang/ro/notifications.php | 35 + .../resources/lang/ru/notifications.php | 35 + .../resources/lang/tr/notifications.php | 35 + .../resources/lang/uk/notifications.php | 35 + .../src/BackupDestination/Backup.php | 61 + .../BackupDestination/BackupCollection.php | 49 + .../BackupDestination/BackupDestination.php | 174 + .../BackupDestinationFactory.php | 16 + .../src/BackupServiceProvider.php | 51 + .../src/Commands/BackupCommand.php | 72 + .../src/Commands/BaseCommand.php | 18 + .../src/Commands/CleanupCommand.php | 53 + .../src/Commands/ListCommand.php | 97 + .../src/Commands/MonitorCommand.php | 34 + .../src/Events/BackupHasFailed.php | 22 + .../src/Events/BackupManifestWasCreated.php | 16 + .../src/Events/BackupWasSuccessful.php | 16 + .../src/Events/BackupZipWasCreated.php | 14 + .../src/Events/CleanupHasFailed.php | 22 + .../src/Events/CleanupWasSuccessful.php | 16 + .../src/Events/HealthyBackupWasFound.php | 16 + .../src/Events/UnhealthyBackupWasFound.php | 16 + .../src/Exceptions/CannotCreateDbDumper.php | 13 + .../Exceptions/InvalidBackupDestination.php | 13 + .../src/Exceptions/InvalidBackupJob.php | 23 + .../src/Exceptions/InvalidCommand.php | 13 + .../src/Exceptions/InvalidConfiguration.php | 13 + .../Exceptions/NotificationCouldNotBeSent.php | 15 + .../src/Helpers/ConsoleOutput.php | 28 + .../laravel-backup/src/Helpers/File.php | 49 + .../laravel-backup/src/Helpers/Format.php | 36 + .../laravel-backup/src/Helpers/functions.php | 8 + .../src/Notifications/BaseNotification.php | 71 + .../src/Notifications/EventHandler.php | 74 + .../src/Notifications/Notifiable.php | 25 + .../Notifications/BackupHasFailed.php | 60 + .../Notifications/BackupWasSuccessful.php | 47 + .../Notifications/CleanupHasFailed.php | 60 + .../Notifications/CleanupWasSuccessful.php | 47 + .../Notifications/HealthyBackupWasFound.php | 47 + .../Notifications/UnhealthyBackupWasFound.php | 72 + .../src/Tasks/Backup/BackupJob.php | 277 ++ .../src/Tasks/Backup/BackupJobFactory.php | 31 + .../src/Tasks/Backup/DbDumperFactory.php | 105 + .../src/Tasks/Backup/FileSelection.php | 143 + .../src/Tasks/Backup/Manifest.php | 74 + .../laravel-backup/src/Tasks/Backup/Zip.php | 115 + .../src/Tasks/Cleanup/CleanupJob.php | 63 + .../src/Tasks/Cleanup/CleanupStrategy.php | 19 + .../src/Tasks/Cleanup/Period.php | 31 + .../Cleanup/Strategies/DefaultStrategy.php | 122 + .../Tasks/Monitor/BackupDestinationStatus.php | 159 + .../BackupDestinationStatusFactory.php | 29 + .../spatie/temporary-directory/CHANGELOG.md | 51 + .../temporary-directory/CONTRIBUTING.md | 55 + .../spatie/temporary-directory/LICENSE.md | 21 + .../spatie/temporary-directory/README.md | 145 + .../spatie/temporary-directory/composer.json | 40 + .../src/TemporaryDirectory.php | 162 + .../backup/vendor/symfony/finder/CHANGELOG.md | 74 + .../symfony/finder/Comparator/Comparator.php | 98 + .../finder/Comparator/DateComparator.php | 51 + .../finder/Comparator/NumberComparator.php | 79 + .../Exception/AccessDeniedException.php | 19 + .../Exception/DirectoryNotFoundException.php | 19 + .../backup/vendor/symfony/finder/Finder.php | 812 +++++ .../vendor/symfony/finder/Gitignore.php | 105 + .../backup/vendor/symfony/finder/Glob.php | 116 + .../finder/Iterator/CustomFilterIterator.php | 61 + .../Iterator/DateRangeFilterIterator.php | 58 + .../Iterator/DepthRangeFilterIterator.php | 45 + .../ExcludeDirectoryFilterIterator.php | 87 + .../Iterator/FileTypeFilterIterator.php | 53 + .../Iterator/FilecontentFilterIterator.php | 58 + .../Iterator/FilenameFilterIterator.php | 47 + .../Iterator/MultiplePcreFilterIterator.php | 112 + .../finder/Iterator/PathFilterIterator.php | 56 + .../Iterator/RecursiveDirectoryIterator.php | 144 + .../Iterator/SizeRangeFilterIterator.php | 57 + .../finder/Iterator/SortableIterator.php | 101 + .../backup/vendor/symfony/finder/LICENSE | 19 + .../backup/vendor/symfony/finder/README.md | 14 + .../vendor/symfony/finder/SplFileInfo.php | 85 + .../vendor/symfony/finder/composer.json | 33 + .../vendor/symfony/polyfill-intl-idn/Idn.php | 283 ++ .../vendor/symfony/polyfill-intl-idn/LICENSE | 19 + .../symfony/polyfill-intl-idn/README.md | 12 + .../symfony/polyfill-intl-idn/bootstrap.php | 61 + .../symfony/polyfill-intl-idn/composer.json | 36 + .../vendor/symfony/polyfill-mbstring/LICENSE | 19 + .../symfony/polyfill-mbstring/Mbstring.php | 847 +++++ .../symfony/polyfill-mbstring/README.md | 13 + .../Resources/unidata/lowerCase.php | 1096 +++++++ .../Resources/unidata/titleCaseRegexp.php | 5 + .../Resources/unidata/upperCase.php | 1104 +++++++ .../symfony/polyfill-mbstring/bootstrap.php | 64 + .../symfony/polyfill-mbstring/composer.json | 34 + .../vendor/symfony/polyfill-php72/LICENSE | 19 + .../vendor/symfony/polyfill-php72/Php72.php | 216 ++ .../vendor/symfony/polyfill-php72/README.md | 27 + .../symfony/polyfill-php72/bootstrap.php | 36 + .../symfony/polyfill-php72/composer.json | 31 + .../vendor/symfony/process/CHANGELOG.md | 57 + .../process/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 21 + .../process/Exception/LogicException.php | 21 + .../Exception/ProcessFailedException.php | 54 + .../Exception/ProcessTimedOutException.php | 69 + .../process/Exception/RuntimeException.php | 21 + .../symfony/process/ExecutableFinder.php | 88 + .../vendor/symfony/process/InputStream.php | 92 + .../backup/vendor/symfony/process/LICENSE | 19 + .../symfony/process/PhpExecutableFinder.php | 94 + .../vendor/symfony/process/PhpProcess.php | 76 + .../symfony/process/Pipes/AbstractPipes.php | 182 ++ .../symfony/process/Pipes/PipesInterface.php | 67 + .../symfony/process/Pipes/UnixPipes.php | 153 + .../symfony/process/Pipes/WindowsPipes.php | 191 ++ .../backup/vendor/symfony/process/Process.php | 1746 ++++++++++ .../vendor/symfony/process/ProcessBuilder.php | 280 ++ .../vendor/symfony/process/ProcessUtils.php | 123 + .../backup/vendor/symfony/process/README.md | 13 + .../process/Tests/ErrorProcessInitiator.php | 36 + .../process/Tests/ExecutableFinderTest.php | 175 + .../process/Tests/NonStopableProcess.php | 47 + .../process/Tests/PhpExecutableFinderTest.php | 72 + .../symfony/process/Tests/PhpProcessTest.php | 48 + .../PipeStdinInStdoutStdErrStreamSelect.php | 72 + .../process/Tests/ProcessBuilderTest.php | 220 ++ .../Tests/ProcessFailedExceptionTest.php | 133 + .../symfony/process/Tests/ProcessTest.php | 1589 ++++++++++ .../process/Tests/ProcessUtilsTest.php | 53 + .../symfony/process/Tests/SignalListener.php | 21 + .../vendor/symfony/process/composer.json | 33 + .../vendor/symfony/process/phpunit.xml.dist | 30 + .../gokbakja/components/MachineProduction.php | 29 +- plugins/romanah/gokbakja/components/Order.php | 669 ++-- .../romanah/gokbakja/components/OrderItem.php | 50 +- plugins/romanah/gokbakja/components/Sewer.php | 57 +- plugins/romanah/gokbakja/models/Order.php | 4 + plugins/romanah/gokbakja/models/Shipping.php | 3 +- themes/gokbakja/pages/home.htm | 345 +- themes/gokbakja/pages/orders/order-detail.htm | 6 +- .../pages/orders/order-detaillogistics.htm | 26 +- .../pages/orders/order-detailpayment.htm | 13 +- themes/gokbakja/pages/product/actions.htm | 16 +- .../gokbakja/pages/production/production.htm | 28 +- 1244 files changed, 184684 insertions(+), 419 deletions(-) create mode 100644 plugins/panakour/backup/Dropbox.php create mode 100644 plugins/panakour/backup/DropboxServiceProvider.php create mode 100644 plugins/panakour/backup/LICENCE.md create mode 100644 plugins/panakour/backup/Plugin.php create mode 100644 plugins/panakour/backup/README.md create mode 100644 plugins/panakour/backup/Repository.php create mode 100644 plugins/panakour/backup/WebdavServiceProvider.php create mode 100644 plugins/panakour/backup/assets/css/main.css create mode 100644 plugins/panakour/backup/assets/images/backup-icon.svg create mode 100644 plugins/panakour/backup/assets/js/backups-page.js create mode 100644 plugins/panakour/backup/composer.json create mode 100644 plugins/panakour/backup/config/config.php create mode 100644 plugins/panakour/backup/controllers/Backups.php create mode 100644 plugins/panakour/backup/controllers/backups/_list.htm create mode 100644 plugins/panakour/backup/controllers/backups/_toolbar.htm create mode 100644 plugins/panakour/backup/controllers/backups/index.htm create mode 100644 plugins/panakour/backup/docs/images/oc_backup_config.png create mode 100644 plugins/panakour/backup/docs/images/oc_backup_config_1.png create mode 100644 plugins/panakour/backup/docs/images/oc_backups.png create mode 100644 plugins/panakour/backup/models/Settings.php create mode 100644 plugins/panakour/backup/models/settings/fields.yaml create mode 100644 plugins/panakour/backup/updates/seed_default_settings.php create mode 100644 plugins/panakour/backup/updates/version.yaml create mode 100644 plugins/panakour/backup/vendor/autoload.php create mode 100644 plugins/panakour/backup/vendor/bin/generate_vcards create mode 100644 plugins/panakour/backup/vendor/bin/naturalselection create mode 100644 plugins/panakour/backup/vendor/bin/sabredav create mode 100644 plugins/panakour/backup/vendor/bin/vobject create mode 100644 plugins/panakour/backup/vendor/composer/ClassLoader.php create mode 100644 plugins/panakour/backup/vendor/composer/LICENSE create mode 100644 plugins/panakour/backup/vendor/composer/autoload_classmap.php create mode 100644 plugins/panakour/backup/vendor/composer/autoload_files.php create mode 100644 plugins/panakour/backup/vendor/composer/autoload_namespaces.php create mode 100644 plugins/panakour/backup/vendor/composer/autoload_psr4.php create mode 100644 plugins/panakour/backup/vendor/composer/autoload_real.php create mode 100644 plugins/panakour/backup/vendor/composer/autoload_static.php create mode 100644 plugins/panakour/backup/vendor/composer/installed.json create mode 100644 plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/LICENSE create mode 100644 plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/composer.json create mode 100644 plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/src/GuzzleFactory.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/Dockerfile create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/LICENSE create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/README.md create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/UPGRADING.md create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/composer.json create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Client.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/ClientInterface.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/ClientException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/ConnectException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/GuzzleException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/SeekException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/ServerException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/TransferException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlHandler.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/HandlerStack.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/MessageFormatter.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Middleware.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Pool.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RequestOptions.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RetryMiddleware.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/TransferStats.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/UriTemplate.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Utils.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/functions.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/functions_include.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/LICENSE create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/Makefile create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/README.md create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/composer.json create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/AggregateException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/CancellationException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/Coroutine.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/EachPromise.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/FulfilledPromise.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/Promise.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/PromiseInterface.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/PromisorInterface.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/RejectedPromise.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/RejectionException.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/TaskQueue.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/TaskQueueInterface.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/functions.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/promises/src/functions_include.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/LICENSE create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/README.md create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/composer.json create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/AppendStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/BufferStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/CachingStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/DroppingStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/FnStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/InflateStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/LazyOpenStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/LimitStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/MessageTrait.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/MultipartStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/NoSeekStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/PumpStream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Request.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Response.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Rfc7230.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/ServerRequest.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Stream.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/StreamWrapper.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UploadedFile.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Uri.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UriNormalizer.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UriResolver.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/functions.php create mode 100644 plugins/panakour/backup/vendor/guzzlehttp/psr7/src/functions_include.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem-webdav/LICENCE create mode 100644 plugins/panakour/backup/vendor/league/flysystem-webdav/changelog.md create mode 100644 plugins/panakour/backup/vendor/league/flysystem-webdav/composer.json create mode 100644 plugins/panakour/backup/vendor/league/flysystem-webdav/src/WebDAVAdapter.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/LICENSE create mode 100644 plugins/panakour/backup/vendor/league/flysystem/SECURITY.md create mode 100644 plugins/panakour/backup/vendor/league/flysystem/composer.json create mode 100644 plugins/panakour/backup/vendor/league/flysystem/deprecations.md create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/AbstractAdapter.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/AbstractFtpAdapter.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/CanOverwriteFiles.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Ftp.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Ftpd.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Local.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/NullAdapter.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/NotSupportingVisibilityTrait.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedCopyTrait.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedReadingTrait.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedTrait.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedWritingTrait.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Adapter/SynologyFtp.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/AdapterInterface.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Config.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/ConfigAwareTrait.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/ConnectionErrorException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/ConnectionRuntimeException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Directory.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Exception.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/File.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/FileExistsException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/FileNotFoundException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Filesystem.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/FilesystemException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/FilesystemInterface.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/FilesystemNotFoundException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Handler.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/InvalidRootException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/MountManager.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/NotSupportedException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/AbstractPlugin.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/EmptyDir.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ForcedCopy.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ForcedRename.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/GetWithMetadata.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListFiles.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListPaths.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListWith.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/PluggableTrait.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Plugin/PluginNotFoundException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/PluginInterface.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/ReadInterface.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/RootViolationException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/SafeStorage.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/UnreadableFileException.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Util.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Util/ContentListingFormatter.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Util/MimeType.php create mode 100644 plugins/panakour/backup/vendor/league/flysystem/src/Util/StreamHasher.php create mode 100644 plugins/panakour/backup/vendor/psr/http-message/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/psr/http-message/LICENSE create mode 100644 plugins/panakour/backup/vendor/psr/http-message/README.md create mode 100644 plugins/panakour/backup/vendor/psr/http-message/composer.json create mode 100644 plugins/panakour/backup/vendor/psr/http-message/src/MessageInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/http-message/src/RequestInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/http-message/src/ResponseInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/http-message/src/ServerRequestInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/http-message/src/StreamInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/http-message/src/UploadedFileInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/http-message/src/UriInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/log/LICENSE create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/AbstractLogger.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/InvalidArgumentException.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/LogLevel.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/LoggerAwareInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/LoggerAwareTrait.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/LoggerInterface.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/LoggerTrait.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/NullLogger.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/DummyTest.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php create mode 100644 plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/TestLogger.php create mode 100644 plugins/panakour/backup/vendor/psr/log/README.md create mode 100644 plugins/panakour/backup/vendor/psr/log/composer.json create mode 100644 plugins/panakour/backup/vendor/ralouphie/getallheaders/LICENSE create mode 100644 plugins/panakour/backup/vendor/ralouphie/getallheaders/README.md create mode 100644 plugins/panakour/backup/vendor/ralouphie/getallheaders/composer.json create mode 100644 plugins/panakour/backup/vendor/ralouphie/getallheaders/src/getallheaders.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/sabre/dav/CONTRIBUTING.md create mode 100644 plugins/panakour/backup/vendor/sabre/dav/LICENSE create mode 100644 plugins/panakour/backup/vendor/sabre/dav/README.md create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/build.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/googlecode_upload.py create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/migrateto20.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/migrateto21.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/migrateto30.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/migrateto32.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/naturalselection create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/sabredav create mode 100644 plugins/panakour/backup/vendor/sabre/dav/bin/sabredav.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/composer.json create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/addressbookserver.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/calendarserver.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/fileserver.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/groupwareserver.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/minimal.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.addressbooks.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.calendars.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.locks.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.principals.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.propertystorage.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.users.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.addressbooks.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.calendars.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.locks.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.principals.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.propertystorage.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.users.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.addressbooks.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.calendars.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.locks.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.principals.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.propertystorage.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.users.sql create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_htaccess.conf create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_vhost.conf create mode 100644 plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_vhost_cgi.conf create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SimplePDO.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SyncSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Calendar.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarHome.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarObject.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICalendar.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICalendarObject.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/INode.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Principal/ProxyRead.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Principal/User.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/AddressBook.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Backend/PDO.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Card.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/IAddressBook.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/ICard.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/IDirectory.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Auth/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/HtmlOutputHelper.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.png create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Client.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Collection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/CorePlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/BadRequest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Conflict.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/ConflictingLock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/InsufficientStorage.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/InvalidResourceType.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/LockTokenMatchesRequestUri.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Locked.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/NotFound.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/NotImplemented.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/PaymentRequired.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/PreconditionFailed.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/ServiceUnavailable.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/Directory.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/File.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/Node.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FSExt/Directory.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FSExt/File.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/File.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/ICollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/ICopyTarget.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/IExtendedCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/IFile.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/IMoveTarget.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/IMultiGet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/INode.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/IProperties.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/IQuota.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/Backend/AbstractBackend.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/Backend/BackendInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/Backend/File.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/MkCol.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Mount/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Node.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PartialUpdate/IPatchSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PartialUpdate/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropFind.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropPatch.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Server.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/ServerPlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Sharing/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/SimpleCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/SimpleFile.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/StringUtil.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Sync/ISyncCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Sync/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Tree.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/UUIDUtil.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Version.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Element/Prop.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/LockDiscovery.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Xml/Service.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/ACLTrait.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/FS/Collection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/FS/File.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/IACL.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/IPrincipal.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/IPrincipalCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Plugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Principal.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/phpstan.neon create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractPDOTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/Mock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockScheduling.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockSharing.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockSubscriptionSupport.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/PDOMySQLTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/PDOPgSqlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/PDOSqliteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/SimplePDOTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeNotificationsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSharedCalendarsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSubscriptionsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarObjectTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarQueryVAlarmTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarQueryValidatorTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDbyDayTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDoubleEventsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsFloatingTimeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/FreeBusyReportTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/GetEventsByTimerangeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ICSExportPluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue166Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue172Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue203Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue205Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue211Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue220Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue228Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/JCalTransformTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/CollectionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/NodeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/CollectionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyReadTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyWriteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/UserTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/DeliverNewEventTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/IMip/MockPlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/IMipPluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/InboxTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginBasicTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesWithSharedCalendarTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/ScheduleDeliverTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/SharedCalendarTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/SharingPluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/CreateSubscriptionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/SubscriptionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/TestUtil.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ValidateICalTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteReplyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/SystemStatusTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/AllowedSharingModesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/EmailAddressSetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/InviteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/ScheduleCalendarTranspTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarComponentSetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarDataTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCollationSetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/CalendarQueryReportTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/InviteReplyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/ShareTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AbstractPluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookHomeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookQueryTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookRootTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/AbstractPDOTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/Mock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/PDOMySQLTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/PDOPgSqlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/PDOSqliteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/CardTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/IDirectoryTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/MultiGetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/SogoStripContentTypeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/VCFExportTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/ValidateFilterTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/ValidateVCardTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedAddressDataTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedCollationSetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookMultiGetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookQueryReportTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/AbstractServer.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBasicTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBearerTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractDigestTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractPDOTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/ApacheTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/BasicCallBackTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/FileTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/IMAPTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOMySQLTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOPgSqlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOSqliteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/BasicNodeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/GuessContentTypeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/MapGetToPropFindTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/PropFindAllTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ClientMock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ClientTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/CorePluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/DbTestHelperTrait.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/LockedTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/PaymentRequiredTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/ServiceUnavailableTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/TooManyMatchesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ExceptionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FS/NodeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/DirectoryTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/FileTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/ServerTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/GetIfConditionsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HTTPPreferParsingTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpCopyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpDeleteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpGetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpHeadTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpMoveTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpPutTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Issue33Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/AbstractTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/FileTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/Mock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOMySQLTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOPgSqlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOSqliteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/MSWordTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Plugin2Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/Collection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/File.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/PropertiesCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/SharedNode.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/StreamingFile.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/MockLogger.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mount/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ObjectTreeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PSR3Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/FileMock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/SpecificationTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropFindTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropPatchTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/AbstractPDOTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/Mock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOMysqlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOPgSqlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOSqliteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerEventsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerMKCOLTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPreconditionsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPropsInfiniteDepthTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPropsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerRangeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerSimpleTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerUpdatePropertiesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sharing/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sharing/ShareResourceTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SimpleCollectionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SimpleFileTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/StringUtilTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sync/MockSyncCollection.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sync/PluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SyncTokenPropertyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TemporaryFileFilterTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TestPlugin.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TreeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/UUIDUtilTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/PropTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/ResponseTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/ShareeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/HrefTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/InviteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LastModifiedTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LocalHrefTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LockDiscoveryTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/ShareAccessTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedMethodSetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedReportSetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/PropFindTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/PropPatchTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/ShareResourceTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/SyncCollectionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/ServiceTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/ACLMethodTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/AclPrincipalPropSetReportTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/AllowAccessTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/BlockAccessTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/AceConflictTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NeedPrivilegesExceptionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NoAbstractTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NotRecognizedPrincipalTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NotSupportedPrivilegeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/ExpandPropertiesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/CollectionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/FileTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/HomeCollectionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/MockACLNode.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/MockPrincipal.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginAdminTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginPropertiesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginUpdatePropertiesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/AbstractPDOTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/Mock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOMySQLTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOPgSqlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOSqliteTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalCollectionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalMatchTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalPropertySearchTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalSearchPropertySetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/SimplePluginTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/ACLTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/AclRestrictionsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/CurrentUserPrivilegeSetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/PrincipalTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/SupportedPrivilegeSetTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Request/AclPrincipalPropSetReportTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Request/PrincipalMatchReportTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVServerTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/HTTP/SapiMock.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/TestUtil.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/bootstrap.php create mode 100644 plugins/panakour/backup/vendor/sabre/dav/tests/phpunit.xml create mode 100644 plugins/panakour/backup/vendor/sabre/event/LICENSE create mode 100644 plugins/panakour/backup/vendor/sabre/event/composer.json create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/Emitter.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/EmitterInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/EmitterTrait.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/EventEmitter.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/Loop/Loop.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/Loop/functions.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/Promise.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/Promise/functions.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/Version.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/WildcardEmitter.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/WildcardEmitterTrait.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/lib/coroutine.php create mode 100644 plugins/panakour/backup/vendor/sabre/event/phpstan.neon create mode 100644 plugins/panakour/backup/vendor/sabre/http/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/sabre/http/LICENSE create mode 100644 plugins/panakour/backup/vendor/sabre/http/README.md create mode 100644 plugins/panakour/backup/vendor/sabre/http/composer.json create mode 100644 plugins/panakour/backup/vendor/sabre/http/examples/asyncclient.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/examples/basicauth.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/examples/client.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/examples/digestauth.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/examples/reverseproxy.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/examples/stringify.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Auth/AWS.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Auth/AbstractAuth.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Auth/Basic.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Auth/Bearer.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Auth/Digest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Client.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/ClientException.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/ClientHttpException.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/HttpException.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Message.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/MessageDecoratorTrait.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/MessageInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Request.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/RequestDecorator.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/RequestInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Response.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/ResponseDecorator.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/ResponseInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Sapi.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/Version.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/lib/functions.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/phpstan.neon create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ClientTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/FunctionsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/MessageTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/NegotiateTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/RequestTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ResponseTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/SapiTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/HTTP/URLUtilTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/bootstrap.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/phpcs/ruleset.xml create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/phpunit.xml create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/www/bar.php create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/www/foo create mode 100644 plugins/panakour/backup/vendor/sabre/http/tests/www/large.php create mode 100644 plugins/panakour/backup/vendor/sabre/uri/LICENSE create mode 100644 plugins/panakour/backup/vendor/sabre/uri/composer.json create mode 100644 plugins/panakour/backup/vendor/sabre/uri/lib/InvalidUriException.php create mode 100644 plugins/panakour/backup/vendor/sabre/uri/lib/Version.php create mode 100644 plugins/panakour/backup/vendor/sabre/uri/lib/functions.php create mode 100644 plugins/panakour/backup/vendor/sabre/uri/phpstan.neon create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/LICENSE create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/README.md create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/bench.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/bench_freebusygenerator.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/bench_manipulatevcard.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/fetch_windows_zones.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/generate_vcards create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/generateicalendardata.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/mergeduplicates.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/rrulebench.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/bin/vobject create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/composer.json create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Cli.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/Available.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VAlarm.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VAvailability.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VCalendar.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VCard.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VEvent.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VFreeBusy.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VJournal.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VTimeZone.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VTodo.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/DateTimeParser.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Document.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/ElementList.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/EofException.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/FreeBusyData.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/FreeBusyGenerator.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/Broker.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/ITipException.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/Message.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/InvalidDataException.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Node.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/PHPUnitAssertions.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Parameter.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/ParseException.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/Json.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/MimeDir.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/Parser.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/XML.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Binary.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Boolean.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/FlatText.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/FloatValue.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Date.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/DateTime.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Period.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/IntegerValue.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Text.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Time.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Unknown.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Uri.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/UtcOffset.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/Date.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/DateTime.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/LanguageTag.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Reader.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/EventIterator.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/NoInstancesException.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/RDateIterator.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/RRuleIterator.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Settings.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/ICalendar.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/VCard.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/StringUtil.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/TimeZoneUtil.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/UUIDUtil.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/VCardConverter.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Version.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/Writer.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/exchangezones.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/lotuszones.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/php-bc.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/php-workaround.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/windowszones.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/phpstan.neon create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/resources/schema/xcal.rng create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/resources/schema/xcard.rng create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/AttachIssueTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/BirthdayCalendarGeneratorTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/CliTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/AvailableTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VAlarmTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VAvailabilityTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VCalendarTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VCardTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VEventTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VFreeBusyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VJournalTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VTimeZoneTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VTodoTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ComponentTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/DateTimeParserTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/DocumentTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ElementListTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmClientTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmptyParameterTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmptyValueIssueTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/FreeBusyDataTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/FreeBusyGeneratorTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/GoogleColonEscapingTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ICalendar/AttachParseTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerAttendeeReplyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerDeleteEventTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerNewEventTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerProcessMessageTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerProcessReplyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerSignificantChangesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerTester.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerTimezoneInParseEventInfoWithoutMasterTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerUpdateEventTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/EvolutionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/MessageTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue153Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue259Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue36WorkAroundTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue40Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue64Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue96Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/IssueUndefinedIndexTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/JCalTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/JCardTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/LineFoldingIssueTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ParameterTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/JsonTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/MimeDirTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/QuotedPrintableTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/XmlTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/BinaryTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/BooleanTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/CompoundTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/FloatTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/CalAddressTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/DateTimeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/DurationTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/RecurTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/TextTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/UriTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/DateAndOrTimeTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/LanguageTagTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/PhoneNumberTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/PropertyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ReaderTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/BySetPosHangTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/FifthTuesdayProblemTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue26Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue48Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue50Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MainTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MaxInstancesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MissingOverriddenTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/NoInstancesTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/SameDateForRecurringEventsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/RDateIteratorTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/RRuleIteratorTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/RecurrenceIterator/UntilRespectsTimezoneTest.ics create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/SlashRTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Splitter/ICalendarTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Splitter/VCardTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/StringUtilTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/TimeZoneUtilTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/UUIDUtilTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VCard21Test.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VCardConverterTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VersionTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/WriterTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/issue153.vcf create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/issue64.vcf create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/bootstrap.php create mode 100644 plugins/panakour/backup/vendor/sabre/vobject/tests/phpunit.xml create mode 100644 plugins/panakour/backup/vendor/sabre/xml/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/sabre/xml/LICENSE create mode 100644 plugins/panakour/backup/vendor/sabre/xml/README.md create mode 100644 plugins/panakour/backup/vendor/sabre/xml/composer.json create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/ContextStackTrait.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Deserializer/functions.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Element.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Element/Base.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Element/Cdata.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Element/Elements.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Element/KeyValue.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Element/Uri.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Element/XmlFragment.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/LibXMLException.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/ParseException.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Reader.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Serializer/functions.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Service.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Version.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/Writer.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/XmlDeserializable.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/lib/XmlSerializable.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/phpstan.neon create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ContextStackTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/EnumTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/FunctionCallerTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/KeyValueTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/MixedContentTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/RepeatingElementsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/ValueObjectTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/CDataTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/Eater.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/ElementsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/KeyValueTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/Mock.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/UriTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/XmlFragmentTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/InfiteLoopTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ReaderTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Serializer/EnumTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Serializer/RepeatingElementsTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ServiceTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/WriterTest.php create mode 100644 plugins/panakour/backup/vendor/sabre/xml/tests/phpunit.xml create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/CONTRIBUTING.md create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/LICENSE.md create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/README.md create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/composer.json create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Compressors/Compressor.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Compressors/GzipCompressor.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/MongoDb.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/MySql.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/PostgreSql.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/Sqlite.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/DbDumper.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Exceptions/CannotSetParameter.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Exceptions/CannotStartDump.php create mode 100644 plugins/panakour/backup/vendor/spatie/db-dumper/src/Exceptions/DumpFailed.php create mode 100644 plugins/panakour/backup/vendor/spatie/dropbox-api/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/spatie/dropbox-api/CONTRIBUTING.md create mode 100644 plugins/panakour/backup/vendor/spatie/dropbox-api/LICENSE.md create mode 100644 plugins/panakour/backup/vendor/spatie/dropbox-api/README.md create mode 100644 plugins/panakour/backup/vendor/spatie/dropbox-api/composer.json create mode 100644 plugins/panakour/backup/vendor/spatie/dropbox-api/src/Client.php create mode 100644 plugins/panakour/backup/vendor/spatie/dropbox-api/src/Exceptions/BadRequest.php create mode 100644 plugins/panakour/backup/vendor/spatie/dropbox-api/src/UploadSessionCursor.php create mode 100644 plugins/panakour/backup/vendor/spatie/flysystem-dropbox/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/spatie/flysystem-dropbox/CONTRIBUTING.md create mode 100644 plugins/panakour/backup/vendor/spatie/flysystem-dropbox/LICENSE.md create mode 100644 plugins/panakour/backup/vendor/spatie/flysystem-dropbox/README.md create mode 100644 plugins/panakour/backup/vendor/spatie/flysystem-dropbox/composer.json create mode 100644 plugins/panakour/backup/vendor/spatie/flysystem-dropbox/src/DropboxAdapter.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/CONTRIBUTING.md create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/LICENSE.md create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/README.md create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/UPGRADING.md create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/composer.json create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/config/backup.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ar/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/da/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/de/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/en/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/es/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/fa/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/fr/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/hi/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/id/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/it/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/pl/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/pt-BR/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ro/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ru/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/tr/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/uk/notifications.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/Backup.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupCollection.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupDestination.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupDestinationFactory.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupServiceProvider.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/BackupCommand.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/BaseCommand.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/CleanupCommand.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/ListCommand.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/MonitorCommand.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupHasFailed.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupManifestWasCreated.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupWasSuccessful.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupZipWasCreated.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/CleanupHasFailed.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/CleanupWasSuccessful.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/HealthyBackupWasFound.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/UnhealthyBackupWasFound.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/CannotCreateDbDumper.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/InvalidBackupDestination.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/InvalidBackupJob.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/InvalidCommand.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/InvalidConfiguration.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/NotificationCouldNotBeSent.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/ConsoleOutput.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/File.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/Format.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/functions.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/BaseNotification.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/EventHandler.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifiable.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/BackupHasFailed.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/BackupWasSuccessful.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/CleanupHasFailed.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/CleanupWasSuccessful.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/HealthyBackupWasFound.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/UnhealthyBackupWasFound.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/BackupJob.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/BackupJobFactory.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/DbDumperFactory.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/FileSelection.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/Manifest.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/Zip.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/CleanupJob.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/CleanupStrategy.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/Period.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/Strategies/DefaultStrategy.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Monitor/BackupDestinationStatus.php create mode 100644 plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Monitor/BackupDestinationStatusFactory.php create mode 100644 plugins/panakour/backup/vendor/spatie/temporary-directory/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/spatie/temporary-directory/CONTRIBUTING.md create mode 100644 plugins/panakour/backup/vendor/spatie/temporary-directory/LICENSE.md create mode 100644 plugins/panakour/backup/vendor/spatie/temporary-directory/README.md create mode 100644 plugins/panakour/backup/vendor/spatie/temporary-directory/composer.json create mode 100644 plugins/panakour/backup/vendor/spatie/temporary-directory/src/TemporaryDirectory.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Comparator/Comparator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Comparator/DateComparator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Comparator/NumberComparator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Exception/AccessDeniedException.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Exception/DirectoryNotFoundException.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Finder.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Gitignore.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Glob.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/CustomFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/DateRangeFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/FileTypeFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/FilecontentFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/FilenameFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/PathFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/SizeRangeFilterIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/Iterator/SortableIterator.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/LICENSE create mode 100644 plugins/panakour/backup/vendor/symfony/finder/README.md create mode 100644 plugins/panakour/backup/vendor/symfony/finder/SplFileInfo.php create mode 100644 plugins/panakour/backup/vendor/symfony/finder/composer.json create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/Idn.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/LICENSE create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/README.md create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/bootstrap.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/composer.json create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-mbstring/LICENSE create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Mbstring.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-mbstring/README.md create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-mbstring/bootstrap.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-mbstring/composer.json create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-php72/LICENSE create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-php72/Php72.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-php72/README.md create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-php72/bootstrap.php create mode 100644 plugins/panakour/backup/vendor/symfony/polyfill-php72/composer.json create mode 100644 plugins/panakour/backup/vendor/symfony/process/CHANGELOG.md create mode 100644 plugins/panakour/backup/vendor/symfony/process/Exception/ExceptionInterface.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Exception/InvalidArgumentException.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Exception/LogicException.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Exception/ProcessFailedException.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Exception/ProcessTimedOutException.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Exception/RuntimeException.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/ExecutableFinder.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/InputStream.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/LICENSE create mode 100644 plugins/panakour/backup/vendor/symfony/process/PhpExecutableFinder.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/PhpProcess.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Pipes/AbstractPipes.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Pipes/PipesInterface.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Pipes/UnixPipes.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Pipes/WindowsPipes.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Process.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/ProcessBuilder.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/ProcessUtils.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/README.md create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/ErrorProcessInitiator.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/ExecutableFinderTest.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/NonStopableProcess.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/PhpExecutableFinderTest.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/PhpProcessTest.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/ProcessBuilderTest.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/ProcessFailedExceptionTest.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/ProcessTest.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/ProcessUtilsTest.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/Tests/SignalListener.php create mode 100644 plugins/panakour/backup/vendor/symfony/process/composer.json create mode 100644 plugins/panakour/backup/vendor/symfony/process/phpunit.xml.dist diff --git a/plugins/panakour/backup/Dropbox.php b/plugins/panakour/backup/Dropbox.php new file mode 100644 index 0000000..2edc8f8 --- /dev/null +++ b/plugins/panakour/backup/Dropbox.php @@ -0,0 +1,40 @@ +client = new Client($accessToken); + $this->adapter = new DropboxAdapter($this->client); + $this->fileSystem = new Filesystem($this->adapter); + if (isset($this->fileSystem->listContents()[0])) { + $this->path = $this->fileSystem->listContents()[0]['path']; + } + } + + public function getBackups() + { + return $this->fileSystem->listContents($this->path); + } + + public function downloadBackup($baseName) + { + $file = $this->adapter->read($this->path.'/'.$baseName)['contents']; + header("Content-Type: application/zip"); + echo $file; + } +} diff --git a/plugins/panakour/backup/DropboxServiceProvider.php b/plugins/panakour/backup/DropboxServiceProvider.php new file mode 100644 index 0000000..65a2198 --- /dev/null +++ b/plugins/panakour/backup/DropboxServiceProvider.php @@ -0,0 +1,34 @@ + 'Backup', + 'description' => 'Backup files and database of October CMS', + 'author' => 'Panagiotis Koursaris', + 'icon' => 'icon-floppy-o', + 'homepage' => 'https://github.com/panakour/oc-backup-plugin', + ]; + } + + public function registerNavigation() + { + return [ + 'backup' => [ + 'label' => 'Backup', + 'url' => Backend::url('panakour/backup/backups'), + 'icon' => 'icon-floppy-o', + 'iconSvg' => 'plugins/panakour/backup/assets/images/backup-icon.svg', + 'order' => 200, + 'permissions' => ['panakour.backup.access'], + ], + ]; + } + + public function registerSettings() + { + return [ + 'config' => [ + 'label' => 'Backup', + 'icon' => 'icon-floppy-o', + 'description' => 'Configure the backup system.', + 'category' => SettingsManager::CATEGORY_SYSTEM, + 'class' => Settings::class, + 'order' => 600, + 'permissions' => ['panakour.backup.access'], + ], + ]; + } + + public function registerPermissions() + { + return [ + 'panakour.backup.access' => [ + 'label' => 'Manage backups', + 'tab' => 'Backup' + ], + ]; + } + + public function boot() + { + $this->bootPackages(); + } + + public function bootPackages() + { + $pluginNamespace = str_replace('\\', '.', strtolower(__NAMESPACE__)); + + $aliasLoader = AliasLoader::getInstance(); + + $packages = Config::get($pluginNamespace.'::packages'); + + foreach ($packages as $name => $options) { + if (! empty($options['config']) && ! empty($options['config_namespace'])) { + Config::set($options['config_namespace'], $options['config']); + } + + if (! empty($options['providers'])) { + foreach ($options['providers'] as $provider) { + App::register($provider); + } + } + + if (! empty($options['aliases'])) { + foreach ($options['aliases'] as $alias => $path) { + $aliasLoader->alias($alias, $path); + } + } + } + } +} diff --git a/plugins/panakour/backup/README.md b/plugins/panakour/backup/README.md new file mode 100644 index 0000000..c1fdd0d --- /dev/null +++ b/plugins/panakour/backup/README.md @@ -0,0 +1,104 @@ +# Backup system for October CMS +- [Overview](#introduction) +- [Requirements](#requirements) +- [Features](#features) +- [Usage](#usage) +- [Storage](#storage) +- [Dumping the database](#dumping-db) + +## Introduction +This plugin let you create backups of your files and databases. It uses the amazing laravel package [spatie/laravel-backup](https://github.com/spatie/laravel-backup). + + +## Requirements +This backup package requires **PHP 7.1 or higher** with the [ZIP module](http://php.net/manual/en/book.zip.php) and **Laravel 5.5 or higher**. It's not compatible with Windows servers. + +The plugin needs free disk space where it can create backups. Ensure that you have **at least** as much free space as the total size of the files you want to backup. + +Make sure `mysqldump` is installed on your system if you want to backup MySQL databases. + +Make sure `pg_dump` is installed on your system if you want to backup PostgreSQL databases. + +Make sure `mongodump` is installed on your system if you want to backup Mongo databases. + + +## Features +- With just a click you can: + - Create backups of the whole application. + - Create backups of the database only. + - Create backups of the files only. +- Currently support local and dropbox storage driver. +- Support various Database Driver (MySQL, PostgreSQL, SQLite and Mongo). +- You can easily include and exclude some files using the UI. +- Support gzip to reduce the database size. + + +## Usage +1. To configure the backup system, from backend navigate to `Settings > System > Backup`. +2. To create your first backup, from backend navigate to the backup section from the top main menu. From there you can create and download your backups by click the buttons. + + +## Storage +##### Dropbox usage +The first thing you need to do is get an authorization token at Dropbox. A token can be generated in the [App Console](https://www.dropbox.com/developers/apps) for any Dropbox API app. You'll find more info at [the Dropbox Developer Blog](https://blogs.dropbox.com/developers/2014/05/generate-an-access-token-for-your-own-account/). + +Then add to the `config/filesystems.php` file the followed array with your token and app name: +```php +'disks' => [ + ... + 'dropbox' => [ + 'driver' => 'dropbox', + 'app' => 'app-name', + 'authorizationToken' => 'generated-access-token', + ] +] +``` +Be sure that you select `Dropbox` option from settings. + +##### Webdav usage + +Then add to the `config/filesystems.php` file the followed array with your token and app name: +```php +'disks' => [ + ... + 'webdav' => [ + 'driver' => 'webdav', + 'baseUri' => 'YOUR_WEBDAV_DOMAIN', + 'path_prefix' => '/remote.php/dav/files/USERNAME/', //for nextcloud + 'path_alias' => '', + 'userName' => 'USER', + 'password' => 'PASSWORD', + ], +] +``` +Be sure that you select `Webdav` option from settings. + + +## Dumping the database +`mysqldump` and `pg_dump` are used to dump the database. If they are not installed in a default location, you can add a key named `dump.dump_binary_path` in October's own `database.php` config file. **Only fill in the path to the binary**. Do not include the name of the binary itself. + +If your database dump takes a long time, you might exceed the default timeout of 60 seconds. You can set a higher (or lower) limit by providing a `dump.timeout` config key which specifies, in seconds, how long the command may run. + +Here's an example for MySQL: + +```php +//config/database.php +'connections' => [ + 'mysql' => [ + 'driver' => 'mysql' + ..., + 'dump' => [ + 'dump_binary_path' => '/path/to/the/binary', // only the path, so without `mysqldump` or `pg_dump` + 'use_single_transaction', + 'timeout' => 60 * 5, // 5 minute timeout + 'exclude_tables' => ['table1', 'table2'], + 'add_extra_option' => '--optionname=optionvalue', + ] + ], +``` + + +Also you can create backups from the command line using `Artisan`. +![image](https://raw.githubusercontent.com/panakour/oc-backup-plugin/master/docs/images/oc_backups.png) +![image](https://raw.githubusercontent.com/panakour/oc-backup-plugin/master/docs/images/oc_backup_config.png) +![image](https://raw.githubusercontent.com/panakour/oc-backup-plugin/master/docs/images/oc_backup_config_1.png) diff --git a/plugins/panakour/backup/Repository.php b/plugins/panakour/backup/Repository.php new file mode 100644 index 0000000..ee5327d --- /dev/null +++ b/plugins/panakour/backup/Repository.php @@ -0,0 +1,89 @@ +getLocalBackups(), $this->getWebdavBackups(), $this->getDropBoxBackups()); + } + + public function getLocalBackups() + { + $backups = []; + $localBackupFiles = array_values(array_diff(scandir(Settings::getBackupsPath()), ['.', '..'])); + foreach ($localBackupFiles as $index => $file) { + $backups[$index]['storage'] = 'Local'; + $backups[$index]['fileInfo'] = pathinfo(Settings::getBackupsPath().'/'.$file); + $backups[$index]['size'] = ceil(filesize(Settings::getBackupsPath().'/'.$file) / 1024); + $backups[$index]['lastModified'] = date('d.m.Y', filemtime(Settings::getBackupsPath().'/'.$file)); + } + + return $backups; + } + + public function getWebdavBackups() + { + if (Config::get('filesystems.disks.webdav') === null) { + return []; + } + $backups = []; + $path = "/panakour-backup"; + $webdavBackupFiles = Storage::disk('webdav')->files($path); + foreach ($webdavBackupFiles as $index => $file) { + + //$size = ceil(Storage::disk('webdav')->size($file)/1024); + //$lastModified = date('d.m.Y', Storage::disk('webdav')->lastModified($file)); + + $size = 0; + $lastModified = "00.00.0000"; + + $backups[$index]['storage'] = 'Webdav'; + $backups[$index]['fileInfo']['basename'] = basename($file); + $backups[$index]['fileInfo']['path'] = $file; + $backups[$index]['size'] = $size; + $backups[$index]['lastModified'] = $lastModified; + } + + return $backups; + } + + /** + * Will be removed in the future. + */ + public function getLocalBackupsInTheOldPath() + { + $backups = []; + $path = storage_path('app/panakour-backup'); + if (File::exists($path)) { + $localBackupFiles = array_values(array_diff(scandir($path), ['.', '..'])); + foreach ($localBackupFiles as $index => $file) { + $backups[$index]['storage'] = 'Local'; + $backups[$index]['fileInfo'] = pathinfo($path.'/'.$file); + $backups[$index]['size'] = ceil(filesize($path.'/'.$file) / 1024); + $backups[$index]['lastModified'] = date('d.m.Y', filemtime($path.'/'.$file)); + } + } + + return $backups; + } + + public function getDropboxBackups() + { + $backups = []; + $dropboxBackupFiles = (new Dropbox())->getBackups(); + foreach ($dropboxBackupFiles as $index => $file) { + $backups[$index]['storage'] = 'Dropbox'; + $backups[$index]['fileInfo'] = $file; + $backups[$index]['size'] = ceil($file['size'] / 1024); + $backups[$index]['lastModified'] = date('d.m.Y', ($file['timestamp'])); + } + + return $backups; + } +} diff --git a/plugins/panakour/backup/WebdavServiceProvider.php b/plugins/panakour/backup/WebdavServiceProvider.php new file mode 100644 index 0000000..4f284e3 --- /dev/null +++ b/plugins/panakour/backup/WebdavServiceProvider.php @@ -0,0 +1,79 @@ +fsConfig = $fsConfig; + parent::__construct($client); + } + + public function getUrl($path) + { + if (!empty($this->fsConfig['path_alias'])) { + // with this feature you can use symlink to folder + return $this->fsConfig['baseUri'] . $this->fsConfig['path_alias'] . $path; + } else { + return $this->fsConfig['baseUri'] . $this->fsConfig['path_prefix'] . $path; + } + } + + public function deleteDir($dirname) + { + $location = $this->applyPathPrefix($this->encodePath($dirname)); + + try { + $this->client->request('DELETE', $location . '/'); + + return true; + } catch (NotFound $e) { + return false; + } + } +} + +class WebdavServiceProvider extends ServiceProvider +{ + /** + * Perform post-registration booting of services. + * + * @return void + */ + public function boot() + { + Storage::extend('webdav', function ($app, $config) { + $client = new Client($config); + $adapter = new WebDAVAdapterExt($client, $config); + if (!empty($config['path_prefix'])) { + $adapter->setPathPrefix($config['path_prefix']); + } + + return new Filesystem($adapter); + }); + } + + /** + * Register bindings in the container. + * + * @return void + */ + public function register() + { + // + } +} diff --git a/plugins/panakour/backup/assets/css/main.css b/plugins/panakour/backup/assets/css/main.css new file mode 100644 index 0000000..61c6eeb --- /dev/null +++ b/plugins/panakour/backup/assets/css/main.css @@ -0,0 +1,5 @@ +.loading-indicator-container { + display: none; + position: relative; + min-height: 17px; +} \ No newline at end of file diff --git a/plugins/panakour/backup/assets/images/backup-icon.svg b/plugins/panakour/backup/assets/images/backup-icon.svg new file mode 100644 index 0000000..c3d2c3c --- /dev/null +++ b/plugins/panakour/backup/assets/images/backup-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/panakour/backup/assets/js/backups-page.js b/plugins/panakour/backup/assets/js/backups-page.js new file mode 100644 index 0000000..cdbc203 --- /dev/null +++ b/plugins/panakour/backup/assets/js/backups-page.js @@ -0,0 +1,10 @@ +$(document).ready(function () { + + var $indicator = $('.loading-indicator-container'); + + $('.create-backup').on('click', function () { + $indicator.css('display', 'inline-block'); + }); + +}); + diff --git a/plugins/panakour/backup/composer.json b/plugins/panakour/backup/composer.json new file mode 100644 index 0000000..08e8c8c --- /dev/null +++ b/plugins/panakour/backup/composer.json @@ -0,0 +1,8 @@ +{ + "name": "panakour/backup-plugin", + "type": "october-plugin", + "description": "None", + "require": { + "composer/installers": "~1.0" + } +} \ No newline at end of file diff --git a/plugins/panakour/backup/config/config.php b/plugins/panakour/backup/config/config.php new file mode 100644 index 0000000..9ccfdc4 --- /dev/null +++ b/plugins/panakour/backup/config/config.php @@ -0,0 +1,141 @@ + [ + 'spatie/laravel-backup' => [ + 'providers' => [ + \Spatie\Backup\BackupServiceProvider::class, + \PanaKour\Backup\DropboxServiceProvider::class, + \PanaKour\Backup\WebdavServiceProvider::class, + ], + + 'config_namespace' => 'backup', + + 'config' => [ + 'backup' => [ + 'name' => 'panakour-backup', + 'source' => [ + 'files' => [ + + /* + * The list of directories and files that will be included in the backup. + */ + 'include' => \Panakour\Backup\Models\Settings::getIncludedFiles(), + + /* + * These directories and files will be excluded from the backup. + * + * Directories used by the backup process will automatically be excluded. + */ + 'exclude' => \Panakour\Backup\Models\Settings::getExcludedFiles(), + + /* + * Determines if symlinks should be followed. + */ + 'followLinks' => false, + ], + + /* + * The names of the connections to the databases that should be backed up + * MySQL, PostgreSQL, SQLite and Mongo databases are supported. + */ + 'databases' => [ + \Panakour\Backup\Models\Settings::getDatabaseDriver(), + ], + ], + + /* + * The database dump can be gzipped to decrease diskspace usage. + */ + 'gzip_database_dump' => \Panakour\Backup\Models\Settings::isGzipEnabled(), + + 'destination' => [ + + /* + * The filename prefix used for the backup zip file. + */ + 'filename_prefix' => \Panakour\Backup\Models\Settings::getFileNamePrefix(), + + /* + * The disk names on which the backups will be stored. + */ + 'disks' => [ + \Panakour\Backup\Models\Settings::getStorage(), + ], + ], + ], + + /* + * Here you can specify which backups should be monitored. + * If a backup does not meet the specified requirements the + * UnHealthyBackupWasFound event will be fired. + */ + 'monitorBackups' => [ + [ + 'name' => 'panakour-backup', + 'disks' => [\Panakour\Backup\Models\Settings::getStorage()], + 'newestBackupsShouldNotBeOlderThanDays' => 1, + 'storageUsedMayNotBeHigherThanMegabytes' => 5000, + ], + + /* + [ + 'name' => 'name of the second app', + 'disks' => ['local', 's3'], + 'newestBackupsShouldNotBeOlderThanDays' => 1, + 'storageUsedMayNotBeHigherThanMegabytes' => 5000, + ], + */ + ], + + 'cleanup' => [ + /* + * The strategy that will be used to cleanup old backups. The default strategy + * will keep all backups for a certain amount of days. After that period only + * a daily backup will be kept. After that period only weekly backups will + * be kept and so on. + * + * No matter how you configure it the default strategy will never + * delete the newest backup. + */ + 'strategy' => \Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy::class, + + 'defaultStrategy' => [ + + /* + * The number of days for which backups must be kept. + */ + 'keepAllBackupsForDays' => 7, + + /* + * The number of days for which daily backups must be kept. + */ + 'keepDailyBackupsForDays' => 16, + + /* + * The number of weeks for which one weekly backup must be kept. + */ + 'keepWeeklyBackupsForWeeks' => 8, + + /* + * The number of months for which one monthly backup must be kept. + */ + 'keepMonthlyBackupsForMonths' => 4, + + /* + * The number of years for which one yearly backup must be kept. + */ + 'keepYearlyBackupsForYears' => 2, + + /* + * After cleaning up the backups remove the oldest backup until + * this amount of megabytes has been reached. + */ + 'deleteOldestBackupsWhenUsingMoreMegabytesThan' => 5000, + ], + ], + ], + ], + ], +]; diff --git a/plugins/panakour/backup/controllers/Backups.php b/plugins/panakour/backup/controllers/Backups.php new file mode 100644 index 0000000..6b7f614 --- /dev/null +++ b/plugins/panakour/backup/controllers/Backups.php @@ -0,0 +1,84 @@ +repo = $repository; + BackendMenu::setContext('PanaKour.Backup', 'backup', 'backups'); + } + + public function index() + { + $this->addJs('/plugins/panakour/backup/assets/js/backups-page.js'); + $this->addCss('/plugins/panakour/backup/assets/css/main.css'); + $this->vars['backupFiles'] = $this->repo->getAll(); + $this->vars['oldPathBackupFiles'] = $this->repo->getLocalBackupsInTheOldPath(); + } + + public function createBackup($artisanArguments) + { + set_time_limit(Settings::getMaximumExecutionTime()); + Config::set('filesystems.disks.local.root', storage_path(Settings::UPLOAD_PATH)); + Artisan::call('backup:run', $artisanArguments); + Flash::success('Backup has been created.'); + + return Redirect::to(Backend::url('panakour/backup/backups')); + } + + public function onCreateBackup() + { + return $this->createBackup(['--disable-notifications' => true]); + } + + public function onCreateDatabaseBackup() + { + return $this->createBackup(['--disable-notifications' => true, '--only-db' => true]); + } + + public function onCreateFilesBackup() + { + return $this->createBackup(['--disable-notifications' => true, '--only-files' => true]); + } + + public function downloadDropboxBackup($baseName) + { + (new Dropbox())->downloadBackup($baseName); + } + + public function downloadWebdavBackup($baseName) + { + $path = "panakour-backup"."/".$baseName; + return Storage::disk('webdav')->get($path); + } + + public function onCreateWholeProjectBackup() + { + config([ + "backup.backup.source.files.include" => base_path(), + "backup.backup.source.files.exclude" => [], + + ]); + return $this->createBackup(['--disable-notifications' => true, '--filename' => "whole_project_backup.zip"]); + } +} diff --git a/plugins/panakour/backup/controllers/backups/_list.htm b/plugins/panakour/backup/controllers/backups/_list.htm new file mode 100644 index 0000000..98b456e --- /dev/null +++ b/plugins/panakour/backup/controllers/backups/_list.htm @@ -0,0 +1,77 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameStorageSizeLast ModifiedAction
KB
KB
+
diff --git a/plugins/panakour/backup/controllers/backups/_toolbar.htm b/plugins/panakour/backup/controllers/backups/_toolbar.htm new file mode 100644 index 0000000..190e5bb --- /dev/null +++ b/plugins/panakour/backup/controllers/backups/_toolbar.htm @@ -0,0 +1,49 @@ +
+
+
+ + + + + + + + + +
+
+ +
Getting Backup...
+
+
+ + + Settings + + +
+
+
+ diff --git a/plugins/panakour/backup/controllers/backups/index.htm b/plugins/panakour/backup/controllers/backups/index.htm new file mode 100644 index 0000000..e91d61c --- /dev/null +++ b/plugins/panakour/backup/controllers/backups/index.htm @@ -0,0 +1,2 @@ +makePartial('toolbar') ?> +makePartial('list') ?> \ No newline at end of file diff --git a/plugins/panakour/backup/docs/images/oc_backup_config.png b/plugins/panakour/backup/docs/images/oc_backup_config.png new file mode 100644 index 0000000000000000000000000000000000000000..38ef9f2623d2fa96f3f2ebc23038595b61cc4cbe GIT binary patch literal 164471 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.OBF(^F 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

_y6d@Aq)~>$G`yet9cjRKvyWwdSM5c_ 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

Laflr4)}H9+i~$ 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

cD-H~Lw;0=c`#XffIW-DI_nV`@ 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 literal 0 HcmV?d00001 diff --git a/plugins/panakour/backup/docs/images/oc_backups.png b/plugins/panakour/backup/docs/images/oc_backups.png new file mode 100644 index 0000000000000000000000000000000000000000..b89f0389a7ecf807f02fe57964f486fc71a17996 GIT binary patch literal 66782 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%0AOy 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

9) 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 literal 0 HcmV?d00001 diff --git a/plugins/panakour/backup/models/Settings.php b/plugins/panakour/backup/models/Settings.php new file mode 100644 index 0000000..531ea41 --- /dev/null +++ b/plugins/panakour/backup/models/Settings.php @@ -0,0 +1,97 @@ + 'themes'], + ['path' => 'plugins'], + ]; + $pathsToExclude = [ + ['path' => 'vendor'], + ['path' => 'plugins/rainlab'], + ]; + Settings::set('include_files', $pathsToInclude); + Settings::set('exclude_files', $pathsToExclude); + Settings::set('maximum_execution_time', 30); + } +} diff --git a/plugins/panakour/backup/updates/version.yaml b/plugins/panakour/backup/updates/version.yaml new file mode 100644 index 0000000..79a6928 --- /dev/null +++ b/plugins/panakour/backup/updates/version.yaml @@ -0,0 +1,13 @@ +1.0.1: + - First version of backup plugin. + - seed_default_settings.php +1.0.2: Fixes bug when no backup folder exist +1.0.3: Settings button directly in toolbar of backup list. Fixed monitored backups name +1.0.4: Support Dropbox storage driver +1.0.5: Support Dropbox storage driver (Fix if not configured yet) +1.0.6: Change the backups path to fix 404 error on apache server because of the default .htaccess +1.0.7: Display backup list even if exist only in the old path +1.0.8: Add permissions. +1.0.9: Add option to create backup of whole project independently of settings +1.0.10: Added webdav driver (thanks to @AdrienAdB ) +1.0.11: Fix symfony/process dependency diff --git a/plugins/panakour/backup/vendor/autoload.php b/plugins/panakour/backup/vendor/autoload.php new file mode 100644 index 0000000..892d6a4 --- /dev/null +++ b/plugins/panakour/backup/vendor/autoload.php @@ -0,0 +1,7 @@ + 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/plugins/panakour/backup/vendor/bin/naturalselection b/plugins/panakour/backup/vendor/bin/naturalselection new file mode 100644 index 0000000..7e20439 --- /dev/null +++ b/plugins/panakour/backup/vendor/bin/naturalselection @@ -0,0 +1,140 @@ +#!/usr/bin/env python + +# +# Copyright (c) 2009-2010 Evert Pot +# All rights reserved. +# http://www.rooftopsolutions.nl/ +# +# This utility is distributed along with SabreDAV +# license: http://sabre.io/license/ Modified BSD License + +import os +from optparse import OptionParser +import time + +def getfreespace(path): + stat = os.statvfs(path) + return stat.f_frsize * stat.f_bavail + +def getbytesleft(path,threshold): + return getfreespace(path)-threshold + +def run(cacheDir, threshold, sleep=5, simulate=False, min_erase = 0): + + bytes = getbytesleft(cacheDir,threshold) + if (bytes>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/plugins/panakour/backup/vendor/bin/sabredav b/plugins/panakour/backup/vendor/bin/sabredav new file mode 100644 index 0000000..032371b --- /dev/null +++ b/plugins/panakour/backup/vendor/bin/sabredav @@ -0,0 +1,2 @@ +#!/bin/sh +php -S 0.0.0.0:8080 `dirname $0`/sabredav.php diff --git a/plugins/panakour/backup/vendor/bin/vobject b/plugins/panakour/backup/vendor/bin/vobject new file mode 100644 index 0000000..2aca7e7 --- /dev/null +++ b/plugins/panakour/backup/vendor/bin/vobject @@ -0,0 +1,27 @@ +#!/usr/bin/env php +main($argv)); + diff --git a/plugins/panakour/backup/vendor/composer/ClassLoader.php b/plugins/panakour/backup/vendor/composer/ClassLoader.php new file mode 100644 index 0000000..fce8549 --- /dev/null +++ b/plugins/panakour/backup/vendor/composer/ClassLoader.php @@ -0,0 +1,445 @@ + + * 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') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + 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])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/plugins/panakour/backup/vendor/composer/LICENSE b/plugins/panakour/backup/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/plugins/panakour/backup/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/plugins/panakour/backup/vendor/composer/autoload_classmap.php b/plugins/panakour/backup/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..7a91153 --- /dev/null +++ b/plugins/panakour/backup/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + $vendorDir . '/sabre/uri/lib/functions.php', + '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php', + 'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', + 'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php', + 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', + '2b9d0f43f9552984cfa82fee95491826' => $vendorDir . '/sabre/event/lib/coroutine.php', + 'd81bab31d3feb45bfe2f283ea3c8fdf7' => $vendorDir . '/sabre/event/lib/Loop/functions.php', + 'a1cce3d26cc15c00fcd0b3354bd72c88' => $vendorDir . '/sabre/event/lib/Promise/functions.php', + '3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php', + '93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php', + 'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php', + '0c3c22e27afa83be19b4c938f4c6e9ea' => $vendorDir . '/spatie/laravel-backup/src/Helpers/functions.php', +); diff --git a/plugins/panakour/backup/vendor/composer/autoload_namespaces.php b/plugins/panakour/backup/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..b7fc012 --- /dev/null +++ b/plugins/panakour/backup/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/symfony/polyfill-php72'), + 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), + 'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'), + 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), + 'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'), + 'Spatie\\TemporaryDirectory\\' => array($vendorDir . '/spatie/temporary-directory/src'), + 'Spatie\\FlysystemDropbox\\' => array($vendorDir . '/spatie/flysystem-dropbox/src'), + 'Spatie\\Dropbox\\' => array($vendorDir . '/spatie/dropbox-api/src'), + 'Spatie\\DbDumper\\' => array($vendorDir . '/spatie/db-dumper/src'), + 'Spatie\\Backup\\' => array($vendorDir . '/spatie/laravel-backup/src'), + 'Sabre\\Xml\\' => 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'), + 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), + 'League\\Flysystem\\WebDAV\\' => array($vendorDir . '/league/flysystem-webdav/src'), + 'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'), + 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), + 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), + 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), + 'GrahamCampbell\\GuzzleFactory\\' => array($vendorDir . '/graham-campbell/guzzle-factory/src'), +); diff --git a/plugins/panakour/backup/vendor/composer/autoload_real.php b/plugins/panakour/backup/vendor/composer/autoload_real.php new file mode 100644 index 0000000..d733de8 --- /dev/null +++ b/plugins/panakour/backup/vendor/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\ComposerStaticInitbe9348664e724b21a091b7dc2c806f56::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\ComposerStaticInitbe9348664e724b21a091b7dc2c806f56::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequirebe9348664e724b21a091b7dc2c806f56($fileIdentifier, $file); + } + + return $loader; + } +} + +function composerRequirebe9348664e724b21a091b7dc2c806f56($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/plugins/panakour/backup/vendor/composer/autoload_static.php b/plugins/panakour/backup/vendor/composer/autoload_static.php new file mode 100644 index 0000000..56c7c01 --- /dev/null +++ b/plugins/panakour/backup/vendor/composer/autoload_static.php @@ -0,0 +1,188 @@ + __DIR__ . '/..' . '/sabre/uri/lib/functions.php', + '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php', + 'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', + 'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php', + 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', + '2b9d0f43f9552984cfa82fee95491826' => __DIR__ . '/..' . '/sabre/event/lib/coroutine.php', + 'd81bab31d3feb45bfe2f283ea3c8fdf7' => __DIR__ . '/..' . '/sabre/event/lib/Loop/functions.php', + 'a1cce3d26cc15c00fcd0b3354bd72c88' => __DIR__ . '/..' . '/sabre/event/lib/Promise/functions.php', + '3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php', + '93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php', + 'ebdb698ed4152ae445614b69b5e4bb6a' => __DIR__ . '/..' . '/sabre/http/lib/functions.php', + '0c3c22e27afa83be19b4c938f4c6e9ea' => __DIR__ . '/..' . '/spatie/laravel-backup/src/Helpers/functions.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'S' => + array ( + 'Symfony\\Polyfill\\Php72\\' => 23, + 'Symfony\\Polyfill\\Mbstring\\' => 26, + 'Symfony\\Polyfill\\Intl\\Idn\\' => 26, + 'Symfony\\Component\\Process\\' => 26, + 'Symfony\\Component\\Finder\\' => 25, + 'Spatie\\TemporaryDirectory\\' => 26, + 'Spatie\\FlysystemDropbox\\' => 24, + 'Spatie\\Dropbox\\' => 15, + 'Spatie\\DbDumper\\' => 16, + 'Spatie\\Backup\\' => 14, + '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, + 'Psr\\Http\\Message\\' => 17, + ), + 'L' => + array ( + 'League\\Flysystem\\WebDAV\\' => 24, + 'League\\Flysystem\\' => 17, + ), + 'G' => + array ( + 'GuzzleHttp\\Psr7\\' => 16, + 'GuzzleHttp\\Promise\\' => 19, + 'GuzzleHttp\\' => 11, + 'GrahamCampbell\\GuzzleFactory\\' => 29, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Symfony\\Polyfill\\Php72\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php72', + ), + 'Symfony\\Polyfill\\Mbstring\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', + ), + 'Symfony\\Polyfill\\Intl\\Idn\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn', + ), + 'Symfony\\Component\\Process\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/process', + ), + 'Symfony\\Component\\Finder\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/finder', + ), + 'Spatie\\TemporaryDirectory\\' => + array ( + 0 => __DIR__ . '/..' . '/spatie/temporary-directory/src', + ), + 'Spatie\\FlysystemDropbox\\' => + array ( + 0 => __DIR__ . '/..' . '/spatie/flysystem-dropbox/src', + ), + 'Spatie\\Dropbox\\' => + array ( + 0 => __DIR__ . '/..' . '/spatie/dropbox-api/src', + ), + 'Spatie\\DbDumper\\' => + array ( + 0 => __DIR__ . '/..' . '/spatie/db-dumper/src', + ), + 'Spatie\\Backup\\' => + array ( + 0 => __DIR__ . '/..' . '/spatie/laravel-backup/src', + ), + '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', + ), + 'Psr\\Http\\Message\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-message/src', + ), + 'League\\Flysystem\\WebDAV\\' => + array ( + 0 => __DIR__ . '/..' . '/league/flysystem-webdav/src', + ), + 'League\\Flysystem\\' => + array ( + 0 => __DIR__ . '/..' . '/league/flysystem/src', + ), + 'GuzzleHttp\\Psr7\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', + ), + 'GuzzleHttp\\Promise\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/promises/src', + ), + 'GuzzleHttp\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src', + ), + 'GrahamCampbell\\GuzzleFactory\\' => + array ( + 0 => __DIR__ . '/..' . '/graham-campbell/guzzle-factory/src', + ), + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitbe9348664e724b21a091b7dc2c806f56::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitbe9348664e724b21a091b7dc2c806f56::$prefixDirsPsr4; + + }, null, ClassLoader::class); + } +} diff --git a/plugins/panakour/backup/vendor/composer/installed.json b/plugins/panakour/backup/vendor/composer/installed.json new file mode 100644 index 0000000..e362f90 --- /dev/null +++ b/plugins/panakour/backup/vendor/composer/installed.json @@ -0,0 +1,1524 @@ +[ + { + "name": "graham-campbell/guzzle-factory", + "version": "v3.0.3", + "version_normalized": "3.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Guzzle-Factory.git", + "reference": "de49ec514107b43f3a770743421c9bb78d01d3e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Guzzle-Factory/zipball/de49ec514107b43f3a770743421c9bb78d01d3e2", + "reference": "de49ec514107b43f3a770743421c9bb78d01d3e2", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.2", + "php": "^7.0" + }, + "require-dev": { + "graham-campbell/analyzer": "^2.4", + "phpunit/phpunit": "^6.5|^7.0|^8.0" + }, + "time": "2020-04-13T13:12:06+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GrahamCampbell\\GuzzleFactory\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "description": "Provides A Simple Guzzle Factory With Good Defaults", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Guzzle", + "Guzzle Factory", + "Guzzle-Factory", + "http" + ] + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.5.3", + "version_normalized": "6.5.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aab4ebd862aa7d04f01a4b51849d657db56d882e", + "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.11" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "time": "2020-04-18T10:38:46+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ] + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "version_normalized": "1.3.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "time": "2016-12-20T10:07:11+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ] + }, + { + "name": "guzzlehttp/psr7", + "version": "1.6.1", + "version_normalized": "1.6.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + }, + "time": "2019-07-01T23:21:34+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ] + }, + { + "name": "league/flysystem", + "version": "1.0.67", + "version_normalized": "1.0.67.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "5b1f36c75c4bdde981294c2a0ebdb437ee6f275e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5b1f36c75c4bdde981294c2a0ebdb437ee6f275e", + "reference": "5b1f36c75c4bdde981294c2a0ebdb437ee6f275e", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": ">=5.5.9" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7.26" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "time": "2020-04-16T13:21:26+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ] + }, + { + "name": "league/flysystem-webdav", + "version": "1.0.9", + "version_normalized": "1.0.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-webdav.git", + "reference": "b5c3c756e60cbd495173ce09c0a68a858803f4ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-webdav/zipball/b5c3c756e60cbd495173ce09c0a68a858803f4ce", + "reference": "b5c3c756e60cbd495173ce09c0a68a858803f4ce", + "shasum": "" + }, + "require": { + "league/flysystem": "~1.0", + "php": ">=5.6", + "sabre/dav": "~4.0|~3.1" + }, + "require-dev": { + "mockery/mockery": "~1.2", + "phpunit/phpunit": "~4.8" + }, + "time": "2019-12-13T22:44:03+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "League\\Flysystem\\WebDAV\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for WebDAV" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2016-08-06T14:39:51+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ] + }, + { + "name": "psr/log", + "version": "1.1.3", + "version_normalized": "1.1.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2020-03-23T09:12:05+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.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": "ralouphie/getallheaders", + "version": "3.0.3", + "version_normalized": "3.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "time": "2019-03-08T08:55:37+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders." + }, + { + "name": "sabre/dav", + "version": "4.1.0", + "version_normalized": "4.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "8f6f4d272504ee8424e1d0a47d6efc7772de2270" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/8f6f4d272504ee8424e1d0a47d6efc7772de2270", + "reference": "8f6f4d272504ee8424e1d0a47d6efc7772de2270", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0", + "psr/log": "^1.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "require-dev": { + "evert/phpdoc-md": "~0.1.0", + "friendsofphp/php-cs-fixer": "~2.16.1", + "monolog/monolog": "^1.18", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "suggest": { + "ext-curl": "*", + "ext-imap": "*", + "ext-pdo": "*" + }, + "time": "2020-03-20T08:55:46+00:00", + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "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" + ] + }, + { + "name": "sabre/event", + "version": "5.1.0", + "version_normalized": "5.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "d00a17507af0e7544cfe17096372f5d733e3b276" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/d00a17507af0e7544cfe17096372f5d733e3b276", + "reference": "d00a17507af0e7544cfe17096372f5d733e3b276", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit": "^7 || ^8" + }, + "time": "2020-01-31T18:52:29+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", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ] + }, + { + "name": "sabre/http", + "version": "5.1.0", + "version_normalized": "5.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "23446999f1f6e62892bbd89745070aa902dd3539" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/23446999f1f6e62892bbd89745070aa902dd3539", + "reference": "23446999f1f6e62892bbd89745070aa902dd3539", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "time": "2020-01-31T20:07:09+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": "sabre/uri", + "version": "2.2.0", + "version_normalized": "2.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "059d11012603be2e32ddb7543602965563ddbb09" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/059d11012603be2e32ddb7543602965563ddbb09", + "reference": "059d11012603be2e32ddb7543602965563ddbb09", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit": "^7 || ^8" + }, + "time": "2020-01-31T18:53:43+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/vobject", + "version": "4.3.0", + "version_normalized": "4.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "5b2248d965160f93053f3a24704794a13a22a1bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/5b2248d965160f93053f3a24704794a13a22a1bb", + "reference": "5b2248d965160f93053f3a24704794a13a22a1bb", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1", + "sabre/xml": "^2.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit": "^7 || ^8" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "time": "2020-01-31T18:50:58+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/xml", + "version": "2.2.0", + "version_normalized": "2.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "705f5cbf7f4fb1e3dd47173e3f026892818c8d46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/705f5cbf7f4fb1e3dd47173e3f026892818c8d46", + "reference": "705f5cbf7f4fb1e3dd47173e3f026892818c8d46", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit": "^7 || ^8" + }, + "time": "2020-01-31T18:52:58+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": "spatie/db-dumper", + "version": "2.13.1", + "version_normalized": "2.13.1.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/db-dumper.git", + "reference": "8f88e6f772ddf6c5a71ec9c0b5682ebca3323377" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/db-dumper/zipball/8f88e6f772ddf6c5a71ec9c0b5682ebca3323377", + "reference": "8f88e6f772ddf6c5a71ec9c0b5682ebca3323377", + "shasum": "" + }, + "require": { + "php": "^7.0", + "symfony/process": "^3.0|^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "time": "2019-03-01T15:46:17+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Spatie\\DbDumper\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Dump databases", + "homepage": "https://github.com/spatie/db-dumper", + "keywords": [ + "database", + "db-dumper", + "dump", + "mysqldump", + "spatie" + ] + }, + { + "name": "spatie/dropbox-api", + "version": "1.12.0", + "version_normalized": "1.12.0.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/dropbox-api.git", + "reference": "f599374697e7fefa2b0d64ca25ad7079abf91970" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/dropbox-api/zipball/f599374697e7fefa2b0d64ca25ad7079abf91970", + "reference": "f599374697e7fefa2b0d64ca25ad7079abf91970", + "shasum": "" + }, + "require": { + "graham-campbell/guzzle-factory": "^3.0", + "guzzlehttp/guzzle": "^6.2", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.15|^8.5" + }, + "time": "2020-02-04T07:32:05+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Spatie\\Dropbox\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex.vanderbist@gmail.com", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A minimal implementation of Dropbox API v2", + "homepage": "https://github.com/spatie/dropbox-api", + "keywords": [ + "Dropbox-API", + "api", + "dropbox", + "spatie", + "v2" + ] + }, + { + "name": "spatie/flysystem-dropbox", + "version": "1.2.2", + "version_normalized": "1.2.2.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/flysystem-dropbox.git", + "reference": "512e8d59b3f9b8a6710f932c421032cb490e9869" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/flysystem-dropbox/zipball/512e8d59b3f9b8a6710f932c421032cb490e9869", + "reference": "512e8d59b3f9b8a6710f932c421032cb490e9869", + "shasum": "" + }, + "require": { + "league/flysystem": "^1.0.20", + "php": "^7.0", + "spatie/dropbox-api": "^1.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "time": "2019-12-04T08:18:17+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Spatie\\FlysystemDropbox\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex.vanderbist@gmail.com", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Flysystem Adapter for the Dropbox v2 API", + "homepage": "https://github.com/spatie/flysystem-dropbox", + "keywords": [ + "Flysystem", + "api", + "dropbox", + "flysystem-dropbox", + "spatie", + "v2" + ] + }, + { + "name": "spatie/laravel-backup", + "version": "5.12.1", + "version_normalized": "5.12.1.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-backup.git", + "reference": "553562557ef13fda0e823cc609cd7d4f2c4f2552" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/553562557ef13fda0e823cc609cd7d4f2c4f2552", + "reference": "553562557ef13fda0e823cc609cd7d4f2c4f2552", + "shasum": "" + }, + "require": { + "illuminate/console": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/contracts": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/events": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/filesystem": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/notifications": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/support": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "league/flysystem": "^1.0.27", + "php": "^7.1", + "spatie/db-dumper": "^2.11.1", + "spatie/temporary-directory": "^1.1", + "symfony/finder": "^3.3|^4.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "~3.5.0|~3.6.0|~3.7.0|~3.8.0", + "phpunit/phpunit": "^7.3" + }, + "suggest": { + "guzzlehttp/guzzle": "Allows notifications to be sent via Slack" + }, + "time": "2019-04-04T12:00:30+00:00", + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Backup\\BackupServiceProvider" + ] + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Spatie\\Backup\\": "src" + }, + "files": [ + "src/Helpers/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A Laravel 5 package to backup your application", + "homepage": "https://github.com/spatie/laravel-backup", + "keywords": [ + "backup", + "database", + "laravel-backup", + "spatie" + ] + }, + { + "name": "spatie/temporary-directory", + "version": "1.2.2", + "version_normalized": "1.2.2.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "fcb127e615700751dac2aefee0ea2808ff3f5bb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/fcb127e615700751dac2aefee0ea2808ff3f5bb1", + "reference": "fcb127e615700751dac2aefee0ea2808ff3f5bb1", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.0" + }, + "time": "2019-12-15T18:52:09+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", + "keywords": [ + "spatie", + "temporary-directory" + ] + }, + { + "name": "symfony/finder", + "version": "v4.4.7", + "version_normalized": "4.4.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "5729f943f9854c5781984ed4907bbb817735776b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/5729f943f9854c5781984ed4907bbb817735776b", + "reference": "5729f943f9854c5781984ed4907bbb817735776b", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "time": "2020-03-27T16:54:36+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.15.0", + "version_normalized": "1.15.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf", + "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2020-03-09T19:04:49+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ] + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.15.0", + "version_normalized": "1.15.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac", + "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "time": "2020-03-09T19:04:49+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ] + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.15.0", + "version_normalized": "1.15.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "37b0976c78b94856543260ce09b460a7bc852747" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747", + "reference": "37b0976c78b94856543260ce09b460a7bc852747", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "time": "2020-02-27T09:26:54+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ] + }, + { + "name": "symfony/process", + "version": "v3.4.39", + "version_normalized": "3.4.39.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "1dbc09f6e14703ae2398efc86b02ae2bcd9a9931" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/1dbc09f6e14703ae2398efc86b02ae2bcd9a9931", + "reference": "1dbc09f6e14703ae2398efc86b02ae2bcd9a9931", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "time": "2020-03-20T06:07:50+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com" + } +] diff --git a/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/LICENSE b/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/LICENSE new file mode 100644 index 0000000..f158c8a --- /dev/null +++ b/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017-2020 Graham Campbell + +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/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/composer.json b/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/composer.json new file mode 100644 index 0000000..a0d461e --- /dev/null +++ b/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/composer.json @@ -0,0 +1,40 @@ +{ + "name": "graham-campbell/guzzle-factory", + "description": "Provides A Simple Guzzle Factory With Good Defaults", + "keywords": ["guzzle", "http", "guzzle factory", "guzzle-factory", "Guzzle", "Guzzle Factory", "Guzzle-Factory", "Graham Campbell", "GrahamCampbell"], + "license": "MIT", + "authors": [ + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" + } + ], + "require": { + "php": "^7.0", + "guzzlehttp/guzzle": "^6.2" + }, + "require-dev": { + "graham-campbell/analyzer": "^2.4", + "phpunit/phpunit": "^6.5|^7.0|^8.0" + }, + "autoload": { + "psr-4": { + "GrahamCampbell\\GuzzleFactory\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "GrahamCampbell\\Tests\\GuzzleFactory\\": "tests/" + } + }, + "config": { + "preferred-install": "dist" + }, + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/src/GuzzleFactory.php b/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/src/GuzzleFactory.php new file mode 100644 index 0000000..42971b9 --- /dev/null +++ b/plugins/panakour/backup/vendor/graham-campbell/guzzle-factory/src/GuzzleFactory.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace GrahamCampbell\GuzzleFactory; + +use GuzzleHttp\Client; +use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\TransferException; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * This is the guzzle factory class. + * + * @author Graham Campbell + */ +final class GuzzleFactory +{ + /** + * The default connect timeout. + * + * @var int + */ + const CONNECT_TIMEOUT = 10; + + /** + * The default transport timeout. + * + * @var int + */ + const TIMEOUT = 15; + + /** + * The default backoff multiplier. + * + * @var int + */ + const BACKOFF = 1000; + + /** + * The default 4xx retry codes. + * + * @var int[] + */ + const CODES = [429]; + + /** + * Create a new guzzle client. + * + * @param array $options + * @param int|null $backoff + * @param int[]|null $codes + * + * @return \GuzzleHttp\Client + */ + public static function make(array $options = [], int $backoff = null, array $codes = null) + { + $config = array_merge(['connect_timeout' => self::CONNECT_TIMEOUT, 'timeout' => self::TIMEOUT], $options); + $config['handler'] = self::handler($backoff, $codes, $options['handler'] ?? null); + + return new Client($config); + } + + /** + * Create a new guzzle handler stack. + * + * @param int|null $backoff + * @param int[]|null $codes + * @param \GuzzleHttp\HandlerStack|null $stack + * + * @return \GuzzleHttp\HandlerStack + */ + public static function handler(int $backoff = null, array $codes = null, HandlerStack $stack = null) + { + $stack = $stack ?: HandlerStack::create(); + + $stack->push(Middleware::retry(function ($retries, RequestInterface $request, ResponseInterface $response = null, TransferException $exception = null) use ($codes) { + return $retries < 3 && ($exception instanceof ConnectException || ($response && ($response->getStatusCode() >= 500 || in_array($response->getStatusCode(), $codes === null ? self::CODES : $codes, true)))); + }, function ($retries) use ($backoff) { + return (int) pow(2, $retries) * ($backoff === null ? self::BACKOFF : $backoff); + })); + + return $stack; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/CHANGELOG.md b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/CHANGELOG.md new file mode 100644 index 0000000..a5cb9c1 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/CHANGELOG.md @@ -0,0 +1,1327 @@ +# Change Log + +## 6.5.3 - 2020-04-18 + +* Use Symfony intl-idn polyfill [#2550](https://github.com/guzzle/guzzle/pull/2550) +* Remove use of internal functions [#2548](https://github.com/guzzle/guzzle/pull/2548) + +## 6.5.2 - 2019-12-23 + +* idn_to_ascii() fix for old PHP versions [#2489](https://github.com/guzzle/guzzle/pull/2489) + +## 6.5.1 - 2019-12-21 + +* Better defaults for PHP installations with old ICU lib [#2454](https://github.com/guzzle/guzzle/pull/2454) +* IDN support for redirects [#2424](https://github.com/guzzle/guzzle/pull/2424) + +## 6.5.0 - 2019-12-07 + +* Improvement: Added support for reset internal queue in MockHandler. [#2143](https://github.com/guzzle/guzzle/pull/2143) +* Improvement: Added support to pass arbitrary options to `curl_multi_init`. [#2287](https://github.com/guzzle/guzzle/pull/2287) +* Fix: Gracefully handle passing `null` to the `header` option. [#2132](https://github.com/guzzle/guzzle/pull/2132) +* Fix: `RetryMiddleware` did not do exponential delay between retires due unit mismatch. [#2132](https://github.com/guzzle/guzzle/pull/2132) +* Fix: Prevent undefined offset when using array for ssl_key options. [#2348](https://github.com/guzzle/guzzle/pull/2348) +* Deprecated `ClientInterface::VERSION` + +## 6.4.1 - 2019-10-23 + +* No `guzzle.phar` was created in 6.4.0 due expired API token. This release will fix that +* Added `parent::__construct()` to `FileCookieJar` and `SessionCookieJar` + +## 6.4.0 - 2019-10-23 + +* Improvement: Improved error messages when using curl < 7.21.2 [#2108](https://github.com/guzzle/guzzle/pull/2108) +* Fix: Test if response is readable before returning a summary in `RequestException::getResponseBodySummary()` [#2081](https://github.com/guzzle/guzzle/pull/2081) +* Fix: Add support for GUZZLE_CURL_SELECT_TIMEOUT environment variable [#2161](https://github.com/guzzle/guzzle/pull/2161) +* Improvement: Added `GuzzleHttp\Exception\InvalidArgumentException` [#2163](https://github.com/guzzle/guzzle/pull/2163) +* Improvement: Added `GuzzleHttp\_current_time()` to use `hrtime()` if that function exists. [#2242](https://github.com/guzzle/guzzle/pull/2242) +* Improvement: Added curl's `appconnect_time` in `TransferStats` [#2284](https://github.com/guzzle/guzzle/pull/2284) +* Improvement: Make GuzzleException extend Throwable wherever it's available [#2273](https://github.com/guzzle/guzzle/pull/2273) +* Fix: Prevent concurrent writes to file when saving `CookieJar` [#2335](https://github.com/guzzle/guzzle/pull/2335) +* Improvement: Update `MockHandler` so we can test transfer time [#2362](https://github.com/guzzle/guzzle/pull/2362) + +## 6.3.3 - 2018-04-22 + +* Fix: Default headers when decode_content is specified + + +## 6.3.2 - 2018-03-26 + +* Fix: Release process + + +## 6.3.1 - 2018-03-26 + +* Bug fix: Parsing 0 epoch expiry times in cookies [#2014](https://github.com/guzzle/guzzle/pull/2014) +* Improvement: Better ConnectException detection [#2012](https://github.com/guzzle/guzzle/pull/2012) +* Bug fix: Malformed domain that contains a "/" [#1999](https://github.com/guzzle/guzzle/pull/1999) +* Bug fix: Undefined offset when a cookie has no first key-value pair [#1998](https://github.com/guzzle/guzzle/pull/1998) +* Improvement: Support PHPUnit 6 [#1953](https://github.com/guzzle/guzzle/pull/1953) +* Bug fix: Support empty headers [#1915](https://github.com/guzzle/guzzle/pull/1915) +* Bug fix: Ignore case during header modifications [#1916](https://github.com/guzzle/guzzle/pull/1916) + ++ Minor code cleanups, documentation fixes and clarifications. + + +## 6.3.0 - 2017-06-22 + +* Feature: force IP resolution (ipv4 or ipv6) [#1608](https://github.com/guzzle/guzzle/pull/1608), [#1659](https://github.com/guzzle/guzzle/pull/1659) +* Improvement: Don't include summary in exception message when body is empty [#1621](https://github.com/guzzle/guzzle/pull/1621) +* Improvement: Handle `on_headers` option in MockHandler [#1580](https://github.com/guzzle/guzzle/pull/1580) +* Improvement: Added SUSE Linux CA path [#1609](https://github.com/guzzle/guzzle/issues/1609) +* Improvement: Use class reference for getting the name of the class instead of using hardcoded strings [#1641](https://github.com/guzzle/guzzle/pull/1641) +* Feature: Added `read_timeout` option [#1611](https://github.com/guzzle/guzzle/pull/1611) +* Bug fix: PHP 7.x fixes [#1685](https://github.com/guzzle/guzzle/pull/1685), [#1686](https://github.com/guzzle/guzzle/pull/1686), [#1811](https://github.com/guzzle/guzzle/pull/1811) +* Deprecation: BadResponseException instantiation without a response [#1642](https://github.com/guzzle/guzzle/pull/1642) +* Feature: Added NTLM auth [#1569](https://github.com/guzzle/guzzle/pull/1569) +* Feature: Track redirect HTTP status codes [#1711](https://github.com/guzzle/guzzle/pull/1711) +* Improvement: Check handler type during construction [#1745](https://github.com/guzzle/guzzle/pull/1745) +* Improvement: Always include the Content-Length if there's a body [#1721](https://github.com/guzzle/guzzle/pull/1721) +* Feature: Added convenience method to access a cookie by name [#1318](https://github.com/guzzle/guzzle/pull/1318) +* Bug fix: Fill `CURLOPT_CAPATH` and `CURLOPT_CAINFO` properly [#1684](https://github.com/guzzle/guzzle/pull/1684) +* Improvement: Use `\GuzzleHttp\Promise\rejection_for` function instead of object init [#1827](https://github.com/guzzle/guzzle/pull/1827) + + ++ Minor code cleanups, documentation fixes and clarifications. + +## 6.2.3 - 2017-02-28 + +* Fix deprecations with guzzle/psr7 version 1.4 + +## 6.2.2 - 2016-10-08 + +* Allow to pass nullable Response to delay callable +* Only add scheme when host is present +* Fix drain case where content-length is the literal string zero +* Obfuscate in-URL credentials in exceptions + +## 6.2.1 - 2016-07-18 + +* Address HTTP_PROXY security vulnerability, CVE-2016-5385: + https://httpoxy.org/ +* Fixing timeout bug with StreamHandler: + https://github.com/guzzle/guzzle/pull/1488 +* Only read up to `Content-Length` in PHP StreamHandler to avoid timeouts when + a server does not honor `Connection: close`. +* Ignore URI fragment when sending requests. + +## 6.2.0 - 2016-03-21 + +* Feature: added `GuzzleHttp\json_encode` and `GuzzleHttp\json_decode`. + https://github.com/guzzle/guzzle/pull/1389 +* Bug fix: Fix sleep calculation when waiting for delayed requests. + https://github.com/guzzle/guzzle/pull/1324 +* Feature: More flexible history containers. + https://github.com/guzzle/guzzle/pull/1373 +* Bug fix: defer sink stream opening in StreamHandler. + https://github.com/guzzle/guzzle/pull/1377 +* Bug fix: do not attempt to escape cookie values. + https://github.com/guzzle/guzzle/pull/1406 +* Feature: report original content encoding and length on decoded responses. + https://github.com/guzzle/guzzle/pull/1409 +* Bug fix: rewind seekable request bodies before dispatching to cURL. + https://github.com/guzzle/guzzle/pull/1422 +* Bug fix: provide an empty string to `http_build_query` for HHVM workaround. + https://github.com/guzzle/guzzle/pull/1367 + +## 6.1.1 - 2015-11-22 + +* Bug fix: Proxy::wrapSync() now correctly proxies to the appropriate handler + https://github.com/guzzle/guzzle/commit/911bcbc8b434adce64e223a6d1d14e9a8f63e4e4 +* Feature: HandlerStack is now more generic. + https://github.com/guzzle/guzzle/commit/f2102941331cda544745eedd97fc8fd46e1ee33e +* Bug fix: setting verify to false in the StreamHandler now disables peer + verification. https://github.com/guzzle/guzzle/issues/1256 +* Feature: Middleware now uses an exception factory, including more error + context. https://github.com/guzzle/guzzle/pull/1282 +* Feature: better support for disabled functions. + https://github.com/guzzle/guzzle/pull/1287 +* Bug fix: fixed regression where MockHandler was not using `sink`. + https://github.com/guzzle/guzzle/pull/1292 + +## 6.1.0 - 2015-09-08 + +* Feature: Added the `on_stats` request option to provide access to transfer + statistics for requests. https://github.com/guzzle/guzzle/pull/1202 +* Feature: Added the ability to persist session cookies in CookieJars. + https://github.com/guzzle/guzzle/pull/1195 +* Feature: Some compatibility updates for Google APP Engine + https://github.com/guzzle/guzzle/pull/1216 +* Feature: Added support for NO_PROXY to prevent the use of a proxy based on + a simple set of rules. https://github.com/guzzle/guzzle/pull/1197 +* Feature: Cookies can now contain square brackets. + https://github.com/guzzle/guzzle/pull/1237 +* Bug fix: Now correctly parsing `=` inside of quotes in Cookies. + https://github.com/guzzle/guzzle/pull/1232 +* Bug fix: Cusotm cURL options now correctly override curl options of the + same name. https://github.com/guzzle/guzzle/pull/1221 +* Bug fix: Content-Type header is now added when using an explicitly provided + multipart body. https://github.com/guzzle/guzzle/pull/1218 +* Bug fix: Now ignoring Set-Cookie headers that have no name. +* Bug fix: Reason phrase is no longer cast to an int in some cases in the + cURL handler. https://github.com/guzzle/guzzle/pull/1187 +* Bug fix: Remove the Authorization header when redirecting if the Host + header changes. https://github.com/guzzle/guzzle/pull/1207 +* Bug fix: Cookie path matching fixes + https://github.com/guzzle/guzzle/issues/1129 +* Bug fix: Fixing the cURL `body_as_string` setting + https://github.com/guzzle/guzzle/pull/1201 +* Bug fix: quotes are no longer stripped when parsing cookies. + https://github.com/guzzle/guzzle/issues/1172 +* Bug fix: `form_params` and `query` now always uses the `&` separator. + https://github.com/guzzle/guzzle/pull/1163 +* Bug fix: Adding a Content-Length to PHP stream wrapper requests if not set. + https://github.com/guzzle/guzzle/pull/1189 + +## 6.0.2 - 2015-07-04 + +* Fixed a memory leak in the curl handlers in which references to callbacks + were not being removed by `curl_reset`. +* Cookies are now extracted properly before redirects. +* Cookies now allow more character ranges. +* Decoded Content-Encoding responses are now modified to correctly reflect + their state if the encoding was automatically removed by a handler. This + means that the `Content-Encoding` header may be removed an the + `Content-Length` modified to reflect the message size after removing the + encoding. +* Added a more explicit error message when trying to use `form_params` and + `multipart` in the same request. +* Several fixes for HHVM support. +* Functions are now conditionally required using an additional level of + indirection to help with global Composer installations. + +## 6.0.1 - 2015-05-27 + +* Fixed a bug with serializing the `query` request option where the `&` + separator was missing. +* Added a better error message for when `body` is provided as an array. Please + use `form_params` or `multipart` instead. +* Various doc fixes. + +## 6.0.0 - 2015-05-26 + +* See the UPGRADING.md document for more information. +* Added `multipart` and `form_params` request options. +* Added `synchronous` request option. +* Added the `on_headers` request option. +* Fixed `expect` handling. +* No longer adding default middlewares in the client ctor. These need to be + present on the provided handler in order to work. +* Requests are no longer initiated when sending async requests with the + CurlMultiHandler. This prevents unexpected recursion from requests completing + while ticking the cURL loop. +* Removed the semantics of setting `default` to `true`. This is no longer + required now that the cURL loop is not ticked for async requests. +* Added request and response logging middleware. +* No longer allowing self signed certificates when using the StreamHandler. +* Ensuring that `sink` is valid if saving to a file. +* Request exceptions now include a "handler context" which provides handler + specific contextual information. +* Added `GuzzleHttp\RequestOptions` to allow request options to be applied + using constants. +* `$maxHandles` has been removed from CurlMultiHandler. +* `MultipartPostBody` is now part of the `guzzlehttp/psr7` package. + +## 5.3.0 - 2015-05-19 + +* Mock now supports `save_to` +* Marked `AbstractRequestEvent::getTransaction()` as public. +* Fixed a bug in which multiple headers using different casing would overwrite + previous headers in the associative array. +* Added `Utils::getDefaultHandler()` +* Marked `GuzzleHttp\Client::getDefaultUserAgent` as deprecated. +* URL scheme is now always lowercased. + +## 6.0.0-beta.1 + +* Requires PHP >= 5.5 +* Updated to use PSR-7 + * Requires immutable messages, which basically means an event based system + owned by a request instance is no longer possible. + * Utilizing the [Guzzle PSR-7 package](https://github.com/guzzle/psr7). + * Removed the dependency on `guzzlehttp/streams`. These stream abstractions + are available in the `guzzlehttp/psr7` package under the `GuzzleHttp\Psr7` + namespace. +* Added middleware and handler system + * Replaced the Guzzle event and subscriber system with a middleware system. + * No longer depends on RingPHP, but rather places the HTTP handlers directly + in Guzzle, operating on PSR-7 messages. + * Retry logic is now encapsulated in `GuzzleHttp\Middleware::retry`, which + means the `guzzlehttp/retry-subscriber` is now obsolete. + * Mocking responses is now handled using `GuzzleHttp\Handler\MockHandler`. +* Asynchronous responses + * No longer supports the `future` request option to send an async request. + Instead, use one of the `*Async` methods of a client (e.g., `requestAsync`, + `getAsync`, etc.). + * Utilizing `GuzzleHttp\Promise` instead of React's promise library to avoid + recursion required by chaining and forwarding react promises. See + https://github.com/guzzle/promises + * Added `requestAsync` and `sendAsync` to send request asynchronously. + * Added magic methods for `getAsync()`, `postAsync()`, etc. to send requests + asynchronously. +* Request options + * POST and form updates + * Added the `form_fields` and `form_files` request options. + * Removed the `GuzzleHttp\Post` namespace. + * The `body` request option no longer accepts an array for POST requests. + * The `exceptions` request option has been deprecated in favor of the + `http_errors` request options. + * The `save_to` request option has been deprecated in favor of `sink` request + option. +* Clients no longer accept an array of URI template string and variables for + URI variables. You will need to expand URI templates before passing them + into a client constructor or request method. +* Client methods `get()`, `post()`, `put()`, `patch()`, `options()`, etc. are + now magic methods that will send synchronous requests. +* Replaced `Utils.php` with plain functions in `functions.php`. +* Removed `GuzzleHttp\Collection`. +* Removed `GuzzleHttp\BatchResults`. Batched pool results are now returned as + an array. +* Removed `GuzzleHttp\Query`. Query string handling is now handled using an + associative array passed into the `query` request option. The query string + is serialized using PHP's `http_build_query`. If you need more control, you + can pass the query string in as a string. +* `GuzzleHttp\QueryParser` has been replaced with the + `GuzzleHttp\Psr7\parse_query`. + +## 5.2.0 - 2015-01-27 + +* Added `AppliesHeadersInterface` to make applying headers to a request based + on the body more generic and not specific to `PostBodyInterface`. +* Reduced the number of stack frames needed to send requests. +* Nested futures are now resolved in the client rather than the RequestFsm +* Finishing state transitions is now handled in the RequestFsm rather than the + RingBridge. +* Added a guard in the Pool class to not use recursion for request retries. + +## 5.1.0 - 2014-12-19 + +* Pool class no longer uses recursion when a request is intercepted. +* The size of a Pool can now be dynamically adjusted using a callback. + See https://github.com/guzzle/guzzle/pull/943. +* Setting a request option to `null` when creating a request with a client will + ensure that the option is not set. This allows you to overwrite default + request options on a per-request basis. + See https://github.com/guzzle/guzzle/pull/937. +* Added the ability to limit which protocols are allowed for redirects by + specifying a `protocols` array in the `allow_redirects` request option. +* Nested futures due to retries are now resolved when waiting for synchronous + responses. See https://github.com/guzzle/guzzle/pull/947. +* `"0"` is now an allowed URI path. See + https://github.com/guzzle/guzzle/pull/935. +* `Query` no longer typehints on the `$query` argument in the constructor, + allowing for strings and arrays. +* Exceptions thrown in the `end` event are now correctly wrapped with Guzzle + specific exceptions if necessary. + +## 5.0.3 - 2014-11-03 + +This change updates query strings so that they are treated as un-encoded values +by default where the value represents an un-encoded value to send over the +wire. A Query object then encodes the value before sending over the wire. This +means that even value query string values (e.g., ":") are url encoded. This +makes the Query class match PHP's http_build_query function. However, if you +want to send requests over the wire using valid query string characters that do +not need to be encoded, then you can provide a string to Url::setQuery() and +pass true as the second argument to specify that the query string is a raw +string that should not be parsed or encoded (unless a call to getQuery() is +subsequently made, forcing the query-string to be converted into a Query +object). + +## 5.0.2 - 2014-10-30 + +* Added a trailing `\r\n` to multipart/form-data payloads. See + https://github.com/guzzle/guzzle/pull/871 +* Added a `GuzzleHttp\Pool::send()` convenience method to match the docs. +* Status codes are now returned as integers. See + https://github.com/guzzle/guzzle/issues/881 +* No longer overwriting an existing `application/x-www-form-urlencoded` header + when sending POST requests, allowing for customized headers. See + https://github.com/guzzle/guzzle/issues/877 +* Improved path URL serialization. + + * No longer double percent-encoding characters in the path or query string if + they are already encoded. + * Now properly encoding the supplied path to a URL object, instead of only + encoding ' ' and '?'. + * Note: This has been changed in 5.0.3 to now encode query string values by + default unless the `rawString` argument is provided when setting the query + string on a URL: Now allowing many more characters to be present in the + query string without being percent encoded. See http://tools.ietf.org/html/rfc3986#appendix-A + +## 5.0.1 - 2014-10-16 + +Bugfix release. + +* Fixed an issue where connection errors still returned response object in + error and end events event though the response is unusable. This has been + corrected so that a response is not returned in the `getResponse` method of + these events if the response did not complete. https://github.com/guzzle/guzzle/issues/867 +* Fixed an issue where transfer statistics were not being populated in the + RingBridge. https://github.com/guzzle/guzzle/issues/866 + +## 5.0.0 - 2014-10-12 + +Adding support for non-blocking responses and some minor API cleanup. + +### New Features + +* Added support for non-blocking responses based on `guzzlehttp/guzzle-ring`. +* Added a public API for creating a default HTTP adapter. +* Updated the redirect plugin to be non-blocking so that redirects are sent + concurrently. Other plugins like this can now be updated to be non-blocking. +* Added a "progress" event so that you can get upload and download progress + events. +* Added `GuzzleHttp\Pool` which implements FutureInterface and transfers + requests concurrently using a capped pool size as efficiently as possible. +* Added `hasListeners()` to EmitterInterface. +* Removed `GuzzleHttp\ClientInterface::sendAll` and marked + `GuzzleHttp\Client::sendAll` as deprecated (it's still there, just not the + recommended way). + +### Breaking changes + +The breaking changes in this release are relatively minor. The biggest thing to +look out for is that request and response objects no longer implement fluent +interfaces. + +* Removed the fluent interfaces (i.e., `return $this`) from requests, + responses, `GuzzleHttp\Collection`, `GuzzleHttp\Url`, + `GuzzleHttp\Query`, `GuzzleHttp\Post\PostBody`, and + `GuzzleHttp\Cookie\SetCookie`. This blog post provides a good outline of + why I did this: http://ocramius.github.io/blog/fluent-interfaces-are-evil/. + This also makes the Guzzle message interfaces compatible with the current + PSR-7 message proposal. +* Removed "functions.php", so that Guzzle is truly PSR-4 compliant. Except + for the HTTP request functions from function.php, these functions are now + implemented in `GuzzleHttp\Utils` using camelCase. `GuzzleHttp\json_decode` + moved to `GuzzleHttp\Utils::jsonDecode`. `GuzzleHttp\get_path` moved to + `GuzzleHttp\Utils::getPath`. `GuzzleHttp\set_path` moved to + `GuzzleHttp\Utils::setPath`. `GuzzleHttp\batch` should now be + `GuzzleHttp\Pool::batch`, which returns an `objectStorage`. Using functions.php + caused problems for many users: they aren't PSR-4 compliant, require an + explicit include, and needed an if-guard to ensure that the functions are not + declared multiple times. +* Rewrote adapter layer. + * Removing all classes from `GuzzleHttp\Adapter`, these are now + implemented as callables that are stored in `GuzzleHttp\Ring\Client`. + * Removed the concept of "parallel adapters". Sending requests serially or + concurrently is now handled using a single adapter. + * Moved `GuzzleHttp\Adapter\Transaction` to `GuzzleHttp\Transaction`. The + Transaction object now exposes the request, response, and client as public + properties. The getters and setters have been removed. +* Removed the "headers" event. This event was only useful for changing the + body a response once the headers of the response were known. You can implement + a similar behavior in a number of ways. One example might be to use a + FnStream that has access to the transaction being sent. For example, when the + first byte is written, you could check if the response headers match your + expectations, and if so, change the actual stream body that is being + written to. +* Removed the `asArray` parameter from + `GuzzleHttp\Message\MessageInterface::getHeader`. If you want to get a header + value as an array, then use the newly added `getHeaderAsArray()` method of + `MessageInterface`. This change makes the Guzzle interfaces compatible with + the PSR-7 interfaces. +* `GuzzleHttp\Message\MessageFactory` no longer allows subclasses to add + custom request options using double-dispatch (this was an implementation + detail). Instead, you should now provide an associative array to the + constructor which is a mapping of the request option name mapping to a + function that applies the option value to a request. +* Removed the concept of "throwImmediately" from exceptions and error events. + This control mechanism was used to stop a transfer of concurrent requests + from completing. This can now be handled by throwing the exception or by + cancelling a pool of requests or each outstanding future request individually. +* Updated to "GuzzleHttp\Streams" 3.0. + * `GuzzleHttp\Stream\StreamInterface::getContents()` no longer accepts a + `maxLen` parameter. This update makes the Guzzle streams project + compatible with the current PSR-7 proposal. + * `GuzzleHttp\Stream\Stream::__construct`, + `GuzzleHttp\Stream\Stream::factory`, and + `GuzzleHttp\Stream\Utils::create` no longer accept a size in the second + argument. They now accept an associative array of options, including the + "size" key and "metadata" key which can be used to provide custom metadata. + +## 4.2.2 - 2014-09-08 + +* Fixed a memory leak in the CurlAdapter when reusing cURL handles. +* No longer using `request_fulluri` in stream adapter proxies. +* Relative redirects are now based on the last response, not the first response. + +## 4.2.1 - 2014-08-19 + +* Ensuring that the StreamAdapter does not always add a Content-Type header +* Adding automated github releases with a phar and zip + +## 4.2.0 - 2014-08-17 + +* Now merging in default options using a case-insensitive comparison. + Closes https://github.com/guzzle/guzzle/issues/767 +* Added the ability to automatically decode `Content-Encoding` response bodies + using the `decode_content` request option. This is set to `true` by default + to decode the response body if it comes over the wire with a + `Content-Encoding`. Set this value to `false` to disable decoding the + response content, and pass a string to provide a request `Accept-Encoding` + header and turn on automatic response decoding. This feature now allows you + to pass an `Accept-Encoding` header in the headers of a request but still + disable automatic response decoding. + Closes https://github.com/guzzle/guzzle/issues/764 +* Added the ability to throw an exception immediately when transferring + requests in parallel. Closes https://github.com/guzzle/guzzle/issues/760 +* Updating guzzlehttp/streams dependency to ~2.1 +* No longer utilizing the now deprecated namespaced methods from the stream + package. + +## 4.1.8 - 2014-08-14 + +* Fixed an issue in the CurlFactory that caused setting the `stream=false` + request option to throw an exception. + See: https://github.com/guzzle/guzzle/issues/769 +* TransactionIterator now calls rewind on the inner iterator. + See: https://github.com/guzzle/guzzle/pull/765 +* You can now set the `Content-Type` header to `multipart/form-data` + when creating POST requests to force multipart bodies. + See https://github.com/guzzle/guzzle/issues/768 + +## 4.1.7 - 2014-08-07 + +* Fixed an error in the HistoryPlugin that caused the same request and response + to be logged multiple times when an HTTP protocol error occurs. +* Ensuring that cURL does not add a default Content-Type when no Content-Type + has been supplied by the user. This prevents the adapter layer from modifying + the request that is sent over the wire after any listeners may have already + put the request in a desired state (e.g., signed the request). +* Throwing an exception when you attempt to send requests that have the + "stream" set to true in parallel using the MultiAdapter. +* Only calling curl_multi_select when there are active cURL handles. This was + previously changed and caused performance problems on some systems due to PHP + always selecting until the maximum select timeout. +* Fixed a bug where multipart/form-data POST fields were not correctly + aggregated (e.g., values with "&"). + +## 4.1.6 - 2014-08-03 + +* Added helper methods to make it easier to represent messages as strings, + including getting the start line and getting headers as a string. + +## 4.1.5 - 2014-08-02 + +* Automatically retrying cURL "Connection died, retrying a fresh connect" + errors when possible. +* cURL implementation cleanup +* Allowing multiple event subscriber listeners to be registered per event by + passing an array of arrays of listener configuration. + +## 4.1.4 - 2014-07-22 + +* Fixed a bug that caused multi-part POST requests with more than one field to + serialize incorrectly. +* Paths can now be set to "0" +* `ResponseInterface::xml` now accepts a `libxml_options` option and added a + missing default argument that was required when parsing XML response bodies. +* A `save_to` stream is now created lazily, which means that files are not + created on disk unless a request succeeds. + +## 4.1.3 - 2014-07-15 + +* Various fixes to multipart/form-data POST uploads +* Wrapping function.php in an if-statement to ensure Guzzle can be used + globally and in a Composer install +* Fixed an issue with generating and merging in events to an event array +* POST headers are only applied before sending a request to allow you to change + the query aggregator used before uploading +* Added much more robust query string parsing +* Fixed various parsing and normalization issues with URLs +* Fixing an issue where multi-valued headers were not being utilized correctly + in the StreamAdapter + +## 4.1.2 - 2014-06-18 + +* Added support for sending payloads with GET requests + +## 4.1.1 - 2014-06-08 + +* Fixed an issue related to using custom message factory options in subclasses +* Fixed an issue with nested form fields in a multi-part POST +* Fixed an issue with using the `json` request option for POST requests +* Added `ToArrayInterface` to `GuzzleHttp\Cookie\CookieJar` + +## 4.1.0 - 2014-05-27 + +* Added a `json` request option to easily serialize JSON payloads. +* Added a `GuzzleHttp\json_decode()` wrapper to safely parse JSON. +* Added `setPort()` and `getPort()` to `GuzzleHttp\Message\RequestInterface`. +* Added the ability to provide an emitter to a client in the client constructor. +* Added the ability to persist a cookie session using $_SESSION. +* Added a trait that can be used to add event listeners to an iterator. +* Removed request method constants from RequestInterface. +* Fixed warning when invalid request start-lines are received. +* Updated MessageFactory to work with custom request option methods. +* Updated cacert bundle to latest build. + +4.0.2 (2014-04-16) +------------------ + +* Proxy requests using the StreamAdapter now properly use request_fulluri (#632) +* Added the ability to set scalars as POST fields (#628) + +## 4.0.1 - 2014-04-04 + +* The HTTP status code of a response is now set as the exception code of + RequestException objects. +* 303 redirects will now correctly switch from POST to GET requests. +* The default parallel adapter of a client now correctly uses the MultiAdapter. +* HasDataTrait now initializes the internal data array as an empty array so + that the toArray() method always returns an array. + +## 4.0.0 - 2014-03-29 + +* For more information on the 4.0 transition, see: + http://mtdowling.com/blog/2014/03/15/guzzle-4-rc/ +* For information on changes and upgrading, see: + https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#3x-to-40 +* Added `GuzzleHttp\batch()` as a convenience function for sending requests in + parallel without needing to write asynchronous code. +* Restructured how events are added to `GuzzleHttp\ClientInterface::sendAll()`. + You can now pass a callable or an array of associative arrays where each + associative array contains the "fn", "priority", and "once" keys. + +## 4.0.0.rc-2 - 2014-03-25 + +* Removed `getConfig()` and `setConfig()` from clients to avoid confusion + around whether things like base_url, message_factory, etc. should be able to + be retrieved or modified. +* Added `getDefaultOption()` and `setDefaultOption()` to ClientInterface +* functions.php functions were renamed using snake_case to match PHP idioms +* Added support for `HTTP_PROXY`, `HTTPS_PROXY`, and + `GUZZLE_CURL_SELECT_TIMEOUT` environment variables +* Added the ability to specify custom `sendAll()` event priorities +* Added the ability to specify custom stream context options to the stream + adapter. +* Added a functions.php function for `get_path()` and `set_path()` +* CurlAdapter and MultiAdapter now use a callable to generate curl resources +* MockAdapter now properly reads a body and emits a `headers` event +* Updated Url class to check if a scheme and host are set before adding ":" + and "//". This allows empty Url (e.g., "") to be serialized as "". +* Parsing invalid XML no longer emits warnings +* Curl classes now properly throw AdapterExceptions +* Various performance optimizations +* Streams are created with the faster `Stream\create()` function +* Marked deprecation_proxy() as internal +* Test server is now a collection of static methods on a class + +## 4.0.0-rc.1 - 2014-03-15 + +* See https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#3x-to-40 + +## 3.8.1 - 2014-01-28 + +* Bug: Always using GET requests when redirecting from a 303 response +* Bug: CURLOPT_SSL_VERIFYHOST is now correctly set to false when setting `$certificateAuthority` to false in + `Guzzle\Http\ClientInterface::setSslVerification()` +* Bug: RedirectPlugin now uses strict RFC 3986 compliance when combining a base URL with a relative URL +* Bug: The body of a request can now be set to `"0"` +* Sending PHP stream requests no longer forces `HTTP/1.0` +* Adding more information to ExceptionCollection exceptions so that users have more context, including a stack trace of + each sub-exception +* Updated the `$ref` attribute in service descriptions to merge over any existing parameters of a schema (rather than + clobbering everything). +* Merging URLs will now use the query string object from the relative URL (thus allowing custom query aggregators) +* Query strings are now parsed in a way that they do no convert empty keys with no value to have a dangling `=`. + For example `foo&bar=baz` is now correctly parsed and recognized as `foo&bar=baz` rather than `foo=&bar=baz`. +* Now properly escaping the regular expression delimiter when matching Cookie domains. +* Network access is now disabled when loading XML documents + +## 3.8.0 - 2013-12-05 + +* Added the ability to define a POST name for a file +* JSON response parsing now properly walks additionalProperties +* cURL error code 18 is now retried automatically in the BackoffPlugin +* Fixed a cURL error when URLs contain fragments +* Fixed an issue in the BackoffPlugin retry event where it was trying to access all exceptions as if they were + CurlExceptions +* CURLOPT_PROGRESS function fix for PHP 5.5 (69fcc1e) +* Added the ability for Guzzle to work with older versions of cURL that do not support `CURLOPT_TIMEOUT_MS` +* Fixed a bug that was encountered when parsing empty header parameters +* UriTemplate now has a `setRegex()` method to match the docs +* The `debug` request parameter now checks if it is truthy rather than if it exists +* Setting the `debug` request parameter to true shows verbose cURL output instead of using the LogPlugin +* Added the ability to combine URLs using strict RFC 3986 compliance +* Command objects can now return the validation errors encountered by the command +* Various fixes to cache revalidation (#437 and 29797e5) +* Various fixes to the AsyncPlugin +* Cleaned up build scripts + +## 3.7.4 - 2013-10-02 + +* Bug fix: 0 is now an allowed value in a description parameter that has a default value (#430) +* Bug fix: SchemaFormatter now returns an integer when formatting to a Unix timestamp + (see https://github.com/aws/aws-sdk-php/issues/147) +* Bug fix: Cleaned up and fixed URL dot segment removal to properly resolve internal dots +* Minimum PHP version is now properly specified as 5.3.3 (up from 5.3.2) (#420) +* Updated the bundled cacert.pem (#419) +* OauthPlugin now supports adding authentication to headers or query string (#425) + +## 3.7.3 - 2013-09-08 + +* Added the ability to get the exception associated with a request/command when using `MultiTransferException` and + `CommandTransferException`. +* Setting `additionalParameters` of a response to false is now honored when parsing responses with a service description +* Schemas are only injected into response models when explicitly configured. +* No longer guessing Content-Type based on the path of a request. Content-Type is now only guessed based on the path of + an EntityBody. +* Bug fix: ChunkedIterator can now properly chunk a \Traversable as well as an \Iterator. +* Bug fix: FilterIterator now relies on `\Iterator` instead of `\Traversable`. +* Bug fix: Gracefully handling malformed responses in RequestMediator::writeResponseBody() +* Bug fix: Replaced call to canCache with canCacheRequest in the CallbackCanCacheStrategy of the CachePlugin +* Bug fix: Visiting XML attributes first before visiting XML children when serializing requests +* Bug fix: Properly parsing headers that contain commas contained in quotes +* Bug fix: mimetype guessing based on a filename is now case-insensitive + +## 3.7.2 - 2013-08-02 + +* Bug fix: Properly URL encoding paths when using the PHP-only version of the UriTemplate expander + See https://github.com/guzzle/guzzle/issues/371 +* Bug fix: Cookie domains are now matched correctly according to RFC 6265 + See https://github.com/guzzle/guzzle/issues/377 +* Bug fix: GET parameters are now used when calculating an OAuth signature +* Bug fix: Fixed an issue with cache revalidation where the If-None-Match header was being double quoted +* `Guzzle\Common\AbstractHasDispatcher::dispatch()` now returns the event that was dispatched +* `Guzzle\Http\QueryString::factory()` now guesses the most appropriate query aggregator to used based on the input. + See https://github.com/guzzle/guzzle/issues/379 +* Added a way to add custom domain objects to service description parsing using the `operation.parse_class` event. See + https://github.com/guzzle/guzzle/pull/380 +* cURL multi cleanup and optimizations + +## 3.7.1 - 2013-07-05 + +* Bug fix: Setting default options on a client now works +* Bug fix: Setting options on HEAD requests now works. See #352 +* Bug fix: Moving stream factory before send event to before building the stream. See #353 +* Bug fix: Cookies no longer match on IP addresses per RFC 6265 +* Bug fix: Correctly parsing header parameters that are in `<>` and quotes +* Added `cert` and `ssl_key` as request options +* `Host` header can now diverge from the host part of a URL if the header is set manually +* `Guzzle\Service\Command\LocationVisitor\Request\XmlVisitor` was rewritten to change from using SimpleXML to XMLWriter +* OAuth parameters are only added via the plugin if they aren't already set +* Exceptions are now thrown when a URL cannot be parsed +* Returning `false` if `Guzzle\Http\EntityBody::getContentMd5()` fails +* Not setting a `Content-MD5` on a command if calculating the Content-MD5 fails via the CommandContentMd5Plugin + +## 3.7.0 - 2013-06-10 + +* See UPGRADING.md for more information on how to upgrade. +* Requests now support the ability to specify an array of $options when creating a request to more easily modify a + request. You can pass a 'request.options' configuration setting to a client to apply default request options to + every request created by a client (e.g. default query string variables, headers, curl options, etc.). +* Added a static facade class that allows you to use Guzzle with static methods and mount the class to `\Guzzle`. + See `Guzzle\Http\StaticClient::mount`. +* Added `command.request_options` to `Guzzle\Service\Command\AbstractCommand` to pass request options to requests + created by a command (e.g. custom headers, query string variables, timeout settings, etc.). +* Stream size in `Guzzle\Stream\PhpStreamRequestFactory` will now be set if Content-Length is returned in the + headers of a response +* Added `Guzzle\Common\Collection::setPath($path, $value)` to set a value into an array using a nested key + (e.g. `$collection->setPath('foo/baz/bar', 'test'); echo $collection['foo']['bar']['bar'];`) +* ServiceBuilders now support storing and retrieving arbitrary data +* CachePlugin can now purge all resources for a given URI +* CachePlugin can automatically purge matching cached items when a non-idempotent request is sent to a resource +* CachePlugin now uses the Vary header to determine if a resource is a cache hit +* `Guzzle\Http\Message\Response` now implements `\Serializable` +* Added `Guzzle\Cache\CacheAdapterFactory::fromCache()` to more easily create cache adapters +* `Guzzle\Service\ClientInterface::execute()` now accepts an array, single command, or Traversable +* Fixed a bug in `Guzzle\Http\Message\Header\Link::addLink()` +* Better handling of calculating the size of a stream in `Guzzle\Stream\Stream` using fstat() and caching the size +* `Guzzle\Common\Exception\ExceptionCollection` now creates a more readable exception message +* Fixing BC break: Added back the MonologLogAdapter implementation rather than extending from PsrLog so that older + Symfony users can still use the old version of Monolog. +* Fixing BC break: Added the implementation back in for `Guzzle\Http\Message\AbstractMessage::getTokenizedHeader()`. + Now triggering an E_USER_DEPRECATED warning when used. Use `$message->getHeader()->parseParams()`. +* Several performance improvements to `Guzzle\Common\Collection` +* Added an `$options` argument to the end of the following methods of `Guzzle\Http\ClientInterface`: + createRequest, head, delete, put, patch, post, options, prepareRequest +* Added an `$options` argument to the end of `Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()` +* Added an `applyOptions()` method to `Guzzle\Http\Message\Request\RequestFactoryInterface` +* Changed `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null)` to + `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array())`. You can still pass in a + resource, string, or EntityBody into the $options parameter to specify the download location of the response. +* Changed `Guzzle\Common\Collection::__construct($data)` to no longer accepts a null value for `$data` but a + default `array()` +* Added `Guzzle\Stream\StreamInterface::isRepeatable` +* Removed `Guzzle\Http\ClientInterface::setDefaultHeaders(). Use + $client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. or + $client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))`. +* Removed `Guzzle\Http\ClientInterface::getDefaultHeaders(). Use $client->getConfig()->getPath('request.options/headers')`. +* Removed `Guzzle\Http\ClientInterface::expandTemplate()` +* Removed `Guzzle\Http\ClientInterface::setRequestFactory()` +* Removed `Guzzle\Http\ClientInterface::getCurlMulti()` +* Removed `Guzzle\Http\Message\RequestInterface::canCache` +* Removed `Guzzle\Http\Message\RequestInterface::setIsRedirect` +* Removed `Guzzle\Http\Message\RequestInterface::isRedirect` +* Made `Guzzle\Http\Client::expandTemplate` and `getUriTemplate` protected methods. +* You can now enable E_USER_DEPRECATED warnings to see if you are using a deprecated method by setting + `Guzzle\Common\Version::$emitWarnings` to true. +* Marked `Guzzle\Http\Message\Request::isResponseBodyRepeatable()` as deprecated. Use + `$request->getResponseBody()->isRepeatable()` instead. +* Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use + `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +* Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use + `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +* Marked `Guzzle\Http\Message\Request::setIsRedirect()` as deprecated. Use the HistoryPlugin instead. +* Marked `Guzzle\Http\Message\Request::isRedirect()` as deprecated. Use the HistoryPlugin instead. +* Marked `Guzzle\Cache\CacheAdapterFactory::factory()` as deprecated +* Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. + These will work through Guzzle 4.0 +* Marked 'request.params' for `Guzzle\Http\Client` as deprecated. Use [request.options][params]. +* Marked `Guzzle\Service\Client::enableMagicMethods()` as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client. +* Marked `Guzzle\Service\Client::getDefaultHeaders()` as deprecated. Use $client->getConfig()->getPath('request.options/headers')`. +* Marked `Guzzle\Service\Client::setDefaultHeaders()` as deprecated. Use $client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. +* Marked `Guzzle\Parser\Url\UrlParser` as deprecated. Just use PHP's `parse_url()` and percent encode your UTF-8. +* Marked `Guzzle\Common\Collection::inject()` as deprecated. +* Marked `Guzzle\Plugin\CurlAuth\CurlAuthPlugin` as deprecated. Use `$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest');` +* CacheKeyProviderInterface and DefaultCacheKeyProvider are no longer used. All of this logic is handled in a + CacheStorageInterface. These two objects and interface will be removed in a future version. +* Always setting X-cache headers on cached responses +* Default cache TTLs are now handled by the CacheStorageInterface of a CachePlugin +* `CacheStorageInterface::cache($key, Response $response, $ttl = null)` has changed to `cache(RequestInterface + $request, Response $response);` +* `CacheStorageInterface::fetch($key)` has changed to `fetch(RequestInterface $request);` +* `CacheStorageInterface::delete($key)` has changed to `delete(RequestInterface $request);` +* Added `CacheStorageInterface::purge($url)` +* `DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin + $plugin)` has changed to `DefaultRevalidation::__construct(CacheStorageInterface $cache, + CanCacheStrategyInterface $canCache = null)` +* Added `RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)` + +## 3.6.0 - 2013-05-29 + +* ServiceDescription now implements ToArrayInterface +* Added command.hidden_params to blacklist certain headers from being treated as additionalParameters +* Guzzle can now correctly parse incomplete URLs +* Mixed casing of headers are now forced to be a single consistent casing across all values for that header. +* Messages internally use a HeaderCollection object to delegate handling case-insensitive header resolution +* Removed the whole changedHeader() function system of messages because all header changes now go through addHeader(). +* Specific header implementations can be created for complex headers. When a message creates a header, it uses a + HeaderFactory which can map specific headers to specific header classes. There is now a Link header and + CacheControl header implementation. +* Removed from interface: Guzzle\Http\ClientInterface::setUriTemplate +* Removed from interface: Guzzle\Http\ClientInterface::setCurlMulti() +* Removed Guzzle\Http\Message\Request::receivedRequestHeader() and implemented this functionality in + Guzzle\Http\Curl\RequestMediator +* Removed the optional $asString parameter from MessageInterface::getHeader(). Just cast the header to a string. +* Removed the optional $tryChunkedTransfer option from Guzzle\Http\Message\EntityEnclosingRequestInterface +* Removed the $asObjects argument from Guzzle\Http\Message\MessageInterface::getHeaders() +* Removed Guzzle\Parser\ParserRegister::get(). Use getParser() +* Removed Guzzle\Parser\ParserRegister::set(). Use registerParser(). +* All response header helper functions return a string rather than mixing Header objects and strings inconsistently +* Removed cURL blacklist support. This is no longer necessary now that Expect, Accept, etc. are managed by Guzzle + directly via interfaces +* Removed the injecting of a request object onto a response object. The methods to get and set a request still exist + but are a no-op until removed. +* Most classes that used to require a `Guzzle\Service\Command\CommandInterface` typehint now request a + `Guzzle\Service\Command\ArrayCommandInterface`. +* Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response + on a request while the request is still being transferred +* The ability to case-insensitively search for header values +* Guzzle\Http\Message\Header::hasExactHeader +* Guzzle\Http\Message\Header::raw. Use getAll() +* Deprecated cache control specific methods on Guzzle\Http\Message\AbstractMessage. Use the CacheControl header object + instead. +* `Guzzle\Service\Command\CommandInterface` now extends from ToArrayInterface and ArrayAccess +* Added the ability to cast Model objects to a string to view debug information. + +## 3.5.0 - 2013-05-13 + +* Bug: Fixed a regression so that request responses are parsed only once per oncomplete event rather than multiple times +* Bug: Better cleanup of one-time events across the board (when an event is meant to fire once, it will now remove + itself from the EventDispatcher) +* Bug: `Guzzle\Log\MessageFormatter` now properly writes "total_time" and "connect_time" values +* Bug: Cloning an EntityEnclosingRequest now clones the EntityBody too +* Bug: Fixed an undefined index error when parsing nested JSON responses with a sentAs parameter that reference a + non-existent key +* Bug: All __call() method arguments are now required (helps with mocking frameworks) +* Deprecating Response::getRequest() and now using a shallow clone of a request object to remove a circular reference + to help with refcount based garbage collection of resources created by sending a request +* Deprecating ZF1 cache and log adapters. These will be removed in the next major version. +* Deprecating `Response::getPreviousResponse()` (method signature still exists, but it's deprecated). Use the + HistoryPlugin for a history. +* Added a `responseBody` alias for the `response_body` location +* Refactored internals to no longer rely on Response::getRequest() +* HistoryPlugin can now be cast to a string +* HistoryPlugin now logs transactions rather than requests and responses to more accurately keep track of the requests + and responses that are sent over the wire +* Added `getEffectiveUrl()` and `getRedirectCount()` to Response objects + +## 3.4.3 - 2013-04-30 + +* Bug fix: Fixing bug introduced in 3.4.2 where redirect responses are duplicated on the final redirected response +* Added a check to re-extract the temp cacert bundle from the phar before sending each request + +## 3.4.2 - 2013-04-29 + +* Bug fix: Stream objects now work correctly with "a" and "a+" modes +* Bug fix: Removing `Transfer-Encoding: chunked` header when a Content-Length is present +* Bug fix: AsyncPlugin no longer forces HEAD requests +* Bug fix: DateTime timezones are now properly handled when using the service description schema formatter +* Bug fix: CachePlugin now properly handles stale-if-error directives when a request to the origin server fails +* Setting a response on a request will write to the custom request body from the response body if one is specified +* LogPlugin now writes to php://output when STDERR is undefined +* Added the ability to set multiple POST files for the same key in a single call +* application/x-www-form-urlencoded POSTs now use the utf-8 charset by default +* Added the ability to queue CurlExceptions to the MockPlugin +* Cleaned up how manual responses are queued on requests (removed "queued_response" and now using request.before_send) +* Configuration loading now allows remote files + +## 3.4.1 - 2013-04-16 + +* Large refactoring to how CurlMulti handles work. There is now a proxy that sits in front of a pool of CurlMulti + handles. This greatly simplifies the implementation, fixes a couple bugs, and provides a small performance boost. +* Exceptions are now properly grouped when sending requests in parallel +* Redirects are now properly aggregated when a multi transaction fails +* Redirects now set the response on the original object even in the event of a failure +* Bug fix: Model names are now properly set even when using $refs +* Added support for PHP 5.5's CurlFile to prevent warnings with the deprecated @ syntax +* Added support for oauth_callback in OAuth signatures +* Added support for oauth_verifier in OAuth signatures +* Added support to attempt to retrieve a command first literally, then ucfirst, the with inflection + +## 3.4.0 - 2013-04-11 + +* Bug fix: URLs are now resolved correctly based on http://tools.ietf.org/html/rfc3986#section-5.2. #289 +* Bug fix: Absolute URLs with a path in a service description will now properly override the base URL. #289 +* Bug fix: Parsing a query string with a single PHP array value will now result in an array. #263 +* Bug fix: Better normalization of the User-Agent header to prevent duplicate headers. #264. +* Bug fix: Added `number` type to service descriptions. +* Bug fix: empty parameters are removed from an OAuth signature +* Bug fix: Revalidating a cache entry prefers the Last-Modified over the Date header +* Bug fix: Fixed "array to string" error when validating a union of types in a service description +* Bug fix: Removed code that attempted to determine the size of a stream when data is written to the stream +* Bug fix: Not including an `oauth_token` if the value is null in the OauthPlugin. +* Bug fix: Now correctly aggregating successful requests and failed requests in CurlMulti when a redirect occurs. +* The new default CURLOPT_TIMEOUT setting has been increased to 150 seconds so that Guzzle works on poor connections. +* Added a feature to EntityEnclosingRequest::setBody() that will automatically set the Content-Type of the request if + the Content-Type can be determined based on the entity body or the path of the request. +* Added the ability to overwrite configuration settings in a client when grabbing a throwaway client from a builder. +* Added support for a PSR-3 LogAdapter. +* Added a `command.after_prepare` event +* Added `oauth_callback` parameter to the OauthPlugin +* Added the ability to create a custom stream class when using a stream factory +* Added a CachingEntityBody decorator +* Added support for `additionalParameters` in service descriptions to define how custom parameters are serialized. +* The bundled SSL certificate is now provided in the phar file and extracted when running Guzzle from a phar. +* You can now send any EntityEnclosingRequest with POST fields or POST files and cURL will handle creating bodies +* POST requests using a custom entity body are now treated exactly like PUT requests but with a custom cURL method. This + means that the redirect behavior of POST requests with custom bodies will not be the same as POST requests that use + POST fields or files (the latter is only used when emulating a form POST in the browser). +* Lots of cleanup to CurlHandle::factory and RequestFactory::createRequest + +## 3.3.1 - 2013-03-10 + +* Added the ability to create PHP streaming responses from HTTP requests +* Bug fix: Running any filters when parsing response headers with service descriptions +* Bug fix: OauthPlugin fixes to allow for multi-dimensional array signing, and sorting parameters before signing +* Bug fix: Removed the adding of default empty arrays and false Booleans to responses in order to be consistent across + response location visitors. +* Bug fix: Removed the possibility of creating configuration files with circular dependencies +* RequestFactory::create() now uses the key of a POST file when setting the POST file name +* Added xmlAllowEmpty to serialize an XML body even if no XML specific parameters are set + +## 3.3.0 - 2013-03-03 + +* A large number of performance optimizations have been made +* Bug fix: Added 'wb' as a valid write mode for streams +* Bug fix: `Guzzle\Http\Message\Response::json()` now allows scalar values to be returned +* Bug fix: Fixed bug in `Guzzle\Http\Message\Response` where wrapping quotes were stripped from `getEtag()` +* BC: Removed `Guzzle\Http\Utils` class +* BC: Setting a service description on a client will no longer modify the client's command factories. +* BC: Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using + the 'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io' +* BC: `Guzzle\Stream\Stream::getWrapper()` and `Guzzle\Stream\Stream::getSteamType()` are no longer converted to + lowercase +* Operation parameter objects are now lazy loaded internally +* Added ErrorResponsePlugin that can throw errors for responses defined in service description operations' errorResponses +* Added support for instantiating responseType=class responseClass classes. Classes must implement + `Guzzle\Service\Command\ResponseClassInterface` +* Added support for additionalProperties for top-level parameters in responseType=model responseClasses. These + additional properties also support locations and can be used to parse JSON responses where the outermost part of the + JSON is an array +* Added support for nested renaming of JSON models (rename sentAs to name) +* CachePlugin + * Added support for stale-if-error so that the CachePlugin can now serve stale content from the cache on error + * Debug headers can now added to cached response in the CachePlugin + +## 3.2.0 - 2013-02-14 + +* CurlMulti is no longer reused globally. A new multi object is created per-client. This helps to isolate clients. +* URLs with no path no longer contain a "/" by default +* Guzzle\Http\QueryString does no longer manages the leading "?". This is now handled in Guzzle\Http\Url. +* BadResponseException no longer includes the full request and response message +* Adding setData() to Guzzle\Service\Description\ServiceDescriptionInterface +* Adding getResponseBody() to Guzzle\Http\Message\RequestInterface +* Various updates to classes to use ServiceDescriptionInterface type hints rather than ServiceDescription +* Header values can now be normalized into distinct values when multiple headers are combined with a comma separated list +* xmlEncoding can now be customized for the XML declaration of a XML service description operation +* Guzzle\Http\QueryString now uses Guzzle\Http\QueryAggregator\QueryAggregatorInterface objects to add custom value + aggregation and no longer uses callbacks +* The URL encoding implementation of Guzzle\Http\QueryString can now be customized +* Bug fix: Filters were not always invoked for array service description parameters +* Bug fix: Redirects now use a target response body rather than a temporary response body +* Bug fix: The default exponential backoff BackoffPlugin was not giving when the request threshold was exceeded +* Bug fix: Guzzle now takes the first found value when grabbing Cache-Control directives + +## 3.1.2 - 2013-01-27 + +* Refactored how operation responses are parsed. Visitors now include a before() method responsible for parsing the + response body. For example, the XmlVisitor now parses the XML response into an array in the before() method. +* Fixed an issue where cURL would not automatically decompress responses when the Accept-Encoding header was sent +* CURLOPT_SSL_VERIFYHOST is never set to 1 because it is deprecated (see 5e0ff2ef20f839e19d1eeb298f90ba3598784444) +* Fixed a bug where redirect responses were not chained correctly using getPreviousResponse() +* Setting default headers on a client after setting the user-agent will not erase the user-agent setting + +## 3.1.1 - 2013-01-20 + +* Adding wildcard support to Guzzle\Common\Collection::getPath() +* Adding alias support to ServiceBuilder configs +* Adding Guzzle\Service\Resource\CompositeResourceIteratorFactory and cleaning up factory interface + +## 3.1.0 - 2013-01-12 + +* BC: CurlException now extends from RequestException rather than BadResponseException +* BC: Renamed Guzzle\Plugin\Cache\CanCacheStrategyInterface::canCache() to canCacheRequest() and added CanCacheResponse() +* Added getData to ServiceDescriptionInterface +* Added context array to RequestInterface::setState() +* Bug: Removing hard dependency on the BackoffPlugin from Guzzle\Http +* Bug: Adding required content-type when JSON request visitor adds JSON to a command +* Bug: Fixing the serialization of a service description with custom data +* Made it easier to deal with exceptions thrown when transferring commands or requests in parallel by providing + an array of successful and failed responses +* Moved getPath from Guzzle\Service\Resource\Model to Guzzle\Common\Collection +* Added Guzzle\Http\IoEmittingEntityBody +* Moved command filtration from validators to location visitors +* Added `extends` attributes to service description parameters +* Added getModels to ServiceDescriptionInterface + +## 3.0.7 - 2012-12-19 + +* Fixing phar detection when forcing a cacert to system if null or true +* Allowing filename to be passed to `Guzzle\Http\Message\Request::setResponseBody()` +* Cleaning up `Guzzle\Common\Collection::inject` method +* Adding a response_body location to service descriptions + +## 3.0.6 - 2012-12-09 + +* CurlMulti performance improvements +* Adding setErrorResponses() to Operation +* composer.json tweaks + +## 3.0.5 - 2012-11-18 + +* Bug: Fixing an infinite recursion bug caused from revalidating with the CachePlugin +* Bug: Response body can now be a string containing "0" +* Bug: Using Guzzle inside of a phar uses system by default but now allows for a custom cacert +* Bug: QueryString::fromString now properly parses query string parameters that contain equal signs +* Added support for XML attributes in service description responses +* DefaultRequestSerializer now supports array URI parameter values for URI template expansion +* Added better mimetype guessing to requests and post files + +## 3.0.4 - 2012-11-11 + +* Bug: Fixed a bug when adding multiple cookies to a request to use the correct glue value +* Bug: Cookies can now be added that have a name, domain, or value set to "0" +* Bug: Using the system cacert bundle when using the Phar +* Added json and xml methods to Response to make it easier to parse JSON and XML response data into data structures +* Enhanced cookie jar de-duplication +* Added the ability to enable strict cookie jars that throw exceptions when invalid cookies are added +* Added setStream to StreamInterface to actually make it possible to implement custom rewind behavior for entity bodies +* Added the ability to create any sort of hash for a stream rather than just an MD5 hash + +## 3.0.3 - 2012-11-04 + +* Implementing redirects in PHP rather than cURL +* Added PECL URI template extension and using as default parser if available +* Bug: Fixed Content-Length parsing of Response factory +* Adding rewind() method to entity bodies and streams. Allows for custom rewinding of non-repeatable streams. +* Adding ToArrayInterface throughout library +* Fixing OauthPlugin to create unique nonce values per request + +## 3.0.2 - 2012-10-25 + +* Magic methods are enabled by default on clients +* Magic methods return the result of a command +* Service clients no longer require a base_url option in the factory +* Bug: Fixed an issue with URI templates where null template variables were being expanded + +## 3.0.1 - 2012-10-22 + +* Models can now be used like regular collection objects by calling filter, map, etc. +* Models no longer require a Parameter structure or initial data in the constructor +* Added a custom AppendIterator to get around a PHP bug with the `\AppendIterator` + +## 3.0.0 - 2012-10-15 + +* Rewrote service description format to be based on Swagger + * Now based on JSON schema + * Added nested input structures and nested response models + * Support for JSON and XML input and output models + * Renamed `commands` to `operations` + * Removed dot class notation + * Removed custom types +* Broke the project into smaller top-level namespaces to be more component friendly +* Removed support for XML configs and descriptions. Use arrays or JSON files. +* Removed the Validation component and Inspector +* Moved all cookie code to Guzzle\Plugin\Cookie +* Magic methods on a Guzzle\Service\Client now return the command un-executed. +* Calling getResult() or getResponse() on a command will lazily execute the command if needed. +* Now shipping with cURL's CA certs and using it by default +* Added previousResponse() method to response objects +* No longer sending Accept and Accept-Encoding headers on every request +* Only sending an Expect header by default when a payload is greater than 1MB +* Added/moved client options: + * curl.blacklist to curl.option.blacklist + * Added ssl.certificate_authority +* Added a Guzzle\Iterator component +* Moved plugins from Guzzle\Http\Plugin to Guzzle\Plugin +* Added a more robust backoff retry strategy (replaced the ExponentialBackoffPlugin) +* Added a more robust caching plugin +* Added setBody to response objects +* Updating LogPlugin to use a more flexible MessageFormatter +* Added a completely revamped build process +* Cleaning up Collection class and removing default values from the get method +* Fixed ZF2 cache adapters + +## 2.8.8 - 2012-10-15 + +* Bug: Fixed a cookie issue that caused dot prefixed domains to not match where popular browsers did + +## 2.8.7 - 2012-09-30 + +* Bug: Fixed config file aliases for JSON includes +* Bug: Fixed cookie bug on a request object by using CookieParser to parse cookies on requests +* Bug: Removing the path to a file when sending a Content-Disposition header on a POST upload +* Bug: Hardening request and response parsing to account for missing parts +* Bug: Fixed PEAR packaging +* Bug: Fixed Request::getInfo +* Bug: Fixed cases where CURLM_CALL_MULTI_PERFORM return codes were causing curl transactions to fail +* Adding the ability for the namespace Iterator factory to look in multiple directories +* Added more getters/setters/removers from service descriptions +* Added the ability to remove POST fields from OAuth signatures +* OAuth plugin now supports 2-legged OAuth + +## 2.8.6 - 2012-09-05 + +* Added the ability to modify and build service descriptions +* Added the use of visitors to apply parameters to locations in service descriptions using the dynamic command +* Added a `json` parameter location +* Now allowing dot notation for classes in the CacheAdapterFactory +* Using the union of two arrays rather than an array_merge when extending service builder services and service params +* Ensuring that a service is a string before doing strpos() checks on it when substituting services for references + in service builder config files. +* Services defined in two different config files that include one another will by default replace the previously + defined service, but you can now create services that extend themselves and merge their settings over the previous +* The JsonLoader now supports aliasing filenames with different filenames. This allows you to alias something like + '_default' with a default JSON configuration file. + +## 2.8.5 - 2012-08-29 + +* Bug: Suppressed empty arrays from URI templates +* Bug: Added the missing $options argument from ServiceDescription::factory to enable caching +* Added support for HTTP responses that do not contain a reason phrase in the start-line +* AbstractCommand commands are now invokable +* Added a way to get the data used when signing an Oauth request before a request is sent + +## 2.8.4 - 2012-08-15 + +* Bug: Custom delay time calculations are no longer ignored in the ExponentialBackoffPlugin +* Added the ability to transfer entity bodies as a string rather than streamed. This gets around curl error 65. Set `body_as_string` in a request's curl options to enable. +* Added a StreamInterface, EntityBodyInterface, and added ftell() to Guzzle\Common\Stream +* Added an AbstractEntityBodyDecorator and a ReadLimitEntityBody decorator to transfer only a subset of a decorated stream +* Stream and EntityBody objects will now return the file position to the previous position after a read required operation (e.g. getContentMd5()) +* Added additional response status codes +* Removed SSL information from the default User-Agent header +* DELETE requests can now send an entity body +* Added an EventDispatcher to the ExponentialBackoffPlugin and added an ExponentialBackoffLogger to log backoff retries +* Added the ability of the MockPlugin to consume mocked request bodies +* LogPlugin now exposes request and response objects in the extras array + +## 2.8.3 - 2012-07-30 + +* Bug: Fixed a case where empty POST requests were sent as GET requests +* Bug: Fixed a bug in ExponentialBackoffPlugin that caused fatal errors when retrying an EntityEnclosingRequest that does not have a body +* Bug: Setting the response body of a request to null after completing a request, not when setting the state of a request to new +* Added multiple inheritance to service description commands +* Added an ApiCommandInterface and added `getParamNames()` and `hasParam()` +* Removed the default 2mb size cutoff from the Md5ValidatorPlugin so that it now defaults to validating everything +* Changed CurlMulti::perform to pass a smaller timeout to CurlMulti::executeHandles + +## 2.8.2 - 2012-07-24 + +* Bug: Query string values set to 0 are no longer dropped from the query string +* Bug: A Collection object is no longer created each time a call is made to `Guzzle\Service\Command\AbstractCommand::getRequestHeaders()` +* Bug: `+` is now treated as an encoded space when parsing query strings +* QueryString and Collection performance improvements +* Allowing dot notation for class paths in filters attribute of a service descriptions + +## 2.8.1 - 2012-07-16 + +* Loosening Event Dispatcher dependency +* POST redirects can now be customized using CURLOPT_POSTREDIR + +## 2.8.0 - 2012-07-15 + +* BC: Guzzle\Http\Query + * Query strings with empty variables will always show an equal sign unless the variable is set to QueryString::BLANK (e.g. ?acl= vs ?acl) + * Changed isEncodingValues() and isEncodingFields() to isUrlEncoding() + * Changed setEncodeValues(bool) and setEncodeFields(bool) to useUrlEncoding(bool) + * Changed the aggregation functions of QueryString to be static methods + * Can now use fromString() with querystrings that have a leading ? +* cURL configuration values can be specified in service descriptions using `curl.` prefixed parameters +* Content-Length is set to 0 before emitting the request.before_send event when sending an empty request body +* Cookies are no longer URL decoded by default +* Bug: URI template variables set to null are no longer expanded + +## 2.7.2 - 2012-07-02 + +* BC: Moving things to get ready for subtree splits. Moving Inflection into Common. Moving Guzzle\Http\Parser to Guzzle\Parser. +* BC: Removing Guzzle\Common\Batch\Batch::count() and replacing it with isEmpty() +* CachePlugin now allows for a custom request parameter function to check if a request can be cached +* Bug fix: CachePlugin now only caches GET and HEAD requests by default +* Bug fix: Using header glue when transferring headers over the wire +* Allowing deeply nested arrays for composite variables in URI templates +* Batch divisors can now return iterators or arrays + +## 2.7.1 - 2012-06-26 + +* Minor patch to update version number in UA string +* Updating build process + +## 2.7.0 - 2012-06-25 + +* BC: Inflection classes moved to Guzzle\Inflection. No longer static methods. Can now inject custom inflectors into classes. +* BC: Removed magic setX methods from commands +* BC: Magic methods mapped to service description commands are now inflected in the command factory rather than the client __call() method +* Verbose cURL options are no longer enabled by default. Set curl.debug to true on a client to enable. +* Bug: Now allowing colons in a response start-line (e.g. HTTP/1.1 503 Service Unavailable: Back-end server is at capacity) +* Guzzle\Service\Resource\ResourceIteratorApplyBatched now internally uses the Guzzle\Common\Batch namespace +* Added Guzzle\Service\Plugin namespace and a PluginCollectionPlugin +* Added the ability to set POST fields and files in a service description +* Guzzle\Http\EntityBody::factory() now accepts objects with a __toString() method +* Adding a command.before_prepare event to clients +* Added BatchClosureTransfer and BatchClosureDivisor +* BatchTransferException now includes references to the batch divisor and transfer strategies +* Fixed some tests so that they pass more reliably +* Added Guzzle\Common\Log\ArrayLogAdapter + +## 2.6.6 - 2012-06-10 + +* BC: Removing Guzzle\Http\Plugin\BatchQueuePlugin +* BC: Removing Guzzle\Service\Command\CommandSet +* Adding generic batching system (replaces the batch queue plugin and command set) +* Updating ZF cache and log adapters and now using ZF's composer repository +* Bug: Setting the name of each ApiParam when creating through an ApiCommand +* Adding result_type, result_doc, deprecated, and doc_url to service descriptions +* Bug: Changed the default cookie header casing back to 'Cookie' + +## 2.6.5 - 2012-06-03 + +* BC: Renaming Guzzle\Http\Message\RequestInterface::getResourceUri() to getResource() +* BC: Removing unused AUTH_BASIC and AUTH_DIGEST constants from +* BC: Guzzle\Http\Cookie is now used to manage Set-Cookie data, not Cookie data +* BC: Renaming methods in the CookieJarInterface +* Moving almost all cookie logic out of the CookiePlugin and into the Cookie or CookieJar implementations +* Making the default glue for HTTP headers ';' instead of ',' +* Adding a removeValue to Guzzle\Http\Message\Header +* Adding getCookies() to request interface. +* Making it easier to add event subscribers to HasDispatcherInterface classes. Can now directly call addSubscriber() + +## 2.6.4 - 2012-05-30 + +* BC: Cleaning up how POST files are stored in EntityEnclosingRequest objects. Adding PostFile class. +* BC: Moving ApiCommand specific functionality from the Inspector and on to the ApiCommand +* Bug: Fixing magic method command calls on clients +* Bug: Email constraint only validates strings +* Bug: Aggregate POST fields when POST files are present in curl handle +* Bug: Fixing default User-Agent header +* Bug: Only appending or prepending parameters in commands if they are specified +* Bug: Not requiring response reason phrases or status codes to match a predefined list of codes +* Allowing the use of dot notation for class namespaces when using instance_of constraint +* Added any_match validation constraint +* Added an AsyncPlugin +* Passing request object to the calculateWait method of the ExponentialBackoffPlugin +* Allowing the result of a command object to be changed +* Parsing location and type sub values when instantiating a service description rather than over and over at runtime + +## 2.6.3 - 2012-05-23 + +* [BC] Guzzle\Common\FromConfigInterface no longer requires any config options. +* [BC] Refactoring how POST files are stored on an EntityEnclosingRequest. They are now separate from POST fields. +* You can now use an array of data when creating PUT request bodies in the request factory. +* Removing the requirement that HTTPS requests needed a Cache-Control: public directive to be cacheable. +* [Http] Adding support for Content-Type in multipart POST uploads per upload +* [Http] Added support for uploading multiple files using the same name (foo[0], foo[1]) +* Adding more POST data operations for easier manipulation of POST data. +* You can now set empty POST fields. +* The body of a request is only shown on EntityEnclosingRequest objects that do not use POST files. +* Split the Guzzle\Service\Inspector::validateConfig method into two methods. One to initialize when a command is created, and one to validate. +* CS updates + +## 2.6.2 - 2012-05-19 + +* [Http] Better handling of nested scope requests in CurlMulti. Requests are now always prepares in the send() method rather than the addRequest() method. + +## 2.6.1 - 2012-05-19 + +* [BC] Removing 'path' support in service descriptions. Use 'uri'. +* [BC] Guzzle\Service\Inspector::parseDocBlock is now protected. Adding getApiParamsForClass() with cache. +* [BC] Removing Guzzle\Common\NullObject. Use https://github.com/mtdowling/NullObject if you need it. +* [BC] Removing Guzzle\Common\XmlElement. +* All commands, both dynamic and concrete, have ApiCommand objects. +* Adding a fix for CurlMulti so that if all of the connections encounter some sort of curl error, then the loop exits. +* Adding checks to EntityEnclosingRequest so that empty POST files and fields are ignored. +* Making the method signature of Guzzle\Service\Builder\ServiceBuilder::factory more flexible. + +## 2.6.0 - 2012-05-15 + +* [BC] Moving Guzzle\Service\Builder to Guzzle\Service\Builder\ServiceBuilder +* [BC] Executing a Command returns the result of the command rather than the command +* [BC] Moving all HTTP parsing logic to Guzzle\Http\Parsers. Allows for faster C implementations if needed. +* [BC] Changing the Guzzle\Http\Message\Response::setProtocol() method to accept a protocol and version in separate args. +* [BC] Moving ResourceIterator* to Guzzle\Service\Resource +* [BC] Completely refactored ResourceIterators to iterate over a cloned command object +* [BC] Moved Guzzle\Http\UriTemplate to Guzzle\Http\Parser\UriTemplate\UriTemplate +* [BC] Guzzle\Guzzle is now deprecated +* Moving Guzzle\Common\Guzzle::inject to Guzzle\Common\Collection::inject +* Adding Guzzle\Version class to give version information about Guzzle +* Adding Guzzle\Http\Utils class to provide getDefaultUserAgent() and getHttpDate() +* Adding Guzzle\Curl\CurlVersion to manage caching curl_version() data +* ServiceDescription and ServiceBuilder are now cacheable using similar configs +* Changing the format of XML and JSON service builder configs. Backwards compatible. +* Cleaned up Cookie parsing +* Trimming the default Guzzle User-Agent header +* Adding a setOnComplete() method to Commands that is called when a command completes +* Keeping track of requests that were mocked in the MockPlugin +* Fixed a caching bug in the CacheAdapterFactory +* Inspector objects can be injected into a Command object +* Refactoring a lot of code and tests to be case insensitive when dealing with headers +* Adding Guzzle\Http\Message\HeaderComparison for easy comparison of HTTP headers using a DSL +* Adding the ability to set global option overrides to service builder configs +* Adding the ability to include other service builder config files from within XML and JSON files +* Moving the parseQuery method out of Url and on to QueryString::fromString() as a static factory method. + +## 2.5.0 - 2012-05-08 + +* Major performance improvements +* [BC] Simplifying Guzzle\Common\Collection. Please check to see if you are using features that are now deprecated. +* [BC] Using a custom validation system that allows a flyweight implementation for much faster validation. No longer using Symfony2 Validation component. +* [BC] No longer supporting "{{ }}" for injecting into command or UriTemplates. Use "{}" +* Added the ability to passed parameters to all requests created by a client +* Added callback functionality to the ExponentialBackoffPlugin +* Using microtime in ExponentialBackoffPlugin to allow more granular backoff strategies. +* Rewinding request stream bodies when retrying requests +* Exception is thrown when JSON response body cannot be decoded +* Added configurable magic method calls to clients and commands. This is off by default. +* Fixed a defect that added a hash to every parsed URL part +* Fixed duplicate none generation for OauthPlugin. +* Emitting an event each time a client is generated by a ServiceBuilder +* Using an ApiParams object instead of a Collection for parameters of an ApiCommand +* cache.* request parameters should be renamed to params.cache.* +* Added the ability to set arbitrary curl options on requests (disable_wire, progress, etc.). See CurlHandle. +* Added the ability to disable type validation of service descriptions +* ServiceDescriptions and ServiceBuilders are now Serializable diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/Dockerfile b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/Dockerfile new file mode 100644 index 0000000..f6a0952 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/Dockerfile @@ -0,0 +1,18 @@ +FROM composer:latest as setup + +RUN mkdir /guzzle + +WORKDIR /guzzle + +RUN set -xe \ + && composer init --name=guzzlehttp/test --description="Simple project for testing Guzzle scripts" --author="Márk Sági-Kazár " --no-interaction \ + && composer require guzzlehttp/guzzle + + +FROM php:7.3 + +RUN mkdir /guzzle + +WORKDIR /guzzle + +COPY --from=setup /guzzle /guzzle diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/LICENSE b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/LICENSE new file mode 100644 index 0000000..50a177b --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011-2018 Michael Dowling, https://github.com/mtdowling + +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/plugins/panakour/backup/vendor/guzzlehttp/guzzle/README.md b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/README.md new file mode 100644 index 0000000..5fdb6c5 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/README.md @@ -0,0 +1,90 @@ +Guzzle, PHP HTTP client +======================= + +[![Latest Version](https://img.shields.io/github/release/guzzle/guzzle.svg?style=flat-square)](https://github.com/guzzle/guzzle/releases) +[![Build Status](https://img.shields.io/travis/guzzle/guzzle.svg?style=flat-square)](https://travis-ci.org/guzzle/guzzle) +[![Total Downloads](https://img.shields.io/packagist/dt/guzzlehttp/guzzle.svg?style=flat-square)](https://packagist.org/packages/guzzlehttp/guzzle) + +Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and +trivial to integrate with web services. + +- Simple interface for building query strings, POST requests, streaming large + uploads, streaming large downloads, using HTTP cookies, uploading JSON data, + etc... +- Can send both synchronous and asynchronous requests using the same interface. +- Uses PSR-7 interfaces for requests, responses, and streams. This allows you + to utilize other PSR-7 compatible libraries with Guzzle. +- Abstracts away the underlying HTTP transport, allowing you to write + environment and transport agnostic code; i.e., no hard dependency on cURL, + PHP streams, sockets, or non-blocking event loops. +- Middleware system allows you to augment and compose client behavior. + +```php +$client = new \GuzzleHttp\Client(); +$response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle'); + +echo $response->getStatusCode(); # 200 +echo $response->getHeaderLine('content-type'); # 'application/json; charset=utf8' +echo $response->getBody(); # '{"id": 1420053, "name": "guzzle", ...}' + +# Send an asynchronous request. +$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org'); +$promise = $client->sendAsync($request)->then(function ($response) { + echo 'I completed! ' . $response->getBody(); +}); + +$promise->wait(); +``` + +## Help and docs + +- [Documentation](http://guzzlephp.org/) +- [Stack Overflow](http://stackoverflow.com/questions/tagged/guzzle) +- [Gitter](https://gitter.im/guzzle/guzzle) + + +## Installing Guzzle + +The recommended way to install Guzzle is through +[Composer](http://getcomposer.org). + +```bash +# Install Composer +curl -sS https://getcomposer.org/installer | php +``` + +Next, run the Composer command to install the latest stable version of Guzzle: + +```bash +composer require guzzlehttp/guzzle +``` + +After installing, you need to require Composer's autoloader: + +```php +require 'vendor/autoload.php'; +``` + +You can then later update Guzzle using composer: + + ```bash +composer update + ``` + + +## Version Guidance + +| Version | Status | Packagist | Namespace | Repo | Docs | PSR-7 | PHP Version | +|---------|------------|---------------------|--------------|---------------------|---------------------|-------|-------------| +| 3.x | EOL | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No | >= 5.3.3 | +| 4.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A | No | >= 5.4 | +| 5.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No | >= 5.4 | +| 6.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes | >= 5.5 | + +[guzzle-3-repo]: https://github.com/guzzle/guzzle3 +[guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x +[guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3 +[guzzle-6-repo]: https://github.com/guzzle/guzzle +[guzzle-3-docs]: http://guzzle3.readthedocs.org +[guzzle-5-docs]: http://guzzle.readthedocs.org/en/5.3/ +[guzzle-6-docs]: http://guzzle.readthedocs.org/en/latest/ diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/UPGRADING.md b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/UPGRADING.md new file mode 100644 index 0000000..91d1dcc --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/UPGRADING.md @@ -0,0 +1,1203 @@ +Guzzle Upgrade Guide +==================== + +5.0 to 6.0 +---------- + +Guzzle now uses [PSR-7](http://www.php-fig.org/psr/psr-7/) for HTTP messages. +Due to the fact that these messages are immutable, this prompted a refactoring +of Guzzle to use a middleware based system rather than an event system. Any +HTTP message interaction (e.g., `GuzzleHttp\Message\Request`) need to be +updated to work with the new immutable PSR-7 request and response objects. Any +event listeners or subscribers need to be updated to become middleware +functions that wrap handlers (or are injected into a +`GuzzleHttp\HandlerStack`). + +- Removed `GuzzleHttp\BatchResults` +- Removed `GuzzleHttp\Collection` +- Removed `GuzzleHttp\HasDataTrait` +- Removed `GuzzleHttp\ToArrayInterface` +- The `guzzlehttp/streams` dependency has been removed. Stream functionality + is now present in the `GuzzleHttp\Psr7` namespace provided by the + `guzzlehttp/psr7` package. +- Guzzle no longer uses ReactPHP promises and now uses the + `guzzlehttp/promises` library. We use a custom promise library for three + significant reasons: + 1. React promises (at the time of writing this) are recursive. Promise + chaining and promise resolution will eventually blow the stack. Guzzle + promises are not recursive as they use a sort of trampolining technique. + Note: there has been movement in the React project to modify promises to + no longer utilize recursion. + 2. Guzzle needs to have the ability to synchronously block on a promise to + wait for a result. Guzzle promises allows this functionality (and does + not require the use of recursion). + 3. Because we need to be able to wait on a result, doing so using React + promises requires wrapping react promises with RingPHP futures. This + overhead is no longer needed, reducing stack sizes, reducing complexity, + and improving performance. +- `GuzzleHttp\Mimetypes` has been moved to a function in + `GuzzleHttp\Psr7\mimetype_from_extension` and + `GuzzleHttp\Psr7\mimetype_from_filename`. +- `GuzzleHttp\Query` and `GuzzleHttp\QueryParser` have been removed. Query + strings must now be passed into request objects as strings, or provided to + the `query` request option when creating requests with clients. The `query` + option uses PHP's `http_build_query` to convert an array to a string. If you + need a different serialization technique, you will need to pass the query + string in as a string. There are a couple helper functions that will make + working with query strings easier: `GuzzleHttp\Psr7\parse_query` and + `GuzzleHttp\Psr7\build_query`. +- Guzzle no longer has a dependency on RingPHP. Due to the use of a middleware + system based on PSR-7, using RingPHP and it's middleware system as well adds + more complexity than the benefits it provides. All HTTP handlers that were + present in RingPHP have been modified to work directly with PSR-7 messages + and placed in the `GuzzleHttp\Handler` namespace. This significantly reduces + complexity in Guzzle, removes a dependency, and improves performance. RingPHP + will be maintained for Guzzle 5 support, but will no longer be a part of + Guzzle 6. +- As Guzzle now uses a middleware based systems the event system and RingPHP + integration has been removed. Note: while the event system has been removed, + it is possible to add your own type of event system that is powered by the + middleware system. + - Removed the `Event` namespace. + - Removed the `Subscriber` namespace. + - Removed `Transaction` class + - Removed `RequestFsm` + - Removed `RingBridge` + - `GuzzleHttp\Subscriber\Cookie` is now provided by + `GuzzleHttp\Middleware::cookies` + - `GuzzleHttp\Subscriber\HttpError` is now provided by + `GuzzleHttp\Middleware::httpError` + - `GuzzleHttp\Subscriber\History` is now provided by + `GuzzleHttp\Middleware::history` + - `GuzzleHttp\Subscriber\Mock` is now provided by + `GuzzleHttp\Handler\MockHandler` + - `GuzzleHttp\Subscriber\Prepare` is now provided by + `GuzzleHttp\PrepareBodyMiddleware` + - `GuzzleHttp\Subscriber\Redirect` is now provided by + `GuzzleHttp\RedirectMiddleware` +- Guzzle now uses `Psr\Http\Message\UriInterface` (implements in + `GuzzleHttp\Psr7\Uri`) for URI support. `GuzzleHttp\Url` is now gone. +- Static functions in `GuzzleHttp\Utils` have been moved to namespaced + functions under the `GuzzleHttp` namespace. This requires either a Composer + based autoloader or you to include functions.php. +- `GuzzleHttp\ClientInterface::getDefaultOption` has been renamed to + `GuzzleHttp\ClientInterface::getConfig`. +- `GuzzleHttp\ClientInterface::setDefaultOption` has been removed. +- The `json` and `xml` methods of response objects has been removed. With the + migration to strictly adhering to PSR-7 as the interface for Guzzle messages, + adding methods to message interfaces would actually require Guzzle messages + to extend from PSR-7 messages rather then work with them directly. + +## Migrating to middleware + +The change to PSR-7 unfortunately required significant refactoring to Guzzle +due to the fact that PSR-7 messages are immutable. Guzzle 5 relied on an event +system from plugins. The event system relied on mutability of HTTP messages and +side effects in order to work. With immutable messages, you have to change your +workflow to become more about either returning a value (e.g., functional +middlewares) or setting a value on an object. Guzzle v6 has chosen the +functional middleware approach. + +Instead of using the event system to listen for things like the `before` event, +you now create a stack based middleware function that intercepts a request on +the way in and the promise of the response on the way out. This is a much +simpler and more predictable approach than the event system and works nicely +with PSR-7 middleware. Due to the use of promises, the middleware system is +also asynchronous. + +v5: + +```php +use GuzzleHttp\Event\BeforeEvent; +$client = new GuzzleHttp\Client(); +// Get the emitter and listen to the before event. +$client->getEmitter()->on('before', function (BeforeEvent $e) { + // Guzzle v5 events relied on mutation + $e->getRequest()->setHeader('X-Foo', 'Bar'); +}); +``` + +v6: + +In v6, you can modify the request before it is sent using the `mapRequest` +middleware. The idiomatic way in v6 to modify the request/response lifecycle is +to setup a handler middleware stack up front and inject the handler into a +client. + +```php +use GuzzleHttp\Middleware; +// Create a handler stack that has all of the default middlewares attached +$handler = GuzzleHttp\HandlerStack::create(); +// Push the handler onto the handler stack +$handler->push(Middleware::mapRequest(function (RequestInterface $request) { + // Notice that we have to return a request object + return $request->withHeader('X-Foo', 'Bar'); +})); +// Inject the handler into the client +$client = new GuzzleHttp\Client(['handler' => $handler]); +``` + +## POST Requests + +This version added the [`form_params`](http://guzzle.readthedocs.org/en/latest/request-options.html#form_params) +and `multipart` request options. `form_params` is an associative array of +strings or array of strings and is used to serialize an +`application/x-www-form-urlencoded` POST request. The +[`multipart`](http://guzzle.readthedocs.org/en/latest/request-options.html#multipart) +option is now used to send a multipart/form-data POST request. + +`GuzzleHttp\Post\PostFile` has been removed. Use the `multipart` option to add +POST files to a multipart/form-data request. + +The `body` option no longer accepts an array to send POST requests. Please use +`multipart` or `form_params` instead. + +The `base_url` option has been renamed to `base_uri`. + +4.x to 5.0 +---------- + +## Rewritten Adapter Layer + +Guzzle now uses [RingPHP](http://ringphp.readthedocs.org/en/latest) to send +HTTP requests. The `adapter` option in a `GuzzleHttp\Client` constructor +is still supported, but it has now been renamed to `handler`. Instead of +passing a `GuzzleHttp\Adapter\AdapterInterface`, you must now pass a PHP +`callable` that follows the RingPHP specification. + +## Removed Fluent Interfaces + +[Fluent interfaces were removed](http://ocramius.github.io/blog/fluent-interfaces-are-evil) +from the following classes: + +- `GuzzleHttp\Collection` +- `GuzzleHttp\Url` +- `GuzzleHttp\Query` +- `GuzzleHttp\Post\PostBody` +- `GuzzleHttp\Cookie\SetCookie` + +## Removed functions.php + +Removed "functions.php", so that Guzzle is truly PSR-4 compliant. The following +functions can be used as replacements. + +- `GuzzleHttp\json_decode` -> `GuzzleHttp\Utils::jsonDecode` +- `GuzzleHttp\get_path` -> `GuzzleHttp\Utils::getPath` +- `GuzzleHttp\Utils::setPath` -> `GuzzleHttp\set_path` +- `GuzzleHttp\Pool::batch` -> `GuzzleHttp\batch`. This function is, however, + deprecated in favor of using `GuzzleHttp\Pool::batch()`. + +The "procedural" global client has been removed with no replacement (e.g., +`GuzzleHttp\get()`, `GuzzleHttp\post()`, etc.). Use a `GuzzleHttp\Client` +object as a replacement. + +## `throwImmediately` has been removed + +The concept of "throwImmediately" has been removed from exceptions and error +events. This control mechanism was used to stop a transfer of concurrent +requests from completing. This can now be handled by throwing the exception or +by cancelling a pool of requests or each outstanding future request +individually. + +## headers event has been removed + +Removed the "headers" event. This event was only useful for changing the +body a response once the headers of the response were known. You can implement +a similar behavior in a number of ways. One example might be to use a +FnStream that has access to the transaction being sent. For example, when the +first byte is written, you could check if the response headers match your +expectations, and if so, change the actual stream body that is being +written to. + +## Updates to HTTP Messages + +Removed the `asArray` parameter from +`GuzzleHttp\Message\MessageInterface::getHeader`. If you want to get a header +value as an array, then use the newly added `getHeaderAsArray()` method of +`MessageInterface`. This change makes the Guzzle interfaces compatible with +the PSR-7 interfaces. + +3.x to 4.0 +---------- + +## Overarching changes: + +- Now requires PHP 5.4 or greater. +- No longer requires cURL to send requests. +- Guzzle no longer wraps every exception it throws. Only exceptions that are + recoverable are now wrapped by Guzzle. +- Various namespaces have been removed or renamed. +- No longer requiring the Symfony EventDispatcher. A custom event dispatcher + based on the Symfony EventDispatcher is + now utilized in `GuzzleHttp\Event\EmitterInterface` (resulting in significant + speed and functionality improvements). + +Changes per Guzzle 3.x namespace are described below. + +## Batch + +The `Guzzle\Batch` namespace has been removed. This is best left to +third-parties to implement on top of Guzzle's core HTTP library. + +## Cache + +The `Guzzle\Cache` namespace has been removed. (Todo: No suitable replacement +has been implemented yet, but hoping to utilize a PSR cache interface). + +## Common + +- Removed all of the wrapped exceptions. It's better to use the standard PHP + library for unrecoverable exceptions. +- `FromConfigInterface` has been removed. +- `Guzzle\Common\Version` has been removed. The VERSION constant can be found + at `GuzzleHttp\ClientInterface::VERSION`. + +### Collection + +- `getAll` has been removed. Use `toArray` to convert a collection to an array. +- `inject` has been removed. +- `keySearch` has been removed. +- `getPath` no longer supports wildcard expressions. Use something better like + JMESPath for this. +- `setPath` now supports appending to an existing array via the `[]` notation. + +### Events + +Guzzle no longer requires Symfony's EventDispatcher component. Guzzle now uses +`GuzzleHttp\Event\Emitter`. + +- `Symfony\Component\EventDispatcher\EventDispatcherInterface` is replaced by + `GuzzleHttp\Event\EmitterInterface`. +- `Symfony\Component\EventDispatcher\EventDispatcher` is replaced by + `GuzzleHttp\Event\Emitter`. +- `Symfony\Component\EventDispatcher\Event` is replaced by + `GuzzleHttp\Event\Event`, and Guzzle now has an EventInterface in + `GuzzleHttp\Event\EventInterface`. +- `AbstractHasDispatcher` has moved to a trait, `HasEmitterTrait`, and + `HasDispatcherInterface` has moved to `HasEmitterInterface`. Retrieving the + event emitter of a request, client, etc. now uses the `getEmitter` method + rather than the `getDispatcher` method. + +#### Emitter + +- Use the `once()` method to add a listener that automatically removes itself + the first time it is invoked. +- Use the `listeners()` method to retrieve a list of event listeners rather than + the `getListeners()` method. +- Use `emit()` instead of `dispatch()` to emit an event from an emitter. +- Use `attach()` instead of `addSubscriber()` and `detach()` instead of + `removeSubscriber()`. + +```php +$mock = new Mock(); +// 3.x +$request->getEventDispatcher()->addSubscriber($mock); +$request->getEventDispatcher()->removeSubscriber($mock); +// 4.x +$request->getEmitter()->attach($mock); +$request->getEmitter()->detach($mock); +``` + +Use the `on()` method to add a listener rather than the `addListener()` method. + +```php +// 3.x +$request->getEventDispatcher()->addListener('foo', function (Event $event) { /* ... */ } ); +// 4.x +$request->getEmitter()->on('foo', function (Event $event, $name) { /* ... */ } ); +``` + +## Http + +### General changes + +- The cacert.pem certificate has been moved to `src/cacert.pem`. +- Added the concept of adapters that are used to transfer requests over the + wire. +- Simplified the event system. +- Sending requests in parallel is still possible, but batching is no longer a + concept of the HTTP layer. Instead, you must use the `complete` and `error` + events to asynchronously manage parallel request transfers. +- `Guzzle\Http\Url` has moved to `GuzzleHttp\Url`. +- `Guzzle\Http\QueryString` has moved to `GuzzleHttp\Query`. +- QueryAggregators have been rewritten so that they are simply callable + functions. +- `GuzzleHttp\StaticClient` has been removed. Use the functions provided in + `functions.php` for an easy to use static client instance. +- Exceptions in `GuzzleHttp\Exception` have been updated to all extend from + `GuzzleHttp\Exception\TransferException`. + +### Client + +Calling methods like `get()`, `post()`, `head()`, etc. no longer create and +return a request, but rather creates a request, sends the request, and returns +the response. + +```php +// 3.0 +$request = $client->get('/'); +$response = $request->send(); + +// 4.0 +$response = $client->get('/'); + +// or, to mirror the previous behavior +$request = $client->createRequest('GET', '/'); +$response = $client->send($request); +``` + +`GuzzleHttp\ClientInterface` has changed. + +- The `send` method no longer accepts more than one request. Use `sendAll` to + send multiple requests in parallel. +- `setUserAgent()` has been removed. Use a default request option instead. You + could, for example, do something like: + `$client->setConfig('defaults/headers/User-Agent', 'Foo/Bar ' . $client::getDefaultUserAgent())`. +- `setSslVerification()` has been removed. Use default request options instead, + like `$client->setConfig('defaults/verify', true)`. + +`GuzzleHttp\Client` has changed. + +- The constructor now accepts only an associative array. You can include a + `base_url` string or array to use a URI template as the base URL of a client. + You can also specify a `defaults` key that is an associative array of default + request options. You can pass an `adapter` to use a custom adapter, + `batch_adapter` to use a custom adapter for sending requests in parallel, or + a `message_factory` to change the factory used to create HTTP requests and + responses. +- The client no longer emits a `client.create_request` event. +- Creating requests with a client no longer automatically utilize a URI + template. You must pass an array into a creational method (e.g., + `createRequest`, `get`, `put`, etc.) in order to expand a URI template. + +### Messages + +Messages no longer have references to their counterparts (i.e., a request no +longer has a reference to it's response, and a response no loger has a +reference to its request). This association is now managed through a +`GuzzleHttp\Adapter\TransactionInterface` object. You can get references to +these transaction objects using request events that are emitted over the +lifecycle of a request. + +#### Requests with a body + +- `GuzzleHttp\Message\EntityEnclosingRequest` and + `GuzzleHttp\Message\EntityEnclosingRequestInterface` have been removed. The + separation between requests that contain a body and requests that do not + contain a body has been removed, and now `GuzzleHttp\Message\RequestInterface` + handles both use cases. +- Any method that previously accepts a `GuzzleHttp\Response` object now accept a + `GuzzleHttp\Message\ResponseInterface`. +- `GuzzleHttp\Message\RequestFactoryInterface` has been renamed to + `GuzzleHttp\Message\MessageFactoryInterface`. This interface is used to create + both requests and responses and is implemented in + `GuzzleHttp\Message\MessageFactory`. +- POST field and file methods have been removed from the request object. You + must now use the methods made available to `GuzzleHttp\Post\PostBodyInterface` + to control the format of a POST body. Requests that are created using a + standard `GuzzleHttp\Message\MessageFactoryInterface` will automatically use + a `GuzzleHttp\Post\PostBody` body if the body was passed as an array or if + the method is POST and no body is provided. + +```php +$request = $client->createRequest('POST', '/'); +$request->getBody()->setField('foo', 'bar'); +$request->getBody()->addFile(new PostFile('file_key', fopen('/path/to/content', 'r'))); +``` + +#### Headers + +- `GuzzleHttp\Message\Header` has been removed. Header values are now simply + represented by an array of values or as a string. Header values are returned + as a string by default when retrieving a header value from a message. You can + pass an optional argument of `true` to retrieve a header value as an array + of strings instead of a single concatenated string. +- `GuzzleHttp\PostFile` and `GuzzleHttp\PostFileInterface` have been moved to + `GuzzleHttp\Post`. This interface has been simplified and now allows the + addition of arbitrary headers. +- Custom headers like `GuzzleHttp\Message\Header\Link` have been removed. Most + of the custom headers are now handled separately in specific + subscribers/plugins, and `GuzzleHttp\Message\HeaderValues::parseParams()` has + been updated to properly handle headers that contain parameters (like the + `Link` header). + +#### Responses + +- `GuzzleHttp\Message\Response::getInfo()` and + `GuzzleHttp\Message\Response::setInfo()` have been removed. Use the event + system to retrieve this type of information. +- `GuzzleHttp\Message\Response::getRawHeaders()` has been removed. +- `GuzzleHttp\Message\Response::getMessage()` has been removed. +- `GuzzleHttp\Message\Response::calculateAge()` and other cache specific + methods have moved to the CacheSubscriber. +- Header specific helper functions like `getContentMd5()` have been removed. + Just use `getHeader('Content-MD5')` instead. +- `GuzzleHttp\Message\Response::setRequest()` and + `GuzzleHttp\Message\Response::getRequest()` have been removed. Use the event + system to work with request and response objects as a transaction. +- `GuzzleHttp\Message\Response::getRedirectCount()` has been removed. Use the + Redirect subscriber instead. +- `GuzzleHttp\Message\Response::isSuccessful()` and other related methods have + been removed. Use `getStatusCode()` instead. + +#### Streaming responses + +Streaming requests can now be created by a client directly, returning a +`GuzzleHttp\Message\ResponseInterface` object that contains a body stream +referencing an open PHP HTTP stream. + +```php +// 3.0 +use Guzzle\Stream\PhpStreamRequestFactory; +$request = $client->get('/'); +$factory = new PhpStreamRequestFactory(); +$stream = $factory->fromRequest($request); +$data = $stream->read(1024); + +// 4.0 +$response = $client->get('/', ['stream' => true]); +// Read some data off of the stream in the response body +$data = $response->getBody()->read(1024); +``` + +#### Redirects + +The `configureRedirects()` method has been removed in favor of a +`allow_redirects` request option. + +```php +// Standard redirects with a default of a max of 5 redirects +$request = $client->createRequest('GET', '/', ['allow_redirects' => true]); + +// Strict redirects with a custom number of redirects +$request = $client->createRequest('GET', '/', [ + 'allow_redirects' => ['max' => 5, 'strict' => true] +]); +``` + +#### EntityBody + +EntityBody interfaces and classes have been removed or moved to +`GuzzleHttp\Stream`. All classes and interfaces that once required +`GuzzleHttp\EntityBodyInterface` now require +`GuzzleHttp\Stream\StreamInterface`. Creating a new body for a request no +longer uses `GuzzleHttp\EntityBody::factory` but now uses +`GuzzleHttp\Stream\Stream::factory` or even better: +`GuzzleHttp\Stream\create()`. + +- `Guzzle\Http\EntityBodyInterface` is now `GuzzleHttp\Stream\StreamInterface` +- `Guzzle\Http\EntityBody` is now `GuzzleHttp\Stream\Stream` +- `Guzzle\Http\CachingEntityBody` is now `GuzzleHttp\Stream\CachingStream` +- `Guzzle\Http\ReadLimitEntityBody` is now `GuzzleHttp\Stream\LimitStream` +- `Guzzle\Http\IoEmittyinEntityBody` has been removed. + +#### Request lifecycle events + +Requests previously submitted a large number of requests. The number of events +emitted over the lifecycle of a request has been significantly reduced to make +it easier to understand how to extend the behavior of a request. All events +emitted during the lifecycle of a request now emit a custom +`GuzzleHttp\Event\EventInterface` object that contains context providing +methods and a way in which to modify the transaction at that specific point in +time (e.g., intercept the request and set a response on the transaction). + +- `request.before_send` has been renamed to `before` and now emits a + `GuzzleHttp\Event\BeforeEvent` +- `request.complete` has been renamed to `complete` and now emits a + `GuzzleHttp\Event\CompleteEvent`. +- `request.sent` has been removed. Use `complete`. +- `request.success` has been removed. Use `complete`. +- `error` is now an event that emits a `GuzzleHttp\Event\ErrorEvent`. +- `request.exception` has been removed. Use `error`. +- `request.receive.status_line` has been removed. +- `curl.callback.progress` has been removed. Use a custom `StreamInterface` to + maintain a status update. +- `curl.callback.write` has been removed. Use a custom `StreamInterface` to + intercept writes. +- `curl.callback.read` has been removed. Use a custom `StreamInterface` to + intercept reads. + +`headers` is a new event that is emitted after the response headers of a +request have been received before the body of the response is downloaded. This +event emits a `GuzzleHttp\Event\HeadersEvent`. + +You can intercept a request and inject a response using the `intercept()` event +of a `GuzzleHttp\Event\BeforeEvent`, `GuzzleHttp\Event\CompleteEvent`, and +`GuzzleHttp\Event\ErrorEvent` event. + +See: http://docs.guzzlephp.org/en/latest/events.html + +## Inflection + +The `Guzzle\Inflection` namespace has been removed. This is not a core concern +of Guzzle. + +## Iterator + +The `Guzzle\Iterator` namespace has been removed. + +- `Guzzle\Iterator\AppendIterator`, `Guzzle\Iterator\ChunkedIterator`, and + `Guzzle\Iterator\MethodProxyIterator` are nice, but not a core requirement of + Guzzle itself. +- `Guzzle\Iterator\FilterIterator` is no longer needed because an equivalent + class is shipped with PHP 5.4. +- `Guzzle\Iterator\MapIterator` is not really needed when using PHP 5.5 because + it's easier to just wrap an iterator in a generator that maps values. + +For a replacement of these iterators, see https://github.com/nikic/iter + +## Log + +The LogPlugin has moved to https://github.com/guzzle/log-subscriber. The +`Guzzle\Log` namespace has been removed. Guzzle now relies on +`Psr\Log\LoggerInterface` for all logging. The MessageFormatter class has been +moved to `GuzzleHttp\Subscriber\Log\Formatter`. + +## Parser + +The `Guzzle\Parser` namespace has been removed. This was previously used to +make it possible to plug in custom parsers for cookies, messages, URI +templates, and URLs; however, this level of complexity is not needed in Guzzle +so it has been removed. + +- Cookie: Cookie parsing logic has been moved to + `GuzzleHttp\Cookie\SetCookie::fromString`. +- Message: Message parsing logic for both requests and responses has been moved + to `GuzzleHttp\Message\MessageFactory::fromMessage`. Message parsing is only + used in debugging or deserializing messages, so it doesn't make sense for + Guzzle as a library to add this level of complexity to parsing messages. +- UriTemplate: URI template parsing has been moved to + `GuzzleHttp\UriTemplate`. The Guzzle library will automatically use the PECL + URI template library if it is installed. +- Url: URL parsing is now performed in `GuzzleHttp\Url::fromString` (previously + it was `Guzzle\Http\Url::factory()`). If custom URL parsing is necessary, + then developers are free to subclass `GuzzleHttp\Url`. + +## Plugin + +The `Guzzle\Plugin` namespace has been renamed to `GuzzleHttp\Subscriber`. +Several plugins are shipping with the core Guzzle library under this namespace. + +- `GuzzleHttp\Subscriber\Cookie`: Replaces the old CookiePlugin. Cookie jar + code has moved to `GuzzleHttp\Cookie`. +- `GuzzleHttp\Subscriber\History`: Replaces the old HistoryPlugin. +- `GuzzleHttp\Subscriber\HttpError`: Throws errors when a bad HTTP response is + received. +- `GuzzleHttp\Subscriber\Mock`: Replaces the old MockPlugin. +- `GuzzleHttp\Subscriber\Prepare`: Prepares the body of a request just before + sending. This subscriber is attached to all requests by default. +- `GuzzleHttp\Subscriber\Redirect`: Replaces the RedirectPlugin. + +The following plugins have been removed (third-parties are free to re-implement +these if needed): + +- `GuzzleHttp\Plugin\Async` has been removed. +- `GuzzleHttp\Plugin\CurlAuth` has been removed. +- `GuzzleHttp\Plugin\ErrorResponse\ErrorResponsePlugin` has been removed. This + functionality should instead be implemented with event listeners that occur + after normal response parsing occurs in the guzzle/command package. + +The following plugins are not part of the core Guzzle package, but are provided +in separate repositories: + +- `Guzzle\Http\Plugin\BackoffPlugin` has been rewritten to be much simpler + to build custom retry policies using simple functions rather than various + chained classes. See: https://github.com/guzzle/retry-subscriber +- `Guzzle\Http\Plugin\Cache\CachePlugin` has moved to + https://github.com/guzzle/cache-subscriber +- `Guzzle\Http\Plugin\Log\LogPlugin` has moved to + https://github.com/guzzle/log-subscriber +- `Guzzle\Http\Plugin\Md5\Md5Plugin` has moved to + https://github.com/guzzle/message-integrity-subscriber +- `Guzzle\Http\Plugin\Mock\MockPlugin` has moved to + `GuzzleHttp\Subscriber\MockSubscriber`. +- `Guzzle\Http\Plugin\Oauth\OauthPlugin` has moved to + https://github.com/guzzle/oauth-subscriber + +## Service + +The service description layer of Guzzle has moved into two separate packages: + +- http://github.com/guzzle/command Provides a high level abstraction over web + services by representing web service operations using commands. +- http://github.com/guzzle/guzzle-services Provides an implementation of + guzzle/command that provides request serialization and response parsing using + Guzzle service descriptions. + +## Stream + +Stream have moved to a separate package available at +https://github.com/guzzle/streams. + +`Guzzle\Stream\StreamInterface` has been given a large update to cleanly take +on the responsibilities of `Guzzle\Http\EntityBody` and +`Guzzle\Http\EntityBodyInterface` now that they have been removed. The number +of methods implemented by the `StreamInterface` has been drastically reduced to +allow developers to more easily extend and decorate stream behavior. + +## Removed methods from StreamInterface + +- `getStream` and `setStream` have been removed to better encapsulate streams. +- `getMetadata` and `setMetadata` have been removed in favor of + `GuzzleHttp\Stream\MetadataStreamInterface`. +- `getWrapper`, `getWrapperData`, `getStreamType`, and `getUri` have all been + removed. This data is accessible when + using streams that implement `GuzzleHttp\Stream\MetadataStreamInterface`. +- `rewind` has been removed. Use `seek(0)` for a similar behavior. + +## Renamed methods + +- `detachStream` has been renamed to `detach`. +- `feof` has been renamed to `eof`. +- `ftell` has been renamed to `tell`. +- `readLine` has moved from an instance method to a static class method of + `GuzzleHttp\Stream\Stream`. + +## Metadata streams + +`GuzzleHttp\Stream\MetadataStreamInterface` has been added to denote streams +that contain additional metadata accessible via `getMetadata()`. +`GuzzleHttp\Stream\StreamInterface::getMetadata` and +`GuzzleHttp\Stream\StreamInterface::setMetadata` have been removed. + +## StreamRequestFactory + +The entire concept of the StreamRequestFactory has been removed. The way this +was used in Guzzle 3 broke the actual interface of sending streaming requests +(instead of getting back a Response, you got a StreamInterface). Streaming +PHP requests are now implemented through the `GuzzleHttp\Adapter\StreamAdapter`. + +3.6 to 3.7 +---------- + +### Deprecations + +- You can now enable E_USER_DEPRECATED warnings to see if you are using any deprecated methods.: + +```php +\Guzzle\Common\Version::$emitWarnings = true; +``` + +The following APIs and options have been marked as deprecated: + +- Marked `Guzzle\Http\Message\Request::isResponseBodyRepeatable()` as deprecated. Use `$request->getResponseBody()->isRepeatable()` instead. +- Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +- Marked `Guzzle\Http\Message\Request::canCache()` as deprecated. Use `Guzzle\Plugin\Cache\DefaultCanCacheStrategy->canCacheRequest()` instead. +- Marked `Guzzle\Http\Message\Request::setIsRedirect()` as deprecated. Use the HistoryPlugin instead. +- Marked `Guzzle\Http\Message\Request::isRedirect()` as deprecated. Use the HistoryPlugin instead. +- Marked `Guzzle\Cache\CacheAdapterFactory::factory()` as deprecated +- Marked `Guzzle\Service\Client::enableMagicMethods()` as deprecated. Magic methods can no longer be disabled on a Guzzle\Service\Client. +- Marked `Guzzle\Parser\Url\UrlParser` as deprecated. Just use PHP's `parse_url()` and percent encode your UTF-8. +- Marked `Guzzle\Common\Collection::inject()` as deprecated. +- Marked `Guzzle\Plugin\CurlAuth\CurlAuthPlugin` as deprecated. Use + `$client->getConfig()->setPath('request.options/auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));` or + `$client->setDefaultOption('auth', array('user', 'pass', 'Basic|Digest|NTLM|Any'));` + +3.7 introduces `request.options` as a parameter for a client configuration and as an optional argument to all creational +request methods. When paired with a client's configuration settings, these options allow you to specify default settings +for various aspects of a request. Because these options make other previous configuration options redundant, several +configuration options and methods of a client and AbstractCommand have been deprecated. + +- Marked `Guzzle\Service\Client::getDefaultHeaders()` as deprecated. Use `$client->getDefaultOption('headers')`. +- Marked `Guzzle\Service\Client::setDefaultHeaders()` as deprecated. Use `$client->setDefaultOption('headers/{header_name}', 'value')`. +- Marked 'request.params' for `Guzzle\Http\Client` as deprecated. Use `$client->setDefaultOption('params/{param_name}', 'value')` +- Marked 'command.headers', 'command.response_body' and 'command.on_complete' as deprecated for AbstractCommand. These will work through Guzzle 4.0 + + $command = $client->getCommand('foo', array( + 'command.headers' => array('Test' => '123'), + 'command.response_body' => '/path/to/file' + )); + + // Should be changed to: + + $command = $client->getCommand('foo', array( + 'command.request_options' => array( + 'headers' => array('Test' => '123'), + 'save_as' => '/path/to/file' + ) + )); + +### Interface changes + +Additions and changes (you will need to update any implementations or subclasses you may have created): + +- Added an `$options` argument to the end of the following methods of `Guzzle\Http\ClientInterface`: + createRequest, head, delete, put, patch, post, options, prepareRequest +- Added an `$options` argument to the end of `Guzzle\Http\Message\Request\RequestFactoryInterface::createRequest()` +- Added an `applyOptions()` method to `Guzzle\Http\Message\Request\RequestFactoryInterface` +- Changed `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $body = null)` to + `Guzzle\Http\ClientInterface::get($uri = null, $headers = null, $options = array())`. You can still pass in a + resource, string, or EntityBody into the $options parameter to specify the download location of the response. +- Changed `Guzzle\Common\Collection::__construct($data)` to no longer accepts a null value for `$data` but a + default `array()` +- Added `Guzzle\Stream\StreamInterface::isRepeatable` +- Made `Guzzle\Http\Client::expandTemplate` and `getUriTemplate` protected methods. + +The following methods were removed from interfaces. All of these methods are still available in the concrete classes +that implement them, but you should update your code to use alternative methods: + +- Removed `Guzzle\Http\ClientInterface::setDefaultHeaders(). Use + `$client->getConfig()->setPath('request.options/headers/{header_name}', 'value')`. or + `$client->getConfig()->setPath('request.options/headers', array('header_name' => 'value'))` or + `$client->setDefaultOption('headers/{header_name}', 'value')`. or + `$client->setDefaultOption('headers', array('header_name' => 'value'))`. +- Removed `Guzzle\Http\ClientInterface::getDefaultHeaders(). Use `$client->getConfig()->getPath('request.options/headers')`. +- Removed `Guzzle\Http\ClientInterface::expandTemplate()`. This is an implementation detail. +- Removed `Guzzle\Http\ClientInterface::setRequestFactory()`. This is an implementation detail. +- Removed `Guzzle\Http\ClientInterface::getCurlMulti()`. This is a very specific implementation detail. +- Removed `Guzzle\Http\Message\RequestInterface::canCache`. Use the CachePlugin. +- Removed `Guzzle\Http\Message\RequestInterface::setIsRedirect`. Use the HistoryPlugin. +- Removed `Guzzle\Http\Message\RequestInterface::isRedirect`. Use the HistoryPlugin. + +### Cache plugin breaking changes + +- CacheKeyProviderInterface and DefaultCacheKeyProvider are no longer used. All of this logic is handled in a + CacheStorageInterface. These two objects and interface will be removed in a future version. +- Always setting X-cache headers on cached responses +- Default cache TTLs are now handled by the CacheStorageInterface of a CachePlugin +- `CacheStorageInterface::cache($key, Response $response, $ttl = null)` has changed to `cache(RequestInterface + $request, Response $response);` +- `CacheStorageInterface::fetch($key)` has changed to `fetch(RequestInterface $request);` +- `CacheStorageInterface::delete($key)` has changed to `delete(RequestInterface $request);` +- Added `CacheStorageInterface::purge($url)` +- `DefaultRevalidation::__construct(CacheKeyProviderInterface $cacheKey, CacheStorageInterface $cache, CachePlugin + $plugin)` has changed to `DefaultRevalidation::__construct(CacheStorageInterface $cache, + CanCacheStrategyInterface $canCache = null)` +- Added `RevalidationInterface::shouldRevalidate(RequestInterface $request, Response $response)` + +3.5 to 3.6 +---------- + +* Mixed casing of headers are now forced to be a single consistent casing across all values for that header. +* Messages internally use a HeaderCollection object to delegate handling case-insensitive header resolution +* Removed the whole changedHeader() function system of messages because all header changes now go through addHeader(). + For example, setHeader() first removes the header using unset on a HeaderCollection and then calls addHeader(). + Keeping the Host header and URL host in sync is now handled by overriding the addHeader method in Request. +* Specific header implementations can be created for complex headers. When a message creates a header, it uses a + HeaderFactory which can map specific headers to specific header classes. There is now a Link header and + CacheControl header implementation. +* Moved getLinks() from Response to just be used on a Link header object. + +If you previously relied on Guzzle\Http\Message\Header::raw(), then you will need to update your code to use the +HeaderInterface (e.g. toArray(), getAll(), etc.). + +### Interface changes + +* Removed from interface: Guzzle\Http\ClientInterface::setUriTemplate +* Removed from interface: Guzzle\Http\ClientInterface::setCurlMulti() +* Removed Guzzle\Http\Message\Request::receivedRequestHeader() and implemented this functionality in + Guzzle\Http\Curl\RequestMediator +* Removed the optional $asString parameter from MessageInterface::getHeader(). Just cast the header to a string. +* Removed the optional $tryChunkedTransfer option from Guzzle\Http\Message\EntityEnclosingRequestInterface +* Removed the $asObjects argument from Guzzle\Http\Message\MessageInterface::getHeaders() + +### Removed deprecated functions + +* Removed Guzzle\Parser\ParserRegister::get(). Use getParser() +* Removed Guzzle\Parser\ParserRegister::set(). Use registerParser(). + +### Deprecations + +* The ability to case-insensitively search for header values +* Guzzle\Http\Message\Header::hasExactHeader +* Guzzle\Http\Message\Header::raw. Use getAll() +* Deprecated cache control specific methods on Guzzle\Http\Message\AbstractMessage. Use the CacheControl header object + instead. + +### Other changes + +* All response header helper functions return a string rather than mixing Header objects and strings inconsistently +* Removed cURL blacklist support. This is no longer necessary now that Expect, Accept, etc. are managed by Guzzle + directly via interfaces +* Removed the injecting of a request object onto a response object. The methods to get and set a request still exist + but are a no-op until removed. +* Most classes that used to require a `Guzzle\Service\Command\CommandInterface` typehint now request a + `Guzzle\Service\Command\ArrayCommandInterface`. +* Added `Guzzle\Http\Message\RequestInterface::startResponse()` to the RequestInterface to handle injecting a response + on a request while the request is still being transferred +* `Guzzle\Service\Command\CommandInterface` now extends from ToArrayInterface and ArrayAccess + +3.3 to 3.4 +---------- + +Base URLs of a client now follow the rules of http://tools.ietf.org/html/rfc3986#section-5.2.2 when merging URLs. + +3.2 to 3.3 +---------- + +### Response::getEtag() quote stripping removed + +`Guzzle\Http\Message\Response::getEtag()` no longer strips quotes around the ETag response header + +### Removed `Guzzle\Http\Utils` + +The `Guzzle\Http\Utils` class was removed. This class was only used for testing. + +### Stream wrapper and type + +`Guzzle\Stream\Stream::getWrapper()` and `Guzzle\Stream\Stream::getStreamType()` are no longer converted to lowercase. + +### curl.emit_io became emit_io + +Emitting IO events from a RequestMediator is now a parameter that must be set in a request's curl options using the +'emit_io' key. This was previously set under a request's parameters using 'curl.emit_io' + +3.1 to 3.2 +---------- + +### CurlMulti is no longer reused globally + +Before 3.2, the same CurlMulti object was reused globally for each client. This can cause issue where plugins added +to a single client can pollute requests dispatched from other clients. + +If you still wish to reuse the same CurlMulti object with each client, then you can add a listener to the +ServiceBuilder's `service_builder.create_client` event to inject a custom CurlMulti object into each client as it is +created. + +```php +$multi = new Guzzle\Http\Curl\CurlMulti(); +$builder = Guzzle\Service\Builder\ServiceBuilder::factory('/path/to/config.json'); +$builder->addListener('service_builder.create_client', function ($event) use ($multi) { + $event['client']->setCurlMulti($multi); +} +}); +``` + +### No default path + +URLs no longer have a default path value of '/' if no path was specified. + +Before: + +```php +$request = $client->get('http://www.foo.com'); +echo $request->getUrl(); +// >> http://www.foo.com/ +``` + +After: + +```php +$request = $client->get('http://www.foo.com'); +echo $request->getUrl(); +// >> http://www.foo.com +``` + +### Less verbose BadResponseException + +The exception message for `Guzzle\Http\Exception\BadResponseException` no longer contains the full HTTP request and +response information. You can, however, get access to the request and response object by calling `getRequest()` or +`getResponse()` on the exception object. + +### Query parameter aggregation + +Multi-valued query parameters are no longer aggregated using a callback function. `Guzzle\Http\Query` now has a +setAggregator() method that accepts a `Guzzle\Http\QueryAggregator\QueryAggregatorInterface` object. This object is +responsible for handling the aggregation of multi-valued query string variables into a flattened hash. + +2.8 to 3.x +---------- + +### Guzzle\Service\Inspector + +Change `\Guzzle\Service\Inspector::fromConfig` to `\Guzzle\Common\Collection::fromConfig` + +**Before** + +```php +use Guzzle\Service\Inspector; + +class YourClient extends \Guzzle\Service\Client +{ + public static function factory($config = array()) + { + $default = array(); + $required = array('base_url', 'username', 'api_key'); + $config = Inspector::fromConfig($config, $default, $required); + + $client = new self( + $config->get('base_url'), + $config->get('username'), + $config->get('api_key') + ); + $client->setConfig($config); + + $client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json')); + + return $client; + } +``` + +**After** + +```php +use Guzzle\Common\Collection; + +class YourClient extends \Guzzle\Service\Client +{ + public static function factory($config = array()) + { + $default = array(); + $required = array('base_url', 'username', 'api_key'); + $config = Collection::fromConfig($config, $default, $required); + + $client = new self( + $config->get('base_url'), + $config->get('username'), + $config->get('api_key') + ); + $client->setConfig($config); + + $client->setDescription(ServiceDescription::factory(__DIR__ . DIRECTORY_SEPARATOR . 'client.json')); + + return $client; + } +``` + +### Convert XML Service Descriptions to JSON + +**Before** + +```xml + + + + + + Get a list of groups + + + Uses a search query to get a list of groups + + + + Create a group + + + + + Delete a group by ID + + + + + + + Update a group + + + + + + +``` + +**After** + +```json +{ + "name": "Zendesk REST API v2", + "apiVersion": "2012-12-31", + "description":"Provides access to Zendesk views, groups, tickets, ticket fields, and users", + "operations": { + "list_groups": { + "httpMethod":"GET", + "uri": "groups.json", + "summary": "Get a list of groups" + }, + "search_groups":{ + "httpMethod":"GET", + "uri": "search.json?query=\"{query} type:group\"", + "summary": "Uses a search query to get a list of groups", + "parameters":{ + "query":{ + "location": "uri", + "description":"Zendesk Search Query", + "type": "string", + "required": true + } + } + }, + "create_group": { + "httpMethod":"POST", + "uri": "groups.json", + "summary": "Create a group", + "parameters":{ + "data": { + "type": "array", + "location": "body", + "description":"Group JSON", + "filters": "json_encode", + "required": true + }, + "Content-Type":{ + "type": "string", + "location":"header", + "static": "application/json" + } + } + }, + "delete_group": { + "httpMethod":"DELETE", + "uri": "groups/{id}.json", + "summary": "Delete a group", + "parameters":{ + "id":{ + "location": "uri", + "description":"Group to delete by ID", + "type": "integer", + "required": true + } + } + }, + "get_group": { + "httpMethod":"GET", + "uri": "groups/{id}.json", + "summary": "Get a ticket", + "parameters":{ + "id":{ + "location": "uri", + "description":"Group to get by ID", + "type": "integer", + "required": true + } + } + }, + "update_group": { + "httpMethod":"PUT", + "uri": "groups/{id}.json", + "summary": "Update a group", + "parameters":{ + "id": { + "location": "uri", + "description":"Group to update by ID", + "type": "integer", + "required": true + }, + "data": { + "type": "array", + "location": "body", + "description":"Group JSON", + "filters": "json_encode", + "required": true + }, + "Content-Type":{ + "type": "string", + "location":"header", + "static": "application/json" + } + } + } +} +``` + +### Guzzle\Service\Description\ServiceDescription + +Commands are now called Operations + +**Before** + +```php +use Guzzle\Service\Description\ServiceDescription; + +$sd = new ServiceDescription(); +$sd->getCommands(); // @returns ApiCommandInterface[] +$sd->hasCommand($name); +$sd->getCommand($name); // @returns ApiCommandInterface|null +$sd->addCommand($command); // @param ApiCommandInterface $command +``` + +**After** + +```php +use Guzzle\Service\Description\ServiceDescription; + +$sd = new ServiceDescription(); +$sd->getOperations(); // @returns OperationInterface[] +$sd->hasOperation($name); +$sd->getOperation($name); // @returns OperationInterface|null +$sd->addOperation($operation); // @param OperationInterface $operation +``` + +### Guzzle\Common\Inflection\Inflector + +Namespace is now `Guzzle\Inflection\Inflector` + +### Guzzle\Http\Plugin + +Namespace is now `Guzzle\Plugin`. Many other changes occur within this namespace and are detailed in their own sections below. + +### Guzzle\Http\Plugin\LogPlugin and Guzzle\Common\Log + +Now `Guzzle\Plugin\Log\LogPlugin` and `Guzzle\Log` respectively. + +**Before** + +```php +use Guzzle\Common\Log\ClosureLogAdapter; +use Guzzle\Http\Plugin\LogPlugin; + +/** @var \Guzzle\Http\Client */ +$client; + +// $verbosity is an integer indicating desired message verbosity level +$client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $verbosity = LogPlugin::LOG_VERBOSE); +``` + +**After** + +```php +use Guzzle\Log\ClosureLogAdapter; +use Guzzle\Log\MessageFormatter; +use Guzzle\Plugin\Log\LogPlugin; + +/** @var \Guzzle\Http\Client */ +$client; + +// $format is a string indicating desired message format -- @see MessageFormatter +$client->addSubscriber(new LogPlugin(new ClosureLogAdapter(function($m) { echo $m; }, $format = MessageFormatter::DEBUG_FORMAT); +``` + +### Guzzle\Http\Plugin\CurlAuthPlugin + +Now `Guzzle\Plugin\CurlAuth\CurlAuthPlugin`. + +### Guzzle\Http\Plugin\ExponentialBackoffPlugin + +Now `Guzzle\Plugin\Backoff\BackoffPlugin`, and other changes. + +**Before** + +```php +use Guzzle\Http\Plugin\ExponentialBackoffPlugin; + +$backoffPlugin = new ExponentialBackoffPlugin($maxRetries, array_merge( + ExponentialBackoffPlugin::getDefaultFailureCodes(), array(429) + )); + +$client->addSubscriber($backoffPlugin); +``` + +**After** + +```php +use Guzzle\Plugin\Backoff\BackoffPlugin; +use Guzzle\Plugin\Backoff\HttpBackoffStrategy; + +// Use convenient factory method instead -- see implementation for ideas of what +// you can do with chaining backoff strategies +$backoffPlugin = BackoffPlugin::getExponentialBackoff($maxRetries, array_merge( + HttpBackoffStrategy::getDefaultFailureCodes(), array(429) + )); +$client->addSubscriber($backoffPlugin); +``` + +### Known Issues + +#### [BUG] Accept-Encoding header behavior changed unintentionally. + +(See #217) (Fixed in 09daeb8c666fb44499a0646d655a8ae36456575e) + +In version 2.8 setting the `Accept-Encoding` header would set the CURLOPT_ENCODING option, which permitted cURL to +properly handle gzip/deflate compressed responses from the server. In versions affected by this bug this does not happen. +See issue #217 for a workaround, or use a version containing the fix. diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/composer.json b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/composer.json new file mode 100644 index 0000000..02ab73c --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/composer.json @@ -0,0 +1,59 @@ +{ + "name": "guzzlehttp/guzzle", + "type": "library", + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "framework", + "http", + "rest", + "web service", + "curl", + "client", + "HTTP client" + ], + "homepage": "http://guzzlephp.org/", + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.5", + "ext-json": "*", + "symfony/polyfill-intl-idn": "^1.11", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "config": { + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "autoload-dev": { + "psr-4": { + "GuzzleHttp\\Tests\\": "tests/" + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Client.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Client.php new file mode 100644 index 0000000..cd9a635 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Client.php @@ -0,0 +1,502 @@ + 'http://www.foo.com/1.0/', + * 'timeout' => 0, + * 'allow_redirects' => false, + * 'proxy' => '192.168.16.1:10' + * ]); + * + * Client configuration settings include the following options: + * + * - handler: (callable) Function that transfers HTTP requests over the + * wire. The function is called with a Psr7\Http\Message\RequestInterface + * and array of transfer options, and must return a + * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a + * Psr7\Http\Message\ResponseInterface on success. "handler" is a + * constructor only option that cannot be overridden in per/request + * options. If no handler is provided, a default handler will be created + * that enables all of the request options below by attaching all of the + * default middleware to the handler. + * - base_uri: (string|UriInterface) Base URI of the client that is merged + * into relative URIs. Can be a string or instance of UriInterface. + * - **: any request option + * + * @param array $config Client configuration settings. + * + * @see \GuzzleHttp\RequestOptions for a list of available request options. + */ + public function __construct(array $config = []) + { + if (!isset($config['handler'])) { + $config['handler'] = HandlerStack::create(); + } elseif (!is_callable($config['handler'])) { + throw new \InvalidArgumentException('handler must be a callable'); + } + + // Convert the base_uri to a UriInterface + if (isset($config['base_uri'])) { + $config['base_uri'] = Psr7\uri_for($config['base_uri']); + } + + $this->configureDefaults($config); + } + + /** + * @param string $method + * @param array $args + * + * @return Promise\PromiseInterface + */ + public function __call($method, $args) + { + if (count($args) < 1) { + throw new \InvalidArgumentException('Magic request methods require a URI and optional options array'); + } + + $uri = $args[0]; + $opts = isset($args[1]) ? $args[1] : []; + + return substr($method, -5) === 'Async' + ? $this->requestAsync(substr($method, 0, -5), $uri, $opts) + : $this->request($method, $uri, $opts); + } + + /** + * Asynchronously send an HTTP request. + * + * @param array $options Request options to apply to the given + * request and to the transfer. See \GuzzleHttp\RequestOptions. + * + * @return Promise\PromiseInterface + */ + public function sendAsync(RequestInterface $request, array $options = []) + { + // Merge the base URI into the request URI if needed. + $options = $this->prepareDefaults($options); + + return $this->transfer( + $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), + $options + ); + } + + /** + * Send an HTTP request. + * + * @param array $options Request options to apply to the given + * request and to the transfer. See \GuzzleHttp\RequestOptions. + * + * @return ResponseInterface + * @throws GuzzleException + */ + public function send(RequestInterface $request, array $options = []) + { + $options[RequestOptions::SYNCHRONOUS] = true; + return $this->sendAsync($request, $options)->wait(); + } + + /** + * Create and send an asynchronous HTTP request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string $method HTTP method + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. + * + * @return Promise\PromiseInterface + */ + public function requestAsync($method, $uri = '', array $options = []) + { + $options = $this->prepareDefaults($options); + // Remove request modifying parameter because it can be done up-front. + $headers = isset($options['headers']) ? $options['headers'] : []; + $body = isset($options['body']) ? $options['body'] : null; + $version = isset($options['version']) ? $options['version'] : '1.1'; + // Merge the URI into the base URI. + $uri = $this->buildUri($uri, $options); + if (is_array($body)) { + $this->invalidBody(); + } + $request = new Psr7\Request($method, $uri, $headers, $body, $version); + // Remove the option so that they are not doubly-applied. + unset($options['headers'], $options['body'], $options['version']); + + return $this->transfer($request, $options); + } + + /** + * Create and send an HTTP request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string $method HTTP method. + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. + * + * @return ResponseInterface + * @throws GuzzleException + */ + public function request($method, $uri = '', array $options = []) + { + $options[RequestOptions::SYNCHRONOUS] = true; + return $this->requestAsync($method, $uri, $options)->wait(); + } + + /** + * Get a client configuration option. + * + * These options include default request options of the client, a "handler" + * (if utilized by the concrete client), and a "base_uri" if utilized by + * the concrete client. + * + * @param string|null $option The config option to retrieve. + * + * @return mixed + */ + public function getConfig($option = null) + { + return $option === null + ? $this->config + : (isset($this->config[$option]) ? $this->config[$option] : null); + } + + /** + * @param string|null $uri + * + * @return UriInterface + */ + private function buildUri($uri, array $config) + { + // for BC we accept null which would otherwise fail in uri_for + $uri = Psr7\uri_for($uri === null ? '' : $uri); + + if (isset($config['base_uri'])) { + $uri = Psr7\UriResolver::resolve(Psr7\uri_for($config['base_uri']), $uri); + } + + if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) { + $idnOptions = ($config['idn_conversion'] === true) ? IDNA_DEFAULT : $config['idn_conversion']; + $uri = Utils::idnUriConvert($uri, $idnOptions); + } + + return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; + } + + /** + * Configures the default options for a client. + * + * @param array $config + * @return void + */ + private function configureDefaults(array $config) + { + $defaults = [ + 'allow_redirects' => RedirectMiddleware::$defaultSettings, + 'http_errors' => true, + 'decode_content' => true, + 'verify' => true, + 'cookies' => false, + 'idn_conversion' => true, + ]; + + // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. + + // We can only trust the HTTP_PROXY environment variable in a CLI + // process due to the fact that PHP has no reliable mechanism to + // get environment variables that start with "HTTP_". + if (php_sapi_name() === 'cli' && getenv('HTTP_PROXY')) { + $defaults['proxy']['http'] = getenv('HTTP_PROXY'); + } + + if ($proxy = getenv('HTTPS_PROXY')) { + $defaults['proxy']['https'] = $proxy; + } + + if ($noProxy = getenv('NO_PROXY')) { + $cleanedNoProxy = str_replace(' ', '', $noProxy); + $defaults['proxy']['no'] = explode(',', $cleanedNoProxy); + } + + $this->config = $config + $defaults; + + if (!empty($config['cookies']) && $config['cookies'] === true) { + $this->config['cookies'] = new CookieJar(); + } + + // Add the default user-agent header. + if (!isset($this->config['headers'])) { + $this->config['headers'] = ['User-Agent' => default_user_agent()]; + } else { + // Add the User-Agent header if one was not already set. + foreach (array_keys($this->config['headers']) as $name) { + if (strtolower($name) === 'user-agent') { + return; + } + } + $this->config['headers']['User-Agent'] = default_user_agent(); + } + } + + /** + * Merges default options into the array. + * + * @param array $options Options to modify by reference + * + * @return array + */ + private function prepareDefaults(array $options) + { + $defaults = $this->config; + + if (!empty($defaults['headers'])) { + // Default headers are only added if they are not present. + $defaults['_conditional'] = $defaults['headers']; + unset($defaults['headers']); + } + + // Special handling for headers is required as they are added as + // conditional headers and as headers passed to a request ctor. + if (array_key_exists('headers', $options)) { + // Allows default headers to be unset. + if ($options['headers'] === null) { + $defaults['_conditional'] = []; + unset($options['headers']); + } elseif (!is_array($options['headers'])) { + throw new \InvalidArgumentException('headers must be an array'); + } + } + + // Shallow merge defaults underneath options. + $result = $options + $defaults; + + // Remove null values. + foreach ($result as $k => $v) { + if ($v === null) { + unset($result[$k]); + } + } + + return $result; + } + + /** + * Transfers the given request and applies request options. + * + * The URI of the request is not modified and the request options are used + * as-is without merging in default options. + * + * @param array $options See \GuzzleHttp\RequestOptions. + * + * @return Promise\PromiseInterface + */ + private function transfer(RequestInterface $request, array $options) + { + // save_to -> sink + if (isset($options['save_to'])) { + $options['sink'] = $options['save_to']; + unset($options['save_to']); + } + + // exceptions -> http_errors + if (isset($options['exceptions'])) { + $options['http_errors'] = $options['exceptions']; + unset($options['exceptions']); + } + + $request = $this->applyOptions($request, $options); + /** @var HandlerStack $handler */ + $handler = $options['handler']; + + try { + return Promise\promise_for($handler($request, $options)); + } catch (\Exception $e) { + return Promise\rejection_for($e); + } + } + + /** + * Applies the array of request options to a request. + * + * @param RequestInterface $request + * @param array $options + * + * @return RequestInterface + */ + private function applyOptions(RequestInterface $request, array &$options) + { + $modify = [ + 'set_headers' => [], + ]; + + if (isset($options['headers'])) { + $modify['set_headers'] = $options['headers']; + unset($options['headers']); + } + + if (isset($options['form_params'])) { + if (isset($options['multipart'])) { + throw new \InvalidArgumentException('You cannot use ' + . 'form_params and multipart at the same time. Use the ' + . 'form_params option if you want to send application/' + . 'x-www-form-urlencoded requests, and the multipart ' + . 'option to send multipart/form-data requests.'); + } + $options['body'] = http_build_query($options['form_params'], '', '&'); + unset($options['form_params']); + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + if (isset($options['multipart'])) { + $options['body'] = new Psr7\MultipartStream($options['multipart']); + unset($options['multipart']); + } + + if (isset($options['json'])) { + $options['body'] = \GuzzleHttp\json_encode($options['json']); + unset($options['json']); + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'application/json'; + } + + if (!empty($options['decode_content']) + && $options['decode_content'] !== true + ) { + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\_caseless_remove(['Accept-Encoding'], $options['_conditional']); + $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; + } + + if (isset($options['body'])) { + if (is_array($options['body'])) { + $this->invalidBody(); + } + $modify['body'] = Psr7\stream_for($options['body']); + unset($options['body']); + } + + if (!empty($options['auth']) && is_array($options['auth'])) { + $value = $options['auth']; + $type = isset($value[2]) ? strtolower($value[2]) : 'basic'; + switch ($type) { + case 'basic': + // Ensure that we don't have the header in different case and set the new value. + $modify['set_headers'] = Psr7\_caseless_remove(['Authorization'], $modify['set_headers']); + $modify['set_headers']['Authorization'] = 'Basic ' + . base64_encode("$value[0]:$value[1]"); + break; + case 'digest': + // @todo: Do not rely on curl + $options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST; + $options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; + break; + case 'ntlm': + $options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_NTLM; + $options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; + break; + } + } + + if (isset($options['query'])) { + $value = $options['query']; + if (is_array($value)) { + $value = http_build_query($value, null, '&', PHP_QUERY_RFC3986); + } + if (!is_string($value)) { + throw new \InvalidArgumentException('query must be a string or array'); + } + $modify['query'] = $value; + unset($options['query']); + } + + // Ensure that sink is not an invalid value. + if (isset($options['sink'])) { + // TODO: Add more sink validation? + if (is_bool($options['sink'])) { + throw new \InvalidArgumentException('sink must not be a boolean'); + } + } + + $request = Psr7\modify_request($request, $modify); + if ($request->getBody() instanceof Psr7\MultipartStream) { + // Use a multipart/form-data POST if a Content-Type is not set. + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' + . $request->getBody()->getBoundary(); + } + + // Merge in conditional headers if they are not present. + if (isset($options['_conditional'])) { + // Build up the changes so it's in a single clone of the message. + $modify = []; + foreach ($options['_conditional'] as $k => $v) { + if (!$request->hasHeader($k)) { + $modify['set_headers'][$k] = $v; + } + } + $request = Psr7\modify_request($request, $modify); + // Don't pass this internal value along to middleware/handlers. + unset($options['_conditional']); + } + + return $request; + } + + /** + * Throw Exception with pre-set message. + * @return void + * @throws InvalidArgumentException Invalid body. + */ + private function invalidBody() + { + throw new \InvalidArgumentException('Passing in the "body" request ' + . 'option as an array to send a POST request has been deprecated. ' + . 'Please use the "form_params" request option to send a ' + . 'application/x-www-form-urlencoded request, or the "multipart" ' + . 'request option to send a multipart/form-data request.'); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/ClientInterface.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/ClientInterface.php new file mode 100644 index 0000000..76872dd --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/ClientInterface.php @@ -0,0 +1,87 @@ +strictMode = $strictMode; + + foreach ($cookieArray as $cookie) { + if (!($cookie instanceof SetCookie)) { + $cookie = new SetCookie($cookie); + } + $this->setCookie($cookie); + } + } + + /** + * Create a new Cookie jar from an associative array and domain. + * + * @param array $cookies Cookies to create the jar from + * @param string $domain Domain to set the cookies to + * + * @return self + */ + public static function fromArray(array $cookies, $domain) + { + $cookieJar = new self(); + foreach ($cookies as $name => $value) { + $cookieJar->setCookie(new SetCookie([ + 'Domain' => $domain, + 'Name' => $name, + 'Value' => $value, + 'Discard' => true + ])); + } + + return $cookieJar; + } + + /** + * @deprecated + */ + public static function getCookieValue($value) + { + return $value; + } + + /** + * Evaluate if this cookie should be persisted to storage + * that survives between requests. + * + * @param SetCookie $cookie Being evaluated. + * @param bool $allowSessionCookies If we should persist session cookies + * @return bool + */ + public static function shouldPersist( + SetCookie $cookie, + $allowSessionCookies = false + ) { + if ($cookie->getExpires() || $allowSessionCookies) { + if (!$cookie->getDiscard()) { + return true; + } + } + + return false; + } + + /** + * Finds and returns the cookie based on the name + * + * @param string $name cookie name to search for + * @return SetCookie|null cookie that was found or null if not found + */ + public function getCookieByName($name) + { + // don't allow a non string name + if ($name === null || !is_scalar($name)) { + return null; + } + foreach ($this->cookies as $cookie) { + if ($cookie->getName() !== null && strcasecmp($cookie->getName(), $name) === 0) { + return $cookie; + } + } + + return null; + } + + public function toArray() + { + return array_map(function (SetCookie $cookie) { + return $cookie->toArray(); + }, $this->getIterator()->getArrayCopy()); + } + + public function clear($domain = null, $path = null, $name = null) + { + if (!$domain) { + $this->cookies = []; + return; + } elseif (!$path) { + $this->cookies = array_filter( + $this->cookies, + function (SetCookie $cookie) use ($domain) { + return !$cookie->matchesDomain($domain); + } + ); + } elseif (!$name) { + $this->cookies = array_filter( + $this->cookies, + function (SetCookie $cookie) use ($path, $domain) { + return !($cookie->matchesPath($path) && + $cookie->matchesDomain($domain)); + } + ); + } else { + $this->cookies = array_filter( + $this->cookies, + function (SetCookie $cookie) use ($path, $domain, $name) { + return !($cookie->getName() == $name && + $cookie->matchesPath($path) && + $cookie->matchesDomain($domain)); + } + ); + } + } + + public function clearSessionCookies() + { + $this->cookies = array_filter( + $this->cookies, + function (SetCookie $cookie) { + return !$cookie->getDiscard() && $cookie->getExpires(); + } + ); + } + + public function setCookie(SetCookie $cookie) + { + // If the name string is empty (but not 0), ignore the set-cookie + // string entirely. + $name = $cookie->getName(); + if (!$name && $name !== '0') { + return false; + } + + // Only allow cookies with set and valid domain, name, value + $result = $cookie->validate(); + if ($result !== true) { + if ($this->strictMode) { + throw new \RuntimeException('Invalid cookie: ' . $result); + } else { + $this->removeCookieIfEmpty($cookie); + return false; + } + } + + // Resolve conflicts with previously set cookies + foreach ($this->cookies as $i => $c) { + + // Two cookies are identical, when their path, and domain are + // identical. + if ($c->getPath() != $cookie->getPath() || + $c->getDomain() != $cookie->getDomain() || + $c->getName() != $cookie->getName() + ) { + continue; + } + + // The previously set cookie is a discard cookie and this one is + // not so allow the new cookie to be set + if (!$cookie->getDiscard() && $c->getDiscard()) { + unset($this->cookies[$i]); + continue; + } + + // If the new cookie's expiration is further into the future, then + // replace the old cookie + if ($cookie->getExpires() > $c->getExpires()) { + unset($this->cookies[$i]); + continue; + } + + // If the value has changed, we better change it + if ($cookie->getValue() !== $c->getValue()) { + unset($this->cookies[$i]); + continue; + } + + // The cookie exists, so no need to continue + return false; + } + + $this->cookies[] = $cookie; + + return true; + } + + public function count() + { + return count($this->cookies); + } + + public function getIterator() + { + return new \ArrayIterator(array_values($this->cookies)); + } + + public function extractCookies( + RequestInterface $request, + ResponseInterface $response + ) { + if ($cookieHeader = $response->getHeader('Set-Cookie')) { + foreach ($cookieHeader as $cookie) { + $sc = SetCookie::fromString($cookie); + if (!$sc->getDomain()) { + $sc->setDomain($request->getUri()->getHost()); + } + if (0 !== strpos($sc->getPath(), '/')) { + $sc->setPath($this->getCookiePathFromRequest($request)); + } + $this->setCookie($sc); + } + } + } + + /** + * Computes cookie path following RFC 6265 section 5.1.4 + * + * @link https://tools.ietf.org/html/rfc6265#section-5.1.4 + * + * @param RequestInterface $request + * @return string + */ + private function getCookiePathFromRequest(RequestInterface $request) + { + $uriPath = $request->getUri()->getPath(); + if ('' === $uriPath) { + return '/'; + } + if (0 !== strpos($uriPath, '/')) { + return '/'; + } + if ('/' === $uriPath) { + return '/'; + } + if (0 === $lastSlashPos = strrpos($uriPath, '/')) { + return '/'; + } + + return substr($uriPath, 0, $lastSlashPos); + } + + public function withCookieHeader(RequestInterface $request) + { + $values = []; + $uri = $request->getUri(); + $scheme = $uri->getScheme(); + $host = $uri->getHost(); + $path = $uri->getPath() ?: '/'; + + foreach ($this->cookies as $cookie) { + if ($cookie->matchesPath($path) && + $cookie->matchesDomain($host) && + !$cookie->isExpired() && + (!$cookie->getSecure() || $scheme === 'https') + ) { + $values[] = $cookie->getName() . '=' + . $cookie->getValue(); + } + } + + return $values + ? $request->withHeader('Cookie', implode('; ', $values)) + : $request; + } + + /** + * If a cookie already exists and the server asks to set it again with a + * null value, the cookie must be deleted. + * + * @param SetCookie $cookie + */ + private function removeCookieIfEmpty(SetCookie $cookie) + { + $cookieValue = $cookie->getValue(); + if ($cookieValue === null || $cookieValue === '') { + $this->clear( + $cookie->getDomain(), + $cookie->getPath(), + $cookie->getName() + ); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php new file mode 100644 index 0000000..6ee1188 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php @@ -0,0 +1,84 @@ +filename = $cookieFile; + $this->storeSessionCookies = $storeSessionCookies; + + if (file_exists($cookieFile)) { + $this->load($cookieFile); + } + } + + /** + * Saves the file when shutting down + */ + public function __destruct() + { + $this->save($this->filename); + } + + /** + * Saves the cookies to a file. + * + * @param string $filename File to save + * @throws \RuntimeException if the file cannot be found or created + */ + public function save($filename) + { + $json = []; + foreach ($this as $cookie) { + /** @var SetCookie $cookie */ + if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { + $json[] = $cookie->toArray(); + } + } + + $jsonStr = \GuzzleHttp\json_encode($json); + if (false === file_put_contents($filename, $jsonStr, LOCK_EX)) { + throw new \RuntimeException("Unable to save file {$filename}"); + } + } + + /** + * Load cookies from a JSON formatted file. + * + * Old cookies are kept unless overwritten by newly loaded ones. + * + * @param string $filename Cookie file to load. + * @throws \RuntimeException if the file cannot be loaded. + */ + public function load($filename) + { + $json = file_get_contents($filename); + if (false === $json) { + throw new \RuntimeException("Unable to load file {$filename}"); + } elseif ($json === '') { + return; + } + + $data = \GuzzleHttp\json_decode($json, true); + if (is_array($data)) { + foreach (json_decode($json, true) as $cookie) { + $this->setCookie(new SetCookie($cookie)); + } + } elseif (strlen($data)) { + throw new \RuntimeException("Invalid cookie file: {$filename}"); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php new file mode 100644 index 0000000..0224a24 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php @@ -0,0 +1,72 @@ +sessionKey = $sessionKey; + $this->storeSessionCookies = $storeSessionCookies; + $this->load(); + } + + /** + * Saves cookies to session when shutting down + */ + public function __destruct() + { + $this->save(); + } + + /** + * Save cookies to the client session + */ + public function save() + { + $json = []; + foreach ($this as $cookie) { + /** @var SetCookie $cookie */ + if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { + $json[] = $cookie->toArray(); + } + } + + $_SESSION[$this->sessionKey] = json_encode($json); + } + + /** + * Load the contents of the client session into the data array + */ + protected function load() + { + if (!isset($_SESSION[$this->sessionKey])) { + return; + } + $data = json_decode($_SESSION[$this->sessionKey], true); + if (is_array($data)) { + foreach ($data as $cookie) { + $this->setCookie(new SetCookie($cookie)); + } + } elseif (strlen($data)) { + throw new \RuntimeException("Invalid cookie data"); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php new file mode 100644 index 0000000..3d776a7 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php @@ -0,0 +1,403 @@ + null, + 'Value' => null, + 'Domain' => null, + 'Path' => '/', + 'Max-Age' => null, + 'Expires' => null, + 'Secure' => false, + 'Discard' => false, + 'HttpOnly' => false + ]; + + /** @var array Cookie data */ + private $data; + + /** + * Create a new SetCookie object from a string + * + * @param string $cookie Set-Cookie header string + * + * @return self + */ + public static function fromString($cookie) + { + // Create the default return array + $data = self::$defaults; + // Explode the cookie string using a series of semicolons + $pieces = array_filter(array_map('trim', explode(';', $cookie))); + // The name of the cookie (first kvp) must exist and include an equal sign. + if (empty($pieces[0]) || !strpos($pieces[0], '=')) { + return new self($data); + } + + // Add the cookie pieces into the parsed data array + foreach ($pieces as $part) { + $cookieParts = explode('=', $part, 2); + $key = trim($cookieParts[0]); + $value = isset($cookieParts[1]) + ? trim($cookieParts[1], " \n\r\t\0\x0B") + : true; + + // Only check for non-cookies when cookies have been found + if (empty($data['Name'])) { + $data['Name'] = $key; + $data['Value'] = $value; + } else { + foreach (array_keys(self::$defaults) as $search) { + if (!strcasecmp($search, $key)) { + $data[$search] = $value; + continue 2; + } + } + $data[$key] = $value; + } + } + + return new self($data); + } + + /** + * @param array $data Array of cookie data provided by a Cookie parser + */ + public function __construct(array $data = []) + { + $this->data = array_replace(self::$defaults, $data); + // Extract the Expires value and turn it into a UNIX timestamp if needed + if (!$this->getExpires() && $this->getMaxAge()) { + // Calculate the Expires date + $this->setExpires(time() + $this->getMaxAge()); + } elseif ($this->getExpires() && !is_numeric($this->getExpires())) { + $this->setExpires($this->getExpires()); + } + } + + public function __toString() + { + $str = $this->data['Name'] . '=' . $this->data['Value'] . '; '; + foreach ($this->data as $k => $v) { + if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) { + if ($k === 'Expires') { + $str .= 'Expires=' . gmdate('D, d M Y H:i:s \G\M\T', $v) . '; '; + } else { + $str .= ($v === true ? $k : "{$k}={$v}") . '; '; + } + } + } + + return rtrim($str, '; '); + } + + public function toArray() + { + return $this->data; + } + + /** + * Get the cookie name + * + * @return string + */ + public function getName() + { + return $this->data['Name']; + } + + /** + * Set the cookie name + * + * @param string $name Cookie name + */ + public function setName($name) + { + $this->data['Name'] = $name; + } + + /** + * Get the cookie value + * + * @return string + */ + public function getValue() + { + return $this->data['Value']; + } + + /** + * Set the cookie value + * + * @param string $value Cookie value + */ + public function setValue($value) + { + $this->data['Value'] = $value; + } + + /** + * Get the domain + * + * @return string|null + */ + public function getDomain() + { + return $this->data['Domain']; + } + + /** + * Set the domain of the cookie + * + * @param string $domain + */ + public function setDomain($domain) + { + $this->data['Domain'] = $domain; + } + + /** + * Get the path + * + * @return string + */ + public function getPath() + { + return $this->data['Path']; + } + + /** + * Set the path of the cookie + * + * @param string $path Path of the cookie + */ + public function setPath($path) + { + $this->data['Path'] = $path; + } + + /** + * Maximum lifetime of the cookie in seconds + * + * @return int|null + */ + public function getMaxAge() + { + return $this->data['Max-Age']; + } + + /** + * Set the max-age of the cookie + * + * @param int $maxAge Max age of the cookie in seconds + */ + public function setMaxAge($maxAge) + { + $this->data['Max-Age'] = $maxAge; + } + + /** + * The UNIX timestamp when the cookie Expires + * + * @return mixed + */ + public function getExpires() + { + return $this->data['Expires']; + } + + /** + * Set the unix timestamp for which the cookie will expire + * + * @param int $timestamp Unix timestamp + */ + public function setExpires($timestamp) + { + $this->data['Expires'] = is_numeric($timestamp) + ? (int) $timestamp + : strtotime($timestamp); + } + + /** + * Get whether or not this is a secure cookie + * + * @return bool|null + */ + public function getSecure() + { + return $this->data['Secure']; + } + + /** + * Set whether or not the cookie is secure + * + * @param bool $secure Set to true or false if secure + */ + public function setSecure($secure) + { + $this->data['Secure'] = $secure; + } + + /** + * Get whether or not this is a session cookie + * + * @return bool|null + */ + public function getDiscard() + { + return $this->data['Discard']; + } + + /** + * Set whether or not this is a session cookie + * + * @param bool $discard Set to true or false if this is a session cookie + */ + public function setDiscard($discard) + { + $this->data['Discard'] = $discard; + } + + /** + * Get whether or not this is an HTTP only cookie + * + * @return bool + */ + public function getHttpOnly() + { + return $this->data['HttpOnly']; + } + + /** + * Set whether or not this is an HTTP only cookie + * + * @param bool $httpOnly Set to true or false if this is HTTP only + */ + public function setHttpOnly($httpOnly) + { + $this->data['HttpOnly'] = $httpOnly; + } + + /** + * Check if the cookie matches a path value. + * + * A request-path path-matches a given cookie-path if at least one of + * the following conditions holds: + * + * - The cookie-path and the request-path are identical. + * - The cookie-path is a prefix of the request-path, and the last + * character of the cookie-path is %x2F ("/"). + * - The cookie-path is a prefix of the request-path, and the first + * character of the request-path that is not included in the cookie- + * path is a %x2F ("/") character. + * + * @param string $requestPath Path to check against + * + * @return bool + */ + public function matchesPath($requestPath) + { + $cookiePath = $this->getPath(); + + // Match on exact matches or when path is the default empty "/" + if ($cookiePath === '/' || $cookiePath == $requestPath) { + return true; + } + + // Ensure that the cookie-path is a prefix of the request path. + if (0 !== strpos($requestPath, $cookiePath)) { + return false; + } + + // Match if the last character of the cookie-path is "/" + if (substr($cookiePath, -1, 1) === '/') { + return true; + } + + // Match if the first character not included in cookie path is "/" + return substr($requestPath, strlen($cookiePath), 1) === '/'; + } + + /** + * Check if the cookie matches a domain value + * + * @param string $domain Domain to check against + * + * @return bool + */ + public function matchesDomain($domain) + { + // Remove the leading '.' as per spec in RFC 6265. + // http://tools.ietf.org/html/rfc6265#section-5.2.3 + $cookieDomain = ltrim($this->getDomain(), '.'); + + // Domain not set or exact match. + if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) { + return true; + } + + // Matching the subdomain according to RFC 6265. + // http://tools.ietf.org/html/rfc6265#section-5.1.3 + if (filter_var($domain, FILTER_VALIDATE_IP)) { + return false; + } + + return (bool) preg_match('/\.' . preg_quote($cookieDomain, '/') . '$/', $domain); + } + + /** + * Check if the cookie is expired + * + * @return bool + */ + public function isExpired() + { + return $this->getExpires() !== null && time() > $this->getExpires(); + } + + /** + * Check if the cookie is valid according to RFC 6265 + * + * @return bool|string Returns true if valid or an error message if invalid + */ + public function validate() + { + // Names must not be empty, but can be 0 + $name = $this->getName(); + if (empty($name) && !is_numeric($name)) { + return 'The cookie name must not be empty'; + } + + // Check if any of the invalid characters are present in the cookie name + if (preg_match( + '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', + $name + )) { + return 'Cookie name must not contain invalid characters: ASCII ' + . 'Control characters (0-31;127), space, tab and the ' + . 'following characters: ()<>@,;:\"/?={}'; + } + + // Value must not be empty, but can be 0 + $value = $this->getValue(); + if (empty($value) && !is_numeric($value)) { + return 'The cookie value must not be empty'; + } + + // Domains must not be empty, but can be 0 + // A "0" is not a valid internet domain, but may be used as server name + // in a private network. + $domain = $this->getDomain(); + if (empty($domain) && !is_numeric($domain)) { + return 'The cookie domain must not be empty'; + } + + return true; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php new file mode 100644 index 0000000..427d896 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php @@ -0,0 +1,27 @@ +getStatusCode() + : 0; + parent::__construct($message, $code, $previous); + $this->request = $request; + $this->response = $response; + $this->handlerContext = $handlerContext; + } + + /** + * Wrap non-RequestExceptions with a RequestException + * + * @param RequestInterface $request + * @param \Exception $e + * + * @return RequestException + */ + public static function wrapException(RequestInterface $request, \Exception $e) + { + return $e instanceof RequestException + ? $e + : new RequestException($e->getMessage(), $request, null, $e); + } + + /** + * Factory method to create a new exception with a normalized error message + * + * @param RequestInterface $request Request + * @param ResponseInterface $response Response received + * @param \Exception $previous Previous exception + * @param array $ctx Optional handler context. + * + * @return self + */ + public static function create( + RequestInterface $request, + ResponseInterface $response = null, + \Exception $previous = null, + array $ctx = [] + ) { + if (!$response) { + return new self( + 'Error completing request', + $request, + null, + $previous, + $ctx + ); + } + + $level = (int) floor($response->getStatusCode() / 100); + if ($level === 4) { + $label = 'Client error'; + $className = ClientException::class; + } elseif ($level === 5) { + $label = 'Server error'; + $className = ServerException::class; + } else { + $label = 'Unsuccessful request'; + $className = __CLASS__; + } + + $uri = $request->getUri(); + $uri = static::obfuscateUri($uri); + + // Client Error: `GET /` resulted in a `404 Not Found` response: + // ... (truncated) + $message = sprintf( + '%s: `%s %s` resulted in a `%s %s` response', + $label, + $request->getMethod(), + $uri, + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + $summary = static::getResponseBodySummary($response); + + if ($summary !== null) { + $message .= ":\n{$summary}\n"; + } + + return new $className($message, $request, $response, $previous, $ctx); + } + + /** + * Get a short summary of the response + * + * Will return `null` if the response is not printable. + * + * @param ResponseInterface $response + * + * @return string|null + */ + public static function getResponseBodySummary(ResponseInterface $response) + { + return \GuzzleHttp\Psr7\get_message_body_summary($response); + } + + /** + * Obfuscates URI if there is a username and a password present + * + * @param UriInterface $uri + * + * @return UriInterface + */ + private static function obfuscateUri(UriInterface $uri) + { + $userInfo = $uri->getUserInfo(); + + if (false !== ($pos = strpos($userInfo, ':'))) { + return $uri->withUserInfo(substr($userInfo, 0, $pos), '***'); + } + + return $uri; + } + + /** + * Get the request that caused the exception + * + * @return RequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * Get the associated response + * + * @return ResponseInterface|null + */ + public function getResponse() + { + return $this->response; + } + + /** + * Check if a response was received + * + * @return bool + */ + public function hasResponse() + { + return $this->response !== null; + } + + /** + * Get contextual information about the error from the underlying handler. + * + * The contents of this array will vary depending on which handler you are + * using. It may also be just an empty array. Relying on this data will + * couple you to a specific handler, but can give more debug information + * when needed. + * + * @return array + */ + public function getHandlerContext() + { + return $this->handlerContext; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/SeekException.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/SeekException.php new file mode 100644 index 0000000..a77c289 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/SeekException.php @@ -0,0 +1,27 @@ +stream = $stream; + $msg = $msg ?: 'Could not seek the stream to position ' . $pos; + parent::__construct($msg); + } + + /** + * @return StreamInterface + */ + public function getStream() + { + return $this->stream; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/ServerException.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/ServerException.php new file mode 100644 index 0000000..127094c --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Exception/ServerException.php @@ -0,0 +1,9 @@ +maxHandles = $maxHandles; + } + + public function create(RequestInterface $request, array $options) + { + if (isset($options['curl']['body_as_string'])) { + $options['_body_as_string'] = $options['curl']['body_as_string']; + unset($options['curl']['body_as_string']); + } + + $easy = new EasyHandle; + $easy->request = $request; + $easy->options = $options; + $conf = $this->getDefaultConf($easy); + $this->applyMethod($easy, $conf); + $this->applyHandlerOptions($easy, $conf); + $this->applyHeaders($easy, $conf); + unset($conf['_headers']); + + // Add handler options from the request configuration options + if (isset($options['curl'])) { + $conf = array_replace($conf, $options['curl']); + } + + $conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); + $easy->handle = $this->handles + ? array_pop($this->handles) + : curl_init(); + curl_setopt_array($easy->handle, $conf); + + return $easy; + } + + public function release(EasyHandle $easy) + { + $resource = $easy->handle; + unset($easy->handle); + + if (count($this->handles) >= $this->maxHandles) { + curl_close($resource); + } else { + // Remove all callback functions as they can hold onto references + // and are not cleaned up by curl_reset. Using curl_setopt_array + // does not work for some reason, so removing each one + // individually. + curl_setopt($resource, CURLOPT_HEADERFUNCTION, null); + curl_setopt($resource, CURLOPT_READFUNCTION, null); + curl_setopt($resource, CURLOPT_WRITEFUNCTION, null); + curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null); + curl_reset($resource); + $this->handles[] = $resource; + } + } + + /** + * Completes a cURL transaction, either returning a response promise or a + * rejected promise. + * + * @param callable $handler + * @param EasyHandle $easy + * @param CurlFactoryInterface $factory Dictates how the handle is released + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public static function finish( + callable $handler, + EasyHandle $easy, + CurlFactoryInterface $factory + ) { + if (isset($easy->options['on_stats'])) { + self::invokeStats($easy); + } + + if (!$easy->response || $easy->errno) { + return self::finishError($handler, $easy, $factory); + } + + // Return the response if it is present and there is no error. + $factory->release($easy); + + // Rewind the body of the response if possible. + $body = $easy->response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + + return new FulfilledPromise($easy->response); + } + + private static function invokeStats(EasyHandle $easy) + { + $curlStats = curl_getinfo($easy->handle); + $curlStats['appconnect_time'] = curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME); + $stats = new TransferStats( + $easy->request, + $easy->response, + $curlStats['total_time'], + $easy->errno, + $curlStats + ); + call_user_func($easy->options['on_stats'], $stats); + } + + private static function finishError( + callable $handler, + EasyHandle $easy, + CurlFactoryInterface $factory + ) { + // Get error information and release the handle to the factory. + $ctx = [ + 'errno' => $easy->errno, + 'error' => curl_error($easy->handle), + 'appconnect_time' => curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME), + ] + curl_getinfo($easy->handle); + $ctx[self::CURL_VERSION_STR] = curl_version()['version']; + $factory->release($easy); + + // Retry when nothing is present or when curl failed to rewind. + if (empty($easy->options['_err_message']) + && (!$easy->errno || $easy->errno == 65) + ) { + return self::retryFailedRewind($handler, $easy, $ctx); + } + + return self::createRejection($easy, $ctx); + } + + private static function createRejection(EasyHandle $easy, array $ctx) + { + static $connectionErrors = [ + CURLE_OPERATION_TIMEOUTED => true, + CURLE_COULDNT_RESOLVE_HOST => true, + CURLE_COULDNT_CONNECT => true, + CURLE_SSL_CONNECT_ERROR => true, + CURLE_GOT_NOTHING => true, + ]; + + // If an exception was encountered during the onHeaders event, then + // return a rejected promise that wraps that exception. + if ($easy->onHeadersException) { + return \GuzzleHttp\Promise\rejection_for( + new RequestException( + 'An error was encountered during the on_headers event', + $easy->request, + $easy->response, + $easy->onHeadersException, + $ctx + ) + ); + } + if (version_compare($ctx[self::CURL_VERSION_STR], self::LOW_CURL_VERSION_NUMBER)) { + $message = sprintf( + 'cURL error %s: %s (%s)', + $ctx['errno'], + $ctx['error'], + 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html' + ); + } else { + $message = sprintf( + 'cURL error %s: %s (%s) for %s', + $ctx['errno'], + $ctx['error'], + 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html', + $easy->request->getUri() + ); + } + + // Create a connection exception if it was a specific error code. + $error = isset($connectionErrors[$easy->errno]) + ? new ConnectException($message, $easy->request, null, $ctx) + : new RequestException($message, $easy->request, $easy->response, null, $ctx); + + return \GuzzleHttp\Promise\rejection_for($error); + } + + private function getDefaultConf(EasyHandle $easy) + { + $conf = [ + '_headers' => $easy->request->getHeaders(), + CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), + CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), + CURLOPT_RETURNTRANSFER => false, + CURLOPT_HEADER => false, + CURLOPT_CONNECTTIMEOUT => 150, + ]; + + if (defined('CURLOPT_PROTOCOLS')) { + $conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $version = $easy->request->getProtocolVersion(); + if ($version == 1.1) { + $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; + } elseif ($version == 2.0) { + $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; + } else { + $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; + } + + return $conf; + } + + private function applyMethod(EasyHandle $easy, array &$conf) + { + $body = $easy->request->getBody(); + $size = $body->getSize(); + + if ($size === null || $size > 0) { + $this->applyBody($easy->request, $easy->options, $conf); + return; + } + + $method = $easy->request->getMethod(); + if ($method === 'PUT' || $method === 'POST') { + // See http://tools.ietf.org/html/rfc7230#section-3.3.2 + if (!$easy->request->hasHeader('Content-Length')) { + $conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; + } + } elseif ($method === 'HEAD') { + $conf[CURLOPT_NOBODY] = true; + unset( + $conf[CURLOPT_WRITEFUNCTION], + $conf[CURLOPT_READFUNCTION], + $conf[CURLOPT_FILE], + $conf[CURLOPT_INFILE] + ); + } + } + + private function applyBody(RequestInterface $request, array $options, array &$conf) + { + $size = $request->hasHeader('Content-Length') + ? (int) $request->getHeaderLine('Content-Length') + : null; + + // Send the body as a string if the size is less than 1MB OR if the + // [curl][body_as_string] request value is set. + if (($size !== null && $size < 1000000) || + !empty($options['_body_as_string']) + ) { + $conf[CURLOPT_POSTFIELDS] = (string) $request->getBody(); + // Don't duplicate the Content-Length header + $this->removeHeader('Content-Length', $conf); + $this->removeHeader('Transfer-Encoding', $conf); + } else { + $conf[CURLOPT_UPLOAD] = true; + if ($size !== null) { + $conf[CURLOPT_INFILESIZE] = $size; + $this->removeHeader('Content-Length', $conf); + } + $body = $request->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + $conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) { + return $body->read($length); + }; + } + + // If the Expect header is not present, prevent curl from adding it + if (!$request->hasHeader('Expect')) { + $conf[CURLOPT_HTTPHEADER][] = 'Expect:'; + } + + // cURL sometimes adds a content-type by default. Prevent this. + if (!$request->hasHeader('Content-Type')) { + $conf[CURLOPT_HTTPHEADER][] = 'Content-Type:'; + } + } + + private function applyHeaders(EasyHandle $easy, array &$conf) + { + foreach ($conf['_headers'] as $name => $values) { + foreach ($values as $value) { + $value = (string) $value; + if ($value === '') { + // cURL requires a special format for empty headers. + // See https://github.com/guzzle/guzzle/issues/1882 for more details. + $conf[CURLOPT_HTTPHEADER][] = "$name;"; + } else { + $conf[CURLOPT_HTTPHEADER][] = "$name: $value"; + } + } + } + + // Remove the Accept header if one was not set + if (!$easy->request->hasHeader('Accept')) { + $conf[CURLOPT_HTTPHEADER][] = 'Accept:'; + } + } + + /** + * Remove a header from the options array. + * + * @param string $name Case-insensitive header to remove + * @param array $options Array of options to modify + */ + private function removeHeader($name, array &$options) + { + foreach (array_keys($options['_headers']) as $key) { + if (!strcasecmp($key, $name)) { + unset($options['_headers'][$key]); + return; + } + } + } + + private function applyHandlerOptions(EasyHandle $easy, array &$conf) + { + $options = $easy->options; + if (isset($options['verify'])) { + if ($options['verify'] === false) { + unset($conf[CURLOPT_CAINFO]); + $conf[CURLOPT_SSL_VERIFYHOST] = 0; + $conf[CURLOPT_SSL_VERIFYPEER] = false; + } else { + $conf[CURLOPT_SSL_VERIFYHOST] = 2; + $conf[CURLOPT_SSL_VERIFYPEER] = true; + if (is_string($options['verify'])) { + // Throw an error if the file/folder/link path is not valid or doesn't exist. + if (!file_exists($options['verify'])) { + throw new \InvalidArgumentException( + "SSL CA bundle not found: {$options['verify']}" + ); + } + // If it's a directory or a link to a directory use CURLOPT_CAPATH. + // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. + if (is_dir($options['verify']) || + (is_link($options['verify']) && is_dir(readlink($options['verify'])))) { + $conf[CURLOPT_CAPATH] = $options['verify']; + } else { + $conf[CURLOPT_CAINFO] = $options['verify']; + } + } + } + } + + if (!empty($options['decode_content'])) { + $accept = $easy->request->getHeaderLine('Accept-Encoding'); + if ($accept) { + $conf[CURLOPT_ENCODING] = $accept; + } else { + $conf[CURLOPT_ENCODING] = ''; + // Don't let curl send the header over the wire + $conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; + } + } + + if (isset($options['sink'])) { + $sink = $options['sink']; + if (!is_string($sink)) { + $sink = \GuzzleHttp\Psr7\stream_for($sink); + } elseif (!is_dir(dirname($sink))) { + // Ensure that the directory exists before failing in curl. + throw new \RuntimeException(sprintf( + 'Directory %s does not exist for sink value of %s', + dirname($sink), + $sink + )); + } else { + $sink = new LazyOpenStream($sink, 'w+'); + } + $easy->sink = $sink; + $conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) { + return $sink->write($write); + }; + } else { + // Use a default temp stream if no sink was set. + $conf[CURLOPT_FILE] = fopen('php://temp', 'w+'); + $easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]); + } + $timeoutRequiresNoSignal = false; + if (isset($options['timeout'])) { + $timeoutRequiresNoSignal |= $options['timeout'] < 1; + $conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; + } + + // CURL default value is CURL_IPRESOLVE_WHATEVER + if (isset($options['force_ip_resolve'])) { + if ('v4' === $options['force_ip_resolve']) { + $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; + } elseif ('v6' === $options['force_ip_resolve']) { + $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6; + } + } + + if (isset($options['connect_timeout'])) { + $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; + $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; + } + + if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + $conf[CURLOPT_NOSIGNAL] = true; + } + + if (isset($options['proxy'])) { + if (!is_array($options['proxy'])) { + $conf[CURLOPT_PROXY] = $options['proxy']; + } else { + $scheme = $easy->request->getUri()->getScheme(); + if (isset($options['proxy'][$scheme])) { + $host = $easy->request->getUri()->getHost(); + if (!isset($options['proxy']['no']) || + !\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no']) + ) { + $conf[CURLOPT_PROXY] = $options['proxy'][$scheme]; + } + } + } + } + + if (isset($options['cert'])) { + $cert = $options['cert']; + if (is_array($cert)) { + $conf[CURLOPT_SSLCERTPASSWD] = $cert[1]; + $cert = $cert[0]; + } + if (!file_exists($cert)) { + throw new \InvalidArgumentException( + "SSL certificate not found: {$cert}" + ); + } + $conf[CURLOPT_SSLCERT] = $cert; + } + + if (isset($options['ssl_key'])) { + if (is_array($options['ssl_key'])) { + if (count($options['ssl_key']) === 2) { + list($sslKey, $conf[CURLOPT_SSLKEYPASSWD]) = $options['ssl_key']; + } else { + list($sslKey) = $options['ssl_key']; + } + } + + $sslKey = isset($sslKey) ? $sslKey: $options['ssl_key']; + + if (!file_exists($sslKey)) { + throw new \InvalidArgumentException( + "SSL private key not found: {$sslKey}" + ); + } + $conf[CURLOPT_SSLKEY] = $sslKey; + } + + if (isset($options['progress'])) { + $progress = $options['progress']; + if (!is_callable($progress)) { + throw new \InvalidArgumentException( + 'progress client option must be callable' + ); + } + $conf[CURLOPT_NOPROGRESS] = false; + $conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) { + $args = func_get_args(); + // PHP 5.5 pushed the handle onto the start of the args + if (is_resource($args[0])) { + array_shift($args); + } + call_user_func_array($progress, $args); + }; + } + + if (!empty($options['debug'])) { + $conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']); + $conf[CURLOPT_VERBOSE] = true; + } + } + + /** + * This function ensures that a response was set on a transaction. If one + * was not set, then the request is retried if possible. This error + * typically means you are sending a payload, curl encountered a + * "Connection died, retrying a fresh connect" error, tried to rewind the + * stream, and then encountered a "necessary data rewind wasn't possible" + * error, causing the request to be sent through curl_multi_info_read() + * without an error status. + */ + private static function retryFailedRewind( + callable $handler, + EasyHandle $easy, + array $ctx + ) { + try { + // Only rewind if the body has been read from. + $body = $easy->request->getBody(); + if ($body->tell() > 0) { + $body->rewind(); + } + } catch (\RuntimeException $e) { + $ctx['error'] = 'The connection unexpectedly failed without ' + . 'providing an error. The request would have been retried, ' + . 'but attempting to rewind the request body failed. ' + . 'Exception: ' . $e; + return self::createRejection($easy, $ctx); + } + + // Retry no more than 3 times before giving up. + if (!isset($easy->options['_curl_retries'])) { + $easy->options['_curl_retries'] = 1; + } elseif ($easy->options['_curl_retries'] == 2) { + $ctx['error'] = 'The cURL request was retried 3 times ' + . 'and did not succeed. The most likely reason for the failure ' + . 'is that cURL was unable to rewind the body of the request ' + . 'and subsequent retries resulted in the same error. Turn on ' + . 'the debug option to see what went wrong. See ' + . 'https://bugs.php.net/bug.php?id=47204 for more information.'; + return self::createRejection($easy, $ctx); + } else { + $easy->options['_curl_retries']++; + } + + return $handler($easy->request, $easy->options); + } + + private function createHeaderFn(EasyHandle $easy) + { + if (isset($easy->options['on_headers'])) { + $onHeaders = $easy->options['on_headers']; + + if (!is_callable($onHeaders)) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + } else { + $onHeaders = null; + } + + return function ($ch, $h) use ( + $onHeaders, + $easy, + &$startingResponse + ) { + $value = trim($h); + if ($value === '') { + $startingResponse = true; + $easy->createResponse(); + if ($onHeaders !== null) { + try { + $onHeaders($easy->response); + } catch (\Exception $e) { + // Associate the exception with the handle and trigger + // a curl header write error by returning 0. + $easy->onHeadersException = $e; + return -1; + } + } + } elseif ($startingResponse) { + $startingResponse = false; + $easy->headers = [$value]; + } else { + $easy->headers[] = $value; + } + return strlen($h); + }; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php new file mode 100644 index 0000000..b0fc236 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php @@ -0,0 +1,27 @@ +factory = isset($options['handle_factory']) + ? $options['handle_factory'] + : new CurlFactory(3); + } + + public function __invoke(RequestInterface $request, array $options) + { + if (isset($options['delay'])) { + usleep($options['delay'] * 1000); + } + + $easy = $this->factory->create($request, $options); + curl_exec($easy->handle); + $easy->errno = curl_errno($easy->handle); + + return CurlFactory::finish($this, $easy, $this->factory); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php new file mode 100644 index 0000000..8eaa34f --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php @@ -0,0 +1,220 @@ +factory = isset($options['handle_factory']) + ? $options['handle_factory'] : new CurlFactory(50); + + if (isset($options['select_timeout'])) { + $this->selectTimeout = $options['select_timeout']; + } elseif ($selectTimeout = getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { + $this->selectTimeout = $selectTimeout; + } else { + $this->selectTimeout = 1; + } + + $this->options = isset($options['options']) ? $options['options'] : []; + } + + public function __get($name) + { + if ($name === '_mh') { + $this->_mh = curl_multi_init(); + + foreach ($this->options as $option => $value) { + // A warning is raised in case of a wrong option. + curl_multi_setopt($this->_mh, $option, $value); + } + + // Further calls to _mh will return the value directly, without entering the + // __get() method at all. + return $this->_mh; + } + + throw new \BadMethodCallException(); + } + + public function __destruct() + { + if (isset($this->_mh)) { + curl_multi_close($this->_mh); + unset($this->_mh); + } + } + + public function __invoke(RequestInterface $request, array $options) + { + $easy = $this->factory->create($request, $options); + $id = (int) $easy->handle; + + $promise = new Promise( + [$this, 'execute'], + function () use ($id) { + return $this->cancel($id); + } + ); + + $this->addRequest(['easy' => $easy, 'deferred' => $promise]); + + return $promise; + } + + /** + * Ticks the curl event loop. + */ + public function tick() + { + // Add any delayed handles if needed. + if ($this->delays) { + $currentTime = Utils::currentTime(); + foreach ($this->delays as $id => $delay) { + if ($currentTime >= $delay) { + unset($this->delays[$id]); + curl_multi_add_handle( + $this->_mh, + $this->handles[$id]['easy']->handle + ); + } + } + } + + // Step through the task queue which may add additional requests. + P\queue()->run(); + + if ($this->active && + curl_multi_select($this->_mh, $this->selectTimeout) === -1 + ) { + // Perform a usleep if a select returns -1. + // See: https://bugs.php.net/bug.php?id=61141 + usleep(250); + } + + while (curl_multi_exec($this->_mh, $this->active) === CURLM_CALL_MULTI_PERFORM); + + $this->processMessages(); + } + + /** + * Runs until all outstanding connections have completed. + */ + public function execute() + { + $queue = P\queue(); + + while ($this->handles || !$queue->isEmpty()) { + // If there are no transfers, then sleep for the next delay + if (!$this->active && $this->delays) { + usleep($this->timeToNext()); + } + $this->tick(); + } + } + + private function addRequest(array $entry) + { + $easy = $entry['easy']; + $id = (int) $easy->handle; + $this->handles[$id] = $entry; + if (empty($easy->options['delay'])) { + curl_multi_add_handle($this->_mh, $easy->handle); + } else { + $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000); + } + } + + /** + * Cancels a handle from sending and removes references to it. + * + * @param int $id Handle ID to cancel and remove. + * + * @return bool True on success, false on failure. + */ + private function cancel($id) + { + // Cannot cancel if it has been processed. + if (!isset($this->handles[$id])) { + return false; + } + + $handle = $this->handles[$id]['easy']->handle; + unset($this->delays[$id], $this->handles[$id]); + curl_multi_remove_handle($this->_mh, $handle); + curl_close($handle); + + return true; + } + + private function processMessages() + { + while ($done = curl_multi_info_read($this->_mh)) { + $id = (int) $done['handle']; + curl_multi_remove_handle($this->_mh, $done['handle']); + + if (!isset($this->handles[$id])) { + // Probably was cancelled. + continue; + } + + $entry = $this->handles[$id]; + unset($this->handles[$id], $this->delays[$id]); + $entry['easy']->errno = $done['result']; + $entry['deferred']->resolve( + CurlFactory::finish( + $this, + $entry['easy'], + $this->factory + ) + ); + } + } + + private function timeToNext() + { + $currentTime = Utils::currentTime(); + $nextTime = PHP_INT_MAX; + foreach ($this->delays as $time) { + if ($time < $nextTime) { + $nextTime = $time; + } + } + + return max(0, $nextTime - $currentTime) * 1000000; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php new file mode 100644 index 0000000..7754e91 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php @@ -0,0 +1,92 @@ +headers)) { + throw new \RuntimeException('No headers have been received'); + } + + // HTTP-version SP status-code SP reason-phrase + $startLine = explode(' ', array_shift($this->headers), 3); + $headers = \GuzzleHttp\headers_from_lines($this->headers); + $normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); + + if (!empty($this->options['decode_content']) + && isset($normalizedKeys['content-encoding']) + ) { + $headers['x-encoded-content-encoding'] + = $headers[$normalizedKeys['content-encoding']]; + unset($headers[$normalizedKeys['content-encoding']]); + if (isset($normalizedKeys['content-length'])) { + $headers['x-encoded-content-length'] + = $headers[$normalizedKeys['content-length']]; + + $bodyLength = (int) $this->sink->getSize(); + if ($bodyLength) { + $headers[$normalizedKeys['content-length']] = $bodyLength; + } else { + unset($headers[$normalizedKeys['content-length']]); + } + } + } + + // Attach a response to the easy handle with the parsed headers. + $this->response = new Response( + $startLine[1], + $headers, + $this->sink, + substr($startLine[0], 5), + isset($startLine[2]) ? (string) $startLine[2] : null + ); + } + + public function __get($name) + { + $msg = $name === 'handle' + ? 'The EasyHandle has been released' + : 'Invalid property: ' . $name; + throw new \BadMethodCallException($msg); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php new file mode 100644 index 0000000..5b312bc --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php @@ -0,0 +1,195 @@ +onFulfilled = $onFulfilled; + $this->onRejected = $onRejected; + + if ($queue) { + call_user_func_array([$this, 'append'], $queue); + } + } + + public function __invoke(RequestInterface $request, array $options) + { + if (!$this->queue) { + throw new \OutOfBoundsException('Mock queue is empty'); + } + + if (isset($options['delay']) && is_numeric($options['delay'])) { + usleep($options['delay'] * 1000); + } + + $this->lastRequest = $request; + $this->lastOptions = $options; + $response = array_shift($this->queue); + + if (isset($options['on_headers'])) { + if (!is_callable($options['on_headers'])) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + try { + $options['on_headers']($response); + } catch (\Exception $e) { + $msg = 'An error was encountered during the on_headers event'; + $response = new RequestException($msg, $request, $response, $e); + } + } + + if (is_callable($response)) { + $response = call_user_func($response, $request, $options); + } + + $response = $response instanceof \Exception + ? \GuzzleHttp\Promise\rejection_for($response) + : \GuzzleHttp\Promise\promise_for($response); + + return $response->then( + function ($value) use ($request, $options) { + $this->invokeStats($request, $options, $value); + if ($this->onFulfilled) { + call_user_func($this->onFulfilled, $value); + } + if (isset($options['sink'])) { + $contents = (string) $value->getBody(); + $sink = $options['sink']; + + if (is_resource($sink)) { + fwrite($sink, $contents); + } elseif (is_string($sink)) { + file_put_contents($sink, $contents); + } elseif ($sink instanceof \Psr\Http\Message\StreamInterface) { + $sink->write($contents); + } + } + + return $value; + }, + function ($reason) use ($request, $options) { + $this->invokeStats($request, $options, null, $reason); + if ($this->onRejected) { + call_user_func($this->onRejected, $reason); + } + return \GuzzleHttp\Promise\rejection_for($reason); + } + ); + } + + /** + * Adds one or more variadic requests, exceptions, callables, or promises + * to the queue. + */ + public function append() + { + foreach (func_get_args() as $value) { + if ($value instanceof ResponseInterface + || $value instanceof \Exception + || $value instanceof PromiseInterface + || is_callable($value) + ) { + $this->queue[] = $value; + } else { + throw new \InvalidArgumentException('Expected a response or ' + . 'exception. Found ' . \GuzzleHttp\describe_type($value)); + } + } + } + + /** + * Get the last received request. + * + * @return RequestInterface + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * Get the last received request options. + * + * @return array + */ + public function getLastOptions() + { + return $this->lastOptions; + } + + /** + * Returns the number of remaining items in the queue. + * + * @return int + */ + public function count() + { + return count($this->queue); + } + + public function reset() + { + $this->queue = []; + } + + private function invokeStats( + RequestInterface $request, + array $options, + ResponseInterface $response = null, + $reason = null + ) { + if (isset($options['on_stats'])) { + $transferTime = isset($options['transfer_time']) ? $options['transfer_time'] : 0; + $stats = new TransferStats($request, $response, $transferTime, $reason); + call_user_func($options['on_stats'], $stats); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php new file mode 100644 index 0000000..f8b00be --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php @@ -0,0 +1,55 @@ +withoutHeader('Expect'); + + // Append a content-length header if body size is zero to match + // cURL's behavior. + if (0 === $request->getBody()->getSize()) { + $request = $request->withHeader('Content-Length', '0'); + } + + return $this->createResponse( + $request, + $options, + $this->createStream($request, $options), + $startTime + ); + } catch (\InvalidArgumentException $e) { + throw $e; + } catch (\Exception $e) { + // Determine if the error was a networking error. + $message = $e->getMessage(); + // This list can probably get more comprehensive. + if (strpos($message, 'getaddrinfo') // DNS lookup failed + || strpos($message, 'Connection refused') + || strpos($message, "couldn't connect to host") // error on HHVM + || strpos($message, "connection attempt failed") + ) { + $e = new ConnectException($e->getMessage(), $request, $e); + } + $e = RequestException::wrapException($request, $e); + $this->invokeStats($options, $request, $startTime, null, $e); + + return \GuzzleHttp\Promise\rejection_for($e); + } + } + + private function invokeStats( + array $options, + RequestInterface $request, + $startTime, + ResponseInterface $response = null, + $error = null + ) { + if (isset($options['on_stats'])) { + $stats = new TransferStats( + $request, + $response, + Utils::currentTime() - $startTime, + $error, + [] + ); + call_user_func($options['on_stats'], $stats); + } + } + + private function createResponse( + RequestInterface $request, + array $options, + $stream, + $startTime + ) { + $hdrs = $this->lastHeaders; + $this->lastHeaders = []; + $parts = explode(' ', array_shift($hdrs), 3); + $ver = explode('/', $parts[0])[1]; + $status = $parts[1]; + $reason = isset($parts[2]) ? $parts[2] : null; + $headers = \GuzzleHttp\headers_from_lines($hdrs); + list($stream, $headers) = $this->checkDecode($options, $headers, $stream); + $stream = Psr7\stream_for($stream); + $sink = $stream; + + if (strcasecmp('HEAD', $request->getMethod())) { + $sink = $this->createSink($stream, $options); + } + + $response = new Psr7\Response($status, $headers, $sink, $ver, $reason); + + if (isset($options['on_headers'])) { + try { + $options['on_headers']($response); + } catch (\Exception $e) { + $msg = 'An error was encountered during the on_headers event'; + $ex = new RequestException($msg, $request, $response, $e); + return \GuzzleHttp\Promise\rejection_for($ex); + } + } + + // Do not drain when the request is a HEAD request because they have + // no body. + if ($sink !== $stream) { + $this->drain( + $stream, + $sink, + $response->getHeaderLine('Content-Length') + ); + } + + $this->invokeStats($options, $request, $startTime, $response, null); + + return new FulfilledPromise($response); + } + + private function createSink(StreamInterface $stream, array $options) + { + if (!empty($options['stream'])) { + return $stream; + } + + $sink = isset($options['sink']) + ? $options['sink'] + : fopen('php://temp', 'r+'); + + return is_string($sink) + ? new Psr7\LazyOpenStream($sink, 'w+') + : Psr7\stream_for($sink); + } + + private function checkDecode(array $options, array $headers, $stream) + { + // Automatically decode responses when instructed. + if (!empty($options['decode_content'])) { + $normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); + if (isset($normalizedKeys['content-encoding'])) { + $encoding = $headers[$normalizedKeys['content-encoding']]; + if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { + $stream = new Psr7\InflateStream( + Psr7\stream_for($stream) + ); + $headers['x-encoded-content-encoding'] + = $headers[$normalizedKeys['content-encoding']]; + // Remove content-encoding header + unset($headers[$normalizedKeys['content-encoding']]); + // Fix content-length header + if (isset($normalizedKeys['content-length'])) { + $headers['x-encoded-content-length'] + = $headers[$normalizedKeys['content-length']]; + + $length = (int) $stream->getSize(); + if ($length === 0) { + unset($headers[$normalizedKeys['content-length']]); + } else { + $headers[$normalizedKeys['content-length']] = [$length]; + } + } + } + } + } + + return [$stream, $headers]; + } + + /** + * Drains the source stream into the "sink" client option. + * + * @param StreamInterface $source + * @param StreamInterface $sink + * @param string $contentLength Header specifying the amount of + * data to read. + * + * @return StreamInterface + * @throws \RuntimeException when the sink option is invalid. + */ + private function drain( + StreamInterface $source, + StreamInterface $sink, + $contentLength + ) { + // If a content-length header is provided, then stop reading once + // that number of bytes has been read. This can prevent infinitely + // reading from a stream when dealing with servers that do not honor + // Connection: Close headers. + Psr7\copy_to_stream( + $source, + $sink, + (strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 + ); + + $sink->seek(0); + $source->close(); + + return $sink; + } + + /** + * Create a resource and check to ensure it was created successfully + * + * @param callable $callback Callable that returns stream resource + * + * @return resource + * @throws \RuntimeException on error + */ + private function createResource(callable $callback) + { + $errors = null; + set_error_handler(function ($_, $msg, $file, $line) use (&$errors) { + $errors[] = [ + 'message' => $msg, + 'file' => $file, + 'line' => $line + ]; + return true; + }); + + $resource = $callback(); + restore_error_handler(); + + if (!$resource) { + $message = 'Error creating resource: '; + foreach ($errors as $err) { + foreach ($err as $key => $value) { + $message .= "[$key] $value" . PHP_EOL; + } + } + throw new \RuntimeException(trim($message)); + } + + return $resource; + } + + private function createStream(RequestInterface $request, array $options) + { + static $methods; + if (!$methods) { + $methods = array_flip(get_class_methods(__CLASS__)); + } + + // HTTP/1.1 streams using the PHP stream wrapper require a + // Connection: close header + if ($request->getProtocolVersion() == '1.1' + && !$request->hasHeader('Connection') + ) { + $request = $request->withHeader('Connection', 'close'); + } + + // Ensure SSL is verified by default + if (!isset($options['verify'])) { + $options['verify'] = true; + } + + $params = []; + $context = $this->getDefaultContext($request); + + if (isset($options['on_headers']) && !is_callable($options['on_headers'])) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + + if (!empty($options)) { + foreach ($options as $key => $value) { + $method = "add_{$key}"; + if (isset($methods[$method])) { + $this->{$method}($request, $context, $value, $params); + } + } + } + + if (isset($options['stream_context'])) { + if (!is_array($options['stream_context'])) { + throw new \InvalidArgumentException('stream_context must be an array'); + } + $context = array_replace_recursive( + $context, + $options['stream_context'] + ); + } + + // Microsoft NTLM authentication only supported with curl handler + if (isset($options['auth']) + && is_array($options['auth']) + && isset($options['auth'][2]) + && 'ntlm' == $options['auth'][2] + ) { + throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); + } + + $uri = $this->resolveHost($request, $options); + + $context = $this->createResource( + function () use ($context, $params) { + return stream_context_create($context, $params); + } + ); + + return $this->createResource( + function () use ($uri, &$http_response_header, $context, $options) { + $resource = fopen((string) $uri, 'r', null, $context); + $this->lastHeaders = $http_response_header; + + if (isset($options['read_timeout'])) { + $readTimeout = $options['read_timeout']; + $sec = (int) $readTimeout; + $usec = ($readTimeout - $sec) * 100000; + stream_set_timeout($resource, $sec, $usec); + } + + return $resource; + } + ); + } + + private function resolveHost(RequestInterface $request, array $options) + { + $uri = $request->getUri(); + + if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) { + if ('v4' === $options['force_ip_resolve']) { + $records = dns_get_record($uri->getHost(), DNS_A); + if (!isset($records[0]['ip'])) { + throw new ConnectException( + sprintf( + "Could not resolve IPv4 address for host '%s'", + $uri->getHost() + ), + $request + ); + } + $uri = $uri->withHost($records[0]['ip']); + } elseif ('v6' === $options['force_ip_resolve']) { + $records = dns_get_record($uri->getHost(), DNS_AAAA); + if (!isset($records[0]['ipv6'])) { + throw new ConnectException( + sprintf( + "Could not resolve IPv6 address for host '%s'", + $uri->getHost() + ), + $request + ); + } + $uri = $uri->withHost('[' . $records[0]['ipv6'] . ']'); + } + } + + return $uri; + } + + private function getDefaultContext(RequestInterface $request) + { + $headers = ''; + foreach ($request->getHeaders() as $name => $value) { + foreach ($value as $val) { + $headers .= "$name: $val\r\n"; + } + } + + $context = [ + 'http' => [ + 'method' => $request->getMethod(), + 'header' => $headers, + 'protocol_version' => $request->getProtocolVersion(), + 'ignore_errors' => true, + 'follow_location' => 0, + ], + ]; + + $body = (string) $request->getBody(); + + if (!empty($body)) { + $context['http']['content'] = $body; + // Prevent the HTTP handler from adding a Content-Type header. + if (!$request->hasHeader('Content-Type')) { + $context['http']['header'] .= "Content-Type:\r\n"; + } + } + + $context['http']['header'] = rtrim($context['http']['header']); + + return $context; + } + + private function add_proxy(RequestInterface $request, &$options, $value, &$params) + { + if (!is_array($value)) { + $options['http']['proxy'] = $value; + } else { + $scheme = $request->getUri()->getScheme(); + if (isset($value[$scheme])) { + if (!isset($value['no']) + || !\GuzzleHttp\is_host_in_noproxy( + $request->getUri()->getHost(), + $value['no'] + ) + ) { + $options['http']['proxy'] = $value[$scheme]; + } + } + } + } + + private function add_timeout(RequestInterface $request, &$options, $value, &$params) + { + if ($value > 0) { + $options['http']['timeout'] = $value; + } + } + + private function add_verify(RequestInterface $request, &$options, $value, &$params) + { + if ($value === true) { + // PHP 5.6 or greater will find the system cert by default. When + // < 5.6, use the Guzzle bundled cacert. + if (PHP_VERSION_ID < 50600) { + $options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle(); + } + } elseif (is_string($value)) { + $options['ssl']['cafile'] = $value; + if (!file_exists($value)) { + throw new \RuntimeException("SSL CA bundle not found: $value"); + } + } elseif ($value === false) { + $options['ssl']['verify_peer'] = false; + $options['ssl']['verify_peer_name'] = false; + return; + } else { + throw new \InvalidArgumentException('Invalid verify request option'); + } + + $options['ssl']['verify_peer'] = true; + $options['ssl']['verify_peer_name'] = true; + $options['ssl']['allow_self_signed'] = false; + } + + private function add_cert(RequestInterface $request, &$options, $value, &$params) + { + if (is_array($value)) { + $options['ssl']['passphrase'] = $value[1]; + $value = $value[0]; + } + + if (!file_exists($value)) { + throw new \RuntimeException("SSL certificate not found: {$value}"); + } + + $options['ssl']['local_cert'] = $value; + } + + private function add_progress(RequestInterface $request, &$options, $value, &$params) + { + $this->addNotification( + $params, + function ($code, $a, $b, $c, $transferred, $total) use ($value) { + if ($code == STREAM_NOTIFY_PROGRESS) { + $value($total, $transferred, null, null); + } + } + ); + } + + private function add_debug(RequestInterface $request, &$options, $value, &$params) + { + if ($value === false) { + return; + } + + static $map = [ + STREAM_NOTIFY_CONNECT => 'CONNECT', + STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', + STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', + STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', + STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', + STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', + STREAM_NOTIFY_PROGRESS => 'PROGRESS', + STREAM_NOTIFY_FAILURE => 'FAILURE', + STREAM_NOTIFY_COMPLETED => 'COMPLETED', + STREAM_NOTIFY_RESOLVE => 'RESOLVE', + ]; + static $args = ['severity', 'message', 'message_code', + 'bytes_transferred', 'bytes_max']; + + $value = \GuzzleHttp\debug_resource($value); + $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); + $this->addNotification( + $params, + function () use ($ident, $value, $map, $args) { + $passed = func_get_args(); + $code = array_shift($passed); + fprintf($value, '<%s> [%s] ', $ident, $map[$code]); + foreach (array_filter($passed) as $i => $v) { + fwrite($value, $args[$i] . ': "' . $v . '" '); + } + fwrite($value, "\n"); + } + ); + } + + private function addNotification(array &$params, callable $notify) + { + // Wrap the existing function if needed. + if (!isset($params['notification'])) { + $params['notification'] = $notify; + } else { + $params['notification'] = $this->callArray([ + $params['notification'], + $notify + ]); + } + } + + private function callArray(array $functions) + { + return function () use ($functions) { + $args = func_get_args(); + foreach ($functions as $fn) { + call_user_func_array($fn, $args); + } + }; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/HandlerStack.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/HandlerStack.php new file mode 100644 index 0000000..6a49cc0 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/HandlerStack.php @@ -0,0 +1,277 @@ +push(Middleware::httpErrors(), 'http_errors'); + $stack->push(Middleware::redirect(), 'allow_redirects'); + $stack->push(Middleware::cookies(), 'cookies'); + $stack->push(Middleware::prepareBody(), 'prepare_body'); + + return $stack; + } + + /** + * @param callable $handler Underlying HTTP handler. + */ + public function __construct(callable $handler = null) + { + $this->handler = $handler; + } + + /** + * Invokes the handler stack as a composed handler + * + * @param RequestInterface $request + * @param array $options + * + * @return ResponseInterface|PromiseInterface + */ + public function __invoke(RequestInterface $request, array $options) + { + $handler = $this->resolve(); + + return $handler($request, $options); + } + + /** + * Dumps a string representation of the stack. + * + * @return string + */ + public function __toString() + { + $depth = 0; + $stack = []; + if ($this->handler) { + $stack[] = "0) Handler: " . $this->debugCallable($this->handler); + } + + $result = ''; + foreach (array_reverse($this->stack) as $tuple) { + $depth++; + $str = "{$depth}) Name: '{$tuple[1]}', "; + $str .= "Function: " . $this->debugCallable($tuple[0]); + $result = "> {$str}\n{$result}"; + $stack[] = $str; + } + + foreach (array_keys($stack) as $k) { + $result .= "< {$stack[$k]}\n"; + } + + return $result; + } + + /** + * Set the HTTP handler that actually returns a promise. + * + * @param callable $handler Accepts a request and array of options and + * returns a Promise. + */ + public function setHandler(callable $handler) + { + $this->handler = $handler; + $this->cached = null; + } + + /** + * Returns true if the builder has a handler. + * + * @return bool + */ + public function hasHandler() + { + return (bool) $this->handler; + } + + /** + * Unshift a middleware to the bottom of the stack. + * + * @param callable $middleware Middleware function + * @param string $name Name to register for this middleware. + */ + public function unshift(callable $middleware, $name = null) + { + array_unshift($this->stack, [$middleware, $name]); + $this->cached = null; + } + + /** + * Push a middleware to the top of the stack. + * + * @param callable $middleware Middleware function + * @param string $name Name to register for this middleware. + */ + public function push(callable $middleware, $name = '') + { + $this->stack[] = [$middleware, $name]; + $this->cached = null; + } + + /** + * Add a middleware before another middleware by name. + * + * @param string $findName Middleware to find + * @param callable $middleware Middleware function + * @param string $withName Name to register for this middleware. + */ + public function before($findName, callable $middleware, $withName = '') + { + $this->splice($findName, $withName, $middleware, true); + } + + /** + * Add a middleware after another middleware by name. + * + * @param string $findName Middleware to find + * @param callable $middleware Middleware function + * @param string $withName Name to register for this middleware. + */ + public function after($findName, callable $middleware, $withName = '') + { + $this->splice($findName, $withName, $middleware, false); + } + + /** + * Remove a middleware by instance or name from the stack. + * + * @param callable|string $remove Middleware to remove by instance or name. + */ + public function remove($remove) + { + $this->cached = null; + $idx = is_callable($remove) ? 0 : 1; + $this->stack = array_values(array_filter( + $this->stack, + function ($tuple) use ($idx, $remove) { + return $tuple[$idx] !== $remove; + } + )); + } + + /** + * Compose the middleware and handler into a single callable function. + * + * @return callable + */ + public function resolve() + { + if (!$this->cached) { + if (!($prev = $this->handler)) { + throw new \LogicException('No handler has been specified'); + } + + foreach (array_reverse($this->stack) as $fn) { + $prev = $fn[0]($prev); + } + + $this->cached = $prev; + } + + return $this->cached; + } + + /** + * @param string $name + * @return int + */ + private function findByName($name) + { + foreach ($this->stack as $k => $v) { + if ($v[1] === $name) { + return $k; + } + } + + throw new \InvalidArgumentException("Middleware not found: $name"); + } + + /** + * Splices a function into the middleware list at a specific position. + * + * @param string $findName + * @param string $withName + * @param callable $middleware + * @param bool $before + */ + private function splice($findName, $withName, callable $middleware, $before) + { + $this->cached = null; + $idx = $this->findByName($findName); + $tuple = [$middleware, $withName]; + + if ($before) { + if ($idx === 0) { + array_unshift($this->stack, $tuple); + } else { + $replacement = [$tuple, $this->stack[$idx]]; + array_splice($this->stack, $idx, 1, $replacement); + } + } elseif ($idx === count($this->stack) - 1) { + $this->stack[] = $tuple; + } else { + $replacement = [$this->stack[$idx], $tuple]; + array_splice($this->stack, $idx, 1, $replacement); + } + } + + /** + * Provides a debug string for a given callable. + * + * @param array|callable $fn Function to write as a string. + * + * @return string + */ + private function debugCallable($fn) + { + if (is_string($fn)) { + return "callable({$fn})"; + } + + if (is_array($fn)) { + return is_string($fn[0]) + ? "callable({$fn[0]}::{$fn[1]})" + : "callable(['" . get_class($fn[0]) . "', '{$fn[1]}'])"; + } + + return 'callable(' . spl_object_hash($fn) . ')'; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/MessageFormatter.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/MessageFormatter.php new file mode 100644 index 0000000..dc36bb5 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/MessageFormatter.php @@ -0,0 +1,185 @@ +>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}"; + const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}'; + + /** @var string Template used to format log messages */ + private $template; + + /** + * @param string $template Log message template + */ + public function __construct($template = self::CLF) + { + $this->template = $template ?: self::CLF; + } + + /** + * Returns a formatted message string. + * + * @param RequestInterface $request Request that was sent + * @param ResponseInterface $response Response that was received + * @param \Exception $error Exception that was received + * + * @return string + */ + public function format( + RequestInterface $request, + ResponseInterface $response = null, + \Exception $error = null + ) { + $cache = []; + + return preg_replace_callback( + '/{\s*([A-Za-z_\-\.0-9]+)\s*}/', + function (array $matches) use ($request, $response, $error, &$cache) { + if (isset($cache[$matches[1]])) { + return $cache[$matches[1]]; + } + + $result = ''; + switch ($matches[1]) { + case 'request': + $result = Psr7\str($request); + break; + case 'response': + $result = $response ? Psr7\str($response) : ''; + break; + case 'req_headers': + $result = trim($request->getMethod() + . ' ' . $request->getRequestTarget()) + . ' HTTP/' . $request->getProtocolVersion() . "\r\n" + . $this->headers($request); + break; + case 'res_headers': + $result = $response ? + sprintf( + 'HTTP/%s %d %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ) . "\r\n" . $this->headers($response) + : 'NULL'; + break; + case 'req_body': + $result = $request->getBody(); + break; + case 'res_body': + $result = $response ? $response->getBody() : 'NULL'; + break; + case 'ts': + case 'date_iso_8601': + $result = gmdate('c'); + break; + case 'date_common_log': + $result = date('d/M/Y:H:i:s O'); + break; + case 'method': + $result = $request->getMethod(); + break; + case 'version': + $result = $request->getProtocolVersion(); + break; + case 'uri': + case 'url': + $result = $request->getUri(); + break; + case 'target': + $result = $request->getRequestTarget(); + break; + case 'req_version': + $result = $request->getProtocolVersion(); + break; + case 'res_version': + $result = $response + ? $response->getProtocolVersion() + : 'NULL'; + break; + case 'host': + $result = $request->getHeaderLine('Host'); + break; + case 'hostname': + $result = gethostname(); + break; + case 'code': + $result = $response ? $response->getStatusCode() : 'NULL'; + break; + case 'phrase': + $result = $response ? $response->getReasonPhrase() : 'NULL'; + break; + case 'error': + $result = $error ? $error->getMessage() : 'NULL'; + break; + default: + // handle prefixed dynamic headers + if (strpos($matches[1], 'req_header_') === 0) { + $result = $request->getHeaderLine(substr($matches[1], 11)); + } elseif (strpos($matches[1], 'res_header_') === 0) { + $result = $response + ? $response->getHeaderLine(substr($matches[1], 11)) + : 'NULL'; + } + } + + $cache[$matches[1]] = $result; + return $result; + }, + $this->template + ); + } + + /** + * Get headers from message as string + * + * @return string + */ + private function headers(MessageInterface $message) + { + $result = ''; + foreach ($message->getHeaders() as $name => $values) { + $result .= $name . ': ' . implode(', ', $values) . "\r\n"; + } + + return trim($result); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Middleware.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Middleware.php new file mode 100644 index 0000000..bffc197 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Middleware.php @@ -0,0 +1,254 @@ +withCookieHeader($request); + return $handler($request, $options) + ->then( + function ($response) use ($cookieJar, $request) { + $cookieJar->extractCookies($request, $response); + return $response; + } + ); + }; + }; + } + + /** + * Middleware that throws exceptions for 4xx or 5xx responses when the + * "http_error" request option is set to true. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function httpErrors() + { + return function (callable $handler) { + return function ($request, array $options) use ($handler) { + if (empty($options['http_errors'])) { + return $handler($request, $options); + } + return $handler($request, $options)->then( + function (ResponseInterface $response) use ($request) { + $code = $response->getStatusCode(); + if ($code < 400) { + return $response; + } + throw RequestException::create($request, $response); + } + ); + }; + }; + } + + /** + * Middleware that pushes history data to an ArrayAccess container. + * + * @param array|\ArrayAccess $container Container to hold the history (by reference). + * + * @return callable Returns a function that accepts the next handler. + * @throws \InvalidArgumentException if container is not an array or ArrayAccess. + */ + public static function history(&$container) + { + if (!is_array($container) && !$container instanceof \ArrayAccess) { + throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); + } + + return function (callable $handler) use (&$container) { + return function ($request, array $options) use ($handler, &$container) { + return $handler($request, $options)->then( + function ($value) use ($request, &$container, $options) { + $container[] = [ + 'request' => $request, + 'response' => $value, + 'error' => null, + 'options' => $options + ]; + return $value; + }, + function ($reason) use ($request, &$container, $options) { + $container[] = [ + 'request' => $request, + 'response' => null, + 'error' => $reason, + 'options' => $options + ]; + return \GuzzleHttp\Promise\rejection_for($reason); + } + ); + }; + }; + } + + /** + * Middleware that invokes a callback before and after sending a request. + * + * The provided listener cannot modify or alter the response. It simply + * "taps" into the chain to be notified before returning the promise. The + * before listener accepts a request and options array, and the after + * listener accepts a request, options array, and response promise. + * + * @param callable $before Function to invoke before forwarding the request. + * @param callable $after Function invoked after forwarding. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function tap(callable $before = null, callable $after = null) + { + return function (callable $handler) use ($before, $after) { + return function ($request, array $options) use ($handler, $before, $after) { + if ($before) { + $before($request, $options); + } + $response = $handler($request, $options); + if ($after) { + $after($request, $options, $response); + } + return $response; + }; + }; + } + + /** + * Middleware that handles request redirects. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function redirect() + { + return function (callable $handler) { + return new RedirectMiddleware($handler); + }; + } + + /** + * Middleware that retries requests based on the boolean result of + * invoking the provided "decider" function. + * + * If no delay function is provided, a simple implementation of exponential + * backoff will be utilized. + * + * @param callable $decider Function that accepts the number of retries, + * a request, [response], and [exception] and + * returns true if the request is to be retried. + * @param callable $delay Function that accepts the number of retries and + * returns the number of milliseconds to delay. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function retry(callable $decider, callable $delay = null) + { + return function (callable $handler) use ($decider, $delay) { + return new RetryMiddleware($decider, $handler, $delay); + }; + } + + /** + * Middleware that logs requests, responses, and errors using a message + * formatter. + * + * @param LoggerInterface $logger Logs messages. + * @param MessageFormatter $formatter Formatter used to create message strings. + * @param string $logLevel Level at which to log requests. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = 'info' /* \Psr\Log\LogLevel::INFO */) + { + return function (callable $handler) use ($logger, $formatter, $logLevel) { + return function ($request, array $options) use ($handler, $logger, $formatter, $logLevel) { + return $handler($request, $options)->then( + function ($response) use ($logger, $request, $formatter, $logLevel) { + $message = $formatter->format($request, $response); + $logger->log($logLevel, $message); + return $response; + }, + function ($reason) use ($logger, $request, $formatter) { + $response = $reason instanceof RequestException + ? $reason->getResponse() + : null; + $message = $formatter->format($request, $response, $reason); + $logger->notice($message); + return \GuzzleHttp\Promise\rejection_for($reason); + } + ); + }; + }; + } + + /** + * This middleware adds a default content-type if possible, a default + * content-length or transfer-encoding header, and the expect header. + * + * @return callable + */ + public static function prepareBody() + { + return function (callable $handler) { + return new PrepareBodyMiddleware($handler); + }; + } + + /** + * Middleware that applies a map function to the request before passing to + * the next handler. + * + * @param callable $fn Function that accepts a RequestInterface and returns + * a RequestInterface. + * @return callable + */ + public static function mapRequest(callable $fn) + { + return function (callable $handler) use ($fn) { + return function ($request, array $options) use ($handler, $fn) { + return $handler($fn($request), $options); + }; + }; + } + + /** + * Middleware that applies a map function to the resolved promise's + * response. + * + * @param callable $fn Function that accepts a ResponseInterface and + * returns a ResponseInterface. + * @return callable + */ + public static function mapResponse(callable $fn) + { + return function (callable $handler) use ($fn) { + return function ($request, array $options) use ($handler, $fn) { + return $handler($request, $options)->then($fn); + }; + }; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Pool.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Pool.php new file mode 100644 index 0000000..5838db4 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Pool.php @@ -0,0 +1,134 @@ + $rfn) { + if ($rfn instanceof RequestInterface) { + yield $key => $client->sendAsync($rfn, $opts); + } elseif (is_callable($rfn)) { + yield $key => $rfn($opts); + } else { + throw new \InvalidArgumentException('Each value yielded by ' + . 'the iterator must be a Psr7\Http\Message\RequestInterface ' + . 'or a callable that returns a promise that fulfills ' + . 'with a Psr7\Message\Http\ResponseInterface object.'); + } + } + }; + + $this->each = new EachPromise($requests(), $config); + } + + /** + * Get promise + * + * @return PromiseInterface + */ + public function promise() + { + return $this->each->promise(); + } + + /** + * Sends multiple requests concurrently and returns an array of responses + * and exceptions that uses the same ordering as the provided requests. + * + * IMPORTANT: This method keeps every request and response in memory, and + * as such, is NOT recommended when sending a large number or an + * indeterminate number of requests concurrently. + * + * @param ClientInterface $client Client used to send the requests + * @param array|\Iterator $requests Requests to send concurrently. + * @param array $options Passes through the options available in + * {@see GuzzleHttp\Pool::__construct} + * + * @return array Returns an array containing the response or an exception + * in the same order that the requests were sent. + * @throws \InvalidArgumentException if the event format is incorrect. + */ + public static function batch( + ClientInterface $client, + $requests, + array $options = [] + ) { + $res = []; + self::cmpCallback($options, 'fulfilled', $res); + self::cmpCallback($options, 'rejected', $res); + $pool = new static($client, $requests, $options); + $pool->promise()->wait(); + ksort($res); + + return $res; + } + + /** + * Execute callback(s) + * + * @return void + */ + private static function cmpCallback(array &$options, $name, array &$results) + { + if (!isset($options[$name])) { + $options[$name] = function ($v, $k) use (&$results) { + $results[$k] = $v; + }; + } else { + $currentFn = $options[$name]; + $options[$name] = function ($v, $k) use (&$results, $currentFn) { + $currentFn($v, $k); + $results[$k] = $v; + }; + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php new file mode 100644 index 0000000..568a1e9 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php @@ -0,0 +1,111 @@ +nextHandler = $nextHandler; + } + + /** + * @param RequestInterface $request + * @param array $options + * + * @return PromiseInterface + */ + public function __invoke(RequestInterface $request, array $options) + { + $fn = $this->nextHandler; + + // Don't do anything if the request has no body. + if ($request->getBody()->getSize() === 0) { + return $fn($request, $options); + } + + $modify = []; + + // Add a default content-type if possible. + if (!$request->hasHeader('Content-Type')) { + if ($uri = $request->getBody()->getMetadata('uri')) { + if ($type = Psr7\mimetype_from_filename($uri)) { + $modify['set_headers']['Content-Type'] = $type; + } + } + } + + // Add a default content-length or transfer-encoding header. + if (!$request->hasHeader('Content-Length') + && !$request->hasHeader('Transfer-Encoding') + ) { + $size = $request->getBody()->getSize(); + if ($size !== null) { + $modify['set_headers']['Content-Length'] = $size; + } else { + $modify['set_headers']['Transfer-Encoding'] = 'chunked'; + } + } + + // Add the expect header if needed. + $this->addExpectHeader($request, $options, $modify); + + return $fn(Psr7\modify_request($request, $modify), $options); + } + + /** + * Add expect header + * + * @return void + */ + private function addExpectHeader( + RequestInterface $request, + array $options, + array &$modify + ) { + // Determine if the Expect header should be used + if ($request->hasHeader('Expect')) { + return; + } + + $expect = isset($options['expect']) ? $options['expect'] : null; + + // Return if disabled or if you're not using HTTP/1.1 or HTTP/2.0 + if ($expect === false || $request->getProtocolVersion() < 1.1) { + return; + } + + // The expect header is unconditionally enabled + if ($expect === true) { + $modify['set_headers']['Expect'] = '100-Continue'; + return; + } + + // By default, send the expect header when the payload is > 1mb + if ($expect === null) { + $expect = 1048576; + } + + // Always add if the body cannot be rewound, the size cannot be + // determined, or the size is greater than the cutoff threshold + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { + $modify['set_headers']['Expect'] = '100-Continue'; + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php new file mode 100644 index 0000000..e4644b7 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php @@ -0,0 +1,255 @@ + 5, + 'protocols' => ['http', 'https'], + 'strict' => false, + 'referer' => false, + 'track_redirects' => false, + ]; + + /** @var callable */ + private $nextHandler; + + /** + * @param callable $nextHandler Next handler to invoke. + */ + public function __construct(callable $nextHandler) + { + $this->nextHandler = $nextHandler; + } + + /** + * @param RequestInterface $request + * @param array $options + * + * @return PromiseInterface + */ + public function __invoke(RequestInterface $request, array $options) + { + $fn = $this->nextHandler; + + if (empty($options['allow_redirects'])) { + return $fn($request, $options); + } + + if ($options['allow_redirects'] === true) { + $options['allow_redirects'] = self::$defaultSettings; + } elseif (!is_array($options['allow_redirects'])) { + throw new \InvalidArgumentException('allow_redirects must be true, false, or array'); + } else { + // Merge the default settings with the provided settings + $options['allow_redirects'] += self::$defaultSettings; + } + + if (empty($options['allow_redirects']['max'])) { + return $fn($request, $options); + } + + return $fn($request, $options) + ->then(function (ResponseInterface $response) use ($request, $options) { + return $this->checkRedirect($request, $options, $response); + }); + } + + /** + * @param RequestInterface $request + * @param array $options + * @param ResponseInterface $response + * + * @return ResponseInterface|PromiseInterface + */ + public function checkRedirect( + RequestInterface $request, + array $options, + ResponseInterface $response + ) { + if (substr($response->getStatusCode(), 0, 1) != '3' + || !$response->hasHeader('Location') + ) { + return $response; + } + + $this->guardMax($request, $options); + $nextRequest = $this->modifyRequest($request, $options, $response); + + if (isset($options['allow_redirects']['on_redirect'])) { + call_user_func( + $options['allow_redirects']['on_redirect'], + $request, + $response, + $nextRequest->getUri() + ); + } + + /** @var PromiseInterface|ResponseInterface $promise */ + $promise = $this($nextRequest, $options); + + // Add headers to be able to track history of redirects. + if (!empty($options['allow_redirects']['track_redirects'])) { + return $this->withTracking( + $promise, + (string) $nextRequest->getUri(), + $response->getStatusCode() + ); + } + + return $promise; + } + + /** + * Enable tracking on promise. + * + * @return PromiseInterface + */ + private function withTracking(PromiseInterface $promise, $uri, $statusCode) + { + return $promise->then( + function (ResponseInterface $response) use ($uri, $statusCode) { + // Note that we are pushing to the front of the list as this + // would be an earlier response than what is currently present + // in the history header. + $historyHeader = $response->getHeader(self::HISTORY_HEADER); + $statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER); + array_unshift($historyHeader, $uri); + array_unshift($statusHeader, $statusCode); + return $response->withHeader(self::HISTORY_HEADER, $historyHeader) + ->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader); + } + ); + } + + /** + * Check for too many redirects + * + * @return void + * + * @throws TooManyRedirectsException Too many redirects. + */ + private function guardMax(RequestInterface $request, array &$options) + { + $current = isset($options['__redirect_count']) + ? $options['__redirect_count'] + : 0; + $options['__redirect_count'] = $current + 1; + $max = $options['allow_redirects']['max']; + + if ($options['__redirect_count'] > $max) { + throw new TooManyRedirectsException( + "Will not follow more than {$max} redirects", + $request + ); + } + } + + /** + * @param RequestInterface $request + * @param array $options + * @param ResponseInterface $response + * + * @return RequestInterface + */ + public function modifyRequest( + RequestInterface $request, + array $options, + ResponseInterface $response + ) { + // Request modifications to apply. + $modify = []; + $protocols = $options['allow_redirects']['protocols']; + + // Use a GET request if this is an entity enclosing request and we are + // not forcing RFC compliance, but rather emulating what all browsers + // would do. + $statusCode = $response->getStatusCode(); + if ($statusCode == 303 || + ($statusCode <= 302 && !$options['allow_redirects']['strict']) + ) { + $modify['method'] = 'GET'; + $modify['body'] = ''; + } + + $uri = $this->redirectUri($request, $response, $protocols); + if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) { + $idnOptions = ($options['idn_conversion'] === true) ? IDNA_DEFAULT : $options['idn_conversion']; + $uri = Utils::idnUriConvert($uri, $idnOptions); + } + + $modify['uri'] = $uri; + Psr7\rewind_body($request); + + // Add the Referer header if it is told to do so and only + // add the header if we are not redirecting from https to http. + if ($options['allow_redirects']['referer'] + && $modify['uri']->getScheme() === $request->getUri()->getScheme() + ) { + $uri = $request->getUri()->withUserInfo(''); + $modify['set_headers']['Referer'] = (string) $uri; + } else { + $modify['remove_headers'][] = 'Referer'; + } + + // Remove Authorization header if host is different. + if ($request->getUri()->getHost() !== $modify['uri']->getHost()) { + $modify['remove_headers'][] = 'Authorization'; + } + + return Psr7\modify_request($request, $modify); + } + + /** + * Set the appropriate URL on the request based on the location header + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param array $protocols + * + * @return UriInterface + */ + private function redirectUri( + RequestInterface $request, + ResponseInterface $response, + array $protocols + ) { + $location = Psr7\UriResolver::resolve( + $request->getUri(), + new Psr7\Uri($response->getHeaderLine('Location')) + ); + + // Ensure that the redirect URI is allowed based on the protocols. + if (!in_array($location->getScheme(), $protocols)) { + throw new BadResponseException( + sprintf( + 'Redirect URI, %s, does not use one of the allowed redirect protocols: %s', + $location, + implode(', ', $protocols) + ), + $request, + $response + ); + } + + return $location; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RequestOptions.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RequestOptions.php new file mode 100644 index 0000000..355f658 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/RequestOptions.php @@ -0,0 +1,263 @@ +decider = $decider; + $this->nextHandler = $nextHandler; + $this->delay = $delay ?: __CLASS__ . '::exponentialDelay'; + } + + /** + * Default exponential backoff delay function. + * + * @param int $retries + * + * @return int milliseconds. + */ + public static function exponentialDelay($retries) + { + return (int) pow(2, $retries - 1) * 1000; + } + + /** + * @param RequestInterface $request + * @param array $options + * + * @return PromiseInterface + */ + public function __invoke(RequestInterface $request, array $options) + { + if (!isset($options['retries'])) { + $options['retries'] = 0; + } + + $fn = $this->nextHandler; + return $fn($request, $options) + ->then( + $this->onFulfilled($request, $options), + $this->onRejected($request, $options) + ); + } + + /** + * Execute fulfilled closure + * + * @return mixed + */ + private function onFulfilled(RequestInterface $req, array $options) + { + return function ($value) use ($req, $options) { + if (!call_user_func( + $this->decider, + $options['retries'], + $req, + $value, + null + )) { + return $value; + } + return $this->doRetry($req, $options, $value); + }; + } + + /** + * Execute rejected closure + * + * @return callable + */ + private function onRejected(RequestInterface $req, array $options) + { + return function ($reason) use ($req, $options) { + if (!call_user_func( + $this->decider, + $options['retries'], + $req, + null, + $reason + )) { + return \GuzzleHttp\Promise\rejection_for($reason); + } + return $this->doRetry($req, $options); + }; + } + + /** + * @return self + */ + private function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null) + { + $options['delay'] = call_user_func($this->delay, ++$options['retries'], $response); + + return $this($request, $options); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/TransferStats.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/TransferStats.php new file mode 100644 index 0000000..87fb3c0 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/TransferStats.php @@ -0,0 +1,126 @@ +request = $request; + $this->response = $response; + $this->transferTime = $transferTime; + $this->handlerErrorData = $handlerErrorData; + $this->handlerStats = $handlerStats; + } + + /** + * @return RequestInterface + */ + public function getRequest() + { + return $this->request; + } + + /** + * Returns the response that was received (if any). + * + * @return ResponseInterface|null + */ + public function getResponse() + { + return $this->response; + } + + /** + * Returns true if a response was received. + * + * @return bool + */ + public function hasResponse() + { + return $this->response !== null; + } + + /** + * Gets handler specific error data. + * + * This might be an exception, a integer representing an error code, or + * anything else. Relying on this value assumes that you know what handler + * you are using. + * + * @return mixed + */ + public function getHandlerErrorData() + { + return $this->handlerErrorData; + } + + /** + * Get the effective URI the request was sent to. + * + * @return UriInterface + */ + public function getEffectiveUri() + { + return $this->request->getUri(); + } + + /** + * Get the estimated time the request was being transferred by the handler. + * + * @return float|null Time in seconds. + */ + public function getTransferTime() + { + return $this->transferTime; + } + + /** + * Gets an array of all of the handler specific transfer data. + * + * @return array + */ + public function getHandlerStats() + { + return $this->handlerStats; + } + + /** + * Get a specific handler statistic from the handler by name. + * + * @param string $stat Handler specific transfer stat to retrieve. + * + * @return mixed|null + */ + public function getHandlerStat($stat) + { + return isset($this->handlerStats[$stat]) + ? $this->handlerStats[$stat] + : null; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/UriTemplate.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/UriTemplate.php new file mode 100644 index 0000000..96dcfd0 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/UriTemplate.php @@ -0,0 +1,237 @@ + ['prefix' => '', 'joiner' => ',', 'query' => false], + '+' => ['prefix' => '', 'joiner' => ',', 'query' => false], + '#' => ['prefix' => '#', 'joiner' => ',', 'query' => false], + '.' => ['prefix' => '.', 'joiner' => '.', 'query' => false], + '/' => ['prefix' => '/', 'joiner' => '/', 'query' => false], + ';' => ['prefix' => ';', 'joiner' => ';', 'query' => true], + '?' => ['prefix' => '?', 'joiner' => '&', 'query' => true], + '&' => ['prefix' => '&', 'joiner' => '&', 'query' => true] + ]; + + /** @var array Delimiters */ + private static $delims = [':', '/', '?', '#', '[', ']', '@', '!', '$', + '&', '\'', '(', ')', '*', '+', ',', ';', '=']; + + /** @var array Percent encoded delimiters */ + private static $delimsPct = ['%3A', '%2F', '%3F', '%23', '%5B', '%5D', + '%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C', + '%3B', '%3D']; + + public function expand($template, array $variables) + { + if (false === strpos($template, '{')) { + return $template; + } + + $this->template = $template; + $this->variables = $variables; + + return preg_replace_callback( + '/\{([^\}]+)\}/', + [$this, 'expandMatch'], + $this->template + ); + } + + /** + * Parse an expression into parts + * + * @param string $expression Expression to parse + * + * @return array Returns an associative array of parts + */ + private function parseExpression($expression) + { + $result = []; + + if (isset(self::$operatorHash[$expression[0]])) { + $result['operator'] = $expression[0]; + $expression = substr($expression, 1); + } else { + $result['operator'] = ''; + } + + foreach (explode(',', $expression) as $value) { + $value = trim($value); + $varspec = []; + if ($colonPos = strpos($value, ':')) { + $varspec['value'] = substr($value, 0, $colonPos); + $varspec['modifier'] = ':'; + $varspec['position'] = (int) substr($value, $colonPos + 1); + } elseif (substr($value, -1) === '*') { + $varspec['modifier'] = '*'; + $varspec['value'] = substr($value, 0, -1); + } else { + $varspec['value'] = (string) $value; + $varspec['modifier'] = ''; + } + $result['values'][] = $varspec; + } + + return $result; + } + + /** + * Process an expansion + * + * @param array $matches Matches met in the preg_replace_callback + * + * @return string Returns the replacement string + */ + private function expandMatch(array $matches) + { + static $rfc1738to3986 = ['+' => '%20', '%7e' => '~']; + + $replacements = []; + $parsed = self::parseExpression($matches[1]); + $prefix = self::$operatorHash[$parsed['operator']]['prefix']; + $joiner = self::$operatorHash[$parsed['operator']]['joiner']; + $useQuery = self::$operatorHash[$parsed['operator']]['query']; + + foreach ($parsed['values'] as $value) { + if (!isset($this->variables[$value['value']])) { + continue; + } + + $variable = $this->variables[$value['value']]; + $actuallyUseQuery = $useQuery; + $expanded = ''; + + if (is_array($variable)) { + $isAssoc = $this->isAssoc($variable); + $kvp = []; + foreach ($variable as $key => $var) { + if ($isAssoc) { + $key = rawurlencode($key); + $isNestedArray = is_array($var); + } else { + $isNestedArray = false; + } + + if (!$isNestedArray) { + $var = rawurlencode($var); + if ($parsed['operator'] === '+' || + $parsed['operator'] === '#' + ) { + $var = $this->decodeReserved($var); + } + } + + if ($value['modifier'] === '*') { + if ($isAssoc) { + if ($isNestedArray) { + // Nested arrays must allow for deeply nested + // structures. + $var = strtr( + http_build_query([$key => $var]), + $rfc1738to3986 + ); + } else { + $var = $key . '=' . $var; + } + } elseif ($key > 0 && $actuallyUseQuery) { + $var = $value['value'] . '=' . $var; + } + } + + $kvp[$key] = $var; + } + + if (empty($variable)) { + $actuallyUseQuery = false; + } elseif ($value['modifier'] === '*') { + $expanded = implode($joiner, $kvp); + if ($isAssoc) { + // Don't prepend the value name when using the explode + // modifier with an associative array. + $actuallyUseQuery = false; + } + } else { + if ($isAssoc) { + // When an associative array is encountered and the + // explode modifier is not set, then the result must be + // a comma separated list of keys followed by their + // respective values. + foreach ($kvp as $k => &$v) { + $v = $k . ',' . $v; + } + } + $expanded = implode(',', $kvp); + } + } else { + if ($value['modifier'] === ':') { + $variable = substr($variable, 0, $value['position']); + } + $expanded = rawurlencode($variable); + if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { + $expanded = $this->decodeReserved($expanded); + } + } + + if ($actuallyUseQuery) { + if (!$expanded && $joiner !== '&') { + $expanded = $value['value']; + } else { + $expanded = $value['value'] . '=' . $expanded; + } + } + + $replacements[] = $expanded; + } + + $ret = implode($joiner, $replacements); + if ($ret && $prefix) { + return $prefix . $ret; + } + + return $ret; + } + + /** + * Determines if an array is associative. + * + * This makes the assumption that input arrays are sequences or hashes. + * This assumption is a tradeoff for accuracy in favor of speed, but it + * should work in almost every case where input is supplied for a URI + * template. + * + * @param array $array Array to check + * + * @return bool + */ + private function isAssoc(array $array) + { + return $array && array_keys($array)[0] !== 0; + } + + /** + * Removes percent encoding on reserved characters (used with + and # + * modifiers). + * + * @param string $string String to fix + * + * @return string + */ + private function decodeReserved($string) + { + return str_replace(self::$delimsPct, self::$delims, $string); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Utils.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Utils.php new file mode 100644 index 0000000..c8fc1ae --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/Utils.php @@ -0,0 +1,67 @@ +getHost()) { + $idnaVariant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : 0; + $asciiHost = $idnaVariant === 0 + ? idn_to_ascii($uri->getHost(), $options) + : idn_to_ascii($uri->getHost(), $options, $idnaVariant, $info); + if ($asciiHost === false) { + $errorBitSet = isset($info['errors']) ? $info['errors'] : 0; + + $errorConstants = array_filter(array_keys(get_defined_constants()), function ($name) { + return substr($name, 0, 11) === 'IDNA_ERROR_'; + }); + + $errors = []; + foreach ($errorConstants as $errorConstant) { + if ($errorBitSet & constant($errorConstant)) { + $errors[] = $errorConstant; + } + } + + $errorMessage = 'IDN conversion failed'; + if ($errors) { + $errorMessage .= ' (errors: ' . implode(', ', $errors) . ')'; + } + + throw new InvalidArgumentException($errorMessage); + } else { + if ($uri->getHost() !== $asciiHost) { + // Replace URI only if the ASCII version is different + $uri = $uri->withHost($asciiHost); + } + } + } + + return $uri; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/functions.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/functions.php new file mode 100644 index 0000000..c2afd8c --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/functions.php @@ -0,0 +1,334 @@ +expand($template, $variables); +} + +/** + * Debug function used to describe the provided value type and class. + * + * @param mixed $input + * + * @return string Returns a string containing the type of the variable and + * if a class is provided, the class name. + */ +function describe_type($input) +{ + switch (gettype($input)) { + case 'object': + return 'object(' . get_class($input) . ')'; + case 'array': + return 'array(' . count($input) . ')'; + default: + ob_start(); + var_dump($input); + // normalize float vs double + return str_replace('double(', 'float(', rtrim(ob_get_clean())); + } +} + +/** + * Parses an array of header lines into an associative array of headers. + * + * @param iterable $lines Header lines array of strings in the following + * format: "Name: Value" + * @return array + */ +function headers_from_lines($lines) +{ + $headers = []; + + foreach ($lines as $line) { + $parts = explode(':', $line, 2); + $headers[trim($parts[0])][] = isset($parts[1]) + ? trim($parts[1]) + : null; + } + + return $headers; +} + +/** + * Returns a debug stream based on the provided variable. + * + * @param mixed $value Optional value + * + * @return resource + */ +function debug_resource($value = null) +{ + if (is_resource($value)) { + return $value; + } elseif (defined('STDOUT')) { + return STDOUT; + } + + return fopen('php://output', 'w'); +} + +/** + * Chooses and creates a default handler to use based on the environment. + * + * The returned handler is not wrapped by any default middlewares. + * + * @return callable Returns the best handler for the given system. + * @throws \RuntimeException if no viable Handler is available. + */ +function choose_handler() +{ + $handler = null; + if (function_exists('curl_multi_exec') && function_exists('curl_exec')) { + $handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler()); + } elseif (function_exists('curl_exec')) { + $handler = new CurlHandler(); + } elseif (function_exists('curl_multi_exec')) { + $handler = new CurlMultiHandler(); + } + + if (ini_get('allow_url_fopen')) { + $handler = $handler + ? Proxy::wrapStreaming($handler, new StreamHandler()) + : new StreamHandler(); + } elseif (!$handler) { + throw new \RuntimeException('GuzzleHttp requires cURL, the ' + . 'allow_url_fopen ini setting, or a custom HTTP handler.'); + } + + return $handler; +} + +/** + * Get the default User-Agent string to use with Guzzle + * + * @return string + */ +function default_user_agent() +{ + static $defaultAgent = ''; + + if (!$defaultAgent) { + $defaultAgent = 'GuzzleHttp/' . Client::VERSION; + if (extension_loaded('curl') && function_exists('curl_version')) { + $defaultAgent .= ' curl/' . \curl_version()['version']; + } + $defaultAgent .= ' PHP/' . PHP_VERSION; + } + + return $defaultAgent; +} + +/** + * Returns the default cacert bundle for the current system. + * + * First, the openssl.cafile and curl.cainfo php.ini settings are checked. + * If those settings are not configured, then the common locations for + * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X + * and Windows are checked. If any of these file locations are found on + * disk, they will be utilized. + * + * Note: the result of this function is cached for subsequent calls. + * + * @return string + * @throws \RuntimeException if no bundle can be found. + */ +function default_ca_bundle() +{ + static $cached = null; + static $cafiles = [ + // Red Hat, CentOS, Fedora (provided by the ca-certificates package) + '/etc/pki/tls/certs/ca-bundle.crt', + // Ubuntu, Debian (provided by the ca-certificates package) + '/etc/ssl/certs/ca-certificates.crt', + // FreeBSD (provided by the ca_root_nss package) + '/usr/local/share/certs/ca-root-nss.crt', + // SLES 12 (provided by the ca-certificates package) + '/var/lib/ca-certificates/ca-bundle.pem', + // OS X provided by homebrew (using the default path) + '/usr/local/etc/openssl/cert.pem', + // Google app engine + '/etc/ca-certificates.crt', + // Windows? + 'C:\\windows\\system32\\curl-ca-bundle.crt', + 'C:\\windows\\curl-ca-bundle.crt', + ]; + + if ($cached) { + return $cached; + } + + if ($ca = ini_get('openssl.cafile')) { + return $cached = $ca; + } + + if ($ca = ini_get('curl.cainfo')) { + return $cached = $ca; + } + + foreach ($cafiles as $filename) { + if (file_exists($filename)) { + return $cached = $filename; + } + } + + throw new \RuntimeException( + <<< EOT +No system CA bundle could be found in any of the the common system locations. +PHP versions earlier than 5.6 are not properly configured to use the system's +CA bundle by default. In order to verify peer certificates, you will need to +supply the path on disk to a certificate bundle to the 'verify' request +option: http://docs.guzzlephp.org/en/latest/clients.html#verify. If you do not +need a specific certificate bundle, then Mozilla provides a commonly used CA +bundle which can be downloaded here (provided by the maintainer of cURL): +https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt. Once +you have a CA bundle available on disk, you can set the 'openssl.cafile' PHP +ini setting to point to the path to the file, allowing you to omit the 'verify' +request option. See http://curl.haxx.se/docs/sslcerts.html for more +information. +EOT + ); +} + +/** + * Creates an associative array of lowercase header names to the actual + * header casing. + * + * @param array $headers + * + * @return array + */ +function normalize_header_keys(array $headers) +{ + $result = []; + foreach (array_keys($headers) as $key) { + $result[strtolower($key)] = $key; + } + + return $result; +} + +/** + * Returns true if the provided host matches any of the no proxy areas. + * + * This method will strip a port from the host if it is present. Each pattern + * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a + * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" == + * "baz.foo.com", but ".foo.com" != "foo.com"). + * + * Areas are matched in the following cases: + * 1. "*" (without quotes) always matches any hosts. + * 2. An exact match. + * 3. The area starts with "." and the area is the last part of the host. e.g. + * '.mit.edu' will match any host that ends with '.mit.edu'. + * + * @param string $host Host to check against the patterns. + * @param array $noProxyArray An array of host patterns. + * + * @return bool + */ +function is_host_in_noproxy($host, array $noProxyArray) +{ + if (strlen($host) === 0) { + throw new \InvalidArgumentException('Empty host provided'); + } + + // Strip port if present. + if (strpos($host, ':')) { + $host = explode($host, ':', 2)[0]; + } + + foreach ($noProxyArray as $area) { + // Always match on wildcards. + if ($area === '*') { + return true; + } elseif (empty($area)) { + // Don't match on empty values. + continue; + } elseif ($area === $host) { + // Exact matches. + return true; + } else { + // Special match if the area when prefixed with ".". Remove any + // existing leading "." and add a new leading ".". + $area = '.' . ltrim($area, '.'); + if (substr($host, -(strlen($area))) === $area) { + return true; + } + } + } + + return false; +} + +/** + * Wrapper for json_decode that throws when an error occurs. + * + * @param string $json JSON data to parse + * @param bool $assoc When true, returned objects will be converted + * into associative arrays. + * @param int $depth User specified recursion depth. + * @param int $options Bitmask of JSON decode options. + * + * @return mixed + * @throws Exception\InvalidArgumentException if the JSON cannot be decoded. + * @link http://www.php.net/manual/en/function.json-decode.php + */ +function json_decode($json, $assoc = false, $depth = 512, $options = 0) +{ + $data = \json_decode($json, $assoc, $depth, $options); + if (JSON_ERROR_NONE !== json_last_error()) { + throw new Exception\InvalidArgumentException( + 'json_decode error: ' . json_last_error_msg() + ); + } + + return $data; +} + +/** + * Wrapper for JSON encoding that throws when an error occurs. + * + * @param mixed $value The value being encoded + * @param int $options JSON encode option bitmask + * @param int $depth Set the maximum depth. Must be greater than zero. + * + * @return string + * @throws Exception\InvalidArgumentException if the JSON cannot be encoded. + * @link http://www.php.net/manual/en/function.json-encode.php + */ +function json_encode($value, $options = 0, $depth = 512) +{ + $json = \json_encode($value, $options, $depth); + if (JSON_ERROR_NONE !== json_last_error()) { + throw new Exception\InvalidArgumentException( + 'json_encode error: ' . json_last_error_msg() + ); + } + + return $json; +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/functions_include.php b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/functions_include.php new file mode 100644 index 0000000..a93393a --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/guzzle/src/functions_include.php @@ -0,0 +1,6 @@ + + +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/plugins/panakour/backup/vendor/guzzlehttp/promises/Makefile b/plugins/panakour/backup/vendor/guzzlehttp/promises/Makefile new file mode 100644 index 0000000..8d5b3ef --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/Makefile @@ -0,0 +1,13 @@ +all: clean test + +test: + vendor/bin/phpunit + +coverage: + vendor/bin/phpunit --coverage-html=artifacts/coverage + +view-coverage: + open artifacts/coverage/index.html + +clean: + rm -rf artifacts/* diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/README.md b/plugins/panakour/backup/vendor/guzzlehttp/promises/README.md new file mode 100644 index 0000000..7b607e2 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/README.md @@ -0,0 +1,504 @@ +# Guzzle Promises + +[Promises/A+](https://promisesaplus.com/) implementation that handles promise +chaining and resolution iteratively, allowing for "infinite" promise chaining +while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/) +for a general introduction to promises. + +- [Features](#features) +- [Quick start](#quick-start) +- [Synchronous wait](#synchronous-wait) +- [Cancellation](#cancellation) +- [API](#api) + - [Promise](#promise) + - [FulfilledPromise](#fulfilledpromise) + - [RejectedPromise](#rejectedpromise) +- [Promise interop](#promise-interop) +- [Implementation notes](#implementation-notes) + + +# Features + +- [Promises/A+](https://promisesaplus.com/) implementation. +- Promise resolution and chaining is handled iteratively, allowing for + "infinite" promise chaining. +- Promises have a synchronous `wait` method. +- Promises can be cancelled. +- Works with any object that has a `then` function. +- C# style async/await coroutine promises using + `GuzzleHttp\Promise\coroutine()`. + + +# Quick start + +A *promise* represents the eventual result of an asynchronous operation. The +primary way of interacting with a promise is through its `then` method, which +registers callbacks to receive either a promise's eventual value or the reason +why the promise cannot be fulfilled. + + +## Callbacks + +Callbacks are registered with the `then` method by providing an optional +`$onFulfilled` followed by an optional `$onRejected` function. + + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise->then( + // $onFulfilled + function ($value) { + echo 'The promise was fulfilled.'; + }, + // $onRejected + function ($reason) { + echo 'The promise was rejected.'; + } +); +``` + +*Resolving* a promise means that you either fulfill a promise with a *value* or +reject a promise with a *reason*. Resolving a promises triggers callbacks +registered with the promises's `then` method. These callbacks are triggered +only once and in the order in which they were added. + + +## Resolving a promise + +Promises are fulfilled using the `resolve($value)` method. Resolving a promise +with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger +all of the onFulfilled callbacks (resolving a promise with a rejected promise +will reject the promise and trigger the `$onRejected` callbacks). + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise + ->then(function ($value) { + // Return a value and don't break the chain + return "Hello, " . $value; + }) + // This then is executed after the first then and receives the value + // returned from the first then. + ->then(function ($value) { + echo $value; + }); + +// Resolving the promise triggers the $onFulfilled callbacks and outputs +// "Hello, reader". +$promise->resolve('reader.'); +``` + + +## Promise forwarding + +Promises can be chained one after the other. Each then in the chain is a new +promise. The return value of a promise is what's forwarded to the next +promise in the chain. Returning a promise in a `then` callback will cause the +subsequent promises in the chain to only be fulfilled when the returned promise +has been fulfilled. The next promise in the chain will be invoked with the +resolved value of the promise. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$nextPromise = new Promise(); + +$promise + ->then(function ($value) use ($nextPromise) { + echo $value; + return $nextPromise; + }) + ->then(function ($value) { + echo $value; + }); + +// Triggers the first callback and outputs "A" +$promise->resolve('A'); +// Triggers the second callback and outputs "B" +$nextPromise->resolve('B'); +``` + +## Promise rejection + +When a promise is rejected, the `$onRejected` callbacks are invoked with the +rejection reason. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise->then(null, function ($reason) { + echo $reason; +}); + +$promise->reject('Error!'); +// Outputs "Error!" +``` + +## Rejection forwarding + +If an exception is thrown in an `$onRejected` callback, subsequent +`$onRejected` callbacks are invoked with the thrown exception as the reason. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise(); +$promise->then(null, function ($reason) { + throw new \Exception($reason); +})->then(null, function ($reason) { + assert($reason->getMessage() === 'Error!'); +}); + +$promise->reject('Error!'); +``` + +You can also forward a rejection down the promise chain by returning a +`GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or +`$onRejected` callback. + +```php +use GuzzleHttp\Promise\Promise; +use GuzzleHttp\Promise\RejectedPromise; + +$promise = new Promise(); +$promise->then(null, function ($reason) { + return new RejectedPromise($reason); +})->then(null, function ($reason) { + assert($reason === 'Error!'); +}); + +$promise->reject('Error!'); +``` + +If an exception is not thrown in a `$onRejected` callback and the callback +does not return a rejected promise, downstream `$onFulfilled` callbacks are +invoked using the value returned from the `$onRejected` callback. + +```php +use GuzzleHttp\Promise\Promise; +use GuzzleHttp\Promise\RejectedPromise; + +$promise = new Promise(); +$promise + ->then(null, function ($reason) { + return "It's ok"; + }) + ->then(function ($value) { + assert($value === "It's ok"); + }); + +$promise->reject('Error!'); +``` + +# Synchronous wait + +You can synchronously force promises to complete using a promise's `wait` +method. When creating a promise, you can provide a wait function that is used +to synchronously force a promise to complete. When a wait function is invoked +it is expected to deliver a value to the promise or reject the promise. If the +wait function does not deliver a value, then an exception is thrown. The wait +function provided to a promise constructor is invoked when the `wait` function +of the promise is called. + +```php +$promise = new Promise(function () use (&$promise) { + $promise->resolve('foo'); +}); + +// Calling wait will return the value of the promise. +echo $promise->wait(); // outputs "foo" +``` + +If an exception is encountered while invoking the wait function of a promise, +the promise is rejected with the exception and the exception is thrown. + +```php +$promise = new Promise(function () use (&$promise) { + throw new \Exception('foo'); +}); + +$promise->wait(); // throws the exception. +``` + +Calling `wait` on a promise that has been fulfilled will not trigger the wait +function. It will simply return the previously resolved value. + +```php +$promise = new Promise(function () { die('this is not called!'); }); +$promise->resolve('foo'); +echo $promise->wait(); // outputs "foo" +``` + +Calling `wait` on a promise that has been rejected will throw an exception. If +the rejection reason is an instance of `\Exception` the reason is thrown. +Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason +can be obtained by calling the `getReason` method of the exception. + +```php +$promise = new Promise(); +$promise->reject('foo'); +$promise->wait(); +``` + +> PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo' + + +## Unwrapping a promise + +When synchronously waiting on a promise, you are joining the state of the +promise into the current state of execution (i.e., return the value of the +promise if it was fulfilled or throw an exception if it was rejected). This is +called "unwrapping" the promise. Waiting on a promise will by default unwrap +the promise state. + +You can force a promise to resolve and *not* unwrap the state of the promise +by passing `false` to the first argument of the `wait` function: + +```php +$promise = new Promise(); +$promise->reject('foo'); +// This will not throw an exception. It simply ensures the promise has +// been resolved. +$promise->wait(false); +``` + +When unwrapping a promise, the resolved value of the promise will be waited +upon until the unwrapped value is not a promise. This means that if you resolve +promise A with a promise B and unwrap promise A, the value returned by the +wait function will be the value delivered to promise B. + +**Note**: when you do not unwrap the promise, no value is returned. + + +# Cancellation + +You can cancel a promise that has not yet been fulfilled using the `cancel()` +method of a promise. When creating a promise you can provide an optional +cancel function that when invoked cancels the action of computing a resolution +of the promise. + + +# API + + +## Promise + +When creating a promise object, you can provide an optional `$waitFn` and +`$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is +expected to resolve the promise. `$cancelFn` is a function with no arguments +that is expected to cancel the computation of a promise. It is invoked when the +`cancel()` method of a promise is called. + +```php +use GuzzleHttp\Promise\Promise; + +$promise = new Promise( + function () use (&$promise) { + $promise->resolve('waited'); + }, + function () { + // do something that will cancel the promise computation (e.g., close + // a socket, cancel a database query, etc...) + } +); + +assert('waited' === $promise->wait()); +``` + +A promise has the following methods: + +- `then(callable $onFulfilled, callable $onRejected) : PromiseInterface` + + Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler. + +- `otherwise(callable $onRejected) : PromiseInterface` + + Appends a rejection handler callback to the promise, and returns a new promise resolving to the return value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled. + +- `wait($unwrap = true) : mixed` + + Synchronously waits on the promise to complete. + + `$unwrap` controls whether or not the value of the promise is returned for a + fulfilled promise or if an exception is thrown if the promise is rejected. + This is set to `true` by default. + +- `cancel()` + + Attempts to cancel the promise if possible. The promise being cancelled and + the parent most ancestor that has not yet been resolved will also be + cancelled. Any promises waiting on the cancelled promise to resolve will also + be cancelled. + +- `getState() : string` + + Returns the state of the promise. One of `pending`, `fulfilled`, or + `rejected`. + +- `resolve($value)` + + Fulfills the promise with the given `$value`. + +- `reject($reason)` + + Rejects the promise with the given `$reason`. + + +## FulfilledPromise + +A fulfilled promise can be created to represent a promise that has been +fulfilled. + +```php +use GuzzleHttp\Promise\FulfilledPromise; + +$promise = new FulfilledPromise('value'); + +// Fulfilled callbacks are immediately invoked. +$promise->then(function ($value) { + echo $value; +}); +``` + + +## RejectedPromise + +A rejected promise can be created to represent a promise that has been +rejected. + +```php +use GuzzleHttp\Promise\RejectedPromise; + +$promise = new RejectedPromise('Error'); + +// Rejected callbacks are immediately invoked. +$promise->then(null, function ($reason) { + echo $reason; +}); +``` + + +# Promise interop + +This library works with foreign promises that have a `then` method. This means +you can use Guzzle promises with [React promises](https://github.com/reactphp/promise) +for example. When a foreign promise is returned inside of a then method +callback, promise resolution will occur recursively. + +```php +// Create a React promise +$deferred = new React\Promise\Deferred(); +$reactPromise = $deferred->promise(); + +// Create a Guzzle promise that is fulfilled with a React promise. +$guzzlePromise = new \GuzzleHttp\Promise\Promise(); +$guzzlePromise->then(function ($value) use ($reactPromise) { + // Do something something with the value... + // Return the React promise + return $reactPromise; +}); +``` + +Please note that wait and cancel chaining is no longer possible when forwarding +a foreign promise. You will need to wrap a third-party promise with a Guzzle +promise in order to utilize wait and cancel functions with foreign promises. + + +## Event Loop Integration + +In order to keep the stack size constant, Guzzle promises are resolved +asynchronously using a task queue. When waiting on promises synchronously, the +task queue will be automatically run to ensure that the blocking promise and +any forwarded promises are resolved. When using promises asynchronously in an +event loop, you will need to run the task queue on each tick of the loop. If +you do not run the task queue, then promises will not be resolved. + +You can run the task queue using the `run()` method of the global task queue +instance. + +```php +// Get the global task queue +$queue = \GuzzleHttp\Promise\queue(); +$queue->run(); +``` + +For example, you could use Guzzle promises with React using a periodic timer: + +```php +$loop = React\EventLoop\Factory::create(); +$loop->addPeriodicTimer(0, [$queue, 'run']); +``` + +*TODO*: Perhaps adding a `futureTick()` on each tick would be faster? + + +# Implementation notes + + +## Promise resolution and chaining is handled iteratively + +By shuffling pending handlers from one owner to another, promises are +resolved iteratively, allowing for "infinite" then chaining. + +```php +then(function ($v) { + // The stack size remains constant (a good thing) + echo xdebug_get_stack_depth() . ', '; + return $v + 1; + }); +} + +$parent->resolve(0); +var_dump($p->wait()); // int(1000) + +``` + +When a promise is fulfilled or rejected with a non-promise value, the promise +then takes ownership of the handlers of each child promise and delivers values +down the chain without using recursion. + +When a promise is resolved with another promise, the original promise transfers +all of its pending handlers to the new promise. When the new promise is +eventually resolved, all of the pending handlers are delivered the forwarded +value. + + +## A promise is the deferred. + +Some promise libraries implement promises using a deferred object to represent +a computation and a promise object to represent the delivery of the result of +the computation. This is a nice separation of computation and delivery because +consumers of the promise cannot modify the value that will be eventually +delivered. + +One side effect of being able to implement promise resolution and chaining +iteratively is that you need to be able for one promise to reach into the state +of another promise to shuffle around ownership of handlers. In order to achieve +this without making the handlers of a promise publicly mutable, a promise is +also the deferred value, allowing promises of the same parent class to reach +into and modify the private properties of promises of the same type. While this +does allow consumers of the value to modify the resolution or rejection of the +deferred, it is a small price to pay for keeping the stack size constant. + +```php +$promise = new Promise(); +$promise->then(function ($value) { echo $value; }); +// The promise is the deferred value, so you can deliver a value to it. +$promise->resolve('foo'); +// prints "foo" +``` diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/composer.json b/plugins/panakour/backup/vendor/guzzlehttp/promises/composer.json new file mode 100644 index 0000000..ec41a61 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/composer.json @@ -0,0 +1,34 @@ +{ + "name": "guzzlehttp/promises", + "description": "Guzzle promises library", + "keywords": ["promise"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": ["src/functions_include.php"] + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-ci": "vendor/bin/phpunit --coverage-text" + }, + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/AggregateException.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/AggregateException.php new file mode 100644 index 0000000..6a5690c --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/AggregateException.php @@ -0,0 +1,16 @@ +then(function ($v) { echo $v; }); + * + * @param callable $generatorFn Generator function to wrap into a promise. + * + * @return Promise + * @link https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration + */ +final class Coroutine implements PromiseInterface +{ + /** + * @var PromiseInterface|null + */ + private $currentPromise; + + /** + * @var Generator + */ + private $generator; + + /** + * @var Promise + */ + private $result; + + public function __construct(callable $generatorFn) + { + $this->generator = $generatorFn(); + $this->result = new Promise(function () { + while (isset($this->currentPromise)) { + $this->currentPromise->wait(); + } + }); + $this->nextCoroutine($this->generator->current()); + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ) { + return $this->result->then($onFulfilled, $onRejected); + } + + public function otherwise(callable $onRejected) + { + return $this->result->otherwise($onRejected); + } + + public function wait($unwrap = true) + { + return $this->result->wait($unwrap); + } + + public function getState() + { + return $this->result->getState(); + } + + public function resolve($value) + { + $this->result->resolve($value); + } + + public function reject($reason) + { + $this->result->reject($reason); + } + + public function cancel() + { + $this->currentPromise->cancel(); + $this->result->cancel(); + } + + private function nextCoroutine($yielded) + { + $this->currentPromise = promise_for($yielded) + ->then([$this, '_handleSuccess'], [$this, '_handleFailure']); + } + + /** + * @internal + */ + public function _handleSuccess($value) + { + unset($this->currentPromise); + try { + $next = $this->generator->send($value); + if ($this->generator->valid()) { + $this->nextCoroutine($next); + } else { + $this->result->resolve($value); + } + } catch (Exception $exception) { + $this->result->reject($exception); + } catch (Throwable $throwable) { + $this->result->reject($throwable); + } + } + + /** + * @internal + */ + public function _handleFailure($reason) + { + unset($this->currentPromise); + try { + $nextYield = $this->generator->throw(exception_for($reason)); + // The throw was caught, so keep iterating on the coroutine + $this->nextCoroutine($nextYield); + } catch (Exception $exception) { + $this->result->reject($exception); + } catch (Throwable $throwable) { + $this->result->reject($throwable); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/EachPromise.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/EachPromise.php new file mode 100644 index 0000000..d0ddf60 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/EachPromise.php @@ -0,0 +1,229 @@ +iterable = iter_for($iterable); + + if (isset($config['concurrency'])) { + $this->concurrency = $config['concurrency']; + } + + if (isset($config['fulfilled'])) { + $this->onFulfilled = $config['fulfilled']; + } + + if (isset($config['rejected'])) { + $this->onRejected = $config['rejected']; + } + } + + public function promise() + { + if ($this->aggregate) { + return $this->aggregate; + } + + try { + $this->createPromise(); + $this->iterable->rewind(); + $this->refillPending(); + } catch (\Throwable $e) { + $this->aggregate->reject($e); + } catch (\Exception $e) { + $this->aggregate->reject($e); + } + + return $this->aggregate; + } + + private function createPromise() + { + $this->mutex = false; + $this->aggregate = new Promise(function () { + reset($this->pending); + if (empty($this->pending) && !$this->iterable->valid()) { + $this->aggregate->resolve(null); + return; + } + + // Consume a potentially fluctuating list of promises while + // ensuring that indexes are maintained (precluding array_shift). + while ($promise = current($this->pending)) { + next($this->pending); + $promise->wait(); + if ($this->aggregate->getState() !== PromiseInterface::PENDING) { + return; + } + } + }); + + // Clear the references when the promise is resolved. + $clearFn = function () { + $this->iterable = $this->concurrency = $this->pending = null; + $this->onFulfilled = $this->onRejected = null; + }; + + $this->aggregate->then($clearFn, $clearFn); + } + + private function refillPending() + { + if (!$this->concurrency) { + // Add all pending promises. + while ($this->addPending() && $this->advanceIterator()); + return; + } + + // Add only up to N pending promises. + $concurrency = is_callable($this->concurrency) + ? call_user_func($this->concurrency, count($this->pending)) + : $this->concurrency; + $concurrency = max($concurrency - count($this->pending), 0); + // Concurrency may be set to 0 to disallow new promises. + if (!$concurrency) { + return; + } + // Add the first pending promise. + $this->addPending(); + // Note this is special handling for concurrency=1 so that we do + // not advance the iterator after adding the first promise. This + // helps work around issues with generators that might not have the + // next value to yield until promise callbacks are called. + while (--$concurrency + && $this->advanceIterator() + && $this->addPending()); + } + + private function addPending() + { + if (!$this->iterable || !$this->iterable->valid()) { + return false; + } + + $promise = promise_for($this->iterable->current()); + $idx = $this->iterable->key(); + + $this->pending[$idx] = $promise->then( + function ($value) use ($idx) { + if ($this->onFulfilled) { + call_user_func( + $this->onFulfilled, $value, $idx, $this->aggregate + ); + } + $this->step($idx); + }, + function ($reason) use ($idx) { + if ($this->onRejected) { + call_user_func( + $this->onRejected, $reason, $idx, $this->aggregate + ); + } + $this->step($idx); + } + ); + + return true; + } + + private function advanceIterator() + { + // Place a lock on the iterator so that we ensure to not recurse, + // preventing fatal generator errors. + if ($this->mutex) { + return false; + } + + $this->mutex = true; + + try { + $this->iterable->next(); + $this->mutex = false; + return true; + } catch (\Throwable $e) { + $this->aggregate->reject($e); + $this->mutex = false; + return false; + } catch (\Exception $e) { + $this->aggregate->reject($e); + $this->mutex = false; + return false; + } + } + + private function step($idx) + { + // If the promise was already resolved, then ignore this step. + if ($this->aggregate->getState() !== PromiseInterface::PENDING) { + return; + } + + unset($this->pending[$idx]); + + // Only refill pending promises if we are not locked, preventing the + // EachPromise to recursively invoke the provided iterator, which + // cause a fatal error: "Cannot resume an already running generator" + if ($this->advanceIterator() && !$this->checkIfFinished()) { + // Add more pending promises if possible. + $this->refillPending(); + } + } + + private function checkIfFinished() + { + if (!$this->pending && !$this->iterable->valid()) { + // Resolve the promise if there's nothing left to do. + $this->aggregate->resolve(null); + return true; + } + + return false; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/FulfilledPromise.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/FulfilledPromise.php new file mode 100644 index 0000000..dbbeeb9 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/FulfilledPromise.php @@ -0,0 +1,82 @@ +value = $value; + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ) { + // Return itself if there is no onFulfilled function. + if (!$onFulfilled) { + return $this; + } + + $queue = queue(); + $p = new Promise([$queue, 'run']); + $value = $this->value; + $queue->add(static function () use ($p, $value, $onFulfilled) { + if ($p->getState() === self::PENDING) { + try { + $p->resolve($onFulfilled($value)); + } catch (\Throwable $e) { + $p->reject($e); + } catch (\Exception $e) { + $p->reject($e); + } + } + }); + + return $p; + } + + public function otherwise(callable $onRejected) + { + return $this->then(null, $onRejected); + } + + public function wait($unwrap = true, $defaultDelivery = null) + { + return $unwrap ? $this->value : null; + } + + public function getState() + { + return self::FULFILLED; + } + + public function resolve($value) + { + if ($value !== $this->value) { + throw new \LogicException("Cannot resolve a fulfilled promise"); + } + } + + public function reject($reason) + { + throw new \LogicException("Cannot reject a fulfilled promise"); + } + + public function cancel() + { + // pass + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/Promise.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/Promise.php new file mode 100644 index 0000000..844ada0 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/Promise.php @@ -0,0 +1,280 @@ +waitFn = $waitFn; + $this->cancelFn = $cancelFn; + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ) { + if ($this->state === self::PENDING) { + $p = new Promise(null, [$this, 'cancel']); + $this->handlers[] = [$p, $onFulfilled, $onRejected]; + $p->waitList = $this->waitList; + $p->waitList[] = $this; + return $p; + } + + // Return a fulfilled promise and immediately invoke any callbacks. + if ($this->state === self::FULFILLED) { + return $onFulfilled + ? promise_for($this->result)->then($onFulfilled) + : promise_for($this->result); + } + + // It's either cancelled or rejected, so return a rejected promise + // and immediately invoke any callbacks. + $rejection = rejection_for($this->result); + return $onRejected ? $rejection->then(null, $onRejected) : $rejection; + } + + public function otherwise(callable $onRejected) + { + return $this->then(null, $onRejected); + } + + public function wait($unwrap = true) + { + $this->waitIfPending(); + + $inner = $this->result instanceof PromiseInterface + ? $this->result->wait($unwrap) + : $this->result; + + if ($unwrap) { + if ($this->result instanceof PromiseInterface + || $this->state === self::FULFILLED + ) { + return $inner; + } else { + // It's rejected so "unwrap" and throw an exception. + throw exception_for($inner); + } + } + } + + public function getState() + { + return $this->state; + } + + public function cancel() + { + if ($this->state !== self::PENDING) { + return; + } + + $this->waitFn = $this->waitList = null; + + if ($this->cancelFn) { + $fn = $this->cancelFn; + $this->cancelFn = null; + try { + $fn(); + } catch (\Throwable $e) { + $this->reject($e); + } catch (\Exception $e) { + $this->reject($e); + } + } + + // Reject the promise only if it wasn't rejected in a then callback. + if ($this->state === self::PENDING) { + $this->reject(new CancellationException('Promise has been cancelled')); + } + } + + public function resolve($value) + { + $this->settle(self::FULFILLED, $value); + } + + public function reject($reason) + { + $this->settle(self::REJECTED, $reason); + } + + private function settle($state, $value) + { + if ($this->state !== self::PENDING) { + // Ignore calls with the same resolution. + if ($state === $this->state && $value === $this->result) { + return; + } + throw $this->state === $state + ? new \LogicException("The promise is already {$state}.") + : new \LogicException("Cannot change a {$this->state} promise to {$state}"); + } + + if ($value === $this) { + throw new \LogicException('Cannot fulfill or reject a promise with itself'); + } + + // Clear out the state of the promise but stash the handlers. + $this->state = $state; + $this->result = $value; + $handlers = $this->handlers; + $this->handlers = null; + $this->waitList = $this->waitFn = null; + $this->cancelFn = null; + + if (!$handlers) { + return; + } + + // If the value was not a settled promise or a thenable, then resolve + // it in the task queue using the correct ID. + if (!method_exists($value, 'then')) { + $id = $state === self::FULFILLED ? 1 : 2; + // It's a success, so resolve the handlers in the queue. + queue()->add(static function () use ($id, $value, $handlers) { + foreach ($handlers as $handler) { + self::callHandler($id, $value, $handler); + } + }); + } elseif ($value instanceof Promise + && $value->getState() === self::PENDING + ) { + // We can just merge our handlers onto the next promise. + $value->handlers = array_merge($value->handlers, $handlers); + } else { + // Resolve the handlers when the forwarded promise is resolved. + $value->then( + static function ($value) use ($handlers) { + foreach ($handlers as $handler) { + self::callHandler(1, $value, $handler); + } + }, + static function ($reason) use ($handlers) { + foreach ($handlers as $handler) { + self::callHandler(2, $reason, $handler); + } + } + ); + } + } + + /** + * Call a stack of handlers using a specific callback index and value. + * + * @param int $index 1 (resolve) or 2 (reject). + * @param mixed $value Value to pass to the callback. + * @param array $handler Array of handler data (promise and callbacks). + * + * @return array Returns the next group to resolve. + */ + private static function callHandler($index, $value, array $handler) + { + /** @var PromiseInterface $promise */ + $promise = $handler[0]; + + // The promise may have been cancelled or resolved before placing + // this thunk in the queue. + if ($promise->getState() !== self::PENDING) { + return; + } + + try { + if (isset($handler[$index])) { + $promise->resolve($handler[$index]($value)); + } elseif ($index === 1) { + // Forward resolution values as-is. + $promise->resolve($value); + } else { + // Forward rejections down the chain. + $promise->reject($value); + } + } catch (\Throwable $reason) { + $promise->reject($reason); + } catch (\Exception $reason) { + $promise->reject($reason); + } + } + + private function waitIfPending() + { + if ($this->state !== self::PENDING) { + return; + } elseif ($this->waitFn) { + $this->invokeWaitFn(); + } elseif ($this->waitList) { + $this->invokeWaitList(); + } else { + // If there's not wait function, then reject the promise. + $this->reject('Cannot wait on a promise that has ' + . 'no internal wait function. You must provide a wait ' + . 'function when constructing the promise to be able to ' + . 'wait on a promise.'); + } + + queue()->run(); + + if ($this->state === self::PENDING) { + $this->reject('Invoking the wait callback did not resolve the promise'); + } + } + + private function invokeWaitFn() + { + try { + $wfn = $this->waitFn; + $this->waitFn = null; + $wfn(true); + } catch (\Exception $reason) { + if ($this->state === self::PENDING) { + // The promise has not been resolved yet, so reject the promise + // with the exception. + $this->reject($reason); + } else { + // The promise was already resolved, so there's a problem in + // the application. + throw $reason; + } + } + } + + private function invokeWaitList() + { + $waitList = $this->waitList; + $this->waitList = null; + + foreach ($waitList as $result) { + while (true) { + $result->waitIfPending(); + + if ($result->result instanceof Promise) { + $result = $result->result; + } else { + if ($result->result instanceof PromiseInterface) { + $result->result->wait(false); + } + break; + } + } + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/PromiseInterface.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/PromiseInterface.php new file mode 100644 index 0000000..8f5f4b9 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/PromiseInterface.php @@ -0,0 +1,93 @@ +reason = $reason; + } + + public function then( + callable $onFulfilled = null, + callable $onRejected = null + ) { + // If there's no onRejected callback then just return self. + if (!$onRejected) { + return $this; + } + + $queue = queue(); + $reason = $this->reason; + $p = new Promise([$queue, 'run']); + $queue->add(static function () use ($p, $reason, $onRejected) { + if ($p->getState() === self::PENDING) { + try { + // Return a resolved promise if onRejected does not throw. + $p->resolve($onRejected($reason)); + } catch (\Throwable $e) { + // onRejected threw, so return a rejected promise. + $p->reject($e); + } catch (\Exception $e) { + // onRejected threw, so return a rejected promise. + $p->reject($e); + } + } + }); + + return $p; + } + + public function otherwise(callable $onRejected) + { + return $this->then(null, $onRejected); + } + + public function wait($unwrap = true, $defaultDelivery = null) + { + if ($unwrap) { + throw exception_for($this->reason); + } + } + + public function getState() + { + return self::REJECTED; + } + + public function resolve($value) + { + throw new \LogicException("Cannot resolve a rejected promise"); + } + + public function reject($reason) + { + if ($reason !== $this->reason) { + throw new \LogicException("Cannot reject a rejected promise"); + } + } + + public function cancel() + { + // pass + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/RejectionException.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/RejectionException.php new file mode 100644 index 0000000..07c1136 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/RejectionException.php @@ -0,0 +1,47 @@ +reason = $reason; + + $message = 'The promise was rejected'; + + if ($description) { + $message .= ' with reason: ' . $description; + } elseif (is_string($reason) + || (is_object($reason) && method_exists($reason, '__toString')) + ) { + $message .= ' with reason: ' . $this->reason; + } elseif ($reason instanceof \JsonSerializable) { + $message .= ' with reason: ' + . json_encode($this->reason, JSON_PRETTY_PRINT); + } + + parent::__construct($message); + } + + /** + * Returns the rejection reason. + * + * @return mixed + */ + public function getReason() + { + return $this->reason; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/TaskQueue.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/TaskQueue.php new file mode 100644 index 0000000..6e8a2a0 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/TaskQueue.php @@ -0,0 +1,66 @@ +run(); + */ +class TaskQueue implements TaskQueueInterface +{ + private $enableShutdown = true; + private $queue = []; + + public function __construct($withShutdown = true) + { + if ($withShutdown) { + register_shutdown_function(function () { + if ($this->enableShutdown) { + // Only run the tasks if an E_ERROR didn't occur. + $err = error_get_last(); + if (!$err || ($err['type'] ^ E_ERROR)) { + $this->run(); + } + } + }); + } + } + + public function isEmpty() + { + return !$this->queue; + } + + public function add(callable $task) + { + $this->queue[] = $task; + } + + public function run() + { + /** @var callable $task */ + while ($task = array_shift($this->queue)) { + $task(); + } + } + + /** + * The task queue will be run and exhausted by default when the process + * exits IFF the exit is not the result of a PHP E_ERROR error. + * + * You can disable running the automatic shutdown of the queue by calling + * this function. If you disable the task queue shutdown process, then you + * MUST either run the task queue (as a result of running your event loop + * or manually using the run() method) or wait on each outstanding promise. + * + * Note: This shutdown will occur before any destructors are triggered. + */ + public function disableShutdown() + { + $this->enableShutdown = false; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/TaskQueueInterface.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/TaskQueueInterface.php new file mode 100644 index 0000000..ac8306e --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/TaskQueueInterface.php @@ -0,0 +1,25 @@ + + * while ($eventLoop->isRunning()) { + * GuzzleHttp\Promise\queue()->run(); + * } + * + * + * @param TaskQueueInterface $assign Optionally specify a new queue instance. + * + * @return TaskQueueInterface + */ +function queue(TaskQueueInterface $assign = null) +{ + static $queue; + + if ($assign) { + $queue = $assign; + } elseif (!$queue) { + $queue = new TaskQueue(); + } + + return $queue; +} + +/** + * Adds a function to run in the task queue when it is next `run()` and returns + * a promise that is fulfilled or rejected with the result. + * + * @param callable $task Task function to run. + * + * @return PromiseInterface + */ +function task(callable $task) +{ + $queue = queue(); + $promise = new Promise([$queue, 'run']); + $queue->add(function () use ($task, $promise) { + try { + $promise->resolve($task()); + } catch (\Throwable $e) { + $promise->reject($e); + } catch (\Exception $e) { + $promise->reject($e); + } + }); + + return $promise; +} + +/** + * Creates a promise for a value if the value is not a promise. + * + * @param mixed $value Promise or value. + * + * @return PromiseInterface + */ +function promise_for($value) +{ + if ($value instanceof PromiseInterface) { + return $value; + } + + // Return a Guzzle promise that shadows the given promise. + if (method_exists($value, 'then')) { + $wfn = method_exists($value, 'wait') ? [$value, 'wait'] : null; + $cfn = method_exists($value, 'cancel') ? [$value, 'cancel'] : null; + $promise = new Promise($wfn, $cfn); + $value->then([$promise, 'resolve'], [$promise, 'reject']); + return $promise; + } + + return new FulfilledPromise($value); +} + +/** + * Creates a rejected promise for a reason if the reason is not a promise. If + * the provided reason is a promise, then it is returned as-is. + * + * @param mixed $reason Promise or reason. + * + * @return PromiseInterface + */ +function rejection_for($reason) +{ + if ($reason instanceof PromiseInterface) { + return $reason; + } + + return new RejectedPromise($reason); +} + +/** + * Create an exception for a rejected promise value. + * + * @param mixed $reason + * + * @return \Exception|\Throwable + */ +function exception_for($reason) +{ + return $reason instanceof \Exception || $reason instanceof \Throwable + ? $reason + : new RejectionException($reason); +} + +/** + * Returns an iterator for the given value. + * + * @param mixed $value + * + * @return \Iterator + */ +function iter_for($value) +{ + if ($value instanceof \Iterator) { + return $value; + } elseif (is_array($value)) { + return new \ArrayIterator($value); + } else { + return new \ArrayIterator([$value]); + } +} + +/** + * Synchronously waits on a promise to resolve and returns an inspection state + * array. + * + * Returns a state associative array containing a "state" key mapping to a + * valid promise state. If the state of the promise is "fulfilled", the array + * will contain a "value" key mapping to the fulfilled value of the promise. If + * the promise is rejected, the array will contain a "reason" key mapping to + * the rejection reason of the promise. + * + * @param PromiseInterface $promise Promise or value. + * + * @return array + */ +function inspect(PromiseInterface $promise) +{ + try { + return [ + 'state' => PromiseInterface::FULFILLED, + 'value' => $promise->wait() + ]; + } catch (RejectionException $e) { + return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()]; + } catch (\Throwable $e) { + return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; + } catch (\Exception $e) { + return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; + } +} + +/** + * Waits on all of the provided promises, but does not unwrap rejected promises + * as thrown exception. + * + * Returns an array of inspection state arrays. + * + * @param PromiseInterface[] $promises Traversable of promises to wait upon. + * + * @return array + * @see GuzzleHttp\Promise\inspect for the inspection state array format. + */ +function inspect_all($promises) +{ + $results = []; + foreach ($promises as $key => $promise) { + $results[$key] = inspect($promise); + } + + return $results; +} + +/** + * Waits on all of the provided promises and returns the fulfilled values. + * + * Returns an array that contains the value of each promise (in the same order + * the promises were provided). An exception is thrown if any of the promises + * are rejected. + * + * @param mixed $promises Iterable of PromiseInterface objects to wait on. + * + * @return array + * @throws \Exception on error + * @throws \Throwable on error in PHP >=7 + */ +function unwrap($promises) +{ + $results = []; + foreach ($promises as $key => $promise) { + $results[$key] = $promise->wait(); + } + + return $results; +} + +/** + * Given an array of promises, return a promise that is fulfilled when all the + * items in the array are fulfilled. + * + * The promise's fulfillment value is an array with fulfillment values at + * respective positions to the original array. If any promise in the array + * rejects, the returned promise is rejected with the rejection reason. + * + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + */ +function all($promises) +{ + $results = []; + return each( + $promises, + function ($value, $idx) use (&$results) { + $results[$idx] = $value; + }, + function ($reason, $idx, Promise $aggregate) { + $aggregate->reject($reason); + } + )->then(function () use (&$results) { + ksort($results); + return $results; + }); +} + +/** + * Initiate a competitive race between multiple promises or values (values will + * become immediately fulfilled promises). + * + * When count amount of promises have been fulfilled, the returned promise is + * fulfilled with an array that contains the fulfillment values of the winners + * in order of resolution. + * + * This prommise is rejected with a {@see GuzzleHttp\Promise\AggregateException} + * if the number of fulfilled promises is less than the desired $count. + * + * @param int $count Total number of promises. + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + */ +function some($count, $promises) +{ + $results = []; + $rejections = []; + + return each( + $promises, + function ($value, $idx, PromiseInterface $p) use (&$results, $count) { + if ($p->getState() !== PromiseInterface::PENDING) { + return; + } + $results[$idx] = $value; + if (count($results) >= $count) { + $p->resolve(null); + } + }, + function ($reason) use (&$rejections) { + $rejections[] = $reason; + } + )->then( + function () use (&$results, &$rejections, $count) { + if (count($results) !== $count) { + throw new AggregateException( + 'Not enough promises to fulfill count', + $rejections + ); + } + ksort($results); + return array_values($results); + } + ); +} + +/** + * Like some(), with 1 as count. However, if the promise fulfills, the + * fulfillment value is not an array of 1 but the value directly. + * + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + */ +function any($promises) +{ + return some(1, $promises)->then(function ($values) { return $values[0]; }); +} + +/** + * Returns a promise that is fulfilled when all of the provided promises have + * been fulfilled or rejected. + * + * The returned promise is fulfilled with an array of inspection state arrays. + * + * @param mixed $promises Promises or values. + * + * @return PromiseInterface + * @see GuzzleHttp\Promise\inspect for the inspection state array format. + */ +function settle($promises) +{ + $results = []; + + return each( + $promises, + function ($value, $idx) use (&$results) { + $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; + }, + function ($reason, $idx) use (&$results) { + $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; + } + )->then(function () use (&$results) { + ksort($results); + return $results; + }); +} + +/** + * Given an iterator that yields promises or values, returns a promise that is + * fulfilled with a null value when the iterator has been consumed or the + * aggregate promise has been fulfilled or rejected. + * + * $onFulfilled is a function that accepts the fulfilled value, iterator + * index, and the aggregate promise. The callback can invoke any necessary side + * effects and choose to resolve or reject the aggregate promise if needed. + * + * $onRejected is a function that accepts the rejection reason, iterator + * index, and the aggregate promise. The callback can invoke any necessary side + * effects and choose to resolve or reject the aggregate promise if needed. + * + * @param mixed $iterable Iterator or array to iterate over. + * @param callable $onFulfilled + * @param callable $onRejected + * + * @return PromiseInterface + */ +function each( + $iterable, + callable $onFulfilled = null, + callable $onRejected = null +) { + return (new EachPromise($iterable, [ + 'fulfilled' => $onFulfilled, + 'rejected' => $onRejected + ]))->promise(); +} + +/** + * Like each, but only allows a certain number of outstanding promises at any + * given time. + * + * $concurrency may be an integer or a function that accepts the number of + * pending promises and returns a numeric concurrency limit value to allow for + * dynamic a concurrency size. + * + * @param mixed $iterable + * @param int|callable $concurrency + * @param callable $onFulfilled + * @param callable $onRejected + * + * @return PromiseInterface + */ +function each_limit( + $iterable, + $concurrency, + callable $onFulfilled = null, + callable $onRejected = null +) { + return (new EachPromise($iterable, [ + 'fulfilled' => $onFulfilled, + 'rejected' => $onRejected, + 'concurrency' => $concurrency + ]))->promise(); +} + +/** + * Like each_limit, but ensures that no promise in the given $iterable argument + * is rejected. If any promise is rejected, then the aggregate promise is + * rejected with the encountered rejection. + * + * @param mixed $iterable + * @param int|callable $concurrency + * @param callable $onFulfilled + * + * @return PromiseInterface + */ +function each_limit_all( + $iterable, + $concurrency, + callable $onFulfilled = null +) { + return each_limit( + $iterable, + $concurrency, + $onFulfilled, + function ($reason, $idx, PromiseInterface $aggregate) { + $aggregate->reject($reason); + } + ); +} + +/** + * Returns true if a promise is fulfilled. + * + * @param PromiseInterface $promise + * + * @return bool + */ +function is_fulfilled(PromiseInterface $promise) +{ + return $promise->getState() === PromiseInterface::FULFILLED; +} + +/** + * Returns true if a promise is rejected. + * + * @param PromiseInterface $promise + * + * @return bool + */ +function is_rejected(PromiseInterface $promise) +{ + return $promise->getState() === PromiseInterface::REJECTED; +} + +/** + * Returns true if a promise is fulfilled or rejected. + * + * @param PromiseInterface $promise + * + * @return bool + */ +function is_settled(PromiseInterface $promise) +{ + return $promise->getState() !== PromiseInterface::PENDING; +} + +/** + * @see Coroutine + * + * @param callable $generatorFn + * + * @return PromiseInterface + */ +function coroutine(callable $generatorFn) +{ + return new Coroutine($generatorFn); +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/promises/src/functions_include.php b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/functions_include.php new file mode 100644 index 0000000..34cd171 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/promises/src/functions_include.php @@ -0,0 +1,6 @@ +withPath('foo')->withHost('example.com')` will throw an exception + because the path of a URI with an authority must start with a slash "/" or be empty + - `(new Uri())->withScheme('http')` will return `'http://localhost'` + +### Deprecated + +- `Uri::resolve` in favor of `UriResolver::resolve` +- `Uri::removeDotSegments` in favor of `UriResolver::removeDotSegments` + +### Fixed + +- `Stream::read` when length parameter <= 0. +- `copy_to_stream` reads bytes in chunks instead of `maxLen` into memory. +- `ServerRequest::getUriFromGlobals` when `Host` header contains port. +- Compatibility of URIs with `file` scheme and empty host. + + +## [1.3.1] - 2016-06-25 + +### Fixed + +- `Uri::__toString` for network path references, e.g. `//example.org`. +- Missing lowercase normalization for host. +- Handling of URI components in case they are `'0'` in a lot of places, + e.g. as a user info password. +- `Uri::withAddedHeader` to correctly merge headers with different case. +- Trimming of header values in `Uri::withAddedHeader`. Header values may + be surrounded by whitespace which should be ignored according to RFC 7230 + Section 3.2.4. This does not apply to header names. +- `Uri::withAddedHeader` with an array of header values. +- `Uri::resolve` when base path has no slash and handling of fragment. +- Handling of encoding in `Uri::with(out)QueryValue` so one can pass the + key/value both in encoded as well as decoded form to those methods. This is + consistent with withPath, withQuery etc. +- `ServerRequest::withoutAttribute` when attribute value is null. + + +## [1.3.0] - 2016-04-13 + +### Added + +- Remaining interfaces needed for full PSR7 compatibility + (ServerRequestInterface, UploadedFileInterface, etc.). +- Support for stream_for from scalars. + +### Changed + +- Can now extend Uri. + +### Fixed +- A bug in validating request methods by making it more permissive. + + +## [1.2.3] - 2016-02-18 + +### Fixed + +- Support in `GuzzleHttp\Psr7\CachingStream` for seeking forward on remote + streams, which can sometimes return fewer bytes than requested with `fread`. +- Handling of gzipped responses with FNAME headers. + + +## [1.2.2] - 2016-01-22 + +### Added + +- Support for URIs without any authority. +- Support for HTTP 451 'Unavailable For Legal Reasons.' +- Support for using '0' as a filename. +- Support for including non-standard ports in Host headers. + + +## [1.2.1] - 2015-11-02 + +### Changes + +- Now supporting negative offsets when seeking to SEEK_END. + + +## [1.2.0] - 2015-08-15 + +### Changed + +- Body as `"0"` is now properly added to a response. +- Now allowing forward seeking in CachingStream. +- Now properly parsing HTTP requests that contain proxy targets in + `parse_request`. +- functions.php is now conditionally required. +- user-info is no longer dropped when resolving URIs. + + +## [1.1.0] - 2015-06-24 + +### Changed + +- URIs can now be relative. +- `multipart/form-data` headers are now overridden case-insensitively. +- URI paths no longer encode the following characters because they are allowed + in URIs: "(", ")", "*", "!", "'" +- A port is no longer added to a URI when the scheme is missing and no port is + present. + + +## 1.0.0 - 2015-05-19 + +Initial release. + +Currently unsupported: + +- `Psr\Http\Message\ServerRequestInterface` +- `Psr\Http\Message\UploadedFileInterface` + + + +[Unreleased]: https://github.com/guzzle/psr7/compare/1.6.0...HEAD +[1.6.0]: https://github.com/guzzle/psr7/compare/1.5.2...1.6.0 +[1.5.2]: https://github.com/guzzle/psr7/compare/1.5.1...1.5.2 +[1.5.1]: https://github.com/guzzle/psr7/compare/1.5.0...1.5.1 +[1.5.0]: https://github.com/guzzle/psr7/compare/1.4.2...1.5.0 +[1.4.2]: https://github.com/guzzle/psr7/compare/1.4.1...1.4.2 +[1.4.1]: https://github.com/guzzle/psr7/compare/1.4.0...1.4.1 +[1.4.0]: https://github.com/guzzle/psr7/compare/1.3.1...1.4.0 +[1.3.1]: https://github.com/guzzle/psr7/compare/1.3.0...1.3.1 +[1.3.0]: https://github.com/guzzle/psr7/compare/1.2.3...1.3.0 +[1.2.3]: https://github.com/guzzle/psr7/compare/1.2.2...1.2.3 +[1.2.2]: https://github.com/guzzle/psr7/compare/1.2.1...1.2.2 +[1.2.1]: https://github.com/guzzle/psr7/compare/1.2.0...1.2.1 +[1.2.0]: https://github.com/guzzle/psr7/compare/1.1.0...1.2.0 +[1.1.0]: https://github.com/guzzle/psr7/compare/1.0.0...1.1.0 diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/LICENSE b/plugins/panakour/backup/vendor/guzzlehttp/psr7/LICENSE new file mode 100644 index 0000000..581d95f --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Michael Dowling, https://github.com/mtdowling + +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/plugins/panakour/backup/vendor/guzzlehttp/psr7/README.md b/plugins/panakour/backup/vendor/guzzlehttp/psr7/README.md new file mode 100644 index 0000000..c60a6a3 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/README.md @@ -0,0 +1,745 @@ +# PSR-7 Message Implementation + +This repository contains a full [PSR-7](http://www.php-fig.org/psr/psr-7/) +message implementation, several stream decorators, and some helpful +functionality like query string parsing. + + +[![Build Status](https://travis-ci.org/guzzle/psr7.svg?branch=master)](https://travis-ci.org/guzzle/psr7) + + +# Stream implementation + +This package comes with a number of stream implementations and stream +decorators. + + +## AppendStream + +`GuzzleHttp\Psr7\AppendStream` + +Reads from multiple streams, one after the other. + +```php +use GuzzleHttp\Psr7; + +$a = Psr7\stream_for('abc, '); +$b = Psr7\stream_for('123.'); +$composed = new Psr7\AppendStream([$a, $b]); + +$composed->addStream(Psr7\stream_for(' Above all listen to me')); + +echo $composed; // abc, 123. Above all listen to me. +``` + + +## BufferStream + +`GuzzleHttp\Psr7\BufferStream` + +Provides a buffer stream that can be written to fill a buffer, and read +from to remove bytes from the buffer. + +This stream returns a "hwm" metadata value that tells upstream consumers +what the configured high water mark of the stream is, or the maximum +preferred size of the buffer. + +```php +use GuzzleHttp\Psr7; + +// When more than 1024 bytes are in the buffer, it will begin returning +// false to writes. This is an indication that writers should slow down. +$buffer = new Psr7\BufferStream(1024); +``` + + +## CachingStream + +The CachingStream is used to allow seeking over previously read bytes on +non-seekable streams. This can be useful when transferring a non-seekable +entity body fails due to needing to rewind the stream (for example, resulting +from a redirect). Data that is read from the remote stream will be buffered in +a PHP temp stream so that previously read bytes are cached first in memory, +then on disk. + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\stream_for(fopen('http://www.google.com', 'r')); +$stream = new Psr7\CachingStream($original); + +$stream->read(1024); +echo $stream->tell(); +// 1024 + +$stream->seek(0); +echo $stream->tell(); +// 0 +``` + + +## DroppingStream + +`GuzzleHttp\Psr7\DroppingStream` + +Stream decorator that begins dropping data once the size of the underlying +stream becomes too full. + +```php +use GuzzleHttp\Psr7; + +// Create an empty stream +$stream = Psr7\stream_for(); + +// Start dropping data when the stream has more than 10 bytes +$dropping = new Psr7\DroppingStream($stream, 10); + +$dropping->write('01234567890123456789'); +echo $stream; // 0123456789 +``` + + +## FnStream + +`GuzzleHttp\Psr7\FnStream` + +Compose stream implementations based on a hash of functions. + +Allows for easy testing and extension of a provided stream without needing +to create a concrete class for a simple extension point. + +```php + +use GuzzleHttp\Psr7; + +$stream = Psr7\stream_for('hi'); +$fnStream = Psr7\FnStream::decorate($stream, [ + 'rewind' => function () use ($stream) { + echo 'About to rewind - '; + $stream->rewind(); + echo 'rewound!'; + } +]); + +$fnStream->rewind(); +// Outputs: About to rewind - rewound! +``` + + +## InflateStream + +`GuzzleHttp\Psr7\InflateStream` + +Uses PHP's zlib.inflate filter to inflate deflate or gzipped content. + +This stream decorator skips the first 10 bytes of the given stream to remove +the gzip header, converts the provided stream to a PHP stream resource, +then appends the zlib.inflate filter. The stream is then converted back +to a Guzzle stream resource to be used as a Guzzle stream. + + +## LazyOpenStream + +`GuzzleHttp\Psr7\LazyOpenStream` + +Lazily reads or writes to a file that is opened only after an IO operation +take place on the stream. + +```php +use GuzzleHttp\Psr7; + +$stream = new Psr7\LazyOpenStream('/path/to/file', 'r'); +// The file has not yet been opened... + +echo $stream->read(10); +// The file is opened and read from only when needed. +``` + + +## LimitStream + +`GuzzleHttp\Psr7\LimitStream` + +LimitStream can be used to read a subset or slice of an existing stream object. +This can be useful for breaking a large file into smaller pieces to be sent in +chunks (e.g. Amazon S3's multipart upload API). + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\stream_for(fopen('/tmp/test.txt', 'r+')); +echo $original->getSize(); +// >>> 1048576 + +// Limit the size of the body to 1024 bytes and start reading from byte 2048 +$stream = new Psr7\LimitStream($original, 1024, 2048); +echo $stream->getSize(); +// >>> 1024 +echo $stream->tell(); +// >>> 0 +``` + + +## MultipartStream + +`GuzzleHttp\Psr7\MultipartStream` + +Stream that when read returns bytes for a streaming multipart or +multipart/form-data stream. + + +## NoSeekStream + +`GuzzleHttp\Psr7\NoSeekStream` + +NoSeekStream wraps a stream and does not allow seeking. + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\stream_for('foo'); +$noSeek = new Psr7\NoSeekStream($original); + +echo $noSeek->read(3); +// foo +var_export($noSeek->isSeekable()); +// false +$noSeek->seek(0); +var_export($noSeek->read(3)); +// NULL +``` + + +## PumpStream + +`GuzzleHttp\Psr7\PumpStream` + +Provides a read only stream that pumps data from a PHP callable. + +When invoking the provided callable, the PumpStream will pass the amount of +data requested to read to the callable. The callable can choose to ignore +this value and return fewer or more bytes than requested. Any extra data +returned by the provided callable is buffered internally until drained using +the read() function of the PumpStream. The provided callable MUST return +false when there is no more data to read. + + +## Implementing stream decorators + +Creating a stream decorator is very easy thanks to the +`GuzzleHttp\Psr7\StreamDecoratorTrait`. This trait provides methods that +implement `Psr\Http\Message\StreamInterface` by proxying to an underlying +stream. Just `use` the `StreamDecoratorTrait` and implement your custom +methods. + +For example, let's say we wanted to call a specific function each time the last +byte is read from a stream. This could be implemented by overriding the +`read()` method. + +```php +use Psr\Http\Message\StreamInterface; +use GuzzleHttp\Psr7\StreamDecoratorTrait; + +class EofCallbackStream implements StreamInterface +{ + use StreamDecoratorTrait; + + private $callback; + + public function __construct(StreamInterface $stream, callable $cb) + { + $this->stream = $stream; + $this->callback = $cb; + } + + public function read($length) + { + $result = $this->stream->read($length); + + // Invoke the callback when EOF is hit. + if ($this->eof()) { + call_user_func($this->callback); + } + + return $result; + } +} +``` + +This decorator could be added to any existing stream and used like so: + +```php +use GuzzleHttp\Psr7; + +$original = Psr7\stream_for('foo'); + +$eofStream = new EofCallbackStream($original, function () { + echo 'EOF!'; +}); + +$eofStream->read(2); +$eofStream->read(1); +// echoes "EOF!" +$eofStream->seek(0); +$eofStream->read(3); +// echoes "EOF!" +``` + + +## PHP StreamWrapper + +You can use the `GuzzleHttp\Psr7\StreamWrapper` class if you need to use a +PSR-7 stream as a PHP stream resource. + +Use the `GuzzleHttp\Psr7\StreamWrapper::getResource()` method to create a PHP +stream from a PSR-7 stream. + +```php +use GuzzleHttp\Psr7\StreamWrapper; + +$stream = GuzzleHttp\Psr7\stream_for('hello!'); +$resource = StreamWrapper::getResource($stream); +echo fread($resource, 6); // outputs hello! +``` + + +# Function API + +There are various functions available under the `GuzzleHttp\Psr7` namespace. + + +## `function str` + +`function str(MessageInterface $message)` + +Returns the string representation of an HTTP message. + +```php +$request = new GuzzleHttp\Psr7\Request('GET', 'http://example.com'); +echo GuzzleHttp\Psr7\str($request); +``` + + +## `function uri_for` + +`function uri_for($uri)` + +This function accepts a string or `Psr\Http\Message\UriInterface` and returns a +UriInterface for the given value. If the value is already a `UriInterface`, it +is returned as-is. + +```php +$uri = GuzzleHttp\Psr7\uri_for('http://example.com'); +assert($uri === GuzzleHttp\Psr7\uri_for($uri)); +``` + + +## `function stream_for` + +`function stream_for($resource = '', array $options = [])` + +Create a new stream based on the input type. + +Options is an associative array that can contain the following keys: + +* - metadata: Array of custom metadata. +* - size: Size of the stream. + +This method accepts the following `$resource` types: + +- `Psr\Http\Message\StreamInterface`: Returns the value as-is. +- `string`: Creates a stream object that uses the given string as the contents. +- `resource`: Creates a stream object that wraps the given PHP stream resource. +- `Iterator`: If the provided value implements `Iterator`, then a read-only + stream object will be created that wraps the given iterable. Each time the + stream is read from, data from the iterator will fill a buffer and will be + continuously called until the buffer is equal to the requested read size. + Subsequent read calls will first read from the buffer and then call `next` + on the underlying iterator until it is exhausted. +- `object` with `__toString()`: If the object has the `__toString()` method, + the object will be cast to a string and then a stream will be returned that + uses the string value. +- `NULL`: When `null` is passed, an empty stream object is returned. +- `callable` When a callable is passed, a read-only stream object will be + created that invokes the given callable. The callable is invoked with the + number of suggested bytes to read. The callable can return any number of + bytes, but MUST return `false` when there is no more data to return. The + stream object that wraps the callable will invoke the callable until the + number of requested bytes are available. Any additional bytes will be + buffered and used in subsequent reads. + +```php +$stream = GuzzleHttp\Psr7\stream_for('foo'); +$stream = GuzzleHttp\Psr7\stream_for(fopen('/path/to/file', 'r')); + +$generator = function ($bytes) { + for ($i = 0; $i < $bytes; $i++) { + yield ' '; + } +} + +$stream = GuzzleHttp\Psr7\stream_for($generator(100)); +``` + + +## `function parse_header` + +`function parse_header($header)` + +Parse an array of header values containing ";" separated data into an array of +associative arrays representing the header key value pair data of the header. +When a parameter does not contain a value, but just contains a key, this +function will inject a key with a '' string value. + + +## `function normalize_header` + +`function normalize_header($header)` + +Converts an array of header values that may contain comma separated headers +into an array of headers with no comma separated values. + + +## `function modify_request` + +`function modify_request(RequestInterface $request, array $changes)` + +Clone and modify a request with the given changes. This method is useful for +reducing the number of clones needed to mutate a message. + +The changes can be one of: + +- method: (string) Changes the HTTP method. +- set_headers: (array) Sets the given headers. +- remove_headers: (array) Remove the given headers. +- body: (mixed) Sets the given body. +- uri: (UriInterface) Set the URI. +- query: (string) Set the query string value of the URI. +- version: (string) Set the protocol version. + + +## `function rewind_body` + +`function rewind_body(MessageInterface $message)` + +Attempts to rewind a message body and throws an exception on failure. The body +of the message will only be rewound if a call to `tell()` returns a value other +than `0`. + + +## `function try_fopen` + +`function try_fopen($filename, $mode)` + +Safely opens a PHP stream resource using a filename. + +When fopen fails, PHP normally raises a warning. This function adds an error +handler that checks for errors and throws an exception instead. + + +## `function copy_to_string` + +`function copy_to_string(StreamInterface $stream, $maxLen = -1)` + +Copy the contents of a stream into a string until the given number of bytes +have been read. + + +## `function copy_to_stream` + +`function copy_to_stream(StreamInterface $source, StreamInterface $dest, $maxLen = -1)` + +Copy the contents of a stream into another stream until the given number of +bytes have been read. + + +## `function hash` + +`function hash(StreamInterface $stream, $algo, $rawOutput = false)` + +Calculate a hash of a Stream. This method reads the entire stream to calculate +a rolling hash (based on PHP's hash_init functions). + + +## `function readline` + +`function readline(StreamInterface $stream, $maxLength = null)` + +Read a line from the stream up to the maximum allowed buffer length. + + +## `function parse_request` + +`function parse_request($message)` + +Parses a request message string into a request object. + + +## `function parse_response` + +`function parse_response($message)` + +Parses a response message string into a response object. + + +## `function parse_query` + +`function parse_query($str, $urlEncoding = true)` + +Parse a query string into an associative array. + +If multiple values are found for the same key, the value of that key value pair +will become an array. This function does not parse nested PHP style arrays into +an associative array (e.g., `foo[a]=1&foo[b]=2` will be parsed into +`['foo[a]' => '1', 'foo[b]' => '2']`). + + +## `function build_query` + +`function build_query(array $params, $encoding = PHP_QUERY_RFC3986)` + +Build a query string from an array of key value pairs. + +This function can use the return value of parse_query() to build a query string. +This function does not modify the provided keys when an array is encountered +(like http_build_query would). + + +## `function mimetype_from_filename` + +`function mimetype_from_filename($filename)` + +Determines the mimetype of a file by looking at its extension. + + +## `function mimetype_from_extension` + +`function mimetype_from_extension($extension)` + +Maps a file extensions to a mimetype. + + +# Additional URI Methods + +Aside from the standard `Psr\Http\Message\UriInterface` implementation in form of the `GuzzleHttp\Psr7\Uri` class, +this library also provides additional functionality when working with URIs as static methods. + +## URI Types + +An instance of `Psr\Http\Message\UriInterface` can either be an absolute URI or a relative reference. +An absolute URI has a scheme. A relative reference is used to express a URI relative to another URI, +the base URI. Relative references can be divided into several forms according to +[RFC 3986 Section 4.2](https://tools.ietf.org/html/rfc3986#section-4.2): + +- network-path references, e.g. `//example.com/path` +- absolute-path references, e.g. `/path` +- relative-path references, e.g. `subpath` + +The following methods can be used to identify the type of the URI. + +### `GuzzleHttp\Psr7\Uri::isAbsolute` + +`public static function isAbsolute(UriInterface $uri): bool` + +Whether the URI is absolute, i.e. it has a scheme. + +### `GuzzleHttp\Psr7\Uri::isNetworkPathReference` + +`public static function isNetworkPathReference(UriInterface $uri): bool` + +Whether the URI is a network-path reference. A relative reference that begins with two slash characters is +termed an network-path reference. + +### `GuzzleHttp\Psr7\Uri::isAbsolutePathReference` + +`public static function isAbsolutePathReference(UriInterface $uri): bool` + +Whether the URI is a absolute-path reference. A relative reference that begins with a single slash character is +termed an absolute-path reference. + +### `GuzzleHttp\Psr7\Uri::isRelativePathReference` + +`public static function isRelativePathReference(UriInterface $uri): bool` + +Whether the URI is a relative-path reference. A relative reference that does not begin with a slash character is +termed a relative-path reference. + +### `GuzzleHttp\Psr7\Uri::isSameDocumentReference` + +`public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool` + +Whether the URI is a same-document reference. A same-document reference refers to a URI that is, aside from its +fragment component, identical to the base URI. When no base URI is given, only an empty URI reference +(apart from its fragment) is considered a same-document reference. + +## URI Components + +Additional methods to work with URI components. + +### `GuzzleHttp\Psr7\Uri::isDefaultPort` + +`public static function isDefaultPort(UriInterface $uri): bool` + +Whether the URI has the default port of the current scheme. `Psr\Http\Message\UriInterface::getPort` may return null +or the standard port. This method can be used independently of the implementation. + +### `GuzzleHttp\Psr7\Uri::composeComponents` + +`public static function composeComponents($scheme, $authority, $path, $query, $fragment): string` + +Composes a URI reference string from its various components according to +[RFC 3986 Section 5.3](https://tools.ietf.org/html/rfc3986#section-5.3). Usually this method does not need to be called +manually but instead is used indirectly via `Psr\Http\Message\UriInterface::__toString`. + +### `GuzzleHttp\Psr7\Uri::fromParts` + +`public static function fromParts(array $parts): UriInterface` + +Creates a URI from a hash of [`parse_url`](http://php.net/manual/en/function.parse-url.php) components. + + +### `GuzzleHttp\Psr7\Uri::withQueryValue` + +`public static function withQueryValue(UriInterface $uri, $key, $value): UriInterface` + +Creates a new URI with a specific query string value. Any existing query string values that exactly match the +provided key are removed and replaced with the given key value pair. A value of null will set the query string +key without a value, e.g. "key" instead of "key=value". + +### `GuzzleHttp\Psr7\Uri::withQueryValues` + +`public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface` + +Creates a new URI with multiple query string values. It has the same behavior as `withQueryValue()` but for an +associative array of key => value. + +### `GuzzleHttp\Psr7\Uri::withoutQueryValue` + +`public static function withoutQueryValue(UriInterface $uri, $key): UriInterface` + +Creates a new URI with a specific query string value removed. Any existing query string values that exactly match the +provided key are removed. + +## Reference Resolution + +`GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the context of a base URI according +to [RFC 3986 Section 5](https://tools.ietf.org/html/rfc3986#section-5). This is for example also what web browsers +do when resolving a link in a website based on the current request URI. + +### `GuzzleHttp\Psr7\UriResolver::resolve` + +`public static function resolve(UriInterface $base, UriInterface $rel): UriInterface` + +Converts the relative URI into a new URI that is resolved against the base URI. + +### `GuzzleHttp\Psr7\UriResolver::removeDotSegments` + +`public static function removeDotSegments(string $path): string` + +Removes dot segments from a path and returns the new path according to +[RFC 3986 Section 5.2.4](https://tools.ietf.org/html/rfc3986#section-5.2.4). + +### `GuzzleHttp\Psr7\UriResolver::relativize` + +`public static function relativize(UriInterface $base, UriInterface $target): UriInterface` + +Returns the target URI as a relative reference from the base URI. This method is the counterpart to resolve(): + +```php +(string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) +``` + +One use-case is to use the current request URI as base URI and then generate relative links in your documents +to reduce the document size or offer self-contained downloadable document archives. + +```php +$base = new Uri('http://example.com/a/b/'); +echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. +echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. +echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. +echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. +``` + +## Normalization and Comparison + +`GuzzleHttp\Psr7\UriNormalizer` provides methods to normalize and compare URIs according to +[RFC 3986 Section 6](https://tools.ietf.org/html/rfc3986#section-6). + +### `GuzzleHttp\Psr7\UriNormalizer::normalize` + +`public static function normalize(UriInterface $uri, $flags = self::PRESERVING_NORMALIZATIONS): UriInterface` + +Returns a normalized URI. The scheme and host component are already normalized to lowercase per PSR-7 UriInterface. +This methods adds additional normalizations that can be configured with the `$flags` parameter which is a bitmask +of normalizations to apply. The following normalizations are available: + +- `UriNormalizer::PRESERVING_NORMALIZATIONS` + + Default normalizations which only include the ones that preserve semantics. + +- `UriNormalizer::CAPITALIZE_PERCENT_ENCODING` + + All letters within a percent-encoding triplet (e.g., "%3A") are case-insensitive, and should be capitalized. + + Example: `http://example.org/a%c2%b1b` → `http://example.org/a%C2%B1b` + +- `UriNormalizer::DECODE_UNRESERVED_CHARACTERS` + + Decodes percent-encoded octets of unreserved characters. For consistency, percent-encoded octets in the ranges of + ALPHA (%41–%5A and %61–%7A), DIGIT (%30–%39), hyphen (%2D), period (%2E), underscore (%5F), or tilde (%7E) should + not be created by URI producers and, when found in a URI, should be decoded to their corresponding unreserved + characters by URI normalizers. + + Example: `http://example.org/%7Eusern%61me/` → `http://example.org/~username/` + +- `UriNormalizer::CONVERT_EMPTY_PATH` + + Converts the empty path to "/" for http and https URIs. + + Example: `http://example.org` → `http://example.org/` + +- `UriNormalizer::REMOVE_DEFAULT_HOST` + + Removes the default host of the given URI scheme from the URI. Only the "file" scheme defines the default host + "localhost". All of `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` are equivalent according to + RFC 3986. + + Example: `file://localhost/myfile` → `file:///myfile` + +- `UriNormalizer::REMOVE_DEFAULT_PORT` + + Removes the default port of the given URI scheme from the URI. + + Example: `http://example.org:80/` → `http://example.org/` + +- `UriNormalizer::REMOVE_DOT_SEGMENTS` + + Removes unnecessary dot-segments. Dot-segments in relative-path references are not removed as it would + change the semantics of the URI reference. + + Example: `http://example.org/../a/b/../c/./d.html` → `http://example.org/a/c/d.html` + +- `UriNormalizer::REMOVE_DUPLICATE_SLASHES` + + Paths which include two or more adjacent slashes are converted to one. Webservers usually ignore duplicate slashes + and treat those URIs equivalent. But in theory those URIs do not need to be equivalent. So this normalization + may change the semantics. Encoded slashes (%2F) are not removed. + + Example: `http://example.org//foo///bar.html` → `http://example.org/foo/bar.html` + +- `UriNormalizer::SORT_QUERY_PARAMETERS` + + Sort query parameters with their values in alphabetical order. However, the order of parameters in a URI may be + significant (this is not defined by the standard). So this normalization is not safe and may change the semantics + of the URI. + + Example: `?lang=en&article=fred` → `?article=fred&lang=en` + +### `GuzzleHttp\Psr7\UriNormalizer::isEquivalent` + +`public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS): bool` + +Whether two URIs can be considered equivalent. Both URIs are normalized automatically before comparison with the given +`$normalizations` bitmask. The method also accepts relative URI references and returns true when they are equivalent. +This of course assumes they will be resolved against the same base URI. If this is not the case, determination of +equivalence or difference of relative references does not mean anything. diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/composer.json b/plugins/panakour/backup/vendor/guzzlehttp/psr7/composer.json new file mode 100644 index 0000000..168a055 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/composer.json @@ -0,0 +1,49 @@ +{ + "name": "guzzlehttp/psr7", + "type": "library", + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": ["request", "response", "message", "stream", "http", "uri", "url", "psr-7"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8", + "ext-zlib": "*" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": ["src/functions_include.php"] + }, + "autoload-dev": { + "psr-4": { + "GuzzleHttp\\Tests\\Psr7\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/AppendStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/AppendStream.php new file mode 100644 index 0000000..472a0d6 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/AppendStream.php @@ -0,0 +1,241 @@ +addStream($stream); + } + } + + public function __toString() + { + try { + $this->rewind(); + return $this->getContents(); + } catch (\Exception $e) { + return ''; + } + } + + /** + * Add a stream to the AppendStream + * + * @param StreamInterface $stream Stream to append. Must be readable. + * + * @throws \InvalidArgumentException if the stream is not readable + */ + public function addStream(StreamInterface $stream) + { + if (!$stream->isReadable()) { + throw new \InvalidArgumentException('Each stream must be readable'); + } + + // The stream is only seekable if all streams are seekable + if (!$stream->isSeekable()) { + $this->seekable = false; + } + + $this->streams[] = $stream; + } + + public function getContents() + { + return copy_to_string($this); + } + + /** + * Closes each attached stream. + * + * {@inheritdoc} + */ + public function close() + { + $this->pos = $this->current = 0; + $this->seekable = true; + + foreach ($this->streams as $stream) { + $stream->close(); + } + + $this->streams = []; + } + + /** + * Detaches each attached stream. + * + * Returns null as it's not clear which underlying stream resource to return. + * + * {@inheritdoc} + */ + public function detach() + { + $this->pos = $this->current = 0; + $this->seekable = true; + + foreach ($this->streams as $stream) { + $stream->detach(); + } + + $this->streams = []; + } + + public function tell() + { + return $this->pos; + } + + /** + * Tries to calculate the size by adding the size of each stream. + * + * If any of the streams do not return a valid number, then the size of the + * append stream cannot be determined and null is returned. + * + * {@inheritdoc} + */ + public function getSize() + { + $size = 0; + + foreach ($this->streams as $stream) { + $s = $stream->getSize(); + if ($s === null) { + return null; + } + $size += $s; + } + + return $size; + } + + public function eof() + { + return !$this->streams || + ($this->current >= count($this->streams) - 1 && + $this->streams[$this->current]->eof()); + } + + public function rewind() + { + $this->seek(0); + } + + /** + * Attempts to seek to the given position. Only supports SEEK_SET. + * + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if (!$this->seekable) { + throw new \RuntimeException('This AppendStream is not seekable'); + } elseif ($whence !== SEEK_SET) { + throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); + } + + $this->pos = $this->current = 0; + + // Rewind each stream + foreach ($this->streams as $i => $stream) { + try { + $stream->rewind(); + } catch (\Exception $e) { + throw new \RuntimeException('Unable to seek stream ' + . $i . ' of the AppendStream', 0, $e); + } + } + + // Seek to the actual position by reading from each stream + while ($this->pos < $offset && !$this->eof()) { + $result = $this->read(min(8096, $offset - $this->pos)); + if ($result === '') { + break; + } + } + } + + /** + * Reads from all of the appended streams until the length is met or EOF. + * + * {@inheritdoc} + */ + public function read($length) + { + $buffer = ''; + $total = count($this->streams) - 1; + $remaining = $length; + $progressToNext = false; + + while ($remaining > 0) { + + // Progress to the next stream if needed. + if ($progressToNext || $this->streams[$this->current]->eof()) { + $progressToNext = false; + if ($this->current === $total) { + break; + } + $this->current++; + } + + $result = $this->streams[$this->current]->read($remaining); + + // Using a loose comparison here to match on '', false, and null + if ($result == null) { + $progressToNext = true; + continue; + } + + $buffer .= $result; + $remaining = $length - strlen($buffer); + } + + $this->pos += strlen($buffer); + + return $buffer; + } + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return false; + } + + public function isSeekable() + { + return $this->seekable; + } + + public function write($string) + { + throw new \RuntimeException('Cannot write to an AppendStream'); + } + + public function getMetadata($key = null) + { + return $key ? null : []; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/BufferStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/BufferStream.php new file mode 100644 index 0000000..af4d4c2 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/BufferStream.php @@ -0,0 +1,137 @@ +hwm = $hwm; + } + + public function __toString() + { + return $this->getContents(); + } + + public function getContents() + { + $buffer = $this->buffer; + $this->buffer = ''; + + return $buffer; + } + + public function close() + { + $this->buffer = ''; + } + + public function detach() + { + $this->close(); + } + + public function getSize() + { + return strlen($this->buffer); + } + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return true; + } + + public function isSeekable() + { + return false; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \RuntimeException('Cannot seek a BufferStream'); + } + + public function eof() + { + return strlen($this->buffer) === 0; + } + + public function tell() + { + throw new \RuntimeException('Cannot determine the position of a BufferStream'); + } + + /** + * Reads data from the buffer. + */ + public function read($length) + { + $currentLength = strlen($this->buffer); + + if ($length >= $currentLength) { + // No need to slice the buffer because we don't have enough data. + $result = $this->buffer; + $this->buffer = ''; + } else { + // Slice up the result to provide a subset of the buffer. + $result = substr($this->buffer, 0, $length); + $this->buffer = substr($this->buffer, $length); + } + + return $result; + } + + /** + * Writes data to the buffer. + */ + public function write($string) + { + $this->buffer .= $string; + + // TODO: What should happen here? + if (strlen($this->buffer) >= $this->hwm) { + return false; + } + + return strlen($string); + } + + public function getMetadata($key = null) + { + if ($key == 'hwm') { + return $this->hwm; + } + + return $key ? null : []; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/CachingStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/CachingStream.php new file mode 100644 index 0000000..ed68f08 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/CachingStream.php @@ -0,0 +1,138 @@ +remoteStream = $stream; + $this->stream = $target ?: new Stream(fopen('php://temp', 'r+')); + } + + public function getSize() + { + return max($this->stream->getSize(), $this->remoteStream->getSize()); + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + if ($whence == SEEK_SET) { + $byte = $offset; + } elseif ($whence == SEEK_CUR) { + $byte = $offset + $this->tell(); + } elseif ($whence == SEEK_END) { + $size = $this->remoteStream->getSize(); + if ($size === null) { + $size = $this->cacheEntireStream(); + } + $byte = $size + $offset; + } else { + throw new \InvalidArgumentException('Invalid whence'); + } + + $diff = $byte - $this->stream->getSize(); + + if ($diff > 0) { + // Read the remoteStream until we have read in at least the amount + // of bytes requested, or we reach the end of the file. + while ($diff > 0 && !$this->remoteStream->eof()) { + $this->read($diff); + $diff = $byte - $this->stream->getSize(); + } + } else { + // We can just do a normal seek since we've already seen this byte. + $this->stream->seek($byte); + } + } + + public function read($length) + { + // Perform a regular read on any previously read data from the buffer + $data = $this->stream->read($length); + $remaining = $length - strlen($data); + + // More data was requested so read from the remote stream + if ($remaining) { + // If data was written to the buffer in a position that would have + // been filled from the remote stream, then we must skip bytes on + // the remote stream to emulate overwriting bytes from that + // position. This mimics the behavior of other PHP stream wrappers. + $remoteData = $this->remoteStream->read( + $remaining + $this->skipReadBytes + ); + + if ($this->skipReadBytes) { + $len = strlen($remoteData); + $remoteData = substr($remoteData, $this->skipReadBytes); + $this->skipReadBytes = max(0, $this->skipReadBytes - $len); + } + + $data .= $remoteData; + $this->stream->write($remoteData); + } + + return $data; + } + + public function write($string) + { + // When appending to the end of the currently read stream, you'll want + // to skip bytes from being read from the remote stream to emulate + // other stream wrappers. Basically replacing bytes of data of a fixed + // length. + $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); + if ($overflow > 0) { + $this->skipReadBytes += $overflow; + } + + return $this->stream->write($string); + } + + public function eof() + { + return $this->stream->eof() && $this->remoteStream->eof(); + } + + /** + * Close both the remote stream and buffer stream + */ + public function close() + { + $this->remoteStream->close() && $this->stream->close(); + } + + private function cacheEntireStream() + { + $target = new FnStream(['write' => 'strlen']); + copy_to_stream($this, $target); + + return $this->tell(); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/DroppingStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/DroppingStream.php new file mode 100644 index 0000000..8935c80 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/DroppingStream.php @@ -0,0 +1,42 @@ +stream = $stream; + $this->maxLength = $maxLength; + } + + public function write($string) + { + $diff = $this->maxLength - $this->stream->getSize(); + + // Begin returning 0 when the underlying stream is too large. + if ($diff <= 0) { + return 0; + } + + // Write the stream or a subset of the stream if needed. + if (strlen($string) < $diff) { + return $this->stream->write($string); + } + + return $this->stream->write(substr($string, 0, $diff)); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/FnStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/FnStream.php new file mode 100644 index 0000000..73daea6 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/FnStream.php @@ -0,0 +1,158 @@ +methods = $methods; + + // Create the functions on the class + foreach ($methods as $name => $fn) { + $this->{'_fn_' . $name} = $fn; + } + } + + /** + * Lazily determine which methods are not implemented. + * @throws \BadMethodCallException + */ + public function __get($name) + { + throw new \BadMethodCallException(str_replace('_fn_', '', $name) + . '() is not implemented in the FnStream'); + } + + /** + * The close method is called on the underlying stream only if possible. + */ + public function __destruct() + { + if (isset($this->_fn_close)) { + call_user_func($this->_fn_close); + } + } + + /** + * An unserialize would allow the __destruct to run when the unserialized value goes out of scope. + * @throws \LogicException + */ + public function __wakeup() + { + throw new \LogicException('FnStream should never be unserialized'); + } + + /** + * Adds custom functionality to an underlying stream by intercepting + * specific method calls. + * + * @param StreamInterface $stream Stream to decorate + * @param array $methods Hash of method name to a closure + * + * @return FnStream + */ + public static function decorate(StreamInterface $stream, array $methods) + { + // If any of the required methods were not provided, then simply + // proxy to the decorated stream. + foreach (array_diff(self::$slots, array_keys($methods)) as $diff) { + $methods[$diff] = [$stream, $diff]; + } + + return new self($methods); + } + + public function __toString() + { + return call_user_func($this->_fn___toString); + } + + public function close() + { + return call_user_func($this->_fn_close); + } + + public function detach() + { + return call_user_func($this->_fn_detach); + } + + public function getSize() + { + return call_user_func($this->_fn_getSize); + } + + public function tell() + { + return call_user_func($this->_fn_tell); + } + + public function eof() + { + return call_user_func($this->_fn_eof); + } + + public function isSeekable() + { + return call_user_func($this->_fn_isSeekable); + } + + public function rewind() + { + call_user_func($this->_fn_rewind); + } + + public function seek($offset, $whence = SEEK_SET) + { + call_user_func($this->_fn_seek, $offset, $whence); + } + + public function isWritable() + { + return call_user_func($this->_fn_isWritable); + } + + public function write($string) + { + return call_user_func($this->_fn_write, $string); + } + + public function isReadable() + { + return call_user_func($this->_fn_isReadable); + } + + public function read($length) + { + return call_user_func($this->_fn_read, $length); + } + + public function getContents() + { + return call_user_func($this->_fn_getContents); + } + + public function getMetadata($key = null) + { + return call_user_func($this->_fn_getMetadata, $key); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/InflateStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/InflateStream.php new file mode 100644 index 0000000..5e4f602 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/InflateStream.php @@ -0,0 +1,52 @@ +read(10); + $filenameHeaderLength = $this->getLengthOfPossibleFilenameHeader($stream, $header); + // Skip the header, that is 10 + length of filename + 1 (nil) bytes + $stream = new LimitStream($stream, -1, 10 + $filenameHeaderLength); + $resource = StreamWrapper::getResource($stream); + stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ); + $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); + } + + /** + * @param StreamInterface $stream + * @param $header + * @return int + */ + private function getLengthOfPossibleFilenameHeader(StreamInterface $stream, $header) + { + $filename_header_length = 0; + + if (substr(bin2hex($header), 6, 2) === '08') { + // we have a filename, read until nil + $filename_header_length = 1; + while ($stream->read(1) !== chr(0)) { + $filename_header_length++; + } + } + + return $filename_header_length; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/LazyOpenStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/LazyOpenStream.php new file mode 100644 index 0000000..02cec3a --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/LazyOpenStream.php @@ -0,0 +1,39 @@ +filename = $filename; + $this->mode = $mode; + } + + /** + * Creates the underlying stream lazily when required. + * + * @return StreamInterface + */ + protected function createStream() + { + return stream_for(try_fopen($this->filename, $this->mode)); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/LimitStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/LimitStream.php new file mode 100644 index 0000000..e4f239e --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/LimitStream.php @@ -0,0 +1,155 @@ +stream = $stream; + $this->setLimit($limit); + $this->setOffset($offset); + } + + public function eof() + { + // Always return true if the underlying stream is EOF + if ($this->stream->eof()) { + return true; + } + + // No limit and the underlying stream is not at EOF + if ($this->limit == -1) { + return false; + } + + return $this->stream->tell() >= $this->offset + $this->limit; + } + + /** + * Returns the size of the limited subset of data + * {@inheritdoc} + */ + public function getSize() + { + if (null === ($length = $this->stream->getSize())) { + return null; + } elseif ($this->limit == -1) { + return $length - $this->offset; + } else { + return min($this->limit, $length - $this->offset); + } + } + + /** + * Allow for a bounded seek on the read limited stream + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if ($whence !== SEEK_SET || $offset < 0) { + throw new \RuntimeException(sprintf( + 'Cannot seek to offset %s with whence %s', + $offset, + $whence + )); + } + + $offset += $this->offset; + + if ($this->limit !== -1) { + if ($offset > $this->offset + $this->limit) { + $offset = $this->offset + $this->limit; + } + } + + $this->stream->seek($offset); + } + + /** + * Give a relative tell() + * {@inheritdoc} + */ + public function tell() + { + return $this->stream->tell() - $this->offset; + } + + /** + * Set the offset to start limiting from + * + * @param int $offset Offset to seek to and begin byte limiting from + * + * @throws \RuntimeException if the stream cannot be seeked. + */ + public function setOffset($offset) + { + $current = $this->stream->tell(); + + if ($current !== $offset) { + // If the stream cannot seek to the offset position, then read to it + if ($this->stream->isSeekable()) { + $this->stream->seek($offset); + } elseif ($current > $offset) { + throw new \RuntimeException("Could not seek to stream offset $offset"); + } else { + $this->stream->read($offset - $current); + } + } + + $this->offset = $offset; + } + + /** + * Set the limit of bytes that the decorator allows to be read from the + * stream. + * + * @param int $limit Number of bytes to allow to be read from the stream. + * Use -1 for no limit. + */ + public function setLimit($limit) + { + $this->limit = $limit; + } + + public function read($length) + { + if ($this->limit == -1) { + return $this->stream->read($length); + } + + // Check if the current position is less than the total allowed + // bytes + original offset + $remaining = ($this->offset + $this->limit) - $this->stream->tell(); + if ($remaining > 0) { + // Only return the amount of requested data, ensuring that the byte + // limit is not exceeded + return $this->stream->read(min($remaining, $length)); + } + + return ''; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/MessageTrait.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/MessageTrait.php new file mode 100644 index 0000000..a7966d1 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/MessageTrait.php @@ -0,0 +1,213 @@ + array of values */ + private $headers = []; + + /** @var array Map of lowercase header name => original name at registration */ + private $headerNames = []; + + /** @var string */ + private $protocol = '1.1'; + + /** @var StreamInterface */ + private $stream; + + public function getProtocolVersion() + { + return $this->protocol; + } + + public function withProtocolVersion($version) + { + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = $version; + return $new; + } + + public function getHeaders() + { + return $this->headers; + } + + public function hasHeader($header) + { + return isset($this->headerNames[strtolower($header)]); + } + + public function getHeader($header) + { + $header = strtolower($header); + + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function getHeaderLine($header) + { + return implode(', ', $this->getHeader($header)); + } + + public function withHeader($header, $value) + { + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + public function withAddedHeader($header, $value) + { + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $new->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + } + + return $new; + } + + public function withoutHeader($header) + { + $normalized = strtolower($header); + + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getBody() + { + if (!$this->stream) { + $this->stream = stream_for(''); + } + + return $this->stream; + } + + public function withBody(StreamInterface $body) + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + return $new; + } + + private function setHeaders(array $headers) + { + $this->headerNames = $this->headers = []; + foreach ($headers as $header => $value) { + if (is_int($header)) { + // Numeric array keys are converted to int by PHP but having a header name '123' is not forbidden by the spec + // and also allowed in withHeader(). So we need to cast it to string again for the following assertion to pass. + $header = (string) $header; + } + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + if (isset($this->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $this->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + } + } + + private function normalizeHeaderValue($value) + { + if (!is_array($value)) { + return $this->trimHeaderValues([$value]); + } + + if (count($value) === 0) { + throw new \InvalidArgumentException('Header value can not be an empty array.'); + } + + return $this->trimHeaderValues($value); + } + + /** + * Trims whitespace from the header values. + * + * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field. + * + * header-field = field-name ":" OWS field-value OWS + * OWS = *( SP / HTAB ) + * + * @param string[] $values Header values + * + * @return string[] Trimmed header values + * + * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + */ + private function trimHeaderValues(array $values) + { + return array_map(function ($value) { + if (!is_scalar($value) && null !== $value) { + throw new \InvalidArgumentException(sprintf( + 'Header value must be scalar or null but %s provided.', + is_object($value) ? get_class($value) : gettype($value) + )); + } + + return trim((string) $value, " \t"); + }, $values); + } + + private function assertHeader($header) + { + if (!is_string($header)) { + throw new \InvalidArgumentException(sprintf( + 'Header name must be a string but %s provided.', + is_object($header) ? get_class($header) : gettype($header) + )); + } + + if ($header === '') { + throw new \InvalidArgumentException('Header name can not be empty.'); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/MultipartStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/MultipartStream.php new file mode 100644 index 0000000..c0fd584 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/MultipartStream.php @@ -0,0 +1,153 @@ +boundary = $boundary ?: sha1(uniqid('', true)); + $this->stream = $this->createStream($elements); + } + + /** + * Get the boundary + * + * @return string + */ + public function getBoundary() + { + return $this->boundary; + } + + public function isWritable() + { + return false; + } + + /** + * Get the headers needed before transferring the content of a POST file + */ + private function getHeaders(array $headers) + { + $str = ''; + foreach ($headers as $key => $value) { + $str .= "{$key}: {$value}\r\n"; + } + + return "--{$this->boundary}\r\n" . trim($str) . "\r\n\r\n"; + } + + /** + * Create the aggregate stream that will be used to upload the POST data + */ + protected function createStream(array $elements) + { + $stream = new AppendStream(); + + foreach ($elements as $element) { + $this->addElement($stream, $element); + } + + // Add the trailing boundary with CRLF + $stream->addStream(stream_for("--{$this->boundary}--\r\n")); + + return $stream; + } + + private function addElement(AppendStream $stream, array $element) + { + foreach (['contents', 'name'] as $key) { + if (!array_key_exists($key, $element)) { + throw new \InvalidArgumentException("A '{$key}' key is required"); + } + } + + $element['contents'] = stream_for($element['contents']); + + if (empty($element['filename'])) { + $uri = $element['contents']->getMetadata('uri'); + if (substr($uri, 0, 6) !== 'php://') { + $element['filename'] = $uri; + } + } + + list($body, $headers) = $this->createElement( + $element['name'], + $element['contents'], + isset($element['filename']) ? $element['filename'] : null, + isset($element['headers']) ? $element['headers'] : [] + ); + + $stream->addStream(stream_for($this->getHeaders($headers))); + $stream->addStream($body); + $stream->addStream(stream_for("\r\n")); + } + + /** + * @return array + */ + private function createElement($name, StreamInterface $stream, $filename, array $headers) + { + // Set a default content-disposition header if one was no provided + $disposition = $this->getHeader($headers, 'content-disposition'); + if (!$disposition) { + $headers['Content-Disposition'] = ($filename === '0' || $filename) + ? sprintf('form-data; name="%s"; filename="%s"', + $name, + basename($filename)) + : "form-data; name=\"{$name}\""; + } + + // Set a default content-length header if one was no provided + $length = $this->getHeader($headers, 'content-length'); + if (!$length) { + if ($length = $stream->getSize()) { + $headers['Content-Length'] = (string) $length; + } + } + + // Set a default Content-Type if one was not supplied + $type = $this->getHeader($headers, 'content-type'); + if (!$type && ($filename === '0' || $filename)) { + if ($type = mimetype_from_filename($filename)) { + $headers['Content-Type'] = $type; + } + } + + return [$stream, $headers]; + } + + private function getHeader(array $headers, $key) + { + $lowercaseHeader = strtolower($key); + foreach ($headers as $k => $v) { + if (strtolower($k) === $lowercaseHeader) { + return $v; + } + } + + return null; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/NoSeekStream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/NoSeekStream.php new file mode 100644 index 0000000..2332218 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/NoSeekStream.php @@ -0,0 +1,22 @@ +source = $source; + $this->size = isset($options['size']) ? $options['size'] : null; + $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; + $this->buffer = new BufferStream(); + } + + public function __toString() + { + try { + return copy_to_string($this); + } catch (\Exception $e) { + return ''; + } + } + + public function close() + { + $this->detach(); + } + + public function detach() + { + $this->tellPos = false; + $this->source = null; + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + return $this->tellPos; + } + + public function eof() + { + return !$this->source; + } + + public function isSeekable() + { + return false; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \RuntimeException('Cannot seek a PumpStream'); + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + throw new \RuntimeException('Cannot write to a PumpStream'); + } + + public function isReadable() + { + return true; + } + + public function read($length) + { + $data = $this->buffer->read($length); + $readLen = strlen($data); + $this->tellPos += $readLen; + $remaining = $length - $readLen; + + if ($remaining) { + $this->pump($remaining); + $data .= $this->buffer->read($remaining); + $this->tellPos += strlen($data) - $readLen; + } + + return $data; + } + + public function getContents() + { + $result = ''; + while (!$this->eof()) { + $result .= $this->read(1000000); + } + + return $result; + } + + public function getMetadata($key = null) + { + if (!$key) { + return $this->metadata; + } + + return isset($this->metadata[$key]) ? $this->metadata[$key] : null; + } + + private function pump($length) + { + if ($this->source) { + do { + $data = call_user_func($this->source, $length); + if ($data === false || $data === null) { + $this->source = null; + return; + } + $this->buffer->write($data); + $length -= strlen($data); + } while ($length > 0); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Request.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Request.php new file mode 100644 index 0000000..59f337d --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Request.php @@ -0,0 +1,151 @@ +assertMethod($method); + if (!($uri instanceof UriInterface)) { + $uri = new Uri($uri); + } + + $this->method = strtoupper($method); + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + + if (!isset($this->headerNames['host'])) { + $this->updateHostFromUri(); + } + + if ($body !== '' && $body !== null) { + $this->stream = stream_for($body); + } + } + + public function getRequestTarget() + { + if ($this->requestTarget !== null) { + return $this->requestTarget; + } + + $target = $this->uri->getPath(); + if ($target == '') { + $target = '/'; + } + if ($this->uri->getQuery() != '') { + $target .= '?' . $this->uri->getQuery(); + } + + return $target; + } + + public function withRequestTarget($requestTarget) + { + if (preg_match('#\s#', $requestTarget)) { + throw new InvalidArgumentException( + 'Invalid request target provided; cannot contain whitespace' + ); + } + + $new = clone $this; + $new->requestTarget = $requestTarget; + return $new; + } + + public function getMethod() + { + return $this->method; + } + + public function withMethod($method) + { + $this->assertMethod($method); + $new = clone $this; + $new->method = strtoupper($method); + return $new; + } + + public function getUri() + { + return $this->uri; + } + + public function withUri(UriInterface $uri, $preserveHost = false) + { + if ($uri === $this->uri) { + return $this; + } + + $new = clone $this; + $new->uri = $uri; + + if (!$preserveHost || !isset($this->headerNames['host'])) { + $new->updateHostFromUri(); + } + + return $new; + } + + private function updateHostFromUri() + { + $host = $this->uri->getHost(); + + if ($host == '') { + return; + } + + if (($port = $this->uri->getPort()) !== null) { + $host .= ':' . $port; + } + + if (isset($this->headerNames['host'])) { + $header = $this->headerNames['host']; + } else { + $header = 'Host'; + $this->headerNames['host'] = 'Host'; + } + // Ensure Host is the first header. + // See: http://tools.ietf.org/html/rfc7230#section-5.4 + $this->headers = [$header => [$host]] + $this->headers; + } + + private function assertMethod($method) + { + if (!is_string($method) || $method === '') { + throw new \InvalidArgumentException('Method must be a non-empty string.'); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Response.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Response.php new file mode 100644 index 0000000..e7e04d8 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Response.php @@ -0,0 +1,154 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-status', + 208 => 'Already Reported', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Switch Proxy', + 307 => 'Temporary 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 Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 511 => 'Network Authentication Required', + ]; + + /** @var string */ + private $reasonPhrase = ''; + + /** @var int */ + private $statusCode = 200; + + /** + * @param int $status Status code + * @param array $headers Response headers + * @param string|null|resource|StreamInterface $body Response body + * @param string $version Protocol version + * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) + */ + public function __construct( + $status = 200, + array $headers = [], + $body = null, + $version = '1.1', + $reason = null + ) { + $this->assertStatusCodeIsInteger($status); + $status = (int) $status; + $this->assertStatusCodeRange($status); + + $this->statusCode = $status; + + if ($body !== '' && $body !== null) { + $this->stream = stream_for($body); + } + + $this->setHeaders($headers); + if ($reason == '' && isset(self::$phrases[$this->statusCode])) { + $this->reasonPhrase = self::$phrases[$this->statusCode]; + } else { + $this->reasonPhrase = (string) $reason; + } + + $this->protocol = $version; + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + public function withStatus($code, $reasonPhrase = '') + { + $this->assertStatusCodeIsInteger($code); + $code = (int) $code; + $this->assertStatusCodeRange($code); + + $new = clone $this; + $new->statusCode = $code; + if ($reasonPhrase == '' && isset(self::$phrases[$new->statusCode])) { + $reasonPhrase = self::$phrases[$new->statusCode]; + } + $new->reasonPhrase = $reasonPhrase; + return $new; + } + + private function assertStatusCodeIsInteger($statusCode) + { + if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) { + throw new \InvalidArgumentException('Status code must be an integer value.'); + } + } + + private function assertStatusCodeRange($statusCode) + { + if ($statusCode < 100 || $statusCode >= 600) { + throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.'); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Rfc7230.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Rfc7230.php new file mode 100644 index 0000000..505e474 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Rfc7230.php @@ -0,0 +1,18 @@ +@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; + const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/ServerRequest.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/ServerRequest.php new file mode 100644 index 0000000..1a09a6c --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/ServerRequest.php @@ -0,0 +1,376 @@ +serverParams = $serverParams; + + parent::__construct($method, $uri, $headers, $body, $version); + } + + /** + * Return an UploadedFile instance array. + * + * @param array $files A array which respect $_FILES structure + * @throws InvalidArgumentException for unrecognized values + * @return array + */ + public static function normalizeFiles(array $files) + { + $normalized = []; + + foreach ($files as $key => $value) { + if ($value instanceof UploadedFileInterface) { + $normalized[$key] = $value; + } elseif (is_array($value) && isset($value['tmp_name'])) { + $normalized[$key] = self::createUploadedFileFromSpec($value); + } elseif (is_array($value)) { + $normalized[$key] = self::normalizeFiles($value); + continue; + } else { + throw new InvalidArgumentException('Invalid value in files specification'); + } + } + + return $normalized; + } + + /** + * Create and return an UploadedFile instance from a $_FILES specification. + * + * If the specification represents an array of values, this method will + * delegate to normalizeNestedFileSpec() and return that return value. + * + * @param array $value $_FILES struct + * @return array|UploadedFileInterface + */ + private static function createUploadedFileFromSpec(array $value) + { + if (is_array($value['tmp_name'])) { + return self::normalizeNestedFileSpec($value); + } + + return new UploadedFile( + $value['tmp_name'], + (int) $value['size'], + (int) $value['error'], + $value['name'], + $value['type'] + ); + } + + /** + * Normalize an array of file specifications. + * + * Loops through all nested files and returns a normalized array of + * UploadedFileInterface instances. + * + * @param array $files + * @return UploadedFileInterface[] + */ + private static function normalizeNestedFileSpec(array $files = []) + { + $normalizedFiles = []; + + foreach (array_keys($files['tmp_name']) as $key) { + $spec = [ + 'tmp_name' => $files['tmp_name'][$key], + 'size' => $files['size'][$key], + 'error' => $files['error'][$key], + 'name' => $files['name'][$key], + 'type' => $files['type'][$key], + ]; + $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); + } + + return $normalizedFiles; + } + + /** + * Return a ServerRequest populated with superglobals: + * $_GET + * $_POST + * $_COOKIE + * $_FILES + * $_SERVER + * + * @return ServerRequestInterface + */ + public static function fromGlobals() + { + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + $headers = getallheaders(); + $uri = self::getUriFromGlobals(); + $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); + $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; + + $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); + + return $serverRequest + ->withCookieParams($_COOKIE) + ->withQueryParams($_GET) + ->withParsedBody($_POST) + ->withUploadedFiles(self::normalizeFiles($_FILES)); + } + + private static function extractHostAndPortFromAuthority($authority) + { + $uri = 'http://'.$authority; + $parts = parse_url($uri); + if (false === $parts) { + return [null, null]; + } + + $host = isset($parts['host']) ? $parts['host'] : null; + $port = isset($parts['port']) ? $parts['port'] : null; + + return [$host, $port]; + } + + /** + * Get a Uri populated with values from $_SERVER. + * + * @return UriInterface + */ + public static function getUriFromGlobals() + { + $uri = new Uri(''); + + $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); + + $hasPort = false; + if (isset($_SERVER['HTTP_HOST'])) { + list($host, $port) = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); + if ($host !== null) { + $uri = $uri->withHost($host); + } + + if ($port !== null) { + $hasPort = true; + $uri = $uri->withPort($port); + } + } elseif (isset($_SERVER['SERVER_NAME'])) { + $uri = $uri->withHost($_SERVER['SERVER_NAME']); + } elseif (isset($_SERVER['SERVER_ADDR'])) { + $uri = $uri->withHost($_SERVER['SERVER_ADDR']); + } + + if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { + $uri = $uri->withPort($_SERVER['SERVER_PORT']); + } + + $hasQuery = false; + if (isset($_SERVER['REQUEST_URI'])) { + $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); + $uri = $uri->withPath($requestUriParts[0]); + if (isset($requestUriParts[1])) { + $hasQuery = true; + $uri = $uri->withQuery($requestUriParts[1]); + } + } + + if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { + $uri = $uri->withQuery($_SERVER['QUERY_STRING']); + } + + return $uri; + } + + + /** + * {@inheritdoc} + */ + public function getServerParams() + { + return $this->serverParams; + } + + /** + * {@inheritdoc} + */ + public function getUploadedFiles() + { + return $this->uploadedFiles; + } + + /** + * {@inheritdoc} + */ + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->uploadedFiles = $uploadedFiles; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getCookieParams() + { + return $this->cookieParams; + } + + /** + * {@inheritdoc} + */ + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookieParams = $cookies; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getQueryParams() + { + return $this->queryParams; + } + + /** + * {@inheritdoc} + */ + public function withQueryParams(array $query) + { + $new = clone $this; + $new->queryParams = $query; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getParsedBody() + { + return $this->parsedBody; + } + + /** + * {@inheritdoc} + */ + public function withParsedBody($data) + { + $new = clone $this; + $new->parsedBody = $data; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * {@inheritdoc} + */ + public function getAttribute($attribute, $default = null) + { + if (false === array_key_exists($attribute, $this->attributes)) { + return $default; + } + + return $this->attributes[$attribute]; + } + + /** + * {@inheritdoc} + */ + public function withAttribute($attribute, $value) + { + $new = clone $this; + $new->attributes[$attribute] = $value; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withoutAttribute($attribute) + { + if (false === array_key_exists($attribute, $this->attributes)) { + return $this; + } + + $new = clone $this; + unset($new->attributes[$attribute]); + + return $new; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Stream.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Stream.php new file mode 100644 index 0000000..d9e7409 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Stream.php @@ -0,0 +1,267 @@ +size = $options['size']; + } + + $this->customMetadata = isset($options['metadata']) + ? $options['metadata'] + : []; + + $this->stream = $stream; + $meta = stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable']; + $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']); + $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']); + $this->uri = $this->getMetadata('uri'); + } + + /** + * Closes the stream when the destructed + */ + public function __destruct() + { + $this->close(); + } + + public function __toString() + { + try { + $this->seek(0); + return (string) stream_get_contents($this->stream); + } catch (\Exception $e) { + return ''; + } + } + + public function getContents() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + $contents = stream_get_contents($this->stream); + + if ($contents === false) { + throw new \RuntimeException('Unable to read stream contents'); + } + + return $contents; + } + + public function close() + { + if (isset($this->stream)) { + if (is_resource($this->stream)) { + fclose($this->stream); + } + $this->detach(); + } + } + + public function detach() + { + if (!isset($this->stream)) { + return null; + } + + $result = $this->stream; + unset($this->stream); + $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + + return $result; + } + + public function getSize() + { + if ($this->size !== null) { + return $this->size; + } + + if (!isset($this->stream)) { + return null; + } + + // Clear the stat cache if the stream has a URI + if ($this->uri) { + clearstatcache(true, $this->uri); + } + + $stats = fstat($this->stream); + if (isset($stats['size'])) { + $this->size = $stats['size']; + return $this->size; + } + + return null; + } + + public function isReadable() + { + return $this->readable; + } + + public function isWritable() + { + return $this->writable; + } + + public function isSeekable() + { + return $this->seekable; + } + + public function eof() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + return feof($this->stream); + } + + public function tell() + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + $result = ftell($this->stream); + + if ($result === false) { + throw new \RuntimeException('Unable to determine stream position'); + } + + return $result; + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + $whence = (int) $whence; + + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->seekable) { + throw new \RuntimeException('Stream is not seekable'); + } + if (fseek($this->stream, $offset, $whence) === -1) { + throw new \RuntimeException('Unable to seek to stream position ' + . $offset . ' with whence ' . var_export($whence, true)); + } + } + + public function read($length) + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->readable) { + throw new \RuntimeException('Cannot read from non-readable stream'); + } + if ($length < 0) { + throw new \RuntimeException('Length parameter cannot be negative'); + } + + if (0 === $length) { + return ''; + } + + $string = fread($this->stream, $length); + if (false === $string) { + throw new \RuntimeException('Unable to read from stream'); + } + + return $string; + } + + public function write($string) + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->writable) { + throw new \RuntimeException('Cannot write to a non-writable stream'); + } + + // We can't know the size after writing anything + $this->size = null; + $result = fwrite($this->stream, $string); + + if ($result === false) { + throw new \RuntimeException('Unable to write to stream'); + } + + return $result; + } + + public function getMetadata($key = null) + { + if (!isset($this->stream)) { + return $key ? null : []; + } elseif (!$key) { + return $this->customMetadata + stream_get_meta_data($this->stream); + } elseif (isset($this->customMetadata[$key])) { + return $this->customMetadata[$key]; + } + + $meta = stream_get_meta_data($this->stream); + + return isset($meta[$key]) ? $meta[$key] : null; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php new file mode 100644 index 0000000..daec6f5 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php @@ -0,0 +1,149 @@ +stream = $stream; + } + + /** + * Magic method used to create a new stream if streams are not added in + * the constructor of a decorator (e.g., LazyOpenStream). + * + * @param string $name Name of the property (allows "stream" only). + * + * @return StreamInterface + */ + public function __get($name) + { + if ($name == 'stream') { + $this->stream = $this->createStream(); + return $this->stream; + } + + throw new \UnexpectedValueException("$name not found on class"); + } + + public function __toString() + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + return $this->getContents(); + } catch (\Exception $e) { + // Really, PHP? https://bugs.php.net/bug.php?id=53648 + trigger_error('StreamDecorator::__toString exception: ' + . (string) $e, E_USER_ERROR); + return ''; + } + } + + public function getContents() + { + return copy_to_string($this); + } + + /** + * Allow decorators to implement custom methods + * + * @param string $method Missing method name + * @param array $args Method arguments + * + * @return mixed + */ + public function __call($method, array $args) + { + $result = call_user_func_array([$this->stream, $method], $args); + + // Always return the wrapped object if the result is a return $this + return $result === $this->stream ? $this : $result; + } + + public function close() + { + $this->stream->close(); + } + + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } + + public function detach() + { + return $this->stream->detach(); + } + + public function getSize() + { + return $this->stream->getSize(); + } + + public function eof() + { + return $this->stream->eof(); + } + + public function tell() + { + return $this->stream->tell(); + } + + public function isReadable() + { + return $this->stream->isReadable(); + } + + public function isWritable() + { + return $this->stream->isWritable(); + } + + public function isSeekable() + { + return $this->stream->isSeekable(); + } + + public function rewind() + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET) + { + $this->stream->seek($offset, $whence); + } + + public function read($length) + { + return $this->stream->read($length); + } + + public function write($string) + { + return $this->stream->write($string); + } + + /** + * Implement in subclasses to dynamically create streams when requested. + * + * @return StreamInterface + * @throws \BadMethodCallException + */ + protected function createStream() + { + throw new \BadMethodCallException('Not implemented'); + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/StreamWrapper.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/StreamWrapper.php new file mode 100644 index 0000000..0f3a285 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/StreamWrapper.php @@ -0,0 +1,161 @@ +isReadable()) { + $mode = $stream->isWritable() ? 'r+' : 'r'; + } elseif ($stream->isWritable()) { + $mode = 'w'; + } else { + throw new \InvalidArgumentException('The stream must be readable, ' + . 'writable, or both.'); + } + + return fopen('guzzle://stream', $mode, null, self::createStreamContext($stream)); + } + + /** + * Creates a stream context that can be used to open a stream as a php stream resource. + * + * @param StreamInterface $stream + * + * @return resource + */ + public static function createStreamContext(StreamInterface $stream) + { + return stream_context_create([ + 'guzzle' => ['stream' => $stream] + ]); + } + + /** + * Registers the stream wrapper if needed + */ + public static function register() + { + if (!in_array('guzzle', stream_get_wrappers())) { + stream_wrapper_register('guzzle', __CLASS__); + } + } + + public function stream_open($path, $mode, $options, &$opened_path) + { + $options = stream_context_get_options($this->context); + + if (!isset($options['guzzle']['stream'])) { + return false; + } + + $this->mode = $mode; + $this->stream = $options['guzzle']['stream']; + + return true; + } + + public function stream_read($count) + { + return $this->stream->read($count); + } + + public function stream_write($data) + { + return (int) $this->stream->write($data); + } + + public function stream_tell() + { + return $this->stream->tell(); + } + + public function stream_eof() + { + return $this->stream->eof(); + } + + public function stream_seek($offset, $whence) + { + $this->stream->seek($offset, $whence); + + return true; + } + + public function stream_cast($cast_as) + { + $stream = clone($this->stream); + + return $stream->detach(); + } + + public function stream_stat() + { + static $modeMap = [ + 'r' => 33060, + 'rb' => 33060, + 'r+' => 33206, + 'w' => 33188, + 'wb' => 33188 + ]; + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => $modeMap[$this->mode], + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $this->stream->getSize() ?: 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0 + ]; + } + + public function url_stat($path, $flags) + { + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0 + ]; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UploadedFile.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UploadedFile.php new file mode 100644 index 0000000..e62bd5c --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UploadedFile.php @@ -0,0 +1,316 @@ +setError($errorStatus); + $this->setSize($size); + $this->setClientFilename($clientFilename); + $this->setClientMediaType($clientMediaType); + + if ($this->isOk()) { + $this->setStreamOrFile($streamOrFile); + } + } + + /** + * Depending on the value set file or stream variable + * + * @param mixed $streamOrFile + * @throws InvalidArgumentException + */ + private function setStreamOrFile($streamOrFile) + { + if (is_string($streamOrFile)) { + $this->file = $streamOrFile; + } elseif (is_resource($streamOrFile)) { + $this->stream = new Stream($streamOrFile); + } elseif ($streamOrFile instanceof StreamInterface) { + $this->stream = $streamOrFile; + } else { + throw new InvalidArgumentException( + 'Invalid stream or file provided for UploadedFile' + ); + } + } + + /** + * @param int $error + * @throws InvalidArgumentException + */ + private function setError($error) + { + if (false === is_int($error)) { + throw new InvalidArgumentException( + 'Upload file error status must be an integer' + ); + } + + if (false === in_array($error, UploadedFile::$errors)) { + throw new InvalidArgumentException( + 'Invalid error status for UploadedFile' + ); + } + + $this->error = $error; + } + + /** + * @param int $size + * @throws InvalidArgumentException + */ + private function setSize($size) + { + if (false === is_int($size)) { + throw new InvalidArgumentException( + 'Upload file size must be an integer' + ); + } + + $this->size = $size; + } + + /** + * @param mixed $param + * @return boolean + */ + private function isStringOrNull($param) + { + return in_array(gettype($param), ['string', 'NULL']); + } + + /** + * @param mixed $param + * @return boolean + */ + private function isStringNotEmpty($param) + { + return is_string($param) && false === empty($param); + } + + /** + * @param string|null $clientFilename + * @throws InvalidArgumentException + */ + private function setClientFilename($clientFilename) + { + if (false === $this->isStringOrNull($clientFilename)) { + throw new InvalidArgumentException( + 'Upload file client filename must be a string or null' + ); + } + + $this->clientFilename = $clientFilename; + } + + /** + * @param string|null $clientMediaType + * @throws InvalidArgumentException + */ + private function setClientMediaType($clientMediaType) + { + if (false === $this->isStringOrNull($clientMediaType)) { + throw new InvalidArgumentException( + 'Upload file client media type must be a string or null' + ); + } + + $this->clientMediaType = $clientMediaType; + } + + /** + * Return true if there is no upload error + * + * @return boolean + */ + private function isOk() + { + return $this->error === UPLOAD_ERR_OK; + } + + /** + * @return boolean + */ + public function isMoved() + { + return $this->moved; + } + + /** + * @throws RuntimeException if is moved or not ok + */ + private function validateActive() + { + if (false === $this->isOk()) { + throw new RuntimeException('Cannot retrieve stream due to upload error'); + } + + if ($this->isMoved()) { + throw new RuntimeException('Cannot retrieve stream after it has already been moved'); + } + } + + /** + * {@inheritdoc} + * @throws RuntimeException if the upload was not successful. + */ + public function getStream() + { + $this->validateActive(); + + if ($this->stream instanceof StreamInterface) { + return $this->stream; + } + + return new LazyOpenStream($this->file, 'r+'); + } + + /** + * {@inheritdoc} + * + * @see http://php.net/is_uploaded_file + * @see http://php.net/move_uploaded_file + * @param string $targetPath Path to which to move the uploaded file. + * @throws RuntimeException if the upload was not successful. + * @throws InvalidArgumentException if the $path specified is invalid. + * @throws RuntimeException on any error during the move operation, or on + * the second or subsequent call to the method. + */ + public function moveTo($targetPath) + { + $this->validateActive(); + + if (false === $this->isStringNotEmpty($targetPath)) { + throw new InvalidArgumentException( + 'Invalid path provided for move operation; must be a non-empty string' + ); + } + + if ($this->file) { + $this->moved = php_sapi_name() == 'cli' + ? rename($this->file, $targetPath) + : move_uploaded_file($this->file, $targetPath); + } else { + copy_to_stream( + $this->getStream(), + new LazyOpenStream($targetPath, 'w') + ); + + $this->moved = true; + } + + if (false === $this->moved) { + throw new RuntimeException( + sprintf('Uploaded file could not be moved to %s', $targetPath) + ); + } + } + + /** + * {@inheritdoc} + * + * @return int|null The file size in bytes or null if unknown. + */ + public function getSize() + { + return $this->size; + } + + /** + * {@inheritdoc} + * + * @see http://php.net/manual/en/features.file-upload.errors.php + * @return int One of PHP's UPLOAD_ERR_XXX constants. + */ + public function getError() + { + return $this->error; + } + + /** + * {@inheritdoc} + * + * @return string|null The filename sent by the client or null if none + * was provided. + */ + public function getClientFilename() + { + return $this->clientFilename; + } + + /** + * {@inheritdoc} + */ + public function getClientMediaType() + { + return $this->clientMediaType; + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Uri.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Uri.php new file mode 100644 index 0000000..825a25e --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/Uri.php @@ -0,0 +1,760 @@ + 80, + 'https' => 443, + 'ftp' => 21, + 'gopher' => 70, + 'nntp' => 119, + 'news' => 119, + 'telnet' => 23, + 'tn3270' => 23, + 'imap' => 143, + 'pop' => 110, + 'ldap' => 389, + ]; + + private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; + private static $charSubDelims = '!\$&\'\(\)\*\+,;='; + private static $replaceQuery = ['=' => '%3D', '&' => '%26']; + + /** @var string Uri scheme. */ + private $scheme = ''; + + /** @var string Uri user info. */ + private $userInfo = ''; + + /** @var string Uri host. */ + private $host = ''; + + /** @var int|null Uri port. */ + private $port; + + /** @var string Uri path. */ + private $path = ''; + + /** @var string Uri query string. */ + private $query = ''; + + /** @var string Uri fragment. */ + private $fragment = ''; + + /** + * @param string $uri URI to parse + */ + public function __construct($uri = '') + { + // weak type check to also accept null until we can add scalar type hints + if ($uri != '') { + $parts = parse_url($uri); + if ($parts === false) { + throw new \InvalidArgumentException("Unable to parse URI: $uri"); + } + $this->applyParts($parts); + } + } + + public function __toString() + { + return self::composeComponents( + $this->scheme, + $this->getAuthority(), + $this->path, + $this->query, + $this->fragment + ); + } + + /** + * Composes a URI reference string from its various components. + * + * Usually this method does not need to be called manually but instead is used indirectly via + * `Psr\Http\Message\UriInterface::__toString`. + * + * PSR-7 UriInterface treats an empty component the same as a missing component as + * getQuery(), getFragment() etc. always return a string. This explains the slight + * difference to RFC 3986 Section 5.3. + * + * Another adjustment is that the authority separator is added even when the authority is missing/empty + * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with + * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But + * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to + * that format). + * + * @param string $scheme + * @param string $authority + * @param string $path + * @param string $query + * @param string $fragment + * + * @return string + * + * @link https://tools.ietf.org/html/rfc3986#section-5.3 + */ + public static function composeComponents($scheme, $authority, $path, $query, $fragment) + { + $uri = ''; + + // weak type checks to also accept null until we can add scalar type hints + if ($scheme != '') { + $uri .= $scheme . ':'; + } + + if ($authority != ''|| $scheme === 'file') { + $uri .= '//' . $authority; + } + + $uri .= $path; + + if ($query != '') { + $uri .= '?' . $query; + } + + if ($fragment != '') { + $uri .= '#' . $fragment; + } + + return $uri; + } + + /** + * Whether the URI has the default port of the current scheme. + * + * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used + * independently of the implementation. + * + * @param UriInterface $uri + * + * @return bool + */ + public static function isDefaultPort(UriInterface $uri) + { + return $uri->getPort() === null + || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]); + } + + /** + * Whether the URI is absolute, i.e. it has a scheme. + * + * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true + * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative + * to another URI, the base URI. Relative references can be divided into several forms: + * - network-path references, e.g. '//example.com/path' + * - absolute-path references, e.g. '/path' + * - relative-path references, e.g. 'subpath' + * + * @param UriInterface $uri + * + * @return bool + * @see Uri::isNetworkPathReference + * @see Uri::isAbsolutePathReference + * @see Uri::isRelativePathReference + * @link https://tools.ietf.org/html/rfc3986#section-4 + */ + public static function isAbsolute(UriInterface $uri) + { + return $uri->getScheme() !== ''; + } + + /** + * Whether the URI is a network-path reference. + * + * A relative reference that begins with two slash characters is termed an network-path reference. + * + * @param UriInterface $uri + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isNetworkPathReference(UriInterface $uri) + { + return $uri->getScheme() === '' && $uri->getAuthority() !== ''; + } + + /** + * Whether the URI is a absolute-path reference. + * + * A relative reference that begins with a single slash character is termed an absolute-path reference. + * + * @param UriInterface $uri + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isAbsolutePathReference(UriInterface $uri) + { + return $uri->getScheme() === '' + && $uri->getAuthority() === '' + && isset($uri->getPath()[0]) + && $uri->getPath()[0] === '/'; + } + + /** + * Whether the URI is a relative-path reference. + * + * A relative reference that does not begin with a slash character is termed a relative-path reference. + * + * @param UriInterface $uri + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public static function isRelativePathReference(UriInterface $uri) + { + return $uri->getScheme() === '' + && $uri->getAuthority() === '' + && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); + } + + /** + * Whether the URI is a same-document reference. + * + * A same-document reference refers to a URI that is, aside from its fragment + * component, identical to the base URI. When no base URI is given, only an empty + * URI reference (apart from its fragment) is considered a same-document reference. + * + * @param UriInterface $uri The URI to check + * @param UriInterface|null $base An optional base URI to compare against + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.4 + */ + public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null) + { + if ($base !== null) { + $uri = UriResolver::resolve($base, $uri); + + return ($uri->getScheme() === $base->getScheme()) + && ($uri->getAuthority() === $base->getAuthority()) + && ($uri->getPath() === $base->getPath()) + && ($uri->getQuery() === $base->getQuery()); + } + + return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; + } + + /** + * Removes dot segments from a path and returns the new path. + * + * @param string $path + * + * @return string + * + * @deprecated since version 1.4. Use UriResolver::removeDotSegments instead. + * @see UriResolver::removeDotSegments + */ + public static function removeDotSegments($path) + { + return UriResolver::removeDotSegments($path); + } + + /** + * Converts the relative URI into a new URI that is resolved against the base URI. + * + * @param UriInterface $base Base URI + * @param string|UriInterface $rel Relative URI + * + * @return UriInterface + * + * @deprecated since version 1.4. Use UriResolver::resolve instead. + * @see UriResolver::resolve + */ + public static function resolve(UriInterface $base, $rel) + { + if (!($rel instanceof UriInterface)) { + $rel = new self($rel); + } + + return UriResolver::resolve($base, $rel); + } + + /** + * Creates a new URI with a specific query string value removed. + * + * Any existing query string values that exactly match the provided key are + * removed. + * + * @param UriInterface $uri URI to use as a base. + * @param string $key Query string key to remove. + * + * @return UriInterface + */ + public static function withoutQueryValue(UriInterface $uri, $key) + { + $result = self::getFilteredQueryString($uri, [$key]); + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a new URI with a specific query string value. + * + * Any existing query string values that exactly match the provided key are + * removed and replaced with the given key value pair. + * + * A value of null will set the query string key without a value, e.g. "key" + * instead of "key=value". + * + * @param UriInterface $uri URI to use as a base. + * @param string $key Key to set. + * @param string|null $value Value to set + * + * @return UriInterface + */ + public static function withQueryValue(UriInterface $uri, $key, $value) + { + $result = self::getFilteredQueryString($uri, [$key]); + + $result[] = self::generateQueryString($key, $value); + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a new URI with multiple specific query string values. + * + * It has the same behavior as withQueryValue() but for an associative array of key => value. + * + * @param UriInterface $uri URI to use as a base. + * @param array $keyValueArray Associative array of key and values + * + * @return UriInterface + */ + public static function withQueryValues(UriInterface $uri, array $keyValueArray) + { + $result = self::getFilteredQueryString($uri, array_keys($keyValueArray)); + + foreach ($keyValueArray as $key => $value) { + $result[] = self::generateQueryString($key, $value); + } + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a URI from a hash of `parse_url` components. + * + * @param array $parts + * + * @return UriInterface + * @link http://php.net/manual/en/function.parse-url.php + * + * @throws \InvalidArgumentException If the components do not form a valid URI. + */ + public static function fromParts(array $parts) + { + $uri = new self(); + $uri->applyParts($parts); + $uri->validateState(); + + return $uri; + } + + public function getScheme() + { + return $this->scheme; + } + + public function getAuthority() + { + $authority = $this->host; + if ($this->userInfo !== '') { + $authority = $this->userInfo . '@' . $authority; + } + + if ($this->port !== null) { + $authority .= ':' . $this->port; + } + + return $authority; + } + + public function getUserInfo() + { + return $this->userInfo; + } + + public function getHost() + { + return $this->host; + } + + public function getPort() + { + return $this->port; + } + + public function getPath() + { + return $this->path; + } + + public function getQuery() + { + return $this->query; + } + + public function getFragment() + { + return $this->fragment; + } + + public function withScheme($scheme) + { + $scheme = $this->filterScheme($scheme); + + if ($this->scheme === $scheme) { + return $this; + } + + $new = clone $this; + $new->scheme = $scheme; + $new->removeDefaultPort(); + $new->validateState(); + + return $new; + } + + public function withUserInfo($user, $password = null) + { + $info = $this->filterUserInfoComponent($user); + if ($password !== null) { + $info .= ':' . $this->filterUserInfoComponent($password); + } + + if ($this->userInfo === $info) { + return $this; + } + + $new = clone $this; + $new->userInfo = $info; + $new->validateState(); + + return $new; + } + + public function withHost($host) + { + $host = $this->filterHost($host); + + if ($this->host === $host) { + return $this; + } + + $new = clone $this; + $new->host = $host; + $new->validateState(); + + return $new; + } + + public function withPort($port) + { + $port = $this->filterPort($port); + + if ($this->port === $port) { + return $this; + } + + $new = clone $this; + $new->port = $port; + $new->removeDefaultPort(); + $new->validateState(); + + return $new; + } + + public function withPath($path) + { + $path = $this->filterPath($path); + + if ($this->path === $path) { + return $this; + } + + $new = clone $this; + $new->path = $path; + $new->validateState(); + + return $new; + } + + public function withQuery($query) + { + $query = $this->filterQueryAndFragment($query); + + if ($this->query === $query) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + public function withFragment($fragment) + { + $fragment = $this->filterQueryAndFragment($fragment); + + if ($this->fragment === $fragment) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + /** + * Apply parse_url parts to a URI. + * + * @param array $parts Array of parse_url parts to apply. + */ + private function applyParts(array $parts) + { + $this->scheme = isset($parts['scheme']) + ? $this->filterScheme($parts['scheme']) + : ''; + $this->userInfo = isset($parts['user']) + ? $this->filterUserInfoComponent($parts['user']) + : ''; + $this->host = isset($parts['host']) + ? $this->filterHost($parts['host']) + : ''; + $this->port = isset($parts['port']) + ? $this->filterPort($parts['port']) + : null; + $this->path = isset($parts['path']) + ? $this->filterPath($parts['path']) + : ''; + $this->query = isset($parts['query']) + ? $this->filterQueryAndFragment($parts['query']) + : ''; + $this->fragment = isset($parts['fragment']) + ? $this->filterQueryAndFragment($parts['fragment']) + : ''; + if (isset($parts['pass'])) { + $this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']); + } + + $this->removeDefaultPort(); + } + + /** + * @param string $scheme + * + * @return string + * + * @throws \InvalidArgumentException If the scheme is invalid. + */ + private function filterScheme($scheme) + { + if (!is_string($scheme)) { + throw new \InvalidArgumentException('Scheme must be a string'); + } + + return strtolower($scheme); + } + + /** + * @param string $component + * + * @return string + * + * @throws \InvalidArgumentException If the user info is invalid. + */ + private function filterUserInfoComponent($component) + { + if (!is_string($component)) { + throw new \InvalidArgumentException('User info must be a string'); + } + + return preg_replace_callback( + '/(?:[^%' . self::$charUnreserved . self::$charSubDelims . ']+|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $component + ); + } + + /** + * @param string $host + * + * @return string + * + * @throws \InvalidArgumentException If the host is invalid. + */ + private function filterHost($host) + { + if (!is_string($host)) { + throw new \InvalidArgumentException('Host must be a string'); + } + + return strtolower($host); + } + + /** + * @param int|null $port + * + * @return int|null + * + * @throws \InvalidArgumentException If the port is invalid. + */ + private function filterPort($port) + { + if ($port === null) { + return null; + } + + $port = (int) $port; + if (0 > $port || 0xffff < $port) { + throw new \InvalidArgumentException( + sprintf('Invalid port: %d. Must be between 0 and 65535', $port) + ); + } + + return $port; + } + + /** + * @param UriInterface $uri + * @param array $keys + * + * @return array + */ + private static function getFilteredQueryString(UriInterface $uri, array $keys) + { + $current = $uri->getQuery(); + + if ($current === '') { + return []; + } + + $decodedKeys = array_map('rawurldecode', $keys); + + return array_filter(explode('&', $current), function ($part) use ($decodedKeys) { + return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true); + }); + } + + /** + * @param string $key + * @param string|null $value + * + * @return string + */ + private static function generateQueryString($key, $value) + { + // Query string separators ("=", "&") within the key or value need to be encoded + // (while preventing double-encoding) before setting the query string. All other + // chars that need percent-encoding will be encoded by withQuery(). + $queryString = strtr($key, self::$replaceQuery); + + if ($value !== null) { + $queryString .= '=' . strtr($value, self::$replaceQuery); + } + + return $queryString; + } + + private function removeDefaultPort() + { + if ($this->port !== null && self::isDefaultPort($this)) { + $this->port = null; + } + } + + /** + * Filters the path of a URI + * + * @param string $path + * + * @return string + * + * @throws \InvalidArgumentException If the path is invalid. + */ + private function filterPath($path) + { + if (!is_string($path)) { + throw new \InvalidArgumentException('Path must be a string'); + } + + return preg_replace_callback( + '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $path + ); + } + + /** + * Filters the query string or fragment of a URI. + * + * @param string $str + * + * @return string + * + * @throws \InvalidArgumentException If the query or fragment is invalid. + */ + private function filterQueryAndFragment($str) + { + if (!is_string($str)) { + throw new \InvalidArgumentException('Query and fragment must be a string'); + } + + return preg_replace_callback( + '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $str + ); + } + + private function rawurlencodeMatchZero(array $match) + { + return rawurlencode($match[0]); + } + + private function validateState() + { + if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { + $this->host = self::HTTP_DEFAULT_HOST; + } + + if ($this->getAuthority() === '') { + if (0 === strpos($this->path, '//')) { + throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"'); + } + if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { + throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon'); + } + } elseif (isset($this->path[0]) && $this->path[0] !== '/') { + @trigger_error( + 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' . + 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.', + E_USER_DEPRECATED + ); + $this->path = '/'. $this->path; + //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty'); + } + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UriNormalizer.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UriNormalizer.php new file mode 100644 index 0000000..384c29e --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UriNormalizer.php @@ -0,0 +1,216 @@ +getPath() === '' && + ($uri->getScheme() === 'http' || $uri->getScheme() === 'https') + ) { + $uri = $uri->withPath('/'); + } + + if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') { + $uri = $uri->withHost(''); + } + + if ($flags & self::REMOVE_DEFAULT_PORT && $uri->getPort() !== null && Uri::isDefaultPort($uri)) { + $uri = $uri->withPort(null); + } + + if ($flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri)) { + $uri = $uri->withPath(UriResolver::removeDotSegments($uri->getPath())); + } + + if ($flags & self::REMOVE_DUPLICATE_SLASHES) { + $uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath())); + } + + if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') { + $queryKeyValues = explode('&', $uri->getQuery()); + sort($queryKeyValues); + $uri = $uri->withQuery(implode('&', $queryKeyValues)); + } + + return $uri; + } + + /** + * Whether two URIs can be considered equivalent. + * + * Both URIs are normalized automatically before comparison with the given $normalizations bitmask. The method also + * accepts relative URI references and returns true when they are equivalent. This of course assumes they will be + * resolved against the same base URI. If this is not the case, determination of equivalence or difference of + * relative references does not mean anything. + * + * @param UriInterface $uri1 An URI to compare + * @param UriInterface $uri2 An URI to compare + * @param int $normalizations A bitmask of normalizations to apply, see constants + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-6.1 + */ + public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS) + { + return (string) self::normalize($uri1, $normalizations) === (string) self::normalize($uri2, $normalizations); + } + + private static function capitalizePercentEncoding(UriInterface $uri) + { + $regex = '/(?:%[A-Fa-f0-9]{2})++/'; + + $callback = function (array $match) { + return strtoupper($match[0]); + }; + + return + $uri->withPath( + preg_replace_callback($regex, $callback, $uri->getPath()) + )->withQuery( + preg_replace_callback($regex, $callback, $uri->getQuery()) + ); + } + + private static function decodeUnreservedCharacters(UriInterface $uri) + { + $regex = '/%(?:2D|2E|5F|7E|3[0-9]|[46][1-9A-F]|[57][0-9A])/i'; + + $callback = function (array $match) { + return rawurldecode($match[0]); + }; + + return + $uri->withPath( + preg_replace_callback($regex, $callback, $uri->getPath()) + )->withQuery( + preg_replace_callback($regex, $callback, $uri->getQuery()) + ); + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UriResolver.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UriResolver.php new file mode 100644 index 0000000..c1cb8a2 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/UriResolver.php @@ -0,0 +1,219 @@ +getScheme() != '') { + return $rel->withPath(self::removeDotSegments($rel->getPath())); + } + + if ($rel->getAuthority() != '') { + $targetAuthority = $rel->getAuthority(); + $targetPath = self::removeDotSegments($rel->getPath()); + $targetQuery = $rel->getQuery(); + } else { + $targetAuthority = $base->getAuthority(); + if ($rel->getPath() === '') { + $targetPath = $base->getPath(); + $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); + } else { + if ($rel->getPath()[0] === '/') { + $targetPath = $rel->getPath(); + } else { + if ($targetAuthority != '' && $base->getPath() === '') { + $targetPath = '/' . $rel->getPath(); + } else { + $lastSlashPos = strrpos($base->getPath(), '/'); + if ($lastSlashPos === false) { + $targetPath = $rel->getPath(); + } else { + $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath(); + } + } + } + $targetPath = self::removeDotSegments($targetPath); + $targetQuery = $rel->getQuery(); + } + } + + return new Uri(Uri::composeComponents( + $base->getScheme(), + $targetAuthority, + $targetPath, + $targetQuery, + $rel->getFragment() + )); + } + + /** + * Returns the target URI as a relative reference from the base URI. + * + * This method is the counterpart to resolve(): + * + * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) + * + * One use-case is to use the current request URI as base URI and then generate relative links in your documents + * to reduce the document size or offer self-contained downloadable document archives. + * + * $base = new Uri('http://example.com/a/b/'); + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. + * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. + * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. + * + * This method also accepts a target that is already relative and will try to relativize it further. Only a + * relative-path reference will be returned as-is. + * + * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well + * + * @param UriInterface $base Base URI + * @param UriInterface $target Target URI + * + * @return UriInterface The relative URI reference + */ + public static function relativize(UriInterface $base, UriInterface $target) + { + if ($target->getScheme() !== '' && + ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') + ) { + return $target; + } + + if (Uri::isRelativePathReference($target)) { + // As the target is already highly relative we return it as-is. It would be possible to resolve + // the target with `$target = self::resolve($base, $target);` and then try make it more relative + // by removing a duplicate query. But let's not do that automatically. + return $target; + } + + if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { + return $target->withScheme(''); + } + + // We must remove the path before removing the authority because if the path starts with two slashes, the URI + // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also + // invalid. + $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); + + if ($base->getPath() !== $target->getPath()) { + return $emptyPathUri->withPath(self::getRelativePath($base, $target)); + } + + if ($base->getQuery() === $target->getQuery()) { + // Only the target fragment is left. And it must be returned even if base and target fragment are the same. + return $emptyPathUri->withQuery(''); + } + + // If the base URI has a query but the target has none, we cannot return an empty path reference as it would + // inherit the base query component when resolving. + if ($target->getQuery() === '') { + $segments = explode('/', $target->getPath()); + $lastSegment = end($segments); + + return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); + } + + return $emptyPathUri; + } + + private static function getRelativePath(UriInterface $base, UriInterface $target) + { + $sourceSegments = explode('/', $base->getPath()); + $targetSegments = explode('/', $target->getPath()); + array_pop($sourceSegments); + $targetLastSegment = array_pop($targetSegments); + foreach ($sourceSegments as $i => $segment) { + if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { + unset($sourceSegments[$i], $targetSegments[$i]); + } else { + break; + } + } + $targetSegments[] = $targetLastSegment; + $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments); + + // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". + // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used + // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. + if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) { + $relativePath = "./$relativePath"; + } elseif ('/' === $relativePath[0]) { + if ($base->getAuthority() != '' && $base->getPath() === '') { + // In this case an extra slash is added by resolve() automatically. So we must not add one here. + $relativePath = ".$relativePath"; + } else { + $relativePath = "./$relativePath"; + } + } + + return $relativePath; + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/functions.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/functions.php new file mode 100644 index 0000000..8e6dafe --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/functions.php @@ -0,0 +1,899 @@ +getMethod() . ' ' + . $message->getRequestTarget()) + . ' HTTP/' . $message->getProtocolVersion(); + if (!$message->hasHeader('host')) { + $msg .= "\r\nHost: " . $message->getUri()->getHost(); + } + } elseif ($message instanceof ResponseInterface) { + $msg = 'HTTP/' . $message->getProtocolVersion() . ' ' + . $message->getStatusCode() . ' ' + . $message->getReasonPhrase(); + } else { + throw new \InvalidArgumentException('Unknown message type'); + } + + foreach ($message->getHeaders() as $name => $values) { + $msg .= "\r\n{$name}: " . implode(', ', $values); + } + + return "{$msg}\r\n\r\n" . $message->getBody(); +} + +/** + * Returns a UriInterface for the given value. + * + * This function accepts a string or {@see Psr\Http\Message\UriInterface} and + * returns a UriInterface for the given value. If the value is already a + * `UriInterface`, it is returned as-is. + * + * @param string|UriInterface $uri + * + * @return UriInterface + * @throws \InvalidArgumentException + */ +function uri_for($uri) +{ + if ($uri instanceof UriInterface) { + return $uri; + } elseif (is_string($uri)) { + return new Uri($uri); + } + + throw new \InvalidArgumentException('URI must be a string or UriInterface'); +} + +/** + * Create a new stream based on the input type. + * + * Options is an associative array that can contain the following keys: + * - metadata: Array of custom metadata. + * - size: Size of the stream. + * + * @param resource|string|null|int|float|bool|StreamInterface|callable|\Iterator $resource Entity body data + * @param array $options Additional options + * + * @return StreamInterface + * @throws \InvalidArgumentException if the $resource arg is not valid. + */ +function stream_for($resource = '', array $options = []) +{ + if (is_scalar($resource)) { + $stream = fopen('php://temp', 'r+'); + if ($resource !== '') { + fwrite($stream, $resource); + fseek($stream, 0); + } + return new Stream($stream, $options); + } + + switch (gettype($resource)) { + case 'resource': + return new Stream($resource, $options); + case 'object': + if ($resource instanceof StreamInterface) { + return $resource; + } elseif ($resource instanceof \Iterator) { + return new PumpStream(function () use ($resource) { + if (!$resource->valid()) { + return false; + } + $result = $resource->current(); + $resource->next(); + return $result; + }, $options); + } elseif (method_exists($resource, '__toString')) { + return stream_for((string) $resource, $options); + } + break; + case 'NULL': + return new Stream(fopen('php://temp', 'r+'), $options); + } + + if (is_callable($resource)) { + return new PumpStream($resource, $options); + } + + throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource)); +} + +/** + * Parse an array of header values containing ";" separated data into an + * array of associative arrays representing the header key value pair + * data of the header. When a parameter does not contain a value, but just + * contains a key, this function will inject a key with a '' string value. + * + * @param string|array $header Header to parse into components. + * + * @return array Returns the parsed header values. + */ +function parse_header($header) +{ + static $trimmed = "\"' \n\t\r"; + $params = $matches = []; + + foreach (normalize_header($header) as $val) { + $part = []; + foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) { + if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) { + $m = $matches[0]; + if (isset($m[1])) { + $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); + } else { + $part[] = trim($m[0], $trimmed); + } + } + } + if ($part) { + $params[] = $part; + } + } + + return $params; +} + +/** + * Converts an array of header values that may contain comma separated + * headers into an array of headers with no comma separated values. + * + * @param string|array $header Header to normalize. + * + * @return array Returns the normalized header field values. + */ +function normalize_header($header) +{ + if (!is_array($header)) { + return array_map('trim', explode(',', $header)); + } + + $result = []; + foreach ($header as $value) { + foreach ((array) $value as $v) { + if (strpos($v, ',') === false) { + $result[] = $v; + continue; + } + foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $v) as $vv) { + $result[] = trim($vv); + } + } + } + + return $result; +} + +/** + * Clone and modify a request with the given changes. + * + * The changes can be one of: + * - method: (string) Changes the HTTP method. + * - set_headers: (array) Sets the given headers. + * - remove_headers: (array) Remove the given headers. + * - body: (mixed) Sets the given body. + * - uri: (UriInterface) Set the URI. + * - query: (string) Set the query string value of the URI. + * - version: (string) Set the protocol version. + * + * @param RequestInterface $request Request to clone and modify. + * @param array $changes Changes to apply. + * + * @return RequestInterface + */ +function modify_request(RequestInterface $request, array $changes) +{ + if (!$changes) { + return $request; + } + + $headers = $request->getHeaders(); + + if (!isset($changes['uri'])) { + $uri = $request->getUri(); + } else { + // Remove the host header if one is on the URI + if ($host = $changes['uri']->getHost()) { + $changes['set_headers']['Host'] = $host; + + if ($port = $changes['uri']->getPort()) { + $standardPorts = ['http' => 80, 'https' => 443]; + $scheme = $changes['uri']->getScheme(); + if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { + $changes['set_headers']['Host'] .= ':'.$port; + } + } + } + $uri = $changes['uri']; + } + + if (!empty($changes['remove_headers'])) { + $headers = _caseless_remove($changes['remove_headers'], $headers); + } + + if (!empty($changes['set_headers'])) { + $headers = _caseless_remove(array_keys($changes['set_headers']), $headers); + $headers = $changes['set_headers'] + $headers; + } + + if (isset($changes['query'])) { + $uri = $uri->withQuery($changes['query']); + } + + if ($request instanceof ServerRequestInterface) { + return (new ServerRequest( + isset($changes['method']) ? $changes['method'] : $request->getMethod(), + $uri, + $headers, + isset($changes['body']) ? $changes['body'] : $request->getBody(), + isset($changes['version']) + ? $changes['version'] + : $request->getProtocolVersion(), + $request->getServerParams() + )) + ->withParsedBody($request->getParsedBody()) + ->withQueryParams($request->getQueryParams()) + ->withCookieParams($request->getCookieParams()) + ->withUploadedFiles($request->getUploadedFiles()); + } + + return new Request( + isset($changes['method']) ? $changes['method'] : $request->getMethod(), + $uri, + $headers, + isset($changes['body']) ? $changes['body'] : $request->getBody(), + isset($changes['version']) + ? $changes['version'] + : $request->getProtocolVersion() + ); +} + +/** + * Attempts to rewind a message body and throws an exception on failure. + * + * The body of the message will only be rewound if a call to `tell()` returns a + * value other than `0`. + * + * @param MessageInterface $message Message to rewind + * + * @throws \RuntimeException + */ +function rewind_body(MessageInterface $message) +{ + $body = $message->getBody(); + + if ($body->tell()) { + $body->rewind(); + } +} + +/** + * Safely opens a PHP stream resource using a filename. + * + * When fopen fails, PHP normally raises a warning. This function adds an + * error handler that checks for errors and throws an exception instead. + * + * @param string $filename File to open + * @param string $mode Mode used to open the file + * + * @return resource + * @throws \RuntimeException if the file cannot be opened + */ +function try_fopen($filename, $mode) +{ + $ex = null; + set_error_handler(function () use ($filename, $mode, &$ex) { + $ex = new \RuntimeException(sprintf( + 'Unable to open %s using mode %s: %s', + $filename, + $mode, + func_get_args()[1] + )); + }); + + $handle = fopen($filename, $mode); + restore_error_handler(); + + if ($ex) { + /** @var $ex \RuntimeException */ + throw $ex; + } + + return $handle; +} + +/** + * Copy the contents of a stream into a string until the given number of + * bytes have been read. + * + * @param StreamInterface $stream Stream to read + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + * @return string + * @throws \RuntimeException on error. + */ +function copy_to_string(StreamInterface $stream, $maxLen = -1) +{ + $buffer = ''; + + if ($maxLen === -1) { + while (!$stream->eof()) { + $buf = $stream->read(1048576); + // Using a loose equality here to match on '' and false. + if ($buf == null) { + break; + } + $buffer .= $buf; + } + return $buffer; + } + + $len = 0; + while (!$stream->eof() && $len < $maxLen) { + $buf = $stream->read($maxLen - $len); + // Using a loose equality here to match on '' and false. + if ($buf == null) { + break; + } + $buffer .= $buf; + $len = strlen($buffer); + } + + return $buffer; +} + +/** + * Copy the contents of a stream into another stream until the given number + * of bytes have been read. + * + * @param StreamInterface $source Stream to read from + * @param StreamInterface $dest Stream to write to + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + * + * @throws \RuntimeException on error. + */ +function copy_to_stream( + StreamInterface $source, + StreamInterface $dest, + $maxLen = -1 +) { + $bufferSize = 8192; + + if ($maxLen === -1) { + while (!$source->eof()) { + if (!$dest->write($source->read($bufferSize))) { + break; + } + } + } else { + $remaining = $maxLen; + while ($remaining > 0 && !$source->eof()) { + $buf = $source->read(min($bufferSize, $remaining)); + $len = strlen($buf); + if (!$len) { + break; + } + $remaining -= $len; + $dest->write($buf); + } + } +} + +/** + * Calculate a hash of a Stream + * + * @param StreamInterface $stream Stream to calculate the hash for + * @param string $algo Hash algorithm (e.g. md5, crc32, etc) + * @param bool $rawOutput Whether or not to use raw output + * + * @return string Returns the hash of the stream + * @throws \RuntimeException on error. + */ +function hash( + StreamInterface $stream, + $algo, + $rawOutput = false +) { + $pos = $stream->tell(); + + if ($pos > 0) { + $stream->rewind(); + } + + $ctx = hash_init($algo); + while (!$stream->eof()) { + hash_update($ctx, $stream->read(1048576)); + } + + $out = hash_final($ctx, (bool) $rawOutput); + $stream->seek($pos); + + return $out; +} + +/** + * Read a line from the stream up to the maximum allowed buffer length + * + * @param StreamInterface $stream Stream to read from + * @param int $maxLength Maximum buffer length + * + * @return string + */ +function readline(StreamInterface $stream, $maxLength = null) +{ + $buffer = ''; + $size = 0; + + while (!$stream->eof()) { + // Using a loose equality here to match on '' and false. + if (null == ($byte = $stream->read(1))) { + return $buffer; + } + $buffer .= $byte; + // Break when a new line is found or the max length - 1 is reached + if ($byte === "\n" || ++$size === $maxLength - 1) { + break; + } + } + + return $buffer; +} + +/** + * Parses a request message string into a request object. + * + * @param string $message Request message string. + * + * @return Request + */ +function parse_request($message) +{ + $data = _parse_message($message); + $matches = []; + if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { + throw new \InvalidArgumentException('Invalid request string'); + } + $parts = explode(' ', $data['start-line'], 3); + $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; + + $request = new Request( + $parts[0], + $matches[1] === '/' ? _parse_request_uri($parts[1], $data['headers']) : $parts[1], + $data['headers'], + $data['body'], + $version + ); + + return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); +} + +/** + * Parses a response message string into a response object. + * + * @param string $message Response message string. + * + * @return Response + */ +function parse_response($message) +{ + $data = _parse_message($message); + // According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space + // between status-code and reason-phrase is required. But browsers accept + // responses without space and reason as well. + if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { + throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']); + } + $parts = explode(' ', $data['start-line'], 3); + + return new Response( + $parts[1], + $data['headers'], + $data['body'], + explode('/', $parts[0])[1], + isset($parts[2]) ? $parts[2] : null + ); +} + +/** + * Parse a query string into an associative array. + * + * If multiple values are found for the same key, the value of that key + * value pair will become an array. This function does not parse nested + * PHP style arrays into an associative array (e.g., foo[a]=1&foo[b]=2 will + * be parsed into ['foo[a]' => '1', 'foo[b]' => '2']). + * + * @param string $str Query string to parse + * @param int|bool $urlEncoding How the query string is encoded + * + * @return array + */ +function parse_query($str, $urlEncoding = true) +{ + $result = []; + + if ($str === '') { + return $result; + } + + if ($urlEncoding === true) { + $decoder = function ($value) { + return rawurldecode(str_replace('+', ' ', $value)); + }; + } elseif ($urlEncoding === PHP_QUERY_RFC3986) { + $decoder = 'rawurldecode'; + } elseif ($urlEncoding === PHP_QUERY_RFC1738) { + $decoder = 'urldecode'; + } else { + $decoder = function ($str) { return $str; }; + } + + foreach (explode('&', $str) as $kvp) { + $parts = explode('=', $kvp, 2); + $key = $decoder($parts[0]); + $value = isset($parts[1]) ? $decoder($parts[1]) : null; + if (!isset($result[$key])) { + $result[$key] = $value; + } else { + if (!is_array($result[$key])) { + $result[$key] = [$result[$key]]; + } + $result[$key][] = $value; + } + } + + return $result; +} + +/** + * Build a query string from an array of key value pairs. + * + * This function can use the return value of parse_query() to build a query + * string. This function does not modify the provided keys when an array is + * encountered (like http_build_query would). + * + * @param array $params Query string parameters. + * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 + * to encode using RFC3986, or PHP_QUERY_RFC1738 + * to encode using RFC1738. + * @return string + */ +function build_query(array $params, $encoding = PHP_QUERY_RFC3986) +{ + if (!$params) { + return ''; + } + + if ($encoding === false) { + $encoder = function ($str) { return $str; }; + } elseif ($encoding === PHP_QUERY_RFC3986) { + $encoder = 'rawurlencode'; + } elseif ($encoding === PHP_QUERY_RFC1738) { + $encoder = 'urlencode'; + } else { + throw new \InvalidArgumentException('Invalid type'); + } + + $qs = ''; + foreach ($params as $k => $v) { + $k = $encoder($k); + if (!is_array($v)) { + $qs .= $k; + if ($v !== null) { + $qs .= '=' . $encoder($v); + } + $qs .= '&'; + } else { + foreach ($v as $vv) { + $qs .= $k; + if ($vv !== null) { + $qs .= '=' . $encoder($vv); + } + $qs .= '&'; + } + } + } + + return $qs ? (string) substr($qs, 0, -1) : ''; +} + +/** + * Determines the mimetype of a file by looking at its extension. + * + * @param $filename + * + * @return null|string + */ +function mimetype_from_filename($filename) +{ + return mimetype_from_extension(pathinfo($filename, PATHINFO_EXTENSION)); +} + +/** + * Maps a file extensions to a mimetype. + * + * @param $extension string The file extension. + * + * @return string|null + * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types + */ +function mimetype_from_extension($extension) +{ + static $mimetypes = [ + '3gp' => 'video/3gpp', + '7z' => 'application/x-7z-compressed', + 'aac' => 'audio/x-aac', + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'asc' => 'text/plain', + 'asf' => 'video/x-ms-asf', + 'atom' => 'application/atom+xml', + 'avi' => 'video/x-msvideo', + 'bmp' => 'image/bmp', + 'bz2' => 'application/x-bzip2', + 'cer' => 'application/pkix-cert', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'cu' => 'application/cu-seeme', + 'deb' => 'application/x-debian-package', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dvi' => 'application/x-dvi', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'etx' => 'text/x-setext', + 'flac' => 'audio/flac', + 'flv' => 'video/x-flv', + 'gif' => 'image/gif', + 'gz' => 'application/gzip', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ini' => 'text/plain', + 'iso' => 'application/x-iso9660-image', + 'jar' => 'application/java-archive', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'text/javascript', + 'json' => 'application/json', + 'latex' => 'application/x-latex', + 'log' => 'text/plain', + 'm4a' => 'audio/mp4', + 'm4v' => 'video/mp4', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mov' => 'video/quicktime', + 'mkv' => 'video/x-matroska', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mp4a' => 'audio/mp4', + 'mp4v' => 'video/mp4', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'oga' => 'audio/ogg', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'pbm' => 'image/x-portable-bitmap', + 'pdf' => 'application/pdf', + 'pgm' => 'image/x-portable-graymap', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'ppm' => 'image/x-portable-pixmap', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ps' => 'application/postscript', + 'qt' => 'video/quicktime', + 'rar' => 'application/x-rar-compressed', + 'ras' => 'image/x-cmu-raster', + 'rss' => 'application/rss+xml', + 'rtf' => 'application/rtf', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'svg' => 'image/svg+xml', + 'swf' => 'application/x-shockwave-flash', + 'tar' => 'application/x-tar', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'torrent' => 'application/x-bittorrent', + 'ttf' => 'application/x-font-ttf', + 'txt' => 'text/plain', + 'wav' => 'audio/x-wav', + 'webm' => 'video/webm', + 'webp' => 'image/webp', + 'wma' => 'audio/x-ms-wma', + 'wmv' => 'video/x-ms-wmv', + 'woff' => 'application/x-font-woff', + 'wsdl' => 'application/wsdl+xml', + 'xbm' => 'image/x-xbitmap', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xml' => 'application/xml', + 'xpm' => 'image/x-xpixmap', + 'xwd' => 'image/x-xwindowdump', + 'yaml' => 'text/yaml', + 'yml' => 'text/yaml', + 'zip' => 'application/zip', + ]; + + $extension = strtolower($extension); + + return isset($mimetypes[$extension]) + ? $mimetypes[$extension] + : null; +} + +/** + * Parses an HTTP message into an associative array. + * + * The array contains the "start-line" key containing the start line of + * the message, "headers" key containing an associative array of header + * array values, and a "body" key containing the body of the message. + * + * @param string $message HTTP request or response to parse. + * + * @return array + * @internal + */ +function _parse_message($message) +{ + if (!$message) { + throw new \InvalidArgumentException('Invalid message'); + } + + $message = ltrim($message, "\r\n"); + + $messageParts = preg_split("/\r?\n\r?\n/", $message, 2); + + if ($messageParts === false || count($messageParts) !== 2) { + throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); + } + + list($rawHeaders, $body) = $messageParts; + $rawHeaders .= "\r\n"; // Put back the delimiter we split previously + $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); + + if ($headerParts === false || count($headerParts) !== 2) { + throw new \InvalidArgumentException('Invalid message: Missing status line'); + } + + list($startLine, $rawHeaders) = $headerParts; + + if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { + // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 + $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); + } + + /** @var array[] $headerLines */ + $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); + + // If these aren't the same, then one line didn't match and there's an invalid header. + if ($count !== substr_count($rawHeaders, "\n")) { + // Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4 + if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { + throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); + } + + throw new \InvalidArgumentException('Invalid header syntax'); + } + + $headers = []; + + foreach ($headerLines as $headerLine) { + $headers[$headerLine[1]][] = $headerLine[2]; + } + + return [ + 'start-line' => $startLine, + 'headers' => $headers, + 'body' => $body, + ]; +} + +/** + * Constructs a URI for an HTTP request message. + * + * @param string $path Path from the start-line + * @param array $headers Array of headers (each value an array). + * + * @return string + * @internal + */ +function _parse_request_uri($path, array $headers) +{ + $hostKey = array_filter(array_keys($headers), function ($k) { + return strtolower($k) === 'host'; + }); + + // If no host is found, then a full URI cannot be constructed. + if (!$hostKey) { + return $path; + } + + $host = $headers[reset($hostKey)][0]; + $scheme = substr($host, -4) === ':443' ? 'https' : 'http'; + + return $scheme . '://' . $host . '/' . ltrim($path, '/'); +} + +/** + * Get a short summary of the message body + * + * Will return `null` if the response is not printable. + * + * @param MessageInterface $message The message to get the body summary + * @param int $truncateAt The maximum allowed size of the summary + * + * @return null|string + */ +function get_message_body_summary(MessageInterface $message, $truncateAt = 120) +{ + $body = $message->getBody(); + + if (!$body->isSeekable() || !$body->isReadable()) { + return null; + } + + $size = $body->getSize(); + + if ($size === 0) { + return null; + } + + $summary = $body->read($truncateAt); + $body->rewind(); + + if ($size > $truncateAt) { + $summary .= ' (truncated...)'; + } + + // Matches any printable character, including unicode characters: + // letters, marks, numbers, punctuation, spacing, and separators. + if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/', $summary)) { + return null; + } + + return $summary; +} + +/** @internal */ +function _caseless_remove($keys, array $data) +{ + $result = []; + + foreach ($keys as &$key) { + $key = strtolower($key); + } + + foreach ($data as $k => $v) { + if (!in_array(strtolower($k), $keys)) { + $result[$k] = $v; + } + } + + return $result; +} diff --git a/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/functions_include.php b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/functions_include.php new file mode 100644 index 0000000..96a4a83 --- /dev/null +++ b/plugins/panakour/backup/vendor/guzzlehttp/psr7/src/functions_include.php @@ -0,0 +1,6 @@ + [int]) + + +## 1.0.0 + +Initial release diff --git a/plugins/panakour/backup/vendor/league/flysystem-webdav/composer.json b/plugins/panakour/backup/vendor/league/flysystem-webdav/composer.json new file mode 100644 index 0000000..64d4c38 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem-webdav/composer.json @@ -0,0 +1,33 @@ +{ + "name": "league/flysystem-webdav", + "description": "Flysystem adapter for WebDAV", + "license": "MIT", + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "require": { + "php": ">=5.6", + "league/flysystem": "~1.0", + "sabre/dav": "~4.0|~3.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.8", + "mockery/mockery": "~1.2" + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\WebDAV\\": "src" + } + }, + "config": { + "bin-dir": "bin" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem-webdav/src/WebDAVAdapter.php b/plugins/panakour/backup/vendor/league/flysystem-webdav/src/WebDAVAdapter.php new file mode 100644 index 0000000..c6a8eed --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem-webdav/src/WebDAVAdapter.php @@ -0,0 +1,419 @@ + 'size', + '{DAV:}getcontenttype' => 'mimetype', + 'content-length' => 'size', + 'content-type' => 'mimetype', + ]; + + /** + * @var Client + */ + protected $client; + + /** + * @var bool + */ + protected $useStreamedCopy = true; + + /** + * Constructor. + * + * @param Client $client + * @param string $prefix + * @param bool $useStreamedCopy + */ + public function __construct(Client $client, $prefix = null, $useStreamedCopy = true) + { + $this->client = $client; + $this->setPathPrefix($prefix); + $this->setUseStreamedCopy($useStreamedCopy); + } + + /** + * url encode a path + * + * @param string $path + * + * @return string + */ + protected function encodePath($path) + { + $a = explode('/', $path); + for ($i=0; $iapplyPathPrefix($this->encodePath($path)); + + try { + $result = $this->client->propFind($location, static::$metadataFields); + + if (empty($result)) { + return false; + } + + return $this->normalizeObject($result, $path); + } catch (Exception $e) { + return false; + } catch (HttpException $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function has($path) + { + return $this->getMetadata($path); + } + + /** + * {@inheritdoc} + */ + public function read($path) + { + $location = $this->applyPathPrefix($this->encodePath($path)); + + try { + $response = $this->client->request('GET', $location); + + if ($response['statusCode'] !== 200) { + return false; + } + + return array_merge([ + 'contents' => $response['body'], + 'timestamp' => strtotime(is_array($response['headers']['last-modified']) + ? current($response['headers']['last-modified']) + : $response['headers']['last-modified']), + 'path' => $path, + ], Util::map($response['headers'], static::$resultMap)); + } catch (Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function write($path, $contents, Config $config) + { + if (!$this->createDir(Util::dirname($path), $config)) { + return false; + } + + $location = $this->applyPathPrefix($this->encodePath($path)); + $response = $this->client->request('PUT', $location, $contents); + + if ($response['statusCode'] >= 400) { + return false; + } + + $result = compact('path', 'contents'); + + if ($config->get('visibility')) { + throw new LogicException(__CLASS__.' does not support visibility settings.'); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function writeStream($path, $resource, Config $config) + { + return $this->write($path, $resource, $config); + } + + /** + * {@inheritdoc} + */ + public function update($path, $contents, Config $config) + { + return $this->write($path, $contents, $config); + } + + /** + * {@inheritdoc} + */ + public function updateStream($path, $resource, Config $config) + { + return $this->update($path, $resource, $config); + } + + /** + * {@inheritdoc} + */ + public function rename($path, $newpath) + { + $location = $this->applyPathPrefix($this->encodePath($path)); + $newLocation = $this->applyPathPrefix($this->encodePath($newpath)); + + try { + $response = $this->client->request('MOVE', '/'.ltrim($location, '/'), null, [ + 'Destination' => '/'.ltrim($newLocation, '/'), + ]); + + if ($response['statusCode'] >= 200 && $response['statusCode'] < 300) { + return true; + } + } catch (NotFound $e) { + // Would have returned false here, but would be redundant + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function copy($path, $newpath) + { + if ($this->useStreamedCopy === true) { + return $this->streamedCopy($path, $newpath); + } else { + return $this->nativeCopy($path, $newpath); + } + } + + /** + * {@inheritdoc} + */ + public function delete($path) + { + $location = $this->applyPathPrefix($this->encodePath($path)); + + try { + $response = $this->client->request('DELETE', $location)['statusCode']; + + + return $response >= 200 && $response < 300; + } catch (NotFound $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function createDir($path, Config $config) + { + $encodedPath = $this->encodePath($path); + $path = trim($path, '/'); + + $result = compact('path') + ['type' => 'dir']; + + if (Util::normalizeDirname($path) === '' || $this->has($path)) { + return $result; + } + + $directories = explode('/', $path); + if (count($directories) > 1) { + $parentDirectories = array_splice($directories, 0, count($directories) - 1); + if (!$this->createDir(implode('/', $parentDirectories), $config)) { + return false; + } + } + + $location = $this->applyPathPrefix($encodedPath); + $response = $this->client->request('MKCOL', $location . $this->pathSeparator); + + if ($response['statusCode'] !== 201) { + return false; + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function deleteDir($dirname) + { + return $this->delete($dirname); + } + + /** + * {@inheritdoc} + */ + public function listContents($directory = '', $recursive = false) + { + $location = $this->applyPathPrefix($this->encodePath($directory)); + $response = $this->client->propFind($location . '/', static::$metadataFields, 1); + + array_shift($response); + $result = []; + + foreach ($response as $path => $object) { + $path = $this->removePathPrefix(rawurldecode($path)); + $object = $this->normalizeObject($object, $path); + $result[] = $object; + + if ($recursive && $object['type'] === 'dir') { + $result = array_merge($result, $this->listContents($object['path'], true)); + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getSize($path) + { + return $this->getMetadata($path); + } + + /** + * {@inheritdoc} + */ + public function getTimestamp($path) + { + return $this->getMetadata($path); + } + + /** + * {@inheritdoc} + */ + public function getMimetype($path) + { + return $this->getMetadata($path); + } + + /** + * @return boolean + */ + public function getUseStreamedCopy() + { + return $this->useStreamedCopy; + } + + /** + * @param boolean $useStreamedCopy + */ + public function setUseStreamedCopy($useStreamedCopy) + { + $this->useStreamedCopy = (bool)$useStreamedCopy; + } + + /** + * Copy a file through WebDav COPY method. + * + * @param string $path + * @param string $newPath + * + * @return bool + */ + protected function nativeCopy($path, $newPath) + { + if (!$this->createDir(Util::dirname($newPath), new Config())) { + return false; + } + + $location = $this->applyPathPrefix($this->encodePath($path)); + $newLocation = $this->applyPathPrefix($this->encodePath($newPath)); + + try { + $destination = $this->client->getAbsoluteUrl($newLocation); + $response = $this->client->request('COPY', '/'.ltrim($location, '/'), null, [ + 'Destination' => $destination, + ]); + + if ($response['statusCode'] >= 200 && $response['statusCode'] < 300) { + return true; + } + } catch (NotFound $e) { + // Would have returned false here, but would be redundant + } + + return false; + } + + /** + * Normalise a WebDAV repsonse object. + * + * @param array $object + * @param string $path + * + * @return array + */ + protected function normalizeObject(array $object, $path) + { + if ($this->isDirectory($object)) { + return ['type' => 'dir', 'path' => trim($path, '/')]; + } + + $result = Util::map($object, static::$resultMap); + + if (isset($object['{DAV:}getlastmodified'])) { + $result['timestamp'] = strtotime($object['{DAV:}getlastmodified']); + } + + $result['type'] = 'file'; + $result['path'] = trim($path, '/'); + + return $result; + } + + /** + * @param array $object + * @return bool + */ + protected function isDirectory(array $object) + { + if (isset($object['{DAV:}resourcetype'])) { + /** @var ResourceType $resourceType */ + $resourceType = $object['{DAV:}resourcetype']; + return $resourceType->is('{DAV:}collection'); + } + + return isset($object['{DAV:}iscollection']) && $object['{DAV:}iscollection'] === '1'; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/LICENSE b/plugins/panakour/backup/vendor/league/flysystem/LICENSE new file mode 100644 index 0000000..f2684c8 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013-2019 Frank de Jonge + +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/plugins/panakour/backup/vendor/league/flysystem/SECURITY.md b/plugins/panakour/backup/vendor/league/flysystem/SECURITY.md new file mode 100644 index 0000000..f5b205e --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 1.0.x | :white_check_mark: | +| 2.0.x | :x: | + +## Reporting a Vulnerability + +When you've encountered a security vulnerability, please disclose it securely. + +The security process is described at: +[https://flysystem.thephpleague.com/docs/security/](https://flysystem.thephpleague.com/docs/security/) + diff --git a/plugins/panakour/backup/vendor/league/flysystem/composer.json b/plugins/panakour/backup/vendor/league/flysystem/composer.json new file mode 100644 index 0000000..51169de --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/composer.json @@ -0,0 +1,71 @@ +{ + "name": "league/flysystem", + "type": "library", + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "filesystem", "filesystems", "files", "storage", "dropbox", "aws", + "abstraction", "s3", "ftp", "sftp", "remote", "webdav", + "file systems", "cloud", "cloud files", "rackspace", "copy.com" + ], + "funding": [ + { + "type": "other", + "url": "https://offset.earth/frankdejonge" + } + ], + "license": "MIT", + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "require": { + "php": ">=5.5.9", + "ext-fileinfo": "*" + }, + "require-dev": { + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7.26" + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "League\\Flysystem\\Stub\\": "stub/" + }, + "files": [ + "tests/PHPUnitHacks.php" + ] + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "scripts": { + "phpstan": "php phpstan.php" + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/deprecations.md b/plugins/panakour/backup/vendor/league/flysystem/deprecations.md new file mode 100644 index 0000000..c336a42 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/deprecations.md @@ -0,0 +1,19 @@ +# Deprecations + +This document lists all the planned deprecations. + +## Handlers will be removed in 2.0 + +The `Handler` type and associated calls will be removed in version 2.0. + +### Upgrade path + +You should create your own implementation for handling OOP usage, +but it's recommended to move away from using an OOP-style wrapper entirely. + +The reason for this is that it's too easy for implementation details (for +your application this is Flysystem) to leak into the application. The most +important part for Flysystem is that it improves portability and creates a +solid boundary between your application core and the infrastructure you use. +The OOP-style handling breaks this principle, therefore I want to stop +promoting it. diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/AbstractAdapter.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/AbstractAdapter.php new file mode 100644 index 0000000..e577ac4 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/AbstractAdapter.php @@ -0,0 +1,72 @@ +pathPrefix = null; + + return; + } + + $this->pathPrefix = rtrim($prefix, '\\/') . $this->pathSeparator; + } + + /** + * Get the path prefix. + * + * @return string|null path prefix or null if pathPrefix is empty + */ + public function getPathPrefix() + { + return $this->pathPrefix; + } + + /** + * Prefix a path. + * + * @param string $path + * + * @return string prefixed path + */ + public function applyPathPrefix($path) + { + return $this->getPathPrefix() . ltrim($path, '\\/'); + } + + /** + * Remove a path prefix. + * + * @param string $path + * + * @return string path without the prefix + */ + public function removePathPrefix($path) + { + return substr($path, strlen($this->getPathPrefix())); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/AbstractFtpAdapter.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/AbstractFtpAdapter.php new file mode 100644 index 0000000..95a6b4d --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/AbstractFtpAdapter.php @@ -0,0 +1,697 @@ +safeStorage = new SafeStorage(); + $this->setConfig($config); + } + + /** + * Set the config. + * + * @param array $config + * + * @return $this + */ + public function setConfig(array $config) + { + foreach ($this->configurable as $setting) { + if ( ! isset($config[$setting])) { + continue; + } + + $method = 'set' . ucfirst($setting); + + if (method_exists($this, $method)) { + $this->$method($config[$setting]); + } + } + + return $this; + } + + /** + * Returns the host. + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Set the host. + * + * @param string $host + * + * @return $this + */ + public function setHost($host) + { + $this->host = $host; + + return $this; + } + + /** + * Set the public permission value. + * + * @param int $permPublic + * + * @return $this + */ + public function setPermPublic($permPublic) + { + $this->permPublic = $permPublic; + + return $this; + } + + /** + * Set the private permission value. + * + * @param int $permPrivate + * + * @return $this + */ + public function setPermPrivate($permPrivate) + { + $this->permPrivate = $permPrivate; + + return $this; + } + + /** + * Returns the ftp port. + * + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * Returns the root folder to work from. + * + * @return string + */ + public function getRoot() + { + return $this->root; + } + + /** + * Set the ftp port. + * + * @param int|string $port + * + * @return $this + */ + public function setPort($port) + { + $this->port = (int) $port; + + return $this; + } + + /** + * Set the root folder to work from. + * + * @param string $root + * + * @return $this + */ + public function setRoot($root) + { + $this->root = rtrim($root, '\\/') . $this->separator; + + return $this; + } + + /** + * Returns the ftp username. + * + * @return string username + */ + public function getUsername() + { + $username = $this->safeStorage->retrieveSafely('username'); + + return $username !== null ? $username : 'anonymous'; + } + + /** + * Set ftp username. + * + * @param string $username + * + * @return $this + */ + public function setUsername($username) + { + $this->safeStorage->storeSafely('username', $username); + + return $this; + } + + /** + * Returns the password. + * + * @return string password + */ + public function getPassword() + { + return $this->safeStorage->retrieveSafely('password'); + } + + /** + * Set the ftp password. + * + * @param string $password + * + * @return $this + */ + public function setPassword($password) + { + $this->safeStorage->storeSafely('password', $password); + + return $this; + } + + /** + * Returns the amount of seconds before the connection will timeout. + * + * @return int + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Set the amount of seconds before the connection should timeout. + * + * @param int $timeout + * + * @return $this + */ + public function setTimeout($timeout) + { + $this->timeout = (int) $timeout; + + return $this; + } + + /** + * Return the FTP system type. + * + * @return string + */ + public function getSystemType() + { + return $this->systemType; + } + + /** + * Set the FTP system type (windows or unix). + * + * @param string $systemType + * + * @return $this + */ + public function setSystemType($systemType) + { + $this->systemType = strtolower($systemType); + + return $this; + } + + /** + * True to enable timestamps for FTP servers that return unix-style listings. + * + * @param bool $bool + * + * @return $this + */ + public function setEnableTimestampsOnUnixListings($bool = false) + { + $this->enableTimestampsOnUnixListings = $bool; + + return $this; + } + + /** + * @inheritdoc + */ + public function listContents($directory = '', $recursive = false) + { + return $this->listDirectoryContents($directory, $recursive); + } + + abstract protected function listDirectoryContents($directory, $recursive = false); + + /** + * Normalize a directory listing. + * + * @param array $listing + * @param string $prefix + * + * @return array directory listing + */ + protected function normalizeListing(array $listing, $prefix = '') + { + $base = $prefix; + $result = []; + $listing = $this->removeDotDirectories($listing); + + while ($item = array_shift($listing)) { + if (preg_match('#^.*:$#', $item)) { + $base = preg_replace('~^\./*|:$~', '', $item); + continue; + } + + $result[] = $this->normalizeObject($item, $base); + } + + return $this->sortListing($result); + } + + /** + * Sort a directory listing. + * + * @param array $result + * + * @return array sorted listing + */ + protected function sortListing(array $result) + { + $compare = function ($one, $two) { + return strnatcmp($one['path'], $two['path']); + }; + + usort($result, $compare); + + return $result; + } + + /** + * Normalize a file entry. + * + * @param string $item + * @param string $base + * + * @return array normalized file array + * + * @throws NotSupportedException + */ + protected function normalizeObject($item, $base) + { + $systemType = $this->systemType ?: $this->detectSystemType($item); + + if ($systemType === 'unix') { + return $this->normalizeUnixObject($item, $base); + } elseif ($systemType === 'windows') { + return $this->normalizeWindowsObject($item, $base); + } + + throw NotSupportedException::forFtpSystemType($systemType); + } + + /** + * Normalize a Unix file entry. + * + * Given $item contains: + * '-rw-r--r-- 1 ftp ftp 409 Aug 19 09:01 file1.txt' + * + * This function will return: + * [ + * 'type' => 'file', + * 'path' => 'file1.txt', + * 'visibility' => 'public', + * 'size' => 409, + * 'timestamp' => 1566205260 + * ] + * + * @param string $item + * @param string $base + * + * @return array normalized file array + */ + protected function normalizeUnixObject($item, $base) + { + $item = preg_replace('#\s+#', ' ', trim($item), 7); + + if (count(explode(' ', $item, 9)) !== 9) { + throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); + } + + list($permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $timeOrYear, $name) = explode(' ', $item, 9); + $type = $this->detectType($permissions); + $path = $base === '' ? $name : $base . $this->separator . $name; + + if ($type === 'dir') { + return compact('type', 'path'); + } + + $permissions = $this->normalizePermissions($permissions); + $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE; + $size = (int) $size; + + $result = compact('type', 'path', 'visibility', 'size'); + if ($this->enableTimestampsOnUnixListings) { + $timestamp = $this->normalizeUnixTimestamp($month, $day, $timeOrYear); + $result += compact('timestamp'); + } + + return $result; + } + + /** + * Only accurate to the minute (current year), or to the day. + * + * Inadequacies in timestamp accuracy are due to limitations of the FTP 'LIST' command + * + * Note: The 'MLSD' command is a machine-readable replacement for 'LIST' + * but many FTP servers do not support it :( + * + * @param string $month e.g. 'Aug' + * @param string $day e.g. '19' + * @param string $timeOrYear e.g. '09:01' OR '2015' + * + * @return int + */ + protected function normalizeUnixTimestamp($month, $day, $timeOrYear) + { + if (is_numeric($timeOrYear)) { + $year = $timeOrYear; + $hour = '00'; + $minute = '00'; + $seconds = '00'; + } else { + $year = date('Y'); + list($hour, $minute) = explode(':', $timeOrYear); + $seconds = '00'; + } + $dateTime = DateTime::createFromFormat('Y-M-j-G:i:s', "{$year}-{$month}-{$day}-{$hour}:{$minute}:{$seconds}"); + + return $dateTime->getTimestamp(); + } + + /** + * Normalize a Windows/DOS file entry. + * + * @param string $item + * @param string $base + * + * @return array normalized file array + */ + protected function normalizeWindowsObject($item, $base) + { + $item = preg_replace('#\s+#', ' ', trim($item), 3); + + if (count(explode(' ', $item, 4)) !== 4) { + throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); + } + + list($date, $time, $size, $name) = explode(' ', $item, 4); + $path = $base === '' ? $name : $base . $this->separator . $name; + + // Check for the correct date/time format + $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i'; + $dt = DateTime::createFromFormat($format, $date . $time); + $timestamp = $dt ? $dt->getTimestamp() : (int) strtotime("$date $time"); + + if ($size === '') { + $type = 'dir'; + + return compact('type', 'path', 'timestamp'); + } + + $type = 'file'; + $visibility = AdapterInterface::VISIBILITY_PUBLIC; + $size = (int) $size; + + return compact('type', 'path', 'visibility', 'size', 'timestamp'); + } + + /** + * Get the system type from a listing item. + * + * @param string $item + * + * @return string the system type + */ + protected function detectSystemType($item) + { + return preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item) ? 'windows' : 'unix'; + } + + /** + * Get the file type from the permissions. + * + * @param string $permissions + * + * @return string file type + */ + protected function detectType($permissions) + { + return substr($permissions, 0, 1) === 'd' ? 'dir' : 'file'; + } + + /** + * Normalize a permissions string. + * + * @param string $permissions + * + * @return int + */ + protected function normalizePermissions($permissions) + { + if (is_numeric($permissions)) { + return ((int) $permissions) & 0777; + } + + // remove the type identifier + $permissions = substr($permissions, 1); + + // map the string rights to the numeric counterparts + $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1']; + $permissions = strtr($permissions, $map); + + // split up the permission groups + $parts = str_split($permissions, 3); + + // convert the groups + $mapper = function ($part) { + return array_sum(str_split($part)); + }; + + // converts to decimal number + return octdec(implode('', array_map($mapper, $parts))); + } + + /** + * Filter out dot-directories. + * + * @param array $list + * + * @return array + */ + public function removeDotDirectories(array $list) + { + $filter = function ($line) { + return $line !== '' && ! preg_match('#.* \.(\.)?$|^total#', $line); + }; + + return array_filter($list, $filter); + } + + /** + * @inheritdoc + */ + public function has($path) + { + return $this->getMetadata($path); + } + + /** + * @inheritdoc + */ + public function getSize($path) + { + return $this->getMetadata($path); + } + + /** + * @inheritdoc + */ + public function getVisibility($path) + { + return $this->getMetadata($path); + } + + /** + * Ensure a directory exists. + * + * @param string $dirname + */ + public function ensureDirectory($dirname) + { + $dirname = (string) $dirname; + + if ($dirname !== '' && ! $this->has($dirname)) { + $this->createDir($dirname, new Config()); + } + } + + /** + * @return mixed + */ + public function getConnection() + { + $tries = 0; + + while ( ! $this->isConnected() && $tries < 3) { + $tries++; + $this->disconnect(); + $this->connect(); + } + + return $this->connection; + } + + /** + * Get the public permission value. + * + * @return int + */ + public function getPermPublic() + { + return $this->permPublic; + } + + /** + * Get the private permission value. + * + * @return int + */ + public function getPermPrivate() + { + return $this->permPrivate; + } + + /** + * Disconnect on destruction. + */ + public function __destruct() + { + $this->disconnect(); + } + + /** + * Establish a connection. + */ + abstract public function connect(); + + /** + * Close the connection. + */ + abstract public function disconnect(); + + /** + * Check if a connection is active. + * + * @return bool + */ + abstract public function isConnected(); +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/CanOverwriteFiles.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/CanOverwriteFiles.php new file mode 100644 index 0000000..fd8d216 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/CanOverwriteFiles.php @@ -0,0 +1,12 @@ +transferMode = $mode; + + return $this; + } + + /** + * Set if Ssl is enabled. + * + * @param bool $ssl + * + * @return $this + */ + public function setSsl($ssl) + { + $this->ssl = (bool) $ssl; + + return $this; + } + + /** + * Set if passive mode should be used. + * + * @param bool $passive + */ + public function setPassive($passive = true) + { + $this->passive = $passive; + } + + /** + * @param bool $ignorePassiveAddress + */ + public function setIgnorePassiveAddress($ignorePassiveAddress) + { + $this->ignorePassiveAddress = $ignorePassiveAddress; + } + + /** + * @param bool $recurseManually + */ + public function setRecurseManually($recurseManually) + { + $this->recurseManually = $recurseManually; + } + + /** + * @param bool $utf8 + */ + public function setUtf8($utf8) + { + $this->utf8 = (bool) $utf8; + } + + /** + * Connect to the FTP server. + */ + public function connect() + { + if ($this->ssl) { + $this->connection = @ftp_ssl_connect($this->getHost(), $this->getPort(), $this->getTimeout()); + } else { + $this->connection = @ftp_connect($this->getHost(), $this->getPort(), $this->getTimeout()); + } + + if ( ! $this->connection) { + throw new ConnectionRuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort()); + } + + $this->login(); + $this->setUtf8Mode(); + $this->setConnectionPassiveMode(); + $this->setConnectionRoot(); + $this->isPureFtpd = $this->isPureFtpdServer(); + } + + /** + * Set the connection to UTF-8 mode. + */ + protected function setUtf8Mode() + { + if ($this->utf8) { + $response = ftp_raw($this->connection, "OPTS UTF8 ON"); + if (substr($response[0], 0, 3) !== '200') { + throw new ConnectionRuntimeException( + 'Could not set UTF-8 mode for connection: ' . $this->getHost() . '::' . $this->getPort() + ); + } + } + } + + /** + * Set the connections to passive mode. + * + * @throws ConnectionRuntimeException + */ + protected function setConnectionPassiveMode() + { + if (is_bool($this->ignorePassiveAddress) && defined('FTP_USEPASVADDRESS')) { + ftp_set_option($this->connection, FTP_USEPASVADDRESS, ! $this->ignorePassiveAddress); + } + + if ( ! ftp_pasv($this->connection, $this->passive)) { + throw new ConnectionRuntimeException( + 'Could not set passive mode for connection: ' . $this->getHost() . '::' . $this->getPort() + ); + } + } + + /** + * Set the connection root. + */ + protected function setConnectionRoot() + { + $root = $this->getRoot(); + $connection = $this->connection; + + if ($root && ! ftp_chdir($connection, $root)) { + throw new InvalidRootException('Root is invalid or does not exist: ' . $this->getRoot()); + } + + // Store absolute path for further reference. + // This is needed when creating directories and + // initial root was a relative path, else the root + // would be relative to the chdir'd path. + $this->root = ftp_pwd($connection); + } + + /** + * Login. + * + * @throws ConnectionRuntimeException + */ + protected function login() + { + set_error_handler(function () { + }); + $isLoggedIn = ftp_login( + $this->connection, + $this->getUsername(), + $this->getPassword() + ); + restore_error_handler(); + + if ( ! $isLoggedIn) { + $this->disconnect(); + throw new ConnectionRuntimeException( + 'Could not login with connection: ' . $this->getHost() . '::' . $this->getPort( + ) . ', username: ' . $this->getUsername() + ); + } + } + + /** + * Disconnect from the FTP server. + */ + public function disconnect() + { + if (is_resource($this->connection)) { + @ftp_close($this->connection); + } + + $this->connection = null; + } + + /** + * @inheritdoc + */ + public function write($path, $contents, Config $config) + { + $stream = fopen('php://temp', 'w+b'); + fwrite($stream, $contents); + rewind($stream); + $result = $this->writeStream($path, $stream, $config); + fclose($stream); + + if ($result === false) { + return false; + } + + $result['contents'] = $contents; + $result['mimetype'] = $config->get('mimetype') ?: Util::guessMimeType($path, $contents); + + return $result; + } + + /** + * @inheritdoc + */ + public function writeStream($path, $resource, Config $config) + { + $this->ensureDirectory(Util::dirname($path)); + + if ( ! ftp_fput($this->getConnection(), $path, $resource, $this->transferMode)) { + return false; + } + + if ($visibility = $config->get('visibility')) { + $this->setVisibility($path, $visibility); + } + + $type = 'file'; + + return compact('type', 'path', 'visibility'); + } + + /** + * @inheritdoc + */ + public function update($path, $contents, Config $config) + { + return $this->write($path, $contents, $config); + } + + /** + * @inheritdoc + */ + public function updateStream($path, $resource, Config $config) + { + return $this->writeStream($path, $resource, $config); + } + + /** + * @inheritdoc + */ + public function rename($path, $newpath) + { + return ftp_rename($this->getConnection(), $path, $newpath); + } + + /** + * @inheritdoc + */ + public function delete($path) + { + return ftp_delete($this->getConnection(), $path); + } + + /** + * @inheritdoc + */ + public function deleteDir($dirname) + { + $connection = $this->getConnection(); + $contents = array_reverse($this->listDirectoryContents($dirname, false)); + + foreach ($contents as $object) { + if ($object['type'] === 'file') { + if ( ! ftp_delete($connection, $object['path'])) { + return false; + } + } elseif ( ! $this->deleteDir($object['path'])) { + return false; + } + } + + return ftp_rmdir($connection, $dirname); + } + + /** + * @inheritdoc + */ + public function createDir($dirname, Config $config) + { + $connection = $this->getConnection(); + $directories = explode('/', $dirname); + + foreach ($directories as $directory) { + if (false === $this->createActualDirectory($directory, $connection)) { + $this->setConnectionRoot(); + + return false; + } + + ftp_chdir($connection, $directory); + } + + $this->setConnectionRoot(); + + return ['type' => 'dir', 'path' => $dirname]; + } + + /** + * Create a directory. + * + * @param string $directory + * @param resource $connection + * + * @return bool + */ + protected function createActualDirectory($directory, $connection) + { + // List the current directory + $listing = ftp_nlist($connection, '.') ?: []; + + foreach ($listing as $key => $item) { + if (preg_match('~^\./.*~', $item)) { + $listing[$key] = substr($item, 2); + } + } + + if (in_array($directory, $listing, true)) { + return true; + } + + return (boolean) ftp_mkdir($connection, $directory); + } + + /** + * @inheritdoc + */ + public function getMetadata($path) + { + if ($path === '') { + return ['type' => 'dir', 'path' => '']; + } + + if (@ftp_chdir($this->getConnection(), $path) === true) { + $this->setConnectionRoot(); + + return ['type' => 'dir', 'path' => $path]; + } + + $listing = $this->ftpRawlist('-A', str_replace('*', '\\*', $path)); + + if (empty($listing) || in_array('total 0', $listing, true)) { + return false; + } + + if (preg_match('/.* not found/', $listing[0])) { + return false; + } + + if (preg_match('/^total [0-9]*$/', $listing[0])) { + array_shift($listing); + } + + return $this->normalizeObject($listing[0], ''); + } + + /** + * @inheritdoc + */ + public function getMimetype($path) + { + if ( ! $metadata = $this->getMetadata($path)) { + return false; + } + + $metadata['mimetype'] = MimeType::detectByFilename($path); + + return $metadata; + } + + /** + * @inheritdoc + */ + public function getTimestamp($path) + { + $timestamp = ftp_mdtm($this->getConnection(), $path); + + return ($timestamp !== -1) ? ['path' => $path, 'timestamp' => $timestamp] : false; + } + + /** + * @inheritdoc + */ + public function read($path) + { + if ( ! $object = $this->readStream($path)) { + return false; + } + + $object['contents'] = stream_get_contents($object['stream']); + fclose($object['stream']); + unset($object['stream']); + + return $object; + } + + /** + * @inheritdoc + */ + public function readStream($path) + { + $stream = fopen('php://temp', 'w+b'); + $result = ftp_fget($this->getConnection(), $stream, $path, $this->transferMode); + rewind($stream); + + if ( ! $result) { + fclose($stream); + + return false; + } + + return ['type' => 'file', 'path' => $path, 'stream' => $stream]; + } + + /** + * @inheritdoc + */ + public function setVisibility($path, $visibility) + { + $mode = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? $this->getPermPublic() : $this->getPermPrivate(); + + if ( ! ftp_chmod($this->getConnection(), $mode, $path)) { + return false; + } + + return compact('path', 'visibility'); + } + + /** + * @inheritdoc + * + * @param string $directory + */ + protected function listDirectoryContents($directory, $recursive = true) + { + $directory = str_replace('*', '\\*', $directory); + + if ($recursive && $this->recurseManually) { + return $this->listDirectoryContentsRecursive($directory); + } + + $options = $recursive ? '-alnR' : '-aln'; + $listing = $this->ftpRawlist($options, $directory); + + return $listing ? $this->normalizeListing($listing, $directory) : []; + } + + /** + * @inheritdoc + * + * @param string $directory + */ + protected function listDirectoryContentsRecursive($directory) + { + $listing = $this->normalizeListing($this->ftpRawlist('-aln', $directory) ?: [], $directory); + $output = []; + + foreach ($listing as $item) { + $output[] = $item; + if ($item['type'] !== 'dir') { + continue; + } + $output = array_merge($output, $this->listDirectoryContentsRecursive($item['path'])); + } + + return $output; + } + + /** + * Check if the connection is open. + * + * @return bool + * + * @throws ConnectionErrorException + */ + public function isConnected() + { + return is_resource($this->connection) + && $this->getRawExecResponseCode('NOOP') === 200; + } + + /** + * @return bool + */ + protected function isPureFtpdServer() + { + $response = ftp_raw($this->connection, 'HELP'); + + return stripos(implode(' ', $response), 'Pure-FTPd') !== false; + } + + /** + * The ftp_rawlist function with optional escaping. + * + * @param string $options + * @param string $path + * + * @return array + */ + protected function ftpRawlist($options, $path) + { + $connection = $this->getConnection(); + + if ($this->isPureFtpd) { + $path = str_replace(' ', '\ ', $path); + } + + return ftp_rawlist($connection, $options . ' ' . $path); + } + + private function getRawExecResponseCode($command) + { + $response = @ftp_raw($this->connection, trim($command)); + + return (int) preg_replace('/\D/', '', implode(' ', $response)); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Ftpd.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Ftpd.php new file mode 100644 index 0000000..d5349e4 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Ftpd.php @@ -0,0 +1,45 @@ + 'dir', 'path' => '']; + } + if (@ftp_chdir($this->getConnection(), $path) === true) { + $this->setConnectionRoot(); + + return ['type' => 'dir', 'path' => $path]; + } + + if ( ! ($object = ftp_raw($this->getConnection(), 'STAT ' . $path)) || count($object) < 3) { + return false; + } + + if (substr($object[1], 0, 5) === "ftpd:") { + return false; + } + + return $this->normalizeObject($object[1], ''); + } + + /** + * @inheritdoc + */ + protected function listDirectoryContents($directory, $recursive = true) + { + $listing = ftp_rawlist($this->getConnection(), $directory, $recursive); + + if ($listing === false || ( ! empty($listing) && substr($listing[0], 0, 5) === "ftpd:")) { + return []; + } + + return $this->normalizeListing($listing, $directory); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Local.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Local.php new file mode 100644 index 0000000..2b892ab --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Local.php @@ -0,0 +1,532 @@ + [ + 'public' => 0644, + 'private' => 0600, + ], + 'dir' => [ + 'public' => 0755, + 'private' => 0700, + ], + ]; + + /** + * @var string + */ + protected $pathSeparator = DIRECTORY_SEPARATOR; + + /** + * @var array + */ + protected $permissionMap; + + /** + * @var int + */ + protected $writeFlags; + + /** + * @var int + */ + private $linkHandling; + + /** + * Constructor. + * + * @param string $root + * @param int $writeFlags + * @param int $linkHandling + * @param array $permissions + * + * @throws LogicException + */ + public function __construct($root, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS, array $permissions = []) + { + $root = is_link($root) ? realpath($root) : $root; + $this->permissionMap = array_replace_recursive(static::$permissions, $permissions); + $this->ensureDirectory($root); + + if ( ! is_dir($root) || ! is_readable($root)) { + throw new LogicException('The root path ' . $root . ' is not readable.'); + } + + $this->setPathPrefix($root); + $this->writeFlags = $writeFlags; + $this->linkHandling = $linkHandling; + } + + /** + * Ensure the root directory exists. + * + * @param string $root root directory path + * + * @return void + * + * @throws Exception in case the root directory can not be created + */ + protected function ensureDirectory($root) + { + if ( ! is_dir($root)) { + $umask = umask(0); + + if ( ! @mkdir($root, $this->permissionMap['dir']['public'], true)) { + $mkdirError = error_get_last(); + } + + umask($umask); + clearstatcache(false, $root); + + if ( ! is_dir($root)) { + $errorMessage = isset($mkdirError['message']) ? $mkdirError['message'] : ''; + throw new Exception(sprintf('Impossible to create the root directory "%s". %s', $root, $errorMessage)); + } + } + } + + /** + * @inheritdoc + */ + public function has($path) + { + $location = $this->applyPathPrefix($path); + + return file_exists($location); + } + + /** + * @inheritdoc + */ + public function write($path, $contents, Config $config) + { + $location = $this->applyPathPrefix($path); + $this->ensureDirectory(dirname($location)); + + if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) { + return false; + } + + $type = 'file'; + $result = compact('contents', 'type', 'size', 'path'); + + if ($visibility = $config->get('visibility')) { + $result['visibility'] = $visibility; + $this->setVisibility($path, $visibility); + } + + return $result; + } + + /** + * @inheritdoc + */ + public function writeStream($path, $resource, Config $config) + { + $location = $this->applyPathPrefix($path); + $this->ensureDirectory(dirname($location)); + $stream = fopen($location, 'w+b'); + + if ( ! $stream || stream_copy_to_stream($resource, $stream) === false || ! fclose($stream)) { + return false; + } + + $type = 'file'; + $result = compact('type', 'path'); + + if ($visibility = $config->get('visibility')) { + $this->setVisibility($path, $visibility); + $result['visibility'] = $visibility; + } + + return $result; + } + + /** + * @inheritdoc + */ + public function readStream($path) + { + $location = $this->applyPathPrefix($path); + $stream = fopen($location, 'rb'); + + return ['type' => 'file', 'path' => $path, 'stream' => $stream]; + } + + /** + * @inheritdoc + */ + public function updateStream($path, $resource, Config $config) + { + return $this->writeStream($path, $resource, $config); + } + + /** + * @inheritdoc + */ + public function update($path, $contents, Config $config) + { + $location = $this->applyPathPrefix($path); + $size = file_put_contents($location, $contents, $this->writeFlags); + + if ($size === false) { + return false; + } + + $type = 'file'; + + $result = compact('type', 'path', 'size', 'contents'); + + if ($mimetype = $config->get('mimetype') ?: Util::guessMimeType($path, $contents)) { + $result['mimetype'] = $mimetype; + } + + return $result; + } + + /** + * @inheritdoc + */ + public function read($path) + { + $location = $this->applyPathPrefix($path); + $contents = @file_get_contents($location); + + if ($contents === false) { + return false; + } + + return ['type' => 'file', 'path' => $path, 'contents' => $contents]; + } + + /** + * @inheritdoc + */ + public function rename($path, $newpath) + { + $location = $this->applyPathPrefix($path); + $destination = $this->applyPathPrefix($newpath); + $parentDirectory = $this->applyPathPrefix(Util::dirname($newpath)); + $this->ensureDirectory($parentDirectory); + + return rename($location, $destination); + } + + /** + * @inheritdoc + */ + public function copy($path, $newpath) + { + $location = $this->applyPathPrefix($path); + $destination = $this->applyPathPrefix($newpath); + $this->ensureDirectory(dirname($destination)); + + return copy($location, $destination); + } + + /** + * @inheritdoc + */ + public function delete($path) + { + $location = $this->applyPathPrefix($path); + + return @unlink($location); + } + + /** + * @inheritdoc + */ + public function listContents($directory = '', $recursive = false) + { + $result = []; + $location = $this->applyPathPrefix($directory); + + if ( ! is_dir($location)) { + return []; + } + + $iterator = $recursive ? $this->getRecursiveDirectoryIterator($location) : $this->getDirectoryIterator($location); + + foreach ($iterator as $file) { + $path = $this->getFilePath($file); + + if (preg_match('#(^|/|\\\\)\.{1,2}$#', $path)) { + continue; + } + + $result[] = $this->normalizeFileInfo($file); + } + + unset($iterator); + + return array_filter($result); + } + + /** + * @inheritdoc + */ + public function getMetadata($path) + { + $location = $this->applyPathPrefix($path); + clearstatcache(false, $location); + $info = new SplFileInfo($location); + + return $this->normalizeFileInfo($info); + } + + /** + * @inheritdoc + */ + public function getSize($path) + { + return $this->getMetadata($path); + } + + /** + * @inheritdoc + */ + public function getMimetype($path) + { + $location = $this->applyPathPrefix($path); + $finfo = new Finfo(FILEINFO_MIME_TYPE); + $mimetype = $finfo->file($location); + + if (in_array($mimetype, ['application/octet-stream', 'inode/x-empty', 'application/x-empty'])) { + $mimetype = Util\MimeType::detectByFilename($location); + } + + return ['path' => $path, 'type' => 'file', 'mimetype' => $mimetype]; + } + + /** + * @inheritdoc + */ + public function getTimestamp($path) + { + return $this->getMetadata($path); + } + + /** + * @inheritdoc + */ + public function getVisibility($path) + { + $location = $this->applyPathPrefix($path); + clearstatcache(false, $location); + $permissions = octdec(substr(sprintf('%o', fileperms($location)), -4)); + $type = is_dir($location) ? 'dir' : 'file'; + + foreach ($this->permissionMap[$type] as $visibility => $visibilityPermissions) { + if ($visibilityPermissions == $permissions) { + return compact('path', 'visibility'); + } + } + + $visibility = substr(sprintf('%o', fileperms($location)), -4); + + return compact('path', 'visibility'); + } + + /** + * @inheritdoc + */ + public function setVisibility($path, $visibility) + { + $location = $this->applyPathPrefix($path); + $type = is_dir($location) ? 'dir' : 'file'; + $success = chmod($location, $this->permissionMap[$type][$visibility]); + + if ($success === false) { + return false; + } + + return compact('path', 'visibility'); + } + + /** + * @inheritdoc + */ + public function createDir($dirname, Config $config) + { + $location = $this->applyPathPrefix($dirname); + $umask = umask(0); + $visibility = $config->get('visibility', 'public'); + $return = ['path' => $dirname, 'type' => 'dir']; + + if ( ! is_dir($location)) { + if (false === @mkdir($location, $this->permissionMap['dir'][$visibility], true) + || false === is_dir($location)) { + $return = false; + } + } + + umask($umask); + + return $return; + } + + /** + * @inheritdoc + */ + public function deleteDir($dirname) + { + $location = $this->applyPathPrefix($dirname); + + if ( ! is_dir($location)) { + return false; + } + + $contents = $this->getRecursiveDirectoryIterator($location, RecursiveIteratorIterator::CHILD_FIRST); + + /** @var SplFileInfo $file */ + foreach ($contents as $file) { + $this->guardAgainstUnreadableFileInfo($file); + $this->deleteFileInfoObject($file); + } + + unset($contents); + + return rmdir($location); + } + + /** + * @param SplFileInfo $file + */ + protected function deleteFileInfoObject(SplFileInfo $file) + { + switch ($file->getType()) { + case 'dir': + rmdir($file->getRealPath()); + break; + case 'link': + unlink($file->getPathname()); + break; + default: + unlink($file->getRealPath()); + } + } + + /** + * Normalize the file info. + * + * @param SplFileInfo $file + * + * @return array|void + * + * @throws NotSupportedException + */ + protected function normalizeFileInfo(SplFileInfo $file) + { + if ( ! $file->isLink()) { + return $this->mapFileInfo($file); + } + + if ($this->linkHandling & self::DISALLOW_LINKS) { + throw NotSupportedException::forLink($file); + } + } + + /** + * Get the normalized path from a SplFileInfo object. + * + * @param SplFileInfo $file + * + * @return string + */ + protected function getFilePath(SplFileInfo $file) + { + $location = $file->getPathname(); + $path = $this->removePathPrefix($location); + + return trim(str_replace('\\', '/', $path), '/'); + } + + /** + * @param string $path + * @param int $mode + * + * @return RecursiveIteratorIterator + */ + protected function getRecursiveDirectoryIterator($path, $mode = RecursiveIteratorIterator::SELF_FIRST) + { + return new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), + $mode + ); + } + + /** + * @param string $path + * + * @return DirectoryIterator + */ + protected function getDirectoryIterator($path) + { + $iterator = new DirectoryIterator($path); + + return $iterator; + } + + /** + * @param SplFileInfo $file + * + * @return array + */ + protected function mapFileInfo(SplFileInfo $file) + { + $normalized = [ + 'type' => $file->getType(), + 'path' => $this->getFilePath($file), + ]; + + $normalized['timestamp'] = $file->getMTime(); + + if ($normalized['type'] === 'file') { + $normalized['size'] = $file->getSize(); + } + + return $normalized; + } + + /** + * @param SplFileInfo $file + * + * @throws UnreadableFileException + */ + protected function guardAgainstUnreadableFileInfo(SplFileInfo $file) + { + if ( ! $file->isReadable()) { + throw UnreadableFileException::forFileInfo($file); + } + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/NullAdapter.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/NullAdapter.php new file mode 100644 index 0000000..2527087 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/NullAdapter.php @@ -0,0 +1,144 @@ +get('visibility')) { + $result['visibility'] = $visibility; + } + + return $result; + } + + /** + * @inheritdoc + */ + public function update($path, $contents, Config $config) + { + return false; + } + + /** + * @inheritdoc + */ + public function read($path) + { + return false; + } + + /** + * @inheritdoc + */ + public function rename($path, $newpath) + { + return false; + } + + /** + * @inheritdoc + */ + public function delete($path) + { + return false; + } + + /** + * @inheritdoc + */ + public function listContents($directory = '', $recursive = false) + { + return []; + } + + /** + * @inheritdoc + */ + public function getMetadata($path) + { + return false; + } + + /** + * @inheritdoc + */ + public function getSize($path) + { + return false; + } + + /** + * @inheritdoc + */ + public function getMimetype($path) + { + return false; + } + + /** + * @inheritdoc + */ + public function getTimestamp($path) + { + return false; + } + + /** + * @inheritdoc + */ + public function getVisibility($path) + { + return false; + } + + /** + * @inheritdoc + */ + public function setVisibility($path, $visibility) + { + return compact('visibility'); + } + + /** + * @inheritdoc + */ + public function createDir($dirname, Config $config) + { + return ['path' => $dirname, 'type' => 'dir']; + } + + /** + * @inheritdoc + */ + public function deleteDir($dirname) + { + return false; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/NotSupportingVisibilityTrait.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/NotSupportingVisibilityTrait.php new file mode 100644 index 0000000..fc0a747 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/NotSupportingVisibilityTrait.php @@ -0,0 +1,33 @@ +readStream($path); + + if ($response === false || ! is_resource($response['stream'])) { + return false; + } + + $result = $this->writeStream($newpath, $response['stream'], new Config()); + + if ($result !== false && is_resource($response['stream'])) { + fclose($response['stream']); + } + + return $result !== false; + } + + // Required abstract method + + /** + * @param string $path + * + * @return resource + */ + abstract public function readStream($path); + + /** + * @param string $path + * @param resource $resource + * @param Config $config + * + * @return resource + */ + abstract public function writeStream($path, $resource, Config $config); +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedReadingTrait.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedReadingTrait.php new file mode 100644 index 0000000..2b31c01 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedReadingTrait.php @@ -0,0 +1,44 @@ +read($path)) { + return false; + } + + $stream = fopen('php://temp', 'w+b'); + fwrite($stream, $data['contents']); + rewind($stream); + $data['stream'] = $stream; + unset($data['contents']); + + return $data; + } + + /** + * Reads a file. + * + * @param string $path + * + * @return array|false + * + * @see League\Flysystem\ReadInterface::read() + */ + abstract public function read($path); +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedTrait.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedTrait.php new file mode 100644 index 0000000..8042496 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/Polyfill/StreamedTrait.php @@ -0,0 +1,9 @@ +stream($path, $resource, $config, 'write'); + } + + /** + * Update a file using a stream. + * + * @param string $path + * @param resource $resource + * @param Config $config Config object or visibility setting + * + * @return mixed false of file metadata + */ + public function updateStream($path, $resource, Config $config) + { + return $this->stream($path, $resource, $config, 'update'); + } + + // Required abstract methods + abstract public function write($pash, $contents, Config $config); + abstract public function update($pash, $contents, Config $config); +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/SynologyFtp.php b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/SynologyFtp.php new file mode 100644 index 0000000..fe0d344 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Adapter/SynologyFtp.php @@ -0,0 +1,8 @@ +settings = $settings; + } + + /** + * Get a setting. + * + * @param string $key + * @param mixed $default + * + * @return mixed config setting or default when not found + */ + public function get($key, $default = null) + { + if ( ! array_key_exists($key, $this->settings)) { + return $this->getDefault($key, $default); + } + + return $this->settings[$key]; + } + + /** + * Check if an item exists by key. + * + * @param string $key + * + * @return bool + */ + public function has($key) + { + if (array_key_exists($key, $this->settings)) { + return true; + } + + return $this->fallback instanceof Config + ? $this->fallback->has($key) + : false; + } + + /** + * Try to retrieve a default setting from a config fallback. + * + * @param string $key + * @param mixed $default + * + * @return mixed config setting or default when not found + */ + protected function getDefault($key, $default) + { + if ( ! $this->fallback) { + return $default; + } + + return $this->fallback->get($key, $default); + } + + /** + * Set a setting. + * + * @param string $key + * @param mixed $value + * + * @return $this + */ + public function set($key, $value) + { + $this->settings[$key] = $value; + + return $this; + } + + /** + * Set the fallback. + * + * @param Config $fallback + * + * @return $this + */ + public function setFallback(Config $fallback) + { + $this->fallback = $fallback; + + return $this; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/ConfigAwareTrait.php b/plugins/panakour/backup/vendor/league/flysystem/src/ConfigAwareTrait.php new file mode 100644 index 0000000..202d605 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/ConfigAwareTrait.php @@ -0,0 +1,49 @@ +config = $config ? Util::ensureConfig($config) : new Config; + } + + /** + * Get the Config. + * + * @return Config config object + */ + public function getConfig() + { + return $this->config; + } + + /** + * Convert a config array to a Config object with the correct fallback. + * + * @param array $config + * + * @return Config + */ + protected function prepareConfig(array $config) + { + $config = new Config($config); + $config->setFallback($this->getConfig()); + + return $config; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/ConnectionErrorException.php b/plugins/panakour/backup/vendor/league/flysystem/src/ConnectionErrorException.php new file mode 100644 index 0000000..adb651d --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/ConnectionErrorException.php @@ -0,0 +1,9 @@ +filesystem->deleteDir($this->path); + } + + /** + * List the directory contents. + * + * @param bool $recursive + * + * @return array|bool directory contents or false + */ + public function getContents($recursive = false) + { + return $this->filesystem->listContents($this->path, $recursive); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Exception.php b/plugins/panakour/backup/vendor/league/flysystem/src/Exception.php new file mode 100644 index 0000000..4596c0a --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Exception.php @@ -0,0 +1,8 @@ +filesystem->has($this->path); + } + + /** + * Read the file. + * + * @return string|false file contents + */ + public function read() + { + return $this->filesystem->read($this->path); + } + + /** + * Read the file as a stream. + * + * @return resource|false file stream + */ + public function readStream() + { + return $this->filesystem->readStream($this->path); + } + + /** + * Write the new file. + * + * @param string $content + * + * @return bool success boolean + */ + public function write($content) + { + return $this->filesystem->write($this->path, $content); + } + + /** + * Write the new file using a stream. + * + * @param resource $resource + * + * @return bool success boolean + */ + public function writeStream($resource) + { + return $this->filesystem->writeStream($this->path, $resource); + } + + /** + * Update the file contents. + * + * @param string $content + * + * @return bool success boolean + */ + public function update($content) + { + return $this->filesystem->update($this->path, $content); + } + + /** + * Update the file contents with a stream. + * + * @param resource $resource + * + * @return bool success boolean + */ + public function updateStream($resource) + { + return $this->filesystem->updateStream($this->path, $resource); + } + + /** + * Create the file or update if exists. + * + * @param string $content + * + * @return bool success boolean + */ + public function put($content) + { + return $this->filesystem->put($this->path, $content); + } + + /** + * Create the file or update if exists using a stream. + * + * @param resource $resource + * + * @return bool success boolean + */ + public function putStream($resource) + { + return $this->filesystem->putStream($this->path, $resource); + } + + /** + * Rename the file. + * + * @param string $newpath + * + * @return bool success boolean + */ + public function rename($newpath) + { + if ($this->filesystem->rename($this->path, $newpath)) { + $this->path = $newpath; + + return true; + } + + return false; + } + + /** + * Copy the file. + * + * @param string $newpath + * + * @return File|false new file or false + */ + public function copy($newpath) + { + if ($this->filesystem->copy($this->path, $newpath)) { + return new File($this->filesystem, $newpath); + } + + return false; + } + + /** + * Get the file's timestamp. + * + * @return string|false The timestamp or false on failure. + */ + public function getTimestamp() + { + return $this->filesystem->getTimestamp($this->path); + } + + /** + * Get the file's mimetype. + * + * @return string|false The file mime-type or false on failure. + */ + public function getMimetype() + { + return $this->filesystem->getMimetype($this->path); + } + + /** + * Get the file's visibility. + * + * @return string|false The visibility (public|private) or false on failure. + */ + public function getVisibility() + { + return $this->filesystem->getVisibility($this->path); + } + + /** + * Get the file's metadata. + * + * @return array|false The file metadata or false on failure. + */ + public function getMetadata() + { + return $this->filesystem->getMetadata($this->path); + } + + /** + * Get the file size. + * + * @return int|false The file size or false on failure. + */ + public function getSize() + { + return $this->filesystem->getSize($this->path); + } + + /** + * Delete the file. + * + * @return bool success boolean + */ + public function delete() + { + return $this->filesystem->delete($this->path); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/FileExistsException.php b/plugins/panakour/backup/vendor/league/flysystem/src/FileExistsException.php new file mode 100644 index 0000000..c82e20c --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/FileExistsException.php @@ -0,0 +1,37 @@ +path = $path; + + parent::__construct('File already exists at path: ' . $this->getPath(), $code, $previous); + } + + /** + * Get the path which was found. + * + * @return string + */ + public function getPath() + { + return $this->path; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/FileNotFoundException.php b/plugins/panakour/backup/vendor/league/flysystem/src/FileNotFoundException.php new file mode 100644 index 0000000..989df69 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/FileNotFoundException.php @@ -0,0 +1,37 @@ +path = $path; + + parent::__construct('File not found at path: ' . $this->getPath(), $code, $previous); + } + + /** + * Get the path which was not found. + * + * @return string + */ + public function getPath() + { + return $this->path; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Filesystem.php b/plugins/panakour/backup/vendor/league/flysystem/src/Filesystem.php new file mode 100644 index 0000000..4509526 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Filesystem.php @@ -0,0 +1,408 @@ +adapter = $adapter; + $this->setConfig($config); + } + + /** + * Get the Adapter. + * + * @return AdapterInterface adapter + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * @inheritdoc + */ + public function has($path) + { + $path = Util::normalizePath($path); + + return strlen($path) === 0 ? false : (bool) $this->getAdapter()->has($path); + } + + /** + * @inheritdoc + */ + public function write($path, $contents, array $config = []) + { + $path = Util::normalizePath($path); + $this->assertAbsent($path); + $config = $this->prepareConfig($config); + + return (bool) $this->getAdapter()->write($path, $contents, $config); + } + + /** + * @inheritdoc + */ + public function writeStream($path, $resource, array $config = []) + { + if ( ! is_resource($resource)) { + throw new InvalidArgumentException(__METHOD__ . ' expects argument #2 to be a valid resource.'); + } + + $path = Util::normalizePath($path); + $this->assertAbsent($path); + $config = $this->prepareConfig($config); + + Util::rewindStream($resource); + + return (bool) $this->getAdapter()->writeStream($path, $resource, $config); + } + + /** + * @inheritdoc + */ + public function put($path, $contents, array $config = []) + { + $path = Util::normalizePath($path); + $config = $this->prepareConfig($config); + + if ( ! $this->getAdapter() instanceof CanOverwriteFiles && $this->has($path)) { + return (bool) $this->getAdapter()->update($path, $contents, $config); + } + + return (bool) $this->getAdapter()->write($path, $contents, $config); + } + + /** + * @inheritdoc + */ + public function putStream($path, $resource, array $config = []) + { + if ( ! is_resource($resource)) { + throw new InvalidArgumentException(__METHOD__ . ' expects argument #2 to be a valid resource.'); + } + + $path = Util::normalizePath($path); + $config = $this->prepareConfig($config); + Util::rewindStream($resource); + + if ( ! $this->getAdapter() instanceof CanOverwriteFiles && $this->has($path)) { + return (bool) $this->getAdapter()->updateStream($path, $resource, $config); + } + + return (bool) $this->getAdapter()->writeStream($path, $resource, $config); + } + + /** + * @inheritdoc + */ + public function readAndDelete($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + $contents = $this->read($path); + + if ($contents === false) { + return false; + } + + $this->delete($path); + + return $contents; + } + + /** + * @inheritdoc + */ + public function update($path, $contents, array $config = []) + { + $path = Util::normalizePath($path); + $config = $this->prepareConfig($config); + + $this->assertPresent($path); + + return (bool) $this->getAdapter()->update($path, $contents, $config); + } + + /** + * @inheritdoc + */ + public function updateStream($path, $resource, array $config = []) + { + if ( ! is_resource($resource)) { + throw new InvalidArgumentException(__METHOD__ . ' expects argument #2 to be a valid resource.'); + } + + $path = Util::normalizePath($path); + $config = $this->prepareConfig($config); + $this->assertPresent($path); + Util::rewindStream($resource); + + return (bool) $this->getAdapter()->updateStream($path, $resource, $config); + } + + /** + * @inheritdoc + */ + public function read($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + if ( ! ($object = $this->getAdapter()->read($path))) { + return false; + } + + return $object['contents']; + } + + /** + * @inheritdoc + */ + public function readStream($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + if ( ! $object = $this->getAdapter()->readStream($path)) { + return false; + } + + return $object['stream']; + } + + /** + * @inheritdoc + */ + public function rename($path, $newpath) + { + $path = Util::normalizePath($path); + $newpath = Util::normalizePath($newpath); + $this->assertPresent($path); + $this->assertAbsent($newpath); + + return (bool) $this->getAdapter()->rename($path, $newpath); + } + + /** + * @inheritdoc + */ + public function copy($path, $newpath) + { + $path = Util::normalizePath($path); + $newpath = Util::normalizePath($newpath); + $this->assertPresent($path); + $this->assertAbsent($newpath); + + return $this->getAdapter()->copy($path, $newpath); + } + + /** + * @inheritdoc + */ + public function delete($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + return $this->getAdapter()->delete($path); + } + + /** + * @inheritdoc + */ + public function deleteDir($dirname) + { + $dirname = Util::normalizePath($dirname); + + if ($dirname === '') { + throw new RootViolationException('Root directories can not be deleted.'); + } + + return (bool) $this->getAdapter()->deleteDir($dirname); + } + + /** + * @inheritdoc + */ + public function createDir($dirname, array $config = []) + { + $dirname = Util::normalizePath($dirname); + $config = $this->prepareConfig($config); + + return (bool) $this->getAdapter()->createDir($dirname, $config); + } + + /** + * @inheritdoc + */ + public function listContents($directory = '', $recursive = false) + { + $directory = Util::normalizePath($directory); + $contents = $this->getAdapter()->listContents($directory, $recursive); + + return (new ContentListingFormatter($directory, $recursive, $this->config->get('case_sensitive', true))) + ->formatListing($contents); + } + + /** + * @inheritdoc + */ + public function getMimetype($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + if (( ! $object = $this->getAdapter()->getMimetype($path)) || ! array_key_exists('mimetype', $object)) { + return false; + } + + return $object['mimetype']; + } + + /** + * @inheritdoc + */ + public function getTimestamp($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + if (( ! $object = $this->getAdapter()->getTimestamp($path)) || ! array_key_exists('timestamp', $object)) { + return false; + } + + return $object['timestamp']; + } + + /** + * @inheritdoc + */ + public function getVisibility($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + if (( ! $object = $this->getAdapter()->getVisibility($path)) || ! array_key_exists('visibility', $object)) { + return false; + } + + return $object['visibility']; + } + + /** + * @inheritdoc + */ + public function getSize($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + if (( ! $object = $this->getAdapter()->getSize($path)) || ! array_key_exists('size', $object)) { + return false; + } + + return (int) $object['size']; + } + + /** + * @inheritdoc + */ + public function setVisibility($path, $visibility) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + return (bool) $this->getAdapter()->setVisibility($path, $visibility); + } + + /** + * @inheritdoc + */ + public function getMetadata($path) + { + $path = Util::normalizePath($path); + $this->assertPresent($path); + + return $this->getAdapter()->getMetadata($path); + } + + /** + * @inheritdoc + */ + public function get($path, Handler $handler = null) + { + $path = Util::normalizePath($path); + + if ( ! $handler) { + $metadata = $this->getMetadata($path); + $handler = ($metadata && $metadata['type'] === 'file') ? new File($this, $path) : new Directory($this, $path); + } + + $handler->setPath($path); + $handler->setFilesystem($this); + + return $handler; + } + + /** + * Assert a file is present. + * + * @param string $path path to file + * + * @throws FileNotFoundException + * + * @return void + */ + public function assertPresent($path) + { + if ($this->config->get('disable_asserts', false) === false && ! $this->has($path)) { + throw new FileNotFoundException($path); + } + } + + /** + * Assert a file is absent. + * + * @param string $path path to file + * + * @throws FileExistsException + * + * @return void + */ + public function assertAbsent($path) + { + if ($this->config->get('disable_asserts', false) === false && $this->has($path)) { + throw new FileExistsException($path); + } + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/FilesystemException.php b/plugins/panakour/backup/vendor/league/flysystem/src/FilesystemException.php new file mode 100644 index 0000000..3121e53 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/FilesystemException.php @@ -0,0 +1,7 @@ +path = $path; + $this->filesystem = $filesystem; + } + + /** + * Check whether the entree is a directory. + * + * @return bool + */ + public function isDir() + { + return $this->getType() === 'dir'; + } + + /** + * Check whether the entree is a file. + * + * @return bool + */ + public function isFile() + { + return $this->getType() === 'file'; + } + + /** + * Retrieve the entree type (file|dir). + * + * @return string file or dir + */ + public function getType() + { + $metadata = $this->filesystem->getMetadata($this->path); + + return $metadata ? $metadata['type'] : 'dir'; + } + + /** + * Set the Filesystem object. + * + * @param FilesystemInterface $filesystem + * + * @return $this + */ + public function setFilesystem(FilesystemInterface $filesystem) + { + $this->filesystem = $filesystem; + + return $this; + } + + /** + * Retrieve the Filesystem object. + * + * @return FilesystemInterface + */ + public function getFilesystem() + { + return $this->filesystem; + } + + /** + * Set the entree path. + * + * @param string $path + * + * @return $this + */ + public function setPath($path) + { + $this->path = $path; + + return $this; + } + + /** + * Retrieve the entree path. + * + * @return string path + */ + public function getPath() + { + return $this->path; + } + + /** + * Plugins pass-through. + * + * @param string $method + * @param array $arguments + * + * @return mixed + */ + public function __call($method, array $arguments) + { + array_unshift($arguments, $this->path); + $callback = [$this->filesystem, $method]; + + try { + return call_user_func_array($callback, $arguments); + } catch (BadMethodCallException $e) { + throw new BadMethodCallException( + 'Call to undefined method ' + . get_called_class() + . '::' . $method + ); + } + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/InvalidRootException.php b/plugins/panakour/backup/vendor/league/flysystem/src/InvalidRootException.php new file mode 100644 index 0000000..468d1d5 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/InvalidRootException.php @@ -0,0 +1,9 @@ + Filesystem,] + * + * @throws InvalidArgumentException + */ + public function __construct(array $filesystems = []) + { + $this->mountFilesystems($filesystems); + } + + /** + * Mount filesystems. + * + * @param FilesystemInterface[] $filesystems [:prefix => Filesystem,] + * + * @throws InvalidArgumentException + * + * @return $this + */ + public function mountFilesystems(array $filesystems) + { + foreach ($filesystems as $prefix => $filesystem) { + $this->mountFilesystem($prefix, $filesystem); + } + + return $this; + } + + /** + * Mount filesystems. + * + * @param string $prefix + * @param FilesystemInterface $filesystem + * + * @throws InvalidArgumentException + * + * @return $this + */ + public function mountFilesystem($prefix, FilesystemInterface $filesystem) + { + if ( ! is_string($prefix)) { + throw new InvalidArgumentException(__METHOD__ . ' expects argument #1 to be a string.'); + } + + $this->filesystems[$prefix] = $filesystem; + + return $this; + } + + /** + * Get the filesystem with the corresponding prefix. + * + * @param string $prefix + * + * @throws FilesystemNotFoundException + * + * @return FilesystemInterface + */ + public function getFilesystem($prefix) + { + if ( ! isset($this->filesystems[$prefix])) { + throw new FilesystemNotFoundException('No filesystem mounted with prefix ' . $prefix); + } + + return $this->filesystems[$prefix]; + } + + /** + * Retrieve the prefix from an arguments array. + * + * @param array $arguments + * + * @throws InvalidArgumentException + * + * @return array [:prefix, :arguments] + */ + public function filterPrefix(array $arguments) + { + if (empty($arguments)) { + throw new InvalidArgumentException('At least one argument needed'); + } + + $path = array_shift($arguments); + + if ( ! is_string($path)) { + throw new InvalidArgumentException('First argument should be a string'); + } + + list($prefix, $path) = $this->getPrefixAndPath($path); + array_unshift($arguments, $path); + + return [$prefix, $arguments]; + } + + /** + * @param string $directory + * @param bool $recursive + * + * @throws InvalidArgumentException + * @throws FilesystemNotFoundException + * + * @return array + */ + public function listContents($directory = '', $recursive = false) + { + list($prefix, $directory) = $this->getPrefixAndPath($directory); + $filesystem = $this->getFilesystem($prefix); + $result = $filesystem->listContents($directory, $recursive); + + foreach ($result as &$file) { + $file['filesystem'] = $prefix; + } + + return $result; + } + + /** + * Call forwarder. + * + * @param string $method + * @param array $arguments + * + * @throws InvalidArgumentException + * @throws FilesystemNotFoundException + * + * @return mixed + */ + public function __call($method, $arguments) + { + list($prefix, $arguments) = $this->filterPrefix($arguments); + + return $this->invokePluginOnFilesystem($method, $arguments, $prefix); + } + + /** + * @param string $from + * @param string $to + * @param array $config + * + * @throws InvalidArgumentException + * @throws FilesystemNotFoundException + * @throws FileExistsException + * + * @return bool + */ + public function copy($from, $to, array $config = []) + { + list($prefixFrom, $from) = $this->getPrefixAndPath($from); + + $buffer = $this->getFilesystem($prefixFrom)->readStream($from); + + if ($buffer === false) { + return false; + } + + list($prefixTo, $to) = $this->getPrefixAndPath($to); + + $result = $this->getFilesystem($prefixTo)->writeStream($to, $buffer, $config); + + if (is_resource($buffer)) { + fclose($buffer); + } + + return $result; + } + + /** + * List with plugin adapter. + * + * @param array $keys + * @param string $directory + * @param bool $recursive + * + * @throws InvalidArgumentException + * @throws FilesystemNotFoundException + * + * @return array + */ + public function listWith(array $keys = [], $directory = '', $recursive = false) + { + list($prefix, $directory) = $this->getPrefixAndPath($directory); + $arguments = [$keys, $directory, $recursive]; + + return $this->invokePluginOnFilesystem('listWith', $arguments, $prefix); + } + + /** + * Move a file. + * + * @param string $from + * @param string $to + * @param array $config + * + * @throws InvalidArgumentException + * @throws FilesystemNotFoundException + * + * @return bool + */ + public function move($from, $to, array $config = []) + { + list($prefixFrom, $pathFrom) = $this->getPrefixAndPath($from); + list($prefixTo, $pathTo) = $this->getPrefixAndPath($to); + + if ($prefixFrom === $prefixTo) { + $filesystem = $this->getFilesystem($prefixFrom); + $renamed = $filesystem->rename($pathFrom, $pathTo); + + if ($renamed && isset($config['visibility'])) { + return $filesystem->setVisibility($pathTo, $config['visibility']); + } + + return $renamed; + } + + $copied = $this->copy($from, $to, $config); + + if ($copied) { + return $this->delete($from); + } + + return false; + } + + /** + * Invoke a plugin on a filesystem mounted on a given prefix. + * + * @param string $method + * @param array $arguments + * @param string $prefix + * + * @throws FilesystemNotFoundException + * + * @return mixed + */ + public function invokePluginOnFilesystem($method, $arguments, $prefix) + { + $filesystem = $this->getFilesystem($prefix); + + try { + return $this->invokePlugin($method, $arguments, $filesystem); + } catch (PluginNotFoundException $e) { + // Let it pass, it's ok, don't panic. + } + + $callback = [$filesystem, $method]; + + return call_user_func_array($callback, $arguments); + } + + /** + * @param string $path + * + * @throws InvalidArgumentException + * + * @return string[] [:prefix, :path] + */ + protected function getPrefixAndPath($path) + { + if (strpos($path, '://') < 1) { + throw new InvalidArgumentException('No prefix detected in path: ' . $path); + } + + return explode('://', $path, 2); + } + + /** + * Check whether a file exists. + * + * @param string $path + * + * @return bool + */ + public function has($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->has($path); + } + + /** + * Read a file. + * + * @param string $path The path to the file. + * + * @throws FileNotFoundException + * + * @return string|false The file contents or false on failure. + */ + public function read($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->read($path); + } + + /** + * Retrieves a read-stream for a path. + * + * @param string $path The path to the file. + * + * @throws FileNotFoundException + * + * @return resource|false The path resource or false on failure. + */ + public function readStream($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->readStream($path); + } + + /** + * Get a file's metadata. + * + * @param string $path The path to the file. + * + * @throws FileNotFoundException + * + * @return array|false The file metadata or false on failure. + */ + public function getMetadata($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->getMetadata($path); + } + + /** + * Get a file's size. + * + * @param string $path The path to the file. + * + * @throws FileNotFoundException + * + * @return int|false The file size or false on failure. + */ + public function getSize($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->getSize($path); + } + + /** + * Get a file's mime-type. + * + * @param string $path The path to the file. + * + * @throws FileNotFoundException + * + * @return string|false The file mime-type or false on failure. + */ + public function getMimetype($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->getMimetype($path); + } + + /** + * Get a file's timestamp. + * + * @param string $path The path to the file. + * + * @throws FileNotFoundException + * + * @return string|false The timestamp or false on failure. + */ + public function getTimestamp($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->getTimestamp($path); + } + + /** + * Get a file's visibility. + * + * @param string $path The path to the file. + * + * @throws FileNotFoundException + * + * @return string|false The visibility (public|private) or false on failure. + */ + public function getVisibility($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->getVisibility($path); + } + + /** + * Write a new file. + * + * @param string $path The path of the new file. + * @param string $contents The file contents. + * @param array $config An optional configuration array. + * + * @throws FileExistsException + * + * @return bool True on success, false on failure. + */ + public function write($path, $contents, array $config = []) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->write($path, $contents, $config); + } + + /** + * Write a new file using a stream. + * + * @param string $path The path of the new file. + * @param resource $resource The file handle. + * @param array $config An optional configuration array. + * + * @throws InvalidArgumentException If $resource is not a file handle. + * @throws FileExistsException + * + * @return bool True on success, false on failure. + */ + public function writeStream($path, $resource, array $config = []) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->writeStream($path, $resource, $config); + } + + /** + * Update an existing file. + * + * @param string $path The path of the existing file. + * @param string $contents The file contents. + * @param array $config An optional configuration array. + * + * @throws FileNotFoundException + * + * @return bool True on success, false on failure. + */ + public function update($path, $contents, array $config = []) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->update($path, $contents, $config); + } + + /** + * Update an existing file using a stream. + * + * @param string $path The path of the existing file. + * @param resource $resource The file handle. + * @param array $config An optional configuration array. + * + * @throws InvalidArgumentException If $resource is not a file handle. + * @throws FileNotFoundException + * + * @return bool True on success, false on failure. + */ + public function updateStream($path, $resource, array $config = []) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->updateStream($path, $resource, $config); + } + + /** + * Rename a file. + * + * @param string $path Path to the existing file. + * @param string $newpath The new path of the file. + * + * @throws FileExistsException Thrown if $newpath exists. + * @throws FileNotFoundException Thrown if $path does not exist. + * + * @return bool True on success, false on failure. + */ + public function rename($path, $newpath) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->rename($path, $newpath); + } + + /** + * Delete a file. + * + * @param string $path + * + * @throws FileNotFoundException + * + * @return bool True on success, false on failure. + */ + public function delete($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->delete($path); + } + + /** + * Delete a directory. + * + * @param string $dirname + * + * @throws RootViolationException Thrown if $dirname is empty. + * + * @return bool True on success, false on failure. + */ + public function deleteDir($dirname) + { + list($prefix, $dirname) = $this->getPrefixAndPath($dirname); + + return $this->getFilesystem($prefix)->deleteDir($dirname); + } + + /** + * Create a directory. + * + * @param string $dirname The name of the new directory. + * @param array $config An optional configuration array. + * + * @return bool True on success, false on failure. + */ + public function createDir($dirname, array $config = []) + { + list($prefix, $dirname) = $this->getPrefixAndPath($dirname); + + return $this->getFilesystem($prefix)->createDir($dirname); + } + + /** + * Set the visibility for a file. + * + * @param string $path The path to the file. + * @param string $visibility One of 'public' or 'private'. + * + * @throws FileNotFoundException + * + * @return bool True on success, false on failure. + */ + public function setVisibility($path, $visibility) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->setVisibility($path, $visibility); + } + + /** + * Create a file or update if exists. + * + * @param string $path The path to the file. + * @param string $contents The file contents. + * @param array $config An optional configuration array. + * + * @return bool True on success, false on failure. + */ + public function put($path, $contents, array $config = []) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->put($path, $contents, $config); + } + + /** + * Create a file or update if exists. + * + * @param string $path The path to the file. + * @param resource $resource The file handle. + * @param array $config An optional configuration array. + * + * @throws InvalidArgumentException Thrown if $resource is not a resource. + * + * @return bool True on success, false on failure. + */ + public function putStream($path, $resource, array $config = []) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->putStream($path, $resource, $config); + } + + /** + * Read and delete a file. + * + * @param string $path The path to the file. + * + * @throws FileNotFoundException + * + * @return string|false The file contents, or false on failure. + */ + public function readAndDelete($path) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->readAndDelete($path); + } + + /** + * Get a file/directory handler. + * + * @deprecated + * + * @param string $path The path to the file. + * @param Handler $handler An optional existing handler to populate. + * + * @return Handler Either a file or directory handler. + */ + public function get($path, Handler $handler = null) + { + list($prefix, $path) = $this->getPrefixAndPath($path); + + return $this->getFilesystem($prefix)->get($path); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/NotSupportedException.php b/plugins/panakour/backup/vendor/league/flysystem/src/NotSupportedException.php new file mode 100644 index 0000000..e0a989b --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/NotSupportedException.php @@ -0,0 +1,37 @@ +getPathname()); + } + + /** + * Create a new exception for a link. + * + * @param string $systemType + * + * @return static + */ + public static function forFtpSystemType($systemType) + { + $message = "The FTP system type '$systemType' is currently not supported."; + + return new static($message); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/AbstractPlugin.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/AbstractPlugin.php new file mode 100644 index 0000000..0d56789 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/AbstractPlugin.php @@ -0,0 +1,24 @@ +filesystem = $filesystem; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/EmptyDir.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/EmptyDir.php new file mode 100644 index 0000000..b5ae7f5 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/EmptyDir.php @@ -0,0 +1,34 @@ +filesystem->listContents($dirname, false); + + foreach ($listing as $item) { + if ($item['type'] === 'dir') { + $this->filesystem->deleteDir($item['path']); + } else { + $this->filesystem->delete($item['path']); + } + } + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ForcedCopy.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ForcedCopy.php new file mode 100644 index 0000000..a41e9f3 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ForcedCopy.php @@ -0,0 +1,44 @@ +filesystem->delete($newpath); + } catch (FileNotFoundException $e) { + // The destination path does not exist. That's ok. + $deleted = true; + } + + if ($deleted) { + return $this->filesystem->copy($path, $newpath); + } + + return false; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ForcedRename.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ForcedRename.php new file mode 100644 index 0000000..3f51cd6 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ForcedRename.php @@ -0,0 +1,44 @@ +filesystem->delete($newpath); + } catch (FileNotFoundException $e) { + // The destination path does not exist. That's ok. + $deleted = true; + } + + if ($deleted) { + return $this->filesystem->rename($path, $newpath); + } + + return false; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/GetWithMetadata.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/GetWithMetadata.php new file mode 100644 index 0000000..6fe4f05 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/GetWithMetadata.php @@ -0,0 +1,51 @@ +filesystem->getMetadata($path); + + if ( ! $object) { + return false; + } + + $keys = array_diff($metadata, array_keys($object)); + + foreach ($keys as $key) { + if ( ! method_exists($this->filesystem, $method = 'get' . ucfirst($key))) { + throw new InvalidArgumentException('Could not fetch metadata: ' . $key); + } + + $object[$key] = $this->filesystem->{$method}($path); + } + + return $object; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListFiles.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListFiles.php new file mode 100644 index 0000000..9669fe7 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListFiles.php @@ -0,0 +1,35 @@ +filesystem->listContents($directory, $recursive); + + $filter = function ($object) { + return $object['type'] === 'file'; + }; + + return array_values(array_filter($contents, $filter)); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListPaths.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListPaths.php new file mode 100644 index 0000000..514bdf0 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListPaths.php @@ -0,0 +1,36 @@ +filesystem->listContents($directory, $recursive); + + foreach ($contents as $object) { + $result[] = $object['path']; + } + + return $result; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListWith.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListWith.php new file mode 100644 index 0000000..d90464e --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/ListWith.php @@ -0,0 +1,60 @@ +filesystem->listContents($directory, $recursive); + + foreach ($contents as $index => $object) { + if ($object['type'] === 'file') { + $missingKeys = array_diff($keys, array_keys($object)); + $contents[$index] = array_reduce($missingKeys, [$this, 'getMetadataByName'], $object); + } + } + + return $contents; + } + + /** + * Get a meta-data value by key name. + * + * @param array $object + * @param string $key + * + * @return array + */ + protected function getMetadataByName(array $object, $key) + { + $method = 'get' . ucfirst($key); + + if ( ! method_exists($this->filesystem, $method)) { + throw new \InvalidArgumentException('Could not get meta-data for key: ' . $key); + } + + $object[$key] = $this->filesystem->{$method}($object['path']); + + return $object; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/PluggableTrait.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/PluggableTrait.php new file mode 100644 index 0000000..922edfe --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/PluggableTrait.php @@ -0,0 +1,97 @@ +plugins[$plugin->getMethod()] = $plugin; + + return $this; + } + + /** + * Find a specific plugin. + * + * @param string $method + * + * @throws PluginNotFoundException + * + * @return PluginInterface + */ + protected function findPlugin($method) + { + if ( ! isset($this->plugins[$method])) { + throw new PluginNotFoundException('Plugin not found for method: ' . $method); + } + + return $this->plugins[$method]; + } + + /** + * Invoke a plugin by method name. + * + * @param string $method + * @param array $arguments + * @param FilesystemInterface $filesystem + * + * @throws PluginNotFoundException + * + * @return mixed + */ + protected function invokePlugin($method, array $arguments, FilesystemInterface $filesystem) + { + $plugin = $this->findPlugin($method); + $plugin->setFilesystem($filesystem); + $callback = [$plugin, 'handle']; + + return call_user_func_array($callback, $arguments); + } + + /** + * Plugins pass-through. + * + * @param string $method + * @param array $arguments + * + * @throws BadMethodCallException + * + * @return mixed + */ + public function __call($method, array $arguments) + { + try { + return $this->invokePlugin($method, $arguments, $this); + } catch (PluginNotFoundException $e) { + throw new BadMethodCallException( + 'Call to undefined method ' + . get_class($this) + . '::' . $method + ); + } + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/PluginNotFoundException.php b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/PluginNotFoundException.php new file mode 100644 index 0000000..fd1d7e7 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Plugin/PluginNotFoundException.php @@ -0,0 +1,10 @@ +hash = spl_object_hash($this); + static::$safeStorage[$this->hash] = []; + } + + public function storeSafely($key, $value) + { + static::$safeStorage[$this->hash][$key] = $value; + } + + public function retrieveSafely($key) + { + if (array_key_exists($key, static::$safeStorage[$this->hash])) { + return static::$safeStorage[$this->hash][$key]; + } + } + + public function __destruct() + { + unset(static::$safeStorage[$this->hash]); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/UnreadableFileException.php b/plugins/panakour/backup/vendor/league/flysystem/src/UnreadableFileException.php new file mode 100644 index 0000000..e668033 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/UnreadableFileException.php @@ -0,0 +1,18 @@ +getRealPath() + ) + ); + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Util.php b/plugins/panakour/backup/vendor/league/flysystem/src/Util.php new file mode 100644 index 0000000..76454a0 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Util.php @@ -0,0 +1,353 @@ + '']; + } + + /** + * Normalize a dirname return value. + * + * @param string $dirname + * + * @return string normalized dirname + */ + public static function normalizeDirname($dirname) + { + return $dirname === '.' ? '' : $dirname; + } + + /** + * Get a normalized dirname from a path. + * + * @param string $path + * + * @return string dirname + */ + public static function dirname($path) + { + return static::normalizeDirname(dirname($path)); + } + + /** + * Map result arrays. + * + * @param array $object + * @param array $map + * + * @return array mapped result + */ + public static function map(array $object, array $map) + { + $result = []; + + foreach ($map as $from => $to) { + if ( ! isset($object[$from])) { + continue; + } + + $result[$to] = $object[$from]; + } + + return $result; + } + + /** + * Normalize path. + * + * @param string $path + * + * @throws LogicException + * + * @return string + */ + public static function normalizePath($path) + { + return static::normalizeRelativePath($path); + } + + /** + * Normalize relative directories in a path. + * + * @param string $path + * + * @throws LogicException + * + * @return string + */ + public static function normalizeRelativePath($path) + { + $path = str_replace('\\', '/', $path); + $path = static::removeFunkyWhiteSpace($path); + + $parts = []; + + foreach (explode('/', $path) as $part) { + switch ($part) { + case '': + case '.': + break; + + case '..': + if (empty($parts)) { + throw new LogicException( + 'Path is outside of the defined root, path: [' . $path . ']' + ); + } + array_pop($parts); + break; + + default: + $parts[] = $part; + break; + } + } + + return implode('/', $parts); + } + + /** + * Removes unprintable characters and invalid unicode characters. + * + * @param string $path + * + * @return string $path + */ + protected static function removeFunkyWhiteSpace($path) + { + // We do this check in a loop, since removing invalid unicode characters + // can lead to new characters being created. + while (preg_match('#\p{C}+|^\./#u', $path)) { + $path = preg_replace('#\p{C}+|^\./#u', '', $path); + } + + return $path; + } + + /** + * Normalize prefix. + * + * @param string $prefix + * @param string $separator + * + * @return string normalized path + */ + public static function normalizePrefix($prefix, $separator) + { + return rtrim($prefix, $separator) . $separator; + } + + /** + * Get content size. + * + * @param string $contents + * + * @return int content size + */ + public static function contentSize($contents) + { + return defined('MB_OVERLOAD_STRING') ? mb_strlen($contents, '8bit') : strlen($contents); + } + + /** + * Guess MIME Type based on the path of the file and it's content. + * + * @param string $path + * @param string|resource $content + * + * @return string|null MIME Type or NULL if no extension detected + */ + public static function guessMimeType($path, $content) + { + $mimeType = MimeType::detectByContent($content); + + if ( ! (empty($mimeType) || in_array($mimeType, ['application/x-empty', 'text/plain', 'text/x-asm']))) { + return $mimeType; + } + + return MimeType::detectByFilename($path); + } + + /** + * Emulate directories. + * + * @param array $listing + * + * @return array listing with emulated directories + */ + public static function emulateDirectories(array $listing) + { + $directories = []; + $listedDirectories = []; + + foreach ($listing as $object) { + list($directories, $listedDirectories) = static::emulateObjectDirectories($object, $directories, $listedDirectories); + } + + $directories = array_diff(array_unique($directories), array_unique($listedDirectories)); + + foreach ($directories as $directory) { + $listing[] = static::pathinfo($directory) + ['type' => 'dir']; + } + + return $listing; + } + + /** + * Ensure a Config instance. + * + * @param null|array|Config $config + * + * @return Config config instance + * + * @throw LogicException + */ + public static function ensureConfig($config) + { + if ($config === null) { + return new Config(); + } + + if ($config instanceof Config) { + return $config; + } + + if (is_array($config)) { + return new Config($config); + } + + throw new LogicException('A config should either be an array or a Flysystem\Config object.'); + } + + /** + * Rewind a stream. + * + * @param resource $resource + */ + public static function rewindStream($resource) + { + if (ftell($resource) !== 0 && static::isSeekableStream($resource)) { + rewind($resource); + } + } + + public static function isSeekableStream($resource) + { + $metadata = stream_get_meta_data($resource); + + return $metadata['seekable']; + } + + /** + * Get the size of a stream. + * + * @param resource $resource + * + * @return int|null stream size + */ + public static function getStreamSize($resource) + { + $stat = fstat($resource); + + if ( ! is_array($stat) || ! isset($stat['size'])) { + return null; + } + + return $stat['size']; + } + + /** + * Emulate the directories of a single object. + * + * @param array $object + * @param array $directories + * @param array $listedDirectories + * + * @return array + */ + protected static function emulateObjectDirectories(array $object, array $directories, array $listedDirectories) + { + if ($object['type'] === 'dir') { + $listedDirectories[] = $object['path']; + } + + if ( ! isset($object['dirname']) || trim($object['dirname']) === '') { + return [$directories, $listedDirectories]; + } + + $parent = $object['dirname']; + + while (isset($parent) && trim($parent) !== '' && ! in_array($parent, $directories)) { + $directories[] = $parent; + $parent = static::dirname($parent); + } + + if (isset($object['type']) && $object['type'] === 'dir') { + $listedDirectories[] = $object['path']; + + return [$directories, $listedDirectories]; + } + + return [$directories, $listedDirectories]; + } + + /** + * Returns the trailing name component of the path. + * + * @param string $path + * + * @return string + */ + private static function basename($path) + { + $separators = DIRECTORY_SEPARATOR === '/' ? '/' : '\/'; + + $path = rtrim($path, $separators); + + $basename = preg_replace('#.*?([^' . preg_quote($separators, '#') . ']+$)#', '$1', $path); + + if (DIRECTORY_SEPARATOR === '/') { + return $basename; + } + // @codeCoverageIgnoreStart + // Extra Windows path munging. This is tested via AppVeyor, but code + // coverage is not reported. + + // Handle relative paths with drive letters. c:file.txt. + while (preg_match('#^[a-zA-Z]{1}:[^\\\/]#', $basename)) { + $basename = substr($basename, 2); + } + + // Remove colon for standalone drive letter names. + if (preg_match('#^[a-zA-Z]{1}:$#', $basename)) { + $basename = rtrim($basename, ':'); + } + + return $basename; + // @codeCoverageIgnoreEnd + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Util/ContentListingFormatter.php b/plugins/panakour/backup/vendor/league/flysystem/src/Util/ContentListingFormatter.php new file mode 100644 index 0000000..ae0d3b9 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Util/ContentListingFormatter.php @@ -0,0 +1,122 @@ +directory = rtrim($directory, '/'); + $this->recursive = $recursive; + $this->caseSensitive = $caseSensitive; + } + + /** + * Format contents listing. + * + * @param array $listing + * + * @return array + */ + public function formatListing(array $listing) + { + $listing = array_filter(array_map([$this, 'addPathInfo'], $listing), [$this, 'isEntryOutOfScope']); + + return $this->sortListing(array_values($listing)); + } + + private function addPathInfo(array $entry) + { + return $entry + Util::pathinfo($entry['path']); + } + + /** + * Determine if the entry is out of scope. + * + * @param array $entry + * + * @return bool + */ + private function isEntryOutOfScope(array $entry) + { + if (empty($entry['path']) && $entry['path'] !== '0') { + return false; + } + + if ($this->recursive) { + return $this->residesInDirectory($entry); + } + + return $this->isDirectChild($entry); + } + + /** + * Check if the entry resides within the parent directory. + * + * @param array $entry + * + * @return bool + */ + private function residesInDirectory(array $entry) + { + if ($this->directory === '') { + return true; + } + + return $this->caseSensitive + ? strpos($entry['path'], $this->directory . '/') === 0 + : stripos($entry['path'], $this->directory . '/') === 0; + } + + /** + * Check if the entry is a direct child of the directory. + * + * @param array $entry + * + * @return bool + */ + private function isDirectChild(array $entry) + { + return $this->caseSensitive + ? $entry['dirname'] === $this->directory + : strcasecmp($this->directory, $entry['dirname']) === 0; + } + + /** + * @param array $listing + * + * @return array + */ + private function sortListing(array $listing) + { + usort($listing, function ($a, $b) { + return strcasecmp($a['path'], $b['path']); + }); + + return $listing; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Util/MimeType.php b/plugins/panakour/backup/vendor/league/flysystem/src/Util/MimeType.php new file mode 100644 index 0000000..84792c5 --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Util/MimeType.php @@ -0,0 +1,248 @@ + 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'csv' => 'text/csv', + 'bin' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'exe' => 'application/octet-stream', + 'class' => 'application/octet-stream', + 'psd' => 'application/x-photoshop', + 'so' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'ai' => 'application/pdf', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => 'application/vnd.ms-excel', + 'xlt' => 'application/vnd.ms-excel', + 'xla' => 'application/vnd.ms-excel', + 'ppt' => 'application/powerpoint', + 'pot' => 'application/vnd.ms-powerpoint', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppa' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', + 'wbxml' => 'application/wbxml', + 'wmlc' => 'application/wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'gzip' => 'application/x-gzip', + 'php' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'php3' => 'application/x-httpd-php', + 'phtml' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'js' => 'application/javascript', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-tar', + 'z' => 'application/x-compress', + 'xhtml' => 'application/xhtml+xml', + 'xht' => 'application/xhtml+xml', + 'rdf' => 'application/rdf+xml', + 'zip' => 'application/x-zip', + 'rar' => 'application/x-rar', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mpga' => 'audio/mpeg', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'aif' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'rv' => 'video/vnd.rn-realvideo', + 'wav' => 'audio/x-wav', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'shtml' => 'text/html', + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'log' => 'text/plain', + 'markdown' => 'text/markdown', + 'md' => 'text/markdown', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'xml' => 'application/xml', + 'xsl' => 'application/xml', + 'dmn' => 'application/octet-stream', + 'bpmn' => 'application/octet-stream', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'docm' => 'application/vnd.ms-word.template.macroEnabled.12', + 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12', + 'dot' => 'application/msword', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'word' => 'application/msword', + 'xl' => 'application/excel', + 'eml' => 'message/rfc822', + 'json' => 'application/json', + 'pem' => 'application/x-x509-user-cert', + 'p10' => 'application/x-pkcs10', + 'p12' => 'application/x-pkcs12', + 'p7a' => 'application/x-pkcs7-signature', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'crt' => 'application/x-x509-ca-cert', + 'crl' => 'application/pkix-crl', + 'der' => 'application/x-x509-ca-cert', + 'kdb' => 'application/octet-stream', + 'pgp' => 'application/pgp', + 'gpg' => 'application/gpg-keys', + 'sst' => 'application/octet-stream', + 'csr' => 'application/octet-stream', + 'rsa' => 'application/x-pkcs7', + 'cer' => 'application/pkix-cert', + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gp', + 'mp4' => 'video/mp4', + 'm4a' => 'audio/x-m4a', + 'f4v' => 'video/mp4', + 'webm' => 'video/webm', + 'aac' => 'audio/x-acc', + 'm4u' => 'application/vnd.mpegurl', + 'm3u' => 'text/plain', + 'xspf' => 'application/xspf+xml', + 'vlc' => 'application/videolan', + 'wmv' => 'video/x-ms-wmv', + 'au' => 'audio/x-au', + 'ac3' => 'audio/ac3', + 'flac' => 'audio/x-flac', + 'ogg' => 'audio/ogg', + 'kmz' => 'application/vnd.google-earth.kmz', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'ics' => 'text/calendar', + 'zsh' => 'text/x-scriptzsh', + '7zip' => 'application/x-7z-compressed', + 'cdr' => 'application/cdr', + 'wma' => 'audio/x-ms-wma', + 'jar' => 'application/java-archive', + 'tex' => 'application/x-tex', + 'latex' => 'application/x-latex', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'webp' => 'image/webp', + ]; + + /** + * Detects MIME Type based on given content. + * + * @param mixed $content + * + * @return string|null MIME Type or NULL if no mime type detected + */ + public static function detectByContent($content) + { + if ( ! class_exists('finfo') || ! is_string($content)) { + return null; + } + try { + $finfo = new finfo(FILEINFO_MIME_TYPE); + + return $finfo->buffer($content) ?: null; + // @codeCoverageIgnoreStart + } catch (ErrorException $e) { + // This is caused by an array to string conversion error. + } + } // @codeCoverageIgnoreEnd + + /** + * Detects MIME Type based on file extension. + * + * @param string $extension + * + * @return string|null MIME Type or NULL if no extension detected + */ + public static function detectByFileExtension($extension) + { + return isset(static::$extensionToMimeTypeMap[$extension]) + ? static::$extensionToMimeTypeMap[$extension] + : 'text/plain'; + } + + /** + * @param string $filename + * + * @return string|null MIME Type or NULL if no extension detected + */ + public static function detectByFilename($filename) + { + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + return empty($extension) ? 'text/plain' : static::detectByFileExtension($extension); + } + + /** + * @return array Map of file extension to MIME Type + */ + public static function getExtensionToMimeTypeMap() + { + return static::$extensionToMimeTypeMap; + } +} diff --git a/plugins/panakour/backup/vendor/league/flysystem/src/Util/StreamHasher.php b/plugins/panakour/backup/vendor/league/flysystem/src/Util/StreamHasher.php new file mode 100644 index 0000000..938ec5d --- /dev/null +++ b/plugins/panakour/backup/vendor/league/flysystem/src/Util/StreamHasher.php @@ -0,0 +1,36 @@ +algo = $algo; + } + + /** + * @param resource $resource + * + * @return string + */ + public function hash($resource) + { + rewind($resource); + $context = hash_init($this->algo); + hash_update_stream($context, $resource); + fclose($resource); + + return hash_final($context); + } +} diff --git a/plugins/panakour/backup/vendor/psr/http-message/CHANGELOG.md b/plugins/panakour/backup/vendor/psr/http-message/CHANGELOG.md new file mode 100644 index 0000000..74b1ef9 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/http-message/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.0.1 - 2016-08-06 + +### Added + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Updated all `@return self` annotation references in interfaces to use + `@return static`, which more closelly follows the semantics of the + specification. +- Updated the `MessageInterface::getHeaders()` return annotation to use the + value `string[][]`, indicating the format is a nested array of strings. +- Updated the `@link` annotation for `RequestInterface::withRequestTarget()` + to point to the correct section of RFC 7230. +- Updated the `ServerRequestInterface::withUploadedFiles()` parameter annotation + to add the parameter name (`$uploadedFiles`). +- Updated a `@throws` annotation for the `UploadedFileInterface::moveTo()` + method to correctly reference the method parameter (it was referencing an + incorrect parameter name previously). + +## 1.0.0 - 2016-05-18 + +Initial stable release; reflects accepted PSR-7 specification. diff --git a/plugins/panakour/backup/vendor/psr/http-message/LICENSE b/plugins/panakour/backup/vendor/psr/http-message/LICENSE new file mode 100644 index 0000000..c2d8e45 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/http-message/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 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/plugins/panakour/backup/vendor/psr/http-message/README.md b/plugins/panakour/backup/vendor/psr/http-message/README.md new file mode 100644 index 0000000..2818533 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/http-message/README.md @@ -0,0 +1,13 @@ +PSR Http Message +================ + +This repository holds all interfaces/classes/traits related to +[PSR-7](http://www.php-fig.org/psr/psr-7/). + +Note that this is not a HTTP message implementation of its own. It is merely an +interface that describes a HTTP message. See the specification for more details. + +Usage +----- + +We'll certainly need some stuff in here. \ No newline at end of file diff --git a/plugins/panakour/backup/vendor/psr/http-message/composer.json b/plugins/panakour/backup/vendor/psr/http-message/composer.json new file mode 100644 index 0000000..b0d2937 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/http-message/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/http-message", + "description": "Common interface for HTTP messages", + "keywords": ["psr", "psr-7", "http", "http-message", "request", "response"], + "homepage": "https://github.com/php-fig/http-message", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/psr/http-message/src/MessageInterface.php b/plugins/panakour/backup/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 0000000..dd46e5e --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,187 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(); + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name); + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name); + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value); + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value); + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader($name); + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(); + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body); +} diff --git a/plugins/panakour/backup/vendor/psr/http-message/src/RequestInterface.php b/plugins/panakour/backup/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 0000000..a96d4fd --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,129 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(); + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query); + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(); + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles); + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data); + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(); + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute($name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute($name, $value); + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute($name); +} diff --git a/plugins/panakour/backup/vendor/psr/http-message/src/StreamInterface.php b/plugins/panakour/backup/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 0000000..f68f391 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,158 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(); + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(); + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(); + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(); + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(); + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(); + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(); + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme($scheme); + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo($user, $password = null); + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost($host); + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort($port); + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath($path); + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery($query); + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment($fragment); + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(); +} diff --git a/plugins/panakour/backup/vendor/psr/log/LICENSE b/plugins/panakour/backup/vendor/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/panakour/backup/vendor/psr/log/Psr/Log/AbstractLogger.php b/plugins/panakour/backup/vendor/psr/log/Psr/Log/AbstractLogger.php new file mode 100644 index 0000000..90e721a --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/psr/log/Psr/Log/InvalidArgumentException.php b/plugins/panakour/backup/vendor/psr/log/Psr/Log/InvalidArgumentException.php new file mode 100644 index 0000000..67f852d --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/log/Psr/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +logger = $logger; + } +} diff --git a/plugins/panakour/backup/vendor/psr/log/Psr/Log/LoggerInterface.php b/plugins/panakour/backup/vendor/psr/log/Psr/Log/LoggerInterface.php new file mode 100644 index 0000000..2206cfd --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/log/Psr/Log/LoggerInterface.php @@ -0,0 +1,125 @@ +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 + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, $message, array $context = array()); +} diff --git a/plugins/panakour/backup/vendor/psr/log/Psr/Log/NullLogger.php b/plugins/panakour/backup/vendor/psr/log/Psr/Log/NullLogger.php new file mode 100644 index 0000000..c8f7293 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/log/Psr/Log/NullLogger.php @@ -0,0 +1,30 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/DummyTest.php b/plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/DummyTest.php new file mode 100644 index 0000000..9638c11 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/DummyTest.php @@ -0,0 +1,18 @@ + ". + * + * 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() + { + $closed = fopen('php://memory', 'r'); + fclose($closed); + + $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'), + 'closed' => $closed, + ); + + $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()); + } +} diff --git a/plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/TestLogger.php b/plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/TestLogger.php new file mode 100644 index 0000000..1be3230 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/log/Psr/Log/Test/TestLogger.php @@ -0,0 +1,147 @@ + $level, + 'message' => $message, + 'context' => $context, + ]; + + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + public function hasRecords($level) + { + return isset($this->recordsByLevel[$level]); + } + + public function hasRecord($record, $level) + { + if (is_string($record)) { + $record = ['message' => $record]; + } + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + return true; + }, $level); + } + + public function hasRecordThatContains($message, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($message) { + return strpos($rec['message'], $message) !== false; + }, $level); + } + + public function hasRecordThatMatches($regex, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($regex) { + return preg_match($regex, $rec['message']) > 0; + }, $level); + } + + public function hasRecordThatPasses(callable $predicate, $level) + { + if (!isset($this->recordsByLevel[$level])) { + return false; + } + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if (call_user_func($predicate, $rec, $i)) { + return true; + } + } + return false; + } + + public function __call($method, $args) + { + if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { + $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; + $level = strtolower($matches[2]); + if (method_exists($this, $genericMethod)) { + $args[] = $level; + return call_user_func_array([$this, $genericMethod], $args); + } + } + throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); + } + + public function reset() + { + $this->records = []; + $this->recordsByLevel = []; + } +} diff --git a/plugins/panakour/backup/vendor/psr/log/README.md b/plugins/panakour/backup/vendor/psr/log/README.md new file mode 100644 index 0000000..a9f20c4 --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/log/README.md @@ -0,0 +1,58 @@ +PSR Log +======= + +This repository holds all interfaces/classes/traits related to +[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +Note that this is not a logger of its own. It is merely an interface that +describes a logger. See the specification for more details. + +Installation +------------ + +```bash +composer require psr/log +``` + +Usage +----- + +If you need a logger, you can use the interface like this: + +```php +logger = $logger; + } + + public function doSomething() + { + if ($this->logger) { + $this->logger->info('Doing work'); + } + + try { + $this->doSomethingElse(); + } catch (Exception $exception) { + $this->logger->error('Oh no!', array('exception' => $exception)); + } + + // do something useful + } +} +``` + +You can then pick one of the implementations of the interface to get a logger. + +If you want to implement the interface, you can require this package and +implement `Psr\Log\LoggerInterface` in your code. Please read the +[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) +for details. diff --git a/plugins/panakour/backup/vendor/psr/log/composer.json b/plugins/panakour/backup/vendor/psr/log/composer.json new file mode 100644 index 0000000..3f6d4ee --- /dev/null +++ b/plugins/panakour/backup/vendor/psr/log/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/log", + "description": "Common interface for logging libraries", + "keywords": ["psr", "psr-3", "log"], + "homepage": "https://github.com/php-fig/log", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/ralouphie/getallheaders/LICENSE b/plugins/panakour/backup/vendor/ralouphie/getallheaders/LICENSE new file mode 100644 index 0000000..be5540c --- /dev/null +++ b/plugins/panakour/backup/vendor/ralouphie/getallheaders/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ralph Khattar + +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/plugins/panakour/backup/vendor/ralouphie/getallheaders/README.md b/plugins/panakour/backup/vendor/ralouphie/getallheaders/README.md new file mode 100644 index 0000000..9430d76 --- /dev/null +++ b/plugins/panakour/backup/vendor/ralouphie/getallheaders/README.md @@ -0,0 +1,27 @@ +getallheaders +============= + +PHP `getallheaders()` polyfill. Compatible with PHP >= 5.3. + +[![Build Status](https://travis-ci.org/ralouphie/getallheaders.svg?branch=master)](https://travis-ci.org/ralouphie/getallheaders) +[![Coverage Status](https://coveralls.io/repos/ralouphie/getallheaders/badge.png?branch=master)](https://coveralls.io/r/ralouphie/getallheaders?branch=master) +[![Latest Stable Version](https://poser.pugx.org/ralouphie/getallheaders/v/stable.png)](https://packagist.org/packages/ralouphie/getallheaders) +[![Latest Unstable Version](https://poser.pugx.org/ralouphie/getallheaders/v/unstable.png)](https://packagist.org/packages/ralouphie/getallheaders) +[![License](https://poser.pugx.org/ralouphie/getallheaders/license.png)](https://packagist.org/packages/ralouphie/getallheaders) + + +This is a simple polyfill for [`getallheaders()`](http://www.php.net/manual/en/function.getallheaders.php). + +## Install + +For PHP version **`>= 5.6`**: + +``` +composer require ralouphie/getallheaders +``` + +For PHP version **`< 5.6`**: + +``` +composer require ralouphie/getallheaders "^2" +``` diff --git a/plugins/panakour/backup/vendor/ralouphie/getallheaders/composer.json b/plugins/panakour/backup/vendor/ralouphie/getallheaders/composer.json new file mode 100644 index 0000000..de8ce62 --- /dev/null +++ b/plugins/panakour/backup/vendor/ralouphie/getallheaders/composer.json @@ -0,0 +1,26 @@ +{ + "name": "ralouphie/getallheaders", + "description": "A polyfill for getallheaders.", + "license": "MIT", + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^5 || ^6.5", + "php-coveralls/php-coveralls": "^2.1" + }, + "autoload": { + "files": ["src/getallheaders.php"] + }, + "autoload-dev": { + "psr-4": { + "getallheaders\\Tests\\": "tests/" + } + } +} diff --git a/plugins/panakour/backup/vendor/ralouphie/getallheaders/src/getallheaders.php b/plugins/panakour/backup/vendor/ralouphie/getallheaders/src/getallheaders.php new file mode 100644 index 0000000..c7285a5 --- /dev/null +++ b/plugins/panakour/backup/vendor/ralouphie/getallheaders/src/getallheaders.php @@ -0,0 +1,46 @@ + 'Content-Type', + 'CONTENT_LENGTH' => 'Content-Length', + 'CONTENT_MD5' => 'Content-Md5', + ); + + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) === 'HTTP_') { + $key = substr($key, 5); + if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) { + $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key)))); + $headers[$key] = $value; + } + } elseif (isset($copy_server[$key])) { + $headers[$copy_server[$key]] = $value; + } + } + + if (!isset($headers['Authorization'])) { + if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { + $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } elseif (isset($_SERVER['PHP_AUTH_USER'])) { + $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; + $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass); + } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { + $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; + } + } + + return $headers; + } + +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/CHANGELOG.md b/plugins/panakour/backup/vendor/sabre/dav/CHANGELOG.md new file mode 100644 index 0000000..932827a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/CHANGELOG.md @@ -0,0 +1,2512 @@ +ChangeLog +========= + +4.1.0 (2020-03-20) +------------------------- +* Support PHP 7.4 +* Drop support for PHP 7.0 +* CalDAV: send MIME-Version header in scheduling emails + +4.0.3 (2020-01-10) +------------------------- +* DAV: Streaming PROPFIND server implementation +* DAVACL: Fix uppercase of NotAuthenticated class +* CalDAV: Return only calendar objects owned by principal itself +* CalDAV: Convert scheduling object data from resource to string +* Browser Plugin: Fix content type guessing if setBaseUri is set to a folder + + +4.0.2 (2019-10-18) +------------------------- +* Fix error with PHP 7.4 +* CardDAV: Fix content-type for Thunderbird + + +4.0.1 (2019-08-20) +------------------------- +* TemporaryFileFilterPlugin: Fix Strict Error +* CalDAV\Plugin: Fix null path + + +4.0.0 (2019-07-01) +------------------------- +* Lock: Support lock timeout value Infinity +* Lock: Hide lock token in lock discovery when not set +* BrowserPlugin: Show display name of nodes +* FSExt: Fix folder (file) move issue if rename fails +* IMipPlugin: Add sender name in invite mail headers +* IMipPlugin: Fix email subject and recipient +* Fix issues with empty content-type header +* Apply new code style +* Fix for litmus test suite - test case: props propfind_invalid2 +* Depend on sabre/xml 2.0.1 +* Depend on sabre/http 5.0 +* Now supports PHP 7.3 +* Now requires PHP 7. +* Using `strict_types` in every php file. +* #896: Using the [sabre/event][evnt] `WildcardEmitter`. This allows event + handlers to listen to events using a wildcard. +* #896: Event listeners that in the past listened to `beforeMethod` or `method` + no longer get called. They must listen to `beforeMethod:*` and `method:*` now. +* #322: Imap authentication backend. (@c0d3z3r0). +* #889: Support for selective property querying in CardDAV's addressbook-query. + (@DeepDiver1975). +* #982: Make sure that files that are siblings of directories, are reported + as files (@nickvergessen) + + +4.0.0-beta1 (2019-05-08) +------------------------- +* Lock: Support lock timeout value Infinity +* Lock: Hide lock token in lock discovery when not set +* BrowserPlugin: Show display name of nodes +* FSExt: Fix folder (file) move issue if rename fails +* IMipPlugin: Add sender name in invite mail headers +* IMipPlugin: Fix email subject and recipient + + +4.0.0-alpha5 (2018-10-15) +------------------------- +* Fix issues with empty content-type header + + +4.0.0-alpha4 (2018-10-12) +------------------------- +* Apply new code style +* Fix for litmus test suite - test case: props propfind_invalid2 +* Depend on sabre/xml 2.0.1 + + +4.0.0-alpha3 (2018-10-05) +------------------------- +* Fixes for PHP 7.3 +* Depend on sabre/http 5.0 + + +4.0.0-alpha2 (2018-09-27) +------------------------- +* Now supports PHP 7.3 + + +4.0.0-alpha1 (2018-06-05) +------------------------- + +* Now requires PHP 7. +* Using `strict_types` in every php file. +* #896: Using the [sabre/event][evnt] `WildcardEmitter`. This allows event + handlers to listen to events using a wildcard. +* #896: Event listeners that in the past listened to `beforeMethod` or `method` + no longer get called. They must listen to `beforeMethod:*` and `method:*` now. +* #322: Imap authentication backend. (@c0d3z3r0). +* #889: Support for selective property querying in CardDAV's addressbook-query. + (@DeepDiver1975). +* #982: Make sure that files that are siblings of directories, are reported + as files (@nickvergessen) + + +3.3.0-alpha1 (2018-06-04) +------------------------- + +* SimpleCollection can now take arrays and strings as argument for super + simple tree creation. +* Added `Sabre\DAV\Server::start()`. This replaces `::exec()`. `::exec()` + is now deprecated, but we're keeping it around for a year or two to make + the transition easier. +* `getChildren()` function in any collection may now return an iterator + instead of an array. This can result in memory savings for large + collections. +* `Tree::getChildren()` now returns an Iterator instead of an array. +* Added `$overrideName` to all `Sabre\DAV\FS` and `Sabre\DAV\FSExt` classes, + so users can specify under what name these nodes show up in the tree. +* #889 Added support for filtering vCard properties in the addressbook-query + REPORT (@DeepDiver1975). +* #918: Add a lot of sqlite indexes. This should speed up sqlite-based + installations quite a bit. +* #982: Make sure that files that are siblings of directories, are reported + as files (@nickvergessen) +* #1058: Don't open file resource on HEAD request (@icewind1991) +* #1031: Fix copyNode for case of file named 0 (@phil-davis) + + +3.2.3 (????-??-??) +------------------ + +* #982: Make sure that files that are siblings of directories, are reported + as files (@nickvergessen) + + +3.2.2 (2017-02-14) +------------------ + +* #943: Fix CardDAV XML reporting bug, which was affecting several CardDAV + clients. Bug was introduced in 3.2.1. +* The zip release ships with [sabre/vobject 4.1.2][vobj], + [sabre/http 4.2.2][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.2.0][uri] and [sabre/xml 1.5.0][xml]. + + +3.2.1 (2017-01-28) +------------------ + +* #877: Fix for syncing large calendars when using the Sqlite PDO backend. + (@theseer). +* #889 Added support for filtering vCard properties in the addressbook-query + REPORT (@DeepDiver1975). +* The zip release ships with [sabre/vobject 4.1.2][vobj], + [sabre/http 4.2.2][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.2.0][uri] and [sabre/xml 1.5.0][xml]. + + +3.2.0 (2016-06-27) +------------------ + +* The default ACL rules allow an unauthenticated user to read information + about nodes that don't have their own ACL defined. This was a security + problem. +* The zip release ships with [sabre/vobject 4.1.0][vobj], + [sabre/http 4.2.1][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.1.0][uri] and [sabre/xml 1.4.2][xml]. + + +3.2.0-beta1 (2016-05-20) +------------------------ + +* #833: Calendars throw exceptions when the sharing plugin is not enabled. +* #834: Return vCards exactly as they were stored if we don't need to convert + in between versions. +* The zip release ships with [sabre/vobject 4.1.0][vobj], + [sabre/http 4.2.1][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.1.0][uri] and [sabre/xml 1.4.1][xml]. + + +3.2.0-alpha1 (2016-05-09) +------------------------- + +* Database changes for CalDAV. If you are using the CalDAV PDO backends, you + must migrate. Run `./bin/migrateto32.php` for more info. +* Support for WebDAV Resource Sharing, an upcoming standard. +* Added support for sharing in the CalDAV PDO backend! Users can now invite + others to their calendar and give them read/read-write access! +* #397: Support for PSR-3. You can now log exceptions with your favourite + psr3-compatible logging tool. +* #825: Actual proper, tested support for PostgreSQL. We require version 9.5. +* Removed database migration script for sabre/dav 1.7. To update from that + version you now first need to update to sabre/dav 3.1. +* Removed deprecated function: `Sabre\DAV\Auth\Plugin::getCurrentUser()`. +* #774: Fixes for getting free disk space on Windows. +* #803: Major changes in the sharing API. If you were using an old sabre/dav + sharing api, head to the website for more detailed migration notes. +* #657: Support for optional auth using `{DAV:}unauthorized` and `{DAV:}all` + privileges. This allows you to assign a privilege to a resource, allowing + non-authenticated users to access it. For instance, this could allow you + to create a public read-only collection. +* #812 #814: ICS/VCF exporter now includes a more useful filename in its + `Content-Disposition` header. (@Xenopathic). +* #801: BC break: If you were using the `Href` object before, it's behavior + now changed a bit, and `LocalHref` was added to replace the old, default + behavior of `Href`. See the migration doc for more info. +* Removed `Sabre\DAVACL\Plugin::$allowAccessToNodesWithoutACL` setting. + Instead, you can provide a set of default ACL rules with + `Sabre\DAVACL\Plugin::setDefaultAcl()`. +* Introduced `Sabre\DAVACL\ACLTrait` which contains a default implementation + of `Sabre\DAV\IACL` with some sane defaults. We're using this trait all over + the place now, reducing the amount of boilerplate. +* Plugins can now control the "Supported Privilege Set". +* Added Sharing, ICSExport and VCFExport plugins to `groupwareserver.php` + example. +* The `{DAV:}all` privilege is now no longer abstract, so it can be assigned + directly. We're using the `{DAV:}all` privilege now in a lot of cases where + we before assigned both `{DAV:}read` and `{DAV:}write`. +* Resources that are not collections no longer support the `{DAV:}bind` and + `{DAV:}unbind` privileges. +* Corrected the CalDAV-scheduling related privileges. +* Doing an `UNLOCK` no longer requires the `{DAV:}write-content` privilege. +* Added a new `getPrincipalByUri` plugin event. Allowing plugins to request + quickly where a principal lives on a server. +* Renamed `phpunit.xml` to `phpunit.xml.dist` to make local modifications easy. +* Functionality from `IShareableCalendar` is merged into `ISharedCalendar`. +* #751: Fixed XML responses from failing `MKCOL` requests. +* #600: Support for `principal-match` ACL `REPORT`. +* #599: Support for `acl-principal-prop-set` ACL `REPORT`. +* #798: Added an index on `firstoccurence` field in MySQL CalDAV backend. This + should speed up common calendar-query requests. +* #759: DAV\Client is now able to actually correctly resolve relative urls. +* #671: We are no longer checking the `read-free-busy` privilege on individual + calendars during freebusy operations in the scheduling plugin. Instead, we + check the `schedule-query-freebusy` privilege on the target users' inbox, + which validates access for the entire account, per the spec. +* The zip release ships with [sabre/vobject 4.1.0][vobj], + [sabre/http 4.2.1][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.1.0][uri] and [sabre/xml 1.4.1][xml]. + + +3.1.5 (????-??-??) +------------------ + +* Fixed: Creating a new calendar on some MySQL configurations caused an error. +* #889 Added support for filtering vCard properties in the addressbook-query + REPORT (@DeepDiver1975). + + + +3.1.4 (2016-05-28) +------------------ + +* #834: Backport from `master`: Return vCards exactly as they were stored if + we don't need to convert in between versions. This should speed up many + large addressbook syncs sometimes up to 50%. +* The zip release ships with [sabre/vobject 4.1.0][vobj], + [sabre/http 4.2.1][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.1.0][uri] and [sabre/xml 1.4.2][xml]. + + +3.1.3 (2016-04-06) +------------------ + +* Set minimum libxml version to 2.7.0 in `composer.json`. +* #805: It wasn't possible to create calendars that hold events, journals and + todos using MySQL, because the `components` column was 1 byte too small. +* The zip release ships with [sabre/vobject 4.1.0][vobj], + [sabre/http 4.2.1][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.1.0][uri] and [sabre/xml 1.4.1][xml]. + + +3.1.2 (2016-03-12) +------------------ + +* #784: Sync logs for address books were not correctly cleaned up after + deleting them. +* #787: Cannot use non-seekable stream-wrappers with range requests. +* Faster XML parsing and generating due to sabre/xml update. +* #793: The Sqlite schema is now more strict and more similar to the MySQL + schema. This solves a problem within Baikal. +* The zip release ships with [sabre/vobject 4.0.3][vobj], + [sabre/http 4.2.1][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.1.0][uri] and [sabre/xml 1.4.1][xml]. + + +3.1.1 (2016-01-25) +------------------ + +* #755: The brower plugin and some operations would break when scheduling and + delegation would both be enabled. +* #757: A bunch of unittest improvements (@jakobsack). +* The zip release ships with [sabre/vobject 4.0.2][vobj], + [sabre/http 4.2.1][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.3.0][xml]. + + +3.1.0 (2016-01-06) +------------------ + +* Better error message when the browser plugin is not enabled. +* Added a super minimal server example. +* #730: Switched all mysql tables to `utf8mb4` character set, allowing you to + use emoji in some tables where you couldn't before. +* #710: Provide an Auth backend that acts as a helper for people implementing + OAuth2 Bearer token. (@fkooman). +* #729: Not all calls to `Sabre\DAV\Tree::getChildren()` were properly cached. +* #727: Added another workaround to make CalDAV work for Windows 10 clients. +* #742: Fixes to make sure that vobject 4 is correctly supported. +* #726: Better error reporting in `Client::propPatch`. We're now throwing + exceptions. +* #608: When a HTTP error is triggered during `Client:propFind`, we're now + throwing `Sabre\HTTP\ClientHttpException` instead of `Sabre\DAV\Exception`. + This new exception contains a LOT more information about the problem. +* #721: Events are now handled in the correct order for `COPY` requests. + Before this subtle bugs could appear that could cause data-loss. +* #747: Now throwing exceptions and setting the HTTP status to 500 in subtle + cases where no other plugin set a correct HTTP status. +* #686: Corrected PDO principal backend's findByURI for email addresses that + don't match the exact capitalization. +* #512: The client now has it's own `User-Agent`. +* #720: Some browser improvements. +* The zip release ships with [sabre/vobject 4.0.1][vobj], + [sabre/http 4.2.1][http], [sabre/event 3.0.0][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.3.0][xml]. + + +3.1.0-alpha2 (2015-09-05) +------------------------- + +* Massive calendars and addressbooks should see a big drop in peak memory + usage. +* Fixed a privilege bug in the availability system. +* #697: Added a "tableName" member to the PropertyStorage PDO backend. (@Frzk). +* #699: PostgreSQL fix for the Locks PDO backend. (@TCKnet) +* Removed the `simplefsserver.php` example file. It's not simple enough. +* #703: PropPatch in client is not correctly encoded. +* #709: Throw exception when running into empty + `supported-calendar-component-set`. +* #711: Don't trigger deserializers for empty elements in `{DAV:}prop`. This + fixes issues when using sabre/dav as a client. +* The zip release ships with [sabre/vobject 4.0.0-alpha2][vobj], + [sabre/http 4.1.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.2.0][xml]. + + +3.1.0-alpha1 (2015-07-19) +------------------------- + +* Now requires PHP 5.5 +* Upgraded to vobject 4, which is a lot faster. +* Support for PHP 7. +* #690: Support for `calendar-availability`, draft 05. + [reference][calendar-availability]. +* #691: Workaround for broken Windows Phone client. +* The zip release ships with [sabre/vobject 4.0.0-alpha1][vobj], + [sabre/http 4.0.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.1.0][xml]. + + +3.0.10 (2016-??-??) +------------------ + +* #889 Added support for filtering vCard properties in the addressbook-query + REPORT (@DeepDiver1975). + + +3.0.9 (2016-04-06) +------------------ + +* Set minimum libxml version to 2.7.0 in `composer.json`. +* #727: Added another workaround to make CalDAV work for Windows 10 clients. +* #805: It wasn't possible to create calendars that hold events, journals and + todos using MySQL, because the `components` column was 1 byte too small. +* The zip release ships with [sabre/vobject 3.5.1][vobj], + [sabre/http 4.2.1][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.1.0][uri] and [sabre/xml 1.4.1][xml]. + + +3.0.8 (2016-03-12) +------------------ + +* #784: Sync logs for address books were not correctly cleaned up after + deleting them. +* #787: Cannot use non-seekable stream-wrappers with range requests. +* Faster XML parsing and generating due to sabre/xml update. +* The zip release ships with [sabre/vobject 3.5.0][vobj], + [sabre/http 4.2.1][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.1.0][uri] and [sabre/xml 1.4.1][xml]. + + +3.0.7 (2016-01-12) +------------------ + +* #752: PHP 7 support for 3.0 branch. (@DeepDiver1975) +* The zip release ships with [sabre/vobject 3.5.0][vobj], + [sabre/http 4.2.1][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.3.0][xml]. + + +3.0.6 (2016-01-04) +------------------ + +* #730: Switched all mysql tables to `utf8mb4` character set, allowing you to + use emoji in some tables where you couldn't before. +* #729: Not all calls to `Sabre\DAV\Tree::getChildren()` were properly cached. +* #734: Return `418 I'm a Teapot` when generating a multistatus response that + has resources with no returned properties. +* #740: Bugs in `migrate20.php` script. +* The zip release ships with [sabre/vobject 3.4.8][vobj], + [sabre/http 4.1.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.3.0][xml]. + + +3.0.5 (2015-09-15) +------------------ + +* #704: Fixed broken uri encoding in multistatus responses. This affected + at least CyberDuck, but probably also others. +* The zip release ships with [sabre/vobject 3.4.7][vobj], +* The zip release ships with [sabre/vobject 3.4.7][vobj], + [sabre/http 4.1.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.2.0][xml]. + + +3.0.4 (2015-09-06) +------------------ + +* #703: PropPatch in client is not correctly encoded. +* #709: Throw exception when running into empty + `supported-calendar-component-set`. +* #711: Don't trigger deserializers for empty elements in `{DAV:}prop`. This + fixes issues when using sabre/dav as a client. +* #705: A `MOVE` request that gets prevented from deleting the source resource + will still remove the target resource. Now all events are triggered before + any destructive operations. +* The zip release ships with [sabre/vobject 3.4.7][vobj], + [sabre/http 4.1.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.2.0][xml]. + + +3.0.3 (2015-08-06) +------------------ + +* #700: Digest Auth fails on `HEAD` requests. +* Fixed example files to no longer use now-deprecated realm argument. +* The zip release ships with [sabre/vobject 3.4.6][vobj], + [sabre/http 4.0.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.1.0][xml]. + + +3.0.2 (2015-07-21) +------------------ + +* #657: Migration script would break when coming a cross an iCalendar object + with no UID. +* #691: Workaround for broken Windows Phone client. +* Fixed a whole bunch of incorrect php docblocks. +* The zip release ships with [sabre/vobject 3.4.5][vobj], + [sabre/http 4.0.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.1.0][xml]. + + +3.0.1 (2015-07-02) +------------------ + +* #674: Postgres sql file fixes. (@davesouthey) +* #677: Resources with the name '0' would not get retrieved when using + `Depth: infinity` in a `PROPFIND` request. +* #680: Fix 'autoprefixing' of dead `{DAV:}href` properties. +* #675: NTLM support in DAV\Client. (@k42b3) +* The zip release ships with [sabre/vobject 3.4.5][vobj], + [sabre/http 4.0.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.1.0][xml]. + + +3.0.0 (2015-06-02) +------------------ + +* No changes since last beta. +* The zip release ships with [sabre/vobject 3.4.5][vobj], + [sabre/http 4.0.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.0.0][xml]. + + +3.0.0-beta3 (2015-05-29) +------------------------ + +* Fixed deserializing href properties with no value. +* Fixed deserializing `{DAV:}propstat` without a `{DAV:}prop`. +* #668: More information about vcf-export-plugin in browser plugin. +* #669: Add export button to browser plugin for address books. (@mgee) +* #670: multiget report hrefs were not decoded. +* The zip release ships with [sabre/vobject 3.4.4][vobj], + [sabre/http 4.0.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.0.0][xml]. + + +3.0.0-beta2 (2015-05-27) +------------------------ + +* A node's properties should not overwrite properties that were already set. +* Some uris were not correctly encoded in notifications. +* The zip release ships with [sabre/vobject 3.4.4][vobj], + [sabre/http 4.0.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.0.0][xml]. + + +3.0.0-beta1 (2015-05-25) +------------------------ + +* `migrate22.php` is now called `migrate30.php`. +* Using php-cs-fixer for automated coding standards enforcement and fixing. +* #660: principals could break html output. +* #662: Fixed several bugs in the `share` request parser. +* #665: Fix a bug in serialization of complex properties in the proppatch + request in the client. +* #666: expand-property report did not correctly prepend the base uri when + generating uris, this caused delegation to break. +* #659: Don't throw errors when when etag-related checks are done on + collections. +* Fully supporting the updated `Prefer` header syntax, as defined in + [rfc7240][rfc7240]. +* The zip release ships with [sabre/vobject 3.4.3][vobj], + [sabre/http 4.0.0][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 1.0.0][xml]. + + +3.0.0-alpha1 (2015-05-19) +------------------------- + +* It's now possible to get all property information from files using the + browser plugin. +* Browser plugin will now show a 'calendar export' button when the + ics-export plugin is enabled. +* Some nodes that by default showed the current time as their last + modification time, now no longer has a last modification time. +* CardDAV namespace was missing from default namespaceMap. +* #646: Properties can now control their own HTML output in the browser plugin. +* #646: Nicer HTML output for the `{DAV:}acl` property. +* Browser plugin no longer shows a few properties that take up a lot of space, + but are likely not really interesting for most users. +* #654: Added a collection, `Sabre\DAVACL\FS\HomeCollection` for automatically + creating a private home collection per-user. +* Changed all MySQL columns from `VARCHAR` to `VARBINARY` where possible. +* Improved older migration scripts a bit to allow easier testing. +* The zip release ships with [sabre/vobject 3.4.3][vobj], + [sabre/http 4.0.0-alpha3][http], [sabre/event 2.0.2][evnt], + [sabre/uri 1.0.1][uri] and [sabre/xml 0.4.3][xml]. + + +2.2.0-alpha4 (2015-04-13) +------------------------- + +* Complete rewrite of the XML system. We now use our own [sabre/xml][xml], + which has a much smarter XML Reader and Writer. +* BC Break: It's no longer possible to instantiate the Locks plugin without + a locks backend. I'm not sure why this ever made sense. +* Simplified the Locking system and fixed a bug related to if tokens checking + locks unrelated to the current request. +* `FSExt` Directory and File no longer do custom property storage. This + functionality is already covered pretty well by the `PropertyStorage` plugin, + so please switch. +* Renamed `Sabre\CardDAV\UserAddressBooks` to `Sabre\CardDAV\AddressBookHome` + to be more consistent with `CalendarHome` as well as the CardDAV + specification. +* `Sabre\DAV\IExtendedCollection` now receives a `Sabre\DAV\MkCol` object as + its second argument, and no longer receives seperate properties and + resourcetype arguments. +* `MKCOL` now integrates better with propertystorage plugins. +* #623: Remove need of temporary files when working with Range requests. + (@dratini0) +* The zip release ships with [sabre/vobject 3.4.2][vobj], + [sabre/http 4.0.0-alpha1][http], [sabre/event 2.0.1][evnt], + [sabre/uri 1.0.0][uri] and [sabre/xml 0.4.3][xml]. + + +2.2.0-alpha3 (2015-02-25) +------------------------- + +* Contains all the changes introduced between 2.1.2 and 2.1.3. +* The zip release ships with [sabre/vobject 3.4.2][vobj], + [sabre/http 4.0.0-alpha1][http], [sabre/event 2.0.1][evnt] and + [sabre/uri 1.0.0][uri]. + + +2.2.0-alpha2 (2015-01-09) +------------------------- + +* Renamed `Sabre\DAV\Auth\Backend\BackendInterface::requireAuth` to + `challenge`, which is a more correct and better sounding name. +* The zip release ships with [sabre/vobject 3.3.5][vobj], + [sabre/http 3.0.4][http], [sabre/event 2.0.1][evnt]. + + +2.2.0-alpha1 (2014-12-10) +------------------------- + +* The browser plugin now has a new page with information about your sabredav + server, and shows information about every plugin that's loaded in the + system. +* #191: The Authentication system can now support multiple authentication + backends. +* Removed: all `$tableName` arguments from every PDO backend. This was already + deprecated, but has now been fully removed. All of these have been replaced + with public properties. +* Deleted several classes that were already deprecated much earlier: + * `Sabre\CalDAV\CalendarRootNode` + * `Sabre\CalDAV\UserCalendars` + * `Sabre\DAV\Exception\FileNotFound` + * `Sabre\DAV\Locks\Backend\FS` + * `Sabre\DAV\PartialUpdate\IFile` + * `Sabre\DAV\URLUtil` +* Removed: `Sabre\DAV\Client::addTrustedCertificates` and + `Sabre\DAV\Client::setVerifyPeer`. +* Removed: `Sabre\DAV\Plugin::getPlugin()` can now no longer return plugins + based on its class name. +* Removed: `Sabre\DAVACL\Plugin::getPrincipalByEmail()`. +* #560: GuessContentType plugin will now set content-type to + `application/octet-stream` if a better content-type could not be determined. +* #568: Added a `componentType` argument to `ICSExportPlugin`, allowing you to + specifically fetch `VEVENT`, `VTODO` or `VJOURNAL`. +* #582: Authentication backend interface changed to be stateless. If you + implemented your own authentication backend, make sure you upgrade your class + to the latest API! +* #582: `Sabre\DAV\Auth\Plugin::getCurrentUser()` is now deprecated. Use + `Sabre\DAV\Auth\Plugin::getCurrentPrincipal()` instead. +* #193: Fix `Sabre\DAV\FSExt\Directory::getQuotaInfo()` on windows. + + +2.1.11 (2016-10-06) +------------------- + +* #805: It wasn't possible to create calendars that hold events, journals and + todos using MySQL, because the `components` column was 1 byte too small. +* The zip release ships with [sabre/vobject 3.5.3][vobj], + [sabre/http 3.0.5][http], and [sabre/event 2.0.2][evnt]. + + +2.1.10 (2016-03-10) +------------------- + +* #784: Sync logs for address books were not correctly cleaned up after + deleting them. +* The zip release ships with [sabre/vobject 3.5.0][vobj], + [sabre/http 3.0.5][http], and [sabre/event 2.0.2][evnt]. + + +2.1.9 (2016-01-25) +------------------ + +* #674: PHP7 support (@DeepDiver1975). +* The zip release ships with [sabre/vobject 3.5.0][vobj], + [sabre/http 3.0.5][http], and [sabre/event 2.0.2][evnt]. + + +2.1.8 (2016-01-04) +------------------ + +* #729: Fixed a caching problem in the Tree object. +* #740: Bugs in `migrate20.php` script. +* The zip release ships with [sabre/vobject 3.4.8][vobj], + [sabre/http 3.0.5][http], and [sabre/event 2.0.2][evnt]. + + +2.1.7 (2015-09-05) +------------------ + +* #705: A `MOVE` request that gets prevented from deleting the source resource + will still remove the target resource. Now all events are triggered before + any destructive operations. +* The zip release ships with [sabre/vobject 3.4.7][vobj], + [sabre/http 3.0.5][http], and [sabre/event 2.0.2][evnt]. + + +2.1.6 (2015-07-21) +------------------ + +* #657: Migration script would break when coming a cross an iCalendar object + with no UID. +* #691: Workaround for broken Windows Phone client. +* The zip release ships with [sabre/vobject 3.4.5][vobj], + [sabre/http 3.0.5][http], and [sabre/event 2.0.2][evnt]. + + +2.1.5 (2015-07-11) +------------------ + +* #677: Resources with the name '0' would not get retrieved when using + `Depth: infinity` in a `PROPFIND` request. +* The zip release ships with [sabre/vobject 3.4.5][vobj], + [sabre/http 3.0.5][http], and [sabre/event 2.0.2][evnt]. + + +2.1.4 (2015-05-25) +------------------ + +* #651: Double-encoded path in the browser plugin. Should fix a few broken + links in some setups. +* #650: Correctly cleaning up change info after deleting calendars (@ErrOrnAmE). +* #658: Updating `schedule-calendar-default-URL` does not work well, so we're + disabling it until there's a better fix. +* The zip release ships with [sabre/vobject 3.4.3][vobj], + [sabre/http 3.0.5][http], and [sabre/event 2.0.2][evnt]. + + +2.1.3 (2015-02-25) +------------------ + +* #586: `SCHEDULE-STATUS` should not contain a reason-phrase. +* #539: Fixed a bug related to scheduling in shared calendars. +* #595: Support for calendar-timezone in iCalendar exports. +* #581: findByUri would send empty prefixes to the principal backend (@soydeedo) +* #611: Escaping a bit more HTML output in the browser plugin. (@LukasReschke) +* #610: Don't allow discovery of arbitrary files using `..` in the browser + plugin (@LukasReschke). +* Browser plugin now shows quota properties. +* #612: PropertyStorage didn't delete properties from nodes when a node's + parents get deleted. +* #581: Fixed problems related to finding attendee information during + scheduling. +* The zip release ships with [sabre/vobject 3.4.2][vobj], + [sabre/http 3.0.4][http], and [sabre/event 2.0.1][evnt]. + + +2.1.2 (2014-12-10) +------------------ + +* #566: Another issue related to the migration script, which would cause + scheduling to not work well for events that were already added before the + migration. +* #567: Doing freebusy requests on accounts that had 0 calendars would throw + a `E_NOTICE`. +* #572: `HEAD` requests trigger a PHP warning. +* #579: Browser plugin can throw exception for a few resourcetypes that didn't + have an icon defined. +* The zip release ships with [sabre/vobject 3.3.4][vobj], + [sabre/http 3.0.4][http], and [sabre/event 2.0.1][evnt]. + + +2.1.1 (2014-11-22) +------------------ + +* #561: IMip Plugin didn't strip mailto: from email addresses. +* #566: Migration process had 2 problems related to adding the `uid` field + to the `calendarobjects` table. +* The zip release ships with [sabre/vobject 3.3.4][vobj], + [sabre/http 3.0.2][http], and [sabre/event 2.0.1][evnt]. + + +2.1.0 (2014-11-19) +------------------ + +* #541: CalDAV PDO backend didn't respect overridden PDO table names. +* #550: Scheduling invites are no longer delivered into shared calendars. +* #554: `calendar-multiget` `REPORT` did not work on inbox items. +* #555: The `calendar-timezone` property is now respected for floating times + and all-day events in the `calendar-query`, `calendar-multiget` and + `free-busy-query` REPORTs. +* #555: The `calendar-timezone` property is also respected for scheduling + free-busy requests. +* #547: CalDAV system too aggressively 'corrects' incoming iCalendar data, and + as a result doesn't return an etag for common cases. +* The zip release ships with [sabre/vobject 3.3.4][vobj], + [sabre/http 3.0.2][http], and [sabre/event 2.0.1][evnt]. + + +2.1.0-alpha2 (2014-10-23) +------------------------- + +* Added: calendar-user-address-set to default principal search properties + list. This should fix iOS attendee autocomplete support. +* Changed: Moved all 'notifications' functionality from `Sabre\CalDAV\Plugin` + to a new plugin: `Sabre\CalDAV\Notifications\Plugin`. If you want to use + notifications-related functionality, just add this plugin. +* Changed: Accessing the caldav inbox, outbox or notification collection no + longer triggers getCalendarsForUser() on backends. +* #533: New invites are no longer delivered to taks-only calendars. +* #538: Added `calendarObjectChange` event. +* Scheduling speedups. +* #539: added `afterResponse` event. (@joserobleda) +* Deprecated: All the "tableName" constructor arguments for all the PDO + backends are now deprecated. They still work, but will be removed in the + next major sabredav version. Every argument that is now deprecated can now + be accessed as a public property on the respective backends. +* #529: Added getCalendarObjectByUID to PDO backend, speeding up scheduling + operations on large calendars. +* The zip release ships with [sabre/vobject 3.3.3][vobj], + [sabre/http 3.0.2][http], and [sabre/event 2.0.1][evnt]. + + +2.1.0-alpha1 (2014-09-23) +------------------------- + +* Added: Support for [rfc6638][rfc6638], also known as CalDAV Scheduling. +* Added: Automatically converting between vCard 3, 4 and jCard using the + `Accept:` header, in CardDAV reports, and automatically converting from + jCard to vCard upon `PUT`. It's important to note that your backends _may_ + now receive both vCard 3.0 and 4.0. +* Added: #444. Collections can now opt-in to support high-speed `MOVE`. +* Changed: PropertyStorage backends now have a `move` method. +* Added: `beforeMove`, and `afterMove` events. +* Changed: A few database changes for the CalDAV PDO backend. Make sure you + run `bin/migrate21.php` to upgrade your database schema. +* Changed: CalDAV backends have a new method: `getCalendarObjectByUID`. This + method MUST be implemented by all backends, but the `AbstractBackend` has a + simple default implementation for this. +* Changed: `Sabre\CalDAV\UserCalendars` has been renamed to + `Sabre\CalDAV\CalendarHome`. +* Changed: `Sabre\CalDAV\CalendarRootNode` has been renamed to + `Sabre\CalDAV\CalendarRoot`. +* Changed: The IMipHandler has been completely removed. With CalDAV scheduling + support, it is no longer needed. It's functionality has been replaced by + `Sabre\CalDAV\Schedule\IMipPlugin`, which can now send emails for clients + other than iCal. +* Removed: `Sabre\DAV\ObjectTree` and `Sabre\DAV\Tree\FileSystem`. All this + functionality has been merged into `Sabre\DAV\Tree`. +* Changed: PrincipalBackend now has a findByUri method. +* Changed: `PrincipalBackend::searchPrincipals` has a new optional `test` + argument. +* Added: Support for the `{http://calendarserver.org/ns/}email-address-set` + property. +* #460: PropertyStorage must move properties during `MOVE` requests. +* Changed: Restructured the zip distribution to be a little bit more lean + and consistent. +* #524: Full support for the `test="anyof"` attribute in principal-search + `REPORT`. +* #472: Always returning lock tokens in the lockdiscovery property. +* Directory entries in the Browser plugin are sorted by type and name. + (@aklomp) +* #486: It's now possible to return additional properties when an 'allprop' + PROPFIND request is being done. (@aklomp) +* Changed: Now return HTTP errors when an addressbook-query REPORT is done + on a uri that's not a vcard. This should help with debugging this common + mistake. +* Changed: `PUT` requests with a `Content-Range` header now emit a 400 status + instead of 501, as per RFC7231. +* Added: Browser plugin can now display the contents of the + `{DAV:}supported-privilege-set` property. +* Added: Now reporting `CALDAV:max-resource-size`, but we're not actively + restricting it yet. +* Changed: CalDAV plugin is now responsible for reporting + `CALDAV:supported-collation-set` and `CALDAV:supported-calendar-data` + properties. +* Added: Now reporting `CARDDAV:max-resource-size`, but we're not actively + restricting it yet. +* Added: Support for `CARDDAV:supported-collation-set`. +* Changed: CardDAV plugin is now responsible for reporting + `CARDDAV:supported-address-data`. This functionality has been removed from + the CardDAV PDO backend. +* When a REPORT is not supported, we now emit HTTP error 415, instead of 403. +* #348: `HEAD` requests now work wherever `GET` also works. +* Changed: Lower priority for the iMip plugins `schedule` event listener. +* Added: #523 Custom CalDAV backends can now mark any calendar as read-only. +* The zip release ships with [sabre/vobject 3.3.3][vobj], + [sabre/http 3.0.0][http], and [sabre/event 2.0.0][evnt]. + + +2.0.9 (2015-09-04) +------------------ + +* #705: A `MOVE` request that gets prevented from deleting the source resource + will still remove the target resource. Now all events are triggered before + any destructive operations. +* The zip release ships with [sabre/vobject 3.4.6][vobj], + [sabre/http 2.0.4][http], and [sabre/event 1.0.1][evnt]. + + + +2.0.8 (2015-07-11) +------------------ + +* #677: Resources with the name '0' would not get retrieved when using + `Depth: infinity` in a `PROPFIND` request. +* The zip release ships with [sabre/vobject 3.3.5][vobj], + [sabre/http 2.0.4][http], and [sabre/event 1.0.1][evnt]. + + +2.0.7 (2015-05-25) +------------------ + +* #650: Correctly cleaning up change info after deleting calendars (@ErrOrnAmE). +* The zip release ships with [sabre/vobject 3.3.4][vobj], + [sabre/http 2.0.4][http], and [sabre/event 1.0.1][evnt]. + + +2.0.6 (2014-12-10) +------------------ + +* Added `Sabre\CalDAV\CalendarRoot` as an alias for + `Sabre\CalDAV\CalendarRootNode`. The latter is going to be deprecated in 2.1, + so this makes it slightly easier to write code that works in both branches. +* #497: Making sure we're initializing the sync-token field with a value after + migration. +* The zip release ships with [sabre/vobject 3.3.4][vobj], + [sabre/http 2.0.4][http], and [sabre/event 1.0.1][evnt]. + + +2.0.5 (2014-10-14) +------------------ + +* #514: CalDAV PDO backend didn't work when overriding the 'calendar changes' + database table name. +* #515: 304 status code was not being sent when checking preconditions. +* The zip release ships with [sabre/vobject 3.3.3][vobj], + [sabre/http 2.0.4][http], and [sabre/event 1.0.1][evnt]. + + +2.0.4 (2014-08-27) +------------------ + +* #483: typo in calendars creation for PostgreSQL. +* #487: Locks are now automatically removed after a node has been deleted. +* #496: Improve CalDAV and CardDAV sync when there is no webdav-sync support. +* Added: Automatically mapping internal sync-tokens to getctag. +* The zip release ships with [sabre/vobject 3.3.1][vobj], + [sabre/http 2.0.4][http], and [sabre/event 1.0.1][evnt]. + + +2.0.3 (2014-07-14) +------------------ + +* #474: Fixed PropertyStorage `pathFilter()`. +* #476: CSP policy incorrect, causing stylesheets to not load in the browser + plugin. +* #475: Href properties in the browser plugin sometimes included a backslash. +* #478: `TooMuchMatches` exception never worked. This was fixed, and we also + took this opportunity to rename it to `TooManyMatches`. +* The zip release ships with [sabre/vobject 3.2.4][vobj], + [sabre/http 2.0.4][http], and [sabre/event 1.0.1][evnt]. + + +2.0.2 (2014-06-12) +------------------ + +* #470: Fixed compatibility with PHP < 5.4.14. +* #467: Fixed a problem in `examples/calendarserver.php`. +* #466: All the postgresql sample files have been updated. +* Fixed: An error would be thrown if a client did a propfind on a node the + user didn't have access to. +* Removed: Old and broken example code from the `examples/` directory. +* The zip release ships with [sabre/vobject 3.2.3][vobj], + [sabre/http 2.0.3][http], and [sabre/event 1.0.1][evnt]. + + +2.0.1 (2014-05-28) +------------------ + +* #459: PROPFIND requests on Files with no Depth header would return a fatal + error. +* #464: A PROPFIND allprops request should not return properties with status + 404. +* The zip release ships with [sabre/vobject 3.2.2][vobj], + [sabre/http 2.0.3][http], and [sabre/event 1.0.0][evnt]. + + +2.0.0 (2014-05-22) +------------------ + +* The zip release ships with [sabre/vobject 3.2.2][vobj], + [sabre/http 2.0.3][http], and [sabre/event 1.0.0][evnt]. +* Fixed: #456: Issue in sqlite migration script. +* Updated: MySQL database schema optimized by using more efficient column types. +* Cleaned up browser design. + + +2.0.0-beta1 (2014-05-15) +------------------------- + +* The zip release ships with [sabre/vobject 3.2.2][vobj], + [sabre/http 2.0.3][http], and [sabre/event 1.0.0][evnt]. +* BC Break: Property updating and fetching got refactored. Read the [migration + document][mi20] for more information. This allows for creation of a generic + property storage, and other property-related functionality that was not + possible before. +* BC Break: Removed `propertyUpdate`, `beforeGetProperties` and + `afterGetProperties` events. +* Fixed: #413: Memory optimizations for the CardDAV PDO backend. +* Updated: Brand new browser plugin with more debugging features and a design + that is slightly less painful. +* Added: Support for the `{DAV:}supported-method-set` property server-wide. +* Making it easier for implementors to override how the CardDAV addressbook + home is located. +* Fixed: Issue #422 Preconditions were not being set on PUT on non-existent + files. Not really a chance for data-loss, but incorrect nevertheless. +* Fixed: Issue #428: Etag check with `If:` fails if the target is a collection. +* Fixed: Issues #430, #431, #433: Locks plugin didn't not properly release + filesystem based locks. +* Fixed: #443. Support for creating new calendar subscriptions for OS X 10.9.2 + and up. +* Removed: `Sabre\DAV\Server::NODE_*` constants. +* Moved all precondition checking into a central place, instead of having to + think about it on a per-method basis. +* jCal transformation for calendar-query REPORT now works again. +* Switched to PSR-4 +* Fixed: #175. Returning ETag header upon a failed `If-Match` or + `If-None-Match` check. +* Removed: `lib/Sabre/autoload.php`. Use `vendor/autoload.php` instead. +* Removed: all the rfc documentation from the sabre/dav source. This made the + package needlessly larger. +* Updated: Issue #439. Lots of updates in PATCH support. The + Sabre_DAV_PartialUpdate_IFile interface is now deprecated and will be + removed in a future version. +* Added: `Sabre\DAV\Exception\LengthRequired`. + +1.9.0-alpha2 (2014-01-14) +------------------------- + +* The zip release ships with sabre/vobject 3.1.3, sabre/http 2.0.1, and + sabre/event 1.0.0. +* Added: Browser can now inspect any node, if ?sabreaction=browser is appended. +* Fixed: Issue #178. Support for multiple items in the Timeout header. +* Fixed: Issue #382. Stricter checking if calendar-query is allowed to run. +* Added: Depth: Infinity support for PROPFIND request. Thanks Thomas Müller and + Markus Goetz. + + +1.9.0-alpha1 (2013-11-07) +------------------------- + +* The zip release ships with sabre/vobject 3.1.3, sabre/http 2.0.0alpha5, and + sabre/event 1.0.0. +* BC Break: The CardDAV and CalDAV BackendInterface each have a new method: + getMultipleCards and getMultipleCalendarObjects. The Abstract and PDO backends + have default implementations, but if you implement that interface directly, + this method is now required. +* BC Break: XML property classes now receive an extra argument in their + unserialize method ($propertyMap). This allows for recursively parsing + properties, if needed. +* BC Break: Now using sabre/event for event emitting/subscription. For plugin + authors this means Server::subscribeEvent is now Server::on, and + Server::broadcastEvent is now Server::emit. +* BC Break: Almost all core functionality moved into a CorePlugin. +* BC Break: Most events triggered by the server got an overhaul. +* Changed: Sabre\HTTP now moved into a dedicated sabre/http package. +* Added: Support for WebDAV-sync (rfc6578). +* Added: Support for caldav-subscriptions, which is an easy way for caldav + clients to manage a list of subscriptions on the server. +* Added: Support for emitting and receiving jCal instead of iCalendar for + CalDAV. +* Added: BasicCallback authenticaton backend, for creating simple authentication + systems without having to define any classes. +* Added: A $transactionType property on the server class. This can be used for + logging and performance measuring purposes. +* Fixed: If event handlers modify the request body from a PUT request, an ETag + is no longer sent back. +* Added: Sabre\DAV\IMultiGet to optimize requests that retrieve information + about lists of resources. +* Added: MultiGet support to default CalDAV and CardDAV backends, speeding up + the multiget and sync reports quite a bit! +* Added: ICSExportPlugin can now generate jCal, filter on time-ranges and expand + recurrences. +* Fixed: Read-only access to calendars still allows the sharee to modify basic + calendar properties, such as the displayname and color. +* Changed: The default supportedPrivilegeSet has changed. Most privileges are no + longer marked as abstract. +* Changed: More elegant ACL management for CalendarObject and Card nodes. +* Added: Browser plugin now marks a carddav directory as type Directory, and a + shared calendar as 'Shared'. +* Added: When debugExceptions is turned on, all previous exceptions are also + traversed. +* Removed: Got rid of the Version classes for CalDAV, CardDAV, HTTP, and DAVACL. + Now that there's no separate packages anymore, this makes a bit more sense. +* Added: Generalized the multistatus response parser a bit more, for better + re-use. +* Added: Sabre\DAV\Client now has support for complex properties for PROPPATCH. + (Issue #299). +* Added: Sabre\DAV\Client has support for gzip and deflate encoding. +* Added: Sabre\DAV\Client now has support for sending objects as streams. +* Added: Deserializer for {DAV:}current-user-privilege-set. +* Added: Addressbooks or backends can now specify custom acl rules when creating + cards. +* Added: The ability for plugins to validate custom tokens in If: headers. +* Changed: Completely refactored the Lock plugin to deal with the new If: header + system. +* Added: Checking preconditions for MOVE, COPY, DELETE and PROPPATCH methods. +* Added: has() method on DAV\Property\SupportedReportSet. +* Added: If header now gets checked (with ETag) all the time. Before the dealing + with the If-header was a responsibility of the Locking plugin. +* Fixed: Outbox access for delegates. +* Added: Issue 333: It's now possible to override the calendar-home in the + CalDAV plugin. +* Added: A negotiateContentType to HTTP\Request. A convenience method. +* Fixed: Issue 349: Denying copying or moving a resource into it's own subtree. +* Fixed: SabreDAV catches every exception again. +* Added: Issue #358, adding a component=vevent parameter to the content-types + for calendar objects, if the caldav backend provides this info. + + +1.8.12-stable (2015-01-21) +-------------------------- + +* The zip release ships with sabre/vobject 2.1.7. +* #568: Support empty usernames and passwords in basic auth. + + +1.8.11 (2014-12-10) +------------------- + +* The zip release ships with sabre/vobject 2.1.6. +* Updated: MySQL database schema optimized by using more efficient column types. +* #516: The DAV client will now only redirect to HTTP and HTTPS urls. + + +1.8.10 (2014-05-15) +------------------- + +* The zip release ships with sabre/vobject 2.1.4. +* includes changes from version 1.7.12. + + +1.8.9 (2014-02-26) +------------------ + +* The zip release ships with sabre/vobject 2.1.3. +* includes changes from version 1.7.11. + + +1.8.8 (2014-02-09) +------------------ + +* includes changes from version 1.7.10. +* The zip release ships with sabre/vobject 2.1.3. + +1.8.7 (2013-10-02) +------------------ + +* the zip release ships with sabre/vobject 2.1.3. +* includes changes from version 1.7.9. + + +1.8.6 (2013-06-18) +------------------ + +* The zip release ships with sabre/vobject 2.1.0. +* Includes changes from version 1.7.8. + + +1.8.5 (2013-04-11) +------------------ + +* The zip release ships with sabre/vobject 2.0.7. +* Includes changes from version 1.7.7. + + +1.8.4 (2013-04-08) +------------------ + +* The zip release ships with sabre/vobject 2.0.7. +* Includes changes from version 1.7.6. + + +1.8.3 (2013-03-01) +------------------ + +* The zip release ships with sabre/vobject 2.0.6. +* Includes changes from version 1.7.5. +* Fixed: organizer email-address for shared calendars is now prefixed with + mailto:, as it should. + + +1.8.2 (2013-01-19) +------------------ + +* The zip release ships with sabre/vobject 2.0.5. +* Includes changes from version 1.7.4. + + +1.8.1 (2012-12-01) +------------------ + +* The zip release ships with sabre/vobject 2.0.5. +* Includes changes from version 1.7.3. +* Fixed: Typo in 1.7 migration script caused it to fail. + + +1.8.0 (2012-11-08) +------------------ + +* The zip release ships with sabre/vobject 2.0.5. +* BC Break: Moved the entire codebase to PHP namespaces. +* BC Break: Every backend package (CalDAV, CardDAV, Auth, Locks, Principals) now + has consistent naming conventions. There's a BackendInterface, and an + AbstractBackend class. +* BC Break: Changed a bunch of constructor signatures in the CalDAV package, to + reduce dependencies on the ACL package. +* BC Break: Sabre_CalDAV_ISharedCalendar now also has a getShares method, so + sharees can figure out who is also on a shared calendar. +* Added: Sabre_DAVACL_IPrincipalCollection interface, to advertise support for + principal-property-search on any node. +* Added: Simple console script to fire up a fileserver in the current directory + using PHP 5.4's built-in webserver. +* Added: Sharee's can now also read out the list of invites for a shared + calendar. +* Added: The Proxy principal classes now both implement an interface, for + greater flexibility. + + +1.7.13 (2014-07-28) +------------------- + +* The zip release ships with sabre/vobject 2.1.4. +* Changed: Removed phing and went with a custom build script for now. + + +1.7.12 (2014-05-15) +------------------- + +* The zip release ships with sabre/vobject 2.1.4. +* Updated: Issue #439. Lots of updates in PATCH support. The + Sabre_DAV_PartialUpdate_IFile interface is now deprecated and will be removed + in a future version. +* Fixed: Restoring old setting after changing libxml_disable_entity_loader. +* Fixed: Issue #422: Preconditions were not being set on PUT on non-existent + files. Not really a chance for data-loss, but incorrect nevertheless. +* Fixed: Issue #427: Now checking preconditions on DELETE requests. +* Fixed: Issue #428: Etag check with If: fails if the target is a collection. +* Fixed: Issue #393: PATCH request with missing end-range was handled + incorrectly. +* Added: Sabre_DAV_Exception_LengthRequired to omit 411 errors. + + +1.7.11 (2014-02-26) +------------------- + +* The zip release ships with sabre/vobject 2.1.3. +* Fixed: Issue #407: large downloads failed. +* Fixed: Issue #414: XXE security problem on older PHP versions. + + +1.7.10 (2014-02-09) +------------------- + +* Fixed: Issue #374: Don't urlescape colon (:) when it's not required. +* Fixed: Potential security vulnerability in the http client. + + +1.7.9 (2013-10-02) +------------------ + +* The zip release ships with sabre/vobject 2.1.3. +* Fixed: Issue #365. Incorrect output when principal urls have spaces in them. +* Added: Issue #367: Automatically adding a UID to vcards that don't have them. + + +1.7.8 (2013-06-17) +------------------ + +* The zip release ships with sabre/vobject 2.1.0. +* Changed: Sabre\DAV\Client::verifyPeer is now a protected property (instead of + private). +* Fixed: Text was incorrectly escaped in the Href and HrefList properties, + disallowing urls with ampersands (&) in them. +* Added: deserializer for Sabre\DAVACL\Property\CurrentUserPrivilegeSet. +* Fixed: Issue 335: Client only deserializes properties with status 200. +* Fixed: Issue 341: Escaping xml in 423 Locked error responses. +* Added: Issue 339: beforeGetPropertiesForPath event. + + +1.7.7 (2013-04-11) +------------------ + +* The zip release ships with sabre/vobject 2.0.7. +* Fixed: Assets in the browser plugins were not being served on windows + machines. + + +1.7.6 (2013-04-08) +------------------ + +* The zip release ships with sabre/vobject 2.0.7. +* Fixed: vcardurl in database schema can now hold 255 characters instead of 80 + (which is often way to small). +* Fixed: The browser plugin potentially allowed people to open any arbitrary + file on windows servers (CVE-2013-1939). + + +1.7.5 (2013-03-01) +------------------ + +* The zip release ships with sabre/vobject 2.0.6. +* Change: No longer advertising support for 4.0 vcards. iOS and OS X address + book don't handle this well, and just advertising 3.0 support seems like the + most logical course of action. +* Added: ->setVerifyPeers to Sabre_DAV_Client (greatly resisting against it, + don't use this..). + + +1.7.4 (2013-01-19) +------------------ + +* The zip release ships with sabre/vobject 2.0.5. +* Changed: To be compatible with MS Office 2011 for Mac, a workaround was + removed that was added to support old versions of Windows XP (pre-SP3). + Indeed! We needed a crazy workaround to work with one MS product in the past, + and we can't keep that workaround to be compatible with another MS product. +* Fixed: expand-properties REPORT had incorrect values for the href element. +* Fixed: Range requests now work for non-seekable streams. (Thanks Alfred + Klomp). +* Fixed: Changed serialization of {DAV:}getlastmodified and {DAV:}supportedlock + to improve compatibility with MS Office 2011 for Mac. +* Changed: reverted the automatic translation of 'DAV:' xml namespaces to + 'urn:DAV' when parsing files. Issues were reported with libxml 2.6.32, on a + relatively recent debian release, so we'll wait till 2015 to take this one out + again. +* Added: Sabre_DAV_Exception_ServiceUnavailable, for emitting 503's. + + +1.7.3 (2012-12-01) +------------------ + +* The zip release ships with sabre/vobject 2.0.5. +* Fixed: Removing double slashes from getPropertiesForPath. +* Change: Marked a few more properties in the CardDAV as protected, instead of + private. +* Fixed: SharingPlugin now plays nicer with other plugins with similar + functionality. +* Fixed: Issue 174. Sending back HTTP/1.0 for requests with this version. + + +1.7.2 (2012-11-08) +------------------ + +* The zip release ships with sabre/vobject 2.0.5. +* Added: ACL plugin advertises support for 'calendarserver-principal- + property-search'. +* Fixed: [#153] Allowing for relative http principals in iMip requests. +* Added: Support for cs:first-name and cs:last-name properties in sharing + invites. +* Fixed: Made a bunch of properties protected, where they were private before. +* Added: Some non-standard properties for sharing to improve compatibility. +* Fixed: some bugfixes in postgres sql script. +* Fixed: When requesting some properties using PROPFIND, they could show up as + both '200 Ok' and '403 Forbidden'. +* Fixed: calendar-proxy principals were not checked for deeper principal + membership than 1 level. +* Fixed: setGroupMemberSet argument now correctly receives relative principal + urls, instead of the absolute ones. +* Fixed: Server class will filter out any bonus properties if any extra were + returned. This means the implementor of the IProperty class can be a bit + lazier when implementing. Note: bug numbers after this line refer to Google + Code tickets. We're using github now. + + +1.7.1 (2012-10-07) +------------------ + +* Fixed: include path problem in the migration script. + + +1.7.0 (2012-10-06) +------------------ + +* BC Break: The calendarobjects database table has a bunch of new fields, and a + migration script is required to ensure everything will keep working. Read the + wiki for more details. +* BC Break: The ICalendar interface now has a new method: calendarQuery. +* BC Break: In this version a number of classes have been deleted, that have + been previously deprecated. Namely: - Sabre_DAV_Directory (now: + Sabre_DAV_Collection) - Sabre_DAV_SimpleDirectory (now: + Sabre_DAV_SimpleCollection) +* BC Break: Sabre_CalDAV_Schedule_IMip::sendMessage now has an extra argument. + If you extended this class, you should fix this method. It's only used for + informational purposes. +* BC Break: The DAV: namespace is no longer converted to urn:DAV. This was a + workaround for a bug in older PHP versions (pre-5.3). +* Removed: Sabre.includes.php was deprecated, and is now removed. +* Removed: Sabre_CalDAV_Server was deprecated, and is now removed. Please use + Sabre_DAV_Server and check the examples in the examples/ directory. +* Changed: The Sabre_VObject library now spawned into it's own project! The + VObject library is still included in the SabreDAV zip package. +* Added: Experimental interfaces to allow implementation of caldav-sharing. Note + that no implementation is provided yet, just the api hooks. +* Added: Free-busy reporting compliant with the caldav-scheduling standard. This + allows iCal and other clients to fetch other users' free-busy data. +* Added: Experimental NotificationSupport interface to add caldav notifications. +* Added: VCF Export plugin. If enabled, it can generate an export of an entire + addressbook. +* Added: Support for PATCH using a SabreDAV format, to live-patch files. +* Added: Support for Prefer: return-minimal and Brief: t headers for PROPFIND + and PROPPATCH requests. +* Changed: Responsibility for dealing with the calendar-query is now moved from + the CalDAV plugin to the CalDAV backends. This allows for heavy optimizations. +* Changed: The CalDAV PDO backend is now a lot faster for common calendar + queries. +* Changed: We are now using the composer autoloader. +* Changed: The CalDAV backend now all implement an interface. +* Changed: Instead of Sabre_DAV_Property, Sabre_DAV_PropertyInterface is now the + basis of every property class. +* Update: Caching results for principal lookups. This should cut down queries + and performance for a number of heavy requests. +* Update: ObjectTree caches lookups much more aggresively, which will help + especially speeding up a bunch of REPORT queries. +* Added: Support for the schedule-calendar-transp property. +* Fixed: Marking both the text/calendar and text/x-vcard as UTF-8 encoded. +* Fixed: Workaround for the SOGO connector, as it doesn't understand receiving + "text/x-vcard; charset=utf-8" for a contenttype. +* Added: Sabre_DAV_Client now throws more specific exceptions in cases where we + already has an exception class. +* Added: Sabre_DAV_PartialUpdate. This plugin allows you to use the PATCH method + to update parts of a file. +* Added: Tons of timezone name mappings for Microsoft Exchange. +* Added: Support for an 'exception' event in the server class. +* Fixed: Uploaded VCards without a UID are now rejected. (thanks Dominik!) +* Fixed: Rejecting calendar objects if they are not in the + supported-calendar-component list. (thanks Armin!) +* Fixed: Issue 219: serialize() now reorders correctly. +* Fixed: Sabre_DAV_XMLUtil no longer returns empty $dom->childNodes if there is + whitespace in $dom. +* Fixed: Returning 409 Conflict instead of 500 when an attempt is made to create + a file as a child of something that's not a collection. +* Fixed: Issue 237: xml-encoding values in SabreDAV error responses. +* Fixed: Returning 403, instead of 501 when an unknown REPORT is requested. +* Fixed: Postfixing slash on {DAV:}owner properties. +* Fixed: Several embarrassing spelling mistakes in docblocks. + + +1.6.10 (2013-06-17) +------------------- + +* Fixed: Text was incorrectly escaped in the Href and HrefList properties, + disallowing urls with ampersands (&) in them. +* Fixed: Issue 341: Escaping xml in 423 Locked error responses. + + +1.6.9 (2013-04-11) +------------------ + +* Fixed: Assets in the browser plugins were not being served on windows + machines. + + +1.6.8 (2013-04-08) +------------------ + +* Fixed: vcardurl in database schema can now hold 255 characters instead of 80 + (which is often way to small). +* Fixed: The browser plugin potentially allowed people to open any arbitrary + file on windows servers. (CVE-2013-1939). + + +1.6.7 (2013-03-01) +------------------ + +* Change: No longer advertising support for 4.0 vcards. iOS and OS X address + book don't handle this well, and just advertising 3.0 support seems like the + most logical course of action. +* Added: ->setVerifyPeers to Sabre_DAV_Client (greatly resisting against it, + don't use this..). + + +1.6.6 (2013-01-19) +------------------ + +* Fixed: Backported a fix for broken XML serialization in error responses. + (Thanks @DeepDiver1975!) + + +1.6.5 (2012-10-04) +------------------ + +* Fixed: Workaround for line-ending bug OS X 10.8 addressbook has. +* Added: Ability to allow users to set SSL certificates for the Client class. + (Thanks schiesbn!). +* Fixed: Directory indexes with lots of nodes should be a lot faster. +* Fixed: Issue 235: E_NOTICE thrown when doing a propfind request with + Sabre_DAV_Client, and no valid properties are returned. +* Fixed: Issue with filtering on alarms in tasks. + + +1.6.4 (2012-08-02) +------------------ + +* Fixed: Issue 220: Calendar-query filters may fail when filtering on alarms, if + an overridden event has it's alarm removed. +* Fixed: Compatibility for OS/X 10.8 iCal in the IMipHandler. +* Fixed: Issue 222: beforeWriteContent shouldn't be called for lock requests. +* Fixed: Problem with POST requests to the outbox if mailto: was not lower + cased. +* Fixed: Yearly recurrence rule expansion on leap-days no behaves correctly. +* Fixed: Correctly checking if recurring, all-day events with no dtstart fall in + a timerange if the start of the time-range exceeds the start of the instance + of an event, but not the end. +* Fixed: All-day recurring events wouldn't match if an occurence ended exactly + on the start of a time-range. +* Fixed: HTTP basic auth did not correctly deal with passwords containing colons + on some servers. +* Fixed: Issue 228: DTEND is now non-inclusive for all-day events in the + calendar-query REPORT and free-busy calculations. + + +1.6.3 (2012-06-12) +------------------ + +* Added: It's now possible to specify in Sabre_DAV_Client which type of + authentication is to be used. +* Fixed: Issue 206: Sabre_DAV_Client PUT requests are fixed. +* Fixed: Issue 205: Parsing an iCalendar 0-second date interval. +* Fixed: Issue 112: Stronger validation of iCalendar objects. Now making sure + every iCalendar object only contains 1 component, and disallowing vcards, + forcing every component to have a UID. +* Fixed: Basic validation for vcards in the CardDAV plugin. +* Fixed: Issue 213: Workaround for an Evolution bug, that prevented it from + updating events. +* Fixed: Issue 211: A time-limit query on a non-relative alarm trigger in a + recurring event could result in an endless loop. +* Fixed: All uri fields are now a maximum of 200 characters. The Bynari outlook + plugin used much longer strings so this should improve compatibility. +* Fixed: Added a workaround for a bug in KDE 4.8.2 contact syncing. See + https://bugs.kde.org/show_bug.cgi?id=300047 +* Fixed: Issue 217: Sabre_DAV_Tree_FileSystem was pretty broken. + + +1.6.2 (2012-04-16) +------------------ + +* Fixed: Sabre_VObject_Node::$parent should have been public. +* Fixed: Recurrence rules of events are now taken into consideration when doing + time-range queries on alarms. +* Fixed: Added a workaround for the fact that php's DateInterval cannot parse + weeks and days at the same time. +* Added: Sabre_DAV_Server::$exposeVersion, allowing you to hide SabreDAV's + version number from various outputs. +* Fixed: DTSTART values would be incorrect when expanding events. +* Fixed: DTSTART and DTEND would be incorrect for expansion of WEEKLY BYDAY + recurrences. +* Fixed: Issue 203: A problem with overridden events hitting the exact date and + time of a subsequent event in the recurrence set. +* Fixed: There was a problem with recurrence rules, for example the 5th tuesday + of the month, if this day did not exist. +* Added: New HTTP status codes from draft-nottingham-http-new-status-04. + + +1.6.1 (2012-03-05) +------------------ + +* Added: createFile and put() can now return an ETag. +* Added: Sending back an ETag on for operations on CardDAV backends. This should + help with OS X 10.6 Addressbook compatibility. +* Fixed: Fixed a bug where an infinite loop could occur in the recurrence + iterator if the recurrence was YEARLY, with a BYMONTH rule, and either BYDAY + or BYMONTHDAY match the first day of the month. +* Fixed: Events that are excluded using EXDATE are still counted in the COUNT= + parameter in the RRULE property. +* Added: Support for time-range filters on VALARM components. +* Fixed: Correctly filtering all-day events. +* Fixed: Sending back correct mimetypes from the browser plugin (thanks + Jürgen). +* Fixed: Issue 195: Sabre_CardDAV pear package had an incorrect dependency. +* Fixed: Calendardata would be destroyed when performing a MOVE request. + + +1.6.0 (2012-02-22) +------------------ + +* BC Break: Now requires PHP 5.3 +* BC Break: Any node that implemented Sabre_DAVACL_IACL must now also implement + the getSupportedPrivilegeSet method. See website for details. +* BC Break: Moved functions from Sabre_CalDAV_XMLUtil to + Sabre_VObject_DateTimeParser. +* BC Break: The Sabre_DAVACL_IPrincipalCollection now has two new methods: + 'searchPrincipals' and 'updatePrincipal'. +* BC Break: Sabre_DAV_ILockable is removed and all related per-node locking + functionality. +* BC Break: Sabre_DAV_Exception_FileNotFound is now deprecated in favor of + Sabre_DAV_Exception_NotFound. The former will be removed in a later version. +* BC Break: Removed Sabre_CalDAV_ICalendarUtil, use Sabre_VObject instead. +* BC Break: Sabre_CalDAV_Server is now deprecated, check out the documentation + on how to setup a caldav server with just Sabre_DAV_Server. +* BC Break: Default Principals PDO backend now needs a new field in the + 'principals' table. See the website for details. +* Added: Ability to create new calendars and addressbooks from within the + browser plugin. +* Added: Browser plugin: icons for various nodes. +* Added: Support for FREEBUSY reports! +* Added: Support for creating principals with admin-level privileges. +* Added: Possibility to let server send out invitation emails on behalf of + CalDAV client, using Sabre_CalDAV_Schedule_IMip. +* Changed: beforeCreateFile event now passes data argument by reference. +* Changed: The 'propertyMap' property from Sabre_VObject_Reader, must now be + specified in Sabre_VObject_Property::$classMap. +* Added: Ability for plugins to tell the ACL plugin which principal plugins are + searchable. +* Added: [DAVACL] Per-node overriding of supported privileges. This allows for + custom privileges where needed. +* Added: [DAVACL] Public 'principalSearch' method on the DAVACL plugin, which + allows for easy searching for principals, based on their properties. +* Added: Sabre_VObject_Component::getComponents() to return a list of only + components and not properties. +* Added: An includes.php file in every sub-package (CalDAV, CardDAV, DAV, + DAVACL, HTTP, VObject) as an alternative to the autoloader. This often works + much faster. +* Added: Support for the 'Me card', which allows Addressbook.app users specify + which vcard is their own. +* Added: Support for updating principal properties in the DAVACL principal + backends. +* Changed: Major refactoring in the calendar-query REPORT code. Should make + things more flexible and correct. +* Changed: The calendar-proxy-[read|write] principals will now only appear in + the tree, if they actually exist in the Principal backend. This should reduce + some problems people have been having with this. +* Changed: Sabre_VObject_Element_* classes are now renamed to + Sabre_VObject_Property. Old classes are retained for backwards compatibility, + but this will be removed in the future. +* Added: Sabre_VObject_FreeBusyGenerator to generate free-busy reports based on + lists of events. +* Added: Sabre_VObject_RecurrenceIterator to find all the dates and times for + recurring events. +* Fixed: Issue 97: Correctly handling RRULE for the calendar-query REPORT. +* Fixed: Issue 154: Encoding of VObject parameters with no value was incorrect. +* Added: Support for {DAV:}acl-restrictions property from RFC3744. +* Added: The contentlength for calendar objects can now be supplied by a CalDAV + backend, allowing for more optimizations. +* Fixed: Much faster implementation of Sabre_DAV_URLUtil::encodePath. +* Fixed: {DAV:}getcontentlength may now be not specified. +* Fixed: Issue 66: Using rawurldecode instead of urldecode to decode paths from + clients. This means that + will now be treated as a literal rather than a + space, and this should improve compatibility with the Windows built-in client. +* Added: Sabre_DAV_Exception_PaymentRequired exception, to emit HTTP 402 status + codes. +* Added: Some mysql unique constraints to example files. +* Fixed: Correctly formatting HTTP dates. +* Fixed: Issue 94: Sending back Last-Modified header for 304 responses. +* Added: Sabre_VObject_Component_VEvent, Sabre_VObject_Component_VJournal, + Sabre_VObject_Component_VTodo and Sabre_VObject_Component_VCalendar. +* Changed: Properties are now also automatically mapped to their appropriate + classes, if they are created using the add() or __set() methods. +* Changed: Cloning VObject objects now clones the entire tree, rather than just + the default shallow copy. +* Added: Support for recurrence expansion in the CALDAV:calendar-multiget and + CALDAV:calendar-query REPORTS. +* Changed: CalDAV PDO backend now sorts calendars based on the internal + 'calendarorder' field. +* Added: Issue 181: Carddav backends may no optionally not supply the carddata + in getCards, if etag and size are specified. This may speed up certain + requests. +* Added: More arguments to beforeWriteContent and beforeCreateFile (see + WritingPlugins wiki document). +* Added: Hook for iCalendar validation. This allows us to validate iCalendar + objects when they're uploaded. At the moment we're just validating syntax. +* Added: VObject now support Windows Timezone names correctly (thanks mrpace2). +* Added: If a timezonename could not be detected, we fall back on the default + PHP timezone. +* Added: Now a Composer package (thanks willdurand). +* Fixed: Support for \N as a newline character in the VObject reader. +* Added: afterWriteContent, afterCreateFile and afterUnbind events. +* Added: Postgresql example files. Not part of the unittests though, so use at + your own risk. +* Fixed: Issue 182: Removed backticks from sql queries, so it will work with + Postgres. + + +1.5.9 (2012-04-16) +------------------ + +* Fixed: Issue with parsing timezone identifiers that were surrounded by quotes. + (Fixes emClient compatibility). + + +1.5.8 (2012-02-22) +------------------ + +* Fixed: Issue 95: Another timezone parsing issue, this time in calendar-query. + + +1.5.7 (2012-02-19) +------------------ + +* Fixed: VObject properties are now always encoded before components. +* Fixed: Sabre_DAVACL had issues with multiple levels of privilege aggregration. +* Changed: Added 'GuessContentType' plugin to fileserver.php example. +* Fixed: The Browser plugin will now trigger the correct events when creating + files. +* Fixed: The ICSExportPlugin now considers ACL's. +* Added: Made it optional to supply carddata from an Addressbook backend when + requesting getCards. This can make some operations much faster, and could + result in much lower memory use. +* Fixed: Issue 187: Sabre_DAV_UUIDUtil was missing from includes file. +* Fixed: Issue 191: beforeUnlock was triggered twice. + + +1.5.6 (2012-01-07) +------------------ + +* Fixed: Issue 174: VObject could break UTF-8 characters. +* Fixed: pear package installation issues. + + +1.5.5 (2011-12-16) +------------------ + +* Fixed: CalDAV time-range filter workaround for recurring events. +* Fixed: Bug in Sabre_DAV_Locks_Backend_File that didn't allow multiple files to + be locked at the same time. + + +1.5.4 (2011-10-28) +------------------ + +* Fixed: GuessContentType plugin now supports mixed case file extensions. +* Fixed: DATE-TIME encoding was wrong in VObject. (we used 'DATETIME'). +* Changed: Sending back HTTP 204 after a PUT request on an existing resource + instead of HTTP 200. This should fix Evolution CardDAV client compatibility. +* Fixed: Issue 95: Parsing X-LIC-LOCATION if it's available. +* Added: All VObject elements now have a reference to their parent node. + + +1.5.3 (2011-09-28) +------------------ + +* Fixed: Sabre_DAV_Collection was missing from the includes file. +* Fixed: Issue 152. iOS 1.4.2 apparantly requires HTTP/1.1 200 OK to be in + uppercase. +* Fixed: Issue 153: Support for files with mixed newline styles in + Sabre_VObject. +* Fixed: Issue 159: Automatically converting any vcard and icalendardata to + UTF-8. +* Added: Sabre_DAV_SimpleFile class for easy static file creation. +* Added: Issue 158: Support for the CARDDAV:supported-address-data property. + + +1.5.2 (2011-09-21) +------------------ + +* Fixed: carddata and calendardata MySQL fields are now of type 'mediumblob'. + 'TEXT' was too small sometimes to hold all the data. +* Fixed: {DAV:}supported-report-set is now correctly reporting the reports for + IAddressBook. +* Added: Sabre_VObject_Property::add() to add duplicate parameters to + properties. +* Added: Issue 151: Sabre_CalDAV_ICalendar and Sabre_CalDAV_ICalendarObject + interfaces. +* Fixed: Issue 140: Not returning 201 Created if an event cancelled the creation + of a file. +* Fixed: Issue 150: Faster URLUtil::encodePath() implementation. +* Fixed: Issue 144: Browser plugin could interfere with + TemporaryFileFilterPlugin if it was loaded first. +* Added: It's not possible to specify more 'alternate uris' in principal + backends. + + +1.5.1 (2011-08-24) +------------------ + +* Fixed: Issue 137. Hiding action interface in HTML browser for non-collections. +* Fixed: addressbook-query is now correctly returned from the + {DAV:}supported-report-set property. +* Fixed: Issue 142: Bugs in groupwareserver.php example. +* Fixed: Issue 139: Rejecting PUT requests with Content-Range. + + +1.5.0 (2011-08-12) +------------------ + +* Added: CardDAV support. +* Added: An experimental WebDAV client. +* Added: MIME-Directory grouping support in the VObject library. This is very + useful for people attempting to parse vcards. +* BC Break: Adding parameters with the VObject libraries now overwrites the + previous parameter, rather than just add it. This makes more sense for 99% of + the cases. +* BC Break: lib/Sabre.autoload.php is now removed in favor of + lib/Sabre/autoload.php. +* Deprecated: Sabre_DAV_Directory is now deprecated and will be removed in a + future version. Use Sabre_DAV_Collection instead. +* Deprecated: Sabre_DAV_SimpleDirectory is now deprecated and will be removed in + a future version. Use Sabre_DAV_SimpleCollection instead. +* Fixed: Problem with overriding tablenames for the CalDAV backend. +* Added: Clark-notation parser to XML utility. +* Added: unset() support to VObject components. +* Fixed: Refactored CalDAV property fetching to be faster and simpler. +* Added: Central string-matcher for CalDAV and CardDAV plugins. +* Added: i;unicode-casemap support +* Fixed: VObject bug: wouldn't parse parameters if they weren't specified in + uppercase. +* Fixed: VObject bug: Parameters now behave more like Properties. +* Fixed: VObject bug: Parameters with no value are now correctly parsed. +* Changed: If calendars don't specify which components they allow, 'all' + components are assumed (e.g.: VEVENT, VTODO, VJOURNAL). +* Changed: Browser plugin now uses POST variable 'sabreAction' instead of + 'action' to reduce the chance of collisions. + + +1.4.4 (2011-07-07) +------------------ + +* Fixed: Issue 131: Custom CalDAV backends could break in certain cases. +* Added: The option to override the default tablename all PDO backends use. + (Issue 60). +* Fixed: Issue 124: 'File' authentication backend now takes realm into + consideration. +* Fixed: Sabre_DAV_Property_HrefList now properly deserializes. This allows + users to update the {DAV:}group-member-set property. +* Added: Helper functions for DateTime-values in Sabre_VObject package. +* Added: VObject library can now automatically map iCalendar properties to + custom classes. + + +1.4.3 (2011-04-25) +------------------ + +* Fixed: Issue 123: Added workaround for Windows 7 UNLOCK bug. +* Fixed: datatype of lastmodified field in mysql.calendars.sql. Please change + the DATETIME field to an INT to ensure this field will work correctly. +* Change: Sabre_DAV_Property_Principal is now renamed to + Sabre_DAVACL_Property_Principal. +* Added: API level support for ACL HTTP method. +* Fixed: Bug in serializing {DAV:}acl property. +* Added: deserializer for {DAV:}resourcetype property. +* Added: deserializer for {DAV:}acl property. +* Added: deserializer for {DAV:}principal property. + + +1.4.2-beta (2011-04-01) +----------------------- + +* Added: It's not possible to disable listing of nodes that are denied read + access by ACL. +* Fixed: Changed a few properties in CalDAV classes from private to protected. +* Fixed: Issue 119: Terrible things could happen when relying on guessBaseUri, + the server was running on the root of the domain and a user tried to access a + file ending in .php. This is a slight BC break. +* Fixed: Issue 118: Lock tokens in If headers without a uri should be treated as + the request uri, not 'all relevant uri's. +* Fixed: Issue 120: PDO backend was incorrectly fetching too much locks in cases + where there were similar named locked files in a directory. + + +1.4.1-beta (2011-02-26) +----------------------- + +* Fixed: Sabre_DAV_Locks_Backend_PDO returned too many locks. +* Fixed: Sabre_HTTP_Request::getHeader didn't return Content-Type when running + on apache, so a few workarounds were added. +* Change: Slightly changed CalDAV Backend API's, to allow for heavy + optimizations. This is non-bc breaking. + + +1.4.0-beta (2011-02-12) +----------------------- + +* Added: Partly RFC3744 ACL support. +* Added: Calendar-delegation (caldav-proxy) support. +* BC break: In order to fix Issue 99, a new argument had to be added to + Sabre_DAV_Locks_Backend_*::getLocks classes. Consult the classes for details. +* Deprecated: Sabre_DAV_Locks_Backend_FS is now deprecated and will be removed + in a later version. Use PDO or the new File class instead. +* Deprecated: The Sabre_CalDAV_ICalendarUtil class is now marked deprecated, and + will be removed in a future version. Please use Sabre_VObject instead. +* Removed: All principal-related functionality has been removed from the + Sabre_DAV_Auth_Plugin, and moved to the Sabre_DAVACL_Plugin. +* Added: VObject library, for easy vcard/icalendar parsing using a natural + interface. +* Added: Ability to automatically generate full .ics feeds off calendars. To + use: Add the Sabre_CalDAV_ICSExportPlugin, and add ?export to your calendar + url. +* Added: Plugins can now specify a pluginname, for easy access using + Sabre_DAV_Server::getPlugin(). +* Added: beforeGetProperties event. +* Added: updateProperties event. +* Added: Principal listings and calendar-access can now be done privately, + disallowing users from accessing or modifying other users' data. +* Added: You can now pass arrays to the Sabre_DAV_Server constructor. If it's an + array with node-objects, a Root collection will automatically be created, and + the nodes are used as top-level children. +* Added: The principal base uri is now customizable. It used to be hardcoded to + 'principals/[user]'. +* Added: getSupportedReportSet method in ServerPlugin class. This allows you to + easily specify which reports you're implementing. +* Added: A '..' link to the HTML browser. +* Fixed: Issue 99: Locks on child elements were ignored when their parent nodes + were deleted. +* Fixed: Issue 90: lockdiscovery property and LOCK response now include a + {DAV}lockroot element. +* Fixed: Issue 96: support for 'default' collation in CalDAV text-match filters. +* Fixed: Issue 102: Ensuring that copy and move with identical source and + destination uri's fails. +* Fixed: Issue 105: Supporting MKCALENDAR with no body. +* Fixed: Issue 109: Small fixes in Sabre_HTTP_Util. +* Fixed: Issue 111: Properly catching the ownername in a lock (if it's a string) +* Fixed: Sabre_DAV_ObjectTree::nodeExist always returned false for the root + node. +* Added: Global way to easily supply new resourcetypes for certain node classes. +* Fixed: Issue 59: Allowing the user to override the authentication realm in + Sabre_CalDAV_Server. +* Update: Issue 97: Looser time-range checking if there's a recurrence rule in + an event. This fixes 'missing recurring events'. + + +1.3.0 (2010-10-14) +------------------ + +* Added: childExists method to Sabre_DAV_ICollection. This is an api break, so + if you implement Sabre_DAV_ICollection directly, add the method. +* Changed: Almost all HTTP method implementations now take a uri argument, + including events. This allows for internal rerouting of certain calls. If you + have custom plugins, make sure they use this argument. If they don't, they + will likely still work, but it might get in the way of future changes. +* Changed: All getETag methods MUST now surround the etag with double-quotes. + This was a mistake made in all previous SabreDAV versions. If you don't do + this, any If-Match, If-None-Match and If: headers using Etags will work + incorrectly. (Issue 85). +* Added: Sabre_DAV_Auth_Backend_AbstractBasic class, which can be used to easily + implement basic authentication. +* Removed: Sabre_DAV_PermissionDenied class. Use Sabre_DAV_Forbidden instead. +* Removed: Sabre_DAV_IDirectory interface, use Sabre_DAV_ICollection instead. +* Added: Browser plugin now uses {DAV:}displayname if this property is + available. +* Added: Cache layer in the ObjectTree. +* Added: Tree classes now have a delete and getChildren method. +* Fixed: If-Modified-Since and If-Unmodified-Since would be incorrect if the + date is an exact match. +* Fixed: Support for multiple ETags in If-Match and If-None-Match headers. +* Fixed: Improved baseUrl handling. +* Fixed: Issue 67: Non-seekable stream support in ::put()/::get(). +* Fixed: Issue 65: Invalid dates are now ignored. +* Updated: Refactoring in Sabre_CalDAV to make everything a bit more ledgable. +* Fixed: Issue 88, Issue 89: Fixed compatibility for running SabreDAV on + Windows. +* Fixed: Issue 86: Fixed Content-Range top-boundary from 'file size' to 'file + size'-1. + + +1.2.5 (2010-08-18) +------------------ + +* Fixed: Issue 73: guessBaseUrl fails for some servers. +* Fixed: Issue 67: SabreDAV works better with non-seekable streams. +* Fixed: If-Modified-Since and If-Unmodified-Since would be incorrect if + the date is an exact match. + + +1.2.4 (2010-07-13) +------------------ + +* Fixed: Issue 62: Guessing baseUrl fails when url contains a query-string. +* Added: Apache configuration sample for CGI/FastCGI setups. +* Fixed: Issue 64: Only returning calendar-data when it was actually requested. + + +1.2.3 (2010-06-26) +------------------ + +* Fixed: Issue 57: Supporting quotes around etags in If-Match and If-None-Match + + +1.2.2 (2010-06-21) +------------------ + +* Updated: SabreDAV now attempts to guess the BaseURI if it's not set. +* Updated: Better compatibility with BitKinex +* Fixed: Issue 56: Incorrect behaviour for If-None-Match headers and GET + requests. +* Fixed: Issue with certain encoded paths in Browser Plugin. + + +1.2.1 (2010-06-07) +------------------ + +* Fixed: Issue 50, patch by Mattijs Hoitink. +* Fixed: Issue 51, Adding windows 7 lockfiles to TemporaryFileFilter. +* Fixed: Issue 38, Allowing custom filters to be added to TemporaryFileFilter. +* Fixed: Issue 53, ETags in the If: header were always failing. This behaviour + is now corrected. +* Added: Apache Authentication backend, in case authentication through .htaccess + is desired. +* Updated: Small improvements to example files. + + +1.2.0 (2010-05-24) +------------------ + +* Fixed: Browser plugin now displays international characters. +* Changed: More properties in CalDAV classes are now protected instead of + private. + + +1.2.0beta3 (2010-05-14) +----------------------- + +* Fixed: Custom properties were not properly sent back for allprops requests. +* Fixed: Issue 49, incorrect parsing of PROPPATCH, affecting Office 2007. +* Changed: Removed CalDAV items from includes.php, and added a few missing ones. + + +1.2.0beta2 (2010-05-04) +----------------------- + +* Fixed: Issue 46: Fatal error for some non-existent nodes. +* Updated: some example sql to include email address. +* Added: 208 and 508 statuscodes from RFC5842. +* Added: Apache2 configuration examples + + +1.2.0beta1 (2010-04-28) +----------------------- + +* Fixed: redundant namespace declaration in resourcetypes. +* Fixed: 2 locking bugs triggered by litmus when no Sabre_DAV_ILockable + interface is used. +* Changed: using http://sabredav.org/ns for all custom xml properties. +* Added: email address property to principals. +* Updated: CalendarObject validation. + + +1.2.0alpha4 (2010-04-24) +------------------------ + +* Added: Support for If-Range, If-Match, If-None-Match, If-Modified-Since, + If-Unmodified-Since. +* Changed: Brand new build system. Functionality is split up between Sabre, + Sabre_HTTP, Sabre_DAV and Sabre_CalDAV packages. In addition to that a new + non-pear package will be created with all this functionality combined. +* Changed: Autoloader moved to Sabre/autoload.php. +* Changed: The Allow: header is now more accurate, with appropriate HTTP methods + per uri. +* Changed: Now throwing back Sabre_DAV_Exception_MethodNotAllowed on a few + places where Sabre_DAV_Exception_NotImplemented was used. + + +1.2.0alpha3 (2010-04-20) +------------------------ + +* Update: Complete rewrite of property updating. Now easier to use and atomic. +* Fixed: Issue 16, automatically adding trailing / to baseUri. +* Added: text/plain is used for .txt files in GuessContentType plugin. +* Added: support for principal-property-search and principal-search-property-set + reports. +* Added: Issue 31: Hiding exception information by default. Can be turned on + with the Sabre_DAV_Server::$debugExceptions property. + + +1.2.0alpha2 (2010-04-08) +------------------------ + +* Added: Calendars are now private and can only be read by the owner. +* Fixed: double namespace declaration in multistatus responses. +* Added: MySQL database dumps. MySQL is now also supported next to SQLite. +* Added: expand-properties REPORT from RFC 3253. +* Added: Sabre_DAV_Property_IHref interface for properties exposing urls. +* Added: Issue 25: Throwing error on broken Finder behaviour. +* Changed: Authentication backend is now aware of current user. + + +1.2.0alpha1 (2010-03-31) +------------------------ + +* Fixed: Issue 26: Workaround for broken GVFS behaviour with encoded special + characters. +* Fixed: Issue 34: Incorrect Lock-Token response header for LOCK. Fixes Office + 2010 compatibility. +* Added: Issue 35: SabreDAV version to header to OPTIONS response to ease + debugging. +* Fixed: Issue 36: Incorrect variable name, throwing error in some requests. +* Fixed: Issue 37: Incorrect smultron regex in temporary filefilter. +* Fixed: Issue 33: Converting ISO-8859-1 characters to UTF-8. +* Fixed: Issue 39 & Issue 40: Basename fails on non-utf-8 locales. +* Added: More unittests. +* Added: SabreDAV version to all error responses. +* Added: URLUtil class for decoding urls. +* Changed: Now using pear.sabredav.org pear channel. +* Changed: Sabre_DAV_Server::getCopyAndMoveInfo is now a public method. + + +1.1.2-alpha (2010-03-18) +------------------------ + +* Added: RFC5397 - current-user-principal support. +* Fixed: Issue 27: encoding entities in property responses. +* Added: naturalselection script now allows the user to specify a 'minimum + number of bytes' for deletion. This should reduce load due to less crawling +* Added: Full support for the calendar-query report. +* Added: More unittests. +* Added: Support for complex property deserialization through the static + ::unserialize() method. +* Added: Support for modifying calendar-component-set +* Fixed: Issue 29: Added TIMEOUT_INFINITE constant + + +1.1.1-alpha (2010-03-11) +------------------------ + +* Added: RFC5689 - Extended MKCOL support. +* Fixed: Evolution support for CalDAV. +* Fixed: PDO-locks backend was pretty much completely broken. This is 100% + unittested now. +* Added: support for ctags. +* Fixed: Comma's between HTTP methods in 'Allow' method. +* Changed: default argument for Sabre_DAV_Locks_Backend_FS. This means a + datadirectory must always be specified from now on. +* Changed: Moved Sabre_DAV_Server::parseProps to + Sabre_DAV_XMLUtil::parseProperties. +* Changed: Sabre_DAV_IDirectory is now Sabre_DAV_ICollection. +* Changed: Sabre_DAV_Exception_PermissionDenied is now + Sabre_DAV_Exception_Forbidden. +* Changed: Sabre_CalDAV_ICalendarCollection is removed. +* Added: Sabre_DAV_IExtendedCollection. +* Added: Many more unittests. +* Added: support for calendar-timezone property. + + +1.1.0-alpha (2010-03-01) +------------------------ + +* Note: This version is forked from version 1.0.5, so release dates may be out + of order. +* Added: CalDAV - RFC 4791 +* Removed: Sabre_PHP_Exception. PHP has a built-in ErrorException for this. +* Added: PDO authentication backend. +* Added: Example sql for auth, caldav, locks for sqlite. +* Added: Sabre_DAV_Browser_GuessContentType plugin +* Changed: Authentication plugin refactored, making it possible to implement + non-digest authentication. +* Fixed: Better error display in browser plugin. +* Added: Support for {DAV:}supported-report-set +* Added: XML utility class with helper functions for the WebDAV protocol. +* Added: Tons of unittests +* Added: PrincipalCollection and Principal classes +* Added: Sabre_DAV_Server::getProperties for easy property retrieval +* Changed: {DAV:}resourceType defaults to 0 +* Changed: Any non-null resourceType now gets a / appended to the href value. + Before this was just for {DAV:}collection's, but this is now also the case for + for example {DAV:}principal. +* Changed: The Href property class can now optionally create non-relative uri's. +* Changed: Sabre_HTTP_Response now returns false if headers are already sent and + header-methods are called. +* Fixed: Issue 19: HEAD requests on Collections +* Fixed: Issue 21: Typo in Sabre_DAV_Property_Response +* Fixed: Issue 18: Doesn't work with Evolution Contacts + + +1.0.15 (2010-05-28) +------------------- + +* Added: Issue 31: Hiding exception information by default. Can be turned on + with the Sabre_DAV_Server::$debugExceptions property. +* Added: Moved autoload from lib/ to lib/Sabre/autoload.php. This is also the + case in the upcoming 1.2.0, so it will improve future compatibility. + + +1.0.14 (2010-04-15) +------------------- + +* Fixed: double namespace declaration in multistatus responses. + + +1.0.13 (2010-03-30) +------------------- + +* Fixed: Issue 40: Last references to basename/dirname + + +1.0.12 (2010-03-30) +------------------- + +* Fixed: Issue 37: Incorrect smultron regex in temporary filefilter. +* Fixed: Issue 26: Workaround for broken GVFS behaviour with encoded special + characters. +* Fixed: Issue 33: Converting ISO-8859-1 characters to UTF-8. +* Fixed: Issue 39: Basename fails on non-utf-8 locales. +* Added: More unittests. +* Added: SabreDAV version to all error responses. +* Added: URLUtil class for decoding urls. +* Updated: Now using pear.sabredav.org pear channel. + + +1.0.11 (2010-03-23) +------------------- + +* Non-public release. This release is identical to 1.0.10, but it is used to + test releasing packages to pear.sabredav.org. + + +1.0.10 (2010-03-22) +------------------- + +* Fixed: Issue 34: Invalid Lock-Token header response. +* Added: Issue 35: Adding SabreDAV version to HTTP OPTIONS responses. + + +1.0.9 (2010-03-19) +------------------ + +* Fixed: Issue 27: Entities not being encoded in PROPFIND responses. +* Fixed: Issue 29: Added missing TIMEOUT_INFINITE constant. + + +1.0.8 (2010-03-03) +------------------ + +* Fixed: Issue 21: typos causing errors +* Fixed: Issue 23: Comma's between methods in Allow header. +* Added: Sabre_DAV_ICollection interface, to aid in future compatibility. +* Added: Sabre_DAV_Exception_Forbidden exception. This will replace + Sabre_DAV_Exception_PermissionDenied in the future, and can already be used to + ensure future compatibility. + + +1.0.7 (2010-02-24) +------------------ + +* Fixed: Issue 19 regression for MS Office + + +1.0.6 (2010-02-23) +------------------ + +* Fixed: Issue 19: HEAD requests on Collections + + +1.0.5 (2010-01-22) +------------------ + +* Fixed: Fatal error when a malformed url was used for unlocking, in conjuction + with Sabre.autoload.php due to a incorrect filename. +* Fixed: Improved unittests and build system + + +1.0.4 (2010-01-11) +------------------ + +* Fixed: needed 2 different releases. One for googlecode and one for pearfarm. + This is to retain the old method to install SabreDAV until pearfarm becomes + the standard installation method. + + +1.0.3 (2010-01-11) +------------------ + +* Added: RFC4709 support (davmount) +* Added: 6 unittests +* Added: naturalselection. A tool to keep cache directories below a specified + theshold. +* Changed: Now using pearfarm.org channel server. + + +1.0.1 (2009-12-22) +------------------ + +* Fixed: Issue 15: typos in examples +* Fixed: Minor pear installation issues + + +1.0.0 (2009-11-02) +------------------ + +* Added: SimpleDirectory class. This class allows creating static directory + structures with ease. +* Changed: Custom complex properties and exceptions now get an instance of + Sabre_DAV_Server as their first argument in serialize() +* Changed: Href complex property now prepends server's baseUri +* Changed: delete before an overwriting copy/move is now handles by server class + instead of tree classes +* Changed: events must now explicitly return false to stop execution. Before, + execution would be stopped by anything loosely evaluating to false. +* Changed: the getPropertiesForPath method now takes a different set of + arguments, and returns a different response. This allows plugin developers to + return statuses for properties other than 200 and 404. The hrefs are now also + always calculated relative to the baseUri, and not the uri of the request. +* Changed: generatePropFindResponse is renamed to generateMultiStatus, and now + takes a list of properties similar to the response of getPropertiesForPath. + This was also needed to improve flexibility for plugin development. +* Changed: Auth plugins are no longer included. They were not yet stable + quality, so they will probably be reintroduced in a later version. +* Changed: PROPPATCH also used generateMultiStatus now. +* Removed: unknownProperties event. This is replaced by the afterGetProperties + event, which should provide more flexibility. +* Fixed: Only calling getSize() on IFile instances in httpHead() +* Added: beforeBind event. This is invoked upon file or directory creation +* Added: beforeWriteContent event, this is invoked by PUT and LOCK on an + existing resource. +* Added: beforeUnbind event. This is invoked right before deletion of any + resource. +* Added: afterGetProperties event. This event can be used to make modifications + to property responses. +* Added: beforeLock and beforeUnlock events. +* Added: afterBind event. +* Fixed: Copy and Move could fail in the root directory. This is now fixed. +* Added: Plugins can now be retrieved by their classname. This is useful for + inter-plugin communication. +* Added: The Auth backend can now return usernames and user-id's. +* Added: The Auth backend got a getUsers method +* Added: Sabre_DAV_FSExt_Directory now returns quota info + + +0.12.1-beta (2009-09-11) +------------------------ + +* Fixed: UNLOCK bug. Unlock didn't work at all + + +0.12-beta (2009-09-10) +---------------------- + +* Updated: Browser plugin now shows multiple {DAV:}resourcetype values if + available. +* Added: Experimental PDO backend for Locks Manager +* Fixed: Sending Content-Length: 0 for every empty response. This improves NGinx + compatibility. +* Fixed: Last modification time is reported in UTC timezone. This improves + Finder compatibility. + + +0.11-beta (2009-08-11) +---------------------- + +* Updated: Now in Beta +* Updated: Pear package no longer includes docs/ directory. These just contained + rfc's, which are publicly available. This reduces the package from ~800k to + ~60k +* Added: generatePropfindResponse now takes a baseUri argument +* Added: ResourceType property can now contain multiple resourcetypes. +* Fixed: Issue 13. + + +0.10-alpha (2009-08-03) +----------------------- + +* Added: Plugin to automatically map GET requests to non-files to PROPFIND + (Sabre_DAV_Browser_MapGetToPropFind). This should allow easier debugging of + complicated WebDAV setups. +* Added: Sabre_DAV_Property_Href class. For future use. +* Added: Ability to choose to use auth-int, auth or both for HTTP Digest + authentication. (Issue 11) +* Changed: Made more methods in Sabre_DAV_Server public. +* Fixed: TemporaryFileFilter plugin now intercepts HTTP LOCK requests to + non-existent files. (Issue 12) +* Added: Central list of defined xml namespace prefixes. This can reduce + Bandwidth and legibility for xml bodies with user-defined namespaces. +* Added: now a PEAR-compatible package again, thanks to Michael Gauthier +* Changed: moved default copy and move logic from ObjectTree to Tree class + +0.9a-alpha (2009-07-21) +---------------------- + +* Fixed: Broken release + +0.9-alpha (2009-07-21) +---------------------- + +* Changed: Major refactoring, removed most of the logic from the Tree objects. + The Server class now directly works with the INode, IFile and IDirectory + objects. If you created your own Tree objects, this will most likely break in + this release. +* Changed: Moved all the Locking logic from the Tree and Server classes into a + separate plugin. +* Changed: TemporaryFileFilter is now a plugin. +* Added: Comes with an autoloader script. This can be used instead of the + includer script, and is preferred by some people. +* Added: AWS Authentication class. +* Added: simpleserversetup.py script. This will quickly get a fileserver up and + running. +* Added: When subscribing to events, it is now possible to supply a priority. + This is for example needed to ensure that the Authentication Plugin is used + before any other Plugin. +* Added: 22 new tests. +* Added: Users-manager plugin for .htdigest files. Experimental and subject to + change. +* Added: RFC 2324 HTTP 418 status code +* Fixed: Exclusive locks could in some cases be picked up as shared locks +* Fixed: Digest auth for non-apache servers had a bug (still not actually tested + this well). + + +0.8-alpha (2009-05-30) +---------------------- + +* Changed: Renamed all exceptions! This is a compatibility break. Every + Exception now follows Sabre_DAV_Exception_FileNotFound convention instead of + Sabre_DAV_FileNotFoundException. +* Added: Browser plugin now allows uploading and creating directories straight + from the browser. +* Added: 12 more unittests +* Fixed: Locking bug, which became prevalent on Windows Vista. +* Fixed: Netdrive support +* Fixed: TemporaryFileFilter filtered out too many files. Fixed some of the + regexes. +* Fixed: Added README and ChangeLog to package + + +0.7-alpha (2009-03-29) +---------------------- + +* Added: System to return complex properties from PROPFIND. +* Added: support for {DAV:}supportedlock. +* Added: support for {DAV:}lockdiscovery. +* Added: 6 new tests. +* Added: New plugin system. +* Added: Simple HTML directory plugin, for browser access. +* Added: Server class now sends back standard pre-condition error xml bodies. + This was new since RFC4918. +* Added: Sabre_DAV_Tree_Aggregate, which can 'host' multiple Tree objects into + one. +* Added: simple basis for HTTP REPORT method. This method is not used yet, but + can be used by plugins to add reports. +* Changed: ->getSize is only called for files, no longer for collections. r303 +* Changed: Sabre_DAV_FilterTree is now Sabre_DAV_Tree_Filter +* Changed: Sabre_DAV_TemporaryFileFilter is now called + Sabre_DAV_Tree_TemporaryFileFilter. +* Changed: removed functions (get(/set)HTTPRequest(/Response)) from Server + class, and using a public property instead. +* Fixed: bug related to parsing proppatch and propfind requests. Didn't show up + in most clients, but it needed fixing regardless. (r255) +* Fixed: auth-int is now properly supported within HTTP Digest. +* Fixed: Using application/xml for a mimetype vs. text/xml as per RFC4918 sec + 8.2. +* Fixed: TemporaryFileFilter now lets through GET's if they actually exist on + the backend. (r274) +* Fixed: Some methods didn't get passed through in the FilterTree (r283). +* Fixed: LockManager is now slightly more complex, Tree classes slightly less. + (r287) + + +0.6-alpha (2009-02-16) +---------------------- + +* Added: Now uses streams for files, instead of strings. This means it won't + require to hold entire files in memory, which can be an issue if you're + dealing with big files. Note that this breaks compatibility for put() and + createFile methods. +* Added: HTTP Digest Authentication helper class. +* Added: Support for HTTP Range header +* Added: Support for ETags within If: headers +* Added: The API can now return ETags and override the default Content-Type +* Added: starting with basic framework for unittesting, using PHPUnit. +* Added: 49 unittests. +* Added: Abstraction for the HTTP request. +* Updated: Using Clark Notation for tags in properties. This means tags are + serialized as {namespace}tagName instead of namespace#tagName +* Fixed: HTTP_BasicAuth class now works as expected. +* Fixed: DAV_Server uses / for a default baseUrl. +* Fixed: Last modification date is no longer ignored in PROPFIND. +* Fixed: PROPFIND now sends back information about the requestUri even when + "Depth: 1" is specified. + + +0.5-alpha (2009-01-14) +---------------------- + +* Added: Added a very simple example for implementing a mapping to PHP file + streams. This should allow easy implementation of for example a WebDAV to FTP + proxy. +* Added: HTTP Basic Authentication helper class. +* Added: Sabre_HTTP_Response class. This centralizes HTTP operations and will be + a start towards the creating of a testing framework. +* Updated: Backwards compatibility break: all require_once() statements are + removed from all the files. It is now recommended to use autoloading of + classes, or just including lib/Sabre.includes.php. This fix was made to allow + easier integration into applications not using this standard inclusion model. +* Updated: Better in-file documentation. +* Updated: Sabre_DAV_Tree can now work with Sabre_DAV_LockManager. +* Updated: Fixes a shared-lock bug. +* Updated: Removed ?> from the bottom of each php file. +* Updated: Split up some operations from Sabre_DAV_Server to + Sabre_HTTP_Response. +* Fixed: examples are now actually included in the pear package. + + +0.4-alpha (2008-11-05) +---------------------- + +* Passes all litmus tests! +* Added: more examples +* Added: Custom property support +* Added: Shared lock support +* Added: Depth support to locks +* Added: Locking on unmapped urls (non-existent nodes) +* Fixed: Advertising as WebDAV class 3 support + + +0.3-alpha (2008-06-29) +---------------------- + +* Fully working in MS Windows clients. +* Added: temporary file filter: support for smultron files. +* Added: Phing build scripts +* Added: PEAR package +* Fixed: MOVE bug identified using finder. +* Fixed: Using gzuncompress instead of gzdecode in the temporary file filter. + This seems more common. + + +0.2-alpha (2008-05-27) +---------------------- + +* Somewhat working in Windows clients +* Added: Working PROPPATCH method (doesn't support custom properties yet) +* Added: Temporary filename handling system +* Added: Sabre_DAV_IQuota to return quota information +* Added: PROPFIND now reads the request body and only supplies the requested + properties + + +0.1-alpha (2008-04-04) +---------------------- + +* First release! +* Passes litmus: basic, http and copymove test. +* Fully working in Finder and DavFS2. + +Project started: 2007-12-13 + +[vobj]: http://sabre.io/vobject/ +[evnt]: http://sabre.io/event/ +[http]: http://sabre.io/http/ +[uri]: http://sabre.io/uri/ +[xml]: http://sabre.io/xml/ +[mi20]: http://sabre.io/dav/upgrade/1.8-to-2.0/ +[rfc6638]: http://tools.ietf.org/html/rfc6638 "CalDAV Scheduling" +[rfc7240]: http://tools.ietf.org/html/rfc7240 +[calendar-availability]: https://tools.ietf.org/html/draft-daboo-calendar-availability-05 diff --git a/plugins/panakour/backup/vendor/sabre/dav/CONTRIBUTING.md b/plugins/panakour/backup/vendor/sabre/dav/CONTRIBUTING.md new file mode 100644 index 0000000..b937db6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/CONTRIBUTING.md @@ -0,0 +1,109 @@ +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. + +Release process +--------------- + +Generally, these are the steps taken to do releases. + +1. Update the changelog. Every repo will have a `CHANGELOG.md` file. This file + should have a new version, and contain all the changes since the last + release. I generally run a `git diff` to figure out if I missed any changes. + This file should also have the current date. +2. If there were BC breaks, this usually now means a major version bump. +3. Ensure that `lib/Version.php` or `lib/DAV/Version.php` also matches this + version number. +4. Tag the release (Example `git tag 3.0.1` and push the tag (`git push --tags`). +5. (only for the sabre/dav project), create a zip distribution. Run + `php bin/build.php`. +6. For the relevant project, go to github and click the 'releases' tab. On this + tab I create the release with the relevant version. I also set the + description of the release to the same information of the changelog. In the + case of the `sabre/dav` project I also upload the zip distribution here. +7. Write a blog post on sabre.io. This also automatically updates twitter. + + +[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/plugins/panakour/backup/vendor/sabre/dav/LICENSE b/plugins/panakour/backup/vendor/sabre/dav/LICENSE new file mode 100644 index 0000000..fd3539e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2007-2016 fruux GmbH (https://fruux.com/). + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of SabreDAV nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/panakour/backup/vendor/sabre/dav/README.md b/plugins/panakour/backup/vendor/sabre/dav/README.md new file mode 100644 index 0000000..acdb1e0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/README.md @@ -0,0 +1,38 @@ +![sabre's logo](http://sabre.io/img/logo.png) sabre/dav +======================================================= + +Introduction +------------ + +sabre/dav is the most popular WebDAV framework for PHP. Use it to create WebDAV, CalDAV and CardDAV servers. + +Full documentation can be found on the website: + +http://sabre.io/ + + +Build status +------------ + +| branch | status | minimum PHP version | +| ------------ | ------ | ------------------- | +| master | [![Build Status](https://travis-ci.org/sabre-io/dav.svg?branch=master)](https://travis-ci.org/sabre-io/dav) | PHP 7.1 | +| 3.1 | [![Build Status](https://travis-ci.org/sabre-io/dav.svg?branch=3.0)](https://travis-ci.org/sabre-io/dav) | PHP 5.5 | +| 3.0 | [![Build Status](https://travis-ci.org/sabre-io/dav.svg?branch=3.0)](https://travis-ci.org/sabre-io/dav) | PHP 5.4 | +| 2.1 | [![Build Status](https://travis-ci.org/sabre-io/dav.svg?branch=2.1)](https://travis-ci.org/sabre-io/dav) | PHP 5.4 | +| 2.0 | [![Build Status](https://travis-ci.org/sabre-io/dav.svg?branch=2.0)](https://travis-ci.org/sabre-io/dav) | PHP 5.4 | +| 1.8 | [![Build Status](https://travis-ci.org/sabre-io/dav.svg?branch=1.8)](https://travis-ci.org/sabre-io/dav) | PHP 5.3 | +| 1.7 | [![Build Status](https://travis-ci.org/sabre-io/dav.svg?branch=1.7)](https://travis-ci.org/sabre-io/dav) | PHP 5.3 | +| 1.6 | [![Build Status](https://travis-ci.org/sabre-io/dav.svg?branch=1.6)](https://travis-ci.org/sabre-io/dav) | PHP 5.3 | + +Documentation +------------- + +* [Introduction](http://sabre.io/dav/). +* [Installation](http://sabre.io/dav/install/). + + +Made at fruux +------------- + +SabreDAV is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support. diff --git a/plugins/panakour/backup/vendor/sabre/dav/bin/build.php b/plugins/panakour/backup/vendor/sabre/dav/bin/build.php new file mode 100644 index 0000000..54174a7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/bin/build.php @@ -0,0 +1,169 @@ +#!/usr/bin/env php + [ + 'init', 'test', 'clean', + ], + 'markrelease' => [ + 'init', 'test', 'clean', + ], + 'clean' => [], + 'test' => [ + 'composerupdate', + ], + 'init' => [], + 'composerupdate' => [], + ]; + +$default = 'buildzip'; + +$baseDir = __DIR__.'/../'; +chdir($baseDir); + +$currentTask = $default; +if ($argc > 1) { + $currentTask = $argv[1]; +} +$version = null; +if ($argc > 2) { + $version = $argv[2]; +} + +if (!isset($tasks[$currentTask])) { + echo 'Task not found: ', $currentTask, "\n"; + 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 (0 != $code) { + 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 (0 !== $code) { + 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/plugins/panakour/backup/vendor/sabre/dav/bin/googlecode_upload.py b/plugins/panakour/backup/vendor/sabre/dav/bin/googlecode_upload.py new file mode 100644 index 0000000..caafd5d --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto20.php b/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto20.php new file mode 100644 index 0000000..c7a8d9e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto20.php @@ -0,0 +1,417 @@ +#!/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/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto21.php b/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto21.php new file mode 100644 index 0000000..9096435 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto21.php @@ -0,0 +1,166 @@ +#!/usr/bin/env php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + +$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + +switch ($driver) { + case 'mysql': + echo "Detected MySQL.\n"; + break; + case 'sqlite': + echo "Detected SQLite.\n"; + break; + default: + echo 'Error: unsupported driver: '.$driver."\n"; + 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/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto30.php b/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto30.php new file mode 100644 index 0000000..25e544c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto30.php @@ -0,0 +1,161 @@ +#!/usr/bin/env php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + +$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + +switch ($driver) { + case 'mysql': + echo "Detected MySQL.\n"; + break; + case 'sqlite': + echo "Detected SQLite.\n"; + break; + default: + echo 'Error: unsupported driver: '.$driver."\n"; + 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/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto32.php b/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto32.php new file mode 100644 index 0000000..57fd355 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/bin/migrateto32.php @@ -0,0 +1,258 @@ +#!/usr/bin/env php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + +$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + +switch ($driver) { + case 'mysql': + echo "Detected MySQL.\n"; + break; + case 'sqlite': + echo "Detected SQLite.\n"; + break; + default: + echo 'Error: unsupported driver: '.$driver."\n"; + 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 (3 === $columnCount) { + echo "The calendars table has 3 columns already. Assuming this part of the migration was already done.\n"; + $migrateCalendars = false; + } else { + echo 'The calendars table has '.$columnCount." columns.\n"; + $migrateCalendars = true; + } +} catch (Exception $e) { + echo "calendars table does not exist. This is a major problem. Exiting.\n"; + exit(-1); +} + +if ($migrateCalendars) { + $calendarBackup = 'calendars_3_1_'.$backupPostfix; + echo "Backing up 'calendars' to '", $calendarBackup, "'\n"; + + switch ($driver) { + case 'mysql': + $pdo->exec('RENAME TABLE calendars TO '.$calendarBackup); + break; + case 'sqlite': + $pdo->exec('ALTER TABLE calendars RENAME TO '.$calendarBackup); + break; + } + + echo "Creating new calendars table.\n"; + switch ($driver) { + case 'mysql': + $pdo->exec(<<exec(<<exec(<<0): + print "Bytes to go before we hit threshold:", bytes + else: + print "Threshold exceeded with:", -bytes, "bytes" + dir = os.listdir(cacheDir) + dir2 = [] + for file in dir: + path = cacheDir + '/' + file + dir2.append({ + "path" : path, + "atime": os.stat(path).st_atime, + "size" : os.stat(path).st_size + }) + + dir2.sort(lambda x,y: int(x["atime"]-y["atime"])) + + filesunlinked = 0 + gainedspace = 0 + + # Left is the amount of bytes that need to be freed up + # The default is the 'min_erase setting' + left = min_erase + + # If the min_erase setting is lower than the amount of bytes over + # the threshold, we use that number instead. + if left < -bytes : + left = -bytes + + print "Need to delete at least:", left; + + for file in dir2: + + # Only deleting files if we're not simulating + if not simulate: os.unlink(file["path"]) + left = int(left - file["size"]) + gainedspace = gainedspace + file["size"] + filesunlinked = filesunlinked + 1 + + if(left<0): + break + + print "%d files deleted (%d bytes)" % (filesunlinked, gainedspace) + + + time.sleep(sleep) + + + +def main(): + parser = OptionParser( + version="naturalselection v0.3", + description="Cache directory manager. Deletes cache entries based on accesstime and free space thresholds.\n" + + "This utility is distributed alongside SabreDAV.", + usage="usage: %prog [options] cacheDirectory", + ) + parser.add_option( + '-s', + dest="simulate", + action="store_true", + help="Don't actually make changes, but just simulate the behaviour", + ) + parser.add_option( + '-r','--runs', + help="How many times to check before exiting. -1 is infinite, which is the default", + type="int", + dest="runs", + default=-1 + ) + parser.add_option( + '-n','--interval', + help="Sleep time in seconds (default = 5)", + type="int", + dest="sleep", + default=5 + ) + parser.add_option( + '-l','--threshold', + help="Threshold in bytes (default = 10737418240, which is 10GB)", + type="int", + dest="threshold", + default=10737418240 + ) + parser.add_option( + '-m', '--min-erase', + help="Minimum number of bytes to erase when the threshold is reached. " + + "Setting this option higher will reduce the number of times the cache directory will need to be scanned. " + + "(the default is 1073741824, which is 1GB.)", + type="int", + dest="min_erase", + default=1073741824 + ) + + options,args = parser.parse_args() + if len(args)<1: + parser.error("This utility requires at least 1 argument") + cacheDir = args[0] + + print "Natural Selection" + print "Cache directory:", cacheDir + free = getfreespace(cacheDir); + print "Current free disk space:", free + + runs = options.runs; + while runs!=0 : + run( + cacheDir, + sleep=options.sleep, + simulate=options.simulate, + threshold=options.threshold, + min_erase=options.min_erase + ) + if runs>0: + runs = runs - 1 + +if __name__ == '__main__' : + main() diff --git a/plugins/panakour/backup/vendor/sabre/dav/bin/sabredav b/plugins/panakour/backup/vendor/sabre/dav/bin/sabredav new file mode 100644 index 0000000..032371b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/bin/sabredav @@ -0,0 +1,2 @@ +#!/bin/sh +php -S 0.0.0.0:8080 `dirname $0`/sabredav.php diff --git a/plugins/panakour/backup/vendor/sabre/dav/bin/sabredav.php b/plugins/panakour/backup/vendor/sabre/dav/bin/sabredav.php new file mode 100644 index 0000000..28341b5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/bin/sabredav.php @@ -0,0 +1,51 @@ +stream = fopen('php://stdout', 'w'); + } + + public function log($msg) + { + fwrite($this->stream, $msg."\n"); + } +} + +$log = new CliLog(); + +if ('cli-server' !== php_sapi_name()) { + 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/plugins/panakour/backup/vendor/sabre/dav/composer.json b/plugins/panakour/backup/vendor/sabre/dav/composer.json new file mode 100644 index 0000000..f0fbf7a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/composer.json @@ -0,0 +1,90 @@ +{ + "name": "sabre/dav", + "type": "library", + "description": "WebDAV Framework for PHP", + "keywords": ["Framework", "WebDAV", "CalDAV", "CardDAV", "iCalendar"], + "homepage": "http://sabre.io/", + "license" : "BSD-3-Clause", + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage" : "http://evertpot.com/", + "role" : "Developer" + } + ], + "require": { + "php": "^7.1.0", + "sabre/vobject": "^4.2.1", + "sabre/event" : "^5.0", + "sabre/xml" : "^2.0.1", + "sabre/http" : "^5.0.5", + "sabre/uri" : "^2.0", + "ext-dom": "*", + "ext-pcre": "*", + "ext-spl": "*", + "ext-simplexml": "*", + "ext-mbstring" : "*", + "ext-ctype" : "*", + "ext-date" : "*", + "ext-iconv" : "*", + "lib-libxml" : ">=2.7.0", + "psr/log": "^1.0", + "ext-json": "*" + }, + "require-dev" : { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit" : "^7.5 || ^8.5 || ^9.0", + "evert/phpdoc-md" : "~0.1.0", + "monolog/monolog": "^1.18" + }, + "suggest" : { + "ext-curl" : "*", + "ext-pdo" : "*", + "ext-imap": "*" + }, + "autoload": { + "psr-4" : { + "Sabre\\DAV\\" : "lib/DAV/", + "Sabre\\DAVACL\\" : "lib/DAVACL/", + "Sabre\\CalDAV\\" : "lib/CalDAV/", + "Sabre\\CardDAV\\" : "lib/CardDAV/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\" : "tests/Sabre/", + "Sabre\\CalDAV\\" : "tests/Sabre/CalDAV", + "Sabre\\CardDAV\\" : "tests/Sabre/CardDAV", + "Sabre\\DAV\\" : "tests/Sabre/DAV", + "Sabre\\DAV\\Property\\" : "tests/Sabre/DAV/Xml/Property", + "Sabre\\DAVACL\\" : "tests/Sabre/DAVACL", + "Sabre\\HTTP\\" : "tests/Sabre/HTTP" + } + }, + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-dav" + }, + "bin" : [ + "bin/sabredav", + "bin/naturalselection" + ], + "scripts": { + "phpstan": [ + "phpstan analyse lib tests" + ], + "cs-fixer": [ + "php-cs-fixer fix" + ], + "phpunit": [ + "phpunit --configuration tests/phpunit.xml" + ], + "test": [ + "composer phpstan", + "composer cs-fixer", + "composer phpunit" + ] + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/addressbookserver.php b/plugins/panakour/backup/vendor/sabre/dav/examples/addressbookserver.php new file mode 100644 index 0000000..48f1e91 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/addressbookserver.php @@ -0,0 +1,51 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +// Autoloader +require_once 'vendor/autoload.php'; + +// Backends +$authBackend = new Sabre\DAV\Auth\Backend\PDO($pdo); +$principalBackend = new Sabre\DAVACL\PrincipalBackend\PDO($pdo); +$carddavBackend = new Sabre\CardDAV\Backend\PDO($pdo); +//$caldavBackend = new Sabre\CalDAV\Backend\PDO($pdo); + +// Setting up the directory tree // +$nodes = [ + new Sabre\DAVACL\PrincipalCollection($principalBackend), +// new Sabre\CalDAV\CalendarRoot($authBackend, $caldavBackend), + new Sabre\CardDAV\AddressBookRoot($principalBackend, $carddavBackend), +]; + +// The object tree needs in turn to be passed to the server class +$server = new Sabre\DAV\Server($nodes); +$server->setBaseUri($baseUri); + +// Plugins +$server->addPlugin(new Sabre\DAV\Auth\Plugin($authBackend)); +$server->addPlugin(new Sabre\DAV\Browser\Plugin()); +//$server->addPlugin(new Sabre\CalDAV\Plugin()); +$server->addPlugin(new Sabre\CardDAV\Plugin()); +$server->addPlugin(new Sabre\DAVACL\Plugin()); +$server->addPlugin(new Sabre\DAV\Sync\Plugin()); + +// And off we go! +$server->start(); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/calendarserver.php b/plugins/panakour/backup/vendor/sabre/dav/examples/calendarserver.php new file mode 100644 index 0000000..88ea961 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/calendarserver.php @@ -0,0 +1,75 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +// Files we need +require_once 'vendor/autoload.php'; + +// Backends +$authBackend = new Sabre\DAV\Auth\Backend\PDO($pdo); +$calendarBackend = new Sabre\CalDAV\Backend\PDO($pdo); +$principalBackend = new Sabre\DAVACL\PrincipalBackend\PDO($pdo); + +// Directory structure +$tree = [ + new Sabre\CalDAV\Principal\Collection($principalBackend), + new Sabre\CalDAV\CalendarRoot($principalBackend, $calendarBackend), +]; + +$server = new Sabre\DAV\Server($tree); + +if (isset($baseUri)) { + $server->setBaseUri($baseUri); +} + +/* Server Plugins */ +$authPlugin = new Sabre\DAV\Auth\Plugin($authBackend); +$server->addPlugin($authPlugin); + +$aclPlugin = new Sabre\DAVACL\Plugin(); +$server->addPlugin($aclPlugin); + +/* CalDAV support */ +$caldavPlugin = new Sabre\CalDAV\Plugin(); +$server->addPlugin($caldavPlugin); + +/* Calendar subscription support */ +$server->addPlugin( + new Sabre\CalDAV\Subscriptions\Plugin() +); + +/* Calendar scheduling support */ +$server->addPlugin( + new Sabre\CalDAV\Schedule\Plugin() +); + +/* WebDAV-Sync plugin */ +$server->addPlugin(new Sabre\DAV\Sync\Plugin()); + +/* CalDAV Sharing support */ +$server->addPlugin(new Sabre\DAV\Sharing\Plugin()); +$server->addPlugin(new Sabre\CalDAV\SharingPlugin()); + +// Support for html frontend +$browser = new Sabre\DAV\Browser\Plugin(); +$server->addPlugin($browser); + +// And off we go! +$server->start(); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/fileserver.php b/plugins/panakour/backup/vendor/sabre/dav/examples/fileserver.php new file mode 100644 index 0000000..667f55d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/fileserver.php @@ -0,0 +1,56 @@ +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); + +// 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->start(); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/groupwareserver.php b/plugins/panakour/backup/vendor/sabre/dav/examples/groupwareserver.php new file mode 100644 index 0000000..675c0a1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/groupwareserver.php @@ -0,0 +1,91 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +// Autoloader +require_once 'vendor/autoload.php'; + +/** + * The backends. Yes we do really need all of them. + * + * This allows any developer to subclass just any of them and hook into their + * own backend systems. + */ +$authBackend = new \Sabre\DAV\Auth\Backend\PDO($pdo); +$principalBackend = new \Sabre\DAVACL\PrincipalBackend\PDO($pdo); +$carddavBackend = new \Sabre\CardDAV\Backend\PDO($pdo); +$caldavBackend = new \Sabre\CalDAV\Backend\PDO($pdo); + +/** + * The directory tree. + * + * Basically this is an array which contains the 'top-level' directories in the + * WebDAV server. + */ +$nodes = [ + // /principals + new \Sabre\CalDAV\Principal\Collection($principalBackend), + // /calendars + new \Sabre\CalDAV\CalendarRoot($principalBackend, $caldavBackend), + // /addressbook + new \Sabre\CardDAV\AddressBookRoot($principalBackend, $carddavBackend), +]; + +// The object tree needs in turn to be passed to the server class +$server = new \Sabre\DAV\Server($nodes); +if (isset($baseUri)) { + $server->setBaseUri($baseUri); +} + +// Plugins +$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend)); +$server->addPlugin(new \Sabre\DAV\Browser\Plugin()); +$server->addPlugin(new \Sabre\DAV\Sync\Plugin()); +$server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); +$server->addPlugin(new \Sabre\DAVACL\Plugin()); + +// CalDAV plugins +$server->addPlugin(new \Sabre\CalDAV\Plugin()); +$server->addPlugin(new \Sabre\CalDAV\Schedule\Plugin()); +$server->addPlugin(new \Sabre\CalDAV\SharingPlugin()); +$server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); + +// CardDAV plugins +$server->addPlugin(new \Sabre\CardDAV\Plugin()); +$server->addPlugin(new \Sabre\CardDAV\VCFExportPlugin()); + +// And off we go! +$server->start(); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/minimal.php b/plugins/panakour/backup/vendor/sabre/dav/examples/minimal.php new file mode 100644 index 0000000..fb7a1fe --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/minimal.php @@ -0,0 +1,20 @@ +addPlugin( + new Sabre\DAV\Browser\Plugin() +); + +$server->start(); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.addressbooks.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.addressbooks.sql new file mode 100644 index 0000000..9ec88ba --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.addressbooks.sql @@ -0,0 +1,28 @@ +CREATE TABLE addressbooks ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + principaluri VARBINARY(255), + displayname VARCHAR(255), + uri VARBINARY(200), + description TEXT, + synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1', + UNIQUE(principaluri(100), uri(100)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE cards ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + addressbookid INT(11) UNSIGNED NOT NULL, + carddata MEDIUMBLOB, + uri VARBINARY(200), + lastmodified INT(11) UNSIGNED, + etag VARBINARY(32), + size INT(11) UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE addressbookchanges ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + uri VARBINARY(200) NOT NULL, + synctoken INT(11) UNSIGNED NOT NULL, + addressbookid INT(11) UNSIGNED NOT NULL, + operation TINYINT(1) NOT NULL, + INDEX addressbookid_synctoken (addressbookid, synctoken) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.calendars.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.calendars.sql new file mode 100644 index 0000000..21c5bcb --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.calendars.sql @@ -0,0 +1,76 @@ +CREATE TABLE calendarobjects ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + calendardata MEDIUMBLOB, + uri VARBINARY(200), + calendarid INTEGER UNSIGNED NOT NULL, + lastmodified INT(11) UNSIGNED, + etag VARBINARY(32), + size INT(11) UNSIGNED NOT NULL, + componenttype VARBINARY(8), + firstoccurence INT(11) UNSIGNED, + lastoccurence INT(11) UNSIGNED, + uid VARBINARY(200), + UNIQUE(calendarid, uri), + INDEX calendarid_time (calendarid, firstoccurence) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE calendars ( + id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + synctoken INTEGER UNSIGNED NOT NULL DEFAULT '1', + components VARBINARY(21) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE calendarinstances ( + id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + calendarid INTEGER UNSIGNED NOT NULL, + principaluri VARBINARY(100), + access TINYINT(1) NOT NULL DEFAULT '1' COMMENT '1 = owner, 2 = read, 3 = readwrite', + displayname VARCHAR(100), + uri VARBINARY(200), + description TEXT, + calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0', + calendarcolor VARBINARY(10), + timezone TEXT, + transparent TINYINT(1) NOT NULL DEFAULT '0', + share_href VARBINARY(100), + share_displayname VARCHAR(100), + share_invitestatus TINYINT(1) NOT NULL DEFAULT '2' COMMENT '1 = noresponse, 2 = accepted, 3 = declined, 4 = invalid', + UNIQUE(principaluri, uri), + UNIQUE(calendarid, principaluri), + UNIQUE(calendarid, share_href) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE calendarchanges ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + uri VARBINARY(200) NOT NULL, + synctoken INT(11) UNSIGNED NOT NULL, + calendarid INT(11) UNSIGNED NOT NULL, + operation TINYINT(1) NOT NULL, + INDEX calendarid_synctoken (calendarid, synctoken) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE calendarsubscriptions ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + uri VARBINARY(200) NOT NULL, + principaluri VARBINARY(100) NOT NULL, + source TEXT, + displayname VARCHAR(100), + refreshrate VARCHAR(10), + calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0', + calendarcolor VARBINARY(10), + striptodos TINYINT(1) NULL, + stripalarms TINYINT(1) NULL, + stripattachments TINYINT(1) NULL, + lastmodified INT(11) UNSIGNED, + UNIQUE(principaluri, uri) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE schedulingobjects ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + principaluri VARBINARY(255), + calendardata MEDIUMBLOB, + uri VARBINARY(200), + lastmodified INT(11) UNSIGNED, + etag VARBINARY(32), + size INT(11) UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.locks.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.locks.sql new file mode 100644 index 0000000..96a3a88 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.locks.sql @@ -0,0 +1,12 @@ +CREATE TABLE locks ( + id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + owner VARCHAR(100), + timeout INTEGER UNSIGNED, + created INTEGER, + token VARBINARY(100), + scope TINYINT, + depth TINYINT, + uri VARBINARY(1000), + INDEX(token), + INDEX(uri(100)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.principals.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.principals.sql new file mode 100644 index 0000000..ea0d16a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.principals.sql @@ -0,0 +1,20 @@ +CREATE TABLE principals ( + id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + uri VARBINARY(200) NOT NULL, + email VARBINARY(80), + displayname VARCHAR(80), + UNIQUE(uri) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE groupmembers ( + id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + principal_id INTEGER UNSIGNED NOT NULL, + member_id INTEGER UNSIGNED NOT NULL, + UNIQUE(principal_id, member_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO principals (uri,email,displayname) VALUES +('principals/admin', 'admin@example.org','Administrator'), +('principals/admin/calendar-proxy-read', null, null), +('principals/admin/calendar-proxy-write', null, null); + diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.propertystorage.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.propertystorage.sql new file mode 100644 index 0000000..1b5ca5a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.propertystorage.sql @@ -0,0 +1,9 @@ +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 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE UNIQUE INDEX path_property ON propertystorage (path(600), name(100)); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.users.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.users.sql new file mode 100644 index 0000000..22ac312 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/mysql.users.sql @@ -0,0 +1,9 @@ +CREATE TABLE users ( + id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + username VARBINARY(50), + digesta1 VARBINARY(32), + UNIQUE(username) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO users (username,digesta1) VALUES +('admin', '87fd274b7b6c01e48d7c2f965da8ddf7'); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.addressbooks.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.addressbooks.sql new file mode 100644 index 0000000..98f414f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.addressbooks.sql @@ -0,0 +1,44 @@ +CREATE TABLE addressbooks ( + id SERIAL NOT NULL, + principaluri VARCHAR(255), + displayname VARCHAR(255), + uri VARCHAR(200), + description TEXT, + synctoken INTEGER NOT NULL DEFAULT 1 +); + +ALTER TABLE ONLY addressbooks + ADD CONSTRAINT addressbooks_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX addressbooks_ukey + ON addressbooks USING btree (principaluri, uri); + +CREATE TABLE cards ( + id SERIAL NOT NULL, + addressbookid INTEGER NOT NULL, + carddata BYTEA, + uri VARCHAR(200), + lastmodified INTEGER, + etag VARCHAR(32), + size INTEGER NOT NULL +); + +ALTER TABLE ONLY cards + ADD CONSTRAINT cards_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX cards_ukey + ON cards USING btree (addressbookid, uri); + +CREATE TABLE addressbookchanges ( + id SERIAL NOT NULL, + uri VARCHAR(200) NOT NULL, + synctoken INTEGER NOT NULL, + addressbookid INTEGER NOT NULL, + operation SMALLINT NOT NULL +); + +ALTER TABLE ONLY addressbookchanges + ADD CONSTRAINT addressbookchanges_pkey PRIMARY KEY (id); + +CREATE INDEX addressbookchanges_addressbookid_synctoken_ix + ON addressbookchanges USING btree (addressbookid, synctoken); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.calendars.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.calendars.sql new file mode 100644 index 0000000..67dc41a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.calendars.sql @@ -0,0 +1,105 @@ +CREATE TABLE calendarobjects ( + id SERIAL NOT NULL, + calendardata BYTEA, + uri VARCHAR(200), + calendarid INTEGER NOT NULL, + lastmodified INTEGER, + etag VARCHAR(32), + size INTEGER NOT NULL, + componenttype VARCHAR(8), + firstoccurence INTEGER, + lastoccurence INTEGER, + uid VARCHAR(200) +); + +ALTER TABLE ONLY calendarobjects + ADD CONSTRAINT calendarobjects_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX calendarobjects_ukey + ON calendarobjects USING btree (calendarid, uri); + + +CREATE TABLE calendars ( + id SERIAL NOT NULL, + synctoken INTEGER NOT NULL DEFAULT 1, + components VARCHAR(21) +); + +ALTER TABLE ONLY calendars + ADD CONSTRAINT calendars_pkey PRIMARY KEY (id); + + +CREATE TABLE calendarinstances ( + id SERIAL NOT NULL, + calendarid INTEGER NOT NULL, + principaluri VARCHAR(100), + access SMALLINT NOT NULL DEFAULT '1', -- '1 = owner, 2 = read, 3 = readwrite' + displayname VARCHAR(100), + uri VARCHAR(200), + description TEXT, + calendarorder INTEGER NOT NULL DEFAULT 0, + calendarcolor VARCHAR(10), + timezone TEXT, + transparent SMALLINT NOT NULL DEFAULT '0', + share_href VARCHAR(100), + share_displayname VARCHAR(100), + share_invitestatus SMALLINT NOT NULL DEFAULT '2' -- '1 = noresponse, 2 = accepted, 3 = declined, 4 = invalid' +); + +ALTER TABLE ONLY calendarinstances + ADD CONSTRAINT calendarinstances_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX calendarinstances_principaluri_uri + ON calendarinstances USING btree (principaluri, uri); + + +CREATE UNIQUE INDEX calendarinstances_principaluri_calendarid + ON calendarinstances USING btree (principaluri, calendarid); + +CREATE UNIQUE INDEX calendarinstances_principaluri_share_href + ON calendarinstances USING btree (principaluri, share_href); + +CREATE TABLE calendarsubscriptions ( + id SERIAL NOT NULL, + uri VARCHAR(200) NOT NULL, + principaluri VARCHAR(100) NOT NULL, + source TEXT, + displayname VARCHAR(100), + refreshrate VARCHAR(10), + calendarorder INTEGER NOT NULL DEFAULT 0, + calendarcolor VARCHAR(10), + striptodos SMALLINT NULL, + stripalarms SMALLINT NULL, + stripattachments SMALLINT NULL, + lastmodified INTEGER +); + +ALTER TABLE ONLY calendarsubscriptions + ADD CONSTRAINT calendarsubscriptions_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX calendarsubscriptions_ukey + ON calendarsubscriptions USING btree (principaluri, uri); + +CREATE TABLE calendarchanges ( + id SERIAL NOT NULL, + uri VARCHAR(200) NOT NULL, + synctoken INTEGER NOT NULL, + calendarid INTEGER NOT NULL, + operation SMALLINT NOT NULL DEFAULT 0 +); + +ALTER TABLE ONLY calendarchanges + ADD CONSTRAINT calendarchanges_pkey PRIMARY KEY (id); + +CREATE INDEX calendarchanges_calendarid_synctoken_ix + ON calendarchanges USING btree (calendarid, synctoken); + +CREATE TABLE schedulingobjects ( + id SERIAL NOT NULL, + principaluri VARCHAR(255), + calendardata BYTEA, + uri VARCHAR(200), + lastmodified INTEGER, + etag VARCHAR(32), + size INTEGER NOT NULL +); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.locks.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.locks.sql new file mode 100644 index 0000000..0290528 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.locks.sql @@ -0,0 +1,19 @@ +CREATE TABLE locks ( + id SERIAL NOT NULL, + owner VARCHAR(100), + timeout INTEGER, + created INTEGER, + token VARCHAR(100), + scope SMALLINT, + depth SMALLINT, + uri TEXT +); + +ALTER TABLE ONLY locks + ADD CONSTRAINT locks_pkey PRIMARY KEY (id); + +CREATE INDEX locks_token_ix + ON locks USING btree (token); + +CREATE INDEX locks_uri_ix + ON locks USING btree (uri); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.principals.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.principals.sql new file mode 100644 index 0000000..5a65260 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.principals.sql @@ -0,0 +1,30 @@ +CREATE TABLE principals ( + id SERIAL NOT NULL, + uri VARCHAR(200) NOT NULL, + email VARCHAR(80), + displayname VARCHAR(80) +); + +ALTER TABLE ONLY principals + ADD CONSTRAINT principals_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX principals_ukey + ON principals USING btree (uri); + +CREATE TABLE groupmembers ( + id SERIAL NOT NULL, + principal_id INTEGER NOT NULL, + member_id INTEGER NOT NULL +); + +ALTER TABLE ONLY groupmembers + ADD CONSTRAINT groupmembers_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX groupmembers_ukey + ON groupmembers USING btree (principal_id, member_id); + +INSERT INTO principals (uri,email,displayname) VALUES +('principals/admin', 'admin@example.org','Administrator'), +('principals/admin/calendar-proxy-read', null, null), +('principals/admin/calendar-proxy-write', null, null); + diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.propertystorage.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.propertystorage.sql new file mode 100644 index 0000000..d1463fa --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.propertystorage.sql @@ -0,0 +1,13 @@ +CREATE TABLE propertystorage ( + id SERIAL NOT NULL, + path VARCHAR(1024) NOT NULL, + name VARCHAR(100) NOT NULL, + valuetype INT, + value BYTEA +); + +ALTER TABLE ONLY propertystorage + ADD CONSTRAINT propertystorage_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX propertystorage_ukey + ON propertystorage (path, name); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.users.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.users.sql new file mode 100644 index 0000000..9d6047b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/pgsql.users.sql @@ -0,0 +1,14 @@ +CREATE TABLE users ( + id SERIAL NOT NULL, + username VARCHAR(50), + digesta1 VARCHAR(32) +); + +ALTER TABLE ONLY users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX users_ukey + ON users USING btree (username); + +INSERT INTO users (username,digesta1) VALUES +('admin', '87fd274b7b6c01e48d7c2f965da8ddf7'); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.addressbooks.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.addressbooks.sql new file mode 100644 index 0000000..60bd708 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.addressbooks.sql @@ -0,0 +1,31 @@ +CREATE TABLE addressbooks ( + id integer primary key asc NOT NULL, + principaluri text NOT NULL, + displayname text, + uri text NOT NULL, + description text, + synctoken integer DEFAULT 1 NOT NULL, + UNIQUE(principaluri, uri) +); + + +CREATE TABLE cards ( + id integer primary key asc NOT NULL, + addressbookid integer NOT NULL, + carddata blob, + uri text NOT NULL, + lastmodified integer, + etag text, + size integer, + UNIQUE(addressbookid, uri) +); + +CREATE TABLE addressbookchanges ( + id integer primary key asc NOT NULL, + uri text, + synctoken integer NOT NULL, + addressbookid integer NOT NULL, + operation integer NOT NULL +); + +CREATE INDEX addressbookid_synctoken ON addressbookchanges (addressbookid, synctoken); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.calendars.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.calendars.sql new file mode 100644 index 0000000..b3dc10a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.calendars.sql @@ -0,0 +1,78 @@ +CREATE TABLE calendarobjects ( + id integer primary key asc NOT NULL, + calendardata blob NOT NULL, + uri text NOT NULL, + calendarid integer NOT NULL, + lastmodified integer NOT NULL, + etag text NOT NULL, + size integer NOT NULL, + componenttype text, + firstoccurence integer, + lastoccurence integer, + uid text, + UNIQUE(calendarid,uri) +); +CREATE INDEX calendarid_time ON calendarobjects (calendarid, firstoccurence); + +CREATE TABLE calendars ( + id integer primary key asc NOT NULL, + synctoken integer DEFAULT 1 NOT NULL, + components text NOT NULL +); + +CREATE TABLE calendarinstances ( + id integer primary key asc NOT NULL, + calendarid integer NOT NULL, + principaluri text NULL, + access integer COMMENT '1 = owner, 2 = read, 3 = readwrite' NOT NULL DEFAULT '1', + displayname text, + uri text NOT NULL, + description text, + calendarorder integer, + calendarcolor text, + timezone text, + transparent bool, + share_href text, + share_displayname text, + share_invitestatus integer DEFAULT '2', + UNIQUE (principaluri, uri), + UNIQUE (calendarid, principaluri), + UNIQUE (calendarid, share_href) +); + +CREATE TABLE calendarchanges ( + id integer primary key asc NOT NULL, + uri text, + synctoken integer NOT NULL, + calendarid integer NOT NULL, + operation integer NOT NULL +); + +CREATE INDEX calendarid_synctoken ON calendarchanges (calendarid, synctoken); + +CREATE TABLE calendarsubscriptions ( + id integer primary key asc NOT NULL, + uri text NOT NULL, + principaluri text NOT NULL, + source text NOT NULL, + displayname text, + refreshrate text, + calendarorder integer, + calendarcolor text, + striptodos bool, + stripalarms bool, + stripattachments bool, + lastmodified int, + UNIQUE(principaluri, uri) +); + +CREATE TABLE schedulingobjects ( + id integer primary key asc NOT NULL, + principaluri text NOT NULL, + calendardata blob, + uri text NOT NULL, + lastmodified integer, + etag text NOT NULL, + size integer NOT NULL, + UNIQUE(principaluri, uri) +); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.locks.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.locks.sql new file mode 100644 index 0000000..dd03466 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.locks.sql @@ -0,0 +1,11 @@ +CREATE TABLE 'locks' ( + id integer primary key asc NOT NULL, + owner text, + timeout integer, + created integer, + token text, + scope integer, + depth integer, + uri text +); +CREATE INDEX idx_uri ON 'locks' (uri); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.principals.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.principals.sql new file mode 100644 index 0000000..4105156 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.principals.sql @@ -0,0 +1,20 @@ +CREATE TABLE principals ( + id INTEGER PRIMARY KEY ASC NOT NULL, + uri TEXT NOT NULL, + email TEXT, + displayname TEXT, + UNIQUE(uri) +); + +CREATE TABLE groupmembers ( + id INTEGER PRIMARY KEY ASC NOT NULL, + principal_id INTEGER NOT NULL, + member_id INTEGER NOT NULL, + UNIQUE(principal_id, member_id) +); + + +INSERT INTO principals (uri,email,displayname) VALUES ('principals/admin', 'admin@example.org','Administrator'); +INSERT INTO principals (uri,email,displayname) VALUES ('principals/admin/calendar-proxy-read', null, null); +INSERT INTO principals (uri,email,displayname) VALUES ('principals/admin/calendar-proxy-write', null, null); + diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.propertystorage.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.propertystorage.sql new file mode 100644 index 0000000..4ccf484 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.propertystorage.sql @@ -0,0 +1,9 @@ +CREATE TABLE propertystorage ( + id integer primary key asc NOT NULL, + path text NOT NULL, + name text NOT NULL, + valuetype integer NOT NULL, + value string +); + +CREATE UNIQUE INDEX path_property ON propertystorage (path, name); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.users.sql b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.users.sql new file mode 100644 index 0000000..5597b05 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/sql/sqlite.users.sql @@ -0,0 +1,9 @@ +CREATE TABLE users ( + id integer primary key asc NOT NULL, + username TEXT NOT NULL, + digesta1 TEXT NOT NULL, + UNIQUE(username) +); + +INSERT INTO users (username,digesta1) VALUES +('admin', '87fd274b7b6c01e48d7c2f965da8ddf7'); diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_htaccess.conf b/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_htaccess.conf new file mode 100644 index 0000000..c5f29ba --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_htaccess.conf @@ -0,0 +1,16 @@ +RewriteEngine On +# This makes every request go to server.php +RewriteRule (.*) server.php [L] + +# Output buffering needs to be off, to prevent high memory usage +php_flag output_buffering off + +# This is also to prevent high memory usage +php_flag always_populate_raw_post_data off + +# This is almost a given, but magic quotes is *still* on on some +# linux distributions +php_flag magic_quotes_gpc off + +# SabreDAV is not compatible with mbstring function overloading +php_flag mbstring.func_overload off diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_vhost.conf b/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_vhost.conf new file mode 100644 index 0000000..27ff412 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_vhost.conf @@ -0,0 +1,29 @@ +# This is a sample configuration to setup a dedicated Apache vhost for WebDAV. +# +# The main thing that should be configured is the servername, and the path to +# your SabreDAV installation (DocumentRoot). +# +# This configuration assumed mod_php5 is used, as it sets a few default php +# settings as well. + + + # Don't forget to change the server name + # ServerName dav.example.org + + # The DocumentRoot is also required + # DocumentRoot /home/sabredav/ + + RewriteEngine On + # This makes every request go to server.php + RewriteRule ^/(.*)$ /server.php [L] + + # Output buffering needs to be off, to prevent high memory usage + php_flag output_buffering off + + # This is also to prevent high memory usage + php_flag always_populate_raw_post_data off + + # SabreDAV is not compatible with mbstring function overloading + php_flag mbstring.func_overload off + + diff --git a/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_vhost_cgi.conf b/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_vhost_cgi.conf new file mode 100644 index 0000000..9784c7b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/examples/webserver/apache2_vhost_cgi.conf @@ -0,0 +1,21 @@ +# This is a sample configuration to setup a dedicated Apache vhost for WebDAV. +# +# The main thing that should be configured is the servername, and the path to +# your SabreDAV installation (DocumentRoot). +# +# This configuration assumes CGI or FastCGI is used. + + + # Don't forget to change the server name + # ServerName dav.example.org + + # The DocumentRoot is also required + # DocumentRoot /home/sabredav/ + + # This makes every request go to server.php. This also makes sure + # the Authentication information is available. If your server script is + # not called server.php, be sure to change it. + RewriteEngine On + RewriteRule ^/(.*)$ /server.php [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php new file mode 100644 index 0000000..c32c864 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php @@ -0,0 +1,216 @@ +getCalendarObject($calendarId, $uri); + }, $uris); + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly 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 + * + * @return array + */ + public function calendarQuery($calendarId, array $filters) + { + $result = []; + $objects = $this->getCalendarObjects($calendarId); + + foreach ($objects as $object) { + if ($this->validateFilterForObject($object, $filters)) { + $result[] = $object['uri']; + } + } + + return $result; + } + + /** + * This method validates if a filter (as passed to calendarQuery) matches + * the given object. + * + * @return bool + */ + protected function validateFilterForObject(array $object, array $filters) + { + // Unfortunately, setting the 'calendardata' here is optional. If + // it was excluded, we actually need another call to get this as + // well. + if (!isset($object['calendardata'])) { + $object = $this->getCalendarObject($object['calendarid'], $object['uri']); + } + + $vObject = VObject\Reader::read($object['calendardata']); + + $validator = new CalDAV\CalendarQueryValidator(); + $result = $validator->validate($vObject, $filters); + + // Destroy circular references so PHP will GC the object. + $vObject->destroy(); + + return $result; + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * + * @return string|null + */ + public function getCalendarObjectByUID($principalUri, $uid) + { + // Note: this is a super slow naive implementation of this method. You + // are highly recommended to optimize it, if your backend allows it. + foreach ($this->getCalendarsForUser($principalUri) as $calendar) { + // We must ignore calendars owned by other principals. + if ($calendar['principaluri'] !== $principalUri) { + continue; + } + + // Ignore calendars that are shared. + if (isset($calendar['{http://sabredav.org/ns}owner-principal']) && $calendar['{http://sabredav.org/ns}owner-principal'] !== $principalUri) { + continue; + } + + $results = $this->calendarQuery( + $calendar['id'], + [ + 'name' => 'VCALENDAR', + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'is-not-defined' => false, + 'time-range' => null, + 'comp-filters' => [], + 'prop-filters' => [ + [ + 'name' => 'UID', + 'is-not-defined' => false, + 'time-range' => null, + 'text-match' => [ + 'value' => $uid, + 'negate-condition' => false, + 'collation' => 'i;octet', + ], + 'param-filters' => [], + ], + ], + ], + ], + ] + ); + if ($results) { + // We have a match + return $calendar['uri'].'/'.$results[0]; + } + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php new file mode 100644 index 0000000..8bfa744 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php @@ -0,0 +1,273 @@ + 'displayname', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + ]; + + /** + * List of subscription properties, and how they map to database fieldnames. + * + * @var array + */ + public $subscriptionPropertyMap = [ + '{DAV:}displayname' => 'displayname', + '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', + ]; + + /** + * Creates the backend. + */ + public function __construct(\PDO $pdo) + { + $this->pdo = $pdo; + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the calendar. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * + * @return array + */ + public function getCalendarsForUser($principalUri) + { + $fields = array_values($this->propertyMap); + $fields[] = 'calendarid'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + $fields[] = 'access'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare(<<calendarInstancesTableName}.id as id, $fields FROM {$this->calendarInstancesTableName} + LEFT JOIN {$this->calendarTableName} ON + {$this->calendarInstancesTableName}.calendarid = {$this->calendarTableName}.id +WHERE principaluri = ? ORDER BY calendarorder ASC +SQL + ); + $stmt->execute([$principalUri]); + + $calendars = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $components = []; + if ($row['components']) { + $components = explode(',', $row['components']); + } + + $calendar = [ + 'id' => [(int) $row['calendarid'], (int) $row['id']], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', + '{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components), + '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'), + 'share-resource-uri' => '/ns/share/'.$row['calendarid'], + ]; + + $calendar['share-access'] = (int) $row['access']; + // 1 = owner, 2 = readonly, 3 = readwrite + if ($row['access'] > 1) { + // We need to find more information about the original owner. + //$stmt2 = $this->pdo->prepare('SELECT principaluri FROM ' . $this->calendarInstancesTableName . ' WHERE access = 1 AND id = ?'); + //$stmt2->execute([$row['id']]); + + // read-only is for backwards compatbility. Might go away in + // the future. + $calendar['read-only'] = \Sabre\DAV\Sharing\Plugin::ACCESS_READ === (int) $row['access']; + } + + foreach ($this->propertyMap as $xmlName => $dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + $calendars[] = $calendar; + } + + return $calendars; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used + * to reference this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * + * @return string + */ + public function createCalendar($principalUri, $calendarUri, array $properties) + { + $fieldNames = [ + 'principaluri', + 'uri', + 'transparent', + 'calendarid', + ]; + $values = [ + ':principaluri' => $principalUri, + ':uri' => $calendarUri, + ':transparent' => 0, + ]; + + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + if (!isset($properties[$sccs])) { + // Default value + $components = 'VEVENT,VTODO'; + } else { + if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) { + throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet'); + } + $components = implode(',', $properties[$sccs]->getValue()); + } + $transp = '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp'; + if (isset($properties[$transp])) { + $values[':transparent'] = 'transparent' === $properties[$transp]->getValue() ? 1 : 0; + } + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarTableName.' (synctoken, components) VALUES (1, ?)'); + $stmt->execute([$components]); + + $calendarId = $this->pdo->lastInsertId( + $this->calendarTableName.'_id_seq' + ); + + $values[':calendarid'] = $calendarId; + + foreach ($this->propertyMap as $xmlName => $dbName) { + if (isset($properties[$xmlName])) { + $values[':'.$dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarInstancesTableName.' ('.implode(', ', $fieldNames).') VALUES ('.implode(', ', array_keys($values)).')'); + + $stmt->execute($values); + + return [ + $calendarId, + $this->pdo->lastInsertId($this->calendarInstancesTableName.'_id_seq'), + ]; + } + + /** + * Updates properties for a calendar. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $calendarId + */ + public function updateCalendar($calendarId, \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] = 'transparent' === $propertyValue->getValue(); + break; + default: + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + break; + } + } + $valuesSql = []; + foreach ($newValues as $fieldName => $value) { + $valuesSql[] = $fieldName.' = ?'; + } + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarInstancesTableName.' SET '.implode(', ', $valuesSql).' WHERE id = ?'); + $newValues['id'] = $instanceId; + $stmt->execute(array_values($newValues)); + + $this->addChange($calendarId, '', 2); + + return true; + }); + } + + /** + * Delete a calendar and all it's objects. + * + * @param mixed $calendarId + */ + public function deleteCalendar($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT access FROM '.$this->calendarInstancesTableName.' where id = ?'); + $stmt->execute([$instanceId]); + $access = (int) $stmt->fetchColumn(); + + if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $access) { + /** + * If the user is the owner of the calendar, we delete all data and all + * instances. + **/ + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarChangesTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarTableName.' WHERE id = ?'); + $stmt->execute([$calendarId]); + } else { + /** + * If it was an instance of a shared calendar, we only delete that + * instance. + */ + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE id = ?'); + $stmt->execute([$instanceId]); + } + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param mixed $calendarId + * + * @return array + */ + public function getCalendarObjects($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'component' => strtolower($row['componenttype']), + ]; + } + + return $result; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param mixed $calendarId + * @param string $objectUri + * + * @return array|null + */ + public function getCalendarObject($calendarId, $objectUri) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'calendardata' => $row['calendardata'], + 'component' => strtolower($row['componenttype']), + ]; + } + + /** + * Returns a list of calendar objects. + * + * This method should work identical to getCalendarObject, but instead + * return all the calendar objects in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $calendarId + * + * @return array + */ + public function getMultipleCalendarObjects($calendarId, array $uris) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $result = []; + foreach (array_chunk($uris, 900) as $chunk) { + $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri IN ('; + // Inserting a whole bunch of question marks + $query .= implode(',', array_fill(0, count($chunk), '?')); + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute(array_merge([$calendarId], $chunk)); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'calendardata' => $row['calendardata'], + 'component' => strtolower($row['componenttype']), + ]; + } + } + + return $result; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)'); + $stmt->execute([ + $calendarId, + $objectUri, + $calendarData, + time(), + $extraData['etag'], + $extraData['size'], + $extraData['componentType'], + $extraData['firstOccurence'], + $extraData['lastOccurence'], + $extraData['uid'], + ]); + $this->addChange($calendarId, $objectUri, 1); + + return '"'.$extraData['etag'].'"'; + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function updateCalendarObject($calendarId, $objectUri, $calendarData) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 2); + + return '"'.$extraData['etag'].'"'; + } + + /** + * Parses some information from calendar objects, used for optimized + * calendar-queries. + * + * Returns an array with the following keys: + * * etag - An md5 checksum of the object without the quotes. + * * size - Size of the object in bytes + * * componentType - VEVENT, VTODO or VJOURNAL + * * firstOccurence + * * lastOccurence + * * uid - value of the UID property + * + * @param string $calendarData + * + * @return array + */ + protected function getDenormalizedData($calendarData) + { + $vObject = VObject\Reader::read($calendarData); + $componentType = null; + $component = null; + $firstOccurence = null; + $lastOccurence = null; + $uid = null; + foreach ($vObject->getComponents() as $component) { + if ('VTIMEZONE' !== $component->name) { + $componentType = $component->name; + $uid = (string) $component->UID; + break; + } + } + if (!$componentType) { + throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); + } + if ('VEVENT' === $componentType) { + $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); + // Finding the last occurence is a bit harder + if (!isset($component->RRULE)) { + if (isset($component->DTEND)) { + $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); + } elseif (isset($component->DURATION)) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue())); + $lastOccurence = $endDate->getTimeStamp(); + } elseif (!$component->DTSTART->hasTime()) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate = $endDate->modify('+1 day'); + $lastOccurence = $endDate->getTimeStamp(); + } else { + $lastOccurence = $firstOccurence; + } + } else { + $it = new VObject\Recur\EventIterator($vObject, (string) $component->UID); + $maxDate = new \DateTime(self::MAX_DATE); + if ($it->isInfinite()) { + $lastOccurence = $maxDate->getTimeStamp(); + } else { + $end = $it->getDtEnd(); + while ($it->valid() && $end < $maxDate) { + $end = $it->getDtEnd(); + $it->next(); + } + $lastOccurence = $end->getTimeStamp(); + } + } + + // Ensure Occurence values are positive + if ($firstOccurence < 0) { + $firstOccurence = 0; + } + if ($lastOccurence < 0) { + $lastOccurence = 0; + } + } + + // Destroy circular references to PHP will GC the object. + $vObject->destroy(); + + return [ + 'etag' => md5($calendarData), + 'size' => strlen($calendarData), + 'componentType' => $componentType, + 'firstOccurence' => $firstOccurence, + 'lastOccurence' => $lastOccurence, + 'uid' => $uid, + ]; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param mixed $calendarId + * @param string $objectUri + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 3); + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly 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 + * + * @return array + */ + public function calendarQuery($calendarId, array $filters) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $componentType = null; + $requirePostFilter = true; + $timeRange = null; + + // if no filters were specified, we don't need to filter after a query + if (!$filters['prop-filters'] && !$filters['comp-filters']) { + $requirePostFilter = false; + } + + // Figuring out if there's a component filter + if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { + $componentType = $filters['comp-filters'][0]['name']; + + // Checking if we need post-filters + 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 ('VEVENT' == $componentType && 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 + */ + public function getCalendarObjectByUID($principalUri, $uid) + { + $query = <<calendarObjectTableName AS calendarobjects +LEFT JOIN + $this->calendarInstancesTableName AS calendar_instances + ON calendarobjects.calendarid = calendar_instances.calendarid +WHERE + calendar_instances.principaluri = ? + AND + calendarobjects.uid = ? + AND + calendar_instances.access = 1 +SQL; + + $stmt = $this->pdo->prepare($query); + $stmt->execute([$principalUri, $uid]); + + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $row['calendaruri'].'/'.$row['objecturi']; + } + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified calendar. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property this is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param mixed $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array + */ + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + // 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 + */ + protected function addChange($calendarId, $objectUri, $operation) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarChangesTableName.' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->calendarTableName.' WHERE id = ?'); + $stmt->execute([ + $objectUri, + $calendarId, + $operation, + $calendarId, + ]); + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET synctoken = synctoken + 1 WHERE id = ?'); + $stmt->execute([ + $calendarId, + ]); + } + + /** + * Returns a list of subscriptions for a principal. + * + * Every subscription is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * subscription. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the subscription. + * * principaluri. The owner of the subscription. Almost always the same as + * principalUri passed to this method. + * * source. Url to the actual feed + * + * Furthermore, all the subscription info must be returned too: + * + * 1. {DAV:}displayname + * 2. {http://apple.com/ns/ical/}refreshrate + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos + * should not be stripped). + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms + * should not be stripped). + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if + * attachments should not be stripped). + * 7. {http://apple.com/ns/ical/}calendar-color + * 8. {http://apple.com/ns/ical/}calendar-order + * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * (should just be an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of + * default components). + * + * @param string $principalUri + * + * @return array + */ + public function getSubscriptionsForUser($principalUri) + { + $fields = array_values($this->subscriptionPropertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'source'; + $fields[] = 'principaluri'; + $fields[] = 'lastmodified'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare('SELECT '.$fields.' FROM '.$this->calendarSubscriptionsTableName.' WHERE principaluri = ? ORDER BY calendarorder ASC'); + $stmt->execute([$principalUri]); + + $subscriptions = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $subscription = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + 'source' => $row['source'], + 'lastmodified' => $row['lastmodified'], + + '{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + ]; + + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { + if (!is_null($row[$dbName])) { + $subscription[$xmlName] = $row[$dbName]; + } + } + + $subscriptions[] = $subscription; + } + + return $subscriptions; + } + + /** + * Creates a new subscription for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this subscription in other methods, such as updateSubscription. + * + * @param string $principalUri + * @param string $uri + * + * @return mixed + */ + public function createSubscription($principalUri, $uri, array $properties) + { + $fieldNames = [ + 'principaluri', + 'uri', + 'source', + 'lastmodified', + ]; + + if (!isset($properties['{http://calendarserver.org/ns/}source'])) { + throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); + } + + $values = [ + ':principaluri' => $principalUri, + ':uri' => $uri, + ':source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), + ':lastmodified' => time(), + ]; + + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { + if (isset($properties[$xmlName])) { + $values[':'.$dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarSubscriptionsTableName.' ('.implode(', ', $fieldNames).') VALUES ('.implode(', ', array_keys($values)).')'); + $stmt->execute($values); + + return $this->pdo->lastInsertId( + $this->calendarSubscriptionsTableName.'_id_seq' + ); + } + + /** + * Updates a subscription. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $subscriptionId + * @param \Sabre\DAV\PropPatch $propPatch + */ + public 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 ('{http://calendarserver.org/ns/}source' === $propertyName) { + $newValues['source'] = $propertyValue->getHref(); + } else { + $fieldName = $this->subscriptionPropertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + } + } + + // Now we're generating the sql query. + $valuesSql = []; + foreach ($newValues as $fieldName => $value) { + $valuesSql[] = $fieldName.' = ?'; + } + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarSubscriptionsTableName.' SET '.implode(', ', $valuesSql).', lastmodified = ? WHERE id = ?'); + $newValues['lastmodified'] = time(); + $newValues['id'] = $subscriptionId; + $stmt->execute(array_values($newValues)); + + return true; + }); + } + + /** + * Deletes a subscription. + * + * @param mixed $subscriptionId + */ + public function deleteSubscription($subscriptionId) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarSubscriptionsTableName.' WHERE id = ?'); + $stmt->execute([$subscriptionId]); + } + + /** + * Returns a single scheduling object. + * + * The returned array should contain the following elements: + * * uri - A unique basename for the object. This will be used to + * construct a full uri. + * * calendardata - The iCalendar object + * * lastmodified - The last modification date. Can be an int for a unix + * timestamp, or a PHP DateTime object. + * * etag - A unique token that must change if the object changed. + * * size - The size of the object, in bytes. + * + * @param string $principalUri + * @param string $objectUri + * + * @return array + */ + public function getSchedulingObject($principalUri, $objectUri) + { + $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ? AND uri = ?'); + $stmt->execute([$principalUri, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'uri' => $row['uri'], + 'calendardata' => $row['calendardata'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + ]; + } + + /** + * Returns all scheduling objects for the inbox collection. + * + * These objects should be returned as an array. Every item in the array + * should follow the same structure as returned from getSchedulingObject. + * + * The main difference is that 'calendardata' is optional. + * + * @param string $principalUri + * + * @return array + */ + public function getSchedulingObjects($principalUri) + { + $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ?'); + $stmt->execute([$principalUri]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'calendardata' => $row['calendardata'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + ]; + } + + return $result; + } + + /** + * Deletes a scheduling object. + * + * @param string $principalUri + * @param string $objectUri + */ + public function deleteSchedulingObject($principalUri, $objectUri) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ? AND uri = ?'); + $stmt->execute([$principalUri, $objectUri]); + } + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string|resource $objectData + */ + public function createSchedulingObject($principalUri, $objectUri, $objectData) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->schedulingObjectTableName.' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)'); + + if (is_resource($objectData)) { + $objectData = stream_get_contents($objectData); + } + + $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData)]); + } + + /** + * Updates the list of shares. + * + * @param mixed $calendarId + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + */ + public function updateInvites($calendarId, array $sharees) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + $currentInvites = $this->getInvites($calendarId); + list($calendarId, $instanceId) = $calendarId; + + $removeStmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE calendarid = ? AND share_href = ? AND access IN (2,3)'); + $updateStmt = $this->pdo->prepare('UPDATE '.$this->calendarInstancesTableName.' SET access = ?, share_displayname = ?, share_invitestatus = ? WHERE calendarid = ? AND share_href = ?'); + + $insertStmt = $this->pdo->prepare(' +INSERT INTO '.$this->calendarInstancesTableName.' + ( + calendarid, + principaluri, + access, + displayname, + uri, + description, + calendarorder, + calendarcolor, + timezone, + transparent, + share_href, + share_displayname, + share_invitestatus + ) + SELECT + ?, + ?, + ?, + displayname, + ?, + description, + calendarorder, + calendarcolor, + timezone, + 1, + ?, + ?, + ? + FROM '.$this->calendarInstancesTableName.' WHERE id = ?'); + + foreach ($sharees as $sharee) { + if (\Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS === $sharee->access) { + // if access was set no NOACCESS, it means access for an + // existing sharee was removed. + $removeStmt->execute([$calendarId, $sharee->href]); + continue; + } + + if (is_null($sharee->principal)) { + // If the server could not determine the principal automatically, + // we will mark the invite status as invalid. + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_INVALID; + } else { + // Because sabre/dav does not yet have an invitation system, + // every invite is automatically accepted for now. + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED; + } + + foreach ($currentInvites as $oldSharee) { + if ($oldSharee->href === $sharee->href) { + // This is an update + $sharee->properties = array_merge( + $oldSharee->properties, + $sharee->properties + ); + $updateStmt->execute([ + $sharee->access, + isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null, + $sharee->inviteStatus ?: $oldSharee->inviteStatus, + $calendarId, + $sharee->href, + ]); + continue 2; + } + } + // If we got here, it means it was a new sharee + $insertStmt->execute([ + $calendarId, + $sharee->principal, + $sharee->access, + \Sabre\DAV\UUIDUtil::getUUID(), + $sharee->href, + isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null, + $sharee->inviteStatus ?: \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE, + $instanceId, + ]); + } + } + + /** + * Returns the list of people whom a calendar is shared with. + * + * Every item in the returned list must be a Sharee object with at + * least the following properties set: + * $href + * $shareAccess + * $inviteStatus + * + * and optionally: + * $properties + * + * @param mixed $calendarId + * + * @return \Sabre\DAV\Xml\Element\Sharee[] + */ + public function getInvites($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to getInvites() is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $query = <<calendarInstancesTableName} +WHERE + calendarid = ? +SQL; + + $stmt = $this->pdo->prepare($query); + $stmt->execute([$calendarId]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = new Sharee([ + 'href' => isset($row['share_href']) ? $row['share_href'] : \Sabre\HTTP\encodePath($row['principaluri']), + 'access' => (int) $row['access'], + /// Everyone is always immediately accepted, for now. + 'inviteStatus' => (int) $row['share_invitestatus'], + 'properties' => !empty($row['share_displayname']) + ? ['{DAV:}displayname' => $row['share_displayname']] + : [], + 'principal' => $row['principaluri'], + ]); + } + + return $result; + } + + /** + * Publishes a calendar. + * + * @param mixed $calendarId + * @param bool $value + */ + public function setPublishStatus($calendarId, $value) + { + throw new DAV\Exception\NotImplemented('Not implemented'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php new file mode 100644 index 0000000..69467e5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php @@ -0,0 +1,66 @@ +pdo = $pdo; + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the calendar. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * + * @return array + */ + public function getCalendarsForUser($principalUri) + { + // Making fields a comma-delimited list + $stmt = $this->pdo->prepare('SELECT id, uri FROM simple_calendars WHERE principaluri = ? ORDER BY id ASC'); + $stmt->execute([$principalUri]); + + $calendars = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $calendars[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $principalUri, + ]; + } + + return $calendars; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used + * to reference this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * + * @return string + */ + public function createCalendar($principalUri, $calendarUri, array $properties) + { + $stmt = $this->pdo->prepare('INSERT INTO simple_calendars (principaluri, uri) VALUES (?, ?)'); + $stmt->execute([$principalUri, $calendarUri]); + + return $this->pdo->lastInsertId(); + } + + /** + * Delete a calendar and all it's objects. + * + * @param string $calendarId + */ + public function deleteCalendar($calendarId) + { + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM simple_calendars WHERE id = ?'); + $stmt->execute([$calendarId]); + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param string $calendarId + * + * @return array + */ + public function getCalendarObjects($calendarId) + { + $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'etag' => '"'.md5($row['calendardata']).'"', + 'calendarid' => $calendarId, + 'size' => strlen($row['calendardata']), + 'calendardata' => $row['calendardata'], + ]; + } + + return $result; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param string $calendarId + * @param string $objectUri + * + * @return array|null + */ + public function getCalendarObject($calendarId, $objectUri) + { + $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'etag' => '"'.md5($row['calendardata']).'"', + 'calendarid' => $calendarId, + 'size' => strlen($row['calendardata']), + 'calendardata' => $row['calendardata'], + ]; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + $stmt = $this->pdo->prepare('INSERT INTO simple_calendarobjects (calendarid, uri, calendardata) VALUES (?,?,?)'); + $stmt->execute([ + $calendarId, + $objectUri, + $calendarData, + ]); + + return '"'.md5($calendarData).'"'; + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function updateCalendarObject($calendarId, $objectUri, $calendarData) + { + $stmt = $this->pdo->prepare('UPDATE simple_calendarobjects SET calendardata = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarData, $calendarId, $objectUri]); + + return '"'.md5($calendarData).'"'; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param string $calendarId + * @param string $objectUri + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php new file mode 100644 index 0000000..7655c2e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php @@ -0,0 +1,89 @@ + 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ); + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property This is * needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array + */ + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null); +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Calendar.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Calendar.php new file mode 100644 index 0000000..9f32e70 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Calendar.php @@ -0,0 +1,451 @@ +caldavBackend = $caldavBackend; + $this->calendarInfo = $calendarInfo; + } + + /** + * Returns the name of the calendar. + * + * @return string + */ + public function getName() + { + return $this->calendarInfo['uri']; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(PropPatch $propPatch) + { + return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch); + } + + /** + * Returns the list of properties. + * + * @param array $requestedProperties + * + * @return array + */ + public function getProperties($requestedProperties) + { + $response = []; + + foreach ($this->calendarInfo as $propName => $propValue) { + if (!is_null($propValue) && '{' === $propName[0]) { + $response[$propName] = $this->calendarInfo[$propName]; + } + } + + return $response; + } + + /** + * Returns a calendar object. + * + * The contained calendar objects are for example Events or Todo's. + * + * @param string $name + * + * @return \Sabre\CalDAV\ICalendarObject + */ + public function getChild($name) + { + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name); + + if (!$obj) { + throw new DAV\Exception\NotFound('Calendar object not found'); + } + $obj['acl'] = $this->getChildACL(); + + return new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + /** + * Returns the full list of calendar objects. + * + * @return array + */ + public function getChildren() + { + $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + return $children; + } + + /** + * This method receives a list of paths in it's first argument. + * It must return an array with Node objects. + * + * If any children are not found, you do not have to return them. + * + * @param string[] $paths + * + * @return array + */ + public function getMultipleChildren(array $paths) + { + $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + return $children; + } + + /** + * Checks if a child-node exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name); + if (!$obj) { + return false; + } else { + return true; + } + } + + /** + * Creates a new directory. + * + * We actually block this, as subdirectories are not allowed in calendars. + * + * @param string $name + */ + public function createDirectory($name) + { + throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed'); + } + + /** + * Creates a new file. + * + * The contents of the new file must be a valid ICalendar string. + * + * @param string $name + * @param resource $calendarData + * + * @return string|null + */ + public 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. + */ + public function delete() + { + $this->caldavBackend->deleteCalendar($this->calendarInfo['id']); + } + + /** + * Renames the calendar. Note that most calendars use the + * {DAV:}displayname to display a name to display a name. + * + * @param string $newName + */ + public function setName($newName) + { + throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported'); + } + + /** + * Returns the last modification date as a unix timestamp. + */ + public function getLastModified() + { + return null; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->calendarInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{'.Plugin::NS_CALDAV.'}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ]; + } + + return $acl; + } + + /** + * This method returns the ACL's for calendar objects in this calendar. + * The result of this method automatically gets passed to the + * calendar-object nodes in the calendar. + * + * @return array + */ + public function getChildACL() + { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + ]; + if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ]; + } + + return $acl; + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre\CalDAV\CalendarQueryParser. + * + * @return array + */ + public function calendarQuery(array $filters) + { + return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters); + } + + /** + * This method returns the current sync-token for this collection. + * This can be any string. + * + * If null is returned from this function, the plugin assumes there's no + * sync information available. + * + * @return string|null + */ + public function getSyncToken() + { + if ( + $this->caldavBackend instanceof Backend\SyncSupport && + isset($this->calendarInfo['{DAV:}sync-token']) + ) { + return $this->calendarInfo['{DAV:}sync-token']; + } + if ( + $this->caldavBackend instanceof Backend\SyncSupport && + isset($this->calendarInfo['{http://sabredav.org/ns}sync-token']) + ) { + return $this->calendarInfo['{http://sabredav.org/ns}sync-token']; + } + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken and the current collection. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * 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 + */ + public function getChanges($syncToken, $syncLevel, $limit = null) + { + if (!$this->caldavBackend instanceof Backend\SyncSupport) { + return null; + } + + return $this->caldavBackend->getChangesForCalendar( + $this->calendarInfo['id'], + $syncToken, + $syncLevel, + $limit + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarHome.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarHome.php new file mode 100644 index 0000000..159ddcc --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarHome.php @@ -0,0 +1,356 @@ +caldavBackend = $caldavBackend; + $this->principalInfo = $principalInfo; + } + + /** + * Returns the name of this object. + * + * @return string + */ + public function getName() + { + list(, $name) = Uri\split($this->principalInfo['uri']); + + return $name; + } + + /** + * Updates the name of this object. + * + * @param string $name + */ + public function setName($name) + { + throw new DAV\Exception\Forbidden(); + } + + /** + * Deletes this object. + */ + public function delete() + { + throw new DAV\Exception\Forbidden(); + } + + /** + * Returns the last modification date. + * + * @return int + */ + public function getLastModified() + { + return null; + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $filename + * @param resource $data + */ + public 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 + */ + public function createDirectory($filename) + { + throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported'); + } + + /** + * Returns a single calendar, by name. + * + * @param string $name + * + * @return Calendar + */ + public function getChild($name) + { + // Special nodes + if ('inbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + } + if ('outbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Outbox($this->principalInfo['uri']); + } + if ('notifications' === $name && $this->caldavBackend instanceof Backend\NotificationSupport) { + return new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // Calendars + foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) { + if ($calendar['uri'] === $name) { + if ($this->caldavBackend instanceof Backend\SharingSupport) { + return new SharedCalendar($this->caldavBackend, $calendar); + } else { + return new Calendar($this->caldavBackend, $calendar); + } + } + } + + if ($this->caldavBackend instanceof Backend\SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + if ($subscription['uri'] === $name) { + return new Subscriptions\Subscription($this->caldavBackend, $subscription); + } + } + } + + throw new NotFound('Node with name \''.$name.'\' could not be found'); + } + + /** + * Checks if a calendar exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + try { + return (bool) $this->getChild($name); + } catch (NotFound $e) { + return false; + } + } + + /** + * Returns a list of calendars. + * + * @return array + */ + public function getChildren() + { + $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); + $objs = []; + foreach ($calendars as $calendar) { + if ($this->caldavBackend instanceof Backend\SharingSupport) { + $objs[] = new SharedCalendar($this->caldavBackend, $calendar); + } else { + $objs[] = new Calendar($this->caldavBackend, $calendar); + } + } + + if ($this->caldavBackend instanceof Backend\SchedulingSupport) { + $objs[] = new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + $objs[] = new Schedule\Outbox($this->principalInfo['uri']); + } + + // We're adding a notifications node, if it's supported by the backend. + if ($this->caldavBackend instanceof Backend\NotificationSupport) { + $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // If the backend supports subscriptions, we'll add those as well, + if ($this->caldavBackend instanceof Backend\SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + $objs[] = new Subscriptions\Subscription($this->caldavBackend, $subscription); + } + } + + return $objs; + } + + /** + * Creates a new calendar or subscription. + * + * @param string $name + * + * @throws DAV\Exception\InvalidResourceType + */ + public function createExtendedCollection($name, MkCol $mkCol) + { + $isCalendar = false; + $isSubscription = false; + foreach ($mkCol->getResourceType() as $rt) { + switch ($rt) { + case '{DAV:}collection': + case '{http://calendarserver.org/ns/}shared-owner': + // ignore + break; + case '{urn:ietf:params:xml:ns:caldav}calendar': + $isCalendar = true; + break; + case '{http://calendarserver.org/ns/}subscribed': + $isSubscription = true; + break; + default: + throw new DAV\Exception\InvalidResourceType('Unknown resourceType: '.$rt); + } + } + + $properties = $mkCol->getRemainingValues(); + $mkCol->setRemainingResultCode(201); + + if ($isSubscription) { + if (!$this->caldavBackend instanceof Backend\SubscriptionSupport) { + throw new DAV\Exception\InvalidResourceType('This backend does not support subscriptions'); + } + $this->caldavBackend->createSubscription($this->principalInfo['uri'], $name, $properties); + } elseif ($isCalendar) { + $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties); + } else { + throw new DAV\Exception\InvalidResourceType('You can only create calendars and subscriptions in this collection'); + } + } + + /** + * Returns the owner of the calendar home. + * + * @return string + */ + public function getOwner() + { + return $this->principalInfo['uri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principalInfo['uri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } + + /** + * This method is called when a user replied to a request to share. + * + * This method should return the url of the newly created calendar if the + * share was accepted. + * + * @param string $href The sharee who is replying (often a mailto: address) + * @param int $status One of the SharingPlugin::STATUS_* constants + * @param string $calendarUri The url to the calendar thats being shared + * @param string $inReplyTo The unique id this message is a response to + * @param string $summary A description of the reply + * + * @return string|null + */ + public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) + { + if (!$this->caldavBackend instanceof Backend\SharingSupport) { + throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.'); + } + + return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary); + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $uid + * + * @return string|null + */ + public function getCalendarObjectByUID($uid) + { + return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarObject.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarObject.php new file mode 100644 index 0000000..671f4b5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarObject.php @@ -0,0 +1,223 @@ +caldavBackend = $caldavBackend; + + if (!isset($objectData['uri'])) { + throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property'); + } + + $this->calendarInfo = $calendarInfo; + $this->objectData = $objectData; + } + + /** + * Returns the uri for this object. + * + * @return string + */ + public function getName() + { + return $this->objectData['uri']; + } + + /** + * Returns the ICalendar-formatted object. + * + * @return string + */ + public function get() + { + // Pre-populating the 'calendardata' is optional, if we don't have it + // already we fetch it from the backend. + if (!isset($this->objectData['calendardata'])) { + $this->objectData = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + } + + return $this->objectData['calendardata']; + } + + /** + * Updates the ICalendar-formatted object. + * + * @param string|resource $calendarData + * + * @return string + */ + public function put($calendarData) + { + if (is_resource($calendarData)) { + $calendarData = stream_get_contents($calendarData); + } + $etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'], $this->objectData['uri'], $calendarData); + $this->objectData['calendardata'] = $calendarData; + $this->objectData['etag'] = $etag; + + return $etag; + } + + /** + * Deletes the calendar object. + */ + public function delete() + { + $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + } + + /** + * Returns the mime content-type. + * + * @return string + */ + public function getContentType() + { + $mime = 'text/calendar; charset=utf-8'; + if (isset($this->objectData['component']) && $this->objectData['component']) { + $mime .= '; component='.$this->objectData['component']; + } + + return $mime; + } + + /** + * Returns an ETag for this object. + * + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * @return string + */ + public function getETag() + { + if (isset($this->objectData['etag'])) { + return $this->objectData['etag']; + } else { + return '"'.md5($this->get()).'"'; + } + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return int + */ + public function getLastModified() + { + return $this->objectData['lastmodified']; + } + + /** + * Returns the size of this object in bytes. + * + * @return int + */ + public function getSize() + { + if (array_key_exists('size', $this->objectData)) { + return $this->objectData['size']; + } else { + return strlen($this->get()); + } + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->calendarInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + // An alternative acl may be specified in the object data. + if (isset($this->objectData['acl'])) { + return $this->objectData['acl']; + } + + // The default ACL + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php new file mode 100644 index 0000000..7ce1c05 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php @@ -0,0 +1,340 @@ +name !== $filters['name']) { + return false; + } + + return + $this->validateCompFilters($vObject, $filters['comp-filters']) && + $this->validatePropFilters($vObject, $filters['prop-filters']); + } + + /** + * This method checks the validity of comp-filters. + * + * A list of comp-filters needs to be specified. Also the parent of the + * component we're checking should be specified, not the component to check + * itself. + * + * @return bool + */ + protected function validateCompFilters(VObject\Component $parent, array $filters) + { + foreach ($filters as $filter) { + $isDefined = isset($parent->{$filter['name']}); + + if ($filter['is-not-defined']) { + if ($isDefined) { + return false; + } else { + continue; + } + } + if (!$isDefined) { + return false; + } + + if ($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. + * + * @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. + * + * @return bool + */ + protected function validateParamFilters(VObject\Property $parent, array $filters) + { + foreach ($filters as $filter) { + $isDefined = isset($parent[$filter['name']]); + + if ($filter['is-not-defined']) { + if ($isDefined) { + return false; + } else { + continue; + } + } + if (!$isDefined) { + return false; + } + + if (!$filter['text-match']) { + continue; + } + + // If there are sub-filters, we need to find at least one parameter + // for which the subfilters hold true. + foreach ($parent[$filter['name']]->getParts() as $paramPart) { + if ($this->validateTextMatch($paramPart, $filter['text-match'])) { + // We had a match, so this param-filter succeeds + continue 2; + } + } + + // If we got here it means there was a text-match filter and there + // were no matches. This means the filter needs to return false. + return false; + } + + // If we got here it means we got through all param-filters alive so the + // filters were all true. + return true; + } + + /** + * This method checks the validity of a text-match. + * + * A single text-match should be specified as well as the specific property + * or parameter we need to validate. + * + * @param VObject\Node|string $check value to check against + * + * @return bool + */ + protected function validateTextMatch($check, array $textMatch) + { + if ($check instanceof VObject\Node) { + $check = $check->getValue(); + } + + $isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']); + + return $textMatch['negate-condition'] xor $isMatching; + } + + /** + * Validates if a component matches the given time range. + * + * This is all based on the rules specified in rfc4791, which are quite + * complex. + * + * @param DateTime $start + * @param DateTime $end + * + * @return bool + */ + protected function validateTimeRange(VObject\Node $component, $start, $end) + { + if (is_null($start)) { + $start = new DateTime('1900-01-01'); + } + if (is_null($end)) { + $end = new DateTime('3000-01-01'); + } + + switch ($component->name) { + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + + return $component->isInTimeRange($start, $end); + + case 'VALARM': + + // If the valarm is wrapped in a recurring event, we need to + // expand the recursions, and validate each. + // + // Our datamodel doesn't easily allow us to do this straight + // in the VALARM component code, so this is a hack, and an + // expensive one too. + if ('VEVENT' === $component->parent->name && $component->parent->RRULE) { + // Fire up the iterator! + $it = new VObject\Recur\EventIterator($component->parent->parent, (string) $component->parent->UID); + while ($it->valid()) { + $expandedEvent = $it->getEventObject(); + + // We need to check from these expanded alarms, which + // one is the first to trigger. Based on this, we can + // determine if we can 'give up' expanding events. + $firstAlarm = null; + if (null !== $expandedEvent->VALARM) { + foreach ($expandedEvent->VALARM as $expandedAlarm) { + $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime(); + if ($expandedAlarm->isInTimeRange($start, $end)) { + return true; + } + + if ('DATE-TIME' === (string) $expandedAlarm->TRIGGER['VALUE']) { + // This is an alarm with a non-relative trigger + // time, likely created by a buggy client. The + // implication is that every alarm in this + // recurring event trigger at the exact same + // time. It doesn't make sense to traverse + // further. + } else { + // We store the first alarm as a means to + // figure out when we can stop traversing. + if (!$firstAlarm || $effectiveTrigger < $firstAlarm) { + $firstAlarm = $effectiveTrigger; + } + } + } + } + if (is_null($firstAlarm)) { + // No alarm was found. + // + // Or technically: No alarm that will change for + // every instance of the recurrence was found, + // which means we can assume there was no match. + return false; + } + if ($firstAlarm > $end) { + return false; + } + $it->next(); + } + + return false; + } else { + return $component->isInTimeRange($start, $end); + } + + // no break + case 'VFREEBUSY': + throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on '.$component->name.' components'); + case 'COMPLETED': + case 'CREATED': + case 'DTEND': + case 'DTSTAMP': + case 'DTSTART': + case 'DUE': + case 'LAST-MODIFIED': + return $start <= $component->getDateTime() && $end >= $component->getDateTime(); + + default: + throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a '.$component->name.' component'); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php new file mode 100644 index 0000000..3038d21 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php @@ -0,0 +1,75 @@ +caldavBackend = $caldavBackend; + } + + /** + * Returns the nodename. + * + * We're overriding this, because the default will be the 'principalPrefix', + * and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT + * + * @return string + */ + public function getName() + { + return Plugin::CALENDAR_ROOT; + } + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return \Sabre\DAV\INode + */ + public function getChildForPrincipal(array $principal) + { + return new CalendarHome($this->caldavBackend, $principal); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php new file mode 100644 index 0000000..e94378a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV, 'cal:supported-calendar-component'); + $errorNode->appendChild($np); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php new file mode 100644 index 0000000..9171e36 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php @@ -0,0 +1,377 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('browserButtonActions', function ($path, $node, &$actions) { + if ($node instanceof ICalendar) { + $actions .= ''; + } + }); + } + + /** + * Intercepts GET requests on calendar urls ending with ?export. + * + * @throws BadRequest + * @throws DAV\Exception\NotFound + * @throws VObject\InvalidDataException + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('export', $queryParams)) { + return; + } + + $path = $request->getPath(); + + $node = $this->server->getProperties($path, [ + '{DAV:}resourcetype', + '{DAV:}displayname', + '{http://sabredav.org/ns}sync-token', + '{DAV:}sync-token', + '{http://apple.com/ns/ical/}calendar-color', + ]); + + if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{'.Plugin::NS_CALDAV.'}calendar')) { + return; + } + // Marking the transactionType, for logging purposes. + $this->server->transactionType = 'get-calendar-export'; + + $properties = $node; + + $start = null; + $end = null; + $expand = false; + $componentType = false; + if (isset($queryParams['start'])) { + if (!ctype_digit($queryParams['start'])) { + throw new BadRequest('The start= parameter must contain a unix timestamp'); + } + $start = DateTime::createFromFormat('U', $queryParams['start']); + } + if (isset($queryParams['end'])) { + if (!ctype_digit($queryParams['end'])) { + throw new BadRequest('The end= parameter must contain a unix timestamp'); + } + $end = DateTime::createFromFormat('U', $queryParams['end']); + } + if (isset($queryParams['expand']) && (bool) $queryParams['expand']) { + if (!$start || !$end) { + throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.'); + } + $expand = true; + $componentType = 'VEVENT'; + } + if (isset($queryParams['componentType'])) { + if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) { + throw new BadRequest('You are not allowed to search for components of type: '.$queryParams['componentType'].' here'); + } + $componentType = $queryParams['componentType']; + } + + $format = \Sabre\HTTP\negotiateContentType( + $request->getHeader('Accept'), + [ + 'text/calendar', + 'application/calendar+json', + ] + ); + + if (isset($queryParams['accept'])) { + if ('application/calendar+json' === $queryParams['accept'] || 'jcal' === $queryParams['accept']) { + $format = 'application/calendar+json'; + } + } + if (!$format) { + $format = 'text/calendar'; + } + + $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response); + + // Returning false to break the event chain + return false; + } + + /** + * This method is responsible for generating the actual, full response. + * + * @param string $path + * @param DateTime|null $start + * @param DateTime|null $end + * @param bool $expand + * @param string $componentType + * @param string $format + * @param array $properties + * + * @throws DAV\Exception\NotFound + * @throws VObject\InvalidDataException + */ + protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) + { + $calDataProp = '{'.Plugin::NS_CALDAV.'}calendar-data'; + $calendarNode = $this->server->tree->getNodeForPath($path); + + $blobs = []; + if ($start || $end || $componentType) { + // If there was a start or end filter, we need to enlist + // calendarQuery for speed. + $queryResult = $calendarNode->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => $componentType, + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + // queryResult is just a list of base urls. We need to prefix the + // calendar path. + $queryResult = array_map( + function ($item) use ($path) { + return $path.'/'.$item; + }, + $queryResult + ); + $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]); + unset($queryResult); + } else { + $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1); + } + + // Flattening the arrays + foreach ($nodes as $node) { + if (isset($node[200][$calDataProp])) { + $blobs[$node['href']] = $node[200][$calDataProp]; + } + } + unset($nodes); + + $mergedCalendar = $this->mergeObjects( + $properties, + $blobs + ); + + if ($expand) { + $calendarTimeZone = null; + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{'.Plugin::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references to PHP will GC the object. + $vtimezoneObj->destroy(); + unset($vtimezoneObj); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + + $mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone); + } + + $filenameExtension = '.ics'; + + switch ($format) { + case 'text/calendar': + $mergedCalendar = $mergedCalendar->serialize(); + $filenameExtension = '.ics'; + break; + case 'application/calendar+json': + $mergedCalendar = json_encode($mergedCalendar->jsonSerialize()); + $filenameExtension = '.json'; + break; + } + + $filename = preg_replace( + '/[^a-zA-Z0-9-_ ]/um', + '', + $calendarNode->getName() + ); + $filename .= '-'.date('Y-m-d').$filenameExtension; + + $response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"'); + $response->setHeader('Content-Type', $format); + + $response->setStatus(200); + $response->setBody($mergedCalendar); + } + + /** + * Merges all calendar objects, and builds one big iCalendar blob. + * + * @param array $properties Some CalDAV properties + * + * @return VObject\Component\VCalendar + */ + public function mergeObjects(array $properties, array $inputObjects) + { + $calendar = new VObject\Component\VCalendar(); + $calendar->VERSION = '2.0'; + if (DAV\Server::$exposeVersion) { + $calendar->PRODID = '-//SabreDAV//SabreDAV '.DAV\Version::VERSION.'//EN'; + } else { + $calendar->PRODID = '-//SabreDAV//SabreDAV//EN'; + } + if (isset($properties['{DAV:}displayname'])) { + $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname']; + } + if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) { + $calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color']; + } + + $collectedTimezones = []; + + $timezones = []; + $objects = []; + + foreach ($inputObjects as $href => $inputObject) { + $nodeComp = VObject\Reader::read($inputObject); + + foreach ($nodeComp->children() as $child) { + switch ($child->name) { + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + $objects[] = clone $child; + break; + + // VTIMEZONE is special, because we need to filter out the duplicates + case 'VTIMEZONE': + // Naively just checking tzid. + if (in_array((string) $child->TZID, $collectedTimezones)) { + break; + } + + $timezones[] = clone $child; + $collectedTimezones[] = $child->TZID; + break; + } + } + // Destroy circular references to PHP will GC the object. + $nodeComp->destroy(); + unset($nodeComp); + } + + foreach ($timezones as $tz) { + $calendar->add($tz); + } + foreach ($objects as $obj) { + $calendar->add($obj); + } + + return $calendar; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'ics-export'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.', + 'link' => 'http://sabre.io/dav/ics-export-plugin/', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICalendar.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICalendar.php new file mode 100644 index 0000000..8636e0b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/ICalendar.php @@ -0,0 +1,20 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + } + + /** + * Returns all notifications for a principal. + * + * @return array + */ + public function getChildren() + { + $children = []; + $notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri); + + foreach ($notifications as $notification) { + $children[] = new Node( + $this->caldavBackend, + $this->principalUri, + $notification + ); + } + + return $children; + } + + /** + * Returns the name of this object. + * + * @return string + */ + public function getName() + { + return 'notifications'; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php new file mode 100644 index 0000000..b12fb39 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php @@ -0,0 +1,25 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + $this->notification = $notification; + } + + /** + * Returns the path name for this notification. + * + * @return string + */ + public function getName() + { + return $this->notification->getId().'.xml'; + } + + /** + * Returns the etag for the notification. + * + * The etag must be surrounded by litteral double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->notification->getETag(); + } + + /** + * This method must return an xml element, using the + * Sabre\CalDAV\Xml\Notification\NotificationInterface classes. + * + * @return NotificationInterface + */ + public function getNotificationType() + { + return $this->notification; + } + + /** + * Deletes this notification. + */ + public function delete() + { + $this->caldavBackend->deleteNotification($this->getOwner(), $this->notification); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php new file mode 100644 index 0000000..56b2fe9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php @@ -0,0 +1,161 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('propFind', [$this, 'propFind']); + + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{'.self::NS_CALENDARSERVER.'}notification'; + + array_push($server->protectedProperties, + '{'.self::NS_CALENDARSERVER.'}notification-URL', + '{'.self::NS_CALENDARSERVER.'}notificationtype' + ); + } + + /** + * PropFind. + */ + public function propFind(PropFind $propFind, BaseINode $node) + { + $caldavPlugin = $this->server->getPlugin('caldav'); + + if ($node instanceof DAVACL\IPrincipal) { + $principalUrl = $node->getPrincipalUrl(); + + // notification-URL property + $propFind->handle('{'.self::NS_CALENDARSERVER.'}notification-URL', function () use ($principalUrl, $caldavPlugin) { + $notificationPath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl).'/notifications/'; + + return new DAV\Xml\Property\Href($notificationPath); + }); + } + + if ($node instanceof INode) { + $propFind->handle( + '{'.self::NS_CALENDARSERVER.'}notificationtype', + [$node, 'getNotificationType'] + ); + } + } + + /** + * This event is triggered before the usual GET request handler. + * + * We use this to intercept GET calls to notification nodes, and return the + * proper response. + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (DAV\Exception\NotFound $e) { + return; + } + + if (!$node instanceof INode) { + return; + } + + $writer = $this->server->xml->getWriter(); + $writer->contextUri = $this->server->getBaseUri(); + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('{http://calendarserver.org/ns/}notification'); + $node->getNotificationType()->xmlSerializeFull($writer); + $writer->endElement(); + + $response->setHeader('Content-Type', 'application/xml'); + $response->setHeader('ETag', $node->getETag()); + $response->setStatus(200); + $response->setBody($writer->outputMemory()); + + // Return false to break the event chain. + return false; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for caldav-notifications, which is required to enable caldav-sharing.', + 'link' => 'http://sabre.io/dav/caldav-sharing/', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Plugin.php b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Plugin.php new file mode 100644 index 0000000..da17204 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/CalDAV/Plugin.php @@ -0,0 +1,1011 @@ +server->tree->getNodeForPath($parent); + + if ($node instanceof DAV\IExtendedCollection) { + try { + $node->getChild($name); + } catch (DAV\Exception\NotFound $e) { + return ['MKCALENDAR']; + } + } + + return []; + } + + /** + * Returns the path to a principal's calendar home. + * + * The return url must not end with a slash. + * This function should return null in case a principal did not have + * a calendar home. + * + * @param string $principalUrl + * + * @return string + */ + public function getCalendarHomeForPrincipal($principalUrl) + { + // The default behavior for most sabre/dav servers is that there is a + // principals root node, which contains users directly under it. + // + // This function assumes that there are two components in a principal + // path. If there's more, we don't return a calendar home. This + // excludes things like the calendar-proxy-read principal (which it + // should). + $parts = explode('/', trim($principalUrl, '/')); + if (2 !== count($parts)) { + return; + } + if ('principals' !== $parts[0]) { + return; + } + + return self::CALENDAR_ROOT.'/'.$parts[1]; + } + + /** + * Returns a list of features for the DAV: HTTP header. + * + * @return array + */ + public function getFeatures() + { + return ['calendar-access', 'calendar-proxy']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'caldav'; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + $node = $this->server->tree->getNodeForPath($uri); + + $reports = []; + if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) { + $reports[] = '{'.self::NS_CALDAV.'}calendar-multiget'; + $reports[] = '{'.self::NS_CALDAV.'}calendar-query'; + } + if ($node instanceof ICalendar) { + $reports[] = '{'.self::NS_CALDAV.'}free-busy-query'; + } + // iCal has a bug where it assumes that sync support is enabled, only + // if we say we support it on the calendar-home, even though this is + // not actually the case. + if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) { + $reports[] = '{DAV:}sync-collection'; + } + + return $reports; + } + + /** + * Initializes the plugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + + $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']); + $server->on('report', [$this, 'report']); + $server->on('propFind', [$this, 'propFind']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); + $server->on('afterMethod:GET', [$this, 'httpAfterGET']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + + $server->xml->namespaceMap[self::NS_CALDAV] = 'cal'; + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + + $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write'; + + array_push($server->protectedProperties, + '{'.self::NS_CALDAV.'}supported-calendar-component-set', + '{'.self::NS_CALDAV.'}supported-calendar-data', + '{'.self::NS_CALDAV.'}max-resource-size', + '{'.self::NS_CALDAV.'}min-date-time', + '{'.self::NS_CALDAV.'}max-date-time', + '{'.self::NS_CALDAV.'}max-instances', + '{'.self::NS_CALDAV.'}max-attendees-per-instance', + '{'.self::NS_CALDAV.'}calendar-home-set', + '{'.self::NS_CALDAV.'}supported-collation-set', + '{'.self::NS_CALDAV.'}calendar-data', + + // CalendarServer extensions + '{'.self::NS_CALENDARSERVER.'}getctag', + '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for', + '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for' + ); + + if ($aclPlugin = $server->getPlugin('acl')) { + $aclPlugin->principalSearchPropertySet['{'.self::NS_CALDAV.'}calendar-user-address-set'] = 'Calendar address'; + } + } + + /** + * This functions handles REPORT requests specific to CalDAV. + * + * @param string $reportName + * @param mixed $report + * @param mixed $path + * + * @return bool + */ + public function report($reportName, $report, $path) + { + switch ($reportName) { + case '{'.self::NS_CALDAV.'}calendar-multiget': + $this->server->transactionType = 'report-calendar-multiget'; + $this->calendarMultiGetReport($report); + + return false; + case '{'.self::NS_CALDAV.'}calendar-query': + $this->server->transactionType = 'report-calendar-query'; + $this->calendarQueryReport($report); + + return false; + case '{'.self::NS_CALDAV.'}free-busy-query': + $this->server->transactionType = 'report-free-busy-query'; + $this->freeBusyQueryReport($report); + + return false; + } + } + + /** + * This function handles the MKCALENDAR HTTP method, which creates + * a new calendar. + * + * @return bool + */ + public function httpMkCalendar(RequestInterface $request, ResponseInterface $response) + { + $body = $request->getBodyAsString(); + $path = $request->getPath(); + + $properties = []; + + if ($body) { + try { + $mkcalendar = $this->server->xml->expect( + '{urn:ietf:params:xml:ns:caldav}mkcalendar', + $body + ); + } catch (\Sabre\Xml\ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + $properties = $mkcalendar->getProperties(); + } + + // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored + // subscriptions. Before that it used MKCOL which was the correct way + // to do this. + // + // If the body had a {DAV:}resourcetype, it means we stumbled upon this + // request, and we simply use it instead of the pre-defined list. + if (isset($properties['{DAV:}resourcetype'])) { + $resourceType = $properties['{DAV:}resourcetype']->getValue(); + } else { + $resourceType = ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar']; + } + + $this->server->createCollection($path, new MkCol($resourceType, $properties)); + + $response->setStatus(201); + $response->setHeader('Content-Length', 0); + + // This breaks the method chain. + return false; + } + + /** + * PropFind. + * + * This method handler is invoked before any after properties for a + * resource are fetched. This allows us to add in any CalDAV specific + * properties. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $ns = '{'.self::NS_CALDAV.'}'; + + if ($node instanceof ICalendarObjectContainer) { + $propFind->handle($ns.'max-resource-size', $this->maxResourceSize); + $propFind->handle($ns.'supported-calendar-data', function () { + return new Xml\Property\SupportedCalendarData(); + }); + $propFind->handle($ns.'supported-collation-set', function () { + return new Xml\Property\SupportedCollationSet(); + }); + } + + if ($node instanceof DAVACL\IPrincipal) { + $principalUrl = $node->getPrincipalUrl(); + + $propFind->handle('{'.self::NS_CALDAV.'}calendar-home-set', function () use ($principalUrl) { + $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl); + if (is_null($calendarHomePath)) { + return null; + } + + return new LocalHref($calendarHomePath.'/'); + }); + // The calendar-user-address-set property is basically mapped to + // the {DAV:}alternate-URI-set property. + $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-address-set', function () use ($node) { + $addresses = $node->getAlternateUriSet(); + $addresses[] = $this->server->getBaseUri().$node->getPrincipalUrl().'/'; + + return new LocalHref($addresses); + }); + // For some reason somebody thought it was a good idea to add + // another one of these properties. We're supporting it too. + $propFind->handle('{'.self::NS_CALENDARSERVER.'}email-address-set', function () use ($node) { + $addresses = $node->getAlternateUriSet(); + $emails = []; + foreach ($addresses as $address) { + if ('mailto:' === substr($address, 0, 7)) { + $emails[] = substr($address, 7); + } + } + + return new Xml\Property\EmailAddressSet($emails); + }); + + // These two properties are shortcuts for ical to easily find + // other principals this principal has access to. + $propRead = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for'; + $propWrite = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for'; + + if (404 === $propFind->getStatus($propRead) || 404 === $propFind->getStatus($propWrite)) { + $aclPlugin = $this->server->getPlugin('acl'); + $membership = $aclPlugin->getPrincipalMembership($propFind->getPath()); + $readList = []; + $writeList = []; + + foreach ($membership as $group) { + $groupNode = $this->server->tree->getNodeForPath($group); + + $listItem = Uri\split($group)[0].'/'; + + // If the node is either ap proxy-read or proxy-write + // group, we grab the parent principal and add it to the + // list. + if ($groupNode instanceof Principal\IProxyRead) { + $readList[] = $listItem; + } + if ($groupNode instanceof Principal\IProxyWrite) { + $writeList[] = $listItem; + } + } + + $propFind->set($propRead, new LocalHref($readList)); + $propFind->set($propWrite, new LocalHref($writeList)); + } + } // instanceof IPrincipal + + if ($node instanceof ICalendarObject) { + // The calendar-data property is not supposed to be a 'real' + // property, but in large chunks of the spec it does act as such. + // Therefore we simply expose it as a property. + $propFind->handle('{'.self::NS_CALDAV.'}calendar-data', function () use ($node) { + $val = $node->get(); + if (is_resource($val)) { + $val = stream_get_contents($val); + } + + // Taking out \r to not screw up the xml output + return str_replace("\r", '', $val); + }); + } + } + + /** + * This function handles the calendar-multiget REPORT. + * + * This report is used by the client to fetch the content of a series + * of urls. Effectively avoiding a lot of redundant requests. + * + * @param CalendarMultiGetReport $report + */ + public function calendarMultiGetReport($report) + { + $needsJson = 'application/calendar+json' === $report->contentType; + + $timeZones = []; + $propertyList = []; + + $paths = array_map( + [$this->server, 'calculateUri'], + $report->hrefs + ); + + foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) { + if (($needsJson || $report->expand) && isset($objProps[200]['{'.self::NS_CALDAV.'}calendar-data'])) { + $vObject = VObject\Reader::read($objProps[200]['{'.self::NS_CALDAV.'}calendar-data']); + + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + list($calendarPath) = Uri\split($uri); + if (!isset($timeZones[$calendarPath])) { + // Checking the calendar-timezone property. + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($calendarPath, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + } else { + // Defaulting to UTC. + $timeZone = new DateTimeZone('UTC'); + } + $timeZones[$calendarPath] = $timeZone; + } + + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]); + } + if ($needsJson) { + $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + + $propertyList[] = $objProps; + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return'])); + } + + /** + * This function handles the calendar-query REPORT. + * + * This report is used by clients to request calendar objects based on + * complex conditions. + * + * @param Xml\Request\CalendarQueryReport $report + */ + public function calendarQueryReport($report) + { + $path = $this->server->getRequestUri(); + + $needsJson = 'application/calendar+json' === $report->contentType; + + $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); + $depth = $this->server->getHTTPDepth(0); + + // The default result is an empty array + $result = []; + + $calendarTimeZone = null; + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + + // Destroy circular references so PHP will garbage collect the + // object. + $vtimezoneObj->destroy(); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + } + + // The calendarobject was requested directly. In this case we handle + // this locally. + if (0 == $depth && $node instanceof ICalendarObject) { + $requestedCalendarData = true; + $requestedProperties = $report->properties; + + if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { + // We always retrieve calendar-data, as we need it for filtering. + $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; + + // If calendar-data wasn't explicitly requested, we need to remove + // it after processing. + $requestedCalendarData = false; + } + + $properties = $this->server->getPropertiesForPath( + $path, + $requestedProperties, + 0 + ); + + // This array should have only 1 element, the first calendar + // object. + $properties = current($properties); + + // If there wasn't any calendar-data returned somehow, we ignore + // this. + if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) { + $validator = new CalendarQueryValidator(); + + $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + if ($validator->validate($vObject, $report->filters)) { + // If the client didn't require the calendar-data property, + // we won't give it back. + if (!$requestedCalendarData) { + unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + } else { + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + if ($needsJson) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } elseif ($report->expand) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + } + + $result = [$properties]; + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + } + + if ($node instanceof ICalendarObjectContainer && 0 === $depth) { + if (0 === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'MSFT-')) { + // Microsoft clients incorrectly supplied depth as 0, when it actually + // should have set depth to 1. We're implementing a workaround here + // to deal with this. + // + // This targets at least the following clients: + // Windows 10 + // Windows Phone 8, 10 + $depth = 1; + } else { + throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1'); + } + } + + // If we're dealing with a calendar, the calendar itself is responsible + // for the calendar-query. + if ($node instanceof ICalendarObjectContainer && 1 == $depth) { + $nodePaths = $node->calendarQuery($report->filters); + + foreach ($nodePaths as $path) { + list($properties) = + $this->server->getPropertiesForPath($this->server->getRequestUri().'/'.$path, $report->properties); + + if (($needsJson || $report->expand)) { + $vObject = VObject\Reader::read($properties[200]['{'.self::NS_CALDAV.'}calendar-data']); + + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + + if ($needsJson) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + $result[] = $properties; + } + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return'])); + } + + /** + * This method is responsible for parsing the request and generating the + * response for the CALDAV:free-busy-query REPORT. + */ + protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) + { + $uri = $this->server->getRequestUri(); + + $acl = $this->server->getPlugin('acl'); + if ($acl) { + $acl->checkPrivileges($uri, '{'.self::NS_CALDAV.'}read-free-busy'); + } + + $calendar = $this->server->tree->getNodeForPath($uri); + if (!$calendar instanceof ICalendar) { + throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars'); + } + + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + + // Figuring out the default timezone for the calendar, for floating + // times. + $calendarProps = $this->server->getProperties($uri, [$tzProp]); + + if (isset($calendarProps[$tzProp])) { + $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references so PHP will garbage collect the object. + $vtimezoneObj->destroy(); + } else { + $calendarTimeZone = new DateTimeZone('UTC'); + } + + // Doing a calendar-query first, to make sure we get the most + // performance. + $urls = $calendar->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $report->start, + 'end' => $report->end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + $objects = array_map(function ($url) use ($calendar) { + $obj = $calendar->getChild($url)->get(); + + return $obj; + }, $urls); + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($report->start, $report->end); + $generator->setTimeZone($calendarTimeZone); + $result = $generator->getResult(); + $result = $result->serialize(); + + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setHeader('Content-Type', 'text/calendar'); + $this->server->httpResponse->setHeader('Content-Length', strlen($result)); + $this->server->httpResponse->setBody($result); + } + + /** + * This method is triggered before a file gets updated with new content. + * + * This plugin uses this method to ensure that CalDAV objects receive + * valid calendar data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) + { + if (!$node instanceof ICalendarObject) { + return; + } + + // We're 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 bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) + { + if (!$parentNode instanceof ICalendar) { + return; + } + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + true + ); + } + + /** + * Checks if the submitted iCalendar data is in fact, valid. + * + * An exception is thrown if it's not. + * + * @param resource|string $data + * @param string $path + * @param bool $modified should be set to true, if this event handler + * changed &$data + * @param RequestInterface $request the http request + * @param ResponseInterface $response the http response + * @param bool $isNew is the item a new one, or an update + */ + protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) + { + // If it's a stream, we convert it to a string first. + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + $before = $data; + + try { + // If the data starts with a [, we can reasonably assume we're dealing + // with a jCal object. + if ('[' === substr($data, 0, 1)) { + $vobj = VObject\Reader::readJson($data); + + // Converting $data back to iCalendar, as that's what we + // technically support everywhere. + $data = $vobj->serialize(); + $modified = true; + } else { + $vobj = VObject\Reader::read($data); + } + } catch (VObject\ParseException $e) { + throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: '.$e->getMessage()); + } + + if ('VCALENDAR' !== $vobj->name) { + throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.'); + } + + $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + + // Get the Supported Components for the target calendar + list($parentPath) = Uri\split($path); + $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); + + if (isset($calendarProperties[$sCCS])) { + $supportedComponents = $calendarProperties[$sCCS]->getValue(); + } else { + $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; + } + + $foundType = null; + + foreach ($vobj->getComponents() as $component) { + switch ($component->name) { + case 'VTIMEZONE': + continue 2; + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + $foundType = $component->name; + break; + } + } + + if (!$foundType || !in_array($foundType, $supportedComponents)) { + throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type '.implode(', ', $supportedComponents)); + } + + $options = VObject\Node::PROFILE_CALDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ('strict' !== $prefer['handling']) { + $options |= VObject\Node::REPAIR; + } + + $messages = $vobj->validate($options); + + $highestLevel = 0; + $warningMessage = null; + + // $messages contains a list of problems with the vcard, along with + // their severity. + foreach ($messages as $message) { + if ($message['level'] > $highestLevel) { + // Recording the highest reported error level. + $highestLevel = $message['level']; + $warningMessage = $message['message']; + } + switch ($message['level']) { + case 1: + // Level 1 means that there was a problem, but it was repaired. + $modified = true; + break; + case 2: + // Level 2 means a warning, but not critical + break; + case 3: + // Level 3 means a critical error + throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: '.$message['message']); + } + } + if ($warningMessage) { + $response->setHeader( + 'X-Sabre-Ew-Gross', + 'iCalendar validation warning: '.$warningMessage + ); + } + + // We use an extra variable to allow event handles to tell us whether + // the object was modified or not. + // + // This helps us determine if we need to re-serialize the object. + $subModified = false; + + $this->server->emit( + 'calendarObjectChange', + [ + $request, + $response, + $vobj, + $parentPath, + &$subModified, + $isNew, + ] + ); + + if ($modified || $subModified) { + // An event handler told us that it modified the object. + $data = $vobj->serialize(); + + // Using md5 to figure out if there was an *actual* change. + if (!$modified && 0 !== strcmp($data, $before)) { + $modified = true; + } + } + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + } + + /** + * This method is triggered whenever a subsystem reqeuests the privileges + * that are supported on a particular node. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + if ($node instanceof ICalendar) { + $supportedPrivilegeSet['{DAV:}read']['aggregates']['{'.self::NS_CALDAV.'}read-free-busy'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new calendars. + * + * @param string $output + * + * @return bool + */ + public function htmlActionsPanel(DAV\INode $node, &$output) + { + if (!$node instanceof CalendarHome) { + return; + } + + $output .= '
+

Create new calendar

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

Create new address book

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

Nodes

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

Properties

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

Actions

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

Plugins

'; + $html .= ''; + foreach ($this->server->getPlugins() as $plugin) { + $info = $plugin->getPluginInfo(); + $html .= ''; + $html .= ''; + $html .= ''; + } + $html .= '
'.$info['name'].''.$info['description'].''; + if (isset($info['link']) && $info['link']) { + $html .= ''; + } + $html .= '
'; + $html .= '
'; + + /* Start of generating actions */ + + $html .= $this->generateFooter(); + + return $html; + } + + /** + * Generates the first block of HTML, including the tag and page + * header. + * + * Returns footer. + * + * @param string $title + * @param string $path + * + * @return string + */ + public function generateHeader($title, $path = null) + { + $version = ''; + if (DAV\Server::$exposeVersion) { + $version = DAV\Version::VERSION; + } + + $vars = [ + 'title' => $this->escapeHTML($title), + 'favicon' => $this->escapeHTML($this->getAssetUrl('favicon.ico')), + 'style' => $this->escapeHTML($this->getAssetUrl('sabredav.css')), + 'iconstyle' => $this->escapeHTML($this->getAssetUrl('openiconic/open-iconic.css')), + 'logo' => $this->escapeHTML($this->getAssetUrl('sabredav.png')), + 'baseUrl' => $this->server->getBaseUri(), + ]; + + $html = << + + + $vars[title] - sabre/dav $version + + + + + + +
+ +
+ + '; + + return $html; + } + + /** + * Generates the page footer. + * + * Returns html. + * + * @return string + */ + public function generateFooter() + { + $version = ''; + if (DAV\Server::$exposeVersion) { + $version = DAV\Version::VERSION; + } + $year = date('Y'); + + return <<Generated by SabreDAV $version (c)2007-$year http://sabre.io/ + + +HTML; + } + + /** + * This method is used to generate the 'actions panel' output for + * collections. + * + * This specifically generates the interfaces for creating new files, and + * creating new directories. + * + * @param mixed $output + * @param string $path + */ + public function htmlActionsPanel(DAV\INode $node, &$output, $path) + { + if (!$node instanceof DAV\ICollection) { + return; + } + + // We also know fairly certain that if an object is a non-extended + // SimpleCollection, we won't need to show the panel either. + if ('Sabre\\DAV\\SimpleCollection' === get_class($node)) { + return; + } + + $output .= << +

Create new folder

+ +
+ + +
+

Upload file

+ +
+
+ +
+HTML; + } + + /** + * This method takes a path/name of an asset and turns it into url + * 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 (false !== strpos($path, '/../') || '/..' === strrchr($path, '/')) { + throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); + } + $realPath = realpath($path); + if ($realPath && 0 === strpos($realPath, realpath($assetDir)) && file_exists($path)) { + return $path; + } + throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); + } + + /** + * This method reads an asset from disk and generates a full http response. + * + * @param string $assetName + */ + protected function serveAsset($assetName) + { + $assetPath = $this->getLocalAssetPath($assetName); + + // Rudimentary mime type detection + $mime = 'application/octet-stream'; + $map = [ + 'ico' => 'image/vnd.microsoft.icon', + 'png' => 'image/png', + 'css' => 'text/css', + ]; + + $ext = substr($assetName, strrpos($assetName, '.') + 1); + if (isset($map[$ext])) { + $mime = $map[$ext]; + } + + $this->server->httpResponse->setHeader('Content-Type', $mime); + $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath)); + $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600'); + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setBody(fopen($assetPath, 'r')); + } + + /** + * Sort helper function: compares two directory entries based on type and + * display name. Collections sort above other types. + * + * @param array $a + * @param array $b + * + * @return int + */ + protected function compareNodes($a, $b) + { + $typeA = (isset($a['{DAV:}resourcetype'])) + ? (in_array('{DAV:}collection', $a['{DAV:}resourcetype']->getValue())) + : false; + + $typeB = (isset($b['{DAV:}resourcetype'])) + ? (in_array('{DAV:}collection', $b['{DAV:}resourcetype']->getValue())) + : false; + + // If same type, sort alphabetically by filename: + if ($typeA === $typeB) { + return strnatcasecmp($a['displayPath'], $b['displayPath']); + } + + return ($typeA < $typeB) ? 1 : -1; + } + + /** + * Maps a resource type to a human-readable string and icon. + * + * @param DAV\INode $node + * + * @return array + */ + private function mapResourceType(array $resourceTypes, $node) + { + if (!$resourceTypes) { + if ($node instanceof DAV\IFile) { + return [ + 'string' => 'File', + 'icon' => 'file', + ]; + } else { + return [ + 'string' => 'Unknown', + 'icon' => 'cog', + ]; + } + } + + $types = [ + '{http://calendarserver.org/ns/}calendar-proxy-write' => [ + 'string' => 'Proxy-Write', + 'icon' => 'people', + ], + '{http://calendarserver.org/ns/}calendar-proxy-read' => [ + 'string' => 'Proxy-Read', + 'icon' => 'people', + ], + '{urn:ietf:params:xml:ns:caldav}schedule-outbox' => [ + 'string' => 'Outbox', + 'icon' => 'inbox', + ], + '{urn:ietf:params:xml:ns:caldav}schedule-inbox' => [ + 'string' => 'Inbox', + 'icon' => 'inbox', + ], + '{urn:ietf:params:xml:ns:caldav}calendar' => [ + 'string' => 'Calendar', + 'icon' => 'calendar', + ], + '{http://calendarserver.org/ns/}shared-owner' => [ + 'string' => 'Shared', + 'icon' => 'calendar', + ], + '{http://calendarserver.org/ns/}subscribed' => [ + 'string' => 'Subscription', + 'icon' => 'calendar', + ], + '{urn:ietf:params:xml:ns:carddav}directory' => [ + 'string' => 'Directory', + 'icon' => 'globe', + ], + '{urn:ietf:params:xml:ns:carddav}addressbook' => [ + 'string' => 'Address book', + 'icon' => 'book', + ], + '{DAV:}principal' => [ + 'string' => 'Principal', + 'icon' => 'person', + ], + '{DAV:}collection' => [ + 'string' => 'Collection', + 'icon' => 'folder', + ], + ]; + + $info = [ + 'string' => [], + 'icon' => 'cog', + ]; + foreach ($resourceTypes as $k => $resourceType) { + if (isset($types[$resourceType])) { + $info['string'][] = $types[$resourceType]['string']; + } else { + $info['string'][] = $resourceType; + } + } + foreach ($types as $key => $resourceInfo) { + if (in_array($key, $resourceTypes)) { + $info['icon'] = $resourceInfo['icon']; + break; + } + } + $info['string'] = implode(', ', $info['string']); + + return $info; + } + + /** + * Draws a table row for a property. + * + * @param string $name + * @param mixed $value + * + * @return string + */ + private function drawPropertyRow($name, $value) + { + $html = new HtmlOutputHelper( + $this->server->getBaseUri(), + $this->server->xml->namespaceMap + ); + + return ''.$html->xmlName($name).''.$this->drawPropertyValue($html, $value).''; + } + + /** + * Draws a table row for a property. + * + * @param HtmlOutputHelper $html + * @param mixed $value + * + * @return string + */ + private function drawPropertyValue($html, $value) + { + if (is_scalar($value)) { + return $html->h($value); + } elseif ($value instanceof HtmlOutput) { + return $value->toHtml($html); + } elseif ($value instanceof \Sabre\Xml\XmlSerializable) { + // There's no default html output for this property, we're going + // to output the actual xml serialization instead. + $xml = $this->server->xml->write('{DAV:}root', $value, $this->server->getBaseUri()); + // removing first and last line, as they contain our root + // element. + $xml = explode("\n", $xml); + $xml = array_slice($xml, 2, -2); + + return '
'.$html->h(implode("\n", $xml)).'
'; + } else { + return 'unknown'; + } + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins; + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'browser'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Generates HTML indexes and debug information for your sabre/dav server', + 'link' => 'http://sabre.io/dav/browser-plugin/', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php new file mode 100644 index 0000000..34702bd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php @@ -0,0 +1,128 @@ +handle('{DAV:}displayname', function() { + * return 'hello'; + * }); + * + * Note that handle will only work the first time. If null is returned, the + * value is ignored. + * + * It's also possible to not pass a callback, but immediately pass a value + * + * @param string $propertyName + * @param mixed $valueOrCallBack + */ + public function handle($propertyName, $valueOrCallBack) + { + if (is_callable($valueOrCallBack)) { + $value = $valueOrCallBack(); + } else { + $value = $valueOrCallBack; + } + if (!is_null($value)) { + $this->result[$propertyName] = [200, $value]; + } + } + + /** + * Sets the value of the property. + * + * If status is not supplied, the status will default to 200 for non-null + * properties, and 404 for null properties. + * + * @param string $propertyName + * @param mixed $value + * @param int $status + */ + public function set($propertyName, $value, $status = null) + { + if (is_null($status)) { + $status = is_null($value) ? 404 : 200; + } + $this->result[$propertyName] = [$status, $value]; + } + + /** + * Returns the current value for a property. + * + * @param string $propertyName + * + * @return mixed + */ + public function get($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null; + } + + /** + * Returns the current status code for a property name. + * + * If the property does not appear in the list of requested properties, + * null will be returned. + * + * @param string $propertyName + * + * @return int|null + */ + public function getStatus($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : 404; + } + + /** + * Returns all propertynames that have a 404 status, and thus don't have a + * value yet. + * + * @return array + */ + public function get404Properties() + { + $result = []; + foreach ($this->result as $propertyName => $stuff) { + if (404 === $stuff[0]) { + $result[] = $propertyName; + } + } + // If there's nothing in this list, we're adding one fictional item. + if (!$result) { + $result[] = '{http://sabredav.org/ns}idk'; + } + + return $result; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2b2c10a22cc7a57c4dc5d7156f184448f2bee92b GIT binary patch literal 4286 zcmc&&O-NKx6uz%14NNBzA}E}JHil3^6a|4&P_&6!RGSD}1rgCAC@`9dTC_@vqNoT8 z0%;dQR0K_{iUM;bf#3*%o1h^C2N7@IH_nmc?Va~t5U6~f`_BE&`Of`&_n~tUev3uN zziw!)bL*XR-2hy!51_yCgT8fb3s`VC=e=KcI5&)PGGQlpSAh?}1mK&Pg8c>z0Y`y$ zAT_6qJ%yV?|0!S$5WO@z3+`QD17OyXL4PyiM}RavtA7Tu7p)pn^p7Ks@m6m7)A}X$ z4Y+@;NrHYq_;V@RoZ|;69MPx!46Ftg*Tc~711C+J`JMuUfYwNBzXPB9sZm3WK9272 z&x|>@f_EO{b3cubqjOyc~J3I$d_lHIpN}q z!{kjX{c{12XF=~Z$w$kazXHB!b53>u!rx}_$e&dD`xNgv+MR&p2yN1xb0>&9t@28Z zV&5u#j_D=P9mI#){2s8@eGGj(?>gooo<%RT14>`VSZ&_l6GlGnan=^bemD56rRN{? zSAqZD$i;oS9SF6#f5I`#^C&hW@13s_lc3LUl(PWmHcop2{vr^kO`kP(*4!m=3Hn3e#Oc!a2;iDn+FbXzcOHEQ zbXZ)u93cj1WA=KS+M>jZ=oYyXq}1?ZdsjsX0A zkJXCvi~cfO@2ffd7r^;>=SsL-3U%l5HRoEZ#0r%`7%&% ziLTXJqU*JeXt3H5`AS#h(dpfl+`Ox|)*~QS%h&VO!d#)!>r3U5_YsDi2fY6Sd&vw% literal 0 HcmV?d00001 diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE new file mode 100644 index 0000000..2199f4a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css new file mode 100644 index 0000000..e748674 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css @@ -0,0 +1,510 @@ +@font-face { + font-family: 'Icons'; + src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot'); + src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot?#iconic-sm') format('embedded-opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.woff') format('woff'), url('?sabreAction=asset&assetName=openiconic/open-iconic.ttf') format('truetype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.otf') format('opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.svg#iconic-sm') format('svg'); + font-weight: normal; + font-style: normal; +} + +.oi[data-glyph].oi-text-replace { + font-size: 0; + line-height: 0; +} + +.oi[data-glyph].oi-text-replace:before { + width: 1em; + text-align: center; +} + +.oi[data-glyph]:before { + font-family: 'Icons'; + display: inline-block; + speak: none; + line-height: 1; + vertical-align: baseline; + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.oi[data-glyph]:empty:before { + width: 1em; + text-align: center; + box-sizing: content-box; +} + +.oi[data-glyph].oi-align-left:before { + text-align: left; +} + +.oi[data-glyph].oi-align-right:before { + text-align: right; +} + +.oi[data-glyph].oi-align-center:before { + text-align: center; +} + +.oi[data-glyph].oi-flip-horizontal:before { + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.oi[data-glyph].oi-flip-vertical:before { + -webkit-transform: scale(1, -1); + -ms-transform: scale(-1, 1); + transform: scale(1, -1); +} +.oi[data-glyph].oi-flip-horizontal-vertical:before { + -webkit-transform: scale(-1, -1); + -ms-transform: scale(-1, 1); + transform: scale(-1, -1); +} + + +.oi[data-glyph=account-login]:before { content:'\e000'; } + +.oi[data-glyph=account-logout]:before { content:'\e001'; } + +.oi[data-glyph=action-redo]:before { content:'\e002'; } + +.oi[data-glyph=action-undo]:before { content:'\e003'; } + +.oi[data-glyph=align-center]:before { content:'\e004'; } + +.oi[data-glyph=align-left]:before { content:'\e005'; } + +.oi[data-glyph=align-right]:before { content:'\e006'; } + +.oi[data-glyph=aperture]:before { content:'\e007'; } + +.oi[data-glyph=arrow-bottom]:before { content:'\e008'; } + +.oi[data-glyph=arrow-circle-bottom]:before { content:'\e009'; } + +.oi[data-glyph=arrow-circle-left]:before { content:'\e00a'; } + +.oi[data-glyph=arrow-circle-right]:before { content:'\e00b'; } + +.oi[data-glyph=arrow-circle-top]:before { content:'\e00c'; } + +.oi[data-glyph=arrow-left]:before { content:'\e00d'; } + +.oi[data-glyph=arrow-right]:before { content:'\e00e'; } + +.oi[data-glyph=arrow-thick-bottom]:before { content:'\e00f'; } + +.oi[data-glyph=arrow-thick-left]:before { content:'\e010'; } + +.oi[data-glyph=arrow-thick-right]:before { content:'\e011'; } + +.oi[data-glyph=arrow-thick-top]:before { content:'\e012'; } + +.oi[data-glyph=arrow-top]:before { content:'\e013'; } + +.oi[data-glyph=audio-spectrum]:before { content:'\e014'; } + +.oi[data-glyph=audio]:before { content:'\e015'; } + +.oi[data-glyph=badge]:before { content:'\e016'; } + +.oi[data-glyph=ban]:before { content:'\e017'; } + +.oi[data-glyph=bar-chart]:before { content:'\e018'; } + +.oi[data-glyph=basket]:before { content:'\e019'; } + +.oi[data-glyph=battery-empty]:before { content:'\e01a'; } + +.oi[data-glyph=battery-full]:before { content:'\e01b'; } + +.oi[data-glyph=beaker]:before { content:'\e01c'; } + +.oi[data-glyph=bell]:before { content:'\e01d'; } + +.oi[data-glyph=bluetooth]:before { content:'\e01e'; } + +.oi[data-glyph=bold]:before { content:'\e01f'; } + +.oi[data-glyph=bolt]:before { content:'\e020'; } + +.oi[data-glyph=book]:before { content:'\e021'; } + +.oi[data-glyph=bookmark]:before { content:'\e022'; } + +.oi[data-glyph=box]:before { content:'\e023'; } + +.oi[data-glyph=briefcase]:before { content:'\e024'; } + +.oi[data-glyph=british-pound]:before { content:'\e025'; } + +.oi[data-glyph=browser]:before { content:'\e026'; } + +.oi[data-glyph=brush]:before { content:'\e027'; } + +.oi[data-glyph=bug]:before { content:'\e028'; } + +.oi[data-glyph=bullhorn]:before { content:'\e029'; } + +.oi[data-glyph=calculator]:before { content:'\e02a'; } + +.oi[data-glyph=calendar]:before { content:'\e02b'; } + +.oi[data-glyph=camera-slr]:before { content:'\e02c'; } + +.oi[data-glyph=caret-bottom]:before { content:'\e02d'; } + +.oi[data-glyph=caret-left]:before { content:'\e02e'; } + +.oi[data-glyph=caret-right]:before { content:'\e02f'; } + +.oi[data-glyph=caret-top]:before { content:'\e030'; } + +.oi[data-glyph=cart]:before { content:'\e031'; } + +.oi[data-glyph=chat]:before { content:'\e032'; } + +.oi[data-glyph=check]:before { content:'\e033'; } + +.oi[data-glyph=chevron-bottom]:before { content:'\e034'; } + +.oi[data-glyph=chevron-left]:before { content:'\e035'; } + +.oi[data-glyph=chevron-right]:before { content:'\e036'; } + +.oi[data-glyph=chevron-top]:before { content:'\e037'; } + +.oi[data-glyph=circle-check]:before { content:'\e038'; } + +.oi[data-glyph=circle-x]:before { content:'\e039'; } + +.oi[data-glyph=clipboard]:before { content:'\e03a'; } + +.oi[data-glyph=clock]:before { content:'\e03b'; } + +.oi[data-glyph=cloud-download]:before { content:'\e03c'; } + +.oi[data-glyph=cloud-upload]:before { content:'\e03d'; } + +.oi[data-glyph=cloud]:before { content:'\e03e'; } + +.oi[data-glyph=cloudy]:before { content:'\e03f'; } + +.oi[data-glyph=code]:before { content:'\e040'; } + +.oi[data-glyph=cog]:before { content:'\e041'; } + +.oi[data-glyph=collapse-down]:before { content:'\e042'; } + +.oi[data-glyph=collapse-left]:before { content:'\e043'; } + +.oi[data-glyph=collapse-right]:before { content:'\e044'; } + +.oi[data-glyph=collapse-up]:before { content:'\e045'; } + +.oi[data-glyph=command]:before { content:'\e046'; } + +.oi[data-glyph=comment-square]:before { content:'\e047'; } + +.oi[data-glyph=compass]:before { content:'\e048'; } + +.oi[data-glyph=contrast]:before { content:'\e049'; } + +.oi[data-glyph=copywriting]:before { content:'\e04a'; } + +.oi[data-glyph=credit-card]:before { content:'\e04b'; } + +.oi[data-glyph=crop]:before { content:'\e04c'; } + +.oi[data-glyph=dashboard]:before { content:'\e04d'; } + +.oi[data-glyph=data-transfer-download]:before { content:'\e04e'; } + +.oi[data-glyph=data-transfer-upload]:before { content:'\e04f'; } + +.oi[data-glyph=delete]:before { content:'\e050'; } + +.oi[data-glyph=dial]:before { content:'\e051'; } + +.oi[data-glyph=document]:before { content:'\e052'; } + +.oi[data-glyph=dollar]:before { content:'\e053'; } + +.oi[data-glyph=double-quote-sans-left]:before { content:'\e054'; } + +.oi[data-glyph=double-quote-sans-right]:before { content:'\e055'; } + +.oi[data-glyph=double-quote-serif-left]:before { content:'\e056'; } + +.oi[data-glyph=double-quote-serif-right]:before { content:'\e057'; } + +.oi[data-glyph=droplet]:before { content:'\e058'; } + +.oi[data-glyph=eject]:before { content:'\e059'; } + +.oi[data-glyph=elevator]:before { content:'\e05a'; } + +.oi[data-glyph=ellipses]:before { content:'\e05b'; } + +.oi[data-glyph=envelope-closed]:before { content:'\e05c'; } + +.oi[data-glyph=envelope-open]:before { content:'\e05d'; } + +.oi[data-glyph=euro]:before { content:'\e05e'; } + +.oi[data-glyph=excerpt]:before { content:'\e05f'; } + +.oi[data-glyph=expand-down]:before { content:'\e060'; } + +.oi[data-glyph=expand-left]:before { content:'\e061'; } + +.oi[data-glyph=expand-right]:before { content:'\e062'; } + +.oi[data-glyph=expand-up]:before { content:'\e063'; } + +.oi[data-glyph=external-link]:before { content:'\e064'; } + +.oi[data-glyph=eye]:before { content:'\e065'; } + +.oi[data-glyph=eyedropper]:before { content:'\e066'; } + +.oi[data-glyph=file]:before { content:'\e067'; } + +.oi[data-glyph=fire]:before { content:'\e068'; } + +.oi[data-glyph=flag]:before { content:'\e069'; } + +.oi[data-glyph=flash]:before { content:'\e06a'; } + +.oi[data-glyph=folder]:before { content:'\e06b'; } + +.oi[data-glyph=fork]:before { content:'\e06c'; } + +.oi[data-glyph=fullscreen-enter]:before { content:'\e06d'; } + +.oi[data-glyph=fullscreen-exit]:before { content:'\e06e'; } + +.oi[data-glyph=globe]:before { content:'\e06f'; } + +.oi[data-glyph=graph]:before { content:'\e070'; } + +.oi[data-glyph=grid-four-up]:before { content:'\e071'; } + +.oi[data-glyph=grid-three-up]:before { content:'\e072'; } + +.oi[data-glyph=grid-two-up]:before { content:'\e073'; } + +.oi[data-glyph=hard-drive]:before { content:'\e074'; } + +.oi[data-glyph=header]:before { content:'\e075'; } + +.oi[data-glyph=headphones]:before { content:'\e076'; } + +.oi[data-glyph=heart]:before { content:'\e077'; } + +.oi[data-glyph=home]:before { content:'\e078'; } + +.oi[data-glyph=image]:before { content:'\e079'; } + +.oi[data-glyph=inbox]:before { content:'\e07a'; } + +.oi[data-glyph=infinity]:before { content:'\e07b'; } + +.oi[data-glyph=info]:before { content:'\e07c'; } + +.oi[data-glyph=italic]:before { content:'\e07d'; } + +.oi[data-glyph=justify-center]:before { content:'\e07e'; } + +.oi[data-glyph=justify-left]:before { content:'\e07f'; } + +.oi[data-glyph=justify-right]:before { content:'\e080'; } + +.oi[data-glyph=key]:before { content:'\e081'; } + +.oi[data-glyph=laptop]:before { content:'\e082'; } + +.oi[data-glyph=layers]:before { content:'\e083'; } + +.oi[data-glyph=lightbulb]:before { content:'\e084'; } + +.oi[data-glyph=link-broken]:before { content:'\e085'; } + +.oi[data-glyph=link-intact]:before { content:'\e086'; } + +.oi[data-glyph=list-rich]:before { content:'\e087'; } + +.oi[data-glyph=list]:before { content:'\e088'; } + +.oi[data-glyph=location]:before { content:'\e089'; } + +.oi[data-glyph=lock-locked]:before { content:'\e08a'; } + +.oi[data-glyph=lock-unlocked]:before { content:'\e08b'; } + +.oi[data-glyph=loop-circular]:before { content:'\e08c'; } + +.oi[data-glyph=loop-square]:before { content:'\e08d'; } + +.oi[data-glyph=loop]:before { content:'\e08e'; } + +.oi[data-glyph=magnifying-glass]:before { content:'\e08f'; } + +.oi[data-glyph=map-marker]:before { content:'\e090'; } + +.oi[data-glyph=map]:before { content:'\e091'; } + +.oi[data-glyph=media-pause]:before { content:'\e092'; } + +.oi[data-glyph=media-play]:before { content:'\e093'; } + +.oi[data-glyph=media-record]:before { content:'\e094'; } + +.oi[data-glyph=media-skip-backward]:before { content:'\e095'; } + +.oi[data-glyph=media-skip-forward]:before { content:'\e096'; } + +.oi[data-glyph=media-step-backward]:before { content:'\e097'; } + +.oi[data-glyph=media-step-forward]:before { content:'\e098'; } + +.oi[data-glyph=media-stop]:before { content:'\e099'; } + +.oi[data-glyph=medical-cross]:before { content:'\e09a'; } + +.oi[data-glyph=menu]:before { content:'\e09b'; } + +.oi[data-glyph=microphone]:before { content:'\e09c'; } + +.oi[data-glyph=minus]:before { content:'\e09d'; } + +.oi[data-glyph=monitor]:before { content:'\e09e'; } + +.oi[data-glyph=moon]:before { content:'\e09f'; } + +.oi[data-glyph=move]:before { content:'\e0a0'; } + +.oi[data-glyph=musical-note]:before { content:'\e0a1'; } + +.oi[data-glyph=paperclip]:before { content:'\e0a2'; } + +.oi[data-glyph=pencil]:before { content:'\e0a3'; } + +.oi[data-glyph=people]:before { content:'\e0a4'; } + +.oi[data-glyph=person]:before { content:'\e0a5'; } + +.oi[data-glyph=phone]:before { content:'\e0a6'; } + +.oi[data-glyph=pie-chart]:before { content:'\e0a7'; } + +.oi[data-glyph=pin]:before { content:'\e0a8'; } + +.oi[data-glyph=play-circle]:before { content:'\e0a9'; } + +.oi[data-glyph=plus]:before { content:'\e0aa'; } + +.oi[data-glyph=power-standby]:before { content:'\e0ab'; } + +.oi[data-glyph=print]:before { content:'\e0ac'; } + +.oi[data-glyph=project]:before { content:'\e0ad'; } + +.oi[data-glyph=pulse]:before { content:'\e0ae'; } + +.oi[data-glyph=puzzle-piece]:before { content:'\e0af'; } + +.oi[data-glyph=question-mark]:before { content:'\e0b0'; } + +.oi[data-glyph=rain]:before { content:'\e0b1'; } + +.oi[data-glyph=random]:before { content:'\e0b2'; } + +.oi[data-glyph=reload]:before { content:'\e0b3'; } + +.oi[data-glyph=resize-both]:before { content:'\e0b4'; } + +.oi[data-glyph=resize-height]:before { content:'\e0b5'; } + +.oi[data-glyph=resize-width]:before { content:'\e0b6'; } + +.oi[data-glyph=rss-alt]:before { content:'\e0b7'; } + +.oi[data-glyph=rss]:before { content:'\e0b8'; } + +.oi[data-glyph=script]:before { content:'\e0b9'; } + +.oi[data-glyph=share-boxed]:before { content:'\e0ba'; } + +.oi[data-glyph=share]:before { content:'\e0bb'; } + +.oi[data-glyph=shield]:before { content:'\e0bc'; } + +.oi[data-glyph=signal]:before { content:'\e0bd'; } + +.oi[data-glyph=signpost]:before { content:'\e0be'; } + +.oi[data-glyph=sort-ascending]:before { content:'\e0bf'; } + +.oi[data-glyph=sort-descending]:before { content:'\e0c0'; } + +.oi[data-glyph=spreadsheet]:before { content:'\e0c1'; } + +.oi[data-glyph=star]:before { content:'\e0c2'; } + +.oi[data-glyph=sun]:before { content:'\e0c3'; } + +.oi[data-glyph=tablet]:before { content:'\e0c4'; } + +.oi[data-glyph=tag]:before { content:'\e0c5'; } + +.oi[data-glyph=tags]:before { content:'\e0c6'; } + +.oi[data-glyph=target]:before { content:'\e0c7'; } + +.oi[data-glyph=task]:before { content:'\e0c8'; } + +.oi[data-glyph=terminal]:before { content:'\e0c9'; } + +.oi[data-glyph=text]:before { content:'\e0ca'; } + +.oi[data-glyph=thumb-down]:before { content:'\e0cb'; } + +.oi[data-glyph=thumb-up]:before { content:'\e0cc'; } + +.oi[data-glyph=timer]:before { content:'\e0cd'; } + +.oi[data-glyph=transfer]:before { content:'\e0ce'; } + +.oi[data-glyph=trash]:before { content:'\e0cf'; } + +.oi[data-glyph=underline]:before { content:'\e0d0'; } + +.oi[data-glyph=vertical-align-bottom]:before { content:'\e0d1'; } + +.oi[data-glyph=vertical-align-center]:before { content:'\e0d2'; } + +.oi[data-glyph=vertical-align-top]:before { content:'\e0d3'; } + +.oi[data-glyph=video]:before { content:'\e0d4'; } + +.oi[data-glyph=volume-high]:before { content:'\e0d5'; } + +.oi[data-glyph=volume-low]:before { content:'\e0d6'; } + +.oi[data-glyph=volume-off]:before { content:'\e0d7'; } + +.oi[data-glyph=warning]:before { content:'\e0d8'; } + +.oi[data-glyph=wifi]:before { content:'\e0d9'; } + +.oi[data-glyph=wrench]:before { content:'\e0da'; } + +.oi[data-glyph=x]:before { content:'\e0db'; } + +.oi[data-glyph=yen]:before { content:'\e0dc'; } + +.oi[data-glyph=zoom-in]:before { content:'\e0dd'; } + +.oi[data-glyph=zoom-out]:before { content:'\e0de'; } diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot new file mode 100644 index 0000000000000000000000000000000000000000..7ca7c170f1a7780c7ed612e469d746adad4fabeb GIT binary patch literal 23144 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 literal 0 HcmV?d00001 diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg new file mode 100644 index 0000000..0792c00 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +Created by FontForge 20120731 at Wed Apr 30 22:56:47 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0f94acd1ebc42d7ae7cf1ada87d4301d73d64e30 GIT binary patch literal 25568 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 cyavdL5`+vYLY% literal 0 HcmV?d00001 diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff new file mode 100644 index 0000000000000000000000000000000000000000..793176af47c13605a36275252d9ce11fda817f33 GIT binary patch literal 12404 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 b0mT0S5)@H#d7`>U00000NkvXXu0mjfEz&h@ literal 0 HcmV?d00001 diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Client.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Client.php new file mode 100644 index 0000000..a9de71c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Client.php @@ -0,0 +1,429 @@ +xml->elementMap. + * It's deprecated as of version 3.0.0, and should no longer be used. + * + * @deprecated + * + * @var array + */ + public $propertyMap = []; + + /** + * Base URI. + * + * This URI will be used to resolve relative urls. + * + * @var string + */ + protected $baseUri; + + /** + * Basic authentication. + */ + const AUTH_BASIC = 1; + + /** + * Digest authentication. + */ + const AUTH_DIGEST = 2; + + /** + * NTLM authentication. + */ + const AUTH_NTLM = 4; + + /** + * Identity encoding, which basically does not nothing. + */ + const ENCODING_IDENTITY = 1; + + /** + * Deflate encoding. + */ + const ENCODING_DEFLATE = 2; + + /** + * Gzip encoding. + */ + const ENCODING_GZIP = 4; + + /** + * Sends all encoding headers. + */ + const ENCODING_ALL = 7; + + /** + * Content-encoding. + * + * @var int + */ + protected $encoding = self::ENCODING_IDENTITY; + + /** + * Constructor. + * + * Settings are provided through the 'settings' argument. The following + * settings are supported: + * + * * baseUri + * * userName (optional) + * * password (optional) + * * proxy (optional) + * * authType (optional) + * * encoding (optional) + * + * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST + * and self::AUTH_NTLM. If you know which authentication method will be + * used, it's recommended to set it, as it will save a great deal of + * requests to 'discover' this information. + * + * Encoding is a bitmap with one of the ENCODING constants. + */ + public function __construct(array $settings) + { + if (!isset($settings['baseUri'])) { + throw new \InvalidArgumentException('A baseUri must be provided'); + } + + parent::__construct(); + + $this->baseUri = $settings['baseUri']; + + if (isset($settings['proxy'])) { + $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']); + } + + if (isset($settings['userName'])) { + $userName = $settings['userName']; + $password = isset($settings['password']) ? $settings['password'] : ''; + + if (isset($settings['authType'])) { + $curlType = 0; + if ($settings['authType'] & self::AUTH_BASIC) { + $curlType |= CURLAUTH_BASIC; + } + if ($settings['authType'] & self::AUTH_DIGEST) { + $curlType |= CURLAUTH_DIGEST; + } + if ($settings['authType'] & self::AUTH_NTLM) { + $curlType |= CURLAUTH_NTLM; + } + } else { + $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST; + } + + $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType); + $this->addCurlSetting(CURLOPT_USERPWD, $userName.':'.$password); + } + + if (isset($settings['encoding'])) { + $encoding = $settings['encoding']; + + $encodings = []; + if ($encoding & self::ENCODING_IDENTITY) { + $encodings[] = 'identity'; + } + if ($encoding & self::ENCODING_DEFLATE) { + $encodings[] = 'deflate'; + } + if ($encoding & self::ENCODING_GZIP) { + $encodings[] = 'gzip'; + } + $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings)); + } + + $this->addCurlSetting(CURLOPT_USERAGENT, 'sabre-dav/'.Version::VERSION.' (http://sabre.io/)'); + + $this->xml = new Xml\Service(); + // BC + $this->propertyMap = &$this->xml->elementMap; + } + + /** + * Does a PROPFIND request. + * + * 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 int $depth + * + * @return array + */ + public 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 ('DAV:' === $namespace) { + $element = $dom->createElement('d:'.$elementName); + } else { + $element = $dom->createElementNS($namespace, 'x:'.$elementName); + } + + $prop->appendChild($element); + } + + $dom->appendChild($root)->appendChild($prop); + $body = $dom->saveXML(); + + $url = $this->getAbsoluteUrl($url); + + $request = new HTTP\Request('PROPFIND', $url, [ + 'Depth' => $depth, + 'Content-Type' => 'application/xml', + ], $body); + + $response = $this->send($request); + + if ((int) $response->getStatus() >= 400) { + throw new HTTP\ClientHttpException($response); + } + + $result = $this->parseMultiStatus($response->getBodyAsString()); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + $result = current($result); + + return isset($result[200]) ? $result[200] : []; + } + + $newResult = []; + foreach ($result as $href => $statusList) { + $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; + } + + return $newResult; + } + + /** + * Updates a list of properties on the server. + * + * The list of properties must have clark-notation properties for the keys, + * and the actual (string) value for the value. If the value is null, an + * attempt is made to delete the property. + * + * @param string $url + * + * @return bool + */ + public function propPatch($url, array $properties) + { + $propPatch = new Xml\Request\PropPatch(); + $propPatch->properties = $properties; + $xml = $this->xml->write( + '{DAV:}propertyupdate', + $propPatch + ); + + $url = $this->getAbsoluteUrl($url); + $request = new HTTP\Request('PROPPATCH', $url, [ + 'Content-Type' => 'application/xml', + ], $xml); + $response = $this->send($request); + + if ($response->getStatus() >= 400) { + throw new HTTP\ClientHttpException($response); + } + + if (207 === $response->getStatus()) { + // If it's a 207, the request could still have failed, but the + // information is hidden in the response body. + $result = $this->parseMultiStatus($response->getBodyAsString()); + + $errorProperties = []; + foreach ($result as $href => $statusList) { + foreach ($statusList as $status => $properties) { + if ($status >= 400) { + foreach ($properties as $propName => $propValue) { + $errorProperties[] = $propName.' ('.$status.')'; + } + } + } + } + if ($errorProperties) { + throw new HTTP\ClientException('PROPPATCH failed. The following properties errored: '.implode(', ', $errorProperties)); + } + } + + return true; + } + + /** + * Performs an HTTP options request. + * + * This method returns all the features from the 'DAV:' header as an array. + * If there was no DAV header, or no contents this method will return an + * empty array. + * + * @return array + */ + public function options() + { + $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl('')); + $response = $this->send($request); + + $dav = $response->getHeader('Dav'); + if (!$dav) { + return []; + } + + $features = explode(',', $dav); + foreach ($features as &$v) { + $v = trim($v); + } + + return $features; + } + + /** + * Performs an actual HTTP request, and returns the result. + * + * If the specified url is relative, it will be expanded based on the base + * url. + * + * The returned array contains 3 keys: + * * body - the response body + * * httpCode - a HTTP code (200, 404, etc) + * * headers - a list of response http headers. The header names have + * been lowercased. + * + * For large uploads, it's highly recommended to specify body as a stream + * resource. You can easily do this by simply passing the result of + * fopen(..., 'r'). + * + * This method will throw an exception if an HTTP error was received. Any + * HTTP status code above 399 is considered an error. + * + * Note that it is no longer recommended to use this method, use the send() + * method instead. + * + * @param string $method + * @param string $url + * @param string|resource|null $body + * + * @throws clientException, in case a curl error occurred + * + * @return array + */ + public function request($method, $url = '', $body = null, array $headers = []) + { + $url = $this->getAbsoluteUrl($url); + + $response = $this->send(new HTTP\Request($method, $url, $headers, $body)); + + return [ + 'body' => $response->getBodyAsString(), + 'statusCode' => (int) $response->getStatus(), + 'headers' => array_change_key_case($response->getHeaders()), + ]; + } + + /** + * Returns the full url based on the given url (which may be relative). All + * urls are expanded based on the base url as given by the server. + * + * @param string $url + * + * @return string + */ + public function getAbsoluteUrl($url) + { + return Uri\resolve( + $this->baseUri, + $url + ); + } + + /** + * Parses a WebDAV multistatus response body. + * + * This method returns an array with the following structure + * + * [ + * 'url/to/resource' => [ + * '200' => [ + * '{DAV:}property1' => 'value1', + * '{DAV:}property2' => 'value2', + * ], + * '404' => [ + * '{DAV:}property1' => null, + * '{DAV:}property2' => null, + * ], + * ], + * 'url/to/resource2' => [ + * .. etc .. + * ] + * ] + * + * @param string $body xml body + * + * @return array + */ + public function parseMultiStatus($body) + { + $multistatus = $this->xml->expect('{DAV:}multistatus', $body); + + $result = []; + + foreach ($multistatus->getResponses() as $response) { + $result[$response->getHref()] = $response->getResponseProperties(); + } + + return $result; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Collection.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Collection.php new file mode 100644 index 0000000..2728bb2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Collection.php @@ -0,0 +1,106 @@ +getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + throw new Exception\NotFound('File not found: '.$name); + } + + /** + * Checks is a child-node exists. + * + * It is generally a good idea to try and override this. Usually it can be optimized. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + try { + $this->getChild($name); + + return true; + } catch (Exception\NotFound $e) { + return false; + } + } + + /** + * Creates a new file in the directory. + * + * Data will either be supplied as a stream resource, or in certain cases + * as a string. Keep in mind that you may have to support either. + * + * After successful creation of the file, you may choose to return the ETag + * of the new file here. + * + * The returned ETag must be surrounded by double-quotes (The quotes should + * be part of the actual string). + * + * If you cannot accurately determine the ETag, you should not return it. + * If you don't store the file exactly as-is (you're transforming it + * somehow) you should also not return an ETag. + * + * This means that if a subsequent GET to this new file does not exactly + * return the same contents of what was submitted here, you are strongly + * recommended to omit the ETag. + * + * @param string $name Name of the file + * @param resource|string $data Initial payload + * + * @return string|null + */ + public function createFile($name, $data = null) + { + throw new Exception\Forbidden('Permission denied to create file (filename '.$name.')'); + } + + /** + * Creates a new subdirectory. + * + * @param string $name + * + * @throws Exception\Forbidden + */ + public function createDirectory($name) + { + throw new Exception\Forbidden('Permission denied to create directory'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/CorePlugin.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/CorePlugin.php new file mode 100644 index 0000000..74350c2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/CorePlugin.php @@ -0,0 +1,902 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet']); + $server->on('method:OPTIONS', [$this, 'httpOptions']); + $server->on('method:HEAD', [$this, 'httpHead']); + $server->on('method:DELETE', [$this, 'httpDelete']); + $server->on('method:PROPFIND', [$this, 'httpPropFind']); + $server->on('method:PROPPATCH', [$this, 'httpPropPatch']); + $server->on('method:PUT', [$this, 'httpPut']); + $server->on('method:MKCOL', [$this, 'httpMkcol']); + $server->on('method:MOVE', [$this, 'httpMove']); + $server->on('method:COPY', [$this, 'httpCopy']); + $server->on('method:REPORT', [$this, 'httpReport']); + + $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); + $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); + $server->on('propFind', [$this, 'propFind']); + $server->on('propFind', [$this, 'propFindNode'], 120); + $server->on('propFind', [$this, 'propFindLate'], 200); + + $server->on('exception', [$this, 'exception']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'core'; + } + + /** + * This is the default implementation for the GET method. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof IFile) { + return; + } + + if ('HEAD' === $request->getHeader('X-Sabre-Original-Method')) { + $body = ''; + } else { + $body = $node->get(); + + // Converting string into stream, if needed. + if (is_string($body)) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $body); + rewind($stream); + $body = $stream; + } + } + + /* + * TODO: getetag, getlastmodified, getsize should also be used using + * this method + */ + $httpHeaders = $this->server->getHTTPHeaders($path); + + /* ContentType needs to get a default, because many webservers will otherwise + * default to text/html, and we don't want this for security reasons. + */ + if (!isset($httpHeaders['Content-Type'])) { + $httpHeaders['Content-Type'] = 'application/octet-stream'; + } + + if (isset($httpHeaders['Content-Length'])) { + $nodeSize = $httpHeaders['Content-Length']; + + // Need to unset Content-Length, because we'll handle that during figuring out the range + unset($httpHeaders['Content-Length']); + } else { + $nodeSize = null; + } + + $response->addHeaders($httpHeaders); + + $range = $this->server->getHTTPRange(); + $ifRange = $request->getHeader('If-Range'); + $ignoreRangeHeader = false; + + // If ifRange is set, and range is specified, we first need to check + // the precondition. + if ($nodeSize && $range && $ifRange) { + // if IfRange is parsable as a date we'll treat it as a DateTime + // otherwise, we must treat it as an etag. + try { + $ifRangeDate = new \DateTime($ifRange); + + // It's a date. We must check if the entity is modified since + // the specified date. + if (!isset($httpHeaders['Last-Modified'])) { + $ignoreRangeHeader = true; + } else { + $modified = new \DateTime($httpHeaders['Last-Modified']); + if ($modified > $ifRangeDate) { + $ignoreRangeHeader = true; + } + } + } catch (\Exception $e) { + // It's an entity. We can do a simple comparison. + if (!isset($httpHeaders['ETag'])) { + $ignoreRangeHeader = true; + } elseif ($httpHeaders['ETag'] !== $ifRange) { + $ignoreRangeHeader = true; + } + } + } + + // We're only going to support HTTP ranges if the backend provided a filesize + if (!$ignoreRangeHeader && $nodeSize && $range) { + // Determining the exact byte offsets + if (!is_null($range[0])) { + $start = $range[0]; + $end = $range[1] ? $range[1] : $nodeSize - 1; + if ($start >= $nodeSize) { + throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$range[0].') exceeded the size of the entity ('.$nodeSize.')'); + } + if ($end < $start) { + throw new Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[1].') is lower than the start offset ('.$range[0].')'); + } + if ($end >= $nodeSize) { + $end = $nodeSize - 1; + } + } else { + $start = $nodeSize - $range[1]; + $end = $nodeSize - 1; + + if ($start < 0) { + $start = 0; + } + } + + // Streams may advertise themselves as seekable, but still not + // actually allow fseek. We'll manually go forward in the stream + // if fseek failed. + if (!stream_get_meta_data($body)['seekable'] || -1 === fseek($body, $start, SEEK_SET)) { + $consumeBlock = 8192; + for ($consumed = 0; $start - $consumed > 0;) { + if (feof($body)) { + throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$start.') exceeded the size of the entity ('.$consumed.')'); + } + $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); + } + } + + $response->setHeader('Content-Length', $end - $start + 1); + $response->setHeader('Content-Range', 'bytes '.$start.'-'.$end.'/'.$nodeSize); + $response->setStatus(206); + $response->setBody($body); + } else { + if ($nodeSize) { + $response->setHeader('Content-Length', $nodeSize); + } + $response->setStatus(200); + $response->setBody($body); + } + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP OPTIONS. + * + * @return bool + */ + public function httpOptions(RequestInterface $request, ResponseInterface $response) + { + $methods = $this->server->getAllowedMethods($request->getPath()); + + $response->setHeader('Allow', strtoupper(implode(', ', $methods))); + $features = ['1', '3', 'extended-mkcol']; + + foreach ($this->server->getPlugins() as $plugin) { + $features = array_merge($features, $plugin->getFeatures()); + } + + $response->setHeader('DAV', implode(', ', $features)); + $response->setHeader('MS-Author-Via', 'DAV'); + $response->setHeader('Accept-Ranges', 'bytes'); + $response->setHeader('Content-Length', '0'); + $response->setStatus(200); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP HEAD. + * + * This method is normally used to take a peak at a url, and only get the + * HTTP response headers, without the body. This is used by clients to + * determine if a remote file was changed, so they can use a local cached + * version, instead of downloading it again + * + * @return bool + */ + public function httpHead(RequestInterface $request, ResponseInterface $response) + { + // This is implemented by changing the HEAD request to a GET request, + // and telling the request handler that is doesn't need to create the body. + $subRequest = clone $request; + $subRequest->setMethod('GET'); + $subRequest->setHeader('X-Sabre-Original-Method', 'HEAD'); + + try { + $this->server->invokeMethod($subRequest, $response, false); + } catch (Exception\NotImplemented $e) { + // Some clients may do HEAD requests on collections, however, GET + // requests and HEAD requests _may_ not be defined on a collection, + // which would trigger a 501. + // This breaks some clients though, so we're transforming these + // 501s into 200s. + $response->setStatus(200); + $response->setBody(''); + $response->setHeader('Content-Type', 'text/plain'); + $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode()); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP Delete. + * + * The HTTP delete method, deletes a given uri + */ + public function httpDelete(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + if (!$this->server->emit('beforeUnbind', [$path])) { + return false; + } + $this->server->tree->delete($path); + $this->server->emit('afterUnbind', [$path]); + + $response->setStatus(204); + $response->setHeader('Content-Length', '0'); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV PROPFIND. + * + * This WebDAV method requests information about an uri resource, or a list of resources + * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value + * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory) + * + * The request body contains an XML data structure that has a list of properties the client understands + * The response body is also an xml document, containing information about every uri resource and the requested properties + * + * It has to return a HTTP 207 Multi-status status code + */ + public function httpPropFind(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $requestBody = $request->getBodyAsString(); + if (strlen($requestBody)) { + try { + $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); + } catch (ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + } else { + $propFindXml = new Xml\Request\PropFind(); + $propFindXml->allProp = true; + $propFindXml->properties = []; + } + + $depth = $this->server->getHTTPDepth(1); + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled + if (!$this->server->enablePropfindDepthInfinity && 0 != $depth) { + $depth = 1; + } + + $newProperties = $this->server->getPropertiesIteratorForPath($path, $propFindXml->properties, $depth); + + // This is a multi-status response + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Vary', 'Brief,Prefer'); + + // Normally this header is only needed for OPTIONS responses, however.. + // iCal seems to also depend on these being set for PROPFIND. Since + // this is not harmful, we'll add it. + $features = ['1', '3', 'extended-mkcol']; + foreach ($this->server->getPlugins() as $plugin) { + $features = array_merge($features, $plugin->getFeatures()); + } + $response->setHeader('DAV', implode(', ', $features)); + + $prefer = $this->server->getHTTPPrefer(); + $minimal = 'minimal' === $prefer['return']; + + $data = $this->server->generateMultiStatus($newProperties, $minimal); + $response->setBody($data); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV PROPPATCH. + * + * This method is called to update properties on a Node. The request is an XML body with all the mutations. + * In this XML body it is specified which properties should be set/updated and/or deleted + * + * @return bool + */ + public function httpPropPatch(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + try { + $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody()); + } catch (ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + $newProperties = $propPatch->properties; + + $result = $this->server->updateProperties($path, $newProperties); + + $prefer = $this->server->getHTTPPrefer(); + $response->setHeader('Vary', 'Brief,Prefer'); + + if ('minimal' === $prefer['return']) { + // If return-minimal is specified, we only have to check if the + // request was successful, and don't need to return the + // multi-status. + $ok = true; + foreach ($result as $prop => $code) { + if ((int) $code > 299) { + $ok = false; + } + } + + if ($ok) { + $response->setStatus(204); + + return false; + } + } + + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + + // Reorganizing the result for generateMultiStatus + $multiStatus = []; + foreach ($result as $propertyName => $code) { + if (isset($multiStatus[$code])) { + $multiStatus[$code][$propertyName] = null; + } else { + $multiStatus[$code] = [$propertyName => null]; + } + } + $multiStatus['href'] = $path; + + $response->setBody( + $this->server->generateMultiStatus([$multiStatus]) + ); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP PUT method. + * + * This HTTP method updates a file, or creates a new one. + * + * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content + * + * @return bool + */ + public function httpPut(RequestInterface $request, ResponseInterface $response) + { + $body = $request->getBodyAsStream(); + $path = $request->getPath(); + + // Intercepting Content-Range + if ($request->getHeader('Content-Range')) { + /* + An origin server that allows PUT on a given target resource MUST send + a 400 (Bad Request) response to a PUT request that contains a + Content-Range header field. + + Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4 + */ + throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.'); + } + + // Intercepting the Finder problem + if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) { + /* + Many webservers will not cooperate well with Finder PUT requests, + because it uses 'Chunked' transfer encoding for the request body. + + The symptom of this problem is that Finder sends files to the + server, but they arrive as 0-length files in PHP. + + If we don't do anything, the user might think they are uploading + files successfully, but they end up empty on the server. Instead, + we throw back an error if we detect this. + + The reason Finder uses Chunked, is because it thinks the files + might change as it's being uploaded, and therefore the + Content-Length can vary. + + Instead it sends the X-Expected-Entity-Length header with the size + of the file at the very start of the request. If this header is set, + but we don't get a request body we will fail the request to + protect the end-user. + */ + + // Only reading first byte + $firstByte = fread($body, 1); + if (1 !== strlen($firstByte)) { + throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.'); + } + + // The body needs to stay intact, so we copy everything to a + // temporary stream. + + $newBody = fopen('php://temp', 'r+'); + fwrite($newBody, $firstByte); + stream_copy_to_stream($body, $newBody); + rewind($newBody); + + $body = $newBody; + } + + if ($this->server->tree->nodeExists($path)) { + $node = $this->server->tree->getNodeForPath($path); + + // If the node is a collection, we'll deny it + if (!($node instanceof IFile)) { + throw new Exception\Conflict('PUT is not allowed on non-files.'); + } + if (!$this->server->updateFile($path, $body, $etag)) { + return false; + } + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(204); + } else { + $etag = null; + // If we got here, the resource didn't exist yet. + if (!$this->server->createFile($path, $body, $etag)) { + // For one reason or another the file was not created. + return false; + } + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(201); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV MKCOL. + * + * The MKCOL method is used to create a new collection (directory) on the server + * + * @return bool + */ + public function httpMkcol(RequestInterface $request, ResponseInterface $response) + { + $requestBody = $request->getBodyAsString(); + $path = $request->getPath(); + + if ($requestBody) { + $contentType = $request->getHeader('Content-Type'); + if (null === $contentType || (0 !== strpos($contentType, 'application/xml') && 0 !== strpos($contentType, 'text/xml'))) { + // We must throw 415 for unsupported mkcol bodies + throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); + } + + try { + $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody); + } catch (\Sabre\Xml\ParseException $e) { + throw new Exception\BadRequest($e->getMessage(), 0, $e); + } + + $properties = $mkcol->getProperties(); + + if (!isset($properties['{DAV:}resourcetype'])) { + throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); + } + $resourceType = $properties['{DAV:}resourcetype']->getValue(); + unset($properties['{DAV:}resourcetype']); + } else { + $properties = []; + $resourceType = ['{DAV:}collection']; + } + + $mkcol = new MkCol($resourceType, $properties); + + $result = $this->server->createCollection($path, $mkcol); + + if (is_array($result)) { + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + + $response->setBody( + $this->server->generateMultiStatus([$result]) + ); + } else { + $response->setHeader('Content-Length', '0'); + $response->setStatus(201); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV HTTP MOVE method. + * + * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo + * + * @return bool + */ + public function httpMove(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $moveInfo = $this->server->getCopyAndMoveInfo($request); + + if ($moveInfo['destinationExists']) { + if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) { + return false; + } + } + if (!$this->server->emit('beforeUnbind', [$path])) { + return false; + } + if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) { + return false; + } + if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) { + return false; + } + + if ($moveInfo['destinationExists']) { + $this->server->tree->delete($moveInfo['destination']); + $this->server->emit('afterUnbind', [$moveInfo['destination']]); + } + + $this->server->tree->move($path, $moveInfo['destination']); + + // Its important afterMove is called before afterUnbind, because it + // allows systems to transfer data from one path to another. + // PropertyStorage uses this. If afterUnbind was first, it would clean + // up all the properties before it has a chance. + $this->server->emit('afterMove', [$path, $moveInfo['destination']]); + $this->server->emit('afterUnbind', [$path]); + $this->server->emit('afterBind', [$moveInfo['destination']]); + + // If a resource was overwritten we should send a 204, otherwise a 201 + $response->setHeader('Content-Length', '0'); + $response->setStatus($moveInfo['destinationExists'] ? 204 : 201); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV HTTP COPY method. + * + * This method copies one uri to a different uri, and works much like the MOVE request + * A lot of the actual request processing is done in getCopyMoveInfo + * + * @return bool + */ + public function httpCopy(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $copyInfo = $this->server->getCopyAndMoveInfo($request); + + if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) { + return false; + } + if ($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. + * + * @return bool + */ + public function httpReport(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $result = $this->server->xml->parse( + $request->getBody(), + $request->getUrl(), + $rootElementName + ); + + if ($this->server->emit('report', [$rootElementName, $result, $path])) { + // If emit returned true, it means the report was not supported + throw new Exception\ReportNotSupported(); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * This method is called during property updates. + * + * Here we check if a user attempted to update a protected property and + * ensure that the process fails if this is the case. + * + * @param string $path + */ + public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) + { + // Comparing the mutation list to the list of protected properties. + $mutations = $propPatch->getMutations(); + + $protected = array_intersect( + $this->server->protectedProperties, + array_keys($mutations) + ); + + if ($protected) { + $propPatch->setResultCode($protected, 403); + } + } + + /** + * This method is called during property updates. + * + * Here we check if a node implements IProperties and let the node handle + * updating of (some) properties. + * + * @param string $path + */ + public function propPatchNodeUpdate($path, PropPatch $propPatch) + { + // This should trigger a 404 if the node doesn't exist. + $node = $this->server->tree->getNodeForPath($path); + + if ($node instanceof IProperties) { + $node->propPatch($propPatch); + } + } + + /** + * This method is called when properties are retrieved. + * + * Here we add all the default properties. + */ + public function propFind(PropFind $propFind, INode $node) + { + $propFind->handle('{DAV:}getlastmodified', function () use ($node) { + $lm = $node->getLastModified(); + if ($lm) { + return new Xml\Property\GetLastModified($lm); + } + }); + + if ($node instanceof IFile) { + $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']); + $propFind->handle('{DAV:}getetag', [$node, 'getETag']); + $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']); + } + + if ($node instanceof IQuota) { + $quotaInfo = null; + $propFind->handle('{DAV:}quota-used-bytes', function () use (&$quotaInfo, $node) { + $quotaInfo = $node->getQuotaInfo(); + + return $quotaInfo[0]; + }); + $propFind->handle('{DAV:}quota-available-bytes', function () use (&$quotaInfo, $node) { + if (!$quotaInfo) { + $quotaInfo = $node->getQuotaInfo(); + } + + return $quotaInfo[1]; + }); + } + + $propFind->handle('{DAV:}supported-report-set', function () use ($propFind) { + $reports = []; + foreach ($this->server->getPlugins() as $plugin) { + $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath())); + } + + return new Xml\Property\SupportedReportSet($reports); + }); + $propFind->handle('{DAV:}resourcetype', function () use ($node) { + return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node)); + }); + $propFind->handle('{DAV:}supported-method-set', function () use ($propFind) { + return new Xml\Property\SupportedMethodSet( + $this->server->getAllowedMethods($propFind->getPath()) + ); + }); + } + + /** + * Fetches properties for a node. + * + * This event is called a bit later, so plugins have a chance first to + * populate the result. + */ + public function propFindNode(PropFind $propFind, INode $node) + { + if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { + $nodeProperties = $node->getProperties($propertyNames); + foreach ($nodeProperties as $propertyName => $propertyValue) { + $propFind->set($propertyName, $propertyValue, 200); + } + } + } + + /** + * This method is called when properties are retrieved. + * + * This specific handler is called very late in the process, because we + * want other systems to first have a chance to handle the properties. + */ + public function propFindLate(PropFind $propFind, INode $node) + { + $propFind->handle('{http://calendarserver.org/ns/}getctag', function () use ($propFind) { + // If we already have a sync-token from the current propFind + // request, we can re-use that. + $val = $propFind->get('{http://sabredav.org/ns}sync-token'); + if ($val) { + return $val; + } + + $val = $propFind->get('{DAV:}sync-token'); + if ($val && is_scalar($val)) { + return $val; + } + if ($val && $val instanceof Xml\Property\Href) { + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); + } + + // If we got here, the earlier two properties may simply not have + // been part of the earlier request. We're going to fetch them. + $result = $this->server->getProperties($propFind->getPath(), [ + '{http://sabredav.org/ns}sync-token', + '{DAV:}sync-token', + ]); + + if (isset($result['{http://sabredav.org/ns}sync-token'])) { + return $result['{http://sabredav.org/ns}sync-token']; + } + if (isset($result['{DAV:}sync-token'])) { + $val = $result['{DAV:}sync-token']; + if (is_scalar($val)) { + return $val; + } elseif ($val instanceof Xml\Property\Href) { + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); + } + } + }); + } + + /** + * Listens for exception events, and automatically logs them. + * + * @param Exception $e + */ + public function exception($e) + { + $logLevel = \Psr\Log\LogLevel::CRITICAL; + if ($e instanceof \Sabre\DAV\Exception) { + // If it's a standard sabre/dav exception, it means we have a http + // status code available. + $code = $e->getHTTPCode(); + + if ($code >= 400 && $code < 500) { + // user error + $logLevel = \Psr\Log\LogLevel::INFO; + } else { + // Server-side error. We mark it's as an error, but it's not + // critical. + $logLevel = \Psr\Log\LogLevel::ERROR; + } + } + + $this->server->getLogger()->log( + $logLevel, + 'Uncaught exception', + [ + 'exception' => $e, + ] + ); + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'The Core plugin provides a lot of the basic functionality required by WebDAV, such as a default implementation for all HTTP and WebDAV methods.', + 'link' => null, + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception.php new file mode 100644 index 0000000..9fc1d16 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception.php @@ -0,0 +1,50 @@ +lock) { + $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:no-conflicting-lock'); + $errorNode->appendChild($error); + $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:', 'd:href', $this->lock->uri)); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php new file mode 100644 index 0000000..2f882c3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php @@ -0,0 +1,29 @@ +ownerDocument->createElementNS('DAV:', 'd:valid-resourcetype'); + $errorNode->appendChild($error); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php new file mode 100644 index 0000000..37b28ca --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php @@ -0,0 +1,34 @@ +ownerDocument->createElementNS('DAV:', 'd:valid-sync-token'); + $errorNode->appendChild($error); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php new file mode 100644 index 0000000..9d26fcb --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php @@ -0,0 +1,30 @@ +ownerDocument->createElementNS('DAV:', 'd:lock-token-matches-request-uri'); + $errorNode->appendChild($error); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Locked.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Locked.php new file mode 100644 index 0000000..28263cf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/Locked.php @@ -0,0 +1,68 @@ +lock = $lock; + } + + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 423; + } + + /** + * This method allows the exception to include additional information into the WebDAV error response. + */ + public function serialize(DAV\Server $server, \DOMElement $errorNode) + { + if ($this->lock) { + $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:lock-token-submitted'); + $errorNode->appendChild($error); + + $href = $errorNode->ownerDocument->createElementNS('DAV:', 'd:href'); + $href->appendChild($errorNode->ownerDocument->createTextNode($this->lock->uri)); + $error->appendChild( + $href + ); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php new file mode 100644 index 0000000..d1ac349 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php @@ -0,0 +1,45 @@ +getAllowedMethods($server->getRequestUri()); + + return [ + 'Allow' => strtoupper(implode(', ', $methods)), + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php new file mode 100644 index 0000000..0a5ba9b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php @@ -0,0 +1,30 @@ +header = $header; + } + + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 412; + } + + /** + * This method allows the exception to include additional information into the WebDAV error response. + */ + public function serialize(DAV\Server $server, \DOMElement $errorNode) + { + if ($this->header) { + $prop = $errorNode->ownerDocument->createElement('s:header'); + $prop->nodeValue = $this->header; + $errorNode->appendChild($prop); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php new file mode 100644 index 0000000..a483838 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php @@ -0,0 +1,28 @@ +ownerDocument->createElementNS('DAV:', 'd:supported-report'); + $errorNode->appendChild($error); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php new file mode 100644 index 0000000..6ccb5b8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php @@ -0,0 +1,30 @@ + + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ServiceUnavailable extends DAV\Exception +{ + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 503; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php new file mode 100644 index 0000000..3f7d2d5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php @@ -0,0 +1,34 @@ +ownerDocument->createElementNS('DAV:', 'd:number-of-matches-within-limits'); + $errorNode->appendChild($error); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php new file mode 100644 index 0000000..bc9da30 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php @@ -0,0 +1,30 @@ +path.'/'.$name; + file_put_contents($newPath, $data); + clearstatcache(true, $newPath); + } + + /** + * Creates a new subdirectory. + * + * @param string $name + */ + public function createDirectory($name) + { + $newPath = $this->path.'/'.$name; + mkdir($newPath); + clearstatcache(true, $newPath); + } + + /** + * Returns a specific child node, referenced by its name. + * + * This method must throw DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws DAV\Exception\NotFound + * + * @return DAV\INode + */ + public function getChild($name) + { + $path = $this->path.'/'.$name; + + if (!file_exists($path)) { + throw new DAV\Exception\NotFound('File with name '.$path.' could not be located'); + } + if (is_dir($path)) { + return new self($path); + } else { + return new File($path); + } + } + + /** + * Returns an array with all the child nodes. + * + * @return DAV\INode[] + */ + public function getChildren() + { + $nodes = []; + $iterator = new \FilesystemIterator( + $this->path, + \FilesystemIterator::CURRENT_AS_SELF + | \FilesystemIterator::SKIP_DOTS + ); + foreach ($iterator as $entry) { + $nodes[] = $this->getChild($entry->getFilename()); + } + + return $nodes; + } + + /** + * Checks if a child exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + $path = $this->path.'/'.$name; + + return file_exists($path); + } + + /** + * Deletes all files in this directory, and then itself. + */ + public function delete() + { + foreach ($this->getChildren() as $child) { + $child->delete(); + } + rmdir($this->path); + } + + /** + * Returns available diskspace information. + * + * @return array + */ + public function getQuotaInfo() + { + $absolute = realpath($this->path); + + return [ + disk_total_space($absolute) - disk_free_space($absolute), + disk_free_space($absolute), + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/File.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/File.php new file mode 100644 index 0000000..b78a801 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/File.php @@ -0,0 +1,87 @@ +path, $data); + clearstatcache(true, $this->path); + } + + /** + * Returns the data. + * + * @return resource + */ + public function get() + { + return fopen($this->path, 'r'); + } + + /** + * Delete the current file. + */ + public function delete() + { + unlink($this->path); + } + + /** + * Returns the size of the node, in bytes. + * + * @return int + */ + public function getSize() + { + return filesize($this->path); + } + + /** + * Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return mixed + */ + public function getETag() + { + return '"'.sha1( + fileinode($this->path). + filesize($this->path). + filemtime($this->path) + ).'"'; + } + + /** + * Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return mixed + */ + public function getContentType() + { + return null; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/Node.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/Node.php new file mode 100644 index 0000000..32aa747 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FS/Node.php @@ -0,0 +1,96 @@ +path = $path; + $this->overrideName = $overrideName; + } + + /** + * Returns the name of the node. + * + * @return string + */ + public function getName() + { + if ($this->overrideName) { + return $this->overrideName; + } + + list(, $name) = Uri\split($this->path); + + return $name; + } + + /** + * Renames the node. + * + * @param string $name The new name + */ + public function setName($name) + { + if ($this->overrideName) { + throw new Forbidden('This node cannot be renamed'); + } + + list($parentPath) = Uri\split($this->path); + list(, $newName) = Uri\split($name); + + $newPath = $parentPath.'/'.$newName; + rename($this->path, $newPath); + + $this->path = $newPath; + } + + /** + * Returns the last modification time, as a unix timestamp. + * + * @return int + */ + public function getLastModified() + { + return filemtime($this->path); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FSExt/Directory.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FSExt/Directory.php new file mode 100644 index 0000000..d6aea00 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FSExt/Directory.php @@ -0,0 +1,212 @@ +path.'/'.$name; + file_put_contents($newPath, $data); + clearstatcache(true, $newPath); + + return '"'.sha1( + fileinode($newPath). + filesize($newPath). + filemtime($newPath) + ).'"'; + } + + /** + * Creates a new subdirectory. + * + * @param string $name + */ + public function createDirectory($name) + { + // We're not allowing dots + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + $newPath = $this->path.'/'.$name; + mkdir($newPath); + clearstatcache(true, $newPath); + } + + /** + * Returns a specific child node, referenced by its name. + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws DAV\Exception\NotFound + * + * @return DAV\INode + */ + public function getChild($name) + { + $path = $this->path.'/'.$name; + + if (!file_exists($path)) { + throw new DAV\Exception\NotFound('File could not be located'); + } + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + if (is_dir($path)) { + return new self($path); + } else { + return new File($path); + } + } + + /** + * Checks if a child exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + $path = $this->path.'/'.$name; + + return file_exists($path); + } + + /** + * Returns an array with all the child nodes. + * + * @return DAV\INode[] + */ + public function getChildren() + { + $nodes = []; + $iterator = new \FilesystemIterator( + $this->path, + \FilesystemIterator::CURRENT_AS_SELF + | \FilesystemIterator::SKIP_DOTS + ); + + foreach ($iterator as $entry) { + $nodes[] = $this->getChild($entry->getFilename()); + } + + return $nodes; + } + + /** + * Deletes all files in this directory, and then itself. + * + * @return bool + */ + public function delete() + { + // Deleting all children + foreach ($this->getChildren() as $child) { + $child->delete(); + } + + // Removing the directory itself + rmdir($this->path); + + return true; + } + + /** + * Returns available diskspace information. + * + * @return array + */ + public function getQuotaInfo() + { + $total = disk_total_space(realpath($this->path)); + $free = disk_free_space(realpath($this->path)); + + return [ + $total - $free, + $free, + ]; + } + + /** + * Moves a node into this collection. + * + * It is up to the implementors to: + * 1. Create the new resource. + * 2. Remove the old resource. + * 3. Transfer any properties or other data. + * + * Generally you should make very sure that your collection can easily move + * the move. + * + * If you don't, just return false, which will trigger sabre/dav to handle + * the move itself. If you return true from this function, the assumption + * is that the move was successful. + * + * @param string $targetName new local file/collection name + * @param string $sourcePath Full path to source node + * @param DAV\INode $sourceNode Source node itself + * + * @return bool + */ + public function moveInto($targetName, $sourcePath, DAV\INode $sourceNode) + { + // We only support FSExt\Directory or FSExt\File objects, so + // anything else we want to quickly reject. + if (!$sourceNode instanceof self && !$sourceNode instanceof File) { + return false; + } + + // PHP allows us to access protected properties from other objects, as + // long as they are defined in a class that has a shared inheritance + // with the current class. + return rename($sourceNode->path, $this->path.'/'.$targetName); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FSExt/File.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FSExt/File.php new file mode 100644 index 0000000..060ef5a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/FSExt/File.php @@ -0,0 +1,150 @@ +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 + */ + public function patch($data, $rangeType, $offset = null) + { + switch ($rangeType) { + case 1: + $f = fopen($this->path, 'a'); + break; + case 2: + $f = fopen($this->path, 'c'); + fseek($f, $offset); + break; + case 3: + $f = fopen($this->path, 'c'); + fseek($f, $offset, SEEK_END); + break; + } + if (is_string($data)) { + fwrite($f, $data); + } else { + stream_copy_to_stream($data, $f); + } + fclose($f); + clearstatcache(true, $this->path); + + return $this->getETag(); + } + + /** + * Returns the data. + * + * @return resource + */ + public function get() + { + return fopen($this->path, 'r'); + } + + /** + * Delete the current file. + * + * @return bool + */ + public function delete() + { + return unlink($this->path); + } + + /** + * Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return string|null + */ + public function getETag() + { + return '"'.sha1( + fileinode($this->path). + filesize($this->path). + filemtime($this->path) + ).'"'; + } + + /** + * Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return string|null + */ + public function getContentType() + { + return null; + } + + /** + * Returns the size of the file, in bytes. + * + * @return int + */ + public function getSize() + { + return filesize($this->path); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/File.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/File.php new file mode 100644 index 0000000..daf83aa --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/File.php @@ -0,0 +1,93 @@ +locksFile = $locksFile; + } + + /** + * Returns a list of Sabre\DAV\Locks\LockInfo objects. + * + * This method should return all the locks for a particular uri, including + * locks that might be set on a parent uri. + * + * If returnChildLocks is set to true, this method should also look for + * any locks in the subtree of the uri for locks. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks) + { + $newLocks = []; + + $locks = $this->getData(); + + foreach ($locks as $lock) { + if ($lock->uri === $uri || + //deep locks on parents + (0 != $lock->depth && 0 === strpos($uri, $lock->uri.'/')) || + + // locks on children + ($returnChildLocks && (0 === strpos($lock->uri, $uri.'/')))) { + $newLocks[] = $lock; + } + } + + // Checking if we can remove any of these locks + foreach ($newLocks as $k => $lock) { + if (time() > $lock->timeout + $lock->created) { + unset($newLocks[$k]); + } + } + + return $newLocks; + } + + /** + * Locks a uri. + * + * @param string $uri + * + * @return bool + */ + public function lock($uri, LockInfo $lockInfo) + { + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 1800; + $lockInfo->created = time(); + $lockInfo->uri = $uri; + + $locks = $this->getData(); + + foreach ($locks as $k => $lock) { + if ( + ($lock->token == $lockInfo->token) || + (time() > $lock->timeout + $lock->created) + ) { + unset($locks[$k]); + } + } + $locks[] = $lockInfo; + $this->putData($locks); + + return true; + } + + /** + * Removes a lock from a uri. + * + * @param string $uri + * + * @return bool + */ + public function unlock($uri, LockInfo $lockInfo) + { + $locks = $this->getData(); + foreach ($locks as $k => $lock) { + if ($lock->token == $lockInfo->token) { + unset($locks[$k]); + $this->putData($locks); + + return true; + } + } + + return false; + } + + /** + * Loads the lockdata from the filesystem. + * + * @return array + */ + protected function getData() + { + if (!file_exists($this->locksFile)) { + return []; + } + + // opening up the file, and creating a shared lock + $handle = fopen($this->locksFile, 'r'); + flock($handle, LOCK_SH); + + // Reading data until the eof + $data = stream_get_contents($handle); + + // We're all good + flock($handle, LOCK_UN); + fclose($handle); + + // Unserializing and checking if the resource file contains data for this file + $data = unserialize($data); + if (!$data) { + return []; + } + + return $data; + } + + /** + * Saves the lockdata. + */ + protected function putData(array $newData) + { + // opening up the file, and creating an exclusive lock + $handle = fopen($this->locksFile, 'a+'); + flock($handle, LOCK_EX); + + // We can only truncate and rewind once the lock is acquired. + ftruncate($handle, 0); + rewind($handle); + + fwrite($handle, serialize($newData)); + flock($handle, LOCK_UN); + fclose($handle); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php new file mode 100644 index 0000000..3f425f9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php @@ -0,0 +1,172 @@ +pdo = $pdo; + } + + /** + * Returns a list of Sabre\DAV\Locks\LockInfo objects. + * + * This method should return all the locks for a particular uri, including + * locks that might be set on a parent uri. + * + * If returnChildLocks is set to true, this method should also look for + * any locks in the subtree of the uri for locks. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks) + { + // NOTE: the following 10 lines or so could be easily replaced by + // pure sql. MySQL's non-standard string concatenation prevents us + // from doing this though. + $query = 'SELECT owner, token, timeout, created, scope, depth, uri FROM '.$this->tableName.' WHERE (created > (? - timeout)) AND ((uri = ?)'; + $params = [time(), $uri]; + + // We need to check locks for every part in the uri. + $uriParts = explode('/', $uri); + + // We already covered the last part of the uri + array_pop($uriParts); + + $currentPath = ''; + + foreach ($uriParts as $part) { + if ($currentPath) { + $currentPath .= '/'; + } + $currentPath .= $part; + + $query .= ' OR (depth!=0 AND uri = ?)'; + $params[] = $currentPath; + } + + if ($returnChildLocks) { + $query .= ' OR (uri LIKE ?)'; + $params[] = $uri.'/%'; + } + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute($params); + $result = $stmt->fetchAll(); + + $lockList = []; + foreach ($result as $row) { + $lockInfo = new LockInfo(); + $lockInfo->owner = $row['owner']; + $lockInfo->token = $row['token']; + $lockInfo->timeout = $row['timeout']; + $lockInfo->created = $row['created']; + $lockInfo->scope = $row['scope']; + $lockInfo->depth = $row['depth']; + $lockInfo->uri = $row['uri']; + $lockList[] = $lockInfo; + } + + return $lockList; + } + + /** + * Locks a uri. + * + * @param string $uri + * + * @return bool + */ + public function lock($uri, LockInfo $lockInfo) + { + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 30 * 60; + $lockInfo->created = time(); + $lockInfo->uri = $uri; + + $locks = $this->getLocks($uri, false); + $exists = false; + foreach ($locks as $lock) { + if ($lock->token == $lockInfo->token) { + $exists = true; + } + } + + if ($exists) { + $stmt = $this->pdo->prepare('UPDATE '.$this->tableName.' SET owner = ?, timeout = ?, scope = ?, depth = ?, uri = ?, created = ? WHERE token = ?'); + $stmt->execute([ + $lockInfo->owner, + $lockInfo->timeout, + $lockInfo->scope, + $lockInfo->depth, + $uri, + $lockInfo->created, + $lockInfo->token, + ]); + } else { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (owner,timeout,scope,depth,uri,created,token) VALUES (?,?,?,?,?,?,?)'); + $stmt->execute([ + $lockInfo->owner, + $lockInfo->timeout, + $lockInfo->scope, + $lockInfo->depth, + $uri, + $lockInfo->created, + $lockInfo->token, + ]); + } + + return true; + } + + /** + * Removes a lock from a uri. + * + * @param string $uri + * + * @return bool + */ + public function unlock($uri, LockInfo $lockInfo) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE uri = ? AND token = ?'); + $stmt->execute([$uri, $lockInfo->token]); + + return 1 === $stmt->rowCount(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php new file mode 100644 index 0000000..df82275 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php @@ -0,0 +1,82 @@ +addPlugin($lockPlugin); + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends DAV\ServerPlugin +{ + /** + * locksBackend. + * + * @var Backend\BackendInterface + */ + protected $locksBackend; + + /** + * server. + * + * @var DAV\Server + */ + protected $server; + + /** + * __construct. + */ + public function __construct(Backend\BackendInterface $locksBackend) + { + $this->locksBackend = $locksBackend; + } + + /** + * Initializes the plugin. + * + * This method is automatically called by the Server class after addPlugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + + $this->server->xml->elementMap['{DAV:}lockinfo'] = 'Sabre\\DAV\\Xml\\Request\\Lock'; + + $server->on('method:LOCK', [$this, 'httpLock']); + $server->on('method:UNLOCK', [$this, 'httpUnlock']); + $server->on('validateTokens', [$this, 'validateTokens']); + $server->on('propFind', [$this, 'propFind']); + $server->on('afterUnbind', [$this, 'afterUnbind']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'locks'; + } + + /** + * This method is called after most properties have been found + * it allows us to add in any Lock-related properties. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $propFind->handle('{DAV:}supportedlock', function () { + return new DAV\Xml\Property\SupportedLock(); + }); + $propFind->handle('{DAV:}lockdiscovery', function () use ($propFind) { + return new DAV\Xml\Property\LockDiscovery( + $this->getLocks($propFind->getPath()) + ); + }); + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) + { + return ['LOCK', 'UNLOCK']; + } + + /** + * Returns a list of features for the HTTP OPTIONS Dav: header. + * + * In this case this is only the number 2. The 2 in the Dav: header + * indicates the server supports locks. + * + * @return array + */ + public function getFeatures() + { + return [2]; + } + + /** + * Returns all lock information on a particular uri. + * + * This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array. + * + * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree + * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object + * for any possible locks and return those as well. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks = false) + { + return $this->locksBackend->getLocks($uri, $returnChildLocks); + } + + /** + * Locks an uri. + * + * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock + * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type + * of lock (shared or exclusive) and the owner of the lock + * + * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock + * + * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3 + * + * @return bool + */ + public function httpLock(RequestInterface $request, ResponseInterface $response) + { + $uri = $request->getPath(); + + $existingLocks = $this->getLocks($uri); + + if ($body = $request->getBodyAsString()) { + // This is a new lock request + + $existingLock = null; + // Checking if there's already non-shared locks on the uri. + foreach ($existingLocks as $existingLock) { + if (LockInfo::EXCLUSIVE === $existingLock->scope) { + throw new DAV\Exception\ConflictingLock($existingLock); + } + } + + $lockInfo = $this->parseLockRequest($body); + $lockInfo->depth = $this->server->getHTTPDepth(); + $lockInfo->uri = $uri; + if ($existingLock && LockInfo::SHARED != $lockInfo->scope) { + throw new DAV\Exception\ConflictingLock($existingLock); + } + } else { + // Gonna check if this was a lock refresh. + $existingLocks = $this->getLocks($uri); + $conditions = $this->server->getIfConditions($request); + $found = null; + + foreach ($existingLocks as $existingLock) { + foreach ($conditions as $condition) { + foreach ($condition['tokens'] as $token) { + if ($token['token'] === 'opaquelocktoken:'.$existingLock->token) { + $found = $existingLock; + break 3; + } + } + } + } + + // If none were found, this request is in error. + if (is_null($found)) { + if ($existingLocks) { + throw new DAV\Exception\Locked(reset($existingLocks)); + } else { + throw new DAV\Exception\BadRequest('An xml body is required for lock requests'); + } + } + + // This must have been a lock refresh + $lockInfo = $found; + + // The resource could have been locked through another uri. + if ($uri != $lockInfo->uri) { + $uri = $lockInfo->uri; + } + } + + if ($timeout = $this->getTimeoutHeader()) { + $lockInfo->timeout = $timeout; + } + + $newFile = false; + + // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first + try { + $this->server->tree->getNodeForPath($uri); + + // We need to call the beforeWriteContent event for RFC3744 + // Edit: looks like this is not used, and causing problems now. + // + // See Issue 222 + // $this->server->emit('beforeWriteContent',array($uri)); + } catch (DAV\Exception\NotFound $e) { + // It didn't, lets create it + $this->server->createFile($uri, fopen('php://memory', 'r')); + $newFile = true; + } + + $this->lockNode($uri, $lockInfo); + + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Lock-Token', 'token.'>'); + $response->setStatus($newFile ? 201 : 200); + $response->setBody($this->generateLockResponse($lockInfo)); + + // Returning false will interrupt the event chain and mark this method + // as 'handled'. + return false; + } + + /** + * Unlocks a uri. + * + * This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header + * The server should return 204 (No content) on success + */ + public function httpUnlock(RequestInterface $request, ResponseInterface $response) + { + $lockToken = $request->getHeader('Lock-Token'); + + // If the locktoken header is not supplied, we need to throw a bad request exception + if (!$lockToken) { + throw new DAV\Exception\BadRequest('No lock token was supplied'); + } + $path = $request->getPath(); + $locks = $this->getLocks($path); + + // Windows sometimes forgets to include < and > in the Lock-Token + // header + if ('<' !== $lockToken[0]) { + $lockToken = '<'.$lockToken.'>'; + } + + foreach ($locks as $lock) { + if ('token.'>' == $lockToken) { + $this->unlockNode($path, $lock); + $response->setHeader('Content-Length', '0'); + $response->setStatus(204); + + // Returning false will break the method chain, and mark the + // method as 'handled'. + return false; + } + } + + // If we got here, it means the locktoken was invalid + throw new DAV\Exception\LockTokenMatchesRequestUri(); + } + + /** + * This method is called after a node is deleted. + * + * We use this event to clean up any locks that still exist on the node. + * + * @param string $path + */ + public function afterUnbind($path) + { + $locks = $this->getLocks($path, $includeChildren = true); + foreach ($locks as $lock) { + $this->unlockNode($path, $lock); + } + } + + /** + * Locks a uri. + * + * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored + * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client + * + * @param string $uri + * + * @return bool + */ + public function lockNode($uri, LockInfo $lockInfo) + { + if (!$this->server->emit('beforeLock', [$uri, $lockInfo])) { + return; + } + + return $this->locksBackend->lock($uri, $lockInfo); + } + + /** + * Unlocks a uri. + * + * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified + * + * @param string $uri + * + * @return bool + */ + public function unlockNode($uri, LockInfo $lockInfo) + { + if (!$this->server->emit('beforeUnlock', [$uri, $lockInfo])) { + return; + } + + return $this->locksBackend->unlock($uri, $lockInfo); + } + + /** + * Returns the contents of the HTTP Timeout header. + * + * The method formats the header into an integer. + * + * @return int + */ + public function getTimeoutHeader() + { + $header = $this->server->httpRequest->getHeader('Timeout'); + + if ($header) { + if (0 === stripos($header, 'second-')) { + $header = (int) (substr($header, 7)); + } elseif (0 === stripos($header, 'infinite')) { + $header = LockInfo::TIMEOUT_INFINITE; + } else { + throw new DAV\Exception\BadRequest('Invalid HTTP timeout header'); + } + } else { + $header = 0; + } + + return $header; + } + + /** + * Generates the response for successful LOCK requests. + * + * @return string + */ + protected function generateLockResponse(LockInfo $lockInfo) + { + return $this->server->xml->write('{DAV:}prop', [ + '{DAV:}lockdiscovery' => new DAV\Xml\Property\LockDiscovery([$lockInfo]), + ]); + } + + /** + * The validateTokens event is triggered before every request. + * + * It's a moment where this plugin can check all the supplied lock tokens + * in the If: header, and check if they are valid. + * + * In addition, it will also ensure that it checks any missing lokens that + * must be present in the request, and reject requests without the proper + * tokens. + * + * @param mixed $conditions + */ + public function validateTokens(RequestInterface $request, &$conditions) + { + // First we need to gather a list of locks that must be satisfied. + $mustLocks = []; + $method = $request->getMethod(); + + // Methods not in that list are operations that doesn't alter any + // resources, and we don't need to check the lock-states for. + switch ($method) { + case 'DELETE': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + true + )); + break; + case 'MKCOL': + case 'MKCALENDAR': + case 'PROPPATCH': + case 'PUT': + case 'PATCH': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + false + )); + break; + case 'MOVE': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + true + )); + $mustLocks = array_merge($mustLocks, $this->getLocks( + $this->server->calculateUri($request->getHeader('Destination')), + false + )); + break; + case 'COPY': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $this->server->calculateUri($request->getHeader('Destination')), + false + )); + break; + case 'LOCK': + //Temporary measure.. figure out later why this is needed + // Here we basically ignore all incoming tokens... + foreach ($conditions as $ii => $condition) { + foreach ($condition['tokens'] as $jj => $token) { + $conditions[$ii]['tokens'][$jj]['validToken'] = true; + } + } + + return; + } + + // It's possible that there's identical locks, because of shared + // parents. We're removing the duplicates here. + $tmp = []; + foreach ($mustLocks as $lock) { + $tmp[$lock->token] = $lock; + } + $mustLocks = array_values($tmp); + + foreach ($conditions as $kk => $condition) { + foreach ($condition['tokens'] as $ii => $token) { + // Lock tokens always start with opaquelocktoken: + if ('opaquelocktoken:' !== substr($token['token'], 0, 16)) { + continue; + } + + $checkToken = substr($token['token'], 16); + // Looping through our list with locks. + foreach ($mustLocks as $jj => $mustLock) { + if ($mustLock->token == $checkToken) { + // We have a match! + // Removing this one from mustlocks + unset($mustLocks[$jj]); + + // Marking the condition as valid. + $conditions[$kk]['tokens'][$ii]['validToken'] = true; + + // Advancing to the next token + continue 2; + } + } + + // If we got here, it means that there was a + // lock-token, but it was not in 'mustLocks'. + // + // This is an edge-case, as it could mean that token + // was specified with a url that was not 'required' to + // check. So we're doing one extra lookup to make sure + // we really don't know this token. + // + // This also gets triggered when the user specified a + // lock-token that was expired. + $oddLocks = $this->getLocks($condition['uri']); + foreach ($oddLocks as $oddLock) { + if ($oddLock->token === $checkToken) { + // We have a hit! + $conditions[$kk]['tokens'][$ii]['validToken'] = true; + continue 2; + } + } + + // If we get all the way here, the lock-token was + // really unknown. + } + } + + // If there's any locks left in the 'mustLocks' array, it means that + // the resource was locked and we must block it. + if ($mustLocks) { + throw new DAV\Exception\Locked(reset($mustLocks)); + } + } + + /** + * Parses a webdav lock xml body, and returns a new Sabre\DAV\Locks\LockInfo object. + * + * @param string $body + * + * @return LockInfo + */ + protected function parseLockRequest($body) + { + $result = $this->server->xml->expect( + '{DAV:}lockinfo', + $body + ); + + $lockInfo = new LockInfo(); + + $lockInfo->owner = $result->owner; + $lockInfo->token = DAV\UUIDUtil::getUUID(); + $lockInfo->scope = $result->scope; + + return $lockInfo; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK', + 'link' => 'http://sabre.io/dav/locks/', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/MkCol.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/MkCol.php new file mode 100644 index 0000000..f3c5ea5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/MkCol.php @@ -0,0 +1,71 @@ +resourceType = $resourceType; + parent::__construct($mutations); + } + + /** + * Returns the resourcetype of the new collection. + * + * @return string[] + */ + public function getResourceType() + { + return $this->resourceType; + } + + /** + * Returns true or false if the MKCOL operation has at least the specified + * resource type. + * + * If the resourcetype is specified as an array, all resourcetypes are + * checked. + * + * @param string|string[] $resourceType + * + * @return bool + */ + public function hasResourceType($resourceType) + { + return 0 === count(array_diff((array) $resourceType, $this->resourceType)); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Mount/Plugin.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Mount/Plugin.php new file mode 100644 index 0000000..b7f4851 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Mount/Plugin.php @@ -0,0 +1,78 @@ +server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + } + + /** + * 'beforeMethod' event handles. This event handles intercepts GET requests ending + * with ?mount. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('mount', $queryParams)) { + return; + } + + $currentUri = $request->getAbsoluteUrl(); + + // Stripping off everything after the ? + list($currentUri) = explode('?', $currentUri); + + $this->davMount($response, $currentUri); + + // Returning false to break the event chain + return false; + } + + /** + * Generates the davmount response. + * + * @param string $uri absolute uri + */ + public function davMount(ResponseInterface $response, $uri) + { + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/davmount+xml'); + ob_start(); + echo '', "\n"; + echo "\n"; + echo ' ', htmlspecialchars($uri, ENT_NOQUOTES, 'UTF-8'), "\n"; + echo ''; + $response->setBody(ob_get_clean()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Node.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Node.php new file mode 100644 index 0000000..948060d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Node.php @@ -0,0 +1,51 @@ +addPlugin($patchPlugin); + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends DAV\ServerPlugin +{ + const RANGE_APPEND = 1; + const RANGE_START = 2; + const RANGE_END = 3; + + /** + * Reference to server. + * + * @var DAV\Server + */ + protected $server; + + /** + * Initializes the plugin. + * + * This method is automatically called by the Server class after addPlugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + $server->on('method:PATCH', [$this, 'httpPatch']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'partialupdate'; + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * We claim to support PATCH method (partirl update) if and only if + * - the node exist + * - the node implements our partial update interface + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) + { + $tree = $this->server->tree; + + if ($tree->nodeExists($uri)) { + $node = $tree->getNodeForPath($uri); + if ($node instanceof IPatchSupport) { + return ['PATCH']; + } + } + + return []; + } + + /** + * Returns a list of features for the HTTP OPTIONS Dav: header. + * + * @return array + */ + public function getFeatures() + { + return ['sabredav-partialupdate']; + } + + /** + * Patch an uri. + * + * The WebDAV patch request can be used to modify only a part of an + * existing resource. If the resource does not exist yet and the first + * offset is not 0, the request fails + */ + public function httpPatch(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + // Get the node. Will throw a 404 if not found + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof IPatchSupport) { + throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.'); + } + + $range = $this->getHTTPUpdateRange($request); + + if (!$range) { + throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers'); + } + + $contentType = strtolower( + (string) $request->getHeader('Content-Type') + ); + + if ('application/x-sabredav-partialupdate' != $contentType) { + throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "'.$contentType.'"'); + } + + $len = $this->server->httpRequest->getHeader('Content-Length'); + if (!$len) { + throw new DAV\Exception\LengthRequired('A Content-Length header is required'); + } + switch ($range[0]) { + case self::RANGE_START: + // Calculate the end-range if it doesn't exist. + if (!$range[2]) { + $range[2] = $range[1] + $len - 1; + } else { + if ($range[2] < $range[1]) { + throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[2].') is lower than the start offset ('.$range[1].')'); + } + if ($range[2] - $range[1] + 1 != $len) { + throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length ('.$len.') is not consistent with begin ('.$range[1].') and end ('.$range[2].') offsets'); + } + } + break; + } + + if (!$this->server->emit('beforeWriteContent', [$path, $node, null])) { + return; + } + + $body = $this->server->httpRequest->getBody(); + + $etag = $node->patch($body, $range[0], isset($range[1]) ? $range[1] : null); + + $this->server->emit('afterWriteContent', [$path, $node]); + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(204); + + // Breaks the event chain + return false; + } + + /** + * Returns the HTTP custom range update header. + * + * This method returns null if there is no well-formed HTTP range request + * header. It returns array(1) if it was an append request, array(2, + * $start, $end) if it's a start and end range, lastly it's array(3, + * $endoffset) if the offset was negative, and should be calculated from + * the end of the file. + * + * Examples: + * + * null - invalid + * [1] - append + * [2,10,15] - update bytes 10, 11, 12, 13, 14, 15 + * [2,10,null] - update bytes 10 until the end of the patch body + * [3,-5] - update from 5 bytes from the end of the file. + * + * @return array|null + */ + public function getHTTPUpdateRange(RequestInterface $request) + { + $range = $request->getHeader('X-Update-Range'); + if (is_null($range)) { + return null; + } + + // Matching "Range: bytes=1234-5678: both numbers are optional + + if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i', $range, $matches)) { + return null; + } + + if ('append' === $matches[1]) { + return [self::RANGE_APPEND]; + } elseif (strlen($matches[2]) > 0) { + return [self::RANGE_START, (int) $matches[2], (int) $matches[3] ?: null]; + } else { + return [self::RANGE_END, (int) $matches[4]]; + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropFind.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropFind.php new file mode 100644 index 0000000..e9ffb07 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropFind.php @@ -0,0 +1,335 @@ +path = $path; + $this->properties = $properties; + $this->depth = $depth; + $this->requestType = $requestType; + + if (self::ALLPROPS === $requestType) { + $this->properties = [ + '{DAV:}getlastmodified', + '{DAV:}getcontentlength', + '{DAV:}resourcetype', + '{DAV:}quota-used-bytes', + '{DAV:}quota-available-bytes', + '{DAV:}getetag', + '{DAV:}getcontenttype', + ]; + } + + foreach ($this->properties as $propertyName) { + // Seeding properties with 404's. + $this->result[$propertyName] = [404, null]; + } + $this->itemsLeft = count($this->result); + } + + /** + * Handles a specific property. + * + * This method checks whether the specified property was requested in this + * PROPFIND request, and if so, it will call the callback and use the + * return value for it's value. + * + * Example: + * + * $propFind->handle('{DAV:}displayname', function() { + * return 'hello'; + * }); + * + * Note that handle will only work the first time. If null is returned, the + * value is ignored. + * + * It's also possible to not pass a callback, but immediately pass a value + * + * @param string $propertyName + * @param mixed $valueOrCallBack + */ + public function handle($propertyName, $valueOrCallBack) + { + if ($this->itemsLeft && isset($this->result[$propertyName]) && 404 === $this->result[$propertyName][0]) { + if (is_callable($valueOrCallBack)) { + $value = $valueOrCallBack(); + } else { + $value = $valueOrCallBack; + } + if (!is_null($value)) { + --$this->itemsLeft; + $this->result[$propertyName] = [200, $value]; + } + } + } + + /** + * Sets the value of the property. + * + * If status is not supplied, the status will default to 200 for non-null + * properties, and 404 for null properties. + * + * @param string $propertyName + * @param mixed $value + * @param int $status + */ + public function set($propertyName, $value, $status = null) + { + if (is_null($status)) { + $status = is_null($value) ? 404 : 200; + } + // If this is an ALLPROPS request and the property is + // unknown, add it to the result; else ignore it: + if (!isset($this->result[$propertyName])) { + if (self::ALLPROPS === $this->requestType) { + $this->result[$propertyName] = [$status, $value]; + } + + return; + } + if (404 !== $status && 404 === $this->result[$propertyName][0]) { + --$this->itemsLeft; + } elseif (404 === $status && 404 !== $this->result[$propertyName][0]) { + ++$this->itemsLeft; + } + $this->result[$propertyName] = [$status, $value]; + } + + /** + * Returns the current value for a property. + * + * @param string $propertyName + * + * @return mixed + */ + public function get($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null; + } + + /** + * Returns the current status code for a property name. + * + * If the property does not appear in the list of requested properties, + * null will be returned. + * + * @param string $propertyName + * + * @return int|null + */ + public function getStatus($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : null; + } + + /** + * Updates the path for this PROPFIND. + * + * @param string $path + */ + public function setPath($path) + { + $this->path = $path; + } + + /** + * Returns the path this PROPFIND request is for. + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Returns the depth of this propfind request. + * + * @return int + */ + public function getDepth() + { + return $this->depth; + } + + /** + * Updates the depth of this propfind request. + * + * @param int $depth + */ + public function setDepth($depth) + { + $this->depth = $depth; + } + + /** + * Returns all propertynames that have a 404 status, and thus don't have a + * value yet. + * + * @return array + */ + public function get404Properties() + { + if (0 === $this->itemsLeft) { + return []; + } + $result = []; + foreach ($this->result as $propertyName => $stuff) { + if (404 === $stuff[0]) { + $result[] = $propertyName; + } + } + + return $result; + } + + /** + * Returns the full list of requested properties. + * + * This returns just their names, not a status or value. + * + * @return array + */ + public function getRequestedProperties() + { + return $this->properties; + } + + /** + * Returns true if this was an '{DAV:}allprops' request. + * + * @return bool + */ + public function isAllProps() + { + return self::ALLPROPS === $this->requestType; + } + + /** + * Returns a result array that's often used in multistatus responses. + * + * The array uses status codes as keys, and property names and value pairs + * as the value of the top array.. such as : + * + * [ + * 200 => [ '{DAV:}displayname' => 'foo' ], + * ] + * + * @return array + */ + public function getResultForMultiStatus() + { + $r = [ + 200 => [], + 404 => [], + ]; + foreach ($this->result as $propertyName => $info) { + if (!isset($r[$info[0]])) { + $r[$info[0]] = [$propertyName => $info[1]]; + } else { + $r[$info[0]][$propertyName] = $info[1]; + } + } + // Removing the 404's for multi-status requests. + if (self::ALLPROPS === $this->requestType) { + unset($r[404]); + } + + return $r; + } + + /** + * The path that we're fetching properties for. + * + * @var string + */ + protected $path; + + /** + * The Depth of the request. + * + * 0 means only the current item. 1 means the current item + its children. + * It can also be DEPTH_INFINITY if this is enabled in the server. + * + * @var int + */ + protected $depth = 0; + + /** + * The type of request. See the TYPE constants. + */ + protected $requestType; + + /** + * A list of requested properties. + * + * @var array + */ + protected $properties = []; + + /** + * The result of the operation. + * + * The keys in this array are property names. + * The values are an array with two elements: the http status code and then + * optionally a value. + * + * Example: + * + * [ + * "{DAV:}owner" : [404], + * "{DAV:}displayname" : [200, "Admin"] + * ] + * + * @var array + */ + protected $result = []; + + /** + * This is used as an internal counter for the number of properties that do + * not yet have a value. + * + * @var int + */ + protected $itemsLeft; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropPatch.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropPatch.php new file mode 100644 index 0000000..092909d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropPatch.php @@ -0,0 +1,337 @@ +mutations = $mutations; + } + + /** + * Call this function if you wish to handle updating certain properties. + * For instance, your class may be responsible for handling updates for the + * {DAV:}displayname property. + * + * In that case, call this method with the first argument + * "{DAV:}displayname" and a second argument that's a method that does the + * actual updating. + * + * It's possible to specify more than one property as an array. + * + * The callback must return a boolean or an it. If the result is true, the + * operation was considered successful. If it's false, it's consided + * failed. + * + * If the result is an integer, we'll use that integer as the http status + * code associated with the operation. + * + * @param string|string[] $properties + */ + public function handle($properties, callable $callback) + { + $usedProperties = []; + foreach ((array) $properties as $propertyName) { + if (array_key_exists($propertyName, $this->mutations) && !isset($this->result[$propertyName])) { + $usedProperties[] = $propertyName; + // HTTP Accepted + $this->result[$propertyName] = 202; + } + } + + // Only registering if there's any unhandled properties. + if (!$usedProperties) { + return; + } + $this->propertyUpdateCallbacks[] = [ + // If the original argument to this method was a string, we need + // to also make sure that it stays that way, so the commit function + // knows how to format the arguments to the callback. + is_string($properties) ? $properties : $usedProperties, + $callback, + ]; + } + + /** + * Call this function if you wish to handle _all_ properties that haven't + * been handled by anything else yet. Note that you effectively claim with + * this that you promise to process _all_ properties that are coming in. + */ + public function handleRemaining(callable $callback) + { + $properties = $this->getRemainingMutations(); + if (!$properties) { + // Nothing to do, don't register callback + return; + } + + foreach ($properties as $propertyName) { + // HTTP Accepted + $this->result[$propertyName] = 202; + + $this->propertyUpdateCallbacks[] = [ + $properties, + $callback, + ]; + } + } + + /** + * Sets the result code for one or more properties. + * + * @param string|string[] $properties + * @param int $resultCode + */ + public function setResultCode($properties, $resultCode) + { + foreach ((array) $properties as $propertyName) { + $this->result[$propertyName] = $resultCode; + } + + if ($resultCode >= 400) { + $this->failed = true; + } + } + + /** + * Sets the result code for all properties that did not have a result yet. + * + * @param int $resultCode + */ + public function setRemainingResultCode($resultCode) + { + $this->setResultCode( + $this->getRemainingMutations(), + $resultCode + ); + } + + /** + * Returns the list of properties that don't have a result code yet. + * + * This method returns a list of property names, but not its values. + * + * @return string[] + */ + public function getRemainingMutations() + { + $remaining = []; + foreach ($this->mutations as $propertyName => $propValue) { + if (!isset($this->result[$propertyName])) { + $remaining[] = $propertyName; + } + } + + return $remaining; + } + + /** + * Returns the list of properties that don't have a result code yet. + * + * This method returns list of properties and their values. + * + * @return array + */ + public function getRemainingValues() + { + $remaining = []; + foreach ($this->mutations as $propertyName => $propValue) { + if (!isset($this->result[$propertyName])) { + $remaining[$propertyName] = $propValue; + } + } + + return $remaining; + } + + /** + * Performs the actual update, and calls all callbacks. + * + * This method returns true or false depending on if the operation was + * successful. + * + * @return bool + */ + public function commit() + { + // First we validate if every property has a handler + foreach ($this->mutations as $propertyName => $value) { + if (!isset($this->result[$propertyName])) { + $this->failed = true; + $this->result[$propertyName] = 403; + } + } + + foreach ($this->propertyUpdateCallbacks as $callbackInfo) { + if ($this->failed) { + break; + } + if (is_string($callbackInfo[0])) { + $this->doCallbackSingleProp($callbackInfo[0], $callbackInfo[1]); + } else { + $this->doCallbackMultiProp($callbackInfo[0], $callbackInfo[1]); + } + } + + /* + * If anywhere in this operation updating a property failed, we must + * update all other properties accordingly. + */ + if ($this->failed) { + foreach ($this->result as $propertyName => $status) { + if (202 === $status) { + // Failed dependency + $this->result[$propertyName] = 424; + } + } + } + + return !$this->failed; + } + + /** + * Executes a property callback with the single-property syntax. + * + * @param string $propertyName + */ + private function doCallBackSingleProp($propertyName, callable $callback) + { + $result = $callback($this->mutations[$propertyName]); + if (is_bool($result)) { + if ($result) { + if (is_null($this->mutations[$propertyName])) { + // Delete + $result = 204; + } else { + // Update + $result = 200; + } + } else { + // Fail + $result = 403; + } + } + if (!is_int($result)) { + throw new UnexpectedValueException('A callback sent to handle() did not return an int or a bool'); + } + $this->result[$propertyName] = $result; + if ($result >= 400) { + $this->failed = true; + } + } + + /** + * Executes a property callback with the multi-property syntax. + */ + private function doCallBackMultiProp(array $propertyList, callable $callback) + { + $argument = []; + foreach ($propertyList as $propertyName) { + $argument[$propertyName] = $this->mutations[$propertyName]; + } + + $result = $callback($argument); + + if (is_array($result)) { + foreach ($propertyList as $propertyName) { + if (!isset($result[$propertyName])) { + $resultCode = 500; + } else { + $resultCode = $result[$propertyName]; + } + if ($resultCode >= 400) { + $this->failed = true; + } + $this->result[$propertyName] = $resultCode; + } + } elseif (true === $result) { + // Success + foreach ($argument as $propertyName => $propertyValue) { + $this->result[$propertyName] = is_null($propertyValue) ? 204 : 200; + } + } elseif (false === $result) { + // Fail :( + $this->failed = true; + foreach ($propertyList as $propertyName) { + $this->result[$propertyName] = 403; + } + } else { + throw new UnexpectedValueException('A callback sent to handle() did not return an array or a bool'); + } + } + + /** + * Returns the result of the operation. + * + * @return array + */ + public function getResult() + { + return $this->result; + } + + /** + * Returns the full list of mutations. + * + * @return array + */ + public function getMutations() + { + return $this->mutations; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php new file mode 100644 index 0000000..64a8825 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php @@ -0,0 +1,75 @@ +isAllProps(). + * + * @param string $path + */ + public function propFind($path, PropFind $propFind); + + /** + * Updates properties for a path. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * Usually you would want to call 'handleRemaining' on this object, to get; + * a list of all properties that need to be stored. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch); + + /** + * This method is called after a node is deleted. + * + * This allows a backend to clean up all associated properties. + * + * The delete method will get called once for the deletion of an entire + * tree. + * + * @param string $path + */ + public function delete($path); + + /** + * This method is called after a successful MOVE. + * + * This should be used to migrate all properties from one path to another. + * Note that entire collections may be moved, so ensure that all properties + * for children are also moved along. + * + * @param string $source + * @param string $destination + */ + public function move($source, $destination); +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php new file mode 100644 index 0000000..8960331 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php @@ -0,0 +1,224 @@ +pdo = $pdo; + } + + /** + * Fetches properties for a path. + * + * This method received a PropFind object, which contains all the + * information about the properties that need to be fetched. + * + * Usually you would just want to call 'get404Properties' on this object, + * as this will give you the _exact_ list of properties that need to be + * fetched, and haven't yet. + * + * However, you can also support the 'allprops' property here. In that + * case, you should check for $propFind->isAllProps(). + * + * @param string $path + */ + public function propFind($path, PropFind $propFind) + { + if (!$propFind->isAllProps() && 0 === count($propFind->get404Properties())) { + return; + } + + $query = 'SELECT name, value, valuetype FROM '.$this->tableName.' WHERE path = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$path]); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ('resource' === gettype($row['value'])) { + $row['value'] = stream_get_contents($row['value']); + } + switch ($row['valuetype']) { + case null: + case self::VT_STRING: + $propFind->set($row['name'], $row['value']); + break; + case self::VT_XML: + $propFind->set($row['name'], new Complex($row['value'])); + break; + case self::VT_OBJECT: + $propFind->set($row['name'], unserialize($row['value'])); + break; + } + } + } + + /** + * Updates properties for a path. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * Usually you would want to call 'handleRemaining' on this object, to get; + * a list of all properties that need to be stored. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch) + { + $propPatch->handleRemaining(function ($properties) use ($path) { + if ('pgsql' === $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME)) { + $updateSql = <<tableName} (path, name, valuetype, value) +VALUES (:path, :name, :valuetype, :value) +ON CONFLICT (path, name) +DO UPDATE SET valuetype = :valuetype, value = :value +SQL; + } else { + $updateSql = <<tableName} (path, name, valuetype, value) +VALUES (:path, :name, :valuetype, :value) +SQL; + } + + $updateStmt = $this->pdo->prepare($updateSql); + $deleteStmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE path = ? AND name = ?'); + + foreach ($properties as $name => $value) { + if (!is_null($value)) { + if (is_scalar($value)) { + $valueType = self::VT_STRING; + } elseif ($value instanceof Complex) { + $valueType = self::VT_XML; + $value = $value->getXml(); + } else { + $valueType = self::VT_OBJECT; + $value = serialize($value); + } + + $updateStmt->bindParam('path', $path, \PDO::PARAM_STR); + $updateStmt->bindParam('name', $name, \PDO::PARAM_STR); + $updateStmt->bindParam('valuetype', $valueType, \PDO::PARAM_INT); + $updateStmt->bindParam('value', $value, \PDO::PARAM_LOB); + + $updateStmt->execute(); + } else { + $deleteStmt->execute([$path, $name]); + } + } + + return true; + }); + } + + /** + * This method is called after a node is deleted. + * + * This allows a backend to clean up all associated properties. + * + * The delete method will get called once for the deletion of an entire + * tree. + * + * @param string $path + */ + public function delete($path) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName." WHERE path = ? OR path LIKE ? ESCAPE '='"); + $childPath = strtr( + $path, + [ + '=' => '==', + '%' => '=%', + '_' => '=_', + ] + ).'/%'; + + $stmt->execute([$path, $childPath]); + } + + /** + * This method is called after a successful MOVE. + * + * This should be used to migrate all properties from one path to another. + * Note that entire collections may be moved, so ensure that all properties + * for children are also moved along. + * + * @param string $source + * @param string $destination + */ + public function move($source, $destination) + { + // I don't know a way to write this all in a single sql query that's + // also compatible across db engines, so we're letting PHP do all the + // updates. Much slower, but it should still be pretty fast in most + // cases. + $select = $this->pdo->prepare('SELECT id, path FROM '.$this->tableName.' WHERE path = ? OR path LIKE ?'); + $select->execute([$source, $source.'/%']); + + $update = $this->pdo->prepare('UPDATE '.$this->tableName.' SET path = ? WHERE id = ?'); + while ($row = $select->fetch(\PDO::FETCH_ASSOC)) { + // Sanity check. SQL may select too many records, such as records + // with different cases. + if ($row['path'] !== $source && 0 !== strpos($row['path'], $source.'/')) { + continue; + } + + $trailingPart = substr($row['path'], strlen($source) + 1); + $newPath = $destination; + if ($trailingPart) { + $newPath .= '/'.$trailingPart; + } + $update->execute([$newPath, $row['id']]); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php new file mode 100644 index 0000000..da47ec9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php @@ -0,0 +1,176 @@ +backend = $backend; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + */ + public function initialize(Server $server) + { + $server->on('propFind', [$this, 'propFind'], 130); + $server->on('propPatch', [$this, 'propPatch'], 300); + $server->on('afterMove', [$this, 'afterMove']); + $server->on('afterUnbind', [$this, 'afterUnbind']); + } + + /** + * Called during PROPFIND operations. + * + * If there's any requested properties that don't have a value yet, this + * plugin will look in the property storage backend to find them. + */ + public function propFind(PropFind $propFind, INode $node) + { + $path = $propFind->getPath(); + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->propFind($propFind->getPath(), $propFind); + } + + /** + * Called during PROPPATCH operations. + * + * If there's any updated properties that haven't been stored, the + * propertystorage backend can handle it. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->propPatch($path, $propPatch); + } + + /** + * Called after a node is deleted. + * + * This allows the backend to clean up any properties still in the + * database. + * + * @param string $path + */ + public function afterUnbind($path) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->delete($path); + } + + /** + * Called after a node is moved. + * + * This allows the backend to move all the associated properties. + * + * @param string $source + * @param string $destination + */ + public function afterMove($source, $destination) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($source)) { + return; + } + // If the destination is filtered, afterUnbind will handle cleaning up + // the properties. + if ($pathFilter && !$pathFilter($destination)) { + return; + } + + $this->backend->move($source, $destination); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'property-storage'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin allows any arbitrary WebDAV property to be set on any resource.', + 'link' => 'http://sabre.io/dav/property-storage/', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Server.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Server.php new file mode 100644 index 0000000..3c23750 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Server.php @@ -0,0 +1,1672 @@ + '{DAV:}collection', + ]; + + /** + * This property allows the usage of Depth: infinity on PROPFIND requests. + * + * By default Depth: infinity is treated as Depth: 1. Allowing Depth: + * infinity is potentially risky, as it allows a single client to do a full + * index of the webdav server, which is an easy DoS attack vector. + * + * Only turn this on if you know what you're doing. + * + * @var bool + */ + public $enablePropfindDepthInfinity = false; + + /** + * Reference to the XML utility object. + * + * @var Xml\Service + */ + public $xml; + + /** + * If this setting is turned off, SabreDAV's version number will be hidden + * from various places. + * + * Some people feel this is a good security measure. + * + * @var bool + */ + public static $exposeVersion = true; + + /** + * If this setting is turned on, any multi status response on any PROPFIND will be streamed to the output buffer. + * This will be beneficial for large result sets which will no longer consume a large amount of memory as well as + * send back data to the client earlier. + * + * @var bool + */ + public static $streamMultiStatus = false; + + /** + * Sets up the server. + * + * If a Sabre\DAV\Tree object is passed as an argument, it will + * use it as the directory tree. If a Sabre\DAV\INode is passed, it + * will create a Sabre\DAV\Tree and use the node as the root. + * + * If nothing is passed, a Sabre\DAV\SimpleCollection is created in + * a Sabre\DAV\Tree. + * + * If an array is passed, we automatically create a root node, and use + * the nodes in the array as top-level children. + * + * @param Tree|INode|array|null $treeOrNode The tree object + */ + public function __construct($treeOrNode = null) + { + if ($treeOrNode instanceof Tree) { + $this->tree = $treeOrNode; + } elseif ($treeOrNode instanceof INode) { + $this->tree = new Tree($treeOrNode); + } elseif (is_array($treeOrNode)) { + $root = new SimpleCollection('root', $treeOrNode); + $this->tree = new Tree($root); + } elseif (is_null($treeOrNode)) { + $root = new SimpleCollection('root'); + $this->tree = new Tree($root); + } else { + throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null'); + } + + $this->xml = new Xml\Service(); + $this->sapi = new HTTP\Sapi(); + $this->httpResponse = new HTTP\Response(); + $this->httpRequest = $this->sapi->getRequest(); + $this->addPlugin(new CorePlugin()); + } + + /** + * Starts the DAV Server. + */ + public function start() + { + try { + // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an + // origin, we must make sure we send back HTTP/1.0 if this was + // requested. + // This is mainly because nginx doesn't support Chunked Transfer + // Encoding, and this forces the webserver SabreDAV is running on, + // to buffer entire responses to calculate Content-Length. + $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); + + // Setting the base url + $this->httpRequest->setBaseUrl($this->getBaseUri()); + $this->invokeMethod($this->httpRequest, $this->httpResponse); + } catch (\Throwable $e) { + try { + $this->emit('exception', [$e]); + } catch (\Exception $ignore) { + } + $DOM = new \DOMDocument('1.0', 'utf-8'); + $DOM->formatOutput = true; + + $error = $DOM->createElementNS('DAV:', 'd:error'); + $error->setAttribute('xmlns:s', self::NS_SABREDAV); + $DOM->appendChild($error); + + $h = function ($v) { + return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8'); + }; + + if (self::$exposeVersion) { + $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); + } + + $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); + $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); + if ($this->debugExceptions) { + $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); + $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); + $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); + $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); + } + + if ($this->debugExceptions) { + $previous = $e; + while ($previous = $previous->getPrevious()) { + $xPrevious = $DOM->createElement('s:previous-exception'); + $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); + $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); + $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); + $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); + $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); + $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); + $error->appendChild($xPrevious); + } + } + + if ($e instanceof Exception) { + $httpCode = $e->getHTTPCode(); + $e->serialize($this, $error); + $headers = $e->getHTTPHeaders($this); + } else { + $httpCode = 500; + $headers = []; + } + $headers['Content-Type'] = 'application/xml; charset=utf-8'; + + $this->httpResponse->setStatus($httpCode); + $this->httpResponse->setHeaders($headers); + $this->httpResponse->setBody($DOM->saveXML()); + $this->sapi->sendResponse($this->httpResponse); + } + } + + /** + * Alias of start(). + * + * @deprecated + */ + public function exec() + { + $this->start(); + } + + /** + * Sets the base server uri. + * + * @param string $uri + */ + public function setBaseUri($uri) + { + // If the baseUri does not end with a slash, we must add it + if ('/' !== $uri[strlen($uri) - 1]) { + $uri .= '/'; + } + + $this->baseUri = $uri; + } + + /** + * Returns the base responding uri. + * + * @return string + */ + public function getBaseUri() + { + if (is_null($this->baseUri)) { + $this->baseUri = $this->guessBaseUri(); + } + + return $this->baseUri; + } + + /** + * This method attempts to detect the base uri. + * Only the PATH_INFO variable is considered. + * + * If this variable is not set, the root (/) is assumed. + * + * @return string + */ + public function guessBaseUri() + { + $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); + $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); + + // If PATH_INFO is found, we can assume it's accurate. + if (!empty($pathInfo)) { + // We need to make sure we ignore the QUERY_STRING part + if ($pos = strpos($uri, '?')) { + $uri = substr($uri, 0, $pos); + } + + // PATH_INFO is only set for urls, such as: /example.php/path + // in that case PATH_INFO contains '/path'. + // Note that REQUEST_URI is percent encoded, while PATH_INFO is + // not, Therefore they are only comparable if we first decode + // REQUEST_INFO as well. + $decodedUri = HTTP\decodePath($uri); + + // A simple sanity check: + if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) { + $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo)); + + return rtrim($baseUri, '/').'/'; + } + + throw new Exception('The REQUEST_URI ('.$uri.') did not end with the contents of PATH_INFO ('.$pathInfo.'). This server might be misconfigured.'); + } + + // The last fallback is that we're just going to assume the server root. + return '/'; + } + + /** + * Adds a plugin to the server. + * + * For more information, console the documentation of Sabre\DAV\ServerPlugin + */ + public function addPlugin(ServerPlugin $plugin) + { + $this->plugins[$plugin->getPluginName()] = $plugin; + $plugin->initialize($this); + } + + /** + * Returns an initialized plugin by it's name. + * + * This function returns null if the plugin was not found. + * + * @param string $name + * + * @return ServerPlugin + */ + public function getPlugin($name) + { + if (isset($this->plugins[$name])) { + return $this->plugins[$name]; + } + + return null; + } + + /** + * Returns all plugins. + * + * @return array + */ + public function getPlugins() + { + return $this->plugins; + } + + /** + * Returns the PSR-3 logger object. + * + * @return LoggerInterface + */ + public function getLogger() + { + if (!$this->logger) { + $this->logger = new NullLogger(); + } + + return $this->logger; + } + + /** + * Handles a http request, and execute a method based on its name. + * + * @param bool $sendResponse whether to send the HTTP response to the DAV client + */ + public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) + { + $method = $request->getMethod(); + + if (!$this->emit('beforeMethod:'.$method, [$request, $response])) { + return; + } + + if (self::$exposeVersion) { + $response->setHeader('X-Sabre-Version', Version::VERSION); + } + + $this->transactionType = strtolower($method); + + if (!$this->checkPreconditions($request, $response)) { + $this->sapi->sendResponse($response); + + return; + } + + if ($this->emit('method:'.$method, [$request, $response])) { + $exMessage = 'There was no plugin in the system that was willing to handle this '.$method.' method.'; + if ('GET' === $method) { + $exMessage .= ' Enable the Browser plugin to get a better result here.'; + } + + // Unsupported method + throw new Exception\NotImplemented($exMessage); + } + + if (!$this->emit('afterMethod:'.$method, [$request, $response])) { + return; + } + + if (null === $response->getStatus()) { + throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.'); + } + if ($sendResponse) { + $this->sapi->sendResponse($response); + $this->emit('afterResponse', [$request, $response]); + } + } + + // {{{ HTTP/WebDAV protocol helpers + + /** + * Returns an array with all the supported HTTP methods for a specific uri. + * + * @param string $path + * + * @return array + */ + public function getAllowedMethods($path) + { + $methods = [ + 'OPTIONS', + 'GET', + 'HEAD', + 'DELETE', + 'PROPFIND', + 'PUT', + 'PROPPATCH', + 'COPY', + 'MOVE', + 'REPORT', + ]; + + // The MKCOL is only allowed on an unmapped uri + try { + $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + $methods[] = 'MKCOL'; + } + + // We're also checking if any of the plugins register any new methods + foreach ($this->plugins as $plugin) { + $methods = array_merge($methods, $plugin->getHTTPMethods($path)); + } + array_unique($methods); + + return $methods; + } + + /** + * Gets the uri for the request, keeping the base uri into consideration. + * + * @return string + */ + public function getRequestUri() + { + return $this->calculateUri($this->httpRequest->getUrl()); + } + + /** + * Turns a URI such as the REQUEST_URI into a local path. + * + * This method: + * * strips off the base path + * * normalizes the path + * * uri-decodes the path + * + * @param string $uri + * + * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri + * + * @return string + */ + public function calculateUri($uri) + { + if ('' != $uri && '/' != $uri[0] && strpos($uri, '://')) { + $uri = parse_url($uri, PHP_URL_PATH); + } + + $uri = Uri\normalize(preg_replace('|/+|', '/', $uri)); + $baseUri = Uri\normalize($this->getBaseUri()); + + if (0 === strpos($uri, $baseUri)) { + return trim(HTTP\decodePath(substr($uri, strlen($baseUri))), '/'); + + // A special case, if the baseUri was accessed without a trailing + // slash, we'll accept it as well. + } elseif ($uri.'/' === $baseUri) { + return ''; + } else { + throw new Exception\Forbidden('Requested uri ('.$uri.') is out of base uri ('.$this->getBaseUri().')'); + } + } + + /** + * Returns the HTTP depth header. + * + * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object + * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent + * + * @param mixed $default + * + * @return int + */ + public function getHTTPDepth($default = self::DEPTH_INFINITY) + { + // If its not set, we'll grab the default + $depth = $this->httpRequest->getHeader('Depth'); + + if (is_null($depth)) { + return $default; + } + + if ('infinity' == $depth) { + return self::DEPTH_INFINITY; + } + + // If its an unknown value. we'll grab the default + if (!ctype_digit($depth)) { + return $default; + } + + return (int) $depth; + } + + /** + * Returns the HTTP range header. + * + * This method returns null if there is no well-formed HTTP range request + * header or array($start, $end). + * + * The first number is the offset of the first byte in the range. + * The second number is the offset of the last byte in the range. + * + * If the second offset is null, it should be treated as the offset of the last byte of the entity + * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity + * + * @return int[]|null + */ + public function getHTTPRange() + { + $range = $this->httpRequest->getHeader('range'); + if (is_null($range)) { + return null; + } + + // Matching "Range: bytes=1234-5678: both numbers are optional + + if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) { + return null; + } + + if ('' === $matches[1] && '' === $matches[2]) { + return null; + } + + return [ + '' !== $matches[1] ? (int) $matches[1] : null, + '' !== $matches[2] ? (int) $matches[2] : null, + ]; + } + + /** + * Returns the HTTP Prefer header information. + * + * The prefer header is defined in: + * http://tools.ietf.org/html/draft-snell-http-prefer-14 + * + * This method will return an array with options. + * + * Currently, the following options may be returned: + * [ + * 'return-asynch' => true, + * 'return-minimal' => true, + * 'return-representation' => true, + * 'wait' => 30, + * 'strict' => true, + * 'lenient' => true, + * ] + * + * This method also supports the Brief header, and will also return + * 'return-minimal' if the brief header was set to 't'. + * + * For the boolean options, false will be returned if the headers are not + * specified. For the integer options it will be 'null'. + * + * @return array + */ + public function getHTTPPrefer() + { + $result = [ + // can be true or false + 'respond-async' => false, + // Could be set to 'representation' or 'minimal'. + 'return' => null, + // Used as a timeout, is usually a number. + 'wait' => null, + // can be 'strict' or 'lenient'. + 'handling' => false, + ]; + + if ($prefer = $this->httpRequest->getHeader('Prefer')) { + $result = array_merge( + $result, + HTTP\parsePrefer($prefer) + ); + } elseif ('t' == $this->httpRequest->getHeader('Brief')) { + $result['return'] = 'minimal'; + } + + return $result; + } + + /** + * Returns information about Copy and Move requests. + * + * This function is created to help getting information about the source and the destination for the + * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions + * + * The returned value is an array with the following keys: + * * destination - Destination path + * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten) + * + * @throws Exception\BadRequest upon missing or broken request headers + * @throws Exception\UnsupportedMediaType when trying to copy into a + * non-collection + * @throws Exception\PreconditionFailed if overwrite is set to false, but + * the destination exists + * @throws Exception\Forbidden when source and destination paths are + * identical + * @throws Exception\Conflict when trying to copy a node into its own + * subtree + * + * @return array + */ + public function getCopyAndMoveInfo(RequestInterface $request) + { + // Collecting the relevant HTTP headers + if (!$request->getHeader('Destination')) { + throw new Exception\BadRequest('The destination header was not supplied'); + } + $destination = $this->calculateUri($request->getHeader('Destination')); + $overwrite = $request->getHeader('Overwrite'); + if (!$overwrite) { + $overwrite = 'T'; + } + if ('T' == strtoupper($overwrite)) { + $overwrite = true; + } elseif ('F' == strtoupper($overwrite)) { + $overwrite = false; + } + // We need to throw a bad request exception, if the header was invalid + else { + throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); + } + list($destinationDir) = Uri\split($destination); + + try { + $destinationParent = $this->tree->getNodeForPath($destinationDir); + if (!($destinationParent instanceof ICollection)) { + throw new Exception\UnsupportedMediaType('The destination node is not a collection'); + } + } catch (Exception\NotFound $e) { + // If the destination parent node is not found, we throw a 409 + throw new Exception\Conflict('The destination node is not found'); + } + + try { + $destinationNode = $this->tree->getNodeForPath($destination); + + // If this succeeded, it means the destination already exists + // we'll need to throw precondition failed in case overwrite is false + if (!$overwrite) { + throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); + } + } catch (Exception\NotFound $e) { + // Destination didn't exist, we're all good + $destinationNode = false; + } + + $requestPath = $request->getPath(); + if ($destination === $requestPath) { + throw new Exception\Forbidden('Source and destination uri are identical.'); + } + if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath.'/') { + throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.'); + } + + // These are the three relevant properties we need to return + return [ + 'destination' => $destination, + 'destinationExists' => (bool) $destinationNode, + 'destinationNode' => $destinationNode, + ]; + } + + /** + * Returns a list of properties for a path. + * + * This is a simplified version getPropertiesForPath. If you aren't + * interested in status codes, but you just want to have a flat list of + * properties, use this method. + * + * Please note though that any problems related to retrieving properties, + * such as permission issues will just result in an empty array being + * returned. + * + * @param string $path + * @param array $propertyNames + * + * @return array + */ + public function getProperties($path, $propertyNames) + { + $result = $this->getPropertiesForPath($path, $propertyNames, 0); + if (isset($result[0][200])) { + return $result[0][200]; + } else { + return []; + } + } + + /** + * A kid-friendly way to fetch properties for a node's children. + * + * The returned array will be indexed by the path of the of child node. + * Only properties that are actually found will be returned. + * + * The parent node will not be returned. + * + * @param string $path + * @param array $propertyNames + * + * @return array + */ + public function getPropertiesForChildren($path, $propertyNames) + { + $result = []; + foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { + // Skipping the parent path + if (0 === $k) { + continue; + } + + $result[$row['href']] = $row[200]; + } + + return $result; + } + + /** + * Returns a list of HTTP headers for a particular resource. + * + * The generated http headers are based on properties provided by the + * resource. The method basically provides a simple mapping between + * DAV property and HTTP header. + * + * The headers are intended to be used for HEAD and GET requests. + * + * @param string $path + * + * @return array + */ + public function getHTTPHeaders($path) + { + $propertyMap = [ + '{DAV:}getcontenttype' => 'Content-Type', + '{DAV:}getcontentlength' => 'Content-Length', + '{DAV:}getlastmodified' => 'Last-Modified', + '{DAV:}getetag' => 'ETag', + ]; + + $properties = $this->getProperties($path, array_keys($propertyMap)); + + $headers = []; + foreach ($propertyMap as $property => $header) { + if (!isset($properties[$property])) { + continue; + } + + if (is_scalar($properties[$property])) { + $headers[$header] = $properties[$property]; + + // GetLastModified gets special cased + } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) { + $headers[$header] = HTTP\toDate($properties[$property]->getTime()); + } + } + + return $headers; + } + + /** + * Small helper to support PROPFIND with DEPTH_INFINITY. + * + * @param array $yieldFirst + * + * @return \Traversable + */ + private function generatePathNodes(PropFind $propFind, array $yieldFirst = null) + { + if (null !== $yieldFirst) { + yield $yieldFirst; + } + $newDepth = $propFind->getDepth(); + $path = $propFind->getPath(); + + if (self::DEPTH_INFINITY !== $newDepth) { + --$newDepth; + } + + $propertyNames = $propFind->getRequestedProperties(); + $propFindType = !empty($propertyNames) ? PropFind::NORMAL : PropFind::ALLPROPS; + + foreach ($this->tree->getChildren($path) as $childNode) { + if ('' !== $path) { + $subPath = $path.'/'.$childNode->getName(); + } else { + $subPath = $childNode->getName(); + } + $subPropFind = new PropFind($subPath, $propertyNames, $newDepth, $propFindType); + + yield [ + $subPropFind, + $childNode, + ]; + + if ((self::DEPTH_INFINITY === $newDepth || $newDepth >= 1) && $childNode instanceof ICollection) { + foreach ($this->generatePathNodes($subPropFind) as $subItem) { + yield $subItem; + } + } + } + } + + /** + * Returns a list of properties for a given path. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * If a depth of 1 is requested child elements will also be returned. + * + * @param string $path + * @param array $propertyNames + * @param int $depth + * + * @return array + * + * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient) + * @see getPropertiesIteratorForPath() + */ + public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) + { + return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth)); + } + + /** + * Returns a list of properties for a given path. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * If a depth of 1 is requested child elements will also be returned. + * + * @param string $path + * @param array $propertyNames + * @param int $depth + * + * @return \Iterator + */ + public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0) + { + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled + if (!$this->enablePropfindDepthInfinity && 0 != $depth) { + $depth = 1; + } + + $path = trim($path, '/'); + + $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; + $propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType); + + $parentNode = $this->tree->getNodeForPath($path); + + $propFindRequests = [[ + $propFind, + $parentNode, + ]]; + + if (($depth > 0 || self::DEPTH_INFINITY === $depth) && $parentNode instanceof ICollection) { + $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests)); + } + + foreach ($propFindRequests as $propFindRequest) { + list($propFind, $node) = $propFindRequest; + $r = $this->getPropertiesByNode($propFind, $node); + if ($r) { + $result = $propFind->getResultForMultiStatus(); + $result['href'] = $propFind->getPath(); + + // WebDAV recommends adding a slash to the path, if the path is + // a collection. + // Furthermore, iCal also demands this to be the case for + // principals. This is non-standard, but we support it. + $resourceType = $this->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result['href'] .= '/'; + } + yield $result; + } + } + } + + /** + * Returns a list of properties for a list of paths. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * The result is returned as an array, with paths for it's keys. + * The result may be returned out of order. + * + * @return array + */ + public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) + { + $result = [ + ]; + + $nodes = $this->tree->getMultipleNodes($paths); + + foreach ($nodes as $path => $node) { + $propFind = new PropFind($path, $propertyNames); + $r = $this->getPropertiesByNode($propFind, $node); + if ($r) { + $result[$path] = $propFind->getResultForMultiStatus(); + $result[$path]['href'] = $path; + + $resourceType = $this->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result[$path]['href'] .= '/'; + } + } + } + + return $result; + } + + /** + * Determines all properties for a node. + * + * This method tries to grab all properties for a node. This method is used + * internally getPropertiesForPath and a few others. + * + * It could be useful to call this, if you already have an instance of your + * target node and simply want to run through the system to get a correct + * list of properties. + * + * @return bool + */ + public function getPropertiesByNode(PropFind $propFind, INode $node) + { + return $this->emit('propFind', [$propFind, $node]); + } + + /** + * This method is invoked by sub-systems creating a new file. + * + * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). + * It was important to get this done through a centralized function, + * allowing plugins to intercept this using the beforeCreateFile event. + * + * This method will return true if the file was actually created + * + * @param string $uri + * @param resource $data + * @param string $etag + * + * @return bool + */ + public function createFile($uri, $data, &$etag = null) + { + list($dir, $name) = Uri\split($uri); + + if (!$this->emit('beforeBind', [$uri])) { + return false; + } + + $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 + */ + public function updateFile($uri, $data, &$etag = null) + { + $node = $this->tree->getNodeForPath($uri); + + // It is possible for an event handler to modify the content of the + // body, before it gets written. If this is the case, $modified + // should be set to true. + // + // If $modified is true, we must not send back an ETag. + $modified = false; + if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) { + return false; + } + + $etag = $node->put($data); + if ($modified) { + $etag = null; + } + $this->emit('afterWriteContent', [$uri, $node]); + + return true; + } + + /** + * This method is invoked by sub-systems creating a new directory. + * + * @param string $uri + */ + public function createDirectory($uri) + { + $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); + } + + /** + * Use this method to create a new collection. + * + * @param string $uri The new uri + * + * @return array|null + */ + public function createCollection($uri, MkCol $mkCol) + { + list($parentUri, $newName) = Uri\split($uri); + + // Making sure the parent exists + try { + $parent = $this->tree->getNodeForPath($parentUri); + } catch (Exception\NotFound $e) { + throw new Exception\Conflict('Parent node does not exist'); + } + + // Making sure the parent is a collection + if (!$parent instanceof ICollection) { + throw new Exception\Conflict('Parent node is not a collection'); + } + + // Making sure the child does not already exist + try { + $parent->getChild($newName); + + // If we got here.. it means there's already a node on that url, and we need to throw a 405 + throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); + } catch (Exception\NotFound $e) { + // NotFound is the expected behavior. + } + + if (!$this->emit('beforeBind', [$uri])) { + return; + } + + if ($parent instanceof IExtendedCollection) { + /* + * If the parent is an instance of IExtendedCollection, it means that + * we can pass the MkCol object directly as it may be able to store + * properties immediately. + */ + $parent->createExtendedCollection($newName, $mkCol); + } else { + /* + * If the parent is a standard ICollection, it means only + * 'standard' collections can be created, so we should fail any + * MKCOL operation that carries extra resourcetypes. + */ + if (count($mkCol->getResourceType()) > 1) { + throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); + } + + $parent->createDirectory($newName); + } + + // If there are any properties that have not been handled/stored, + // we ask the 'propPatch' event to handle them. This will allow for + // example the propertyStorage system to store properties upon MKCOL. + if ($mkCol->getRemainingMutations()) { + $this->emit('propPatch', [$uri, $mkCol]); + } + $success = $mkCol->commit(); + + if (!$success) { + $result = $mkCol->getResult(); + + $formattedResult = [ + 'href' => $uri, + ]; + + foreach ($result as $propertyName => $status) { + if (!isset($formattedResult[$status])) { + $formattedResult[$status] = []; + } + $formattedResult[$status][$propertyName] = null; + } + + return $formattedResult; + } + + $this->tree->markDirty($parentUri); + $this->emit('afterBind', [$uri]); + } + + /** + * This method updates a resource's properties. + * + * The properties array must be a list of properties. Array-keys are + * property names in clarknotation, array-values are it's values. + * If a property must be deleted, the value should be null. + * + * Note that this request should either completely succeed, or + * completely fail. + * + * The response is an array with properties for keys, and http status codes + * as their values. + * + * @param string $path + * + * @return array + */ + public function updateProperties($path, array $properties) + { + $propPatch = new PropPatch($properties); + $this->emit('propPatch', [$path, $propPatch]); + $propPatch->commit(); + + return $propPatch->getResult(); + } + + /** + * This method checks the main HTTP preconditions. + * + * Currently these are: + * * If-Match + * * If-None-Match + * * If-Modified-Since + * * If-Unmodified-Since + * + * The method will return true if all preconditions are met + * The method will return false, or throw an exception if preconditions + * failed. If false is returned the operation should be aborted, and + * the appropriate HTTP response headers are already set. + * + * Normally this method will throw 412 Precondition Failed for failures + * related to If-None-Match, If-Match and If-Unmodified Since. It will + * set the status to 304 Not Modified for If-Modified_since. + * + * @return bool + */ + public function checkPreconditions(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $node = null; + $lastMod = null; + $etag = null; + + if ($ifMatch = $request->getHeader('If-Match')) { + // If-Match contains an entity tag. Only if the entity-tag + // matches we are allowed to make the request succeed. + // If the entity-tag is '*' we are only allowed to make the + // request succeed if a resource exists at that url. + try { + $node = $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); + } + + // Only need to check entity tags if they are not * + if ('*' !== $ifMatch) { + // There can be multiple ETags + $ifMatch = explode(',', $ifMatch); + $haveMatch = false; + foreach ($ifMatch as $ifMatchItem) { + // Stripping any extra spaces + $ifMatchItem = trim($ifMatchItem, ' '); + + $etag = $node instanceof IFile ? $node->getETag() : null; + if ($etag === $ifMatchItem) { + $haveMatch = true; + } else { + // Evolution has a bug where it sometimes prepends the " + // with a \. This is our workaround. + if (str_replace('\\"', '"', $ifMatchItem) === $etag) { + $haveMatch = true; + } + } + } + if (!$haveMatch) { + if ($etag) { + $response->setHeader('ETag', $etag); + } + throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified ETags matched.', 'If-Match'); + } + } + } + + if ($ifNoneMatch = $request->getHeader('If-None-Match')) { + // The If-None-Match header contains an ETag. + // Only if the ETag does not match the current ETag, the request will succeed + // The header can also contain *, in which case the request + // will only succeed if the entity does not exist at all. + $nodeExists = true; + if (!$node) { + try { + $node = $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + $nodeExists = false; + } + } + if ($nodeExists) { + $haveMatch = false; + if ('*' === $ifNoneMatch) { + $haveMatch = true; + } else { + // There might be multiple ETags + $ifNoneMatch = explode(',', $ifNoneMatch); + $etag = $node instanceof IFile ? $node->getETag() : null; + + foreach ($ifNoneMatch as $ifNoneMatchItem) { + // Stripping any extra spaces + $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); + + if ($etag === $ifNoneMatchItem) { + $haveMatch = true; + } + } + } + + if ($haveMatch) { + if ($etag) { + $response->setHeader('ETag', $etag); + } + if ('GET' === $request->getMethod()) { + $response->setStatus(304); + + return false; + } else { + throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); + } + } + } + } + + if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { + // The If-Modified-Since header contains a date. We + // will only return the entity if it has been changed since + // that date. If it hasn't been changed, we return a 304 + // header + // Note that this header only has to be checked if there was no If-None-Match header + // as per the HTTP spec. + $date = HTTP\parseDate($ifModifiedSince); + + if ($date) { + if (is_null($node)) { + $node = $this->tree->getNodeForPath($path); + } + $lastMod = $node->getLastModified(); + if ($lastMod) { + $lastMod = new \DateTime('@'.$lastMod); + if ($lastMod <= $date) { + $response->setStatus(304); + $response->setHeader('Last-Modified', HTTP\toDate($lastMod)); + + return false; + } + } + } + } + + if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { + // The If-Unmodified-Since will allow allow the request if the + // entity has not changed since the specified date. + $date = HTTP\parseDate($ifUnmodifiedSince); + + // We must only check the date if it's valid + if ($date) { + if (is_null($node)) { + $node = $this->tree->getNodeForPath($path); + } + $lastMod = $node->getLastModified(); + if ($lastMod) { + $lastMod = new \DateTime('@'.$lastMod); + if ($lastMod > $date) { + throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); + } + } + } + } + + // Now the hardest, the If: header. The If: header can contain multiple + // urls, ETags and so-called 'state tokens'. + // + // Examples of state tokens include lock-tokens (as defined in rfc4918) + // and sync-tokens (as defined in rfc6578). + // + // The only proper way to deal with these, is to emit events, that a + // Sync and Lock plugin can pick up. + $ifConditions = $this->getIfConditions($request); + + foreach ($ifConditions as $kk => $ifCondition) { + foreach ($ifCondition['tokens'] as $ii => $token) { + $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; + } + } + + // Plugins are responsible for validating all the tokens. + // If a plugin deemed a token 'valid', it will set 'validToken' to + // true. + $this->emit('validateTokens', [$request, &$ifConditions]); + + // Now we're going to analyze the result. + + // Every ifCondition needs to validate to true, so we exit as soon as + // we have an invalid condition. + foreach ($ifConditions as $ifCondition) { + $uri = $ifCondition['uri']; + $tokens = $ifCondition['tokens']; + + // We only need 1 valid token for the condition to succeed. + foreach ($tokens as $token) { + $tokenValid = $token['validToken'] || !$token['token']; + + $etagValid = false; + if (!$token['etag']) { + $etagValid = true; + } + // Checking the ETag, only if the token was already deemed + // valid and there is one. + if ($token['etag'] && $tokenValid) { + // The token was valid, and there was an ETag. We must + // grab the current ETag and check it. + $node = $this->tree->getNodeForPath($uri); + $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; + } + + if (($tokenValid && $etagValid) ^ $token['negate']) { + // Both were valid, so we can go to the next condition. + continue 2; + } + } + + // If we ended here, it means there was no valid ETag + token + // combination found for the current condition. This means we fail! + throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for '.$uri, 'If'); + } + + return true; + } + + /** + * This method is created to extract information from the WebDAV HTTP 'If:' header. + * + * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information + * The function will return an array, containing structs with the following keys + * + * * uri - the uri the condition applies to. + * * tokens - The lock token. another 2 dimensional array containing 3 elements + * + * Example 1: + * + * If: () + * + * Would result in: + * + * [ + * [ + * 'uri' => '/request/uri', + * 'tokens' => [ + * [ + * [ + * 'negate' => false, + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', + * 'etag' => "" + * ] + * ] + * ], + * ] + * ] + * + * Example 2: + * + * If: (Not ["Im An ETag"]) (["Another ETag"]) (Not ["Path2 ETag"]) + * + * Would result in: + * + * [ + * [ + * 'uri' => 'path', + * 'tokens' => [ + * [ + * [ + * 'negate' => true, + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', + * 'etag' => '"Im An ETag"' + * ], + * [ + * 'negate' => false, + * 'token' => '', + * 'etag' => '"Another ETag"' + * ] + * ] + * ], + * ], + * [ + * 'uri' => 'path2', + * 'tokens' => [ + * [ + * [ + * 'negate' => true, + * 'token' => '', + * 'etag' => '"Path2 ETag"' + * ] + * ] + * ], + * ], + * ] + * + * @return array + */ + public function getIfConditions(RequestInterface $request) + { + $header = $request->getHeader('If'); + if (!$header) { + return []; + } + + $matches = []; + + $regex = '/(?:\<(?P.*?)\>\s)?\((?PNot\s)?(?:\<(?P[^\>]*)\>)?(?:\s?)(?:\[(?P[^\]]*)\])?\)/im'; + preg_match_all($regex, $header, $matches, PREG_SET_ORDER); + + $conditions = []; + + foreach ($matches as $match) { + // If there was no uri specified in this match, and there were + // already conditions parsed, we add the condition to the list of + // conditions for the previous uri. + if (!$match['uri'] && count($conditions)) { + $conditions[count($conditions) - 1]['tokens'][] = [ + 'negate' => $match['not'] ? true : false, + 'token' => $match['token'], + 'etag' => isset($match['etag']) ? $match['etag'] : '', + ]; + } else { + if (!$match['uri']) { + $realUri = $request->getPath(); + } else { + $realUri = $this->calculateUri($match['uri']); + } + + $conditions[] = [ + 'uri' => $realUri, + 'tokens' => [ + [ + 'negate' => $match['not'] ? true : false, + 'token' => $match['token'], + 'etag' => isset($match['etag']) ? $match['etag'] : '', + ], + ], + ]; + } + } + + return $conditions; + } + + /** + * Returns an array with resourcetypes for a node. + * + * @return array + */ + public function getResourceTypeForNode(INode $node) + { + $result = []; + foreach ($this->resourceTypeMapping as $className => $resourceType) { + if ($node instanceof $className) { + $result[] = $resourceType; + } + } + + return $result; + } + + // }}} + // {{{ XML Readers & Writers + + /** + * Returns a callback generating a WebDAV propfind response body based on a list of nodes. + * + * If 'strip404s' is set to true, all 404 responses will be removed. + * + * @param array|\Traversable $fileProperties The list with nodes + * @param bool $strip404s + * + * @return callable|string + */ + public function generateMultiStatus($fileProperties, $strip404s = false) + { + $w = $this->xml->getWriter(); + if (self::$streamMultiStatus) { + return function () use ($fileProperties, $strip404s, $w) { + $w->openUri('php://output'); + $this->writeMultiStatus($w, $fileProperties, $strip404s); + $w->flush(); + }; + } + $w->openMemory(); + $this->writeMultiStatus($w, $fileProperties, $strip404s); + + return $w->outputMemory(); + } + + /** + * @param $fileProperties + */ + private function writeMultiStatus(Writer $w, $fileProperties, bool $strip404s) + { + $w->contextUri = $this->baseUri; + $w->startDocument(); + + $w->startElement('{DAV:}multistatus'); + + foreach ($fileProperties as $entry) { + $href = $entry['href']; + unset($entry['href']); + if ($strip404s) { + unset($entry[404]); + } + $response = new Xml\Element\Response( + ltrim($href, '/'), + $entry + ); + $w->write([ + 'name' => '{DAV:}response', + 'value' => $response, + ]); + } + $w->endElement(); + $w->endDocument(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/ServerPlugin.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/ServerPlugin.php new file mode 100644 index 0000000..70acb01 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/ServerPlugin.php @@ -0,0 +1,105 @@ + $this->getPluginName(), + 'description' => null, + 'link' => null, + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php new file mode 100644 index 0000000..a746ac7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php @@ -0,0 +1,69 @@ +server = $server; + + $server->xml->elementMap['{DAV:}share-resource'] = 'Sabre\\DAV\\Xml\\Request\\ShareResource'; + + array_push( + $server->protectedProperties, + '{DAV:}share-mode' + ); + + $server->on('method:POST', [$this, 'httpPost']); + $server->on('propFind', [$this, 'propFind']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('onBrowserPostAction', [$this, 'browserPostAction']); + } + + /** + * Updates the list of sharees on a shared resource. + * + * The sharees array is a list of people that are to be added modified + * or removed in the list of shares. + * + * @param string $path + * @param Sharee[] $sharees + */ + public function shareResource($path, array $sharees) + { + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof ISharedNode) { + throw new Forbidden('Sharing is not allowed on this node'); + } + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}share'); + } + + foreach ($sharees as $sharee) { + // We're going to attempt to get a local principal uri for a share + // href by emitting the getPrincipalByUri event. + $principal = null; + $this->server->emit('getPrincipalByUri', [$sharee->href, &$principal]); + $sharee->principal = $principal; + } + $node->updateInvites($sharees); + } + + /** + * This event is triggered when properties are requested for nodes. + * + * This allows us to inject any sharings-specific properties. + */ + public function propFind(PropFind $propFind, INode $node) + { + if ($node instanceof ISharedNode) { + $propFind->handle('{DAV:}share-access', function () use ($node) { + return new Property\ShareAccess($node->getShareAccess()); + }); + $propFind->handle('{DAV:}invite', function () use ($node) { + return new Property\Invite($node->getInvites()); + }); + $propFind->handle('{DAV:}share-resource-uri', function () use ($node) { + return new Property\Href($node->getShareResourceUri()); + }); + } + } + + /** + * We intercept this to handle POST requests on shared resources. + * + * @return bool|null + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $contentType = $request->getHeader('Content-Type'); + if (null === $contentType) { + return; + } + + // We're only interested in the davsharing content type. + if (false === strpos($contentType, 'application/davsharing+xml')) { + return; + } + + $message = $this->server->xml->parse( + $request->getBody(), + $request->getUrl(), + $documentType + ); + + switch ($documentType) { + case '{DAV:}share-resource': + + $this->shareResource($path, $message->sharees); + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + default: + throw new BadRequest('Unexpected document type: '.$documentType.' for this Content-Type'); + } + } + + /** + * This method is triggered whenever a subsystem reqeuests the privileges + * hat are supported on a particular node. + * + * We need to add a number of privileges for scheduling purposes. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + if ($node instanceof ISharedNode) { + $supportedPrivilegeSet['{DAV:}share'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin implements WebDAV resource sharing', + 'link' => 'https://github.com/evert/webdav-sharing', + ]; + } + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. + * + * @param string $output + * @param string $path + * + * @return bool|null + */ + public function htmlActionsPanel(INode $node, &$output, $path) + { + if (!$node instanceof ISharedNode) { + return; + } + + $aclPlugin = $this->server->getPlugin('acl'); + if ($aclPlugin) { + if (!$aclPlugin->checkPrivileges($path, '{DAV:}share', \Sabre\DAVACL\Plugin::R_PARENT, false)) { + // Sharing is not permitted, we will not draw this interface. + return; + } + } + + $output .= '

+

Share this resource

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

Create new principal

+ + +
+
+
+ +
+ '; + + return false; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for WebDAV ACL (rfc3744)', + 'link' => 'http://sabre.io/dav/acl/', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Principal.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Principal.php new file mode 100644 index 0000000..ada38ab --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Principal.php @@ -0,0 +1,199 @@ +principalBackend = $principalBackend; + $this->principalProperties = $principalProperties; + } + + /** + * Returns the full principal url. + * + * @return string + */ + public function getPrincipalUrl() + { + return $this->principalProperties['uri']; + } + + /** + * Returns a list of alternative urls for a principal. + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + public function getAlternateUriSet() + { + $uris = []; + if (isset($this->principalProperties['{DAV:}alternate-URI-set'])) { + $uris = $this->principalProperties['{DAV:}alternate-URI-set']; + } + + if (isset($this->principalProperties['{http://sabredav.org/ns}email-address'])) { + $uris[] = 'mailto:'.$this->principalProperties['{http://sabredav.org/ns}email-address']; + } + + return array_unique($uris); + } + + /** + * Returns the list of group members. + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + public function getGroupMemberSet() + { + return $this->principalBackend->getGroupMemberSet($this->principalProperties['uri']); + } + + /** + * Returns the list of groups this principal is member of. + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + public function getGroupMembership() + { + return $this->principalBackend->getGroupMemberShip($this->principalProperties['uri']); + } + + /** + * Sets a list of group members. + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + */ + public function setGroupMemberSet(array $groupMembers) + { + $this->principalBackend->setGroupMemberSet($this->principalProperties['uri'], $groupMembers); + } + + /** + * Returns this principals name. + * + * @return string + */ + public function getName() + { + $uri = $this->principalProperties['uri']; + list(, $name) = Uri\split($uri); + + return $name; + } + + /** + * Returns the name of the user. + * + * @return string + */ + public function getDisplayName() + { + if (isset($this->principalProperties['{DAV:}displayname'])) { + return $this->principalProperties['{DAV:}displayname']; + } else { + return $this->getName(); + } + } + + /** + * Returns a list of properties. + * + * @param array $requestedProperties + * + * @return array + */ + public function getProperties($requestedProperties) + { + $newProperties = []; + foreach ($requestedProperties as $propName) { + if (isset($this->principalProperties[$propName])) { + $newProperties[$propName] = $this->principalProperties[$propName]; + } + } + + return $newProperties; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(DAV\PropPatch $propPatch) + { + return $this->principalBackend->updatePrincipal( + $this->principalProperties['uri'], + $propPatch + ); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalProperties['uri']; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php new file mode 100644 index 0000000..03a9c4b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php @@ -0,0 +1,54 @@ +searchPrincipals( + $principalPrefix, + ['{http://sabredav.org/ns}email-address' => substr($uri, 7)] + ); + + if ($result) { + return $result[0]; + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php new file mode 100644 index 0000000..72717a5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php @@ -0,0 +1,143 @@ + [ + 'dbField' => 'displayname', + ], + + /* + * This is the users' primary email-address. + */ + '{http://sabredav.org/ns}email-address' => [ + 'dbField' => 'email', + ], + ]; + + /** + * Sets up the backend. + */ + public function __construct(\PDO $pdo) + { + $this->pdo = $pdo; + } + + /** + * Returns a list of principals based on a prefix. + * + * This prefix will often contain something like 'principals'. You are only + * expected to return principals that are in this base path. + * + * You are expected to return at least a 'uri' for every user, you can + * return any additional properties if you wish so. Common properties are: + * {DAV:}displayname + * {http://sabredav.org/ns}email-address - This is a custom SabreDAV + * field that's actualy injected in a number of other properties. If + * you have an email address, use this property. + * + * @param string $prefixPath + * + * @return array + */ + public function getPrincipalsByPrefix($prefixPath) + { + $fields = [ + 'uri', + ]; + + foreach ($this->fieldMap as $key => $value) { + $fields[] = $value['dbField']; + } + $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '.$this->tableName); + + $principals = []; + + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $prefixPath) { + continue; + } + + $principal = [ + 'uri' => $row['uri'], + ]; + foreach ($this->fieldMap as $key => $value) { + if ($row[$value['dbField']]) { + $principal[$key] = $row[$value['dbField']]; + } + } + $principals[] = $principal; + } + + return $principals; + } + + /** + * Returns a specific principal, specified by it's path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * @param string $path + * + * @return array + */ + public function getPrincipalByPath($path) + { + $fields = [ + 'id', + 'uri', + ]; + + foreach ($this->fieldMap as $key => $value) { + $fields[] = $value['dbField']; + } + $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '.$this->tableName.' WHERE uri = ?'); + $stmt->execute([$path]); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + if (!$row) { + return; + } + + $principal = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + ]; + foreach ($this->fieldMap as $key => $value) { + if ($row[$value['dbField']]) { + $principal[$key] = $row[$value['dbField']]; + } + } + + return $principal; + } + + /** + * Updates one ore more webdav properties on a principal. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $path + */ + public function updatePrincipal($path, DAV\PropPatch $propPatch) + { + $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) { + $query = 'UPDATE '.$this->tableName.' SET '; + $first = true; + + $values = []; + + foreach ($properties as $key => $value) { + $dbField = $this->fieldMap[$key]['dbField']; + + if (!$first) { + $query .= ', '; + } + $first = false; + $query .= $dbField.' = :'.$dbField; + $values[$dbField] = $value; + } + + $query .= ' WHERE uri = :uri'; + $values['uri'] = $path; + + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + return true; + }); + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return an array with full principal uri's. + * + * If somebody attempted to search on a property the backend does not + * support, you should simply return 0 results. + * + * You can also just return 0 results if you choose to not support + * searching at all, but keep in mind that this may stop certain features + * from working. + * + * @param string $prefixPath + * @param string $test + * + * @return array + */ + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') + { + if (0 == count($searchProperties)) { + return []; + } //No criteria + + $query = 'SELECT uri FROM '.$this->tableName.' WHERE '; + $values = []; + foreach ($searchProperties as $property => $value) { + switch ($property) { + case '{DAV:}displayname': + $column = 'displayname'; + break; + case '{http://sabredav.org/ns}email-address': + $column = 'email'; + break; + default: + // Unsupported property + return []; + } + if (count($values) > 0) { + $query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND '); + } + $query .= 'lower('.$column.') LIKE lower(?)'; + $values[] = '%'.$value.'%'; + } + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + $principals = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $prefixPath) { + continue; + } + + $principals[] = $row['uri']; + } + + return $principals; + } + + /** + * Finds a principal by its URI. + * + * This method may receive any type of uri, but mailto: addresses will be + * the most common. + * + * Implementation of this API is optional. It is currently used by the + * CalDAV system to find principals based on their email addresses. If this + * API is not implemented, some features may not work correctly. + * + * This method must return a relative principal path, or null, if the + * principal was not found or you refuse to find it. + * + * @param string $uri + * @param string $principalPrefix + * + * @return string + */ + public function findByUri($uri, $principalPrefix) + { + $uriParts = Uri\parse($uri); + + // Only two types of uri are supported : + // - the "mailto:" scheme with some non-empty address + // - a principals uri, in the form "principals/NAME" + // In both cases, `path` must not be empty. + if (empty($uriParts['path'])) { + return null; + } + + $uri = null; + if ('mailto' === $uriParts['scheme']) { + $query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$uriParts['path']]); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $principalPrefix) { + continue; + } + + $uri = $row['uri']; + break; //Stop on first match + } + } else { + $pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null + + if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) { + // Checking that this uri exists + $query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$uriParts['path']]); + $rows = $stmt->fetchAll(); + + if (count($rows) > 0) { + $uri = $uriParts['path']; + } + } + } + + return $uri; + } + + /** + * Returns the list of members for a group-principal. + * + * @param string $principal + * + * @return array + */ + public function getGroupMemberSet($principal) + { + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new DAV\Exception('Principal not found'); + } + $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?'); + $stmt->execute([$principal['id']]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Returns the list of groups a principal is a member of. + * + * @param string $principal + * + * @return array + */ + public function getGroupMembership($principal) + { + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new DAV\Exception('Principal not found'); + } + $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?'); + $stmt->execute([$principal['id']]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Updates the list of group members for a group principal. + * + * The principals should be passed as a list of uri's. + * + * @param string $principal + */ + public function setGroupMemberSet($principal, array $members) + { + // Grabbing the list of principal id's. + $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');'); + $stmt->execute(array_merge([$principal], $members)); + + $memberIds = []; + $principalId = null; + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($row['uri'] == $principal) { + $principalId = $row['id']; + } else { + $memberIds[] = $row['id']; + } + } + if (!$principalId) { + throw new DAV\Exception('Principal not found'); + } + // Wiping out old members + $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;'); + $stmt->execute([$principalId]); + + foreach ($memberIds as $memberId) { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);'); + $stmt->execute([$principalId, $memberId]); + } + } + + /** + * Creates a new principal. + * + * This method receives a full path for the new principal. The mkCol object + * contains any additional webdav properties specified during the creation + * of the principal. + * + * @param string $path + */ + public function createPrincipal($path, MkCol $mkCol) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)'); + $stmt->execute([$path]); + $this->updatePrincipal($path, $mkCol); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php new file mode 100644 index 0000000..b823b6c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php @@ -0,0 +1,96 @@ +principalBackend, $principal); + } + + /** + * Creates a new collection. + * + * This method will receive a MkCol object with all the information about + * the new collection that's being created. + * + * The MkCol object contains information about the resourceType of the new + * collection. If you don't support the specified resourceType, you should + * throw Exception\InvalidResourceType. + * + * The object also contains a list of WebDAV properties for the new + * collection. + * + * You should call the handle() method on this object to specify exactly + * which properties you are storing. This allows the system to figure out + * exactly which properties you didn't store, which in turn allows other + * plugins (such as the propertystorage plugin) to handle storing the + * property for you. + * + * @param string $name + * + * @throws InvalidResourceType + */ + public function createExtendedCollection($name, MkCol $mkCol) + { + if (!$mkCol->hasResourceType('{DAV:}principal')) { + throw new InvalidResourceType('Only resources of type {DAV:}principal may be created here'); + } + + $this->principalBackend->createPrincipal( + $this->principalPrefix.'/'.$name, + $mkCol + ); + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php new file mode 100644 index 0000000..c6e236d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php @@ -0,0 +1,257 @@ +privileges = $privileges; + $this->prefixBaseUrl = $prefixBaseUrl; + } + + /** + * Returns the list of privileges for this property. + * + * @return array + */ + public function getPrivileges() + { + return $this->privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->privileges as $ace) { + $this->serializeAce($writer, $ace); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + ob_start(); + echo ''; + echo ''; + foreach ($this->privileges as $privilege) { + echo ''; + // if it starts with a {, it's a special principal + if ('{' === $privilege['principal'][0]) { + echo ''; + } else { + echo ''; + } + echo ''; + echo ''; + echo ''; + } + echo '
PrincipalPrivilege
', $html->xmlName($privilege['principal']), '', $html->link($privilege['principal']), '', $html->xmlName($privilege['privilege']), ''; + if (!empty($privilege['protected'])) { + echo '(protected)'; + } + echo '
'; + + return ob_get_clean(); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elementMap = [ + '{DAV:}ace' => 'Sabre\Xml\Element\KeyValue', + '{DAV:}privilege' => 'Sabre\Xml\Element\Elements', + '{DAV:}principal' => 'Sabre\DAVACL\Xml\Property\Principal', + ]; + + $privileges = []; + + foreach ((array) $reader->parseInnerTree($elementMap) as $element) { + if ('{DAV:}ace' !== $element['name']) { + continue; + } + $ace = $element['value']; + + if (empty($ace['{DAV:}principal'])) { + throw new DAV\Exception\BadRequest('Each {DAV:}ace element must have one {DAV:}principal element'); + } + $principal = $ace['{DAV:}principal']; + + switch ($principal->getType()) { + case Principal::HREF: + $principal = $principal->getHref(); + break; + case Principal::AUTHENTICATED: + $principal = '{DAV:}authenticated'; + break; + case Principal::UNAUTHENTICATED: + $principal = '{DAV:}unauthenticated'; + break; + case Principal::ALL: + $principal = '{DAV:}all'; + break; + } + + $protected = array_key_exists('{DAV:}protected', $ace); + + if (!isset($ace['{DAV:}grant'])) { + throw new DAV\Exception\NotImplemented('Every {DAV:}ace element must have a {DAV:}grant element. {DAV:}deny is not yet supported'); + } + foreach ($ace['{DAV:}grant'] as $elem) { + if ('{DAV:}privilege' !== $elem['name']) { + continue; + } + + foreach ($elem['value'] as $priv) { + $privileges[] = [ + 'principal' => $principal, + 'protected' => $protected, + 'privilege' => $priv, + ]; + } + } + } + + return new self($privileges); + } + + /** + * Serializes a single access control entry. + */ + private function serializeAce(Writer $writer, array $ace) + { + $writer->startElement('{DAV:}ace'); + + switch ($ace['principal']) { + case '{DAV:}authenticated': + $principal = new Principal(Principal::AUTHENTICATED); + break; + case '{DAV:}unauthenticated': + $principal = new Principal(Principal::UNAUTHENTICATED); + break; + case '{DAV:}all': + $principal = new Principal(Principal::ALL); + break; + default: + $principal = new Principal(Principal::HREF, $ace['principal']); + break; + } + + $writer->writeElement('{DAV:}principal', $principal); + $writer->startElement('{DAV:}grant'); + $writer->startElement('{DAV:}privilege'); + + $writer->writeElement($ace['privilege']); + + $writer->endElement(); // privilege + $writer->endElement(); // grant + + if (!empty($ace['protected'])) { + $writer->writeElement('{DAV:}protected'); + } + + $writer->endElement(); // ace + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php new file mode 100644 index 0000000..b5629c8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php @@ -0,0 +1,42 @@ +writeElement('{DAV:}grant-only'); + $writer->writeElement('{DAV:}no-invert'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php new file mode 100644 index 0000000..e38a45c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php @@ -0,0 +1,145 @@ +privileges = $privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->privileges as $privName) { + $writer->startElement('{DAV:}privilege'); + $writer->writeElement($privName); + $writer->endElement(); + } + } + + /** + * Returns true or false, whether the specified principal appears in the + * list. + * + * @param string $privilegeName + * + * @return bool + */ + public function has($privilegeName) + { + return in_array($privilegeName, $this->privileges); + } + + /** + * Returns the list of privileges. + * + * @return array + */ + public function getValue() + { + return $this->privileges; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = []; + + $tree = $reader->parseInnerTree(['{DAV:}privilege' => 'Sabre\\Xml\\Element\\Elements']); + foreach ($tree as $element) { + if ('{DAV:}privilege' !== $element['name']) { + continue; + } + $result[] = $element['value'][0]; + } + + return new self($result); + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php new file mode 100644 index 0000000..24aeffa --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php @@ -0,0 +1,184 @@ +type = $type; + if (self::HREF === $type && is_null($href)) { + throw new DAV\Exception('The href argument must be specified for the HREF principal type.'); + } + if ($href) { + $href = rtrim($href, '/').'/'; + parent::__construct($href); + } + } + + /** + * Returns the principal type. + * + * @return int + */ + public function getType() + { + return $this->type; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->type) { + case self::UNAUTHENTICATED: + $writer->writeElement('{DAV:}unauthenticated'); + break; + case self::AUTHENTICATED: + $writer->writeElement('{DAV:}authenticated'); + break; + case self::HREF: + parent::xmlSerialize($writer); + break; + case self::ALL: + $writer->writeElement('{DAV:}all'); + break; + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + switch ($this->type) { + case self::UNAUTHENTICATED: + return 'unauthenticated'; + case self::AUTHENTICATED: + return 'authenticated'; + case self::HREF: + return parent::toHtml($html); + case self::ALL: + return 'all'; + } + } + + /** + * 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. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $tree = $reader->parseInnerTree()[0]; + + switch ($tree['name']) { + case '{DAV:}unauthenticated': + return new self(self::UNAUTHENTICATED); + case '{DAV:}authenticated': + return new self(self::AUTHENTICATED); + case '{DAV:}href': + return new self(self::HREF, $tree['value']); + case '{DAV:}all': + return new self(self::ALL); + default: + throw new BadRequest('Unknown or unsupported principal type: '.$tree['name']); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php new file mode 100644 index 0000000..6e7514b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php @@ -0,0 +1,146 @@ +privileges = $privileges; + } + + /** + * Returns the privilege value. + * + * @return array + */ + public function getValue() + { + return $this->privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $this->serializePriv($writer, '{DAV:}all', ['aggregates' => $this->privileges]); + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + $traverse = function ($privName, $priv) use (&$traverse, $html) { + echo '
  • '; + echo $html->xmlName($privName); + if (isset($priv['abstract']) && $priv['abstract']) { + echo ' (abstract)'; + } + if (isset($priv['description'])) { + echo ' '.$html->h($priv['description']); + } + if (isset($priv['aggregates'])) { + echo "\n
      \n"; + foreach ($priv['aggregates'] as $subPrivName => $subPriv) { + $traverse($subPrivName, $subPriv); + } + echo '
    '; + } + echo "
  • \n"; + }; + + ob_start(); + echo '
      '; + $traverse('{DAV:}all', ['aggregates' => $this->getValue()]); + echo "
    \n"; + + return ob_get_clean(); + } + + /** + * Serializes a property. + * + * This is a recursive function. + * + * @param string $privName + * @param array $privilege + */ + private function serializePriv(Writer $writer, $privName, $privilege) + { + $writer->startElement('{DAV:}supported-privilege'); + + $writer->startElement('{DAV:}privilege'); + $writer->writeElement($privName); + $writer->endElement(); // privilege + + if (!empty($privilege['abstract'])) { + $writer->writeElement('{DAV:}abstract'); + } + if (!empty($privilege['description'])) { + $writer->writeElement('{DAV:}description', $privilege['description']); + } + if (isset($privilege['aggregates'])) { + foreach ($privilege['aggregates'] as $subPrivName => $subPrivilege) { + $this->serializePriv($writer, $subPrivName, $subPrivilege); + } + } + + $writer->endElement(); // supported-privilege + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php new file mode 100644 index 0000000..4fc6127 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php @@ -0,0 +1,66 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\enum'; + + $elems = Deserializer\keyValue( + $reader, + 'DAV:' + ); + + $reader->popContext(); + + $report = new self(); + + if (!empty($elems['prop'])) { + $report->properties = $elems['prop']; + } + + return $report; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php new file mode 100644 index 0000000..70a7e22 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php @@ -0,0 +1,100 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree(); + + $obj = new self(); + $obj->properties = self::traverse($elems); + + return $obj; + } + + /** + * This method is used by deserializeXml, to recursively parse the + * {DAV:}property elements. + * + * @param array $elems + * + * @return array + */ + private static function traverse($elems) + { + $result = []; + + foreach ($elems as $elem) { + if ('{DAV:}property' !== $elem['name']) { + continue; + } + + $namespace = isset($elem['attributes']['namespace']) ? + $elem['attributes']['namespace'] : + 'DAV:'; + + $propName = '{'.$namespace.'}'.$elem['attributes']['name']; + + $value = null; + if (is_array($elem['value'])) { + $value = self::traverse($elem['value']); + } + + $result[$propName] = $value; + } + + return $result; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php new file mode 100644 index 0000000..b495824 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php @@ -0,0 +1,106 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\enum'; + + $elems = Deserializer\keyValue( + $reader, + 'DAV:' + ); + + $reader->popContext(); + + $principalMatch = new self(); + + if (array_key_exists('self', $elems)) { + $principalMatch->type = self::SELF; + } + + if (array_key_exists('principal-property', $elems)) { + $principalMatch->type = self::PRINCIPAL_PROPERTY; + $principalMatch->principalProperty = $elems['principal-property'][0]['name']; + } + + if (!empty($elems['prop'])) { + $principalMatch->properties = $elems['prop']; + } + + return $principalMatch; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php new file mode 100644 index 0000000..bddceca --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php @@ -0,0 +1,122 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $foundSearchProp = false; + $self->test = 'allof'; + if ('anyof' === $reader->getAttribute('test')) { + $self->test = 'anyof'; + } + + $elemMap = [ + '{DAV:}property-search' => 'Sabre\\Xml\\Element\\KeyValue', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]; + + foreach ($reader->parseInnerTree($elemMap) as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $self->properties = array_keys($elem['value']); + break; + case '{DAV:}property-search': + $foundSearchProp = true; + // This property has two sub-elements: + // {DAV:}prop - The property to be searched on. This may + // also be more than one + // {DAV:}match - The value to match with + if (!isset($elem['value']['{DAV:}prop']) || !isset($elem['value']['{DAV:}match'])) { + throw new BadRequest('The {DAV:}property-search element must contain one {DAV:}match and one {DAV:}prop element'); + } + foreach ($elem['value']['{DAV:}prop'] as $propName => $discard) { + $self->searchProperties[$propName] = $elem['value']['{DAV:}match']; + } + break; + case '{DAV:}apply-to-principal-collection-set': + $self->applyToPrincipalCollectionSet = true; + break; + } + } + if (!$foundSearchProp) { + throw new BadRequest('The {DAV:}principal-property-search report must contain at least 1 {DAV:}property-search element'); + } + + return $self; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php new file mode 100644 index 0000000..7f15d8a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php @@ -0,0 +1,58 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + if (!$reader->isEmptyElement) { + throw new BadRequest('The {DAV:}principal-search-property-set element must be empty'); + } + + // The element is actually empty, so there's not much to do. + $reader->next(); + + $self = new self(); + + return $self; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/phpstan.neon b/plugins/panakour/backup/vendor/sabre/dav/phpstan.neon new file mode 100644 index 0000000..5335bc6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + level: 0 diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractPDOTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractPDOTest.php new file mode 100644 index 0000000..9460b89 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractPDOTest.php @@ -0,0 +1,1397 @@ +dropTables([ + 'calendarobjects', + 'calendars', + 'calendarinstances', + 'calendarchanges', + 'calendarsubscriptions', + 'schedulingobjects', + ]); + $this->createSchema('calendars'); + + $this->pdo = $this->getDb(); + } + + public function testConstruct() + { + $backend = new PDO($this->pdo); + $this->assertTrue($backend instanceof PDO); + } + + /** + * @depends testConstruct + */ + public function testGetCalendarsForUserNoCalendars() + { + $backend = new PDO($this->pdo); + $calendars = $backend->getCalendarsForUser('principals/user2'); + $this->assertEquals([], $calendars); + } + + /** + * @depends testConstruct + */ + public 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->assertIsArray($calendars); + $this->assertEquals(1, count($calendars)); + + foreach ($elementCheck as $name => $value) { + $this->assertArrayHasKey($name, $calendars[0]); + $this->assertEquals($value, $calendars[0][$name]); + } + } + + /** + * @depends testConstruct + */ + public 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->assertIsArray($calendars); + $this->assertEquals(1, count($calendars)); + + foreach ($elementCheck as $name => $value) { + $this->assertArrayHasKey($name, $calendars[0]); + $this->assertEquals($value, $calendars[0][$name]); + } + } + + /** + * @depends testConstruct + */ + public function testUpdateCalendarBadId() + { + $this->expectException('InvalidArgumentException'); + $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 + */ + public 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 + */ + public 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 + */ + public function testDeleteCalendarBadID() + { + $this->expectException('InvalidArgumentException'); + $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 + */ + public function testCreateCalendarIncorrectComponentSet() + { + $this->expectException('Sabre\DAV\Exception'); + $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', + ]); + } + + public 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); + } + + public 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->assertIsInt($actual); + break; + case 'calendardata': + if (is_resource($actual)) { + $actual = stream_get_contents($actual); + } + // no break intentional + default: + $this->assertEquals($expected, $actual); + } + } + } + } + + /** + * @depends testGetMultipleObjects + */ + public function testGetMultipleObjectsBadId() + { + $this->expectException('InvalidArgumentException'); + $backend = new PDO($this->pdo); + $backend->getMultipleCalendarObjects('bad-id', ['foo-bar']); + } + + /** + * @depends testCreateCalendarObject + */ + public function testCreateCalendarObjectNoComponent() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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 + */ + public 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 + */ + public function testCreateCalendarObjectBadId() + { + $this->expectException('InvalidArgumentException'); + $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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function testGetCalendarObjectsBadId() + { + $this->expectException('InvalidArgumentException'); + $backend = new PDO($this->pdo); + $backend->getCalendarObjects('bad-id'); + } + + /** + * @depends testGetCalendarObjects + */ + public function testGetCalendarObjectBadId() + { + $this->expectException('InvalidArgumentException'); + $backend = new PDO($this->pdo); + $backend->getCalendarObject('bad-id', 'foo-bar'); + } + + /** + * @depends testCreateCalendarObject + */ + public 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 + */ + public 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 + */ + public function testUpdateCalendarObjectBadId() + { + $this->expectException('InvalidArgumentException'); + $backend = new PDO($this->pdo); + $backend->updateCalendarObject('bad-id', 'object-id', 'objectdata'); + } + + /** + * @depends testCreateCalendarObject + */ + public 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 + */ + public function testDeleteCalendarObjectBadId() + { + $this->expectException('InvalidArgumentException'); + $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'); + } + + public 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)); + } + + /** + * @depends testCalendarQueryNoResult + */ + public function testCalendarQueryBadId() + { + $this->expectException('InvalidArgumentException'); + $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); + } + + public 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)); + } + + public 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)); + } + + public 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)); + } + + public 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)); + } + + public 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)); + } + + public 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 + */ + public function testGetChangesBadId() + { + $this->expectException('InvalidArgumentException'); + $backend = new PDO($this->pdo); + $id = $backend->createCalendar( + 'principals/user1', + 'bla', + [] + ); + $backend->getChangesForCalendar('bad-id', null, 1); + } + + public 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]); + } + } + + public function testCreateSubscriptionFail() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $props = [ + ]; + + $backend = new PDO($this->pdo); + $backend->createSubscription('principals/user1', 'sub1', $props); + } + + public 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]); + } + } + + public 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()); + } + + public 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)); + } + + public function testSchedulingMethods() + { + $backend = new PDO($this->pdo); + + $calData = "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"; + + $backend->createSchedulingObject( + 'principals/user1', + 'schedule1.ics', + $calData + ); + + $calDataResource = "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"; + $stream = fopen('data://text/plain,'.$calData, 'r'); + + $backend->createSchedulingObject( + 'principals/user1', + 'schedule1-resource.ics', + $stream + ); + + $expected = [ + 'calendardata' => $calData, + 'uri' => 'schedule1.ics', + 'etag' => '"'.md5($calData).'"', + 'size' => strlen($calData), + ]; + + $expectedResource = [ + 'calendardata' => $calDataResource, + 'uri' => 'schedule1-resource.ics', + 'etag' => '"'.md5($calDataResource).'"', + 'size' => strlen($calDataResource), + ]; + + $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]); + } + + $resultResource = $backend->getSchedulingObject('principals/user1', 'schedule1-resource.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]); + } + + $backend->deleteSchedulingObject('principals/user1', 'schedule1-resource.ics'); + + $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); + } + + public 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 + */ + public function testGetInvitesBadId() + { + $this->expectException('InvalidArgumentException'); + $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 + */ + public 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 + */ + public function testUpdateInvitesBadId() + { + $this->expectException('InvalidArgumentException'); + $backend = new PDO($this->pdo); + // Add a new invite + $backend->updateInvites( + 'bad-id', + [] + ); + } + + /** + * @depends testUpdateInvites + */ + public 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->assertEqualsCanonicalizing($expected, $result); + } + + /** + * @depends testUpdateInvites + */ + public 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); + } + + public function testSetPublishStatus() + { + $this->expectException('Sabre\DAV\Exception\NotImplemented'); + $backend = new PDO($this->pdo); + $backend->setPublishStatus([1, 1], true); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractTest.php new file mode 100644 index 0000000..166de1d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractTest.php @@ -0,0 +1,184 @@ + 'anything']); + + $abstract->updateCalendar('randomid', $propPatch); + $result = $propPatch->commit(); + + $this->assertFalse($result); + } + + public 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)); + } + + public 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') + ); + } + + public 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 +{ + public function getCalendarsForUser($principalUri) + { + return [ + [ + 'id' => 1, + 'principaluri' => 'principal1', + 'uri' => 'cal1', + ], + [ + 'id' => 2, + 'principaluri' => 'principal1', + '{http://sabredav.org/ns}owner-principal' => 'principal2', + 'uri' => 'cal1', + ], + ]; + } + + public function createCalendar($principalUri, $calendarUri, array $properties) + { + } + + public function deleteCalendar($calendarId) + { + } + + public 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', + ], + ]; + } + } + + public 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", + ]; + } + } + + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + } + + public function updateCalendarObject($calendarId, $objectUri, $calendarData) + { + } + + public function deleteCalendarObject($calendarId, $objectUri) + { + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/Mock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/Mock.php new file mode 100644 index 0000000..01ac1b3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/Mock.php @@ -0,0 +1,247 @@ +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 + */ + public 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 + * + * @return string|int + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + unset($this->calendarData[$calendarId][$objectUri]); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockScheduling.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockScheduling.php new file mode 100644 index 0000000..ac799ca --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockScheduling.php @@ -0,0 +1,89 @@ +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 + */ + public 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 + */ + public 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|resource $objectData; + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockSharing.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockSharing.php new file mode 100644 index 0000000..1526e6e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockSharing.php @@ -0,0 +1,195 @@ +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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 (DAV\Sharing\Plugin::ACCESS_NOACCESS === $sharee->access) { + // 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[] + */ + public 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 + */ + public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) + { + // This operation basically doesn't do anything yet + if (DAV\Sharing\Plugin::INVITE_ACCEPTED === $status) { + return 'calendars/blabla/calendar'; + } + } + + /** + * Publishes a calendar. + * + * @param mixed $calendarId + * @param bool $value + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockSubscriptionSupport.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockSubscriptionSupport.php new file mode 100644 index 0000000..803cfbf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/MockSubscriptionSupport.php @@ -0,0 +1,153 @@ +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 + * + * @return mixed + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/PDOMySQLTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/PDOMySQLTest.php new file mode 100644 index 0000000..66388de --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Backend/PDOMySQLTest.php @@ -0,0 +1,10 @@ +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; + } + + public function testConstruct() + { + $backend = new SimplePDO($this->pdo); + $this->assertTrue($backend instanceof SimplePDO); + } + + /** + * @depends testConstruct + */ + public function testGetCalendarsForUserNoCalendars() + { + $backend = new SimplePDO($this->pdo); + $calendars = $backend->getCalendarsForUser('principals/user2'); + $this->assertEquals([], $calendars); + } + + /** + * @depends testConstruct + */ + public 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->assertIsArray($calendars); + $this->assertEquals(1, count($calendars)); + + foreach ($elementCheck as $name => $value) { + $this->assertArrayHasKey($name, $calendars[0]); + $this->assertEquals($value, $calendars[0][$name]); + } + } + + /** + * @depends testConstruct + */ + public 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 + */ + public 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); + } + + public 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)); + } + + public 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 ('lastmodified' !== $key) { + $this->assertEquals($value, $result[$index][$key]); + } else { + $this->assertTrue(isset($result[$index][$key])); + } + } + } + } + + /** + * @depends testCreateCalendarObject + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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); + } + + public 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)); + } + + public 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)); + } + + public 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)); + } + + public 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)); + } + + public 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)); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeNotificationsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeNotificationsTest.php new file mode 100644 index 0000000..a90e0d6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeNotificationsTest.php @@ -0,0 +1,44 @@ + 'principals/user']); + + $this->assertEquals( + [], + $calendarHome->getChildren() + ); + } + + public function testGetChildNoSupport() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $backend = new Backend\Mock(); + $calendarHome = new CalendarHome($backend, ['uri' => 'principals/user']); + $calendarHome->getChild('notifications'); + } + + public function testGetChildren() + { + $backend = new Backend\MockSharing(); + $calendarHome = new CalendarHome($backend, ['uri' => 'principals/user']); + + $result = $calendarHome->getChildren(); + $this->assertEquals('notifications', $result[0]->getName()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSharedCalendarsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSharedCalendarsTest.php new file mode 100644 index 0000000..2a5b214 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSharedCalendarsTest.php @@ -0,0 +1,75 @@ + 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', + ]); + } + + public function testSimple() + { + $instance = $this->getInstance(); + $this->assertEquals('user1', $instance->getName()); + } + + public 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); + } + + public function testShareReply() + { + $instance = $this->getInstance(); + $result = $instance->shareReply('uri', DAV\Sharing\Plugin::INVITE_DECLINED, 'curi', '1'); + $this->assertNull($result); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSubscriptionsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSubscriptionsTest.php new file mode 100644 index 0000000..5421678 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSubscriptionsTest.php @@ -0,0 +1,79 @@ + '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); + } + + public function testSimple() + { + $instance = $this->getInstance(); + $this->assertEquals('user1', $instance->getName()); + } + + public 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'); + } + + public 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)); + } + + public function testNoSubscriptionSupport() + { + $this->expectException('Sabre\DAV\Exception\InvalidResourceType'); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeTest.php new file mode 100644 index 0000000..64b3b87 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarHomeTest.php @@ -0,0 +1,193 @@ +backend = TestUtil::getBackend(); + $this->usercalendars = new CalendarHome($this->backend, [ + 'uri' => 'principals/user1', + ]); + } + + public function testSimple() + { + $this->assertEquals('user1', $this->usercalendars->getName()); + } + + /** + * @depends testSimple + */ + public function testGetChildNotFound() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $this->usercalendars->getChild('randomname'); + } + + public function testChildExists() + { + $this->assertFalse($this->usercalendars->childExists('foo')); + $this->assertTrue($this->usercalendars->childExists('UUID-123467')); + } + + public function testGetOwner() + { + $this->assertEquals('principals/user1', $this->usercalendars->getOwner()); + } + + public function testGetGroup() + { + $this->assertNull($this->usercalendars->getGroup()); + } + + public 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()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->usercalendars->setACL([]); + } + + /** + * @depends testSimple + */ + public function testSetName() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->usercalendars->setName('bla'); + } + + /** + * @depends testSimple + */ + public function testDelete() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->usercalendars->delete(); + } + + /** + * @depends testSimple + */ + public function testGetLastModified() + { + $this->assertNull($this->usercalendars->getLastModified()); + } + + /** + * @depends testSimple + */ + public function testCreateFile() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->usercalendars->createFile('bla'); + } + + /** + * @depends testSimple + */ + public function testCreateDirectory() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->usercalendars->createDirectory('bla'); + } + + /** + * @depends testSimple + */ + public 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)); + } + + /** + * @depends testSimple + */ + public function testCreateExtendedCollectionBadResourceType() + { + $this->expectException('Sabre\DAV\Exception\InvalidResourceType'); + $mkCol = new MkCol( + ['{DAV:}collection', '{DAV:}blabla'], + [] + ); + $this->usercalendars->createExtendedCollection('newcalendar', $mkCol); + } + + /** + * @depends testSimple + */ + public function testCreateExtendedCollectionNotACalendar() + { + $this->expectException('Sabre\DAV\Exception\InvalidResourceType'); + $mkCol = new MkCol( + ['{DAV:}collection'], + [] + ); + $this->usercalendars->createExtendedCollection('newcalendar', $mkCol); + } + + public function testGetSupportedPrivilegesSet() + { + $this->assertNull($this->usercalendars->getSupportedPrivilegeSet()); + } + + public function testShareReplyFail() + { + $this->expectException('Sabre\DAV\Exception\NotImplemented'); + $this->usercalendars->shareReply('uri', DAV\Sharing\Plugin::INVITE_DECLINED, 'curi', '1'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarObjectTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarObjectTest.php new file mode 100644 index 0000000..b7eb453 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarObjectTest.php @@ -0,0 +1,351 @@ +backend = TestUtil::getBackend(); + + $calendars = $this->backend->getCalendarsForUser('principals/user1'); + $this->assertEquals(2, count($calendars)); + $this->calendar = new Calendar($this->backend, $calendars[0]); + } + + public function teardown(): void + { + unset($this->calendar); + unset($this->backend); + } + + public function testSetup() + { + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $this->assertIsString($children[0]->getName()); + $this->assertIsString($children[0]->get()); + $this->assertIsString($children[0]->getETag()); + $this->assertEquals('text/calendar; charset=utf-8', $children[0]->getContentType()); + } + + public function testInvalidArg1() + { + $this->expectException('InvalidArgumentException'); + $obj = new CalendarObject( + new Backend\Mock([], []), + [], + [] + ); + } + + public function testInvalidArg2() + { + $this->expectException('InvalidArgumentException'); + $obj = new CalendarObject( + new Backend\Mock([], []), + [], + ['calendarid' => '1'] + ); + } + + /** + * @depends testSetup + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function testGetSize() + { + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + + $size = $obj->getSize(); + $this->assertIsInt($size); + } + + public function testGetOwner() + { + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + $this->assertEquals('principals/user1', $obj->getOwner()); + } + + public function testGetGroup() + { + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + $this->assertNull($obj->getGroup()); + } + + public 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()); + } + + public 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()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + $obj->setACL([]); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public function testGetEtag2() + { + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1, + ]; + + $backend = new Backend\Mock([], []); + $obj = new CalendarObject($backend, [], $objectInfo); + + $this->assertEquals('"'.md5('foo').'"', $obj->getETag()); + } + + public function testGetSupportedPrivilegesSet() + { + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1, + ]; + + $backend = new Backend\Mock([], []); + $obj = new CalendarObject($backend, [], $objectInfo); + $this->assertNull($obj->getSupportedPrivilegeSet()); + } + + public function testGetSize1() + { + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1, + ]; + + $backend = new Backend\Mock([], []); + $obj = new CalendarObject($backend, [], $objectInfo); + $this->assertEquals(3, $obj->getSize()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarQueryVAlarmTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarQueryVAlarmTest.php new file mode 100644 index 0000000..660832b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarQueryVAlarmTest.php @@ -0,0 +1,121 @@ +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)); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarQueryValidatorTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarQueryValidatorTest.php new file mode 100644 index 0000000..9dc8ce1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarQueryValidatorTest.php @@ -0,0 +1,823 @@ +assertFalse($validator->validate($vcal, ['name' => 'VFOO'])); + } + + /** + * @param string $icalObject + * @param array $filters + * @param int $outcome + * @dataProvider provider + */ + public 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; + } + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarTest.php new file mode 100644 index 0000000..18c3ec1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/CalendarTest.php @@ -0,0 +1,229 @@ +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]); + } + + public function teardown(): void + { + unset($this->backend); + } + + public function testSimple() + { + $this->assertEquals($this->calendars[0]['uri'], $this->calendar->getName()); + } + + /** + * @depends testSimple + */ + public 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 + */ + public 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()); + } + + /** + * @depends testSimple + */ + public function testGetChildNotFound() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $this->calendar->getChild('randomname'); + } + + /** + * @depends testSimple + */ + public function testGetChildren() + { + $children = $this->calendar->getChildren(); + $this->assertEquals(1, count($children)); + + $this->assertTrue($children[0] instanceof CalendarObject); + } + + /** + * @depends testGetChildren + */ + public function testChildExists() + { + $this->assertFalse($this->calendar->childExists('foo')); + + $children = $this->calendar->getChildren(); + $this->assertTrue($this->calendar->childExists($children[0]->getName())); + } + + public function testCreateDirectory() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->calendar->createDirectory('hello'); + } + + public function testSetName() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->calendar->setName('hello'); + } + + public function testGetLastModified() + { + $this->assertNull($this->calendar->getLastModified()); + } + + public 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); + } + + public 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); + } + + public function testDelete() + { + $this->calendar->delete(); + + $calendars = $this->backend->getCalendarsForUser('principals/user1'); + $this->assertEquals(1, count($calendars)); + } + + public function testGetOwner() + { + $this->assertEquals('principals/user1', $this->calendar->getOwner()); + } + + public function testGetGroup() + { + $this->assertNull($this->calendar->getGroup()); + } + + public 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()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->calendar->setACL([]); + } + + public function testGetSyncToken() + { + $this->assertNull($this->calendar->getSyncToken()); + } + + public function testGetSyncTokenNoSyncSupport() + { + $calendar = new Calendar(new Backend\Mock([], []), []); + $this->assertNull($calendar->getSyncToken()); + } + + public function testGetChanges() + { + $this->assertNull($this->calendar->getChanges(1, 1)); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDTest.php new file mode 100644 index 0000000..93fc56d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDTest.php @@ -0,0 +1,114 @@ + 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 +', + ], + ], + ]; + + public 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); + + $bodyAsString = $response->getBodyAsString(); + // Everts super awesome xml parser. + $body = substr( + $bodyAsString, + $start = strpos($bodyAsString, 'BEGIN:VCALENDAR'), + strpos($bodyAsString, '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 ('DTSTART' == $child->name) { + // 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 ('DTEND' == $child->name) { + // 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDbyDayTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDbyDayTest.php new file mode 100644 index 0000000..50fb6c0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDbyDayTest.php @@ -0,0 +1,104 @@ + 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 +', + ], + ], + ]; + + public 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); + + $bodyAsString = $response->getBodyAsString(); + // Everts super awesome xml parser. + $body = substr( + $bodyAsString, + $start = strpos($bodyAsString, 'BEGIN:VCALENDAR'), + strpos($bodyAsString, '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 ('DTSTART' == $child->name) { + // DTSTART has to be one of two valid values + $this->assertContains($child->getValue(), ['20120214T171500Z', '20120216T171500Z'], 'DTSTART is not a valid value: '.$child->getValue()); + } elseif ('DTEND' == $child->name) { + // 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDoubleEventsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDoubleEventsTest.php new file mode 100644 index 0000000..5e5c153 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDoubleEventsTest.php @@ -0,0 +1,104 @@ + 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 +', + ], + ], + ]; + + public 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); + + $bodyAsString = $response->getBodyAsString(); + // Everts super awesome xml parser. + $body = substr( + $bodyAsString, + $start = strpos($bodyAsString, 'BEGIN:VCALENDAR'), + strpos($bodyAsString, '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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsFloatingTimeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsFloatingTimeTest.php new file mode 100644 index 0000000..75bc963 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ExpandEventsFloatingTimeTest.php @@ -0,0 +1,210 @@ + 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 +', + ], + ], + ]; + + public function testExpandCalendarQuery() + { + $request = new HTTP\Request('REPORT', '/calendars/user1/calendar1', [ + 'Depth' => 1, + 'Content-Type' => 'application/xml', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + $bodyAsString = $response->getBodyAsString(); + // Everts super awesome xml parser. + $body = substr( + $bodyAsString, + $start = strpos($bodyAsString, 'BEGIN:VCALENDAR'), + strpos($bodyAsString, '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 ('DTSTART' == $child->name) { + // DTSTART should be the UTC equivalent of given floating time + $this->assertEquals('20141108T043000Z', $child->getValue()); + } elseif ('DTEND' == $child->name) { + // DTEND should be the UTC equivalent of given floating time + $this->assertEquals('20141108T063000Z', $child->getValue()); + } + } + } + } + + public 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()); + + $bodyAsString = $response->getBodyAsString(); + // Everts super awesome xml parser. + $body = substr( + $bodyAsString, + $start = strpos($bodyAsString, 'BEGIN:VCALENDAR'), + strpos($bodyAsString, '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 ('DTSTART' == $child->name) { + // DTSTART should be the UTC equivalent of given floating time + $this->assertEquals($child->getValue(), '20141108T043000Z'); + } elseif ('DTEND' == $child->name) { + // DTEND should be the UTC equivalent of given floating time + $this->assertEquals($child->getValue(), '20141108T063000Z'); + } + } + } + } + + public 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->getBodyAsString(), + $start = strpos($response->getBodyAsString(), 'BEGIN:VCALENDAR'), + strpos($response->getBodyAsString(), '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 ('DTSTART' == $child->name) { + // DTSTART should be the UTC equivalent of given floating time + $this->assertEquals('20141108T043000Z', $child->getValue()); + } elseif ('DTEND' == $child->name) { + // DTEND should be the UTC equivalent of given floating time + $this->assertEquals('20141108T063000Z', $child->getValue()); + } + } + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/FreeBusyReportTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/FreeBusyReportTest.php new file mode 100644 index 0000000..44823ed --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/FreeBusyReportTest.php @@ -0,0 +1,158 @@ + [ + '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 = new HTTP\Request('GET', '/calendar'); + $this->server->httpRequest = $request; + $this->server->httpResponse = new HTTP\ResponseMock(); + + $this->plugin = new Plugin(); + $this->server->addPlugin($this->plugin); + } + + public 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(false !== strpos($this->server->httpResponse->body, 'BEGIN:VFREEBUSY')); + $this->assertTrue(false !== strpos($this->server->httpResponse->body, '20111005T120000Z/20111005T130000Z')); + $this->assertTrue(false !== strpos($this->server->httpResponse->body, '20111006T100000Z/20111006T110000Z')); + } + + public function testFreeBusyReportNoTimeRange() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $reportXML = << + + +XML; + + $report = $this->server->xml->parse($reportXML, null, $rootElem); + } + + public function testFreeBusyReportWrongNode() + { + $this->expectException('Sabre\DAV\Exception\NotImplemented'); + $request = new HTTP\Request('REPORT', '/'); + $this->server->httpRequest = $request; + + $reportXML = << + + + +XML; + + $report = $this->server->xml->parse($reportXML, null, $rootElem); + $this->plugin->report($rootElem, $report, null); + } + + public function testFreeBusyReportNoACLPlugin() + { + $this->expectException('Sabre\DAV\Exception'); + $this->server = new DAV\Server(); + $this->server->httpRequest = new HTTP\Request('REPORT', '/'); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/GetEventsByTimerangeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/GetEventsByTimerangeTest.php new file mode 100644 index 0000000..e82a85d --- /dev/null +++ b/plugins/panakour/backup/vendor/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 +', + ], + ], + ]; + + public function testQueryTimerange() + { + $request = new HTTP\Request( + 'REPORT', + '/calendars/user1/calendar1', + [ + 'Content-Type' => 'application/xml', + 'Depth' => '1', + ] + ); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + $this->assertTrue(false !== strpos($response->getBodyAsString(), 'BEGIN:VCALENDAR')); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ICSExportPluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ICSExportPluginTest.php new file mode 100644 index 0000000..8771f53 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ICSExportPluginTest.php @@ -0,0 +1,366 @@ +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']); + } + + public 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->getBodyAsString()); + + $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(false !== strpos((string) $obj->PRODID, DAV\Version::VERSION)); + $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'}); + } + + public 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->getBodyAsString()); + + $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(false !== strpos((string) $obj->PRODID, DAV\Version::VERSION)); + $this->assertEquals(1, count($obj->VTIMEZONE)); + $this->assertEquals(1, count($obj->VEVENT)); + } + + public function testBeforeMethodNoExport() + { + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467' + ); + $response = new HTTP\Response(); + $this->assertNull($this->icsExportPlugin->httpGet($request, $response)); + } + + public 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); + } + + public 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->getBodyAsString()); + + $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(false !== strpos((string) $obj->PRODID, DAV\Version::VERSION)); + $this->assertEquals(1, count($obj->VTIMEZONE)); + $this->assertEquals(1, count($obj->VEVENT)); + } + + public function testBadStartParam() + { + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&start=foo' + ); + $this->request($request, 400); + } + + public function testBadEndParam() + { + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&end=foo' + ); + $this->request($request, 400); + } + + public 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->assertNull($obj->VTIMEZONE); + $this->assertNull($obj->VEVENT); + } + + public function testExpandNoStart() + { + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&expand=1&end=2' + ); + $this->request($request, 400); + } + + public 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->assertNull($obj->VTIMEZONE); + $this->assertEquals(1, count($obj->VEVENT)); + } + + public 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')); + } + + public 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')); + } + + public 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')); + } + + public function testFilterComponentVEVENT() + { + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&componentType=VEVENT' + ); + + $response = $this->request($request, 200); + + $obj = VObject\Reader::read($response->getBodyAsString()); + $this->assertEquals(1, count($obj->VTIMEZONE)); + $this->assertEquals(1, count($obj->VEVENT)); + $this->assertNull($obj->VTODO); + } + + public function testFilterComponentVTODO() + { + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&componentType=VTODO' + ); + + $response = $this->request($request, 200); + + $obj = VObject\Reader::read($response->getBodyAsString()); + + $this->assertNull($obj->VTIMEZONE); + $this->assertNull($obj->VEVENT); + $this->assertEquals(1, count($obj->VTODO)); + } + + public function testFilterComponentBadComponent() + { + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&componentType=VVOODOO' + ); + + $response = $this->request($request, 400); + } + + public 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') + ); + } + + public 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') + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue166Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue166Test.php new file mode 100644 index 0000000..02d39fe --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue172Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue172Test.php new file mode 100644 index 0000000..83120fe --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue172Test.php @@ -0,0 +1,140 @@ + '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) + public 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) + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue203Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue203Test.php new file mode 100644 index 0000000..9a786c5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue203Test.php @@ -0,0 +1,138 @@ + 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 +', + ], + ], + ]; + + public 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); + + $bodyAsString = $response->getBodyAsString(); + // Everts super awesome xml parser. + $body = substr( + $bodyAsString, + $start = strpos($bodyAsString, 'BEGIN:VCALENDAR'), + strpos($bodyAsString, '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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue205Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue205Test.php new file mode 100644 index 0000000..b021634 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue205Test.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 +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 +', + ], + ], + ]; + + public 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->getBodyAsString(), 'Exception'), 'Exception occurred: '.$response->getBodyAsString()); + $this->assertFalse(strpos($response->getBodyAsString(), 'Unknown or bad format'), 'DateTime unknown format Exception: '.$response->getBodyAsString()); + + // Everts super awesome xml parser. + $body = substr( + $response->getBodyAsString(), + $start = strpos($response->getBodyAsString(), 'BEGIN:VCALENDAR'), + strpos($response->getBodyAsString(), 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + $vObject = VObject\Reader::read($body); + + $this->assertEquals(1, count($vObject->VEVENT)); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue211Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue211Test.php new file mode 100644 index 0000000..d7fa18c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue211Test.php @@ -0,0 +1,90 @@ + 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 +', + ], + ], + ]; + + public 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->getBodyAsString())); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue220Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue220Test.php new file mode 100644 index 0000000..8e51e49 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue220Test.php @@ -0,0 +1,101 @@ + 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 +', + ], + ], + ]; + + public 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->getBodyAsString(), 'PHPUnit_Framework_Error_Warning'), 'Error Warning occurred: '.$response->getBodyAsString()); + $this->assertFalse(strpos($response->getBodyAsString(), 'Invalid argument supplied for foreach()'), 'Invalid argument supplied for foreach(): '.$response->getBodyAsString()); + + $this->assertEquals(207, $response->status); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue228Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue228Test.php new file mode 100644 index 0000000..1f698e7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Issue228Test.php @@ -0,0 +1,80 @@ + 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 +', + ], + ], + ]; + + public 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->getBodyAsString(), 'BEGIN:VCALENDAR')); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/JCalTransformTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/JCalTransformTest.php new file mode 100644 index 0000000..1125e86 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/JCalTransformTest.php @@ -0,0 +1,254 @@ + 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, + ], + ], + ]; + + public 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_ERROR_NONE !== json_last_error()) { + $this->fail('Json decoding error: '.json_last_error_msg()); + } + $this->assertEquals( + [ + 'vcalendar', + [], + [ + [ + 'vevent', + [], + [], + ], + ], + ], + $response + ); + } + + public function testMultiGet() + { + $xml = << + + + + + /calendars/user1/foo/bar.ics + +XML; + + $headers = []; + $request = new Request('REPORT', '/calendars/user1/foo', $headers, $xml); + + $response = $this->request($request); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->getStatus(), 'Full rsponse: '.$bodyAsString); + + $multiStatus = $this->server->xml->parse($bodyAsString); + + $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 + ); + } + + public function testCalendarQueryDepth1() + { + $xml = << + + + + + + + + +XML; + + $headers = [ + 'Depth' => '1', + ]; + $request = new Request('REPORT', '/calendars/user1/foo', $headers, $xml); + + $response = $this->request($request); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->getStatus(), 'Invalid response code. Full body: '.$bodyAsString); + + $multiStatus = $this->server->xml->parse($bodyAsString); + + $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 + ); + } + + public function testCalendarQueryDepth0() + { + $xml = << + + + + + + + + +XML; + + $headers = [ + 'Depth' => '0', + ]; + $request = new Request('REPORT', '/calendars/user1/foo/bar.ics', $headers, $xml); + + $response = $this->request($request); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->getStatus(), 'Invalid response code. Full body: '.$bodyAsString); + + $multiStatus = $this->server->xml->parse($bodyAsString); + + $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 + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/CollectionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/CollectionTest.php new file mode 100644 index 0000000..594241e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/CollectionTest.php @@ -0,0 +1,78 @@ +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); + } + + public function testGetChildren() + { + $col = $this->getInstance(); + $this->assertEquals('notifications', $col->getName()); + + $this->assertEquals([ + new Node($this->caldavBackend, $this->principalUri, $this->notification), + ], $col->getChildren()); + } + + public function testGetOwner() + { + $col = $this->getInstance(); + $this->assertEquals('principals/user1', $col->getOwner()); + } + + public function testGetGroup() + { + $col = $this->getInstance(); + $this->assertNull($col->getGroup()); + } + + public function testGetACL() + { + $col = $this->getInstance(); + $expected = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + + $this->assertEquals($expected, $col->getACL()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $col = $this->getInstance(); + $col->setACL([]); + } + + public function testGetSupportedPrivilegeSet() + { + $col = $this->getInstance(); + $this->assertNull($col->getSupportedPrivilegeSet()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/NodeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/NodeTest.php new file mode 100644 index 0000000..623525e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/NodeTest.php @@ -0,0 +1,88 @@ +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; + } + + public function testGetId() + { + $node = $this->getInstance(); + $this->assertEquals($this->systemStatus->getId().'.xml', $node->getName()); + } + + public function testGetEtag() + { + $node = $this->getInstance(); + $this->assertEquals('"1"', $node->getETag()); + } + + public function testGetNotificationType() + { + $node = $this->getInstance(); + $this->assertEquals($this->systemStatus, $node->getNotificationType()); + } + + public function testDelete() + { + $node = $this->getInstance(); + $node->delete(); + $this->assertEquals([], $this->caldavBackend->getNotificationsForPrincipal('principals/user1')); + } + + public function testGetGroup() + { + $node = $this->getInstance(); + $this->assertNull($node->getGroup()); + } + + public function testGetACL() + { + $node = $this->getInstance(); + $expected = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + + $this->assertEquals($expected, $node->getACL()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $node = $this->getInstance(); + $node->setACL([]); + } + + public function testGetSupportedPrivilegeSet() + { + $node = $this->getInstance(); + $this->assertNull($node->getSupportedPrivilegeSet()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/PluginTest.php new file mode 100644 index 0000000..1af5615 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Notifications/PluginTest.php @@ -0,0 +1,161 @@ +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('GET', '/'), new HTTP\Response()); + + $this->response = new HTTP\ResponseMock(); + $this->server->httpResponse = $this->response; + } + + public function testSimple() + { + $this->assertEquals([], $this->plugin->getFeatures()); + $this->assertEquals('notifications', $this->plugin->getPluginName()); + $this->assertEquals( + 'notifications', + $this->plugin->getPluginInfo()['name'] + ); + } + + public 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()); + } + + public 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') + ); + } + + public 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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/PluginTest.php new file mode 100644 index 0000000..a4f08f7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/PluginTest.php @@ -0,0 +1,1071 @@ +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('GET', '/'), new \Sabre\HTTP\Response()); + $this->server->addPlugin($authPlugin); + + // This forces a login + $authPlugin->beforeMethod(new HTTP\Request('GET', '/'), new HTTP\Response()); + + $this->response = new HTTP\ResponseMock(); + $this->server->httpResponse = $this->response; + } + + public 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'] + ); + } + + public 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->getBodyAsString()); + } + + public function testGetWithoutContentType() + { + $request = new HTTP\Request('GET', '/'); + $this->plugin->httpAfterGet($request, $this->response); + $this->assertTrue(true); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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->getBodyAsString()); + + $calendars = $this->caldavBackend->getCalendarsForUser('principals/user1'); + $this->assertEquals(3, count($calendars)); + + $newCalendar = null; + foreach ($calendars as $calendar) { + if ('NEWCALENDAR' === $calendar['uri']) { + $newCalendar = $calendar; + break; + } + } + + $this->assertIsArray($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()); + } + + public 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->getBodyAsString()); + + $calendars = $this->caldavBackend->getCalendarsForUser('principals/user1'); + $this->assertEquals(3, count($calendars)); + + $newCalendar = null; + foreach ($calendars as $calendar) { + if ('NEWCALENDAR' === $calendar['uri']) { + $newCalendar = $calendar; + break; + } + } + + $this->assertIsArray($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()); + } + + public 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); + } + + public 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()); + } + + public 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 + */ + public 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()); + } + + public 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 + */ + public 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 + */ + public 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(); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Invalid HTTP status received. Full response body: '.$bodyAsString); + + $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, $bodyAsString); + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + public function testCalendarQueryReport() + { + $body = + ''. + ''. + ''. + ' '. + ' '. + ' '. + ' '. + ''. + ''. + ' '. + ' '. + ' '. + ''. + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467', ['Depth' => '1']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: '.$bodyAsString); + + $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, $bodyAsString); + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + public 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(); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: '.$bodyAsString); + + $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, $bodyAsString); + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + public 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->getBodyAsString()); + } + + /** + * @depends testCalendarQueryReport + */ + public function testCalendarQueryReportNoCalData() + { + $body = + ''. + ''. + ''. + ' '. + ''. + ''. + ' '. + ' '. + ' '. + ''. + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467', [ + 'Depth' => '1', + ]); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $bodyAsString = $this->server->httpResponse->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: '.$bodyAsString); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $bodyAsString); + } + + /** + * @depends testCalendarQueryReport + */ + public 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->getBodyAsString()); + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + public 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(); + + $bodyAsString = $this->server->httpResponse->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: '.$bodyAsString); + + $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, $bodyAsString); + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + public 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(); + + $bodyAsString = $this->server->httpResponse->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: '.$bodyAsString); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $bodyAsString); + } + + public function testHTMLActionsPanel() + { + $output = ''; + $r = $this->server->emit('onHTMLActionsPanel', [$this->server->tree->getNodeForPath('calendars/user1'), &$output]); + $this->assertFalse($r); + + $this->assertTrue((bool) strpos($output, 'Display name')); + } + + /** + * @depends testCalendarMultiGetReport + */ + public 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->getBodyAsString()); + } + + /** + * @depends testCalendarMultiGetReport + */ + public 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->getBodyAsString()); + } + + /** + * @depends testCalendarMultiGetReport + */ + public 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->getBodyAsString()); + } + + /** + * @depends testSupportedReportSetPropertyNonCalendar + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/CollectionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/CollectionTest.php new file mode 100644 index 0000000..277de06 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyReadTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyReadTest.php new file mode 100644 index 0000000..95ff86f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyReadTest.php @@ -0,0 +1,91 @@ + 'principal/user', + ]); + $this->backend = $backend; + + return $principal; + } + + public function testGetName() + { + $i = $this->getInstance(); + $this->assertEquals('calendar-proxy-read', $i->getName()); + } + + public function testGetDisplayName() + { + $i = $this->getInstance(); + $this->assertEquals('calendar-proxy-read', $i->getDisplayName()); + } + + public function testGetLastModified() + { + $i = $this->getInstance(); + $this->assertNull($i->getLastModified()); + } + + public function testDelete() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $i = $this->getInstance(); + $i->delete(); + } + + public function testSetName() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $i = $this->getInstance(); + $i->setName('foo'); + } + + public function testGetAlternateUriSet() + { + $i = $this->getInstance(); + $this->assertEquals([], $i->getAlternateUriSet()); + } + + public function testGetPrincipalUri() + { + $i = $this->getInstance(); + $this->assertEquals('principal/user/calendar-proxy-read', $i->getPrincipalUrl()); + } + + public function testGetGroupMemberSet() + { + $i = $this->getInstance(); + $this->assertEquals([], $i->getGroupMemberSet()); + } + + public function testGetGroupMembership() + { + $i = $this->getInstance(); + $this->assertEquals([], $i->getGroupMembership()); + } + + public function testSetGroupMemberSet() + { + $i = $this->getInstance(); + $i->setGroupMemberSet(['principals/foo']); + + $expected = [ + $i->getPrincipalUrl() => ['principals/foo'], + ]; + + $this->assertEquals($expected, $this->backend->groupMembers); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyWriteTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyWriteTest.php new file mode 100644 index 0000000..df1715e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyWriteTest.php @@ -0,0 +1,39 @@ + 'principal/user', + ]); + $this->backend = $backend; + + return $principal; + } + + public function testGetName() + { + $i = $this->getInstance(); + $this->assertEquals('calendar-proxy-write', $i->getName()); + } + + public function testGetDisplayName() + { + $i = $this->getInstance(); + $this->assertEquals('calendar-proxy-write', $i->getDisplayName()); + } + + public function testGetPrincipalUri() + { + $i = $this->getInstance(); + $this->assertEquals('principal/user/calendar-proxy-write', $i->getPrincipalUrl()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/UserTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/UserTest.php new file mode 100644 index 0000000..fd079ac --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Principal/UserTest.php @@ -0,0 +1,111 @@ +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', + ]); + } + + public function testCreateFile() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $u = $this->getInstance(); + $u->createFile('test'); + } + + public function testCreateDirectory() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $u = $this->getInstance(); + $u->createDirectory('test'); + } + + public function testGetChildProxyRead() + { + $u = $this->getInstance(); + $child = $u->getChild('calendar-proxy-read'); + $this->assertInstanceOf('Sabre\\CalDAV\\Principal\\ProxyRead', $child); + } + + public function testGetChildProxyWrite() + { + $u = $this->getInstance(); + $child = $u->getChild('calendar-proxy-write'); + $this->assertInstanceOf('Sabre\\CalDAV\\Principal\\ProxyWrite', $child); + } + + public function testGetChildNotFound() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $u = $this->getInstance(); + $child = $u->getChild('foo'); + } + + public function testGetChildNotFound2() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $u = $this->getInstance(); + $child = $u->getChild('random'); + } + + public 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]); + } + + public function testChildExist() + { + $u = $this->getInstance(); + $this->assertTrue($u->childExists('calendar-proxy-read')); + $this->assertTrue($u->childExists('calendar-proxy-write')); + $this->assertFalse($u->childExists('foo')); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/DeliverNewEventTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/DeliverNewEventTest.php new file mode 100644 index 0000000..2024c5e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/DeliverNewEventTest.php @@ -0,0 +1,89 @@ +caldavBackend->createCalendar( + 'principals/user1', + 'default', + [ + ] + ); + $this->caldavBackend->createCalendar( + 'principals/user2', + 'default', + [ + ] + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php new file mode 100644 index 0000000..f09d5f6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php @@ -0,0 +1,534 @@ + '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 = new HTTP\Request('GET', '/', [ + '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); + } + + public 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) + ); + } + + public 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) + ); + } + + public 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) + ); + } + + public function testNoItipMethod() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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); + } + + public function testNoVFreeBusy() + { + $this->expectException('Sabre\DAV\Exception\NotImplemented'); + $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); + } + + public function testIncorrectOrganizer() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $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); + } + + public function testNoAttendees() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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); + } + + public function testNoDTStart() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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); + } + + public 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( + false !== strpos($this->response->getBodyAsString(), $string), + 'The response body did not contain: '.$string.'Full response: '.$this->response->getBodyAsString() + ); + } + + $this->assertTrue( + false == strpos($this->response->getBodyAsString(), 'FREEBUSY;FBTYPE=BUSY:20110101T080000Z/20110101T090000Z'), + '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. + */ + public 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( + false !== strpos($this->response->getBodyAsString(), $string), + 'The response body did not contain: '.$string.'Full response: '.$this->response->getBodyAsString() + ); + } + } + + public 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( + false !== strpos($this->response->getBodyAsString(), $string), + 'The response body did not contain: '.$string.'Full response: '.$this->response->getBodyAsString() + ); + } + } + + public 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( + false !== strpos($this->response->getBodyAsString(), $string), + 'The response body did not contain: '.$string.'Full response: '.$this->response->getBodyAsString() + ); + } + } + + public 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( + false !== strpos($this->response->getBodyAsString(), $string), + 'The response body did not contain: '.$string.'Full response: '.$this->response->getBodyAsString() + ); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/IMip/MockPlugin.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/IMip/MockPlugin.php new file mode 100644 index 0000000..cd21d65 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/IMip/MockPlugin.php @@ -0,0 +1,47 @@ +emails[] = [ + 'to' => $to, + 'subject' => $subject, + 'body' => $body, + 'headers' => $headers, + ]; + } + + public function getSentEmails() + { + return $this->emails; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/IMipPluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/IMipPluginTest.php new file mode 100644 index 0000000..e780925 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/IMipPluginTest.php @@ -0,0 +1,239 @@ +assertEquals( + 'imip', + $plugin->getPluginInfo()['name'] + ); + } + + public 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: Sender ', + 'MIME-Version: 1.0', + 'Content-Type: text/calendar; charset=UTF-8; method=REPLY', + 'X-Sabre-Version: '.\Sabre\DAV\Version::VERSION, + ], + ], + ]; + + $this->assertEquals($expected, $result); + } + + public 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); + } + + public 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' => 'Invitation: Birthday party', + 'body' => $ics, + 'headers' => [ + 'Reply-To: Sender ', + 'From: Sender ', + 'MIME-Version: 1.0', + 'Content-Type: text/calendar; charset=UTF-8; method=REQUEST', + 'X-Sabre-Version: '.\Sabre\DAV\Version::VERSION, + ], + ], + ]; + + $this->assertEquals($expected, $result); + } + + public 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: Sender ', + 'MIME-Version: 1.0', + '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)); + } + + public function schedule(Message $message) + { + $plugin = new IMip\MockPlugin('system@example.org'); + + $server = new Server(); + $server->addPlugin($plugin); + $server->emit('schedule', [$message]); + + return $plugin->getSentEmails(); + } + + public 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->assertSame('1', $message->getScheduleStatus()[0]); + } + + public function testRecipientNameIsEmail() + { + $message = new Message(); + $message->sender = 'mailto:sender@example.org'; + $message->senderName = 'Sender'; + $message->recipient = 'mailto:recipient@example.org'; + $message->recipientName = 'recipient@example.org'; + $message->method = 'REQUEST'; + + $ics = <<message = Reader::read($ics); + + $result = $this->schedule($message); + + $this->assertEquals('recipient@example.org', $result[0]['to']); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/InboxTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/InboxTest.php new file mode 100644 index 0000000..d9d49ba --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/InboxTest.php @@ -0,0 +1,133 @@ +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 + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php new file mode 100644 index 0000000..016e266 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php @@ -0,0 +1,128 @@ + 'POST', + 'REQUEST_URI' => '/notfound', + 'HTTP_CONTENT_TYPE' => 'text/calendar', + ]); + + $this->assertHTTPStatus(501, $req); + } + + public function testPostPassThruNotTextCalendar() + { + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1/outbox', + ]); + + $this->assertHTTPStatus(501, $req); + } + + public function testPostPassThruNoOutBox() + { + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars', + 'HTTP_CONTENT_TYPE' => 'text/calendar', + ]); + + $this->assertHTTPStatus(501, $req); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxTest.php new file mode 100644 index 0000000..df70fe7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxTest.php @@ -0,0 +1,47 @@ +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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginBasicTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginBasicTest.php new file mode 100644 index 0000000..31e138c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginBasicTest.php @@ -0,0 +1,37 @@ +assertEquals( + 'caldav-schedule', + $plugin->getPluginInfo()['name'] + ); + } + + public function testOptions() + { + $plugin = new Plugin(); + $expected = [ + 'calendar-auto-schedule', + 'calendar-availability', + ]; + $this->assertEquals($expected, $plugin->getFeatures()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesTest.php new file mode 100644 index 0000000..7a52cf8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesTest.php @@ -0,0 +1,143 @@ +caldavBackend->createCalendar( + 'principals/user1', + 'default', + [ + ] + ); + $this->principalBackend->addPrincipal([ + 'uri' => 'principals/user1/calendar-proxy-read', + ]); + } + + public 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()); + } + + public 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]); + } + + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesWithSharedCalendarTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesWithSharedCalendarTest.php new file mode 100644 index 0000000..2ec5689 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesWithSharedCalendarTest.php @@ -0,0 +1,69 @@ +caldavBackend->createCalendar( + 'principals/user1', + 'shared', + [ + 'share-access' => DAV\Sharing\Plugin::ACCESS_READWRITE, + ] + ); + $this->caldavBackend->createCalendar( + 'principals/user1', + 'default', + [ + ] + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/ScheduleDeliverTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/ScheduleDeliverTest.php new file mode 100644 index 0000000..c4c4060 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/ScheduleDeliverTest.php @@ -0,0 +1,644 @@ + 'principals/user1', + 'uri' => 'cal', + ], + [ + 'principaluri' => 'principals/user2', + 'uri' => 'cal', + ], + ]; + + public function setup(): void + { + $this->calendarObjectUri = '/calendars/user1/cal/object.ics'; + + parent::setUp(); + } + + public function testNewInvite() + { + $newObject = <<deliver(null, $newObject); + $this->assertItemsInInbox('user2', 1); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + } + + public function testNewOnWrongCollection() + { + $newObject = <<calendarObjectUri = '/calendars/user1/object.ics'; + $this->deliver(null, $newObject); + $this->assertItemsInInbox('user2', 0); + } + + public function testNewInviteSchedulingDisabled() + { + $newObject = <<deliver(null, $newObject, true); + $this->assertItemsInInbox('user2', 0); + } + + public function testUpdatedInvite() + { + $newObject = <<deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 1); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + } + + public function testUpdatedInviteSchedulingDisabled() + { + $newObject = <<deliver($oldObject, $newObject, true); + $this->assertItemsInInbox('user2', 0); + } + + public function testUpdatedInviteWrongPath() + { + $newObject = <<calendarObjectUri = '/calendars/user1/inbox/foo.ics'; + $this->deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 0); + } + + public function testDeletedInvite() + { + $newObject = null; + + $oldObject = <<deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 1); + } + + public 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. + */ + public function testUnbindIgnoredOnMove() + { + $newObject = null; + + $oldObject = <<deliver($oldObject, $newObject, false, 'MOVE'); + $this->assertItemsInInbox('user2', 0); + } + + public function testDeletedInviteWrongUrl() + { + $newObject = null; + + $oldObject = <<calendarObjectUri = '/calendars/user1/inbox/foo.ics'; + $this->deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 0); + } + + public 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 + ); + } + + public function testInviteUnknownUser() + { + $newObject = <<deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public function testInviteNoScheduler() + { + $newObject = <<server->removeAllListeners('schedule'); + $this->deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + } + + public function testInviteNoACLPlugin() + { + $this->setupACL = false; + parent::setUp(); + + $newObject = <<deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + } + + protected $calendarObjectUri; + + public function deliver($oldObject, &$newObject, $disableScheduling = false, $method = 'PUT') + { + $this->server->httpRequest->setMethod($method); + $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 + */ + public function putPath($path, $data) + { + list($parent, $base) = Uri\split($path); + $parentNode = $this->server->tree->getNodeForPath($parent); + + /* + if ($parentNode->childExists($base)) { + $childNode = $parentNode->getChild($base); + $childNode->put($data); + } else {*/ + $parentNode->createFile($base, $data); + //} + } + + public function assertItemsInInbox($user, $count) + { + $inboxNode = $this->server->tree->getNodeForPath('calendars/'.$user.'/inbox'); + $this->assertEquals($count, count($inboxNode->getChildren())); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php new file mode 100644 index 0000000..80b4c37 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php @@ -0,0 +1,356 @@ +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); + } + + public function teardown(): void + { + unset($this->inbox); + unset($this->backend); + } + + public function testSetup() + { + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $this->assertIsString($children[0]->getName()); + $this->assertIsString($children[0]->get()); + $this->assertIsString($children[0]->getETag()); + $this->assertEquals('text/calendar; charset=utf-8', $children[0]->getContentType()); + } + + public function testInvalidArg1() + { + $this->expectException('InvalidArgumentException'); + $obj = new SchedulingObject( + new Backend\MockScheduling([], []), + [], + [] + ); + } + + public function testInvalidArg2() + { + $this->expectException('InvalidArgumentException'); + $obj = new SchedulingObject( + new Backend\MockScheduling([], []), + [], + ['calendarid' => '1'] + ); + } + + /** + * @depends testSetup + */ + public function testPut() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $children[0]->put(''); + } + + /** + * @depends testSetup + */ + public 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 + */ + public 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 + */ + public function testGetSize() + { + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + + $size = $obj->getSize(); + $this->assertIsInt($size); + } + + public function testGetOwner() + { + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + $this->assertEquals('principals/user1', $obj->getOwner()); + } + + public function testGetGroup() + { + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + $this->assertNull($obj->getGroup()); + } + + public 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()); + } + + public 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()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + $obj->setACL([]); + } + + public function testGet() + { + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + + $this->assertEquals($this->data, $obj->get()); + } + + public 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()); + } + + public 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()); + } + + public function testGetEtag2() + { + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1, + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + + $this->assertEquals('"'.md5('foo').'"', $obj->getETag()); + } + + public function testGetSupportedPrivilegesSet() + { + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1, + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertNull($obj->getSupportedPrivilegeSet()); + } + + public function testGetSize1() + { + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1, + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertEquals(3, $obj->getSize()); + } + + public function testGetSize2() + { + $objectInfo = [ + 'uri' => 'foo', + 'calendarid' => 1, + 'size' => 4, + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertEquals(4, $obj->getSize()); + } + + public 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()); + } + + public 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()); + } + + public function testGetACL2() + { + $objectInfo = [ + 'uri' => 'foo', + 'calendarid' => 1, + 'acl' => [], + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertEquals([], $obj->getACL()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/SharedCalendarTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/SharedCalendarTest.php new file mode 100644 index 0000000..735bbef --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/SharedCalendarTest.php @@ -0,0 +1,172 @@ + 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); + } + + public 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() + ); + } + + public function testGetOwner() + { + $this->assertEquals('principals/sharee', $this->getInstance()->getOwner()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public function testPublish() + { + $instance = $this->getInstance(); + $this->assertNull($instance->setPublishStatus(true)); + $this->assertNull($instance->setPublishStatus(false)); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/SharingPluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/SharingPluginTest.php new file mode 100644 index 0000000..f11af8b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/SharingPluginTest.php @@ -0,0 +1,383 @@ +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'; + } + + public function testSimple() + { + $this->assertInstanceOf('Sabre\\CalDAV\\SharingPlugin', $this->server->getPlugin('caldav-sharing')); + $this->assertEquals( + 'caldav-sharing', + $this->caldavSharingPlugin->getPluginInfo()['name'] + ); + } + + public function testSetupWithoutCoreSharingPlugin() + { + $this->expectException('LogicException'); + $server = new DAV\Server(); + $server->addPlugin( + new SharingPlugin() + ); + } + + public function testGetFeatures() + { + $this->assertEquals(['calendarserver-sharing'], $this->caldavSharingPlugin->getFeatures()); + } + + public function testBeforeGetShareableCalendar() + { + // Forcing the server to authenticate: + $this->authPlugin->beforeMethod(new HTTP\Request('GET', '/'), 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']); + } + + public 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']); + } + + public 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))); + } + + public function testUpdatePropertiesPassThru() + { + $result = $this->server->updateProperties('calendars/user1/cal3', [ + '{DAV:}foo' => 'bar', + ]); + + $this->assertEquals([ + '{DAV:}foo' => 200, + ], $result); + } + + public function testUnknownMethodNoPOST() + { + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PATCH', + 'REQUEST_URI' => '/', + ]); + + $response = $this->request($request); + + $this->assertEquals(501, $response->status, $response->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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); + + $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') + ); + } + + public 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); + + $this->request($request, 403); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + + // 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()); + } + + public function testPostWithoutContentType() + { + $request = new HTTP\Request('POST', '/'); + $response = new HTTP\ResponseMock(); + + $this->caldavSharingPlugin->httpPost($request, $response); + $this->assertTrue(true); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public function testPublishWrongUrl() + { + $request = new HTTP\Request( + 'POST', + '/calendars/user1', + ['Content-Type' => 'text/xml'] + ); + + $xml = ' + +'; + + $request->setBody($xml); + $this->request($request, 501); + } + + public function testUnpublishWrongUrl() + { + $request = new HTTP\Request( + 'POST', + '/calendars/user1', + ['Content-Type' => 'text/xml'] + ); + $xml = ' + +'; + + $request->setBody($xml); + + $this->request($request, 501); + } + + public 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->getBodyAsString()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/CreateSubscriptionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/CreateSubscriptionTest.php new file mode 100644 index 0000000..2f5f287 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/CreateSubscriptionTest.php @@ -0,0 +1,119 @@ + + + + + + + + + + #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. + */ + public 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']) + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/PluginTest.php new file mode 100644 index 0000000..75ede4d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/PluginTest.php @@ -0,0 +1,49 @@ +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'] + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/SubscriptionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/SubscriptionTest.php new file mode 100644 index 0000000..a656322 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Subscriptions/SubscriptionTest.php @@ -0,0 +1,121 @@ + 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; + } + + public 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()); + } + + public function testValues2() + { + $sub = $this->getSub([ + 'lastmodified' => null, + ]); + + $this->assertEquals(null, $sub->getLastModified()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $sub = $this->getSub(); + $sub->setACL([]); + } + + public function testDelete() + { + $sub = $this->getSub(); + $sub->delete(); + + $this->assertEquals([], $this->backend->getSubscriptionsForUser('principals1/user1')); + } + + public 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'] + ); + } + + public function testBadConstruct() + { + $this->expectException('InvalidArgumentException'); + $caldavBackend = new \Sabre\CalDAV\Backend\MockSubscriptionSupport([], []); + new Subscription($caldavBackend, []); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/TestUtil.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/TestUtil.php new file mode 100644 index 0000000..5de11a3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/TestUtil.php @@ -0,0 +1,102 @@ +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; + } + + public 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; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ValidateICalTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ValidateICalTest.php new file mode 100644 index 0000000..4e24113 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/ValidateICalTest.php @@ -0,0 +1,392 @@ + '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; + } + + /** + * @return Sabre\HTTP\ResponseMock + */ + public function request(HTTP\Request $request) + { + $this->server->httpRequest = $request; + $this->server->exec(); + + return $this->server->httpResponse; + } + + public 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); + } + + public 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->getBodyAsString()); + $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')); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + $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')); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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); + } + + public 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')); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + /** + * 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. + */ + public 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->getBodyAsString()); + $this->assertNull($response->getHeader('ETag')); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteReplyTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteReplyTest.php new file mode 100644 index 0000000..453c211 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteReplyTest.php @@ -0,0 +1,138 @@ +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()); + } + + public 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 + ], + ]; + } + + public function testMissingArg() + { + $this->expectException('InvalidArgumentException'); + new InviteReply([]); + } + + public function testUnknownArg() + { + $this->expectException('InvalidArgumentException'); + new InviteReply([ + 'foo-i-will-break' => true, + + 'id' => 1, + 'etag' => '"bla"', + 'href' => 'abc', + 'dtStamp' => 'def', + 'inReplyTo' => 'qrs', + 'type' => 'ghi', + 'hostUrl' => 'jkl', + ]); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteTest.php new file mode 100644 index 0000000..e8111f3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteTest.php @@ -0,0 +1,157 @@ +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); + } + + public 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 + ], + ]; + } + + public function testMissingArg() + { + $this->expectException('InvalidArgumentException'); + new Invite([]); + } + + public function testUnknownArg() + { + $this->expectException('InvalidArgumentException'); + new Invite([ + 'foo-i-will-break' => true, + + 'id' => 1, + 'etag' => '"bla"', + 'href' => 'abc', + 'dtStamp' => 'def', + 'type' => 'ghi', + 'readOnly' => true, + 'hostUrl' => 'jkl', + 'organizer' => 'mno', + ]); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/SystemStatusTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/SystemStatusTest.php new file mode 100644 index 0000000..bb5f120 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/SystemStatusTest.php @@ -0,0 +1,66 @@ +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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/AllowedSharingModesTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/AllowedSharingModesTest.php new file mode 100644 index 0000000..397a267 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/AllowedSharingModesTest.php @@ -0,0 +1,37 @@ +assertInstanceOf('Sabre\CalDAV\Xml\Property\AllowedSharingModes', $sccs); + } + + /** + * @depends testSimple + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/EmailAddressSetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/EmailAddressSetTest.php new file mode 100644 index 0000000..ebcc21d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/EmailAddressSetTest.php @@ -0,0 +1,39 @@ + 'cs', + 'DAV:' => 'd', + ]; + + public function testSimple() + { + $eas = new EmailAddressSet(['foo@example.org']); + $this->assertEquals(['foo@example.org'], $eas->getValue()); + } + + /** + * @depends testSimple + */ + public function testSerialize() + { + $property = new EmailAddressSet(['foo@example.org']); + + $xml = $this->write([ + '{DAV:}root' => $property, + ]); + + $this->assertXmlStringEqualsXmlString( +' + +foo@example.org +', $xml); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/InviteTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/InviteTest.php new file mode 100644 index 0000000..2dd264b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/InviteTest.php @@ -0,0 +1,109 @@ +namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $this->namespaceMap[CalDAV\Plugin::NS_CALENDARSERVER] = 'cs'; + } + + public function testSimple() + { + $invite = new Invite([]); + $this->assertInstanceOf('Sabre\CalDAV\Xml\Property\Invite', $invite); + $this->assertEquals([], $invite->getValue()); + } + + /** + * @depends testSimple + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/ScheduleCalendarTranspTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/ScheduleCalendarTranspTest.php new file mode 100644 index 0000000..27b196b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/ScheduleCalendarTranspTest.php @@ -0,0 +1,110 @@ +namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $this->namespaceMap[CalDAV\Plugin::NS_CALENDARSERVER] = 'cs'; + } + + public function testSimple() + { + $prop = new ScheduleCalendarTransp(ScheduleCalendarTransp::OPAQUE); + $this->assertEquals( + ScheduleCalendarTransp::OPAQUE, + $prop->getValue() + ); + } + + public function testBadValue() + { + $this->expectException('InvalidArgumentException'); + new ScheduleCalendarTransp('ahhh'); + } + + /** + * @depends testSimple + */ + public function testSerializeOpaque() + { + $property = new ScheduleCalendarTransp(ScheduleCalendarTransp::OPAQUE); + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + +', $xml); + } + + /** + * @depends testSimple + */ + public function testSerializeTransparent() + { + $property = new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT); + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + +', $xml); + } + + public 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'] + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarComponentSetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarComponentSetTest.php new file mode 100644 index 0000000..8abd403 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarComponentSetTest.php @@ -0,0 +1,95 @@ +namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $this->namespaceMap[CalDAV\Plugin::NS_CALENDARSERVER] = 'cs'; + } + + public function testSimple() + { + $prop = new SupportedCalendarComponentSet(['VEVENT']); + $this->assertEquals( + ['VEVENT'], + $prop->getValue() + ); + } + + public function testMultiple() + { + $prop = new SupportedCalendarComponentSet(['VEVENT', 'VTODO']); + $this->assertEquals( + ['VEVENT', 'VTODO'], + $prop->getValue() + ); + } + + /** + * @depends testSimple + * @depends testMultiple + */ + public function testSerialize() + { + $property = new SupportedCalendarComponentSet(['VEVENT', 'VTODO']); + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + + +', $xml); + } + + public 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'] + ); + } + + public function testUnserializeEmpty() + { + $this->expectException('Sabre\Xml\ParseException'); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarDataTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarDataTest.php new file mode 100644 index 0000000..4942745 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarDataTest.php @@ -0,0 +1,35 @@ +assertInstanceOf('Sabre\CalDAV\Xml\Property\SupportedCalendarData', $sccs); + } + + /** + * @depends testSimple + */ + public function testSerialize() + { + $this->namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $property = new SupportedCalendarData(); + + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + +', $xml); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCollationSetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCollationSetTest.php new file mode 100644 index 0000000..a0807d6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCollationSetTest.php @@ -0,0 +1,36 @@ +assertInstanceOf('Sabre\CalDAV\Xml\Property\SupportedCollationSet', $scs); + } + + /** + * @depends testSimple + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/CalendarQueryReportTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/CalendarQueryReportTest.php new file mode 100644 index 0000000..2d8b48e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/CalendarQueryReportTest.php @@ -0,0 +1,348 @@ + 'Sabre\\CalDAV\\Xml\\Request\CalendarQueryReport', + ]; + + public 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'] + ); + } + + public function testDeserializeNoFilter() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + +XML; + + $this->parse($xml); + } + + public 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'] + ); + } + + public function testDeserializeDoubleTopCompFilter() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + + + + + +XML; + + $this->parse($xml); + } + + public function testDeserializeMissingExpandEnd() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + + + + +XML; + + $this->parse($xml); + } + + public function testDeserializeExpandEndBeforeStart() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + + + + +XML; + + $this->parse($xml); + } + + public function testDeserializeTimeRangeOnVCALENDAR() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + + + + +XML; + + $this->parse($xml); + } + + public function testDeserializeTimeRangeEndBeforeStart() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + + + + + + +XML; + + $this->parse($xml); + } + + public function testDeserializeTimeRangePropEndBeforeStart() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + + + + + + + + +XML; + + $this->parse($xml); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/InviteReplyTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/InviteReplyTest.php new file mode 100644 index 0000000..209e240 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/InviteReplyTest.php @@ -0,0 +1,75 @@ + 'Sabre\\CalDAV\\Xml\\Request\\InviteReply', + ]; + + public 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'] + ); + } + + public 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'] + ); + } + + public function testDeserializeNoHostUrl() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + /principal/1 + + blabla + Summary + +XML; + + $this->parse($xml); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/ShareTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/ShareTest.php new file mode 100644 index 0000000..dad9fb0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CalDAV/Xml/Request/ShareTest.php @@ -0,0 +1,82 @@ + 'Sabre\\CalDAV\\Xml\\Request\\Share', + ]; + + public 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'] + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AbstractPluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AbstractPluginTest.php new file mode 100644 index 0000000..6565fc4 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookHomeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookHomeTest.php new file mode 100644 index 0000000..88cb5c6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookHomeTest.php @@ -0,0 +1,131 @@ +backend = new Backend\Mock(); + $this->s = new AddressBookHome( + $this->backend, + 'principals/user1' + ); + } + + public function testGetName() + { + $this->assertEquals('user1', $this->s->getName()); + } + + public function testSetName() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->s->setName('user2'); + } + + public function testDelete() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->s->delete(); + } + + public function testGetLastModified() + { + $this->assertNull($this->s->getLastModified()); + } + + public function testCreateFile() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->s->createFile('bla'); + } + + public function testCreateDirectory() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->s->createDirectory('bla'); + } + + public function testGetChild() + { + $child = $this->s->getChild('book1'); + $this->assertInstanceOf('Sabre\\CardDAV\\AddressBook', $child); + $this->assertEquals('book1', $child->getName()); + } + + public function testGetChild404() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $this->s->getChild('book2'); + } + + public function testGetChildren() + { + $children = $this->s->getChildren(); + $this->assertEquals(2, count($children)); + $this->assertInstanceOf('Sabre\\CardDAV\\AddressBook', $children[0]); + $this->assertEquals('book1', $children[0]->getName()); + } + + public 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]); + } + + public function testCreateExtendedCollectionInvalid() + { + $this->expectException('Sabre\DAV\Exception\InvalidResourceType'); + $resourceType = [ + '{DAV:}collection', + ]; + $this->s->createExtendedCollection('book2', new MkCol($resourceType, ['{DAV:}displayname' => 'a-book 2'])); + } + + public 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()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->s->setACL([]); + } + + public function testGetSupportedPrivilegeSet() + { + $this->assertNull( + $this->s->getSupportedPrivilegeSet() + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookQueryTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookQueryTest.php new file mode 100644 index 0000000..a86d851 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookQueryTest.php @@ -0,0 +1,351 @@ + '1'] + ); + + $request->setBody( +' + + + + + + + +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $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); + } + + public 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(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"'.md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD").'"', + ], + ], + ], $result); + } + + public 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(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $this->assertEquals([], $result); + } + + public 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(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"'.md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD").'"', + ], + ], + ], $result); + } + + public 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(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $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); + } + + public 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(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $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); + } + + public 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->getBodyAsString()); + } + + public 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(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookRootTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookRootTest.php new file mode 100644 index 0000000..c4aff27 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookRootTest.php @@ -0,0 +1,31 @@ +assertEquals('addressbooks', $root->getName()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookTest.php new file mode 100644 index 0000000..e985c54 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/AddressBookTest.php @@ -0,0 +1,171 @@ +backend = new Backend\Mock(); + $this->ab = new AddressBook( + $this->backend, + [ + 'uri' => 'book1', + 'id' => 'foo', + '{DAV:}displayname' => 'd-name', + 'principaluri' => 'principals/user1', + ] + ); + } + + public function testGetName() + { + $this->assertEquals('book1', $this->ab->getName()); + } + + public function testGetChild() + { + $card = $this->ab->getChild('card1'); + $this->assertInstanceOf('Sabre\\CardDAV\\Card', $card); + $this->assertEquals('card1', $card->getName()); + } + + public function testGetChildNotFound() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $card = $this->ab->getChild('card3'); + } + + public function testGetChildren() + { + $cards = $this->ab->getChildren(); + $this->assertEquals(2, count($cards)); + + $this->assertEquals('card1', $cards[0]->getName()); + $this->assertEquals('card2', $cards[1]->getName()); + } + + public function testCreateDirectory() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->ab->createDirectory('name'); + } + + public 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']); + } + + public function testDelete() + { + $this->ab->delete(); + $this->assertEquals(1, count($this->backend->addressBooks)); + } + + public function testSetName() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $this->ab->setName('foo'); + } + + public function testGetLastModified() + { + $this->assertNull($this->ab->getLastModified()); + } + + public 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']); + } + + public function testGetProperties() + { + $props = $this->ab->getProperties(['{DAV:}displayname']); + $this->assertEquals([ + '{DAV:}displayname' => 'd-name', + ], $props); + } + + public 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()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->ab->setACL([]); + } + + public function testGetSupportedPrivilegeSet() + { + $this->assertNull( + $this->ab->getSupportedPrivilegeSet() + ); + } + + public function testGetSyncTokenNoSyncSupport() + { + $this->assertNull($this->ab->getSyncToken()); + } + + public function testGetChangesNoSyncSupport() + { + $this->assertNull($this->ab->getChanges(1, null)); + } + + public 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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/AbstractPDOTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/AbstractPDOTest.php new file mode 100644 index 0000000..bac3b2b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/AbstractPDOTest.php @@ -0,0 +1,351 @@ +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)"); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public function testDeleteAddressBook() + { + $this->backend->deleteAddressBook(1); + + $this->assertEquals([], $this->backend->getAddressBooksForUser('principals/user1')); + } + + public function testCreateAddressBookUnsupportedProp() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $this->backend->createAddressBook('principals/user1', 'book2', [ + '{DAV:}foo' => 'bar', + ]); + } + + public 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); + } + + public function testGetCards() + { + $result = $this->backend->getCards(1); + + $expected = [ + [ + 'id' => 1, + 'uri' => 'card1', + 'lastmodified' => 0, + 'etag' => '"'.md5('card1').'"', + 'size' => 5, + ], + ]; + + $this->assertEquals($expected, $result); + } + + public 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 + */ + public 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 + */ + public 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->assertIsInt($actual); + break; + case 'carddata': + if (is_resource($actual)) { + $actual = stream_get_contents($actual); + } + // no break intended. + default: + $this->assertEquals($expected, $actual); + break; + } + } + } + } + + /** + * @depends testGetCard + */ + public 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 + */ + public function testDeleteCard() + { + $this->backend->deleteCard(1, 'card1'); + $result = $this->backend->getCard(1, 'card1'); + $this->assertFalse($result); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/Mock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/Mock.php new file mode 100644 index 0000000..630465c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/Mock.php @@ -0,0 +1,257 @@ +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", + ], + ]; + } + } + + public 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 + */ + public 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; + }); + } + } + + public function createAddressBook($principalUri, $url, array $properties) + { + $this->addressBooks[] = array_merge($properties, [ + 'id' => $url, + 'uri' => $url, + 'principaluri' => $principalUri, + ]); + } + + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function updateCard($addressBookId, $cardUri, $cardData) + { + if (is_resource($cardData)) { + $cardData = stream_get_contents($cardData); + } + $this->cards[$addressBookId][$cardUri] = $cardData; + + return '"'.md5($cardData).'"'; + } + + public function deleteCard($addressBookId, $cardUri) + { + unset($this->cards[$addressBookId][$cardUri]); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/PDOMySQLTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/PDOMySQLTest.php new file mode 100644 index 0000000..718eec6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Backend/PDOMySQLTest.php @@ -0,0 +1,10 @@ +backend = new Backend\Mock(); + $this->card = new Card( + $this->backend, + [ + 'uri' => 'book1', + 'id' => 'foo', + 'principaluri' => 'principals/user1', + ], + [ + 'uri' => 'card1', + 'addressbookid' => 'foo', + 'carddata' => 'card', + ] + ); + } + + public function testGet() + { + $result = $this->card->get(); + $this->assertEquals('card', $result); + } + + public 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 + */ + public function testPut() + { + $file = fopen('php://memory', 'r+'); + fwrite($file, 'newdata'); + rewind($file); + $this->card->put($file); + $result = $this->card->get(); + $this->assertEquals('newdata', $result); + } + + public function testDelete() + { + $this->card->delete(); + $this->assertEquals(1, count($this->backend->cards['foo'])); + } + + public function testGetContentType() + { + $this->assertEquals('text/vcard; charset=utf-8', $this->card->getContentType()); + } + + public function testGetETag() + { + $this->assertEquals('"'.md5('card').'"', $this->card->getETag()); + } + + public 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()); + } + + public function testGetLastModified() + { + $this->assertEquals(null, $this->card->getLastModified()); + } + + public function testGetSize() + { + $this->assertEquals(4, $this->card->getSize()); + $this->assertEquals(4, $this->card->getSize()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public function testSetACL() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->card->setACL([]); + } + + public function testGetSupportedPrivilegeSet() + { + $this->assertNull( + $this->card->getSupportedPrivilegeSet() + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/IDirectoryTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/IDirectoryTest.php new file mode 100644 index 0000000..760749f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/IDirectoryTest.php @@ -0,0 +1,28 @@ +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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/MultiGetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/MultiGetTest.php new file mode 100644 index 0000000..ac0cd5e --- /dev/null +++ b/plugins/panakour/backup/vendor/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(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $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); + } + + public 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(); + + $bodyAsString = $response->getBodyAsString(); + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:'.$bodyAsString); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($bodyAsString); + + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/PluginTest.php new file mode 100644 index 0000000..b5a68dc --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/PluginTest.php @@ -0,0 +1,101 @@ +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']); + } + + public function testSupportedReportSet() + { + $this->assertEquals([ + '{'.Plugin::NS_CARDDAV.'}addressbook-multiget', + '{'.Plugin::NS_CARDDAV.'}addressbook-query', + ], $this->plugin->getSupportedReportSet('addressbooks/user1/book1')); + } + + public function testSupportedReportSetEmpty() + { + $this->assertEquals([ + ], $this->plugin->getSupportedReportSet('')); + } + + public 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()); + } + + public 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()); + } + + public function testReportPassThrough() + { + $this->assertNull($this->plugin->report('{DAV:}foo', new \DomDocument(), '')); + } + + public function testHTMLActionsPanel() + { + $output = ''; + $r = $this->server->emit('onHTMLActionsPanel', [$this->server->tree->getNodeForPath('addressbooks/user1'), &$output]); + $this->assertFalse($r); + + $this->assertTrue((bool) strpos($output, 'Display name')); + } + + public 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') + ); + } + + public 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()); + } + + public function testGetWithoutContentType() + { + $request = new \Sabre\HTTP\Request('GET', '/'); + $response = new \Sabre\HTTP\ResponseMock(); + $this->plugin->httpAfterGet($request, $response); + $this->assertTrue(true); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/SogoStripContentTypeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/SogoStripContentTypeTest.php new file mode 100644 index 0000000..8d04556 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/SogoStripContentTypeTest.php @@ -0,0 +1,67 @@ + 1, + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + ], + ]; + protected $carddavCards = [ + 1 => [ + 'card1.vcf' => "BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD", + ], + ]; + + public function testDontStrip() + { + $result = $this->server->getProperties('addressbooks/user1/book1/card1.vcf', ['{DAV:}getcontenttype']); + $this->assertEquals([ + '{DAV:}getcontenttype' => 'text/vcard; charset=utf-8', + ], $result); + } + + public function testStrip() + { + $this->server->httpRequest = new HTTP\Request('GET', '/', [ + '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); + } + + public 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')); + } + + public function testStripWithoutGetContentType() + { + $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']); + $this->carddavPlugin->propFindLate($propFind, new \Sabre\DAV\SimpleCollection('foo')); + $this->assertEquals(null, $propFind->get('{DAV:}getcontenttype')); // Property not present + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/VCFExportTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/VCFExportTest.php new file mode 100644 index 0000000..546a4cc --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/VCFExportTest.php @@ -0,0 +1,130 @@ + '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", + ], + ]; + + public function setup(): void + { + parent::setUp(); + $plugin = new VCFExportPlugin(); + $this->server->addPlugin( + $plugin + ); + } + + public function testSimple() + { + $plugin = $this->server->getPlugin('vcf-export'); + $this->assertInstanceOf('Sabre\\CardDAV\\VCFExportPlugin', $plugin); + + $this->assertEquals( + 'vcf-export', + $plugin->getPluginInfo()['name'] + ); + } + + public 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->getBodyAsString()); + + $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->getBodyAsString()); + } + + public function testBrowserIntegration() + { + $plugin = $this->server->getPlugin('vcf-export'); + $actions = ''; + $addressbook = new AddressBook($this->carddavBackend, []); + $this->server->emit('browserButtonActions', ['/foo', $addressbook, &$actions]); + $this->assertStringContainsString('/foo?export', $actions); + } + + public 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') + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/ValidateFilterTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/ValidateFilterTest.php new file mode 100644 index 0000000..de7de19 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/ValidateFilterTest.php @@ -0,0 +1,204 @@ +assertTrue($this->plugin->validateFilters($input, $filters, $test), $message); + } else { + $this->assertFalse($this->plugin->validateFilters($input, $filters, $test), $message); + } + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/ValidateVCardTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/ValidateVCardTest.php new file mode 100644 index 0000000..571cce3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/ValidateVCardTest.php @@ -0,0 +1,293 @@ + '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; + } + + public 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; + } + + public 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); + } + + public 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( + null !== $response->getHeader('ETag'), + '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 + */ + public 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 + */ + public 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); + } + + public 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( + false !== strpos($foo['carddata'], 'UID'), + print_r($foo, true) + ); + } + + public 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->getBodyAsString()); + + $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']); + } + + public 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->getBodyAsString()); + } + + public function testUpdateFile() + { + $this->cardBackend->createCard('addressbook1', 'blabla.vcf', 'foo'); + $request = new HTTP\Request( + 'PUT', + '/addressbooks/admin/addressbook1/blabla.vcf' + ); + + $response = $this->request($request, 415); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedAddressDataTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedAddressDataTest.php new file mode 100644 index 0000000..90f7761 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedAddressDataTest.php @@ -0,0 +1,37 @@ +assertInstanceOf('Sabre\CardDAV\Xml\Property\SupportedAddressData', $property); + } + + /** + * @depends testSimple + */ + public function testSerialize() + { + $property = new SupportedAddressData(); + + $this->namespaceMap[CardDAV\Plugin::NS_CARDDAV] = 'card'; + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' +'. +''. +''. +''. +' +', $xml); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedCollationSetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedCollationSetTest.php new file mode 100644 index 0000000..af5421d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedCollationSetTest.php @@ -0,0 +1,37 @@ +assertInstanceOf('Sabre\CardDAV\Xml\Property\SupportedCollationSet', $property); + } + + /** + * @depends testSimple + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookMultiGetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookMultiGetTest.php new file mode 100644 index 0000000..fa2d801 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookMultiGetTest.php @@ -0,0 +1,89 @@ + 'Sabre\\CardDAV\\Xml\\Request\AddressBookMultiGetReport', + ]; + + /** + * @dataProvider providesAddressDataXml + * + * @param $xml + */ + public function testDeserialize($xml, $expectedProps, $expectedVersion = '3.0') + { + /* lines look a bit odd but this triggers an XML parsing bug */ + $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 = $expectedVersion; + $addressBookMultiGetReport->addressDataProperties = $expectedProps; + + $this->assertEquals( + $addressBookMultiGetReport, + $result['value'] + ); + } + + public function providesAddressDataXml() + { + $simpleXml = << + + + + + + + /foo.vcf + +XML; + $allPropsXml = << + + + + + + + + + /foo.vcf + +XML; + $multiplePropsXml = << + + + + + + + + + + + + + /foo.vcf + +XML; + + return [ + 'address data with version' => [$simpleXml, [], '4.0'], + 'address data with inner all props' => [$allPropsXml, []], + 'address data with mutliple props' => [$multiplePropsXml, ['VERSION', 'UID', 'NICKNAME', 'EMAIL', 'FN']], + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookQueryReportTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookQueryReportTest.php new file mode 100644 index 0000000..65d3d8a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookQueryReportTest.php @@ -0,0 +1,333 @@ + 'Sabre\\CardDAV\\Xml\\Request\AddressBookQueryReport', + ]; + + public 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'] + ); + } + + public 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'] + ); + } + + public function testDeserializeBadTest() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + +XML; + + $this->parse($xml); + } + + /** + * We should error on this, but KDE does this, so we chose to support it. + */ + public 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'] + ); + } + + public 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'] + ); + } + + public function testDeserializeBadMatchType() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + Hello! + + + + +XML; + $this->parse($xml); + } + + public function testDeserializeBadMatchType2() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + No + + + +XML; + $this->parse($xml); + } + + public function testDeserializeDoubleFilter() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = << + + + + + + + + + +XML; + $this->parse($xml); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/AbstractServer.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/AbstractServer.php new file mode 100644 index 0000000..807b663 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/AbstractServer.php @@ -0,0 +1,62 @@ +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'); + } + + public function teardown(): void + { + $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 || '.svn' == $node || '..' == $node) { + continue; + } + $myPath = $path.'/'.$node; + if (is_file($myPath)) { + unlink($myPath); + } else { + $this->deleteTree($myPath); + } + } + if ($deleteRoot) { + rmdir($path); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBasicTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBasicTest.php new file mode 100644 index 0000000..ebc1e3f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBasicTest.php @@ -0,0 +1,90 @@ +assertFalse( + $backend->check($request, $response)[0] + ); + } + + public function testCheckUnknownUser() + { + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/', + 'PHP_AUTH_USER' => 'username', + 'PHP_AUTH_PW' => 'wrongpassword', + ]); + $response = new HTTP\Response(); + + $backend = new AbstractBasicMock(); + + $this->assertFalse( + $backend->check($request, $response)[0] + ); + } + + public function testCheckSuccess() + { + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/', + 'PHP_AUTH_USER' => 'username', + 'PHP_AUTH_PW' => 'password', + ]); + $response = new HTTP\Response(); + + $backend = new AbstractBasicMock(); + $this->assertEquals( + [true, 'principals/username'], + $backend->check($request, $response) + ); + } + + public function testRequireAuth() + { + $request = new HTTP\Request('GET', '/'); + $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", charset="UTF-8"', + $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 + */ + public function validateUserPass($username, $password) + { + return 'username' == $username && 'password' == $password; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBearerTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBearerTest.php new file mode 100644 index 0000000..423601e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBearerTest.php @@ -0,0 +1,83 @@ +assertFalse( + $backend->check($request, $response)[0] + ); + } + + public function testCheckInvalidToken() + { + $request = new HTTP\Request('GET', '/', [ + 'Authorization' => 'Bearer foo', + ]); + $response = new HTTP\Response(); + + $backend = new AbstractBearerMock(); + + $this->assertFalse( + $backend->check($request, $response)[0] + ); + } + + public function testCheckSuccess() + { + $request = new HTTP\Request('GET', '/', [ + 'Authorization' => 'Bearer valid', + ]); + $response = new HTTP\Response(); + + $backend = new AbstractBearerMock(); + $this->assertEquals( + [true, 'principals/username'], + $backend->check($request, $response) + ); + } + + public function testRequireAuth() + { + $request = new HTTP\Request('GET', '/'); + $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 + */ + public function validateBearerToken($bearerToken) + { + return 'valid' === $bearerToken ? 'principals/username' : false; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractDigestTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractDigestTest.php new file mode 100644 index 0000000..a751efd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractDigestTest.php @@ -0,0 +1,134 @@ +assertFalse( + $backend->check($request, $response)[0] + ); + } + + public function testCheckBadGetUserInfoResponse() + { + $header = 'username=null, realm=myRealm, nonce=12345, uri=/, response=HASH, opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/', + 'PHP_AUTH_DIGEST' => $header, + ]); + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $this->assertFalse( + $backend->check($request, $response)[0] + ); + } + + public function testCheckBadGetUserInfoResponse2() + { + $this->expectException('Sabre\DAV\Exception'); + $header = 'username=array, realm=myRealm, nonce=12345, uri=/, response=HASH, opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/', + 'PHP_AUTH_DIGEST' => $header, + ]); + + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $backend->check($request, $response); + } + + public function testCheckUnknownUser() + { + $header = 'username=false, realm=myRealm, nonce=12345, uri=/, response=HASH, opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/', + 'PHP_AUTH_DIGEST' => $header, + ]); + + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $this->assertFalse( + $backend->check($request, $response)[0] + ); + } + + public function testCheckBadPassword() + { + $header = 'username=user, realm=myRealm, nonce=12345, uri=/, response=HASH, opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/', + 'PHP_AUTH_DIGEST' => $header, + ]); + + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $this->assertFalse( + $backend->check($request, $response)[0] + ); + } + + public 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', + 'REQUEST_URI' => '/', + 'PHP_AUTH_DIGEST' => $header, + ]); + + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $this->assertEquals( + [true, 'principals/user'], + $backend->check($request, $response) + ); + } + + public function testRequireAuth() + { + $request = new HTTP\Request('GET', '/'); + $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 +{ + public function getDigestHash($realm, $userName) + { + switch ($userName) { + case 'null': return null; + case 'false': return false; + case 'array': return []; + case 'user': return 'HELLO'; + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractPDOTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractPDOTest.php new file mode 100644 index 0000000..8b874f8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractPDOTest.php @@ -0,0 +1,42 @@ +dropTables('users'); + $this->createSchema('users'); + + $this->getPDO()->query( + "INSERT INTO users (username,digesta1) VALUES ('user','hash')" + ); + } + + public function testConstruct() + { + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertTrue($backend instanceof PDO); + } + + /** + * @depends testConstruct + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/ApacheTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/ApacheTest.php new file mode 100644 index 0000000..a008651 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/ApacheTest.php @@ -0,0 +1,72 @@ +assertInstanceOf('Sabre\DAV\Auth\Backend\Apache', $backend); + } + + public function testNoHeader() + { + $request = new HTTP\Request('GET', '/'); + $response = new HTTP\Response(); + $backend = new Apache(); + + $this->assertFalse( + $backend->check($request, $response)[0] + ); + } + + public function testRemoteUser() + { + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/', + 'REMOTE_USER' => 'username', + ]); + $response = new HTTP\Response(); + $backend = new Apache(); + + $this->assertEquals( + [true, 'principals/username'], + $backend->check($request, $response) + ); + } + + public function testRedirectRemoteUser() + { + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/', + 'REDIRECT_REMOTE_USER' => 'username', + ]); + $response = new HTTP\Response(); + $backend = new Apache(); + + $this->assertEquals( + [true, 'principals/username'], + $backend->check($request, $response) + ); + } + + public function testRequireAuth() + { + $request = new HTTP\Request('GET', '/'); + $response = new HTTP\Response(); + + $backend = new Apache(); + $backend->challenge($request, $response); + + $this->assertNull( + $response->getHeader('WWW-Authenticate') + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/BasicCallBackTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/BasicCallBackTest.php new file mode 100644 index 0000000..1bc2de7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/BasicCallBackTest.php @@ -0,0 +1,34 @@ + 'Basic '.base64_encode('foo:bar'), + ]); + $response = new HTTP\Response(); + + $this->assertEquals( + [true, 'principals/foo'], + $backend->check($request, $response) + ); + + $this->assertEquals(['foo', 'bar'], $args); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/FileTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/FileTest.php new file mode 100644 index 0000000..31a86f9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/FileTest.php @@ -0,0 +1,38 @@ +assertTrue($file instanceof File); + } + + public function testLoadFileBroken() + { + $this->expectException('Sabre\DAV\Exception'); + file_put_contents(SABRE_TEMPDIR.'/backend', 'user:realm:hash'); + $file = new File(SABRE_TEMPDIR.'/backend'); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/IMAPTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/IMAPTest.php new file mode 100644 index 0000000..362745a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/IMAPTest.php @@ -0,0 +1,52 @@ +assertTrue($imap->validateUserPass('username', 'password')); + } + + public function testBadPassword() + { + $mailbox = '{localhost:9993}'; + $imap = new IMAPMock($mailbox); + $this->assertFalse($imap->validateUserPass('username', 'badpassword')); + } +} + +class IMAPMock extends IMAP +{ + /** + * Connects to an IMAP server and tries to authenticate. + * + * @param string $username + * @param string $password + * + * @return bool + */ + public function imapOpen($username, $password) + { + return 'username' == $username && 'password' == $password; + } + + /** + * Validates a username and password. + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $username + * @param string $password + * + * @return bool + */ + public function validateUserPass($username, $password) + { + return parent::validateUserPass($username, $password); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php new file mode 100644 index 0000000..fca7f72 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php @@ -0,0 +1,81 @@ +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] + * + * @return array + */ + public 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. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOMySQLTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOMySQLTest.php new file mode 100644 index 0000000..6ad7906 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOMySQLTest.php @@ -0,0 +1,10 @@ +assertTrue($plugin instanceof Plugin); + $fakeServer->addPlugin($plugin); + $this->assertEquals($plugin, $fakeServer->getPlugin('auth')); + $this->assertIsArray($plugin->getPluginInfo()); + } + + /** + * @depends testInit + */ + public function testAuthenticate() + { + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + $plugin = new Plugin(new Backend\Mock()); + $fakeServer->addPlugin($plugin); + $this->assertTrue( + $fakeServer->emit('beforeMethod:GET', [new HTTP\Request('GET', '/'), new HTTP\Response()]) + ); + } + + /** + * @depends testInit + */ + public function testAuthenticateFail() + { + $this->expectException('Sabre\DAV\Exception\NotAuthenticated'); + $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:GET', [new HTTP\Request('GET', '/'), new HTTP\Response()]); + } + + /** + * @depends testAuthenticateFail + */ + public 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:GET', [new HTTP\Request('GET', '/'), new HTTP\Response()]) + ); + $this->assertEquals(1, count($plugin->getLoginFailedReasons())); + } + + /** + * @depends testAuthenticate + */ + public 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:GET', [new HTTP\Request('GET', '/'), new HTTP\Response()]); + + $this->assertEquals('principals/admin', $plugin->getCurrentPrincipal()); + } + + /** + * @depends testInit + */ + public function testNoAuthBackend() + { + $this->expectException('Sabre\DAV\Exception'); + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + + $plugin = new Plugin(); + $fakeServer->addPlugin($plugin); + $fakeServer->emit('beforeMethod:GET', [new HTTP\Request('GET', '/'), new HTTP\Response()]); + } + + /** + * @depends testInit + */ + public function testInvalidCheckResponse() + { + $this->expectException('Sabre\DAV\Exception'); + $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:GET', [new HTTP\Request('GET', '/'), new HTTP\Response()]); + } + + /** + * @depends testAuthenticate + */ + public function testGetCurrentPrincipal() + { + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + $plugin = new Plugin(new Backend\Mock()); + $fakeServer->addPlugin($plugin); + $fakeServer->emit('beforeMethod:GET', [new HTTP\Request('GET', '/'), new HTTP\Response()]); + $this->assertEquals('principals/admin', $plugin->getCurrentPrincipal()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/BasicNodeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/BasicNodeTest.php new file mode 100644 index 0000000..e9a8edd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/BasicNodeTest.php @@ -0,0 +1,124 @@ +expectException('Sabre\DAV\Exception\Forbidden'); + $file = new FileMock(); + $file->put('hi'); + } + + public function testGet() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $file = new FileMock(); + $file->get(); + } + + public function testGetSize() + { + $file = new FileMock(); + $this->assertEquals(0, $file->getSize()); + } + + public function testGetETag() + { + $file = new FileMock(); + $this->assertNull($file->getETag()); + } + + public function testGetContentType() + { + $file = new FileMock(); + $this->assertNull($file->getContentType()); + } + + public function testDelete() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $file = new FileMock(); + $file->delete(); + } + + public function testSetName() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $file = new FileMock(); + $file->setName('hi'); + } + + public 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); + } + + public function testGetChild() + { + $dir = new DirectoryMock(); + $file = $dir->getChild('mockfile'); + $this->assertTrue($file instanceof FileMock); + } + + public function testChildExists() + { + $dir = new DirectoryMock(); + $this->assertTrue($dir->childExists('mockfile')); + } + + public function testChildExistsFalse() + { + $dir = new DirectoryMock(); + $this->assertFalse($dir->childExists('mockfile2')); + } + + public function testGetChild404() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $dir = new DirectoryMock(); + $file = $dir->getChild('blabla'); + } + + public function testCreateFile() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $dir = new DirectoryMock(); + $dir->createFile('hello', 'data'); + } + + public function testCreateDirectory() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $dir = new DirectoryMock(); + $dir->createDirectory('hello'); + } +} + +class DirectoryMock extends Collection +{ + public function getName() + { + return 'mockdir'; + } + + public function getChildren() + { + return [new FileMock()]; + } +} + +class FileMock extends File +{ + public function getName() + { + return 'mockfile'; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/GuessContentTypeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/GuessContentTypeTest.php new file mode 100644 index 0000000..cb4d3ce --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/GuessContentTypeTest.php @@ -0,0 +1,67 @@ +server->getPropertiesForPath('/somefile.jpg', $properties); + $this->assertArrayHasKey(0, $result); + $this->assertArrayHasKey(404, $result[0]); + $this->assertArrayHasKey('{DAV:}getcontenttype', $result[0][404]); + } + + /** + * @depends testGetProperties + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/MapGetToPropFindTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/MapGetToPropFindTest.php new file mode 100644 index 0000000..00b2661 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/MapGetToPropFindTest.php @@ -0,0 +1,40 @@ +server->addPlugin(new MapGetToPropFind()); + } + + public 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->getBodyAsString()); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/PluginTest.php new file mode 100644 index 0000000..a987525 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/PluginTest.php @@ -0,0 +1,176 @@ +server->addPlugin($this->plugin = new Plugin()); + $this->server->tree->getNodeForPath('')->createDirectory('dir2'); + } + + public 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(false !== strpos($body, 'dir'), $body); + $this->assertTrue(false !== strpos($body, '<a href="/dir/child.txt">')); + } + + /** + * Adding the If-None-Match should have 0 effect, but it threw an error. + */ + public 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(false !== strpos($body, '<title>dir'), $body); + $this->assertTrue(false !== strpos($body, '<a href="/dir/child.txt">')); + } + + public 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(false !== strpos($body, '<title>/'), $body); + $this->assertTrue(false !== strpos($body, '<a href="/dir/">')); + $this->assertTrue(false !== strpos($body, '<span class="btn disabled">')); + } + + public function testGETPassthru() + { + $request = new HTTP\Request('GET', '/random'); + $response = new HTTP\Response(); + $this->assertNull( + $this->plugin->httpGet($request, $response) + ); + } + + public function testPostOtherContentType() + { + $request = new HTTP\Request('POST', '/', ['Content-Type' => 'text/xml']); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(501, $this->response->status); + } + + public 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); + } + + public 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')); + } + + public 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->getBodyAsString()); + $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()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/PropFindAllTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/PropFindAllTest.php new file mode 100644 index 0000000..642ee42 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Browser/PropFindAllTest.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Browser; + +class PropFindAllTest extends \PHPUnit\Framework\TestCase +{ + public 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')); + } + + public 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')); + } + + public 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')); + } + + public 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')); + } + + public function testGet404Properties() + { + $pf = new PropFindAll('foo'); + $pf->set('{DAV:}displayname', null); + $this->assertEquals( + ['{DAV:}displayname'], + $pf->get404Properties() + ); + } + + public function testGet404PropertiesNothing() + { + $pf = new PropFindAll('foo'); + $pf->set('{DAV:}displayname', 'foo'); + $this->assertEquals( + ['{http://sabredav.org/ns}idk'], + $pf->get404Properties() + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ClientMock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ClientMock.php new file mode 100644 index 0000000..7d78774 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ClientMock.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class ClientMock extends Client +{ + public $request; + public $response; + + public $url; + public $curlSettings; + + /** + * Just making this method public. + * + * @param string $url + * + * @return string + */ + public function getAbsoluteUrl($url) + { + return parent::getAbsoluteUrl($url); + } + + public function doRequest(RequestInterface $request): ResponseInterface + { + $this->request = $request; + + return $this->response; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ClientTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ClientTest.php new file mode 100644 index 0000000..85a95c9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ClientTest.php @@ -0,0 +1,285 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP\Response; + +class ClientTest extends \PHPUnit\Framework\TestCase +{ + public function setup(): void + { + if (!function_exists('curl_init')) { + $this->markTestSkipped('CURL must be installed to test the client'); + } + } + + public function testConstruct() + { + $client = new ClientMock([ + 'baseUri' => '/', + ]); + $this->assertInstanceOf('Sabre\DAV\ClientMock', $client); + } + + public function testConstructNoBaseUri() + { + $this->expectException('InvalidArgumentException'); + $client = new ClientMock([]); + } + + public 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]); + } + + public 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]); + } + + public 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]); + } + + public 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]); + } + + public function testProxy() + { + $client = new ClientMock([ + 'baseUri' => '/', + 'proxy' => 'localhost:8888', + ]); + + $this->assertEquals('localhost:8888', $client->curlSettings[CURLOPT_PROXY]); + } + + public 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]); + } + + public 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()); + } + + public function testPropFindError() + { + $this->expectException('Sabre\HTTP\ClientHttpException'); + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $client->response = new Response(405, []); + $client->propFind('foo', ['{DAV:}displayname', '{urn:zim}gir']); + } + + public 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()); + } + + public 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 + */ + public function testPropPatchHTTPError() + { + $this->expectException('Sabre\HTTP\ClientHttpException'); + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $client->response = new Response(403, [], ''); + $client->propPatch('foo', ['{DAV:}displayname' => 'hi', '{urn:zim}gir' => null]); + } + + /** + * @depends testPropPatch + */ + public function testPropPatchMultiStatusError() + { + $this->expectException('Sabre\HTTP\ClientException'); + $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]); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/CorePluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/CorePluginTest.php new file mode 100644 index 0000000..8c04a38 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/CorePluginTest.php @@ -0,0 +1,14 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class CorePluginTest extends \PHPUnit\Framework\TestCase +{ + public function testGetInfo() + { + $corePlugin = new CorePlugin(); + $this->assertEquals('core', $corePlugin->getPluginInfo()['name']); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/DbTestHelperTrait.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/DbTestHelperTrait.php new file mode 100644 index 0000000..31323ac --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/DbTestHelperTrait.php @@ -0,0 +1,126 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use PDO; +use PDOException; + +class DbCache +{ + public static $cache = []; +} + +trait DbTestHelperTrait +{ + /** + * Should be "mysql", "pgsql", "sqlite". + */ + public $driver = null; + + /** + * Returns a fully configured PDO object. + * + * @return PDO + */ + public 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 (null === $pdo) { + $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 + */ + public function getPDO() + { + return $this->getDb(); + } + + /** + * Uses .sql files from the examples directory to initialize the database. + * + * @param string $schemaName + */ + public 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 + */ + public function dropTables($tableNames) + { + $tableNames = (array) $tableNames; + $db = $this->getDb(); + foreach ($tableNames as $tableName) { + $db->exec('DROP TABLE IF EXISTS '.$tableName); + } + } + + public function tearDown(): void + { + switch ($this->driver) { + case 'sqlite': + // Recreating sqlite, just in case + unset(DbCache::$cache[$this->driver]); + unlink(SABRE_TEMPDIR.'/testdb'); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/LockedTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/LockedTest.php new file mode 100644 index 0000000..5fc2715 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/LockedTest.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Exception; + +use DOMDocument; +use Sabre\DAV; + +class LockedTest extends \PHPUnit\Framework\TestCase +{ + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/PaymentRequiredTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/PaymentRequiredTest.php new file mode 100644 index 0000000..42775a3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/PaymentRequiredTest.php @@ -0,0 +1,14 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Exception; + +class PaymentRequiredTest extends \PHPUnit\Framework\TestCase +{ + public function testGetHTTPCode() + { + $ex = new PaymentRequired(); + $this->assertEquals(402, $ex->getHTTPCode()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/ServiceUnavailableTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/ServiceUnavailableTest.php new file mode 100644 index 0000000..3520b62 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/ServiceUnavailableTest.php @@ -0,0 +1,14 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Exception; + +class ServiceUnavailableTest extends \PHPUnit\Framework\TestCase +{ + public function testGetHTTPCode() + { + $ex = new ServiceUnavailable(); + $this->assertEquals(503, $ex->getHTTPCode()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/TooManyMatchesTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/TooManyMatchesTest.php new file mode 100644 index 0000000..159d1a9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Exception/TooManyMatchesTest.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Exception; + +use DOMDocument; +use Sabre\DAV; + +class TooManyMatchesTest extends \PHPUnit\Framework\TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ExceptionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ExceptionTest.php new file mode 100644 index 0000000..7237aea --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ExceptionTest.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class ExceptionTest extends \PHPUnit\Framework\TestCase +{ + public function testStatus() + { + $e = new Exception(); + $this->assertEquals(500, $e->getHTTPCode()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FS/NodeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FS/NodeTest.php new file mode 100644 index 0000000..7931a81 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FS/NodeTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\DAV\FS; + +/** + * This is a test for the Node class. We're actually using the File class to + * test it, as it doesn't override it and we can construct it as it's + * non-abstract. + */ +class NodeTest extends \PHPUnit\Framework\TestCase +{ + public function testConstruct() + { + $node = new File(__FILE__); + $this->assertEquals('NodeTest.php', $node->getName()); + } + + public function testConstructOverrideName() + { + $node = new File(__FILE__, 'foo.txt'); + $this->assertEquals('foo.txt', $node->getName()); + } + + public function testOverrideNameSetName() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $node = new File(__FILE__, 'foo.txt'); + $node->setName('foo2.txt'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/DirectoryTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/DirectoryTest.php new file mode 100644 index 0000000..0deb831 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/DirectoryTest.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\FSExt; + +class DirectoryTest extends \PHPUnit\Framework\TestCase +{ + public function create() + { + return new Directory(SABRE_TEMPDIR); + } + + public function testCreate() + { + $dir = $this->create(); + $this->assertEquals(basename(SABRE_TEMPDIR), $dir->getName()); + } + + public function testChildExistDot() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $dir = $this->create(); + $dir->childExists('..'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/FileTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/FileTest.php new file mode 100644 index 0000000..2b759e5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/FileTest.php @@ -0,0 +1,99 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\FSExt; + +class FileTest extends \PHPUnit\Framework\TestCase +{ + public function setup(): void + { + file_put_contents(SABRE_TEMPDIR.'/file.txt', 'Contents'); + } + + public function teardown(): void + { + \Sabre\TestUtil::clearTempDir(); + } + + public 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 + ); + } + + public 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')); + } + + public 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')); + } + + public function testGet() + { + $file = new File(SABRE_TEMPDIR.'/file.txt'); + $this->assertEquals('Contents', stream_get_contents($file->get())); + } + + public function testDelete() + { + $file = new File(SABRE_TEMPDIR.'/file.txt'); + $file->delete(); + + $this->assertFalse(file_exists(SABRE_TEMPDIR.'/file.txt')); + } + + public function testGetETag() + { + $filename = SABRE_TEMPDIR.'/file.txt'; + $file = new File($filename); + $this->assertEquals( + '"'. + sha1( + fileinode($filename). + filesize($filename). + filemtime($filename) + ).'"', + $file->getETag() + ); + } + + public function testGetContentType() + { + $file = new File(SABRE_TEMPDIR.'/file.txt'); + $this->assertNull($file->getContentType()); + } + + public function testGetSize() + { + $file = new File(SABRE_TEMPDIR.'/file.txt'); + $this->assertEquals(8, $file->getSize()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/ServerTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/ServerTest.php new file mode 100644 index 0000000..79ffb01 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/FSExt/ServerTest.php @@ -0,0 +1,252 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\FSExt; + +use Sabre\DAV; +use Sabre\HTTP; + +class ServerTest extends DAV\AbstractServer +{ + protected function getRootNode() + { + return new Directory($this->tempDir); + } + + public 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\toDate(new \DateTime('@'.filemtime($filename)))], + 'ETag' => ['"'.sha1(fileinode($filename).filesize($filename).filemtime($filename)).'"'], + ], + $this->response->getHeaders() + ); + + $this->assertEquals('Test contents', $this->response->getBodyAsString()); + } + + public 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\toDate(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->getBodyAsString()); + } + + public 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->getBodyAsString()); + $this->assertEquals('Testing new file', file_get_contents($filename)); + } + + public 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')); + } + + public 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->getBodyAsString()); + $this->assertTrue(is_dir($this->tempDir.'/testcol')); + } + + public 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->getBodyAsString()); + $this->assertEquals('Testing updated file', file_get_contents($this->tempDir.'/test.txt')); + } + + public 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->getBodyAsString()); + $this->assertFalse(file_exists($this->tempDir.'/test.txt')); + } + + public 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->getBodyAsString()); + $this->assertFalse(file_exists($this->tempDir.'/testcol')); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + + $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. + */ + public 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->getBodyAsString()); + + $this->assertEquals([ + 'Content-Length' => ['0'], + 'X-Sabre-Version' => [DAV\Version::VERSION], + ], $this->response->getHeaders()); + + $this->assertTrue( + is_dir($this->tempDir.'/tree2/tree1') + ); + } + + public function testCopy() + { + mkdir($this->tempDir.'/testcol'); + + $request = new HTTP\Request('COPY', '/test.txt', ['Destination' => '/testcol/test2.txt']); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(201, $this->response->status); + $this->assertEquals('', $this->response->getBodyAsString()); + + $this->assertEquals([ + 'Content-Length' => ['0'], + 'X-Sabre-Version' => [DAV\Version::VERSION], + ], $this->response->getHeaders()); + + $this->assertTrue(is_file($this->tempDir.'/test.txt')); + $this->assertTrue(is_file($this->tempDir.'/testcol/test2.txt')); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/GetIfConditionsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/GetIfConditionsTest.php new file mode 100644 index 0000000..83367de --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/GetIfConditionsTest.php @@ -0,0 +1,289 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class GetIfConditionsTest extends AbstractServer +{ + public function testNoConditions() + { + $request = new HTTP\Request('GET', '/foo'); + + $conditions = $this->server->getIfConditions($request); + $this->assertEquals([], $conditions); + } + + public 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); + } + + public function testNotLockToken() + { + $request = new HTTP\Request('GET', '/bla', [ + 'If' => '(Not <opaquelocktoken:token1>)', + ]); + + $conditions = $this->server->getIfConditions($request); + + $compare = [ + [ + 'uri' => 'bla', + 'tokens' => [ + [ + 'negate' => true, + 'token' => 'opaquelocktoken:token1', + 'etag' => '', + ], + ], + ], + ]; + $this->assertEquals($compare, $conditions); + } + + public function testLockTokenUrl() + { + $request = new HTTP\Request('GET', '/bla', [ + 'If' => '<http://www.example.com/> (<opaquelocktoken:token1>)', + ]); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + [ + 'uri' => '', + 'tokens' => [ + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token1', + 'etag' => '', + ], + ], + ], + ]; + $this->assertEquals($compare, $conditions); + } + + public function test2LockTokens() + { + $request = new HTTP\Request('GET', '/bla', [ + 'If' => '(<opaquelocktoken:token1>) (Not <opaquelocktoken:token2>)', + ]); + + $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); + } + + public function test2UriLockTokens() + { + $request = new HTTP\Request('GET', '/bla', [ + 'If' => '<http://www.example.org/node1> (<opaquelocktoken:token1>) <http://www.example.org/node2> (Not <opaquelocktoken:token2>)', + ]); + + $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); + } + + public function test2UriMultiLockTokens() + { + $request = new HTTP\Request('GET', '/bla', [ + 'If' => '<http://www.example.org/node1> (<opaquelocktoken:token1>) (<opaquelocktoken:token2>) <http://www.example.org/node2> (Not <opaquelocktoken:token3>)', + ]); + + $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); + } + + public function testEtag() + { + $request = new HTTP\Request('GET', '/foo', [ + 'If' => '(["etag1"])', + ]); + + $conditions = $this->server->getIfConditions($request); + + $compare = [ + [ + 'uri' => 'foo', + 'tokens' => [ + [ + 'negate' => false, + 'token' => '', + 'etag' => '"etag1"', + ], + ], + ], + ]; + $this->assertEquals($compare, $conditions); + } + + public function test2Etags() + { + $request = new HTTP\Request('GET', '/foo', [ + 'If' => '<http://www.example.org/> (["etag1"]) (["etag2"])', + ]); + + $conditions = $this->server->getIfConditions($request); + + $compare = [ + [ + 'uri' => '', + 'tokens' => [ + [ + 'negate' => false, + 'token' => '', + 'etag' => '"etag1"', + ], + [ + 'negate' => false, + 'token' => '', + 'etag' => '"etag2"', + ], + ], + ], + ]; + $this->assertEquals($compare, $conditions); + } + + public function testComplexIf() + { + $request = new HTTP\Request('GET', '/foo', [ + 'If' => '<http://www.example.org/node1> (<opaquelocktoken:token1> ["etag1"]) '. + '(Not <opaquelocktoken:token2>) (["etag2"]) <http://www.example.org/node2> '. + '(<opaquelocktoken:token3>) (Not <opaquelocktoken:token4>) (["etag3"])', + ]); + + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HTTPPreferParsingTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HTTPPreferParsingTest.php new file mode 100644 index 0000000..7d68256 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HTTPPreferParsingTest.php @@ -0,0 +1,175 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class HTTPPreferParsingTest extends \Sabre\DAVServerTest +{ + public function assertParseResult($input, $expected) + { + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'Prefer' => $input, + ]); + + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals( + $expected, + $server->getHTTPPrefer() + ); + } + + public function testParseSimple() + { + $this->assertParseResult( + 'return-asynch', + [ + 'respond-async' => true, + 'return' => null, + 'handling' => null, + 'wait' => null, + ] + ); + } + + public function testParseValue() + { + $this->assertParseResult( + 'wait=10', + [ + 'respond-async' => false, + 'return' => null, + 'handling' => null, + 'wait' => '10', + ] + ); + } + + public function testParseMultiple() + { + $this->assertParseResult( + 'return-minimal, strict,lenient', + [ + 'respond-async' => false, + 'return' => 'minimal', + 'handling' => 'lenient', + 'wait' => null, + ] + ); + } + + public function testParseWeirdValue() + { + $this->assertParseResult( + 'BOOOH', + [ + 'respond-async' => false, + 'return' => null, + 'handling' => null, + 'wait' => null, + 'boooh' => true, + ] + ); + } + + public function testBrief() + { + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'Brief' => 't', + ]); + + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals([ + 'respond-async' => false, + 'return' => 'minimal', + 'handling' => null, + 'wait' => null, + ], $server->getHTTPPrefer()); + } + + /** + * propfindMinimal. + */ + public function testpropfindMinimal() + { + $request = new HTTP\Request('PROPFIND', '/', [ + '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(false !== strpos($body, 'resourcetype'), $body); + $this->assertTrue(false === strpos($body, 'something'), $body); + } + + public 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('', $response->getBodyAsString(), 'Expected empty body: '.$response->getBodyAsString()); + $this->assertEquals(204, $response->status); + } + + public 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(false !== strpos($body, 'something')); + $this->assertTrue(false !== strpos($body, '403 Forbidden'), $body); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpCopyTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpCopyTest.php new file mode 100644 index 0000000..4da3855 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpCopyTest.php @@ -0,0 +1,180 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function setUpTree() + { + $this->tree = new Mock\Collection('root', [ + 'file1' => 'content1', + 'file2' => 'content2', + 'coll1' => [ + 'file3' => 'content3', + 'file4' => 'content4', + ], + ]); + } + + public 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()); + } + + public function testCopyFileToSelf() + { + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file1', + ]); + $response = $this->request($request); + $this->assertEquals(403, $response->getStatus()); + } + + public 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()); + } + + public 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()); + } + + public function testCopyFileToExistingOverwriteBadValue() + { + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file2', + 'Overwrite' => 'B', + ]); + $response = $this->request($request); + $this->assertEquals(400, $response->getStatus()); + } + + public function testCopyFileNonExistantParent() + { + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/notfound/file2', + ]); + $response = $this->request($request); + $this->assertEquals(409, $response->getStatus()); + } + + public 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()); + } + + public function testCopyFileToExistinBlockedCreateDestination() + { + $this->server->on('beforeBind', function ($path) { + if ('file2' === $path) { + 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()); + } + + public 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()); + } + + public function testCopyCollToSelf() + { + $request = new HTTP\Request('COPY', '/coll1', [ + 'Destination' => '/coll1', + ]); + $response = $this->request($request); + $this->assertEquals(403, $response->getStatus()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public function testCopyCollIntoSubtree() + { + $request = new HTTP\Request('COPY', '/coll1', [ + 'Destination' => '/coll1/subcol', + ]); + $response = $this->request($request); + $this->assertEquals(409, $response->getStatus()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpDeleteTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpDeleteTest.php new file mode 100644 index 0000000..f70feba --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpDeleteTest.php @@ -0,0 +1,131 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function setUpTree() + { + $this->tree = new Mock\Collection('root', [ + 'file1' => 'foo', + 'dir' => [ + 'subfile' => 'bar', + 'subfile2' => 'baz', + ], + ]); + } + + /** + * A successful DELETE. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpGetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpGetTest.php new file mode 100644 index 0000000..ad60aab --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpGetTest.php @@ -0,0 +1,150 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function setUpTree() + { + $this->tree = new Mock\Collection('root', [ + 'file1' => 'foo', + new Mock\Collection('dir', []), + new Mock\StreamingFile('streaming', 'stream'), + ]); + } + + public 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()); + } + + public 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()); + } + + public function testGet404() + { + $request = new HTTP\Request('GET', '/notfound'); + $response = $this->request($request); + + $this->assertEquals(404, $response->getStatus()); + } + + public function testGet404_aswell() + { + $request = new HTTP\Request('GET', '/file1/subfile'); + $response = $this->request($request); + + $this->assertEquals(404, $response->getStatus()); + } + + /** + * We automatically normalize double slashes. + */ + public 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()); + } + + public function testGetCollection() + { + $request = new HTTP\Request('GET', '/dir'); + $response = $this->request($request); + + $this->assertEquals(501, $response->getStatus()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpHeadTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpHeadTest.php new file mode 100644 index 0000000..25e723f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpHeadTest.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function setUpTree() + { + $this->tree = new Mock\Collection('root', [ + 'file1' => 'foo', + new Mock\Collection('dir', []), + new Mock\StreamingFile('streaming', 'stream'), + ]); + } + + public 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. + */ + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpMoveTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpMoveTest.php new file mode 100644 index 0000000..bdb4ff8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpMoveTest.php @@ -0,0 +1,110 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function setUpTree() + { + $this->tree = new Mock\Collection('root', [ + 'file1' => 'content1', + 'file2' => 'content2', + ]); + } + + public 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()); + } + + public 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')); + } + + public 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')); + } + + public 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')); + } + + public 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. + */ + public function testMoveToExistingBlockedDeleteSource() + { + $this->server->on('beforeUnbind', function ($path) { + if ('file1' === $path) { + 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpPutTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpPutTest.php new file mode 100644 index 0000000..d3932a4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/HttpPutTest.php @@ -0,0 +1,336 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function setUpTree() + { + $this->tree = new Mock\Collection('root', [ + 'file1' => 'foo', + ]); + } + + /** + * A successful PUT of a new file. + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Issue33Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Issue33Test.php new file mode 100644 index 0000000..36b182c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Issue33Test.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class Issue33Test extends \PHPUnit\Framework\TestCase +{ + public function setup(): void + { + \Sabre\TestUtil::clearTempDir(); + } + + public function testCopyMoveInfo() + { + $bar = new SimpleCollection('bar'); + $root = new SimpleCollection('webdav', [$bar]); + + $server = new Server($root); + $server->setBaseUri('/webdav/'); + + $request = new HTTP\Request('GET', '/webdav/bar', [ + 'Destination' => 'http://dev2.tribalos.com/webdav/%C3%A0fo%C3%B3', + 'Overwrite' => 'F', + ]); + + $server->httpRequest = $request; + + $info = $server->getCopyAndMoveInfo($request); + + $this->assertEquals('%C3%A0fo%C3%B3', urlencode($info['destination'])); + $this->assertFalse($info['destinationExists']); + $this->assertFalse($info['destinationNode']); + } + + public 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()); + } + + public function testDirName() + { + $dirname1 = 'bar'; + $dirname2 = urlencode('%C3%A0fo%C3%B3'); + + $this->assertTrue(dirname($dirname1) == dirname($dirname2)); + } + + /** + * @depends testTreeMove + * @depends testCopyMoveInfo + */ + public function testEverything() + { + $request = new HTTP\Request('MOVE', '/webdav/bar', [ + 'Destination' => 'http://dev2.tribalos.com/webdav/%C3%A0fo%C3%B3', + 'Overwrite' => 'F', + ]); + + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/AbstractTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/AbstractTest.php new file mode 100644 index 0000000..d1cd179 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/AbstractTest.php @@ -0,0 +1,189 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks\Backend; + +use Sabre\DAV; + +abstract class AbstractTest extends \PHPUnit\Framework\TestCase +{ + /** + * @abstract + * + * @return AbstractBackend + */ + abstract public function getBackend(); + + public function testSetup() + { + $backend = $this->getBackend(); + $this->assertInstanceOf('Sabre\\DAV\\Locks\\Backend\\AbstractBackend', $backend); + } + + /** + * @depends testSetup + */ + public 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 + */ + public 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 + */ + public 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)); + } + + public 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 + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/FileTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/FileTest.php new file mode 100644 index 0000000..57a3255 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/FileTest.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks\Backend; + +class FileTest extends AbstractTest +{ + public function getBackend() + { + \Sabre\TestUtil::clearTempDir(); + $backend = new File(SABRE_TEMPDIR.'/lockdb'); + + return $backend; + } + + public function teardown(): void + { + \Sabre\TestUtil::clearTempDir(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/Mock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/Mock.php new file mode 100644 index 0000000..adc416b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/Mock.php @@ -0,0 +1,132 @@ +<?php + +declare(strict_types=1); + +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 + */ + public function getLocks($uri, $returnChildLocks) + { + $newLocks = []; + + $locks = $this->getData(); + + foreach ($locks as $lock) { + if ($lock->uri === $uri || + //deep locks on parents + (0 != $lock->depth && 0 === strpos($uri, $lock->uri.'/')) || + + // locks on children + ($returnChildLocks && (0 === strpos($lock->uri, $uri.'/')))) { + $newLocks[] = $lock; + } + } + + // Checking if we can remove any of these locks + foreach ($newLocks as $k => $lock) { + if (time() > $lock->timeout + $lock->created) { + unset($newLocks[$k]); + } + } + + return $newLocks; + } + + /** + * Locks a uri. + * + * @param string $uri + * + * @return bool + */ + public function lock($uri, LockInfo $lockInfo) + { + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 1800; + $lockInfo->created = time(); + $lockInfo->uri = $uri; + + $locks = $this->getData(); + + foreach ($locks as $k => $lock) { + if ( + ($lock->token == $lockInfo->token) || + (time() > $lock->timeout + $lock->created) + ) { + unset($locks[$k]); + } + } + $locks[] = $lockInfo; + $this->putData($locks); + + return true; + } + + /** + * Removes a lock from a uri. + * + * @param string $uri + * + * @return bool + */ + public function unlock($uri, LockInfo $lockInfo) + { + $locks = $this->getData(); + foreach ($locks as $k => $lock) { + if ($lock->token == $lockInfo->token) { + unset($locks[$k]); + $this->putData($locks); + + return true; + } + } + + return false; + } + + protected $data = []; + + /** + * Loads the lockdata from the filesystem. + * + * @return array + */ + protected function getData() + { + return $this->data; + } + + /** + * Saves the lockdata. + */ + protected function putData(array $newData) + { + $this->data = $newData; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOMySQLTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOMySQLTest.php new file mode 100644 index 0000000..86ffc0b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOMySQLTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks\Backend; + +class PDOMySQLTest extends PDOTest +{ + public $driver = 'mysql'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOPgSqlTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOPgSqlTest.php new file mode 100644 index 0000000..4ab0579 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOPgSqlTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks\Backend; + +class PDOPgSqlTest extends PDOTest +{ + public $driver = 'pgsql'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOSqliteTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOSqliteTest.php new file mode 100644 index 0000000..f0c384d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOSqliteTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks\Backend; + +class PDOSqliteTest extends PDOTest +{ + public $driver = 'sqlite'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOTest.php new file mode 100644 index 0000000..f5ed98f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOTest.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks\Backend; + +abstract class PDOTest extends AbstractTest +{ + use \Sabre\DAV\DbTestHelperTrait; + + public function getBackend() + { + $this->dropTables('locks'); + $this->createSchema('locks'); + + $pdo = $this->getPDO(); + + return new PDO($pdo); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/MSWordTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/MSWordTest.php new file mode 100644 index 0000000..02c3d39 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/MSWordTest.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks; + +use Sabre\DAV; +use Sabre\HTTP; + +class MSWordTest extends \PHPUnit\Framework\TestCase +{ + public function teardown(): void + { + \Sabre\TestUtil::clearTempDir(); + } + + public 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((bool) $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((bool) $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); + } + + public 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; + } + + public 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; + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Plugin2Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Plugin2Test.php new file mode 100644 index 0000000..a4552b2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/Plugin2Test.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks; + +use Sabre\HTTP\Request; + +class Plugin2Test extends \Sabre\DAVServerTest +{ + public $setupLocks = true; + + public function setUpTree() + { + $this->tree = new \Sabre\DAV\FS\Directory(SABRE_TEMPDIR); + } + + public function teardown(): void + { + \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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/PluginTest.php new file mode 100644 index 0000000..96e3939 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Locks/PluginTest.php @@ -0,0 +1,860 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Locks; + +use Sabre\DAV; +use Sabre\HTTP; + +class PluginTest extends DAV\AbstractServer +{ + /** + * @var Plugin + */ + protected $locksPlugin; + + public function setup(): void + { + parent::setUp(); + $locksBackend = new Backend\File(SABRE_TEMPDIR.'/locksdb'); + $locksPlugin = new Plugin($locksBackend); + $this->server->addPlugin($locksPlugin); + $this->locksPlugin = $locksPlugin; + } + + public function testGetInfo() + { + $this->assertArrayHasKey( + 'name', + $this->locksPlugin->getPluginInfo() + ); + } + + public function testGetFeatures() + { + $this->assertEquals([2], $this->locksPlugin->getFeatures()); + } + + public function testGetHTTPMethods() + { + $this->assertEquals(['LOCK', 'UNLOCK'], $this->locksPlugin->getHTTPMethods('')); + } + + public 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); + } + + public 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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), '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->getBodyAsString()); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", 'xmlns\\1="urn:DAV"', $this->response->getBodyAsString()); + $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->getBodyAsString()); + } + + $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 + */ + public 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->getBodyAsString()); + } + + /** + * @depends testLock + */ + public 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 + */ + public 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 + */ + public 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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(201, $this->response->status); + } + + /** + * @depends testLock + */ + public 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 + */ + public 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->getBodyAsString()); + } + + /** + * @depends testLock + */ + public 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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), '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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(423, $this->response->status); + } + + /** + * @depends testLock + */ + public 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->getBodyAsString()); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + ], + $this->server->httpResponse->getHeaders() + ); + } + + /** + * @depends testLock + */ + public 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->getBodyAsString()); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + ], + $this->server->httpResponse->getHeaders() + ); + } + + /** + * @depends testLock + */ + public function testLockRetainOwner() + { + $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>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 + */ + public function testLockPutBadToken() + { + $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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), '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', [ + 'If' => '(<opaquelocktoken:token1>)', + ]); + $request->setBody('newbody'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), '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 + */ + public function testLockDeleteParent() + { + $request = new HTTP\Request('LOCK', '/dir/child.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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(200, $this->response->status); + + $request = new HTTP\Request('DELETE', '/dir'); + $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 + */ + public function testLockDeleteSucceed() + { + $request = new HTTP\Request('LOCK', '/dir/child.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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(200, $this->response->status); + + $request = new HTTP\Request('DELETE', '/dir/child.txt', [ + 'If' => '('.$this->response->getHeader('Lock-Token').')', + ]); + $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 + */ + public function testLockCopyLockSource() + { + $request = new HTTP\Request('LOCK', '/dir/child.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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(200, $this->response->status); + + $request = new HTTP\Request('COPY', '/dir/child.txt', [ + 'Destination' => '/dir/child2.txt', + ]); + + $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 + */ + public function testLockCopyLockDestination() + { + $request = new HTTP\Request('LOCK', '/dir/child2.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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(201, $this->response->status); + + $request = new HTTP\Request('COPY', '/dir/child.txt', [ + 'Destination' => '/dir/child2.txt', + ]); + $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 + */ + public function testLockMoveLockSourceLocked() + { + $request = new HTTP\Request('LOCK', '/dir/child.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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(200, $this->response->status); + + $request = new HTTP\Request('MOVE', '/dir/child.txt', [ + 'Destination' => '/dir/child2.txt', + ]); + $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 + */ + public function testLockMoveLockSourceSucceed() + { + $request = new HTTP\Request('LOCK', '/dir/child.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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(200, $this->response->status); + + $request = new HTTP\Request('MOVE', '/dir/child.txt', [ + 'Destination' => '/dir/child2.txt', + 'If' => '('.$this->response->getHeader('Lock-Token').')', + ]); + $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->getBodyAsString()); + } + + /** + * @depends testLock + */ + public function testLockMoveLockDestination() + { + $request = new HTTP\Request('LOCK', '/dir/child2.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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(201, $this->response->status); + + $request = new HTTP\Request('MOVE', '/dir/child.txt', [ + 'Destination' => '/dir/child2.txt', + ]); + $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 + */ + public function testLockMoveLockParent() + { + $request = new HTTP\Request('LOCK', '/dir', [ + 'Depth' => 'infinite', + ]); + $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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(200, $this->response->status); + + $request = new HTTP\Request('MOVE', '/dir/child.txt', [ + 'Destination' => '/dir/child2.txt', + 'If' => '</dir> ('.$this->response->getHeader('Lock-Token').')', + ]); + $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 + */ + public function testLockPutGoodToken() + { + $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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), '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', [ + 'If' => '('.$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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(204, $this->response->status); + } + + /** + * @depends testLock + */ + public 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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), '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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), 'We did not get a valid Locktoken back ('.$this->response->getHeader('Lock-Token').')'); + + $this->assertEquals(204, $this->response->status); + } + + public function testPutWithIncorrectETag() + { + $request = new HTTP\Request('PUT', '/test.txt', [ + 'If' => '(["etag1"])', + ]); + $request->setBody('newbody'); + $this->server->httpRequest = $request; + $this->server->exec(); + $this->assertEquals(412, $this->response->status); + } + + /** + * @depends testPutWithIncorrectETag + */ + public 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) + ); + + $request = new HTTP\Request('PUT', '/test.txt', [ + 'If' => '(["'.$etag.'"])', + ]); + $request->setBody('newbody'); + + $this->server->httpRequest = $request; + $this->server->exec(); + $this->assertEquals(204, $this->response->status, 'Incorrect status received. Full response body:'.$this->response->getBodyAsString()); + } + + public function testDeleteWithETagOnCollection() + { + $request = new HTTP\Request('DELETE', '/dir', [ + 'If' => '(["etag1"])', + ]); + + $this->server->httpRequest = $request; + $this->server->exec(); + $this->assertEquals(412, $this->response->status); + } + + public function testGetTimeoutHeader() + { + $request = new HTTP\Request('LOCK', '/foo/bar', [ + 'Timeout' => 'second-100', + ]); + + $this->server->httpRequest = $request; + $this->assertEquals(100, $this->locksPlugin->getTimeoutHeader()); + } + + public function testGetTimeoutHeaderTwoItems() + { + $request = new HTTP\Request('LOCK', '/foo/bar', [ + 'Timeout' => 'second-5, infinite', + ]); + $this->server->httpRequest = $request; + $this->assertEquals(5, $this->locksPlugin->getTimeoutHeader()); + } + + public function testGetTimeoutHeaderInfinite() + { + $request = new HTTP\Request('LOCK', '/foo/bar', [ + 'Timeout' => 'infinite, second-5', + ]); + $this->server->httpRequest = $request; + $this->assertEquals(LockInfo::TIMEOUT_INFINITE, $this->locksPlugin->getTimeoutHeader()); + } + + public function testGetTimeoutHeaderInvalid() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $request = new HTTP\Request('GET', '/', ['Timeout' => 'yourmom']); + + $this->server->httpRequest = $request; + $this->locksPlugin->getTimeoutHeader(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/Collection.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/Collection.php new file mode 100644 index 0000000..0412747 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/Collection.php @@ -0,0 +1,157 @@ +<?php + +declare(strict_types=1); + +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 Collection $parent + */ + public 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 + */ + public 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 string|null + */ + public function createFile($name, $data = null) + { + if (null === $data) { + $data = ''; + } + 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 + */ + public function createDirectory($name) + { + $this->children[] = new self($name); + } + + /** + * Returns an array with all the child nodes. + * + * @return \Sabre\DAV\INode[] + */ + public function getChildren() + { + return $this->children; + } + + /** + * Adds an already existing node to this collection. + */ + public function addNode(\Sabre\DAV\INode $node) + { + $this->children[] = $node; + } + + /** + * Removes a childnode from this node. + * + * @param string $name + */ + public 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,. + */ + public function delete() + { + foreach ($this->getChildren() as $child) { + $this->deleteChild($child->getName()); + } + $this->parent->deleteChild($this->getName()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/File.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/File.php new file mode 100644 index 0000000..d48ddaa --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/File.php @@ -0,0 +1,151 @@ +<?php + +declare(strict_types=1); + +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 + */ + public function __construct($name, $contents, Collection $parent = null, $lastModified = -1) + { + $this->name = $name; + $this->put($contents); + $this->parent = $parent; + + if (-1 === $lastModified) { + $lastModified = time(); + } + + $this->lastModified = $lastModified; + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Changes the name of the node. + * + * @param string $name + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function getETag() + { + return '"'.md5($this->contents).'"'; + } + + /** + * Returns the size of the node, in bytes. + * + * @return int + */ + public function getSize() + { + return strlen($this->contents); + } + + /** + * Delete the node. + */ + public 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 + */ + public function getLastModified() + { + return $this->lastModified; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/PropertiesCollection.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/PropertiesCollection.php new file mode 100644 index 0000000..23d236c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/PropertiesCollection.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +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 + */ + public 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. + * + * @return bool|array + */ + public 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 + */ + public function getProperties($requestedProperties) + { + $returnedProperties = []; + foreach ($requestedProperties as $requestedProperty) { + if (isset($this->properties[$requestedProperty])) { + $returnedProperties[$requestedProperty] = + $this->properties[$requestedProperty]; + } + } + + return $returnedProperties; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/SharedNode.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/SharedNode.php new file mode 100644 index 0000000..da1e0ef --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/SharedNode.php @@ -0,0 +1,113 @@ +<?php + +declare(strict_types=1); + +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 = []; + + public function __construct($name, $access) + { + $this->name = $name; + $this->access = $access; + } + + public 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 + */ + public 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 + */ + public function getShareResourceUri() + { + return 'urn:example:bar'; + } + + /** + * Updates the list of sharees. + * + * Every item must be a Sharee object. + * + * @param Sharee[] $sharees + */ + public function updateInvites(array $sharees) + { + foreach ($sharees as $sharee) { + if (\Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS === $sharee->access) { + // 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[] + */ + public function getInvites() + { + return $this->invites; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/StreamingFile.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/StreamingFile.php new file mode 100644 index 0000000..951327e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mock/StreamingFile.php @@ -0,0 +1,96 @@ +<?php + +declare(strict_types=1); + +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 + */ + public 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 + */ + public 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 + */ + public function getETag() + { + return null; + } + + /** + * Returns the size of the node, in bytes. + * + * @return int + */ + public function getSize() + { + return $this->size; + } + + /** + * Allows testing scripts to set the resource's file size. + * + * @param int $size + */ + public function setSize($size) + { + $this->size = $size; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/MockLogger.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/MockLogger.php new file mode 100644 index 0000000..17e2f1d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/MockLogger.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +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 + */ + public function log($level, $message, array $context = []) + { + $this->logs[] = [ + $level, + $message, + $context, + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mount/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mount/PluginTest.php new file mode 100644 index 0000000..54f7e4c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Mount/PluginTest.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Mount; + +use Sabre\DAV; +use Sabre\HTTP; + +class PluginTest extends DAV\AbstractServer +{ + public function setup(): void + { + parent::setUp(); + $this->server->addPlugin(new Plugin()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + $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->getBodyAsString().'. 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ObjectTreeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ObjectTreeTest.php new file mode 100644 index 0000000..7066c49 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ObjectTreeTest.php @@ -0,0 +1,90 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class ObjectTreeTest extends \PHPUnit\Framework\TestCase +{ + protected $tree; + + public function setup(): void + { + \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); + } + + public function teardown(): void + { + \Sabre\TestUtil::clearTempDir(); + } + + public function testGetRootNode() + { + $root = $this->tree->getNodeForPath(''); + $this->assertInstanceOf('Sabre\\DAV\\FSExt\\Directory', $root); + } + + public function testGetSubDir() + { + $root = $this->tree->getNodeForPath('subdir'); + $this->assertInstanceOf('Sabre\\DAV\\FSExt\\Directory', $root); + } + + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PSR3Test.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PSR3Test.php new file mode 100644 index 0000000..62ff0e7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PSR3Test.php @@ -0,0 +1,84 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class PSR3Test extends \PHPUnit\Framework\TestCase +{ + public function testIsLoggerAware() + { + $server = new Server(); + $this->assertInstanceOf( + 'Psr\Log\LoggerAwareInterface', + $server + ); + } + + public function testGetNullLoggerByDefault() + { + $server = new Server(); + $this->assertInstanceOf( + 'Psr\Log\NullLogger', + $server->getLogger() + ); + } + + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/FileMock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/FileMock.php new file mode 100644 index 0000000..72fdb5e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/FileMock.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\PartialUpdate; + +use Sabre\DAV; + +class FileMock implements IPatchSupport +{ + protected $data = ''; + + public 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 + */ + public 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; + } + } + + public function get() + { + return $this->data; + } + + public function getContentType() + { + return 'text/plain'; + } + + public function getSize() + { + return strlen($this->data); + } + + public function getETag() + { + return '"'.$this->data.'"'; + } + + public function delete() + { + throw new DAV\Exception\MethodNotAllowed(); + } + + public function setName($name) + { + throw new DAV\Exception\MethodNotAllowed(); + } + + public function getName() + { + return 'partial'; + } + + public function getLastModified() + { + return null; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/PluginTest.php new file mode 100644 index 0000000..4d99aee --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/PluginTest.php @@ -0,0 +1,122 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\PartialUpdate; + +use Sabre\HTTP; + +class PluginTest extends \Sabre\DAVServerTest +{ + protected $node; + protected $plugin; + + public function setup(): void + { + $this->node = new FileMock(); + $this->tree[] = $this->node; + + parent::setUp(); + + $this->plugin = new Plugin(); + $this->server->addPlugin($this->plugin); + } + + public 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('')); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + $this->assertEquals('aaabbbaa', $this->node->get()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/SpecificationTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/SpecificationTest.php new file mode 100644 index 0000000..a727a13 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PartialUpdate/SpecificationTest.php @@ -0,0 +1,90 @@ +<?php + +declare(strict_types=1); + +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; + + public function setup(): void + { + $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; + } + + public function teardown(): void + { + \Sabre\TestUtil::clearTempDir(); + } + + /** + * @param string $headerValue + * @param string $httpStatus + * @param string $endResult + * @param int $contentLength + * + * @dataProvider data + */ + public 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')); + } + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropFindTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropFindTest.php new file mode 100644 index 0000000..bbaaa76 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropFindTest.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class PropFindTest extends \PHPUnit\Framework\TestCase +{ + public function testHandle() + { + $propFind = new PropFind('foo', ['{DAV:}displayname']); + $propFind->handle('{DAV:}displayname', 'foobar'); + + $this->assertEquals([ + 200 => ['{DAV:}displayname' => 'foobar'], + 404 => [], + ], $propFind->getResultForMultiStatus()); + } + + public function testHandleCallBack() + { + $propFind = new PropFind('foo', ['{DAV:}displayname']); + $propFind->handle('{DAV:}displayname', function () { return 'foobar'; }); + + $this->assertEquals([ + 200 => ['{DAV:}displayname' => 'foobar'], + 404 => [], + ], $propFind->getResultForMultiStatus()); + } + + public function testAllPropDefaults() + { + $propFind = new PropFind('foo', ['{DAV:}displayname'], 0, PropFind::ALLPROPS); + + $this->assertEquals([ + 200 => [], + ], $propFind->getResultForMultiStatus()); + } + + public function testSet() + { + $propFind = new PropFind('foo', ['{DAV:}displayname']); + $propFind->set('{DAV:}displayname', 'bar'); + + $this->assertEquals([ + 200 => ['{DAV:}displayname' => 'bar'], + 404 => [], + ], $propFind->getResultForMultiStatus()); + } + + public function testSetAllpropCustom() + { + $propFind = new PropFind('foo', ['{DAV:}displayname'], 0, PropFind::ALLPROPS); + $propFind->set('{DAV:}customproperty', 'bar'); + + $this->assertEquals([ + 200 => ['{DAV:}customproperty' => 'bar'], + ], $propFind->getResultForMultiStatus()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropPatchTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropPatchTest.php new file mode 100644 index 0000000..98fefb6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropPatchTest.php @@ -0,0 +1,338 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class PropPatchTest extends \PHPUnit\Framework\TestCase +{ + protected $propPatch; + + public function setup(): void + { + $this->propPatch = new PropPatch([ + '{DAV:}displayname' => 'foo', + ]); + $this->assertEquals(['{DAV:}displayname' => 'foo'], $this->propPatch->getMutations()); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public function testHandleNothing() + { + $hasRan = false; + + $this->propPatch->handle('{DAV:}foobar', function ($value) use (&$hasRan) { + $hasRan = true; + }); + + $this->assertFalse($hasRan); + } + + /** + * @depends testHandleSingleSuccess + */ + public 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); + } + + public function testHandleRemainingNothingToDo() + { + $hasRan = false; + + $this->propPatch->handle('{DAV:}displayname', function () {}); + $this->propPatch->handleRemaining(function ($mutations) use (&$hasRan) { + $hasRan = true; + }); + + $this->assertFalse($hasRan); + } + + public function testSetResultCode() + { + $this->propPatch->setResultCode('{DAV:}displayname', 201); + $this->assertTrue($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 201], $result); + } + + public function testSetResultCodeFail() + { + $this->propPatch->setResultCode('{DAV:}displayname', 402); + $this->assertFalse($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 402], $result); + } + + public function testSetRemainingResultCode() + { + $this->propPatch->setRemainingResultCode(204); + $this->assertTrue($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 204], $result); + } + + public function testCommitNoHandler() + { + $this->assertFalse($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 403], $result); + } + + public 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); + } + + public 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()); + } + + public function testHandleSingleBrokenResult() + { + $this->expectException('UnexpectedValueException'); + $propPatch = new PropPatch([ + '{DAV:}a' => 'foo', + ]); + + $propPatch->handle('{DAV:}a', function () { + return []; + }); + $propPatch->commit(); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public function testHandleMultiValueBroken() + { + $this->expectException('UnexpectedValueException'); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/AbstractPDOTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/AbstractPDOTest.php new file mode 100644 index 0000000..a0e274c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/AbstractPDOTest.php @@ -0,0 +1,185 @@ +<?php + +declare(strict_types=1); + +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; + + public 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()); + } + + public function testPropFind() + { + $backend = $this->getBackend(); + + $propFind = new PropFind('dir', ['{DAV:}displayname']); + $backend->propFind('dir', $propFind); + + $this->assertEquals('Directory', $propFind->get('{DAV:}displayname')); + } + + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/Mock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/Mock.php new file mode 100644 index 0000000..391a49e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/Mock.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function move($source, $destination) + { + foreach ($this->data as $path => $props) { + if ($path === $source) { + $this->data[$destination] = $props; + unset($this->data[$path]); + continue; + } + + if (0 === strpos($path, $source.'/')) { + $this->data[$destination.substr($path, strlen($source) + 1)] = $props; + unset($this->data[$path]); + } + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOMysqlTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOMysqlTest.php new file mode 100644 index 0000000..6e11317 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOMysqlTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\PropertyStorage\Backend; + +class PDOMysqlTest extends AbstractPDOTest +{ + public $driver = 'mysql'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOPgSqlTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOPgSqlTest.php new file mode 100644 index 0000000..23955f3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOPgSqlTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\PropertyStorage\Backend; + +class PDOPgSqlTest extends AbstractPDOTest +{ + public $driver = 'pgsql'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOSqliteTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOSqliteTest.php new file mode 100644 index 0000000..37b0ed2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOSqliteTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\PropertyStorage\Backend; + +class PDOSqliteTest extends AbstractPDOTest +{ + public $driver = 'sqlite'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/PluginTest.php new file mode 100644 index 0000000..5da4a15 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/PropertyStorage/PluginTest.php @@ -0,0 +1,109 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\PropertyStorage; + +class PluginTest extends \Sabre\DAVServerTest +{ + protected $backend; + protected $plugin; + + protected $setupFiles = true; + + public function setup(): void + { + parent::setUp(); + $this->backend = new Backend\Mock(); + $this->plugin = new Plugin( + $this->backend + ); + + $this->server->addPlugin($this->plugin); + } + + public function testGetInfo() + { + $this->assertArrayHasKey( + 'name', + $this->plugin->getPluginInfo() + ); + } + + public function testSetProperty() + { + $this->server->updateProperties('', ['{DAV:}displayname' => 'hi']); + $this->assertEquals([ + '' => [ + '{DAV:}displayname' => 'hi', + ], + ], $this->backend->data); + } + + /** + * @depends testSetProperty + */ + public function testGetProperty() + { + $this->testSetProperty(); + $result = $this->server->getProperties('', ['{DAV:}displayname']); + + $this->assertEquals([ + '{DAV:}displayname' => 'hi', + ], $result); + } + + /** + * @depends testSetProperty + */ + public function testDeleteProperty() + { + $this->testSetProperty(); + $this->server->emit('afterUnbind', ['']); + $this->assertEquals([], $this->backend->data); + } + + public 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 + */ + public function testSetPropertyInFilteredPath() + { + $this->plugin->pathFilter = function ($path) { + return false; + }; + + $this->server->updateProperties('', ['{DAV:}displayname' => 'hi']); + $this->assertEquals([], $this->backend->data); + } + + /** + * @depends testSetPropertyInFilteredPath + */ + public function testGetPropertyInFilteredPath() + { + $this->testSetPropertyInFilteredPath(); + $result = $this->server->getProperties('', ['{DAV:}displayname']); + + $this->assertEquals([], $result); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerEventsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerEventsTest.php new file mode 100644 index 0000000..b1f6754 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerEventsTest.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerEventsTest extends AbstractServer +{ + private $tempPath; + + private $exception; + + public function testAfterBind() + { + $this->server->on('afterBind', [$this, 'afterBindHandler']); + $newPath = 'afterBind'; + + $this->tempPath = ''; + $this->server->createFile($newPath, 'body'); + $this->assertEquals($newPath, $this->tempPath); + } + + public function afterBindHandler($path) + { + $this->tempPath = $path; + } + + public 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(); + } + + public 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()); + } + + public function beforeBindCancelHandler($path) + { + return false; + } + + public 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); + } + + public function exceptionHandler(Exception $exception) + { + $this->exception = $exception; + } + + public function testMethod() + { + $k = 1; + $this->server->on('method:*', function ($request, $response) use (&$k) { + ++$k; + + 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerMKCOLTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerMKCOLTest.php new file mode 100644 index 0000000..02c6a46 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerMKCOLTest.php @@ -0,0 +1,354 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerMKCOLTest extends AbstractServer +{ + public 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->getBodyAsString()); + $this->assertTrue(is_dir($this->tempDir.'/testcol')); + } + + /** + * @depends testMkcol + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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->getBodyAsString()); + } + + /** + * @depends testMkcol + */ + public 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->getBodyAsString()); + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + public 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->getBodyAsString()); + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + public 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->getBodyAsString()); + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + public 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->getBodyAsString()); + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + public 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->getBodyAsString()); + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + public 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->getBodyAsString()); + } + + /** + * @depends testMKCOLSuccess + * @depends testMKCOLAlreadyExists + */ + public 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(); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Wrong statuscode received. Full response body: '.$bodyAsString); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $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, + $bodyAsString + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPluginTest.php new file mode 100644 index 0000000..47e1e6b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPluginTest.php @@ -0,0 +1,96 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerPluginTest extends AbstractServer +{ + /** + * @var Sabre\DAV\TestPlugin + */ + protected $testPlugin; + + public function setup(): void + { + parent::setUp(); + + $testPlugin = new TestPlugin(); + $this->server->addPlugin($testPlugin); + $this->testPlugin = $testPlugin; + } + + public 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() + ); + } + + public 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->getBodyAsString()); + $this->assertEquals('OPTIONS', $this->testPlugin->beforeMethod); + } + + public function testGetPlugin() + { + $this->assertEquals($this->testPlugin, $this->server->getPlugin(get_class($this->testPlugin))); + } + + public function testUnknownPlugin() + { + $this->assertNull($this->server->getPlugin('SomeRandomClassName')); + } + + public function testGetSupportedReportSet() + { + $this->assertEquals([], $this->testPlugin->getSupportedReportSet('/')); + } + + public function testGetPlugins() + { + $this->assertEquals( + [ + get_class($this->testPlugin) => $this->testPlugin, + 'core' => $this->server->getPlugin('core'), + ], + $this->server->getPlugins() + ); + } +} + +class ServerPluginMock extends ServerPlugin +{ + public function initialize(Server $s) + { + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPreconditionsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPreconditionsTest.php new file mode 100644 index 0000000..6f4399b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPreconditionsTest.php @@ -0,0 +1,269 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerPreconditionsTest extends \PHPUnit\Framework\TestCase +{ + public function testIfMatchNoNode() + { + $this->expectException('Sabre\DAV\Exception\PreconditionFailed'); + $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); + } + + public 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)); + } + + public function testIfMatchWrongEtag() + { + $this->expectException('Sabre\DAV\Exception\PreconditionFailed'); + $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); + } + + public 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 + */ + public 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)); + } + + public 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)); + } + + public 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)); + } + + public function testIfNoneMatchHasNode() + { + $this->expectException('Sabre\DAV\Exception\PreconditionFailed'); + $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); + } + + public 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)); + } + + public 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)); + } + + public function testIfNoneMatchCorrectEtag() + { + $this->expectException('Sabre\DAV\Exception\PreconditionFailed'); + $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); + } + + public function testIfNoneMatchCorrectEtagMultiple() + { + $this->expectException('Sabre\DAV\Exception\PreconditionFailed'); + $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); + } + + public 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. + */ + public 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); + } + + public function testIfModifiedSinceUnModified() + { + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'If-Modified-Since' => 'Sun, 06 Nov 1994 08:49:37 GMT', + ]); + $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()); + } + + public function testIfModifiedSinceModified() + { + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'If-Modified-Since' => 'Tue, 06 Nov 1984 08:49:37 GMT', + ]); + + $httpResponse = new HTTP\ResponseMock(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + } + + public function testIfModifiedSinceInvalidDate() + { + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'If-Modified-Since' => 'Your mother', + ]); + $httpResponse = new HTTP\ResponseMock(); + + // Invalid dates must be ignored, so this should return true + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + } + + public function testIfModifiedSinceInvalidDate2() + { + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'If-Unmodified-Since' => 'Sun, 06 Nov 1994 08:49:37 EST', + ]); + $httpResponse = new HTTP\ResponseMock(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + } + + public function testIfUnmodifiedSinceUnModified() + { + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'If-Unmodified-Since' => 'Sun, 06 Nov 1994 08:49:37 GMT', + ]); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + } + + public function testIfUnmodifiedSinceModified() + { + $this->expectException('Sabre\DAV\Exception\PreconditionFailed'); + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'If-Unmodified-Since' => 'Tue, 06 Nov 1984 08:49:37 GMT', + ]); + $httpResponse = new HTTP\ResponseMock(); + $server->checkPreconditions($httpRequest, $httpResponse); + } + + public function testIfUnmodifiedSinceInvalidDate() + { + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', [ + 'If-Unmodified-Since' => 'Sun, 06 Nov 1984 08:49:37 CET', + ]); + $httpResponse = new HTTP\ResponseMock(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + } +} + +class ServerPreconditionsNode extends File +{ + public function getETag() + { + return '"abc123"'; + } + + public function getLastModified() + { + /* my birthday & time, I believe */ + return strtotime('1985-04-07 01:30 +02:00'); + } + + public function getName() + { + return 'foo'; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPropsInfiniteDepthTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPropsInfiniteDepthTest.php new file mode 100644 index 0000000..b9e30e5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPropsInfiniteDepthTest.php @@ -0,0 +1,213 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerPropsInfiniteDepthTest extends AbstractServer +{ + protected function getRootNode() + { + return new FSExt\Directory(SABRE_TEMPDIR); + } + + public function setup(): void + { + 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; + } + + public function teardown(): void + { + 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(); + } + + public function testPropFindEmptyBody() + { + $this->sendRequest(''); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Incorrect status received. Full response body: '.$bodyAsString); + + $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"', $bodyAsString); + $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)); + } + + public 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'); + } + + public 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->getBodyAsString()); + $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'); + } + + public 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->getBodyAsString()); + $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(true == count($xml->xpath($test)), '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]); + } + + public function testFilesThatAreSiblingsOfDirectoriesShouldBeReportedAsFiles() + { + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:resourcetype /> + </d:prop> +</d:propfind>'; + + $this->sendRequest($xml); + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", 'xmlns\\1="urn:DAV"', $this->response->getBodyAsString()); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + $pathTests = [ + '/d:multistatus', + '/d:multistatus/d:response', + '/d:multistatus/d:response/d:href', + '/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:resourcetype', + ]; + + $hrefPaths = []; + + foreach ($pathTests as $test) { + $this->assertTrue(true == count($xml->xpath($test)), 'We expected the '.$test.' element to appear in the response, we got: '.$body); + + if ('/d:multistatus/d:response/d:href' === $test) { + foreach ($xml->xpath($test) as $thing) { + /* @var \SimpleXMLElement $thing */ + $hrefPaths[] = strip_tags($thing->asXML()); + } + } elseif ('/d:multistatus/d:response/d:propstat/d:prop/d:resourcetype' === $test) { + $count = 0; + foreach ($xml->xpath($test) as $thing) { + /* @var \SimpleXMLElement $thing */ + if ('.txt' !== substr($hrefPaths[$count], -4)) { + $this->assertEquals('<d:resourcetype><d:collection/></d:resourcetype>', $thing->asXML(), 'Path '.$hrefPaths[$count].' is not reported as a directory'); + } else { + $this->assertEquals('<d:resourcetype/>', $thing->asXML(), 'Path '.$hrefPaths[$count].' is not reported as a file'); + } + + ++$count; + } + } + } + + $val = $xml->xpath('/d:multistatus/d:response/d:propstat/d:status'); + $this->assertEquals(8, count($val), $body); + $this->assertEquals('HTTP/1.1 200 OK', (string) $val[0]); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPropsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPropsTest.php new file mode 100644 index 0000000..cd1ccfa --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerPropsTest.php @@ -0,0 +1,194 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerPropsTest extends AbstractServer +{ + protected function getRootNode() + { + return new FSExt\Directory(SABRE_TEMPDIR); + } + + public function setup(): void + { + 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'))); + } + + public function teardown(): void + { + 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(); + } + + public 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->getBodyAsString()); + $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)); + } + + public 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->getBodyAsString()); + $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)); + } + + public 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->getBodyAsString()); + $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'); + } + + public 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->getBodyAsString()); + $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'); + } + + public 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->getBodyAsString()); + $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(true == count($xml->xpath($test)), '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]); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerRangeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerRangeTest.php new file mode 100644 index 0000000..6d5be46 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerRangeTest.php @@ -0,0 +1,252 @@ +<?php + +declare(strict_types=1); + +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; + + public function setup(): void + { + parent::setUp(); + $this->server->createFile('files/test.txt', 'Test contents'); + + $this->lastModified = HTTP\toDate( + 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); + } + + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function testCrazyRange() + { + $request = new HTTP\Request('GET', '/files/test.txt', ['Range' => 'bytes=8-4']); + $response = $this->request($request); + + $this->assertEquals(416, $response->getStatus()); + } + + public function testNonSeekableStream() + { + $request = new HTTP\Request('GET', '/files/no-seeking.txt', ['Range' => 'bytes=2-5']); + $response = $this->request($request); + + $this->assertEquals(206, $response->getStatus()); + $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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerSimpleTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerSimpleTest.php new file mode 100644 index 0000000..e4dd3cd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerSimpleTest.php @@ -0,0 +1,433 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerSimpleTest extends AbstractServer +{ + public function testConstructArray() + { + $nodes = [ + new SimpleCollection('hello'), + ]; + + $server = new Server($nodes); + $this->assertEquals($nodes[0], $server->tree->getNodeForPath('hello')); + } + + public function testConstructInvalidArg() + { + $this->expectException('Sabre\DAV\Exception'); + $server = new Server(1); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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); + } + + public 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\toDate(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)); + } + + public 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()); + } + } + + public function testCalculateUri() + { + $uris = [ + 'http://www.example.org/root/somepath', + '/root/somepath', + '/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')); + + $this->server->setBaseUri('/'); + + foreach ($uris as $uri) { + $this->assertEquals('root/somepath', $this->server->calculateUri($uri)); + } + + $this->assertEquals('', $this->server->calculateUri('')); + } + + public 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)); + } + } + + public function testCalculateUriBreakout() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $uri = '/path1/'; + + $this->server->setBaseUri('/path2/'); + $this->server->calculateUri($uri); + } + + public function testGuessBaseUri() + { + $serverVars = [ + 'REQUEST_METHOD' => 'GET', + '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 + */ + public function testGuessBaseUriPercentEncoding() + { + $serverVars = [ + 'REQUEST_METHOD' => 'GET', + '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()); + + }*/ + + public function testGuessBaseUri2() + { + $serverVars = [ + 'REQUEST_METHOD' => 'GET', + '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()); + } + + public function testGuessBaseUriNoPathInfo() + { + $serverVars = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/index.php/root', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/', $server->guessBaseUri()); + } + + public function testGuessBaseUriNoPathInfo2() + { + $httpRequest = new HTTP\Request('GET', '/a/b/c/test.php'); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/', $server->guessBaseUri()); + } + + /** + * @depends testGuessBaseUri + */ + public function testGuessBaseUriQueryString() + { + $serverVars = [ + 'REQUEST_METHOD' => 'GET', + '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 + */ + public function testGuessBaseUriBadConfig() + { + $this->expectException('Sabre\DAV\Exception'); + $serverVars = [ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/index.php/root/heyyy', + 'PATH_INFO' => '/root', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $server->guessBaseUri(); + } + + public 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); + } + + public function exceptionTrigger($request, $response) + { + throw new Exception('Hola'); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public function reportHandler($reportName, $result, $path) + { + if ('{http://www.rooftopsolutions.nl/NS}myreport' == $reportName) { + $this->server->httpResponse->setStatus(418); + $this->server->httpResponse->setHeader('testheader', 'testvalue'); + + return false; + } else { + return; + } + } + + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerUpdatePropertiesTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerUpdatePropertiesTest.php new file mode 100644 index 0000000..cb8a4ab --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/ServerUpdatePropertiesTest.php @@ -0,0 +1,97 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class ServerUpdatePropertiesTest extends \PHPUnit\Framework\TestCase +{ + public 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); + } + + public 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); + } + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sharing/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sharing/PluginTest.php new file mode 100644 index 0000000..c1d8eb8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sharing/PluginTest.php @@ -0,0 +1,180 @@ +<?php + +declare(strict_types=1); + +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'; + + public function setUpTree() + { + $this->tree[] = new Mock\SharedNode( + 'shareable', + Plugin::ACCESS_READWRITE + ); + } + + public function testPostWithoutContentType() + { + $request = new \Sabre\HTTP\Request('POST', '/'); + $response = new \Sabre\HTTP\ResponseMock(); + + $this->sharingPlugin->httpPost($request, $response); + $this->assertTrue(true); + } + + public function testFeatures() + { + $this->assertEquals( + ['resource-sharing'], + $this->sharingPlugin->getFeatures() + ); + } + + public 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 + ); + } + + public function testGetPluginInfo() + { + $result = $this->sharingPlugin->getPluginInfo(); + $this->assertIsArray($result); + $this->assertEquals('sharing', $result['name']); + } + + public 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->assertStringContainsString( + 'Share this resource', + $html + ); + } + + public function testBrowserPostActionUnknownAction() + { + $this->assertNull($this->sharingPlugin->browserPostAction( + 'shareable', + 'foo', + [] + )); + } + + public 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() + ); + } + + public function testBrowserPostActionNoHref() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'access' => 'read', + ] + ); + } + + public function testBrowserPostActionNoAccess() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'href' => 'mailto:foo@example.org', + ] + ); + } + + public function testBrowserPostActionBadAccess() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'href' => 'mailto:foo@example.org', + 'access' => 'bleed', + ] + ); + } + + public function testBrowserPostActionAccessDenied() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->aclPlugin->setDefaultAcl([]); + $this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'access' => 'read', + 'href' => 'mailto:foo@example.org', + ] + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sharing/ShareResourceTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sharing/ShareResourceTest.php new file mode 100644 index 0000000..b4982c1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sharing/ShareResourceTest.php @@ -0,0 +1,203 @@ +<?php + +declare(strict_types=1); + +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; + + public function setUpTree() + { + $this->tree[] = $this->sharingNodeMock = new Mock\SharedNode( + 'shareable', + Plugin::ACCESS_SHAREDOWNER + ); + } + + public 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 + */ + public 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 + */ + public 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()); + } + + public 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); + } + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SimpleCollectionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SimpleCollectionTest.php new file mode 100644 index 0000000..6aaaca8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SimpleCollectionTest.php @@ -0,0 +1,49 @@ +<?php + +namespace Sabre\DAV; + +class SimpleCollectionTest extends \PHPUnit\Framework\TestCase +{ + public function testConstructNode() + { + $s = new SimpleCollection('foo', [new SimpleFile('bar.txt', 'hi')]); + $this->assertEquals('foo', $s->getName()); + $this->assertEquals('bar.txt', $s->getChild('bar.txt')->getName()); + } + + public function testConstructNodeArray() + { + $s = new SimpleCollection('foo', [ + 'bar' => [], + 'baz.txt' => 'hi', + 'gir' => [ + 'zim' => 'world', + ], + ]); + $this->assertEquals('foo', $s->getName()); + $this->assertEquals('bar', $s->getChild('bar')->getName()); + $this->assertEquals('hi', $s->getChild('baz.txt')->get()); + $this->assertEquals('world', $s->getChild('gir')->getChild('zim')->get()); + } + + public function testConstructBadParam() + { + $this->expectException('InvalidArgumentException'); + new SimpleCollection('foo', [new \StdClass()]); + } + + public function testGetChildren() + { + $child = new SimpleFile('bar.txt', 'hi'); + + $s = new SimpleCollection('foo', [$child]); + $this->assertEquals([$child], $s->getChildren()); + } + + public function testGetChild404() + { + $this->expectException('Sabre\DAV\Exception\NotFound'); + $s = new SimpleCollection('foo', []); + $s->getChild('404'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SimpleFileTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SimpleFileTest.php new file mode 100644 index 0000000..6edca5e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SimpleFileTest.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class SimpleFileTest extends \PHPUnit\Framework\TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/StringUtilTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/StringUtilTest.php new file mode 100644 index 0000000..bc36c6b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/StringUtilTest.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); + +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 + */ + public function testTextMatch($haystack, $needle, $collation, $matchType, $result) + { + $this->assertEquals($result, StringUtil::textMatch($haystack, $needle, $collation, $matchType)); + } + + public 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], + ]; + } + + public function testBadCollation() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + StringUtil::textMatch('foobar', 'foo', 'blabla', 'contains'); + } + + public function testBadMatchType() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + StringUtil::textMatch('foobar', 'foo', 'i;octet', 'booh'); + } + + public function testEnsureUTF8_ascii() + { + $inputString = 'harkema'; + $outputString = 'harkema'; + + $this->assertEquals( + $outputString, + StringUtil::ensureUTF8($inputString) + ); + } + + public function testEnsureUTF8_latin1() + { + $inputString = "m\xfcnster"; + $outputString = 'münster'; + + $this->assertEquals( + $outputString, + StringUtil::ensureUTF8($inputString) + ); + } + + public function testEnsureUTF8_utf8() + { + $inputString = "m\xc3\xbcnster"; + $outputString = 'münster'; + + $this->assertEquals( + $outputString, + StringUtil::ensureUTF8($inputString) + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sync/MockSyncCollection.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sync/MockSyncCollection.php new file mode 100644 index 0000000..6eed10a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sync/MockSyncCollection.php @@ -0,0 +1,168 @@ +<?php + +declare(strict_types=1); + +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 + */ + public function getSyncToken() + { + // Will be 'null' in the first round, and will increment ever after. + return $this->token; + } + + public 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 + */ + public 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 (0 === $left) { + break; + } + if ($left < 0) { + $modified = array_slice($modified, 0, $left); + } + $left = $limit - (count($modified) + count($deleted)); + if (0 === $left) { + break; + } + if ($left < 0) { + $deleted = array_slice($deleted, 0, $left); + } + break; + } + } + } + + return [ + 'syncToken' => $this->token, + 'added' => $added, + 'modified' => $modified, + 'deleted' => $deleted, + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sync/PluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sync/PluginTest.php new file mode 100644 index 0000000..71ceb03 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Sync/PluginTest.php @@ -0,0 +1,507 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Sync; + +use Sabre\DAV; +use Sabre\HTTP; + +class PluginTest extends \Sabre\DAVServerTest +{ + protected $collection; + + public function setup(): void + { + parent::setUp(); + $this->server->addPlugin(new Plugin()); + } + + public function testGetInfo() + { + $this->assertArrayHasKey( + 'name', + (new Plugin())->getPluginInfo() + ); + } + + public 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', []), + ]; + } + + public 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')); + } + + public 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'])); + } + + public 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->getBodyAsString()); + + $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()); + } + + public 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->getBodyAsString()); + + $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()); + } + + public 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->getBodyAsString()); + + $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()); + } + + public 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->getBodyAsString()); + + $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()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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->getBodyAsString()); + } + + public 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); + } + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SyncTokenPropertyTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SyncTokenPropertyTest.php new file mode 100644 index 0000000..46f9a1c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/SyncTokenPropertyTest.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +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 + */ + public 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 + */ + public 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')); + } + + public 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')); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TemporaryFileFilterTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TemporaryFileFilterTest.php new file mode 100644 index 0000000..951078b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TemporaryFileFilterTest.php @@ -0,0 +1,204 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class TemporaryFileFilterTest extends AbstractServer +{ + public function setup(): void + { + parent::setUp(); + $plugin = new TemporaryFileFilterPlugin(SABRE_TEMPDIR.'/tff'); + $this->server->addPlugin($plugin); + } + + public function testPutNormal() + { + $request = new HTTP\Request('PUT', '/testput.txt', [], 'Testing new file'); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals('', $this->response->getBodyAsString()); + $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')); + } + + public 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->getBodyAsString()); + $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.'); + } + + public 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->getBodyAsString()); + $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()); + } + + public 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->getBodyAsString()); + $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)); + } + + public function testGetWithBrowserPlugin() + { + $this->server->addPlugin(new Browser\Plugin()); + $request = new HTTP\Request('GET', '/'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(200, $this->response->status); + } + + public 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(1 === preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')), '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.'); + } + + public 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->getBodyAsString()); + $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->getBodyAsString()); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + ], $this->response->getHeaders()); + + $this->assertEquals('', $this->response->getBodyAsString()); + } + + public 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(); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals('', $bodyAsString); + $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(); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'Incorrect status code returned. Body: '.$bodyAsString); + $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"', $bodyAsString); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TestPlugin.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TestPlugin.php new file mode 100644 index 0000000..3bfe3b3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TestPlugin.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class TestPlugin extends ServerPlugin +{ + public $beforeMethod; + + public function getFeatures() + { + return ['drinking']; + } + + public function getHTTPMethods($uri) + { + return ['BEER', 'WINE']; + } + + public function initialize(Server $server) + { + $server->on('beforeMethod:*', [$this, 'beforeMethod']); + } + + public function beforeMethod(RequestInterface $request, ResponseInterface $response) + { + $this->beforeMethod = $request->getMethod(); + + return true; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TreeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TreeTest.php new file mode 100644 index 0000000..e3f04ea --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/TreeTest.php @@ -0,0 +1,238 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class TreeTest extends \PHPUnit\Framework\TestCase +{ + public function testNodeExists() + { + $tree = new TreeMock(); + + $this->assertTrue($tree->nodeExists('hi')); + $this->assertFalse($tree->nodeExists('hello')); + } + + public 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([])); + } + + public function testCopyFile() + { + $tree = new TreeMock(); + $tree->copy('hi/file', 'hi/newfile'); + + $this->assertArrayHasKey('newfile', $tree->getNodeForPath('hi')->newFiles); + } + + public function testCopyFile0() + { + $tree = new TreeMock(); + $tree->copy('hi/file', 'hi/0'); + + $this->assertArrayHasKey('0', $tree->getNodeForPath('hi')->newFiles); + } + + public function testMove() + { + $tree = new TreeMock(); + $tree->move('hi', 'hi2'); + + $this->assertEquals('hi2', $tree->getNodeForPath('hi')->getName()); + $this->assertTrue($tree->getNodeForPath('hi')->isRenamed); + } + + public function testDeepMove() + { + $tree = new TreeMock(); + $tree->move('hi/sub', 'hi2'); + + $this->assertArrayHasKey('hi2', $tree->getNodeForPath('')->newDirectories); + $this->assertTrue($tree->getNodeForPath('hi/sub')->isDeleted); + } + + public function testDelete() + { + $tree = new TreeMock(); + $tree->delete('hi'); + $this->assertTrue($tree->getNodeForPath('hi')->isDeleted); + } + + public function testGetChildren() + { + $tree = new TreeMock(); + $children = $tree->getChildren(''); + $firstChild = $children->current(); + $this->assertEquals('hi', $firstChild->getName()); + } + + public 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()); + } + + public 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 = []; + + public 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; + + public function createDirectory($name) + { + $this->newDirectories[$name] = true; + } + + public function createFile($name, $data = null) + { + $this->newFiles[$name] = $data; + } + + public 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); + } + + public function childExists($name) + { + return (bool) $this->getChild($name); + } + + public function delete() + { + $this->isDeleted = true; + } + + public function setName($name) + { + $this->isRenamed = true; + $this->name = $name; + } +} + +class TreeFileTester extends File implements IProperties +{ + public $name; + public $data; + public $properties; + + public function __construct($name, $data = null) + { + $this->name = $name; + if (is_null($data)) { + $data = 'bla'; + } + $this->data = $data; + } + + public function getName() + { + return $this->name; + } + + public function get() + { + return $this->data; + } + + public 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. + */ + public 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. + * + * @return array + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/UUIDUtilTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/UUIDUtilTest.php new file mode 100644 index 0000000..d7ef9be --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/UUIDUtilTest.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV; + +class UUIDUtilTest extends \PHPUnit\Framework\TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/PropTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/PropTest.php new file mode 100644 index 0000000..16bc0d3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/PropTest.php @@ -0,0 +1,148 @@ +<?php + +declare(strict_types=1); + +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 +{ + public function testDeserializeSimple() + { + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:"> + <foo>bar</foo> +</root> +XML; + + $expected = [ + '{DAV:}foo' => 'bar', + ]; + + $this->assertDecodeProp($input, $expected); + } + + public function testDeserializeEmpty() + { + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:" /> +XML; + + $expected = [ + ]; + + $this->assertDecodeProp($input, $expected); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public function testDeserializeCustomBad() + { + $this->expectException('LogicException'); + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:"> + <foo>blabla</foo> +</root> +XML; + + $expected = []; + + $elementMap = [ + '{DAV:}foo' => 'idk?', + ]; + + $this->assertDecodeProp($input, $expected, $elementMap); + } + + public function testDeserializeCustomBadObj() + { + $this->expectException('LogicException'); + $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); + } + + public function assertDecodeProp($input, array $expected, array $elementMap = []) + { + $elementMap['{DAV:}root'] = 'Sabre\DAV\Xml\Element\Prop'; + + $result = $this->parse($input, $elementMap); + $this->assertIsArray($result); + $this->assertEquals($expected, $result['value']); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/ResponseTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/ResponseTest.php new file mode 100644 index 0000000..09f0621 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/ResponseTest.php @@ -0,0 +1,304 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Element; + +use Sabre\DAV; + +class ResponseTest extends DAV\Xml\XmlTest +{ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function testSerializeBreak() + { + $this->expectException('InvalidArgumentException'); + $innerProps = [ + 200 => [ + '{DAV:}link' => new \STDClass(), + ], + ]; + + $property = new Response('uri', $innerProps); + $this->write(['{DAV:}root' => ['{DAV:}response' => $property]]); + } + + public 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 + */ + public 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. + */ + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/ShareeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/ShareeTest.php new file mode 100644 index 0000000..50d7686 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Element/ShareeTest.php @@ -0,0 +1,89 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Element; + +use Sabre\DAV\Sharing\Plugin; +use Sabre\DAV\Xml\XmlTest; + +class ShareeTest extends XmlTest +{ + public function testShareeUnknownPropertyInConstructor() + { + $this->expectException('InvalidArgumentException'); + new Sharee(['foo' => 'bar']); + } + + public 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'] + ); + } + + public function testDeserializeNoHref() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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', + ]); + } + + public function testDeserializeNoShareeAccess() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/HrefTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/HrefTest.php new file mode 100644 index 0000000..9dc748a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/HrefTest.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\DAV\Xml\XmlTest; + +class HrefTest extends XmlTest +{ + public function testConstruct() + { + $href = new Href('path'); + $this->assertEquals('path', $href->getHref()); + } + + public 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); + } + + public 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()); + } + + public 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); + } + + public 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. + */ + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/InviteTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/InviteTest.php new file mode 100644 index 0000000..5023db3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/InviteTest.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); + +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 +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LastModifiedTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LastModifiedTest.php new file mode 100644 index 0000000..9c5f1d7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LastModifiedTest.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Property; + +use DateTime; +use DateTimeZone; +use Sabre\DAV\Xml\XmlTest; + +class LastModifiedTest extends XmlTest +{ + public 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); + } + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LocalHrefTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LocalHrefTest.php new file mode 100644 index 0000000..548658b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LocalHrefTest.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\DAV\Xml\XmlTest; + +class LocalHrefTest extends XmlTest +{ + public function testConstruct() + { + $href = new LocalHref('path'); + $this->assertEquals('path', $href->getHref()); + } + + public 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); + } + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LockDiscoveryTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LockDiscoveryTest.php new file mode 100644 index 0000000..7e8b18c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/LockDiscoveryTest.php @@ -0,0 +1,167 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Locks\LockInfo; +use Sabre\DAV\Xml\XmlTest; + +class LockDiscoveryTest extends XmlTest +{ + public 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); + } + + public 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); + } + + public function testSerializeInfiniteTimeout() + { + $lock = new LockInfo(); + $lock->owner = 'hello'; + $lock->token = 'blabla'; + $lock->timeout = -1; + $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>Infinite</d:timeout> + <d:locktoken> + <d:href>opaquelocktoken:blabla</d:href> + </d:locktoken> + <d:owner>hello</d:owner> + + +</d:activelock> +</d:root> +', $xml); + } + + public function providesSerializeNoLockToken() + { + return [ + [''], + [null], + ]; + } + + /** + * @dataProvider providesSerializeNoLockToken + */ + public function testSerializeNoLockToken($emptyLockToken) + { + $lock = new LockInfo(); + $lock->owner = 'hello'; + $lock->token = $emptyLockToken; + $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:owner>hello</d:owner> + + +</d:activelock> +</d:root> +', $xml); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/ShareAccessTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/ShareAccessTest.php new file mode 100644 index 0000000..0b666da --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/ShareAccessTest.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Sharing\Plugin; +use Sabre\DAV\Xml\XmlTest; + +class ShareAccessTest extends XmlTest +{ + public 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); + } + + public 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] + ); + } + + public function testDeserializeInvalid() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedMethodSetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedMethodSetTest.php new file mode 100644 index 0000000..e54b20c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedMethodSetTest.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Xml\XmlTest; + +class SupportedMethodSetTest extends XmlTest +{ + public function testSimple() + { + $cus = new SupportedMethodSet(['GET', 'PUT']); + $this->assertEquals(['GET', 'PUT'], $cus->getValue()); + + $this->assertTrue($cus->has('GET')); + $this->assertFalse($cus->has('HEAD')); + } + + public 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); + } + + public function testSerializeHtml() + { + $cus = new SupportedMethodSet(['GET', 'PUT']); + $result = $cus->toHtml( + new \Sabre\DAV\Browser\HtmlOutputHelper('/', []) + ); + + $this->assertEquals('GET, PUT', $result); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedReportSetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedReportSetTest.php new file mode 100644 index 0000000..2de3ff6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedReportSetTest.php @@ -0,0 +1,110 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Property; + +use Sabre\DAV; +use Sabre\HTTP; + +class SupportedReportSetTest extends DAV\AbstractServer +{ + public 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(); + } + + public 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); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'We expected a multi-status response. Full response body: '.$bodyAsString); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", 'xmlns\\1="urn:DAV"', $bodyAsString); + $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 + */ + public 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); + + $bodyAsString = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->status, 'We expected a multi-status response. Full response body: '.$bodyAsString); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", 'xmlns\\1="urn:DAV"', $bodyAsString); + $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: '.$bodyAsString); + + $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: '.$bodyAsString); + + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/PropFindTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/PropFindTest.php new file mode 100644 index 0000000..f223c37 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/PropFindTest.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Request; + +use Sabre\DAV\Xml\XmlTest; + +class PropFindTest extends XmlTest +{ + public 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']); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/PropPatchTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/PropPatchTest.php new file mode 100644 index 0000000..d6bd38b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/PropPatchTest.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Request; + +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\XmlTest; + +class PropPatchTest extends XmlTest +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/ShareResourceTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/ShareResourceTest.php new file mode 100644 index 0000000..62ddf4e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/ShareResourceTest.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +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 +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/SyncCollectionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/SyncCollectionTest.php new file mode 100644 index 0000000..8bfce8a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/Request/SyncCollectionTest.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml\Request; + +use Sabre\DAV\Xml\XmlTest; + +class SyncCollectionTest extends XmlTest +{ + public 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']); + } + + public 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']); + } + + public 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']); + } + + public function testDeserializeMissingElem() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/ServiceTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/ServiceTest.php new file mode 100644 index 0000000..a15014e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/ServiceTest.php @@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAV\Xml; + +use PHPUnit\Framework\TestCase; + +class ServiceTest extends TestCase +{ + public function testInvalidNameSpace() + { + $this->expectException('Sabre\Xml\LibXMLException'); + $xml = '<D:propfind xmlns:D="DAV:"><D:prop><bar:foo xmlns:bar=""/></D:prop></D:propfind>'; + $util = new Service(); + $util->expect('{DAV:}propfind', $xml); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php new file mode 100644 index 0000000..7e44fcb --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +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 = '/'; + + public 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(); + } + + public function parse($xml, array $elementMap = []) + { + $reader = new Reader(); + $reader->contextUri = $this->contextUri; + $reader->elementMap = array_merge($this->elementMap, $elementMap); + $reader->xml($xml); + + return $reader->parse(); + } + + public function assertParsedValue($expected, $xml, array $elementMap = []) + { + $result = $this->parse($xml, $elementMap); + $this->assertEquals($expected, $result['value']); + } + + public function cleanUp() + { + libxml_clear_errors(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/ACLMethodTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/ACLMethodTest.php new file mode 100644 index 0000000..715559d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/ACLMethodTest.php @@ -0,0 +1,311 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class ACLMethodTest extends \PHPUnit\Framework\TestCase +{ + public function testCallback() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $acl = new Plugin(); + $server = new DAV\Server(); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpAcl($server->httpRequest, $server->httpResponse); + } + + /** + /** + */ + public function testNotSupportedByNode() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $tree = [ + new DAV\SimpleCollection('test'), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('GET', '/'); + $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); + } + + public function testSuccessSimple() + { + $tree = [ + new MockACLNode('test', []), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('GET', '/'); + $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)); + } + + public function testUnrecognizedPrincipal() + { + $this->expectException('Sabre\DAVACL\Exception\NotRecognizedPrincipal'); + $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); + } + + public function testUnrecognizedPrincipal2() + { + $this->expectException('Sabre\DAVACL\Exception\NotRecognizedPrincipal'); + $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); + } + + public function testUnknownPrivilege() + { + $this->expectException('Sabre\DAVACL\Exception\NotSupportedPrivilege'); + $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); + } + + public function testAbstractPrivilege() + { + $this->expectException('Sabre\DAVACL\Exception\NoAbstract'); + $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); + } + + public function testUpdateProtectedPrivilege() + { + $this->expectException('Sabre\DAVACL\Exception\AceConflict'); + $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); + } + + public function testUpdateProtectedPrivilege2() + { + $this->expectException('Sabre\DAVACL\Exception\AceConflict'); + $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); + } + + public function testUpdateProtectedPrivilege3() + { + $this->expectException('Sabre\DAVACL\Exception\AceConflict'); + $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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/AclPrincipalPropSetReportTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/AclPrincipalPropSetReportTest.php new file mode 100644 index 0000000..676906b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/AclPrincipalPropSetReportTest.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\HTTP\Request; + +class AclPrincipalPropSetReportTest extends \Sabre\DAVServerTest +{ + public $setupACL = true; + public $autoLogin = 'admin'; + + public 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() + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/AllowAccessTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/AllowAccessTest.php new file mode 100644 index 0000000..04dd29c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/AllowAccessTest.php @@ -0,0 +1,120 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class AllowAccessTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DAV\Server + */ + protected $server; + + public function setup(): void + { + $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('GET', '/'), + new \Sabre\HTTP\Response() + ); + $aclPlugin = new Plugin(); + $this->server->addPlugin($aclPlugin); + } + + public function testGet() + { + $this->server->httpRequest->setMethod('GET'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testGetDoesntExist() + { + $this->server->httpRequest->setMethod('GET'); + $this->server->httpRequest->setUrl('/foo'); + + $this->assertTrue($this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testHEAD() + { + $this->server->httpRequest->setMethod('HEAD'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod:HEAD', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testOPTIONS() + { + $this->server->httpRequest->setMethod('OPTIONS'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod:OPTIONS', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testPUT() + { + $this->server->httpRequest->setMethod('PUT'); + $this->server->httpRequest->setUrl('/testdir/file1.txt'); + + $this->assertTrue($this->server->emit('beforeMethod:PUT', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testPROPPATCH() + { + $this->server->httpRequest->setMethod('PROPPATCH'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod:PROPPATCH', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testCOPY() + { + $this->server->httpRequest->setMethod('COPY'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod:COPY', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testMOVE() + { + $this->server->httpRequest->setMethod('MOVE'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod:MOVE', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testLOCK() + { + $this->server->httpRequest->setMethod('LOCK'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod:LOCK', [$this->server->httpRequest, $this->server->httpResponse])); + } + + public function testBeforeBind() + { + $this->assertTrue($this->server->emit('beforeBind', ['testdir/file'])); + } + + public function testBeforeUnbind() + { + $this->assertTrue($this->server->emit('beforeUnbind', ['testdir'])); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/BlockAccessTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/BlockAccessTest.php new file mode 100644 index 0000000..566167e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/BlockAccessTest.php @@ -0,0 +1,180 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class BlockAccessTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DAV\Server + */ + protected $server; + protected $plugin; + + public function setup(): void + { + $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('GET', '/'), + new \Sabre\HTTP\Response() + ); + $this->server->addPlugin($this->plugin); + } + + public function testGet() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('GET'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testGetDoesntExist() + { + $this->server->httpRequest->setMethod('GET'); + $this->server->httpRequest->setUrl('/foo'); + + $r = $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + $this->assertTrue($r); + } + + public function testHEAD() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('HEAD'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testOPTIONS() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('OPTIONS'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testPUT() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('PUT'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testPROPPATCH() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('PROPPATCH'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testCOPY() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('COPY'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testMOVE() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('MOVE'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testACL() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('ACL'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testLOCK() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->httpRequest->setMethod('LOCK'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod:GET', [$this->server->httpRequest, $this->server->httpResponse]); + } + + public function testBeforeBind() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->emit('beforeBind', ['testdir/file']); + } + + public function testBeforeUnbind() + { + $this->expectException('Sabre\DAVACL\Exception\NeedPrivileges'); + $this->server->emit('beforeUnbind', ['testdir']); + } + + public 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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/AceConflictTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/AceConflictTest.php new file mode 100644 index 0000000..60fb8f3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/AceConflictTest.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class AceConflictTest extends \PHPUnit\Framework\TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NeedPrivilegesExceptionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NeedPrivilegesExceptionTest.php new file mode 100644 index 0000000..f08e536 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NeedPrivilegesExceptionTest.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class NeedPrivilegesExceptionTest extends \PHPUnit\Framework\TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NoAbstractTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NoAbstractTest.php new file mode 100644 index 0000000..38e9d8b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NoAbstractTest.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class NoAbstractTest extends \PHPUnit\Framework\TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NotRecognizedPrincipalTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NotRecognizedPrincipalTest.php new file mode 100644 index 0000000..62915ea --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NotRecognizedPrincipalTest.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class NotRecognizedPrincipalTest extends \PHPUnit\Framework\TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NotSupportedPrivilegeTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NotSupportedPrivilegeTest.php new file mode 100644 index 0000000..668c713 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Exception/NotSupportedPrivilegeTest.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class NotSupportedPrivilegeTest extends \PHPUnit\Framework\TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/ExpandPropertiesTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/ExpandPropertiesTest.php new file mode 100644 index 0000000..8afe6d3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/ExpandPropertiesTest.php @@ -0,0 +1,308 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class ExpandPropertiesTest extends \PHPUnit\Framework\TestCase +{ + public 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; + } + + public 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->getBodyAsString()); + $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->getBodyAsString()); + $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->getBodyAsString()); + } + } + + /** + * @depends testSimple + */ + public 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->getBodyAsString()); + $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->getBodyAsString()); + $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 + */ + public 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->getBodyAsString()); + $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 + */ + public 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->getBodyAsString()); + $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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/CollectionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/CollectionTest.php new file mode 100644 index 0000000..b07ec5f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/CollectionTest.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\FS; + +class CollectionTest extends FileTest +{ + public function setup(): void + { + $this->path = SABRE_TEMPDIR; + $this->sut = new Collection($this->path, $this->acl, $this->owner); + } + + public function teardown(): void + { + \Sabre\TestUtil::clearTempDir(); + } + + public 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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/FileTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/FileTest.php new file mode 100644 index 0000000..c1332b0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/FileTest.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); + +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'; + + public function setup(): void + { + $this->sut = new File($this->path, $this->acl, $this->owner); + } + + public function testGetOwner() + { + $this->assertEquals( + $this->owner, + $this->sut->getOwner() + ); + } + + public function testGetGroup() + { + $this->assertNull( + $this->sut->getGroup() + ); + } + + public function testGetACL() + { + $this->assertEquals( + $this->acl, + $this->sut->getACL() + ); + } + + public function testSetAcl() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->sut->setACL([]); + } + + public function testGetSupportedPrivilegeSet() + { + $this->assertNull( + $this->sut->getSupportedPrivilegeSet() + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/HomeCollectionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/HomeCollectionTest.php new file mode 100644 index 0000000..9248056 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/FS/HomeCollectionTest.php @@ -0,0 +1,105 @@ +<?php + +declare(strict_types=1); + +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'; + + public function setup(): void + { + $principalBackend = new PrincipalBackend(); + + $this->path = SABRE_TEMPDIR.'/home'; + + $this->sut = new HomeCollection($principalBackend, $this->path); + $this->sut->collectionName = $this->name; + } + + public function teardown(): void + { + \Sabre\TestUtil::clearTempDir(); + } + + public function testGetName() + { + $this->assertEquals( + $this->name, + $this->sut->getName() + ); + } + + public 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()); + } + + public function testGetOwner() + { + $this->assertNull( + $this->sut->getOwner() + ); + } + + public function testGetGroup() + { + $this->assertNull( + $this->sut->getGroup() + ); + } + + public function testGetACL() + { + $acl = [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + ]; + + $this->assertEquals( + $acl, + $this->sut->getACL() + ); + } + + public function testSetAcl() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $this->sut->setACL([]); + } + + public function testGetSupportedPrivilegeSet() + { + $this->assertNull( + $this->sut->getSupportedPrivilegeSet() + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/MockACLNode.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/MockACLNode.php new file mode 100644 index 0000000..51411f3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/MockACLNode.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class MockACLNode extends DAV\Node implements IACL +{ + public $name; + public $acl; + + public function __construct($name, array $acl = []) + { + $this->name = $name; + $this->acl = $acl; + } + + public function getName() + { + return $this->name; + } + + public function getOwner() + { + return null; + } + + public function getGroup() + { + return null; + } + + public function getACL() + { + return $this->acl; + } + + public function setACL(array $acl) + { + $this->acl = $acl; + } + + public function getSupportedPrivilegeSet() + { + return null; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/MockPrincipal.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/MockPrincipal.php new file mode 100644 index 0000000..f67025c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/MockPrincipal.php @@ -0,0 +1,58 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class MockPrincipal extends DAV\Node implements IPrincipal +{ + public $name; + public $principalUrl; + public $groupMembership = []; + public $groupMemberSet = []; + + public function __construct($name, $principalUrl, array $groupMembership = [], array $groupMemberSet = []) + { + $this->name = $name; + $this->principalUrl = $principalUrl; + $this->groupMembership = $groupMembership; + $this->groupMemberSet = $groupMemberSet; + } + + public function getName() + { + return $this->name; + } + + public function getDisplayName() + { + return $this->getName(); + } + + public function getAlternateUriSet() + { + return []; + } + + public function getPrincipalUrl() + { + return $this->principalUrl; + } + + public function getGroupMemberSet() + { + return $this->groupMemberSet; + } + + public function getGroupMemberShip() + { + return $this->groupMembership; + } + + public function setGroupMemberSet(array $groupMemberSet) + { + $this->groupMemberSet = $groupMemberSet; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginAdminTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginAdminTest.php new file mode 100644 index 0000000..048b9f2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginAdminTest.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class PluginAdminTest extends \PHPUnit\Framework\TestCase +{ + public $server; + + public function setup(): void + { + $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); + } + + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginPropertiesTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginPropertiesTest.php new file mode 100644 index 0000000..16d3e78 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginPropertiesTest.php @@ -0,0 +1,399 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class PluginPropertiesTest extends \PHPUnit\Framework\TestCase +{ + public 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()); + } + + public 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:PROPFIND', [$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()); + } + + public 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); + } + } + + public 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('GET', '/'), 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']); + } + + public 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('GET', '/'), 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']); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginUpdatePropertiesTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginUpdatePropertiesTest.php new file mode 100644 index 0000000..e6796e0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PluginUpdatePropertiesTest.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class PluginUpdatePropertiesTest extends \PHPUnit\Framework\TestCase +{ + public 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); + } + + public 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()); + } + + public 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()); + } + + public function testSetBadValue() + { + $this->expectException('Sabre\DAV\Exception'); + $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(), + ]); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/AbstractPDOTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/AbstractPDOTest.php new file mode 100644 index 0000000..b18ab94 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/AbstractPDOTest.php @@ -0,0 +1,219 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\PrincipalBackend; + +use Sabre\DAV; + +abstract class AbstractPDOTest extends \PHPUnit\Framework\TestCase +{ + use DAV\DbTestHelperTrait; + + public function setup(): void + { + $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)'); + } + + public function testConstruct() + { + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertTrue($backend instanceof PDO); + } + + /** + * @depends testConstruct + */ + public 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 + */ + public 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')); + } + + public function testGetGroupMemberSet() + { + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $expected = ['principals/user']; + + $this->assertEquals($expected, $backend->getGroupMemberSet('principals/group')); + } + + public function testGetGroupMembership() + { + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $expected = ['principals/group']; + + $this->assertEquals($expected, $backend->getGroupMembership('principals/user')); + } + + public 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')); + } + + public 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); + } + + public 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')); + } + + public 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')); + } + + public function testFindByUriUnknownScheme() + { + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertNull($backend->findByUri('http://foo', 'principals')); + } + + public function testFindByUriWithMailtoAddress() + { + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertEquals( + 'principals/user', + $backend->findByUri('mailto:user@example.org', 'principals') + ); + } + + public function testFindByUriWithUri() + { + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertEquals( + 'principals/user', + $backend->findByUri('principals/user', 'principals') + ); + } + + public function testFindByUriWithUnknownUri() + { + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertNull($backend->findByUri('principals/other', 'principals')); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/Mock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/Mock.php new file mode 100644 index 0000000..5f04345 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/Mock.php @@ -0,0 +1,158 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\PrincipalBackend; + +class Mock extends AbstractBackend +{ + public $groupMembers = []; + public $principals; + + public 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', + ], + ]; + } + } + + public function getPrincipalsByPrefix($prefix) + { + $prefix = trim($prefix, '/'); + if ($prefix) { + $prefix .= '/'; + } + $return = []; + + foreach ($this->principals as $principal) { + if ($prefix && 0 !== strpos($principal['uri'], $prefix)) { + continue; + } + + $return[] = $principal; + } + + return $return; + } + + public function addPrincipal(array $principal) + { + $this->principals[] = $principal; + } + + public function getPrincipalByPath($path) + { + foreach ($this->getPrincipalsByPrefix('principals') as $principal) { + if ($principal['uri'] === $path) { + return $principal; + } + } + } + + public 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 (false === mb_stripos($principal[$key], $value, 0, 'UTF-8')) { + continue 2; + } + + // We have a match for this searchProperty! + if ('allof' === $test) { + continue; + } else { + break; + } + } + $matches[] = $principal['uri']; + } + + return $matches; + } + + public function getGroupMemberSet($path) + { + return isset($this->groupMembers[$path]) ? $this->groupMembers[$path] : []; + } + + public function getGroupMembership($path) + { + $membership = []; + foreach ($this->groupMembers as $group => $members) { + if (in_array($path, $members)) { + $membership[] = $group; + } + } + + return $membership; + } + + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOMySQLTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOMySQLTest.php new file mode 100644 index 0000000..54795cf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOMySQLTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\PrincipalBackend; + +class PDOMySQLTest extends AbstractPDOTest +{ + public $driver = 'mysql'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOPgSqlTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOPgSqlTest.php new file mode 100644 index 0000000..7abc816 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOPgSqlTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\PrincipalBackend; + +class PDOPgSqlTest extends AbstractPDOTest +{ + public $driver = 'pgsql'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOSqliteTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOSqliteTest.php new file mode 100644 index 0000000..549e0bd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOSqliteTest.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\PrincipalBackend; + +class PDOSqliteTest extends AbstractPDOTest +{ + public $driver = 'sqlite'; +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalCollectionTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalCollectionTest.php new file mode 100644 index 0000000..2777281 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalCollectionTest.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +class PrincipalCollectionTest extends \PHPUnit\Framework\TestCase +{ + public function testBasic() + { + $backend = new PrincipalBackend\Mock(); + $pc = new PrincipalCollection($backend); + $this->assertTrue($pc instanceof PrincipalCollection); + + $this->assertEquals('principals', $pc->getName()); + } + + /** + * @depends testBasic + */ + public 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 + */ + public function testGetChildrenDisable() + { + $this->expectException('Sabre\DAV\Exception\MethodNotAllowed'); + $backend = new PrincipalBackend\Mock(); + $pc = new PrincipalCollection($backend); + $pc->disableListing = true; + + $children = $pc->getChildren(); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalMatchTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalMatchTest.php new file mode 100644 index 0000000..0746d2e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalMatchTest.php @@ -0,0 +1,121 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\HTTP\Request; + +class PrincipalMatchTest extends \Sabre\DAVServerTest +{ + public $setupACL = true; + public $autoLogin = 'user1'; + + public 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() + ); + } + + public 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() + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalPropertySearchTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalPropertySearchTest.php new file mode 100644 index 0000000..6883f25 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalPropertySearchTest.php @@ -0,0 +1,389 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class PrincipalPropertySearchTest extends \PHPUnit\Framework\TestCase +{ + public 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; + } + + public 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()); + } + + public 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()); + } + + public 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(); + + $bodyAsString = $server->httpResponse->getBodyAsString(); + $this->assertEquals(207, $server->httpResponse->status, $bodyAsString); + $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($bodyAsString); + $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->getBodyAsString()); + } + } + + public 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(); + + $bodyAsString = $server->httpResponse->getBodyAsString(); + $this->assertEquals(207, $server->httpResponse->status, $bodyAsString); + $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($bodyAsString); + $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->getBodyAsString()); + } + } + + public 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(); + + $bodyAsString = $server->httpResponse->getBodyAsString(); + $this->assertEquals(207, $server->httpResponse->status, $bodyAsString); + $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($bodyAsString); + $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->getBodyAsString()); + } + } + + public 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(); + + $bodyAsString = $server->httpResponse->getBodyAsString(); + $this->assertEquals(207, $server->httpResponse->status, $bodyAsString); + $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($bodyAsString); + $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->getBodyAsString()); + } + } +} + +class MockPlugin extends Plugin +{ + public function getCurrentUserPrivilegeSet($node) + { + return [ + '{DAV:}read', + '{DAV:}write', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalSearchPropertySetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalSearchPropertySetTest.php new file mode 100644 index 0000000..ec834fe --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalSearchPropertySetTest.php @@ -0,0 +1,135 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class PrincipalSearchPropertySetTest extends \PHPUnit\Framework\TestCase +{ + public 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; + } + + public 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()); + } + + public 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->getBodyAsString()); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + } + + public 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(); + + $bodyAsString = $server->httpResponse->getBodyAsString(); + $this->assertEquals(200, $server->httpResponse->status, $bodyAsString); + $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($bodyAsString); + $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: '.$bodyAsString); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalTest.php new file mode 100644 index 0000000..7e1656a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/PrincipalTest.php @@ -0,0 +1,192 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class PrincipalTest extends \PHPUnit\Framework\TestCase +{ + public function testConstruct() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertTrue($principal instanceof Principal); + } + + public function testConstructNoUri() + { + $this->expectException('Sabre\DAV\Exception'); + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, []); + } + + public function testGetName() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals('admin', $principal->getName()); + } + + public 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()); + } + + public 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']); + } + + public 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); + } + + public function testGetPrincipalUrl() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals('principals/admin', $principal->getPrincipalUrl()); + } + + public 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()); + } + + public function testGetAlternateUriSetEmpty() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, [ + 'uri' => 'principals/admin', + ]); + + $expected = []; + + $this->assertEquals($expected, $principal->getAlternateUriSet()); + } + + public function testGetGroupMemberSet() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals([], $principal->getGroupMemberSet()); + } + + public function testGetGroupMembership() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals([], $principal->getGroupMembership()); + } + + public 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); + } + + public function testGetOwner() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals('principals/admin', $principal->getOwner()); + } + + public function testGetGroup() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertNull($principal->getGroup()); + } + + public 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()); + } + + public function testSetACl() + { + $this->expectException('Sabre\DAV\Exception\Forbidden'); + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $principal->setACL([]); + } + + public function testGetSupportedPrivilegeSet() + { + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertNull($principal->getSupportedPrivilegeSet()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/SimplePluginTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/SimplePluginTest.php new file mode 100644 index 0000000..effa158 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/SimplePluginTest.php @@ -0,0 +1,302 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class SimplePluginTest extends \PHPUnit\Framework\TestCase +{ + public 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'] + ); + } + + public 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('')); + } + + public function testCurrentUserPrincipalsNotLoggedIn() + { + $acl = new Plugin(); + $acl->allowUnauthenticatedAccess = false; + $server = new DAV\Server(); + $server->addPlugin($acl); + + $this->assertEquals([], $acl->getCurrentUserPrincipals()); + } + + public 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('GET', '/'), new HTTP\Response()); + + $this->assertEquals(['principals/admin'], $acl->getCurrentUserPrincipals()); + } + + public 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('GET', '/'), 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()); + } + + public 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')); + } + + public 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('GET', '/'), 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')); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/ACLTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/ACLTest.php new file mode 100644 index 0000000..26b9f68 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/ACLTest.php @@ -0,0 +1,326 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; + +class ACLTest extends \PHPUnit\Framework\TestCase +{ + public function testConstruct() + { + $acl = new Acl([]); + $this->assertInstanceOf('Sabre\DAVACL\Xml\Property\ACL', $acl); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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()); + } + + public function testUnserializeNoPrincipal() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $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(); + } + + public 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()); + } + + public function testUnserializeDeny() + { + $this->expectException('Sabre\DAV\Exception\NotImplemented'); + $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(); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/AclRestrictionsTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/AclRestrictionsTest.php new file mode 100644 index 0000000..29ebc5a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/AclRestrictionsTest.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; + +class AclRestrictionsTest extends \PHPUnit\Framework\TestCase +{ + public function testConstruct() + { + $prop = new AclRestrictions(); + $this->assertInstanceOf('Sabre\DAVACL\Xml\Property\AclRestrictions', $prop); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/CurrentUserPrivilegeSetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/CurrentUserPrivilegeSetTest.php new file mode 100644 index 0000000..0ced53d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/CurrentUserPrivilegeSetTest.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\Xml\Reader; + +class CurrentUserPrivilegeSetTest extends \PHPUnit\Framework\TestCase +{ + public 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); + } + + public 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')); + } + + public function parse($xml) + { + $reader = new Reader(); + $reader->elementMap['{DAV:}root'] = 'Sabre\\DAVACL\\Xml\\Property\\CurrentUserPrivilegeSet'; + $reader->xml($xml); + $result = $reader->parse(); + + return $result['value']; + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/PrincipalTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/PrincipalTest.php new file mode 100644 index 0000000..694e015 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/PrincipalTest.php @@ -0,0 +1,175 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\Xml\Reader; + +class PrincipalTest extends \PHPUnit\Framework\TestCase +{ + public 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 + */ + public function testNoHref() + { + $this->expectException('Sabre\DAV\Exception'); + $principal = new Principal(Principal::HREF); + } + + /** + * @depends testSimple + */ + public 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 + */ + public 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 + */ + public 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); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public function testUnserializeUnknown() + { + $this->expectException('Sabre\DAV\Exception\BadRequest'); + $xml = '<?xml version="1.0"?> +<d:principal xmlns:d="DAV:">'. +' <d:foo />'. +'</d:principal>'; + + $this->parse($xml); + } + + public 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 + */ + public function testToHtml($principal, $output) + { + $html = $principal->toHtml(new HtmlOutputHelper('/', [])); + + $this->assertXmlStringEqualsXmlString( + $output, + $html + ); + } + + /** + * Provides data for the html tests. + * + * @return array + */ + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/SupportedPrivilegeSetTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/SupportedPrivilegeSetTest.php new file mode 100644 index 0000000..a6797f3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Property/SupportedPrivilegeSetTest.php @@ -0,0 +1,99 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; + +class SupportedPrivilegeSetTest extends \PHPUnit\Framework\TestCase +{ + public function testSimple() + { + $prop = new SupportedPrivilegeSet([ + 'privilege' => '{DAV:}all', + ]); + $this->assertInstanceOf('Sabre\DAVACL\Xml\Property\SupportedPrivilegeSet', $prop); + } + + /** + * @depends testSimple + */ + public 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 + */ + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Request/AclPrincipalPropSetReportTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Request/AclPrincipalPropSetReportTest.php new file mode 100644 index 0000000..0c31a60 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Request/AclPrincipalPropSetReportTest.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Xml\Request; + +class AclPrincipalPropSetReportTest extends \Sabre\DAV\Xml\XmlTest +{ + protected $elementMap = [ + '{DAV:}acl-principal-prop-set' => 'Sabre\DAVACL\Xml\Request\AclPrincipalPropSetReport', + ]; + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Request/PrincipalMatchReportTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Request/PrincipalMatchReportTest.php new file mode 100644 index 0000000..39d6a21 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVACL/Xml/Request/PrincipalMatchReportTest.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\DAVACL\Xml\Request; + +class PrincipalMatchReportTest extends \Sabre\DAV\Xml\XmlTest +{ + protected $elementMap = [ + '{DAV:}principal-match' => 'Sabre\DAVACL\Xml\Request\PrincipalMatchReport', + ]; + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVServerTest.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVServerTest.php new file mode 100644 index 0000000..2f64df0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/DAVServerTest.php @@ -0,0 +1,305 @@ +<?php + +declare(strict_types=1); + +namespace Sabre; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +/** + * 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 CalDAV\ICSExportPlugin + */ + protected $caldavICSExportPlugin; + + /** + * @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; + + public function setup(): void + { + $this->initializeEverything(); + } + + public 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 + */ + public function request($request, $expectedStatus = null) + { + if (is_array($request)) { + $request = HTTP\Sapi::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 + */ + public 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('GET', '/'), new Response()); + } + + /** + * Override this to provide your own Tree for your test-case. + */ + public 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'); + } + } + + public 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(); + } + } + + public function assertHttpStatus($expectedStatus, HTTP\Request $req) + { + $resp = $this->request($req); + $this->assertEquals((int) $expectedStatus, (int) $resp->getStatus(), 'Incorrect HTTP status received: '.$resp->getStatus()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php new file mode 100644 index 0000000..c535792 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +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/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/HTTP/SapiMock.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/HTTP/SapiMock.php new file mode 100644 index 0000000..4860030 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/HTTP/SapiMock.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +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 +{ + public static $sent = 0; + + /** + * Overriding this so nothing is ever echo'd. + */ + public static function sendResponse(ResponseInterface $response) + { + ++self::$sent; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/TestUtil.php b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/TestUtil.php new file mode 100644 index 0000000..4e7ca2f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/Sabre/TestUtil.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); + +namespace Sabre; + +class TestUtil +{ + /** + * This function deletes all the contents of the temporary directory. + */ + public 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); + } + } + + public 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; + } + } + + public static function getSQLiteDB() + { + $pdo = new \PDO('sqlite:'.SABRE_TEMPDIR.'/pdobackend'); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + return $pdo; + } + + public 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/plugins/panakour/backup/vendor/sabre/dav/tests/bootstrap.php b/plugins/panakour/backup/vendor/sabre/dav/tests/bootstrap.php new file mode 100644 index 0000000..d158053 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/bootstrap.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +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'); + +if ('TRUE' === getenv('RUN_TEST_WITH_STREAMING_PROPFIND')) { + echo 'Running unit tests with \Sabre\DAV\Server::$streamMultiStatus = true'; + \Sabre\DAV\Server::$streamMultiStatus = true; +} + +// List of variables that can be set by the environment +$environmentVars = [ + 'SABRE_MYSQLUSER', + 'SABRE_MYSQLPASS', + 'SABRE_MYSQLDSN', + 'SABRE_PGSQLDSN', +]; +foreach ($environmentVars as $var) { + if ($value = getenv($var)) { + define($var, $value); + } +} + +$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')) { + $userConfig = []; + 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/plugins/panakour/backup/vendor/sabre/dav/tests/phpunit.xml b/plugins/panakour/backup/vendor/sabre/dav/tests/phpunit.xml new file mode 100644 index 0000000..7ff4143 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/dav/tests/phpunit.xml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<phpunit + colors="true" + bootstrap="bootstrap.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutOutputDuringTests="true" +> + + <testsuites> + <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> + </testsuites> + + <filter> + <whitelist addUncoveredFilesFromWhitelist="true"> + <directory suffix=".php">../lib/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/plugins/panakour/backup/vendor/sabre/event/LICENSE b/plugins/panakour/backup/vendor/sabre/event/LICENSE new file mode 100644 index 0000000..962a492 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2013-2016 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/panakour/backup/vendor/sabre/event/composer.json b/plugins/panakour/backup/vendor/sabre/event/composer.json new file mode 100644 index 0000000..1447041 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/composer.json @@ -0,0 +1,50 @@ +{ + "name": "sabre/event", + "description": "sabre/event is a library for lightweight event-based programming", + "keywords": [ + "Events", + "EventEmitter", + "Promise", + "Hooks", + "Plugin", + "Signal", + "Async", + "EventLoop", + "Reactor", + "Coroutine" + ], + "homepage": "http://sabre.io/event/", + "license": "BSD-3-Clause", + "require": { + "php": "^7.1" + }, + "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": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit" : "^7 || ^8" + }, + "config" : { + "bin-dir" : "bin/" + } +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/Emitter.php b/plugins/panakour/backup/vendor/sabre/event/lib/Emitter.php new file mode 100644 index 0000000..e1f23fc --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/Emitter.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event; + +/** + * Emitter object. + * + * Instantiate this class, or subclass it for easily creating event emitters. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Emitter implements EmitterInterface +{ + use EmitterTrait; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/EmitterInterface.php b/plugins/panakour/backup/vendor/sabre/event/lib/EmitterInterface.php new file mode 100644 index 0000000..6ce0f34 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/EmitterInterface.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event; + +/** + * Event Emitter Interface. + * + * Anything that accepts listeners and emits events should implement 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 EmitterInterface +{ + /** + * Subscribe to an event. + */ + public function on(string $eventName, callable $callBack, int $priority = 100); + + /** + * Subscribe to an event exactly once. + */ + public function once(string $eventName, callable $callBack, int $priority = 100); + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were successfully + * handled. false is returned if one of the events broke the event chain. + * + * If the continueCallBack is specified, this callback will be called every + * time before the next event handler is called. + * + * If the continueCallback returns false, event propagation stops. This + * allows you to use the eventEmitter as a means for listeners to implement + * functionality in your application, and break the event loop as soon as + * some condition is fulfilled. + * + * Note that returning false from an event subscriber breaks propagation + * and returns false, but if the continue-callback stops propagation, this + * is still considered a 'successful' operation and returns true. + * + * Lastly, if there are 5 event handlers for an event. The continueCallback + * will be called at most 4 times. + */ + public function emit(string $eventName, array $arguments = [], callable $continueCallBack = null): bool; + + /** + * Returns the list of listeners for an event. + * + * The list is returned as an array, and the list of events are sorted by + * their priority. + * + * @return callable[] + */ + public function listeners(string $eventName): array; + + /** + * Removes a specific listener from an event. + * + * If the listener could not be found, this method will return false. If it + * was removed it will return true. + */ + public function removeListener(string $eventName, callable $listener): bool; + + /** + * Removes all listeners. + * + * If the eventName argument is specified, all listeners for that event are + * removed. If it is not specified, every listener for every event is + * removed. + */ + public function removeAllListeners(string $eventName = null); +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/EmitterTrait.php b/plugins/panakour/backup/vendor/sabre/event/lib/EmitterTrait.php new file mode 100644 index 0000000..5502ef9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/EmitterTrait.php @@ -0,0 +1,178 @@ +<?php + +declare(strict_types=1); + +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) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +trait EmitterTrait +{ + /** + * Subscribe to an event. + */ + public function on(string $eventName, callable $callBack, int $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. + */ + public function once(string $eventName, callable $callBack, int $priority = 100) + { + $wrapper = null; + $wrapper = function () use ($eventName, $callBack, &$wrapper) { + $this->removeListener($eventName, $wrapper); + + return \call_user_func_array($callBack, \func_get_args()); + }; + + $this->on($eventName, $wrapper, $priority); + } + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were successfully + * handled. false is returned if one of the events broke the event chain. + * + * If the continueCallBack is specified, this callback will be called every + * time before the next event handler is called. + * + * If the continueCallback returns false, event propagation stops. This + * allows you to use the eventEmitter as a means for listeners to implement + * functionality in your application, and break the event loop as soon as + * some condition is fulfilled. + * + * Note that returning false from an event subscriber breaks propagation + * and returns false, but if the continue-callback stops propagation, this + * is still considered a 'successful' operation and returns true. + * + * Lastly, if there are 5 event handlers for an event. The continueCallback + * will be called at most 4 times. + */ + public function emit(string $eventName, array $arguments = [], callable $continueCallBack = null): bool + { + if (\is_null($continueCallBack)) { + foreach ($this->listeners($eventName) as $listener) { + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + } + } else { + $listeners = $this->listeners($eventName); + $counter = \count($listeners); + + foreach ($listeners as $listener) { + --$counter; + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + + if ($counter > 0) { + if (!$continueCallBack()) { + break; + } + } + } + } + + return true; + } + + /** + * Returns the list of listeners for an event. + * + * The list is returned as an array, and the list of events are sorted by + * their priority. + * + * @return callable[] + */ + public function listeners(string $eventName): array + { + if (!isset($this->listeners[$eventName])) { + return []; + } + + // The list is not sorted + if (!$this->listeners[$eventName][0]) { + // Sorting + \array_multisort($this->listeners[$eventName][1], SORT_NUMERIC, $this->listeners[$eventName][2]); + + // Marking the listeners as sorted + $this->listeners[$eventName][0] = true; + } + + return $this->listeners[$eventName][2]; + } + + /** + * Removes a specific listener from an event. + * + * If the listener could not be found, this method will return false. If it + * was removed it will return true. + */ + public function removeListener(string $eventName, callable $listener): bool + { + if (!isset($this->listeners[$eventName])) { + return false; + } + foreach ($this->listeners[$eventName][2] as $index => $check) { + if ($check === $listener) { + unset($this->listeners[$eventName][1][$index]); + unset($this->listeners[$eventName][2][$index]); + + return true; + } + } + + return false; + } + + /** + * Removes all listeners. + * + * If the eventName argument is specified, all listeners for that event are + * removed. If it is not specified, every listener for every event is + * removed. + */ + public function removeAllListeners(string $eventName = null) + { + if (!\is_null($eventName)) { + unset($this->listeners[$eventName]); + } else { + $this->listeners = []; + } + } + + /** + * The list of listeners. + * + * @var array + */ + protected $listeners = []; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/EventEmitter.php b/plugins/panakour/backup/vendor/sabre/event/lib/EventEmitter.php new file mode 100644 index 0000000..18971e3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/EventEmitter.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event; + +/** + * This is the old name for the Emitter class. + * + * Instead of of using EventEmitter, please use Emitter. They are identical + * otherwise. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class EventEmitter implements EmitterInterface +{ + use EmitterTrait; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/Loop/Loop.php b/plugins/panakour/backup/vendor/sabre/event/lib/Loop/Loop.php new file mode 100644 index 0000000..ec09be9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/Loop/Loop.php @@ -0,0 +1,341 @@ +<?php + +declare(strict_types=1); + +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) 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. + */ + public function setTimeout(callable $cb, float $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 (0 === $index) { + array_unshift($this->timers, [$triggerTime, $cb]); + break; + } + --$index; + } + } + + /** + * Executes a function every x seconds. + * + * The value this function returns can be used to stop the interval with + * clearInterval. + */ + public function setInterval(callable $cb, float $timeout): array + { + $keepGoing = true; + $f = null; + + $f = function () use ($cb, &$f, $timeout, &$keepGoing) { + if ($keepGoing) { + $cb(); + $this->setTimeout($f, $timeout); + } + }; + $this->setTimeout($f, $timeout); + + // Really the only thing that matters is returning the $keepGoing + // boolean value. + // + // We need to pack it in an array to allow returning by reference. + // Because I'm worried people will be confused by using a boolean as a + // sort of identifier, I added an extra string. + return ['I\'m an implementation detail', &$keepGoing]; + } + + /** + * Stops a running interval. + */ + public function clearInterval(array $intervalId) + { + $intervalId[1] = false; + } + + /** + * Runs a function immediately at the next iteration of the loop. + */ + public function nextTick(callable $cb) + { + $this->nextTick[] = $cb; + } + + /** + * Adds a read stream. + * + * The callback will be called as soon as there is something to read from + * the stream. + * + * You MUST call removeReadStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ + public function addReadStream($stream, callable $cb) + { + $this->readStreams[(int) $stream] = $stream; + $this->readCallbacks[(int) $stream] = $cb; + } + + /** + * Adds a write stream. + * + * The callback will be called as soon as the system reports it's ready to + * receive writes on the stream. + * + * You MUST call removeWriteStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ + public function addWriteStream($stream, callable $cb) + { + $this->writeStreams[(int) $stream] = $stream; + $this->writeCallbacks[(int) $stream] = $cb; + } + + /** + * Stop watching a stream for reads. + * + * @param resource $stream + */ + public function removeReadStream($stream) + { + unset( + $this->readStreams[(int) $stream], + $this->readCallbacks[(int) $stream] + ); + } + + /** + * Stop watching a stream for writes. + * + * @param resource $stream + */ + public function removeWriteStream($stream) + { + unset( + $this->writeStreams[(int) $stream], + $this->writeCallbacks[(int) $stream] + ); + } + + /** + * Runs the loop. + * + * This function will run continuously, until there's no more events to + * handle. + */ + public function run() + { + $this->running = true; + + do { + $hasEvents = $this->tick(true); + } while ($this->running && $hasEvents); + $this->running = false; + } + + /** + * Executes all pending events. + * + * If $block is turned true, this function will block until any event is + * triggered. + * + * If there are now timeouts, nextTick callbacks or events in the loop at + * all, this function will exit immediately. + * + * This function will return true if there are _any_ events left in the + * loop after the tick. + */ + public function tick(bool $block = false): bool + { + $this->runNextTicks(); + $nextTimeout = $this->runTimers(); + + // Calculating how long runStreams should at most wait. + if (!$block) { + // Don't wait + $streamWait = 0; + } elseif ($this->nextTick) { + // There's a pending 'nextTick'. Don't wait. + $streamWait = 0; + } elseif (is_numeric($nextTimeout)) { + // Wait until the next Timeout should trigger. + $streamWait = $nextTimeout; + } else { + // Wait indefinitely + $streamWait = null; + } + + $this->runStreams($streamWait); + + return $this->readStreams || $this->writeStreams || $this->nextTick || $this->timers; + } + + /** + * Stops a running eventloop. + */ + public function stop() + { + $this->running = false; + } + + /** + * Executes all 'nextTick' callbacks. + * + * return void + */ + protected function runNextTicks() + { + $nextTick = $this->nextTick; + $this->nextTick = []; + + foreach ($nextTick as $cb) { + $cb(); + } + } + + /** + * Runs all pending timers. + * + * After running the timer callbacks, this function returns the number of + * seconds until the next timer should be executed. + * + * If there's no more pending timers, this function returns null. + * + * @return float|null + */ + protected function runTimers() + { + $now = microtime(true); + while (($timer = array_pop($this->timers)) && $timer[0] < $now) { + $timer[1](); + } + // Add the last timer back to the array. + if ($timer) { + $this->timers[] = $timer; + + return max(0, $timer[0] - microtime(true)); + } + } + + /** + * Runs all pending stream events. + * + * If $timeout is 0, it will return immediately. If $timeout is null, it + * will wait indefinitely. + * + * @param float|null timeout + */ + protected function runStreams($timeout) + { + if ($this->readStreams || $this->writeStreams) { + $read = $this->readStreams; + $write = $this->writeStreams; + $except = null; + if (stream_select($read, $write, $except, (null === $timeout) ? null : 0, $timeout ? (int) ($timeout * 1000000) : 0)) { + // See PHP Bug https://bugs.php.net/bug.php?id=62452 + // Fixed in PHP7 + foreach ($read as $readStream) { + $readCb = $this->readCallbacks[(int) $readStream]; + $readCb(); + } + foreach ($write as $writeStream) { + $writeCb = $this->writeCallbacks[(int) $writeStream]; + $writeCb(); + } + } + } elseif ($this->running && ($this->nextTick || $this->timers)) { + usleep(null !== $timeout ? intval($timeout * 1000000) : 200000); + } + } + + /** + * Is the main loop active. + * + * @var bool + */ + protected $running = false; + + /** + * A list of timers, added by setTimeout. + * + * @var array + */ + protected $timers = []; + + /** + * A list of 'nextTick' callbacks. + * + * @var callable[] + */ + protected $nextTick = []; + + /** + * List of readable streams for stream_select, indexed by stream id. + * + * @var resource[] + */ + protected $readStreams = []; + + /** + * List of writable streams for stream_select, indexed by stream id. + * + * @var resource[] + */ + protected $writeStreams = []; + + /** + * List of read callbacks, indexed by stream id. + * + * @var callable[] + */ + protected $readCallbacks = []; + + /** + * List of write callbacks, indexed by stream id. + * + * @var callable[] + */ + protected $writeCallbacks = []; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/Loop/functions.php b/plugins/panakour/backup/vendor/sabre/event/lib/Loop/functions.php new file mode 100644 index 0000000..bf4d933 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/Loop/functions.php @@ -0,0 +1,143 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event\Loop; + +/** + * Executes a function after x seconds. + */ +function setTimeout(callable $cb, float $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. + */ +function setInterval(callable $cb, float $timeout): array +{ + return instance()->setInterval($cb, $timeout); +} + +/** + * Stops a running interval. + */ +function clearInterval(array $intervalId) +{ + instance()->clearInterval($intervalId); +} + +/** + * Runs a function immediately at the next iteration of the loop. + */ +function nextTick(callable $cb) +{ + instance()->nextTick($cb); +} + +/** + * Adds a read stream. + * + * The callback will be called as soon as there is something to read from + * the stream. + * + * You MUST call removeReadStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ +function addReadStream($stream, callable $cb) +{ + instance()->addReadStream($stream, $cb); +} + +/** + * Adds a write stream. + * + * The callback will be called as soon as the system reports it's ready to + * receive writes on the stream. + * + * You MUST call removeWriteStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ +function addWriteStream($stream, callable $cb) +{ + instance()->addWriteStream($stream, $cb); +} + +/** + * Stop watching a stream for reads. + * + * @param resource $stream + */ +function removeReadStream($stream) +{ + instance()->removeReadStream($stream); +} + +/** + * Stop watching a stream for writes. + * + * @param resource $stream + */ +function removeWriteStream($stream) +{ + instance()->removeWriteStream($stream); +} + +/** + * Runs the loop. + * + * This function will run continuously, until there's no more events to + * handle. + */ +function run() +{ + instance()->run(); +} + +/** + * Executes all pending events. + * + * If $block is turned true, this function will block until any event is + * triggered. + * + * If there are now timeouts, nextTick callbacks or events in the loop at + * all, this function will exit immediately. + * + * This function will return true if there are _any_ events left in the + * loop after the tick. + */ +function tick(bool $block = false): bool +{ + return instance()->tick($block); +} + +/** + * Stops a running eventloop. + */ +function stop() +{ + instance()->stop(); +} + +/** + * Retrieves or sets the global Loop object. + */ +function instance(Loop $newLoop = null): Loop +{ + static $loop; + if ($newLoop) { + $loop = $newLoop; + } elseif (!$loop) { + $loop = new Loop(); + } + + return $loop; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/Promise.php b/plugins/panakour/backup/vendor/sabre/event/lib/Promise.php new file mode 100644 index 0000000..1d4ddd7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/Promise.php @@ -0,0 +1,258 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event; + +use Exception; +use Throwable; + +/** + * 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) 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. + */ + public function __construct(callable $executor = null) + { + if ($executor) { + $executor( + [$this, 'fulfill'], + [$this, 'reject'] + ); + } + } + + /** + * This method allows you to specify the callback that will be called after + * the promise has been fulfilled or rejected. + * + * Both arguments are optional. + * + * This method returns a new promise, which can be used for chaining. + * If either the onFulfilled or onRejected callback is called, you may + * return a result from this callback. + * + * If the result of this callback is yet another promise, the result of + * _that_ promise will be used to set the result of the returned promise. + * + * If either of the callbacks return any other value, the returned promise + * is automatically fulfilled with that value. + * + * If either of the callbacks throw an exception, the returned promise will + * be rejected and the exception will be passed back. + */ + public function then(callable $onFulfilled = null, callable $onRejected = null): Promise + { + // This new subPromise will be returned from this function, and will + // be fulfilled with the result of the onFulfilled or onRejected event + // handlers. + $subPromise = new self(); + + switch ($this->state) { + case self::PENDING: + // The operation is pending, so we keep a reference to the + // event handlers so we can call them later. + $this->subscribers[] = [$subPromise, $onFulfilled, $onRejected]; + break; + case self::FULFILLED: + // The async operation is already fulfilled, so we trigger the + // onFulfilled callback asap. + $this->invokeCallback($subPromise, $onFulfilled); + break; + case self::REJECTED: + // The async operation failed, so we call the onRejected + // callback asap. + $this->invokeCallback($subPromise, $onRejected); + break; + } + + return $subPromise; + } + + /** + * Add a callback for when this promise is rejected. + * + * Its usage is identical to then(). However, the otherwise() function is + * preferred. + */ + public function otherwise(callable $onRejected): Promise + { + return $this->then(null, $onRejected); + } + + /** + * Marks this promise as fulfilled and sets its return value. + * + * @param mixed $value + */ + public function fulfill($value = null) + { + if (self::PENDING !== $this->state) { + throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); + } + $this->state = self::FULFILLED; + $this->value = $value; + foreach ($this->subscribers as $subscriber) { + $this->invokeCallback($subscriber[0], $subscriber[1]); + } + } + + /** + * Marks this promise as rejected, and set it's rejection reason. + */ + public function reject(Throwable $reason) + { + if (self::PENDING !== $this->state) { + throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); + } + $this->state = self::REJECTED; + $this->value = $reason; + foreach ($this->subscribers as $subscriber) { + $this->invokeCallback($subscriber[0], $subscriber[2]); + } + } + + /** + * Stops execution until this promise is resolved. + * + * This method stops execution completely. If the promise is successful with + * a value, this method will return this value. If the promise was + * rejected, this method will throw an exception. + * + * This effectively turns the asynchronous operation into a synchronous + * one. In PHP it might be useful to call this on the last promise in a + * chain. + * + * @return mixed + */ + public function wait() + { + $hasEvents = true; + while (self::PENDING === $this->state) { + if (!$hasEvents) { + throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); + } + + // As long as the promise is not fulfilled, we tell the event loop + // to handle events, and to block. + $hasEvents = Loop\tick(true); + } + + if (self::FULFILLED === $this->state) { + // If the state of this promise is fulfilled, we can return the value. + return $this->value; + } else { + // If we got here, it means that the asynchronous operation + // errored. Therefore we need to throw an exception. + throw $this->value; + } + } + + /** + * A list of subscribers. Subscribers are the callbacks that want us to let + * them know if the callback was fulfilled or rejected. + * + * @var array + */ + protected $subscribers = []; + + /** + * The result of the promise. + * + * If the promise was fulfilled, this will be the result value. If the + * promise was rejected, this property hold the rejection reason. + * + * @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 callable $callBack + */ + private function invokeCallback(Promise $subPromise, callable $callBack = null) + { + // We use 'nextTick' to ensure that the event handlers are always + // triggered outside of the calling stack in which they were originally + // passed to 'then'. + // + // This makes the order of execution more predictable. + Loop\nextTick(function () use ($callBack, $subPromise) { + if (is_callable($callBack)) { + try { + $result = $callBack($this->value); + if ($result instanceof self) { + // If the callback (onRejected or onFulfilled) + // returned a promise, we only fulfill or reject the + // chained promise once that promise has also been + // resolved. + $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']); + } else { + // If the callback returned any other value, we + // immediately fulfill the chained promise. + $subPromise->fulfill($result); + } + } catch (Throwable $e) { + // If the event handler threw an exception, we need to make sure that + // the chained promise is rejected as well. + $subPromise->reject($e); + } + } else { + if (self::FULFILLED === $this->state) { + $subPromise->fulfill($this->value); + } else { + $subPromise->reject($this->value); + } + } + }); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/Promise/functions.php b/plugins/panakour/backup/vendor/sabre/event/lib/Promise/functions.php new file mode 100644 index 0000000..986fe2b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/Promise/functions.php @@ -0,0 +1,128 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event\Promise; + +use Sabre\Event\Promise; +use Throwable; + +/** + * This file contains a set of functions that are useful for dealing with the + * Promise object. + * + * @copyright Copyright (C) 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 immediately + * fail with the first Promise that fails, and its reason. + * + * @param Promise[] $promises + */ +function all(array $promises): Promise +{ + return new Promise(function ($success, $fail) use ($promises) { + if (empty($promises)) { + $success([]); + + return; + } + + $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; + } + )->otherwise( + function ($reason) use ($fail) { + $fail($reason); + } + ); + } + }); +} + +/** + * The race function returns a promise that resolves or rejects as soon as + * one of the promises in the argument resolves or rejects. + * + * The returned promise will resolve or reject with the value or reason of + * that first promise. + * + * @param Promise[] $promises + */ +function race(array $promises): Promise +{ + return new Promise(function ($success, $fail) use ($promises) { + $alreadyDone = false; + foreach ($promises as $promise) { + $promise->then( + function ($result) use ($success, &$alreadyDone) { + if ($alreadyDone) { + return; + } + $alreadyDone = true; + $success($result); + }, + function ($reason) use ($fail, &$alreadyDone) { + if ($alreadyDone) { + return; + } + $alreadyDone = true; + $fail($reason); + } + ); + } + }); +} + +/** + * Returns a Promise that resolves with the given value. + * + * If the value is a promise, the returned promise will attach itself to that + * promise and eventually get the same state as the followed promise. + * + * @param mixed $value + */ +function resolve($value): Promise +{ + if ($value instanceof Promise) { + return $value->then(); + } else { + $promise = new Promise(); + $promise->fulfill($value); + + return $promise; + } +} + +/** + * Returns a Promise that will reject with the given reason. + */ +function reject(Throwable $reason): Promise +{ + $promise = new Promise(); + $promise->reject($reason); + + return $promise; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php b/plugins/panakour/backup/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php new file mode 100644 index 0000000..abb6c10 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +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) 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/plugins/panakour/backup/vendor/sabre/event/lib/Version.php b/plugins/panakour/backup/vendor/sabre/event/lib/Version.php new file mode 100644 index 0000000..e98e2e3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/Version.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event; + +/** + * 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/ Modified BSD License + */ +class Version +{ + /** + * Full version number. + */ + const VERSION = '5.1.0'; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/WildcardEmitter.php b/plugins/panakour/backup/vendor/sabre/event/lib/WildcardEmitter.php new file mode 100644 index 0000000..1b7c248 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/WildcardEmitter.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event; + +/** + * This class is an EventEmitter with support for wildcard event handlers. + * + * What this means is that you can emit events like this: + * + * emit('change:firstName') + * + * and listen to this event like this: + * + * on('change:*') + * + * A few notes: + * + * - Wildcards only work at the end of an event name. + * - Currently you can only use 1 wildcard. + * - Using ":" as a separator is optional, but it's highly recommended to use + * some kind of separator. + * + * The WilcardEmitter is a bit slower than the regular Emitter. If you code + * must be very high performance, it might be better to try to use the other + * emitter. For must usage the difference is negligible though. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class WildcardEmitter implements EmitterInterface +{ + use WildcardEmitterTrait; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/WildcardEmitterTrait.php b/plugins/panakour/backup/vendor/sabre/event/lib/WildcardEmitterTrait.php new file mode 100644 index 0000000..206a8f3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/WildcardEmitterTrait.php @@ -0,0 +1,233 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event; + +/** + * Wildcard Emitter Trait. + * + * This trait provides the implementation for WildCardEmitter + * Refer to that class for the full documentation about this + * trait. + * + * Normally you can just instantiate that class, but if you want to add + * emitter functionality to existing classes, using the trait might be a + * better way to do this. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +trait WildcardEmitterTrait +{ + /** + * Subscribe to an event. + */ + public function on(string $eventName, callable $callBack, int $priority = 100) + { + // If it ends with a wildcard, we use the wildcardListeners array + if ('*' === $eventName[\strlen($eventName) - 1]) { + $eventName = \substr($eventName, 0, -1); + $listeners = &$this->wildcardListeners; + } else { + $listeners = &$this->listeners; + } + + // Always fully reset the listener index. This is fairly sane for most + // applications, because there's a clear "event registering" and "event + // emitting" phase, but can be slow if there's a lot adding and removing + // of listeners during emitting of events. + $this->listenerIndex = []; + + if (!isset($listeners[$eventName])) { + $listeners[$eventName] = []; + } + $listeners[$eventName][] = [$priority, $callBack]; + } + + /** + * Subscribe to an event exactly once. + */ + public function once(string $eventName, callable $callBack, int $priority = 100) + { + $wrapper = null; + $wrapper = function () use ($eventName, $callBack, &$wrapper) { + $this->removeListener($eventName, $wrapper); + + return \call_user_func_array($callBack, \func_get_args()); + }; + + $this->on($eventName, $wrapper, $priority); + } + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were successfully + * handled. false is returned if one of the events broke the event chain. + * + * If the continueCallBack is specified, this callback will be called every + * time before the next event handler is called. + * + * If the continueCallback returns false, event propagation stops. This + * allows you to use the eventEmitter as a means for listeners to implement + * functionality in your application, and break the event loop as soon as + * some condition is fulfilled. + * + * Note that returning false from an event subscriber breaks propagation + * and returns false, but if the continue-callback stops propagation, this + * is still considered a 'successful' operation and returns true. + * + * Lastly, if there are 5 event handlers for an event. The continueCallback + * will be called at most 4 times. + */ + public function emit(string $eventName, array $arguments = [], callable $continueCallBack = null): bool + { + if (\is_null($continueCallBack)) { + foreach ($this->listeners($eventName) as $listener) { + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + } + } else { + $listeners = $this->listeners($eventName); + $counter = \count($listeners); + + foreach ($listeners as $listener) { + --$counter; + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + + if ($counter > 0) { + if (!$continueCallBack()) { + break; + } + } + } + } + + return true; + } + + /** + * Returns the list of listeners for an event. + * + * The list is returned as an array, and the list of events are sorted by + * their priority. + * + * @return callable[] + */ + public function listeners(string $eventName): array + { + if (!\array_key_exists($eventName, $this->listenerIndex)) { + // Create a new index. + $listeners = []; + $listenersPriority = []; + if (isset($this->listeners[$eventName])) { + foreach ($this->listeners[$eventName] as $listener) { + $listenersPriority[] = $listener[0]; + $listeners[] = $listener[1]; + } + } + + foreach ($this->wildcardListeners as $wcEvent => $wcListeners) { + // Wildcard match + if (\substr($eventName, 0, \strlen($wcEvent)) === $wcEvent) { + foreach ($wcListeners as $listener) { + $listenersPriority[] = $listener[0]; + $listeners[] = $listener[1]; + } + } + } + + // Sorting by priority + \array_multisort($listenersPriority, SORT_NUMERIC, $listeners); + + // Creating index + $this->listenerIndex[$eventName] = $listeners; + } + + return $this->listenerIndex[$eventName]; + } + + /** + * Removes a specific listener from an event. + * + * If the listener could not be found, this method will return false. If it + * was removed it will return true. + */ + public function removeListener(string $eventName, callable $listener): bool + { + // If it ends with a wildcard, we use the wildcardListeners array + if ('*' === $eventName[\strlen($eventName) - 1]) { + $eventName = \substr($eventName, 0, -1); + $listeners = &$this->wildcardListeners; + } else { + $listeners = &$this->listeners; + } + + if (!isset($listeners[$eventName])) { + return false; + } + + foreach ($listeners[$eventName] as $index => $check) { + if ($check[1] === $listener) { + // Remove listener + unset($listeners[$eventName][$index]); + // Reset index + $this->listenerIndex = []; + + return true; + } + } + + return false; + } + + /** + * Removes all listeners. + * + * If the eventName argument is specified, all listeners for that event are + * removed. If it is not specified, every listener for every event is + * removed. + */ + public function removeAllListeners(string $eventName = null) + { + if (\is_null($eventName)) { + $this->listeners = []; + $this->wildcardListeners = []; + } else { + if ('*' === $eventName[\strlen($eventName) - 1]) { + // Wildcard event + unset($this->wildcardListeners[\substr($eventName, 0, -1)]); + } else { + unset($this->listeners[$eventName]); + } + } + + // Reset index + $this->listenerIndex = []; + } + + /** + * The list of listeners. + */ + protected $listeners = []; + + /** + * The list of "wildcard listeners". + */ + protected $wildcardListeners = []; + + /** + * An index of listeners for a specific event name. This helps speeding + * up emitting events after all listeners have been set. + * + * If the list of listeners changes though, the index clears. + */ + protected $listenerIndex = []; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/lib/coroutine.php b/plugins/panakour/backup/vendor/sabre/event/lib/coroutine.php new file mode 100644 index 0000000..a6a2baf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/lib/coroutine.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\Event; + +use Generator; +use Throwable; + +/** + * 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(\Throwable $reason) { + * echo "Failed because: $reason\n"; + * } + * + * }); + * + * @return \Sabre\Event\Promise + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +function coroutine(callable $gen): Promise +{ + $generator = $gen(); + if (!$generator instanceof Generator) { + throw new \InvalidArgumentException('You must pass a generator function'); + } + + // This is the value we're returning. + $promise = new Promise(); + + /** + * So tempted to use the mythical y-combinator here, but it's not needed in + * PHP. + */ + $advanceGenerator = function () use (&$advanceGenerator, $generator, $promise) { + while ($generator->valid()) { + $yieldedValue = $generator->current(); + if ($yieldedValue instanceof Promise) { + $yieldedValue->then( + function ($value) use ($generator, &$advanceGenerator) { + $generator->send($value); + $advanceGenerator(); + }, + function (Throwable $reason) use ($generator, $advanceGenerator) { + $generator->throw($reason); + $advanceGenerator(); + } + )->otherwise(function (Throwable $reason) use ($promise) { + // This error handler would be called, if something in the + // generator throws an exception, and it's not caught + // locally. + $promise->reject($reason); + }); + // We need to break out of the loop, because $advanceGenerator + // will be called asynchronously when the promise has a result. + break; + } else { + // If the value was not a promise, we'll just let it pass through. + $generator->send($yieldedValue); + } + } + + // If the generator is at the end, and we didn't run into an exception, + // We're grabbing the "return" value and fulfilling our top-level + // promise with its value. + if (!$generator->valid() && Promise::PENDING === $promise->state) { + $returnValue = $generator->getReturn(); + + // The return value is a promise. + if ($returnValue instanceof Promise) { + $returnValue->then(function ($value) use ($promise) { + $promise->fulfill($value); + }, function (Throwable $reason) use ($promise) { + $promise->reject($reason); + }); + } else { + $promise->fulfill($returnValue); + } + } + }; + + try { + $advanceGenerator(); + } catch (Throwable $e) { + $promise->reject($e); + } + + return $promise; +} diff --git a/plugins/panakour/backup/vendor/sabre/event/phpstan.neon b/plugins/panakour/backup/vendor/sabre/event/phpstan.neon new file mode 100644 index 0000000..213da6d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/event/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + level: 1 diff --git a/plugins/panakour/backup/vendor/sabre/http/CHANGELOG.md b/plugins/panakour/backup/vendor/sabre/http/CHANGELOG.md new file mode 100644 index 0000000..716e09e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/CHANGELOG.md @@ -0,0 +1,333 @@ +ChangeLog +========= + +5.1.0 (2020-01-31) +------------------------- + +* Added support for PHP 7.4, dropped support for PHP 7.0 (@phil-davis) +* Updated testsuite for phpunit8, added phpstan coverage (@phil-davis) +* Added autoload-dev for test classes (@C0pyR1ght) + +5.0.5 (2019-11-28) +------------------------- + +* #138: Fixed possible infinite loop (@dpakach, @vfreex, @phil-davis) +* #136: Improvement regex content-range (@ho4ho) + +5.0.4 (2019-10-08) +------------------------- + +* #133: Fix short Content-Range download - Regression from 5.0.3 (@phil-davis) + +5.0.3 (2019-10-08) +------------------------- + +* #119: Significantly improve file download speed by enabling mmap based stream_copy_to_stream (@vfreex) + +5.0.2 (2019-09-12) +------------------------- + +* #125: Fix Strict Error if Response Body Empty (@WorksDev, @phil-davis) + +5.0.1 (2019-09-11) +------------------------- + +* #121: fix "Trying to access array offset on value of type bool" in 7.4 (@remicollet) +* #115: Reduce memory footprint when parsing HTTP result (@Gasol) +* #114: Misc code improvements (@mrcnpdlk) +* #111, #118: Added phpstan analysis (@DeepDiver1975, @staabm) +* #107: Tested with php 7.3 (@DeepDiver1975) + + +5.0.0 (2018-06-04) +------------------------- + +* #99: Previous CURL opts are not persisted anymore (@christiaan) +* Final release + +5.0.0-alpha1 (2018-02-16) +------------------------- + +* Now requires PHP 7.0+. +* Supports sabre/event 4.x and 5.x +* Depends on sabre/uri 2. +* hhvm is no longer supported starting this release. +* #65: It's now possible to supply request/response bodies using a callback + functions. This allows very high-speed/low-memory responses to be created. + (@petrkotek). +* Strict typing is used every where this is applicable. +* Removed `URLUtil` class. It was deprecated a long time ago, and most of + its functions moved to the `sabre/uri` package. +* Removed `Util` class. Most of its functions moved to the `functions.php` + file. +* #68: The `$method` and `$uri` arguments when constructing a Request object + are now required. +* When `Sapi::getRequest()` is called, we default to setting the HTTP Method + to `CLI`. +* The HTTP response is now initialized with HTTP code `500` instead of `null`, + so if it's not changed, it will be emitted as 500. +* #69: Sending `charset="UTF-8"` on Basic authentication challenges per + [rfc7617][rfc7617]. +* #84: Added support for `SERVER_PROTOCOL HTTP/2.0` (@jens1o) + + +4.2.3 (2017-06-12) +------------------ + +* #74, #77: Work around 4GB file size limit at 32 Bit systems + + +4.2.2 (2017-01-02) +------------------ + +* #72: Handling clients that send invalid `Content-Length` headers. + + +4.2.1 (2016-01-06) +------------------ + +* #56: `getBodyAsString` now returns at most as many bytes as the contents of + the `Content-Length` header. This allows users to pass much larger strings + without having to copy and truncate them. +* The client now sets a default `User-Agent` header identifying this library. + + +4.2.0 (2016-01-04) +------------------ + +* This package now supports sabre/event 3.0. + + +4.1.0 (2015-09-04) +------------------ + +* The async client wouldn't `wait()` for new http requests being started + after the (previous) last request in the queue was resolved. +* Added `Sabre\HTTP\Auth\Bearer`, to easily extract a OAuth2 bearer token. + + +4.0.0 (2015-05-20) +------------------ + +* Deprecated: All static functions from `Sabre\HTTP\URLUtil` and + `Sabre\HTTP\Util` moved to a separate `functions.php`, which is also + autoloaded. The old functions are still there, but will be removed in a + future version. (#49) + + +4.0.0-alpha3 (2015-05-19) +------------------------- + +* Added a parser for the HTTP `Prefer` header, as defined in [RFC7240][rfc7240]. +* Deprecated `Sabre\HTTP\Util::parseHTTPDate`, use `Sabre\HTTP\parseDate()`. +* Deprecated `Sabre\HTTP\Util::toHTTPDate` use `Sabre\HTTP\toDate()`. + + +4.0.0-alpha2 (2015-05-18) +------------------------- + +* #45: Don't send more data than what is promised in the HTTP content-length. + (@dratini0). +* #43: `getCredentials` returns null if incomplete. (@Hywan) +* #48: Now using php-cs-fixer to make our CS consistent (yay!) +* This includes fixes released in version 3.0.5. + + +4.0.0-alpha1 (2015-02-25) +------------------------- + +* #41: Fixing bugs related to comparing URLs in `Request::getPath()`. +* #41: This library now uses the `sabre/uri` package for uri handling. +* Added `421 Misdirected Request` from the HTTP/2.0 spec. + + +3.0.5 (2015-05-11) +------------------ + +* #47 #35: When re-using the client and doing any request after a `HEAD` + request, the client discards the body. + + +3.0.4 (2014-12-10) +------------------ + +* #38: The Authentication helpers no longer overwrite any existing + `WWW-Authenticate` headers, but instead append new headers. This ensures + that multiple authentication systems can exist in the same environment. + + +3.0.3 (2014-12-03) +------------------ + +* Hiding `Authorization` header value from `Request::__toString`. + + +3.0.2 (2014-10-09) +------------------ + +* When parsing `Accept:` headers, we're ignoring invalid parts. Before we + would throw a PHP E_NOTICE. + + +3.0.1 (2014-09-29) +------------------ + +* Minor change in unittests. + + +3.0.0 (2014-09-23) +------------------ + +* `getHeaders()` now returns header values as an array, just like psr/http. +* Added `hasHeader()`. + + +2.1.0-alpha1 (2014-09-15) +------------------------- + +* Changed: Copied most of the header-semantics for the PSR draft for + representing HTTP messages. [Reference here][psr-http]. +* This means that `setHeaders()` does not wipe out every existing header + anymore. +* We also support multiple headers with the same name. +* Use `Request::getHeaderAsArray()` and `Response::getHeaderAsArray()` to + get a hold off multiple headers with the same name. +* If you use `getHeader()`, and there's more than 1 header with that name, we + concatenate all these with a comma. +* `addHeader()` will now preserve an existing header with that name, and add a + second header with the same name. +* The message class should be a lot faster now for looking up headers. No more + array traversal, because we maintain a tiny index. +* Added: `URLUtil::resolve()` to make resolving relative urls super easy. +* Switched to PSR-4. +* #12: Circumventing CURL's FOLLOW_LOCATION and doing it in PHP instead. This + fixes compatibility issues with people that have open_basedir turned on. +* Added: Content negotiation now correctly support mime-type parameters such as + charset. +* Changed: `Util::negotiate()` is now deprecated. Use + `Util::negotiateContentType()` instead. +* #14: The client now only follows http and https urls. + + +2.0.4 (2014-07-14) +------------------ + +* Changed: No longer escaping @ in urls when it's not needed. +* Fixed: #7: Client now correctly deals with responses without a body. + + +2.0.3 (2014-04-17) +------------------ + +* Now works on hhvm! +* Fixed: Now throwing an error when a Request object is being created with + arguments that were valid for sabre/http 1.0. Hopefully this will aid with + debugging for upgraders. + + +2.0.2 (2014-02-09) +------------------ + +* Fixed: Potential security problem in the client. + + +2.0.1 (2014-01-09) +------------------ + +* Fixed: getBodyAsString on an empty body now works. +* Fixed: Version string + + +2.0.0 (2014-01-08) +------------------ + +* Removed: Request::createFromPHPRequest. This is now handled by + Sapi::getRequest. + + +2.0.0alpha6 (2014-01-03) +------------------------ + +* Added: Asynchronous HTTP client. See examples/asyncclient.php. +* Fixed: Issue #4: Don't escape colon (:) when it's not needed. +* Fixed: Fixed a bug in the content negotation script. +* Fixed: Fallback for when CURLOPT_POSTREDIR is not defined (mainly for hhvm). +* Added: The Request and Response object now have a `__toString()` method that + serializes the objects into a standard HTTP message. This is mainly for + debugging purposes. +* Changed: Added Response::getStatusText(). This method returns the + human-readable HTTP status message. This part has been removed from + Response::getStatus(), which now always returns just the status code as an + int. +* Changed: Response::send() is now Sapi::sendResponse($response). +* Changed: Request::createFromPHPRequest is now Sapi::getRequest(). +* Changed: Message::getBodyAsStream and Message::getBodyAsString were added. The + existing Message::getBody changed 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 +[rfc7240]: http://tools.ietf.org/html/rfc7240 +[rfc7617]: https://tools.ietf.org/html/rfc7617 diff --git a/plugins/panakour/backup/vendor/sabre/http/LICENSE b/plugins/panakour/backup/vendor/sabre/http/LICENSE new file mode 100644 index 0000000..864041b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2009-2017 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/panakour/backup/vendor/sabre/http/README.md b/plugins/panakour/backup/vendor/sabre/http/README.md new file mode 100644 index 0000000..8159b4d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/README.md @@ -0,0 +1,747 @@ +sabre/http +========== + +This library provides a toolkit to make working with the [HTTP protocol](https://tools.ietf.org/html/rfc2616) easier. + +Most PHP scripts run within a HTTP request but accessing information about the +HTTP request is cumbersome at least. + +There's bad practices, inconsistencies and confusion. This library is +effectively a wrapper around the following PHP constructs: + +For Input: + +* `$_GET`, +* `$_POST`, +* `$_SERVER`, +* `php://input` or `$HTTP_RAW_POST_DATA`. + +For output: + +* `php://output` or `echo`, +* `header()`. + +What this library provides, is a `Request` object, and a `Response` object. + +The objects are extendable and easily mockable. + +Build status +------------ + +| branch | status | +| ------ | ------ | +| master | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=master)](https://travis-ci.org/sabre-io/http) | +| 4.2 | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=4.2)](https://travis-ci.org/sabre-io/http) | +| 3.0 | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=3.0)](https://travis-ci.org/sabre-io/http) | + +Installation +------------ + +Make sure you have [composer][1] installed. In your project directory, create, +or edit a `composer.json` file, and make sure it contains something like this: + +```json +{ + "require" : { + "sabre/http" : "~5.0.0" + } +} +``` + +After that, just hit `composer install` and you should be rolling. + +Quick history +------------- + +This library came to existence in 2009, as a part of the [`sabre/dav`][2] +project, which uses it heavily. + +It got split off into a separate library to make it easier to manage +releases and hopefully giving it use outside of the scope of just `sabre/dav`. + +Although completely independently developed, this library has a LOT of +overlap with [Symfony's `HttpFoundation`][3]. + +Said library does a lot more stuff and is significantly more popular, +so if you are looking for something to fulfill this particular requirement, +I'd recommend also considering [`HttpFoundation`][3]. + + +Getting started +--------------- + +First and foremost, this library wraps the superglobals. The easiest way to +instantiate a request object is as follows: + +```php +use Sabre\HTTP; + +include 'vendor/autoload.php'; + +$request = HTTP\Sapi::getRequest(); +``` + +This line should only happen once in your entire application. Everywhere else +you should pass this request object around using dependency injection. + +You should always typehint on 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, 1.1 or 2.0. + * + * @param string $version + * @return void + */ +function setHttpVersion($version); + +/** + * Returns the HTTP version. + * + * @return string + */ +function getHttpVersion(); +``` + +### Response + +```php +/** + * Returns the current HTTP status. + * + * This is the status-code as well as the human readable string. + * + * @return string + */ +function getStatus(); + +/** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + * @throws \InvalidArgumentExeption + * @return void + */ +function setStatus($status); + +/** + * Returns the body as a readable stream resource. + * + * Note that the stream may not be rewindable, and therefore may only be + * read once. + * + * @return resource + */ +function getBodyAsStream(); + +/** + * Returns the body as a string. + * + * Note that because the underlying data may be based on a stream, this + * method could only work correctly the first time. + * + * @return string + */ +function getBodyAsString(); + +/** + * Returns the message body, as 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, 1.1 or 2.0. + * + * @param string $version + * @return void + */ +function setHttpVersion($version); + +/** + * Returns the HTTP version. + * + * @return string + */ +function getHttpVersion(); +``` + +Made at fruux +------------- + +This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support. + +[1]: http://getcomposer.org/ +[2]: http://sabre.io/ +[3]: https://github.com/symfony/HttpFoundation +[4]: http://php.net/curl +[5]: https://github.com/fruux/sabre-event +[6]: http://en.wikipedia.org/wiki/Decorator_pattern +[7]: http://guzzlephp.org/ +[8]: http://php.net/curl_multi_init diff --git a/plugins/panakour/backup/vendor/sabre/http/composer.json b/plugins/panakour/backup/vendor/sabre/http/composer.json new file mode 100644 index 0000000..198cf23 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/composer.json @@ -0,0 +1,50 @@ +{ + "name": "sabre/http", + "description" : "The sabre/http library provides utilities for dealing with http requests and responses. ", + "keywords" : [ "HTTP" ], + "homepage" : "https://github.com/fruux/sabre-http", + "license" : "BSD-3-Clause", + "require" : { + "php" : "^7.1", + "ext-mbstring" : "*", + "ext-ctype" : "*", + "ext-curl" : "*", + "sabre/event" : ">=4.0 <6.0", + "sabre/uri" : "^2.0" + }, + "require-dev" : { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit" : "^7.0 || ^8.0" + }, + "suggest" : { + "ext-curl" : " to make http requests with the Client class" + }, + "authors" : [ + { + "name" : "Evert Pot", + "email" : "me@evertpot.com", + "homepage" : "http://evertpot.com/", + "role" : "Developer" + } + ], + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-http" + }, + "autoload" : { + "files" : [ + "lib/functions.php" + ], + "psr-4" : { + "Sabre\\HTTP\\" : "lib/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\HTTP\\" : "tests/HTTP" + } + }, + "config" : { + "bin-dir" : "bin/" + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/examples/asyncclient.php b/plugins/panakour/backup/vendor/sabre/http/examples/asyncclient.php new file mode 100644 index 0000000..af98e25 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/examples/asyncclient.php @@ -0,0 +1,62 @@ +<?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 (Client::STATUS_CURLERROR === $error['status']) { + // Curl errors + echo "$i -> curl error: ".$error['curl_errmsg']."\n"; + } else { + // HTTP errors + echo "$i -> ".$error['response']->getStatus()."\n"; + } + } + ); +} + +// After everything is done, we call 'wait'. This causes the client to wait for +// all outstanding http requests to complete. +$client->wait(); diff --git a/plugins/panakour/backup/vendor/sabre/http/examples/basicauth.php b/plugins/panakour/backup/vendor/sabre/http/examples/basicauth.php new file mode 100644 index 0000000..9c13da8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/examples/basicauth.php @@ -0,0 +1,50 @@ +<?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/plugins/panakour/backup/vendor/sabre/http/examples/client.php b/plugins/panakour/backup/vendor/sabre/http/examples/client.php new file mode 100644 index 0000000..a7cbfc3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/examples/client.php @@ -0,0 +1,37 @@ +<?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/plugins/panakour/backup/vendor/sabre/http/examples/digestauth.php b/plugins/panakour/backup/vendor/sabre/http/examples/digestauth.php new file mode 100644 index 0000000..5594306 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/examples/digestauth.php @@ -0,0 +1,51 @@ +<?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/plugins/panakour/backup/vendor/sabre/http/examples/reverseproxy.php b/plugins/panakour/backup/vendor/sabre/http/examples/reverseproxy.php new file mode 100644 index 0000000..ce51ad4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/examples/reverseproxy.php @@ -0,0 +1,48 @@ +<?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/plugins/panakour/backup/vendor/sabre/http/examples/stringify.php b/plugins/panakour/backup/vendor/sabre/http/examples/stringify.php new file mode 100644 index 0000000..96f7da8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/examples/stringify.php @@ -0,0 +1,50 @@ +<?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/plugins/panakour/backup/vendor/sabre/http/lib/Auth/AWS.php b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/AWS.php new file mode 100644 index 0000000..ffda3cf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/AWS.php @@ -0,0 +1,220 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP; + +/** + * 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. + */ + public function init(): bool + { + $authHeader = $this->request->getHeader('Authorization'); + + if (null === $authHeader) { + $this->errorCode = self::ERR_NOAWSHEADER; + + return false; + } + $authHeader = explode(' ', $authHeader); + + if ('AWS' !== $authHeader[0] || !isset($authHeader[1])) { + $this->errorCode = self::ERR_NOAWSHEADER; + + return false; + } + + list($this->accessKey, $this->signature) = explode(':', $authHeader[1]); + + return true; + } + + /** + * Returns the username for the request. + */ + public function getAccessKey(): string + { + return $this->accessKey; + } + + /** + * Validates the signature based on the secretKey. + */ + public function validate(string $secretKey): bool + { + $contentMD5 = $this->request->getHeader('Content-MD5'); + + if ($contentMD5) { + // We need to validate the integrity of the request + $body = $this->request->getBody(); + $this->request->setBody($body); + + if ($contentMD5 !== base64_encode(md5((string) $body, true))) { + // content-md5 header did not match md5 signature of body + $this->errorCode = self::ERR_MD5CHECKSUMWRONG; + + return false; + } + } + + if (!$requestDate = $this->request->getHeader('x-amz-date')) { + $requestDate = $this->request->getHeader('Date'); + } + + if (!$this->validateRFC2616Date((string) $requestDate)) { + return false; + } + + $amzHeaders = $this->getAmzHeaders(); + + $signature = base64_encode( + $this->hmacsha1($secretKey, + $this->request->getMethod()."\n". + $contentMD5."\n". + $this->request->getHeader('Content-type')."\n". + $requestDate."\n". + $amzHeaders. + $this->request->getUrl() + ) + ); + + if ($this->signature !== $signature) { + $this->errorCode = self::ERR_INVALIDSIGNATURE; + + return false; + } + + return true; + } + + /** + * Returns an HTTP 401 header, forcing login. + * + * This should be called when username and password are incorrect, or not supplied at all + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'AWS'); + $this->response->setStatus(401); + } + + /** + * Makes sure the supplied value is a valid RFC2616 date. + * + * If we would just use strtotime to get a valid timestamp, we have no way of checking if a + * user just supplied the word 'now' for the date header. + * + * This function also makes sure the Date header is within 15 minutes of the operating + * system date, to prevent replay attacks. + */ + protected function validateRFC2616Date(string $dateHeader): bool + { + $date = HTTP\parseDate($dateHeader); + + // Unknown format + if (!$date) { + $this->errorCode = self::ERR_INVALIDDATEFORMAT; + + return false; + } + + $min = new \DateTime('-15 minutes'); + $max = new \DateTime('+15 minutes'); + + // We allow 15 minutes around the current date/time + if ($date > $max || $date < $min) { + $this->errorCode = self::ERR_REQUESTTIMESKEWED; + + return false; + } + + return true; + } + + /** + * Returns a list of AMZ headers. + */ + protected function getAmzHeaders(): string + { + $amzHeaders = []; + $headers = $this->request->getHeaders(); + foreach ($headers as $headerName => $headerValue) { + if (0 === strpos(strtolower($headerName), 'x-amz-')) { + $amzHeaders[strtolower($headerName)] = str_replace(["\r\n"], [' '], $headerValue[0])."\n"; + } + } + ksort($amzHeaders); + + $headerStr = ''; + foreach ($amzHeaders as $h => $v) { + $headerStr .= $h.':'.$v; + } + + return $headerStr; + } + + /** + * Generates an HMAC-SHA1 signature. + */ + private function hmacsha1(string $key, string $message): string + { + if (function_exists('hash_hmac')) { + return hash_hmac('sha1', $message, $key, true); + } + + $blocksize = 64; + if (strlen($key) > $blocksize) { + $key = pack('H*', sha1($key)); + } + $key = str_pad($key, $blocksize, chr(0x00)); + $ipad = str_repeat(chr(0x36), $blocksize); + $opad = str_repeat(chr(0x5c), $blocksize); + $hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message)))); + + return $hmac; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Auth/AbstractAuth.php b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/AbstractAuth.php new file mode 100644 index 0000000..ada6bf0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/AbstractAuth.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function __construct(string $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. + */ + abstract public function requireLogin(); + + /** + * Returns the HTTP realm. + */ + public function getRealm(): string + { + return $this->realm; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Basic.php b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Basic.php new file mode 100644 index 0000000..d04b4a8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Basic.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +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 array|null + */ + public function getCredentials() + { + $auth = $this->request->getHeader('Authorization'); + + if (!$auth) { + return null; + } + + if ('basic ' !== strtolower(substr($auth, 0, 6))) { + return null; + } + + $credentials = explode(':', base64_decode(substr($auth, 6)), 2); + + if (2 !== count($credentials)) { + return null; + } + + return $credentials; + } + + /** + * This method sends the needed HTTP header and statuscode (401) to force + * the user to login. + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'Basic realm="'.$this->realm.'", charset="UTF-8"'); + $this->response->setStatus(401); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Bearer.php b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Bearer.php new file mode 100644 index 0000000..988bb29 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Bearer.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +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 string|null + */ + public function getToken() + { + $auth = $this->request->getHeader('Authorization'); + + if (!$auth) { + return null; + } + + if ('bearer ' !== strtolower(substr($auth, 0, 7))) { + return null; + } + + return substr($auth, 7); + } + + /** + * This method sends the needed HTTP header and statuscode (401) to force + * authentication. + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'Bearer realm="'.$this->realm.'"'); + $this->response->setStatus(401); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Digest.php b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Digest.php new file mode 100644 index 0000000..dd35a0b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Auth/Digest.php @@ -0,0 +1,210 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function __construct(string $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. + */ + public function init() + { + $digest = $this->getDigest(); + $this->digestParts = $this->parseDigest((string) $digest); + } + + /** + * Sets the quality of protection value. + * + * Possible values are: + * Sabre\HTTP\DigestAuth::QOP_AUTH + * Sabre\HTTP\DigestAuth::QOP_AUTHINT + * + * Multiple values can be specified using logical OR. + * + * QOP_AUTHINT ensures integrity of the request body, but this is not + * supported by most HTTP clients. QOP_AUTHINT also requires the entire + * request body to be md5'ed, which can put strains on CPU and memory. + */ + public function setQOP(int $qop) + { + $this->qop = $qop; + } + + /** + * Validates the user. + * + * The A1 parameter should be md5($username . ':' . $realm . ':' . $password); + */ + public function validateA1(string $A1): bool + { + $this->A1 = $A1; + + return $this->validate(); + } + + /** + * Validates authentication through a password. The actual password must be provided here. + * It is strongly recommended not store the password in plain-text and use validateA1 instead. + */ + public function validatePassword(string $password): bool + { + $this->A1 = md5($this->digestParts['username'].':'.$this->realm.':'.$password); + + return $this->validate(); + } + + /** + * Returns the username for the request. + * Returns null if there were none. + * + * @return string|null + */ + public function getUsername() + { + return $this->digestParts['username'] ?? null; + } + + /** + * Validates the digest challenge. + */ + protected function validate(): bool + { + if (!is_array($this->digestParts)) { + return false; + } + + $A2 = $this->request->getMethod().':'.$this->digestParts['uri']; + + if ('auth-int' === $this->digestParts['qop']) { + // Making sure we support this qop value + if (!($this->qop & self::QOP_AUTHINT)) { + return false; + } + // We need to add an md5 of the entire request body to the A2 part of the hash + $body = $this->request->getBody($asString = true); + $this->request->setBody($body); + $A2 .= ':'.md5($body); + } elseif (!($this->qop & self::QOP_AUTH)) { + return false; + } + + $A2 = md5($A2); + + $validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}"); + + return $this->digestParts['response'] === $validResponse; + } + + /** + * Returns an HTTP 401 header, forcing login. + * + * This should be called when username and password are incorrect, or not supplied at all + */ + public function requireLogin() + { + $qop = ''; + switch ($this->qop) { + case self::QOP_AUTH: + $qop = 'auth'; + break; + case self::QOP_AUTHINT: + $qop = 'auth-int'; + break; + case self::QOP_AUTH | self::QOP_AUTHINT: + $qop = 'auth,auth-int'; + break; + } + + $this->response->addHeader('WWW-Authenticate', 'Digest realm="'.$this->realm.'",qop="'.$qop.'",nonce="'.$this->nonce.'",opaque="'.$this->opaque.'"'); + $this->response->setStatus(401); + } + + /** + * This method returns the full digest string. + * + * It should be compatibile with mod_php format and other webservers. + * + * If the header could not be found, null will be returned + * + * @return mixed + */ + public function getDigest() + { + return $this->request->getHeader('Authorization'); + } + + /** + * Parses the different pieces of the digest string into an array. + * + * This method returns false if an incomplete digest was supplied + * + * @return bool|array + */ + protected function parseDigest(string $digest) + { + // protect against missing data + $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; + $data = []; + + preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER); + + foreach ($matches as $m) { + $data[$m[1]] = $m[2] ?: $m[3]; + unset($needed_parts[$m[1]]); + } + + return $needed_parts ? false : $data; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Client.php b/plugins/panakour/backup/vendor/sabre/http/lib/Client.php new file mode 100644 index 0000000..b79c564 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Client.php @@ -0,0 +1,614 @@ +<?php + +declare(strict_types=1); + +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; + + protected $headerLinesMap = []; + + /** + * Initializes the client. + */ + public function __construct() + { + // See https://github.com/sabre-io/http/pull/115#discussion_r241292068 + // Preserve compatibility for sub-classes that implements their own method `parseCurlResult` + $separatedHeaders = __CLASS__ === get_class($this); + + $this->curlSettings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_NOBODY => false, + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + if ($separatedHeaders) { + $this->curlSettings[CURLOPT_HEADERFUNCTION] = [$this, 'receiveCurlHeader']; + } else { + $this->curlSettings[CURLOPT_HEADER] = true; + } + } + + protected function receiveCurlHeader($curlHandle, $headerLine) + { + $this->headerLinesMap[(int) $curlHandle][] = $headerLine; + + return strlen($headerLine); + } + + /** + * Sends a request to a HTTP server, and returns a response. + */ + public function send(RequestInterface $request): ResponseInterface + { + $this->emit('beforeRequest', [$request]); + + $retryCount = 0; + $redirects = 0; + + do { + $doRedirect = false; + $retry = false; + + try { + $response = $this->doRequest($request); + + $code = $response->getStatus(); + + // We are doing in-PHP redirects, because curl's + // FOLLOW_LOCATION throws errors when PHP is configured with + // open_basedir. + // + // https://github.com/fruux/sabre-http/issues/12 + if ($redirects < $this->maxRedirects && in_array($code, [301, 302, 307, 308])) { + $oldLocation = $request->getUrl(); + + // Creating a new instance of the request object. + $request = clone $request; + + // Setting the new location + $request->setUrl(Uri\resolve( + $oldLocation, + $response->getHeader('Location') + )); + + $doRedirect = true; + ++$redirects; + } + + // This was a HTTP error + if ($code >= 400) { + $this->emit('error', [$request, $response, &$retry, $retryCount]); + $this->emit('error:'.$code, [$request, $response, &$retry, $retryCount]); + } + } catch (ClientException $e) { + $this->emit('exception', [$request, $e, &$retry, $retryCount]); + + // If retry was still set to false, it means no event handler + // dealt with the problem. In this case we just re-throw the + // exception. + if (!$retry) { + throw $e; + } + } + + if ($retry) { + ++$retryCount; + } + } while ($retry || $doRedirect); + + $this->emit('afterRequest', [$request, $response]); + + if ($this->throwExceptions && $code >= 400) { + throw new ClientHttpException($response); + } + + return $response; + } + + /** + * Sends a HTTP request asynchronously. + * + * Due to the nature of PHP, you must from time to time poll to see if any + * new responses came in. + * + * After calling sendAsync, you must therefore occasionally call the poll() + * method, or wait(). + */ + public function sendAsync(RequestInterface $request, callable $success = null, callable $error = null) + { + $this->emit('beforeRequest', [$request]); + $this->sendAsyncInternal($request, $success, $error); + $this->poll(); + } + + /** + * This method checks if any http requests have gotten results, and if so, + * call the appropriate success or error handlers. + * + * This method will return true if there are still requests waiting to + * return, and false if all the work is done. + */ + public function poll(): bool + { + // nothing to do? + if (!$this->curlMultiMap) { + return false; + } + + do { + $r = curl_multi_exec( + $this->curlMultiHandle, + $stillRunning + ); + } while (CURLM_CALL_MULTI_PERFORM === $r); + + $messagesInQueue = 0; + do { + messageQueue: + + $status = curl_multi_info_read( + $this->curlMultiHandle, + $messagesInQueue + ); + + if ($status && CURLMSG_DONE === $status['msg']) { + $resourceId = (int) $status['handle']; + list( + $request, + $successCallback, + $errorCallback, + $retryCount) = $this->curlMultiMap[$resourceId]; + unset($this->curlMultiMap[$resourceId]); + + $curlHandle = $status['handle']; + $curlResult = $this->parseResponse(curl_multi_getcontent($curlHandle), $curlHandle); + $retry = false; + + if (self::STATUS_CURLERROR === $curlResult['status']) { + $e = new ClientException($curlResult['curl_errmsg'], $curlResult['curl_errno']); + $this->emit('exception', [$request, $e, &$retry, $retryCount]); + + if ($retry) { + ++$retryCount; + $this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount); + goto messageQueue; + } + + $curlResult['request'] = $request; + + if ($errorCallback) { + $errorCallback($curlResult); + } + } elseif (self::STATUS_HTTPERROR === $curlResult['status']) { + $this->emit('error', [$request, $curlResult['response'], &$retry, $retryCount]); + $this->emit('error:'.$curlResult['http_code'], [$request, $curlResult['response'], &$retry, $retryCount]); + + if ($retry) { + ++$retryCount; + $this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount); + goto messageQueue; + } + + $curlResult['request'] = $request; + + if ($errorCallback) { + $errorCallback($curlResult); + } + } else { + $this->emit('afterRequest', [$request, $curlResult['response']]); + + if ($successCallback) { + $successCallback($curlResult['response']); + } + } + } + } while ($messagesInQueue > 0); + + return count($this->curlMultiMap) > 0; + } + + /** + * Processes every HTTP request in the queue, and waits till they are all + * completed. + */ + public function wait() + { + do { + curl_multi_select($this->curlMultiHandle); + $stillRunning = $this->poll(); + } while ($stillRunning); + } + + /** + * If this is set to true, the Client will automatically throw exceptions + * upon HTTP errors. + * + * This means that if a response came back with a status code greater than + * or equal to 400, we will throw a ClientHttpException. + * + * This only works for the send() method. Throwing exceptions for + * sendAsync() is not supported. + */ + public function setThrowExceptions(bool $throwExceptions) + { + $this->throwExceptions = $throwExceptions; + } + + /** + * Adds a CURL setting. + * + * These settings will be included in every HTTP request. + * + * @param mixed $value + */ + public function addCurlSetting(int $name, $value) + { + $this->curlSettings[$name] = $value; + } + + /** + * This method is responsible for performing a single request. + */ + protected function doRequest(RequestInterface $request): ResponseInterface + { + $settings = $this->createCurlSettingsArray($request); + + if (!$this->curlHandle) { + $this->curlHandle = curl_init(); + } else { + curl_reset($this->curlHandle); + } + + curl_setopt_array($this->curlHandle, $settings); + $response = $this->curlExec($this->curlHandle); + $response = $this->parseResponse($response, $this->curlHandle); + if (self::STATUS_CURLERROR === $response['status']) { + throw new ClientException($response['curl_errmsg'], $response['curl_errno']); + } + + return $response['response']; + } + + /** + * Cached curl handle. + * + * By keeping this resource around for the lifetime of this object, things + * like persistent connections are possible. + * + * @var resource + */ + private $curlHandle; + + /** + * Handler for curl_multi requests. + * + * The first time sendAsync is used, this will be created. + * + * @var resource + */ + private $curlMultiHandle; + + /** + * Has a list of curl handles, as well as their associated success and + * error callbacks. + * + * @var array + */ + private $curlMultiMap = []; + + /** + * Turns a RequestInterface object into an array with settings that can be + * fed to curl_setopt. + */ + protected function createCurlSettingsArray(RequestInterface $request): array + { + $settings = $this->curlSettings; + + switch ($request->getMethod()) { + case 'HEAD': + $settings[CURLOPT_NOBODY] = true; + $settings[CURLOPT_CUSTOMREQUEST] = 'HEAD'; + break; + case 'GET': + $settings[CURLOPT_CUSTOMREQUEST] = 'GET'; + break; + default: + $body = $request->getBody(); + if (is_resource($body)) { + // 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; + + private function parseResponse(string $response, $curlHandle): array + { + $settings = $this->curlSettings; + $separatedHeaders = isset($settings[CURLOPT_HEADERFUNCTION]) && (bool) $settings[CURLOPT_HEADERFUNCTION]; + + if ($separatedHeaders) { + $resourceId = (int) $curlHandle; + if (isset($this->headerLinesMap[$resourceId])) { + $headers = $this->headerLinesMap[$resourceId]; + } else { + $headers = []; + } + $response = $this->parseCurlResponse($headers, $response, $curlHandle); + } else { + $response = $this->parseCurlResult($response, $curlHandle); + } + + return $response; + } + + /** + * Parses the result of a curl call in a format that's a bit more + * convenient to work with. + * + * The method returns an array with the following elements: + * * status - one of the 3 STATUS constants. + * * curl_errno - A curl error number. Only set if status is + * STATUS_CURLERROR. + * * curl_errmsg - A current error message. Only set if status is + * STATUS_CURLERROR. + * * response - Response object. Only set if status is STATUS_SUCCESS, or + * STATUS_HTTPERROR. + * * http_code - HTTP status code, as an int. Only set if Only set if + * status is STATUS_SUCCESS, or STATUS_HTTPERROR + * + * @param resource $curlHandle + */ + protected function parseCurlResponse(array $headerLines, string $body, $curlHandle): array + { + list( + $curlInfo, + $curlErrNo, + $curlErrMsg + ) = $this->curlStuff($curlHandle); + + if ($curlErrNo) { + return [ + 'status' => self::STATUS_CURLERROR, + 'curl_errno' => $curlErrNo, + 'curl_errmsg' => $curlErrMsg, + ]; + } + + $response = new Response(); + $response->setStatus($curlInfo['http_code']); + $response->setBody($body); + + foreach ($headerLines as $header) { + $parts = explode(':', $header, 2); + if (2 === count($parts)) { + $response->addHeader(trim($parts[0]), trim($parts[1])); + } + } + + $httpCode = $response->getStatus(); + + return [ + 'status' => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS, + 'response' => $response, + 'http_code' => $httpCode, + ]; + } + + /** + * Parses the result of a curl call in a format that's a bit more + * convenient to work with. + * + * The method returns an array with the following elements: + * * status - one of the 3 STATUS constants. + * * curl_errno - A curl error number. Only set if status is + * STATUS_CURLERROR. + * * curl_errmsg - A current error message. Only set if status is + * STATUS_CURLERROR. + * * response - Response object. Only set if status is STATUS_SUCCESS, or + * STATUS_HTTPERROR. + * * http_code - HTTP status code, as an int. Only set if Only set if + * status is STATUS_SUCCESS, or STATUS_HTTPERROR + * + * @deprecated Use parseCurlResponse instead + * + * @param resource $curlHandle + */ + protected function parseCurlResult(string $response, $curlHandle): array + { + list( + $curlInfo, + $curlErrNo, + $curlErrMsg + ) = $this->curlStuff($curlHandle); + + if ($curlErrNo) { + return [ + 'status' => self::STATUS_CURLERROR, + 'curl_errno' => $curlErrNo, + 'curl_errmsg' => $curlErrMsg, + ]; + } + + $headerBlob = substr($response, 0, $curlInfo['header_size']); + // In the case of 204 No Content, strlen($response) == $curlInfo['header_size]. + // This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL + // An exception will be thrown when calling getBodyAsString then + $responseBody = substr($response, $curlInfo['header_size']) ?: ''; + + unset($response); + + // In the case of 100 Continue, or redirects we'll have multiple lists + // of headers for each separate HTTP response. We can easily split this + // because they are separated by \r\n\r\n + $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n")); + + // We only care about the last set of headers + $headerBlob = $headerBlob[count($headerBlob) - 1]; + + // Splitting headers + $headerBlob = explode("\r\n", $headerBlob); + + return $this->parseCurlResponse($headerBlob, $responseBody, $curlHandle); + } + + /** + * Sends an asynchronous HTTP request. + * + * We keep this in a separate method, so we can call it without triggering + * the beforeRequest event and don't do the poll(). + */ + protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, int $retryCount = 0) + { + if (!$this->curlMultiHandle) { + $this->curlMultiHandle = curl_multi_init(); + } + $curl = curl_init(); + curl_setopt_array( + $curl, + $this->createCurlSettingsArray($request) + ); + curl_multi_add_handle($this->curlMultiHandle, $curl); + + $resourceId = (int) $curl; + $this->headerLinesMap[$resourceId] = []; + $this->curlMultiMap[$resourceId] = [ + $request, + $success, + $error, + $retryCount, + ]; + } + + // @codeCoverageIgnoreStart + + /** + * Calls curl_exec. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlExec($curlHandle): string + { + $this->headerLinesMap[(int) $curlHandle] = []; + + $result = curl_exec($curlHandle); + if (false === $result) { + $result = ''; + } + + return $result; + } + + /** + * Returns a bunch of information about a curl request. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlStuff($curlHandle): array + { + return [ + curl_getinfo($curlHandle), + curl_errno($curlHandle), + curl_error($curlHandle), + ]; + } + + // @codeCoverageIgnoreEnd +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/ClientException.php b/plugins/panakour/backup/vendor/sabre/http/lib/ClientException.php new file mode 100644 index 0000000..2ca4a28 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/ClientException.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +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/plugins/panakour/backup/vendor/sabre/http/lib/ClientHttpException.php b/plugins/panakour/backup/vendor/sabre/http/lib/ClientHttpException.php new file mode 100644 index 0000000..116ca1f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/ClientHttpException.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function __construct(ResponseInterface $response) + { + $this->response = $response; + parent::__construct($response->getStatusText(), $response->getStatus()); + } + + /** + * The http status code for the error. + */ + public function getHttpStatus(): int + { + return $this->response->getStatus(); + } + + /** + * Returns the full response object. + */ + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/HttpException.php b/plugins/panakour/backup/vendor/sabre/http/lib/HttpException.php new file mode 100644 index 0000000..80b3ae6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/HttpException.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +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 + */ + public function getHttpStatus(); +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Message.php b/plugins/panakour/backup/vendor/sabre/http/lib/Message.php new file mode 100644 index 0000000..90153fd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Message.php @@ -0,0 +1,291 @@ +<?php + +declare(strict_types=1); + +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, string or a callback writing the body to php://output + * + * @var resource|string|callable + */ + protected $body; + + /** + * Contains the list of HTTP headers. + * + * @var array + */ + protected $headers = []; + + /** + * HTTP message version (1.0, 1.1 or 2.0). + * + * @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 + */ + public function getBodyAsStream() + { + $body = $this->getBody(); + if (is_callable($this->body)) { + $body = $this->getBodyAsString(); + } + if (is_string($body) || null === $body) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, (string) $body); + rewind($stream); + + return $stream; + } + + return $body; + } + + /** + * Returns the body as a string. + * + * Note that because the underlying data may be based on a stream, this + * method could only work correctly the first time. + */ + public function getBodyAsString(): string + { + $body = $this->getBody(); + if (is_string($body)) { + return $body; + } + if (null === $body) { + return ''; + } + if (is_callable($body)) { + ob_start(); + $body(); + + return ob_get_clean(); + } + /** + * @var string|int|null + */ + $contentLength = $this->getHeader('Content-Length'); + if (is_int($contentLength) || ctype_digit($contentLength)) { + return stream_get_contents($body, (int) $contentLength); + } + + return stream_get_contents($body); + } + + /** + * Returns the message body, as it's internal representation. + * + * This could be either a string, a stream or a callback writing the body to php://output. + * + * @return resource|string|callable + */ + public function getBody() + { + return $this->body; + } + + /** + * Replaces the body resource with a new stream, string or a callback writing the body to php://output. + * + * @param resource|string|callable $body + */ + public function setBody($body) + { + $this->body = $body; + } + + /** + * Returns all the HTTP headers as an array. + * + * Every header is returned as an array, with one or more values. + */ + public function getHeaders(): array + { + $result = []; + foreach ($this->headers as $headerInfo) { + $result[$headerInfo[0]] = $headerInfo[1]; + } + + return $result; + } + + /** + * Will return true or false, depending on if a HTTP header exists. + */ + public function hasHeader(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + /** + * Returns a specific HTTP header, based on 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(). + * + * @return string|null + */ + public function getHeader(string $name) + { + $name = strtolower($name); + + if (isset($this->headers[$name])) { + return implode(',', $this->headers[$name][1]); + } + + return null; + } + + /** + * Returns a HTTP header as an array. + * + * For every time the HTTP header appeared in the request or response, an + * item will appear in the array. + * + * If the header did not exists, this method will return an empty array. + * + * @return string[] + */ + public function getHeaderAsArray(string $name): array + { + $name = strtolower($name); + + if (isset($this->headers[$name])) { + return $this->headers[$name][1]; + } + + return []; + } + + /** + * Updates a HTTP header. + * + * The case-sensitivity of the name value must be retained as-is. + * + * If the header already existed, it will be overwritten. + * + * @param string|string[] $value + */ + public function setHeader(string $name, $value) + { + $this->headers[strtolower($name)] = [$name, (array) $value]; + } + + /** + * Sets a new set of HTTP headers. + * + * The headers array should contain headernames for keys, and their value + * should be specified as either a string or an array. + * + * Any header that already existed will be overwritten. + */ + public function setHeaders(array $headers) + { + foreach ($headers as $name => $value) { + $this->setHeader($name, $value); + } + } + + /** + * Adds a HTTP header. + * + * This method will not overwrite any existing HTTP header, but instead add + * another value. Individual values can be retrieved with + * getHeadersAsArray. + * + * @param string|string[] $value + */ + public function addHeader(string $name, $value) + { + $lName = strtolower($name); + if (isset($this->headers[$lName])) { + $this->headers[$lName][1] = array_merge( + $this->headers[$lName][1], + (array) $value + ); + } else { + $this->headers[$lName] = [ + $name, + (array) $value, + ]; + } + } + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + */ + public function addHeaders(array $headers) + { + foreach ($headers as $name => $value) { + $this->addHeader($name, $value); + } + } + + /** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insensitive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + */ + public function removeHeader(string $name): bool + { + $name = strtolower($name); + if (!isset($this->headers[$name])) { + return false; + } + unset($this->headers[$name]); + + return true; + } + + /** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + */ + public function setHttpVersion(string $version) + { + $this->httpVersion = $version; + } + + /** + * Returns the HTTP version. + */ + public function getHttpVersion(): string + { + return $this->httpVersion; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/MessageDecoratorTrait.php b/plugins/panakour/backup/vendor/sabre/http/lib/MessageDecoratorTrait.php new file mode 100644 index 0000000..6f49dad --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/MessageDecoratorTrait.php @@ -0,0 +1,206 @@ +<?php + +declare(strict_types=1); + +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 + */ + public 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. + */ + public function getBodyAsString(): string + { + 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 + */ + public function getBody() + { + return $this->inner->getBody(); + } + + /** + * Updates the body resource with a new stream. + * + * @param resource|string|callable $body + */ + public function setBody($body) + { + $this->inner->setBody($body); + } + + /** + * Returns all the HTTP headers as an array. + * + * Every header is returned as an array, with one or more values. + */ + public function getHeaders(): array + { + return $this->inner->getHeaders(); + } + + /** + * Will return true or false, depending on if a HTTP header exists. + */ + public function hasHeader(string $name): bool + { + return $this->inner->hasHeader($name); + } + + /** + * Returns a specific HTTP header, based on 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(). + * + * @return string|null + */ + public function getHeader(string $name) + { + return $this->inner->getHeader($name); + } + + /** + * Returns a HTTP header as an array. + * + * For every time the HTTP header appeared in the request or response, an + * item will appear in the array. + * + * If the header did not exists, this method will return an empty array. + */ + public function getHeaderAsArray(string $name): array + { + return $this->inner->getHeaderAsArray($name); + } + + /** + * Updates a HTTP header. + * + * The case-sensitivity of the name value must be retained as-is. + * + * If the header already existed, it will be overwritten. + * + * @param string|string[] $value + */ + public function setHeader(string $name, $value) + { + $this->inner->setHeader($name, $value); + } + + /** + * Sets a new set of HTTP headers. + * + * The headers array should contain headernames for keys, and their value + * should be specified as either a string or an array. + * + * Any header that already existed will be overwritten. + */ + public function setHeaders(array $headers) + { + $this->inner->setHeaders($headers); + } + + /** + * Adds a HTTP header. + * + * This method will not overwrite any existing HTTP header, but instead add + * another value. Individual values can be retrieved with + * getHeadersAsArray. + * + * @param string|string[] $value + */ + public function addHeader(string $name, $value) + { + $this->inner->addHeader($name, $value); + } + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + */ + public function addHeaders(array $headers) + { + $this->inner->addHeaders($headers); + } + + /** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insensitive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + */ + public function removeHeader(string $name): bool + { + return $this->inner->removeHeader($name); + } + + /** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + */ + public function setHttpVersion(string $version) + { + $this->inner->setHttpVersion($version); + } + + /** + * Returns the HTTP version. + */ + public function getHttpVersion(): string + { + return $this->inner->getHttpVersion(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/MessageInterface.php b/plugins/panakour/backup/vendor/sabre/http/lib/MessageInterface.php new file mode 100644 index 0000000..8504f0f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/MessageInterface.php @@ -0,0 +1,151 @@ +<?php + +declare(strict_types=1); + +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 + */ + public 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. + */ + public function getBodyAsString(): string; + + /** + * Returns the message body, as it's internal representation. + * + * This could be either a string, a stream or a callback writing the body to php://output + * + * @return resource|string|callable + */ + public function getBody(); + + /** + * Updates the body resource with a new stream. + * + * @param resource|string|callable $body + */ + public function setBody($body); + + /** + * Returns all the HTTP headers as an array. + * + * Every header is returned as an array, with one or more values. + */ + public function getHeaders(): array; + + /** + * Will return true or false, depending on if a HTTP header exists. + */ + public function hasHeader(string $name): bool; + + /** + * 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(). + * + * @return string|null + */ + public function getHeader(string $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. + * + * @return string[] + */ + public function getHeaderAsArray(string $name): array; + + /** + * 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|string[] $value + */ + public function setHeader(string $name, $value); + + /** + * Sets a new set of HTTP headers. + * + * The headers array should contain headernames for keys, and their value + * should be specified as either a string or an array. + * + * Any header that already existed will be overwritten. + */ + public function setHeaders(array $headers); + + /** + * Adds a HTTP header. + * + * This method will not overwrite any existing HTTP header, but instead add + * another value. Individual values can be retrieved with + * getHeadersAsArray. + * + * @param string|string[] $value + */ + public function addHeader(string $name, $value); + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + */ + public 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. + */ + public function removeHeader(string $name): bool; + + /** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + */ + public function setHttpVersion(string $version); + + /** + * Returns the HTTP version. + */ + public function getHttpVersion(): string; +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Request.php b/plugins/panakour/backup/vendor/sabre/http/lib/Request.php new file mode 100644 index 0000000..496629a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Request.php @@ -0,0 +1,267 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +use LogicException; +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 resource|callable|string $body + */ + public function __construct(string $method, string $url, array $headers = [], $body = null) + { + $this->setMethod($method); + $this->setUrl($url); + $this->setHeaders($headers); + $this->setBody($body); + } + + /** + * Returns the current HTTP method. + */ + public function getMethod(): string + { + return $this->method; + } + + /** + * Sets the HTTP method. + */ + public function setMethod(string $method) + { + $this->method = $method; + } + + /** + * Returns the request url. + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * Sets the request url. + */ + public function setUrl(string $url) + { + $this->url = $url; + } + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + */ + public function getQueryParameters(): array + { + $url = $this->getUrl(); + if (false === ($index = strpos($url, '?'))) { + return []; + } + + parse_str(substr($url, $index + 1), $queryParams); + + return $queryParams; + } + + protected $absoluteUrl; + + /** + * Sets the absolute url. + */ + public function setAbsoluteUrl(string $url) + { + $this->absoluteUrl = $url; + } + + /** + * Returns the absolute url. + */ + public function getAbsoluteUrl(): string + { + if (!$this->absoluteUrl) { + // Guessing we're a http endpoint. + $this->absoluteUrl = 'http://'. + ($this->getHeader('Host') ?? 'localhost'). + $this->getUrl(); + } + + return $this->absoluteUrl; + } + + /** + * Base url. + * + * @var string + */ + protected $baseUrl = '/'; + + /** + * Sets a base url. + * + * This url is used for relative path calculations. + */ + public function setBaseUrl(string $url) + { + $this->baseUrl = $url; + } + + /** + * Returns the current base url. + */ + public function getBaseUrl(): string + { + return $this->baseUrl; + } + + /** + * Returns the relative path. + * + * This is being calculated using the base url. This path will not start + * with a slash, so it will always return something like + * 'example/path.html'. + * + * If the full path is equal to the base url, this method will return an + * empty string. + * + * This method will also urldecode the path, and if the url was 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. + */ + public function getPath(): string + { + // Removing duplicated slashes. + $uri = str_replace('//', '/', $this->getUrl()); + + $uri = Uri\normalize($uri); + $baseUri = Uri\normalize($this->getBaseUrl()); + + if (0 === strpos($uri, $baseUri)) { + // We're not interested in the query part (everything after the ?). + list($uri) = explode('?', $uri); + + return trim(decodePath(substr($uri, strlen($baseUri))), '/'); + } + + if ($uri.'/' === $baseUri) { + return ''; + } + // A special case, if the baseUri was accessed without a trailing + // slash, we'll accept it as well. + + throw new \LogicException('Requested uri ('.$this->getUrl().') is out of base uri ('.$this->getBaseUrl().')'); + } + + /** + * Equivalent of PHP's $_POST. + * + * @var array + */ + protected $postData = []; + + /** + * Sets the post data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * This would not have been needed, if POST data was accessible as + * php://input, but unfortunately we need to special case it. + */ + public function setPostData(array $postData) + { + $this->postData = $postData; + } + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + */ + public function getPostData(): array + { + return $this->postData; + } + + /** + * An array containing the raw _SERVER array. + * + * @var array + */ + protected $rawServerData; + + /** + * Returns an item from the _SERVER array. + * + * If the value does not exist in the array, null is returned. + * + * @return string|null + */ + public function getRawServerValue(string $valueName) + { + return $this->rawServerData[$valueName] ?? null; + } + + /** + * Sets the _SERVER array. + */ + public function setRawServerData(array $data) + { + $this->rawServerData = $data; + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + $out = $this->getMethod().' '.$this->getUrl().' HTTP/'.$this->getHttpVersion()."\r\n"; + + foreach ($this->getHeaders() as $key => $value) { + foreach ($value as $v) { + if ('Authorization' === $key) { + list($v) = explode(' ', $v, 2); + $v .= ' REDACTED'; + } + $out .= $key.': '.$v."\r\n"; + } + } + $out .= "\r\n"; + $out .= $this->getBodyAsString(); + + return $out; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/RequestDecorator.php b/plugins/panakour/backup/vendor/sabre/http/lib/RequestDecorator.php new file mode 100644 index 0000000..0ad2492 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/RequestDecorator.php @@ -0,0 +1,179 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function __construct(RequestInterface $inner) + { + $this->inner = $inner; + } + + /** + * Returns the current HTTP method. + */ + public function getMethod(): string + { + return $this->inner->getMethod(); + } + + /** + * Sets the HTTP method. + */ + public function setMethod(string $method) + { + $this->inner->setMethod($method); + } + + /** + * Returns the request url. + */ + public function getUrl(): string + { + return $this->inner->getUrl(); + } + + /** + * Sets the request url. + */ + public function setUrl(string $url) + { + $this->inner->setUrl($url); + } + + /** + * Returns the absolute url. + */ + public function getAbsoluteUrl(): string + { + return $this->inner->getAbsoluteUrl(); + } + + /** + * Sets the absolute url. + */ + public function setAbsoluteUrl(string $url) + { + $this->inner->setAbsoluteUrl($url); + } + + /** + * Returns the current base url. + */ + public function getBaseUrl(): string + { + return $this->inner->getBaseUrl(); + } + + /** + * Sets a base url. + * + * This url is used for relative path calculations. + * + * The base url should default to / + */ + public function setBaseUrl(string $url) + { + $this->inner->setBaseUrl($url); + } + + /** + * Returns the relative path. + * + * This is being calculated using the base url. This path will not start + * with a slash, so it will always return something like + * 'example/path.html'. + * + * If the full path is equal to the base url, this method will return an + * empty string. + * + * This method will also urldecode the path, and if the url was 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. + */ + public function getPath(): string + { + return $this->inner->getPath(); + } + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + */ + public function getQueryParameters(): array + { + return $this->inner->getQueryParameters(); + } + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + */ + public function getPostData(): array + { + return $this->inner->getPostData(); + } + + /** + * Sets the post data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * This would not have been needed, if POST data was accessible as + * php://input, but unfortunately we need to special case it. + */ + public function setPostData(array $postData) + { + $this->inner->setPostData($postData); + } + + /** + * Returns an item from the _SERVER array. + * + * If the value does not exist in the array, null is returned. + * + * @return string|null + */ + public function getRawServerValue(string $valueName) + { + return $this->inner->getRawServerValue($valueName); + } + + /** + * Sets the _SERVER array. + */ + public function setRawServerData(array $data) + { + $this->inner->setRawServerData($data); + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + return $this->inner->__toString(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/RequestInterface.php b/plugins/panakour/backup/vendor/sabre/http/lib/RequestInterface.php new file mode 100644 index 0000000..83fa85b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/RequestInterface.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function getMethod(): string; + + /** + * Sets the HTTP method. + */ + public function setMethod(string $method); + + /** + * Returns the request url. + */ + public function getUrl(): string; + + /** + * Sets the request url. + */ + public function setUrl(string $url); + + /** + * Returns the absolute url. + */ + public function getAbsoluteUrl(): string; + + /** + * Sets the absolute url. + */ + public function setAbsoluteUrl(string $url); + + /** + * Returns the current base url. + */ + public function getBaseUrl(): string; + + /** + * Sets a base url. + * + * This url is used for relative path calculations. + * + * The base url should default to / + */ + public function setBaseUrl(string $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. + */ + public function getPath(): string; + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + */ + public function getQueryParameters(): array; + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + */ + public function getPostData(): array; + + /** + * Sets the post data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * This would not have been needed, if POST data was accessible as + * php://input, but unfortunately we need to special case it. + */ + public function setPostData(array $postData); + + /** + * Returns an item from the _SERVER array. + * + * If the value does not exist in the array, null is returned. + * + * @return string|null + */ + public function getRawServerValue(string $valueName); + + /** + * Sets the _SERVER array. + */ + public function setRawServerData(array $data); +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Response.php b/plugins/panakour/backup/vendor/sabre/http/lib/Response.php new file mode 100644 index 0000000..64dfbc0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Response.php @@ -0,0 +1,188 @@ +<?php + +declare(strict_types=1); + +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 + */ + public 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 + */ + public function __construct($status = 500, array $headers = null, $body = null) + { + if (null !== $status) { + $this->setStatus($status); + } + if (null !== $headers) { + $this->setHeaders($headers); + } + if (null !== $body) { + $this->setBody($body); + } + } + + /** + * Returns the current HTTP status code. + */ + public function getStatus(): int + { + return $this->status; + } + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + */ + public function getStatusText(): string + { + return $this->statusText; + } + + /** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + * + * @throws \InvalidArgumentException + */ + public function setStatus($status) + { + if (ctype_digit($status) || is_int($status)) { + $statusCode = $status; + $statusText = self::$statusCodes[$status] ?? 'Unknown'; + } else { + list( + $statusCode, + $statusText + ) = explode(' ', $status, 2); + $statusCode = (int) $statusCode; + } + if ($statusCode < 100 || $statusCode > 999) { + throw new \InvalidArgumentException('The HTTP status code must be exactly 3 digits'); + } + + $this->status = $statusCode; + $this->statusText = $statusText; + } + + /** + * Serializes the response object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + $str = 'HTTP/'.$this->httpVersion.' '.$this->getStatus().' '.$this->getStatusText()."\r\n"; + foreach ($this->getHeaders() as $key => $value) { + foreach ($value as $v) { + $str .= $key.': '.$v."\r\n"; + } + } + $str .= "\r\n"; + $str .= $this->getBodyAsString(); + + return $str; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/ResponseDecorator.php b/plugins/panakour/backup/vendor/sabre/http/lib/ResponseDecorator.php new file mode 100644 index 0000000..5f9fe7c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/ResponseDecorator.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function __construct(ResponseInterface $inner) + { + $this->inner = $inner; + } + + /** + * Returns the current HTTP status code. + */ + public function getStatus(): int + { + return $this->inner->getStatus(); + } + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + */ + public function getStatusText(): string + { + return $this->inner->getStatusText(); + } + + /** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + */ + public function setStatus($status) + { + $this->inner->setStatus($status); + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + return $this->inner->__toString(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/ResponseInterface.php b/plugins/panakour/backup/vendor/sabre/http/lib/ResponseInterface.php new file mode 100644 index 0000000..9bd93f1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/ResponseInterface.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +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. + */ + public function getStatus(): int; + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + */ + public function getStatusText(): string; + + /** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + * + * @throws \InvalidArgumentException + */ + public function setStatus($status); +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Sapi.php b/plugins/panakour/backup/vendor/sabre/http/lib/Sapi.php new file mode 100644 index 0000000..73674a5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Sapi.php @@ -0,0 +1,243 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +use InvalidArgumentException; + +/** + * 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. + */ + public static function getRequest(): Request + { + $serverArr = $_SERVER; + + if ('cli' === PHP_SAPI) { + // If we're running off the CLI, we're going to set some default + // settings. + $serverArr['REQUEST_URI'] = $_SERVER['REQUEST_URI'] ?? '/'; + $serverArr['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'CLI'; + } + + $r = self::createFromServerArray($serverArr); + $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. + */ + public static function sendResponse(ResponseInterface $response) + { + header('HTTP/'.$response->getHttpVersion().' '.$response->getStatus().' '.$response->getStatusText()); + foreach ($response->getHeaders() as $key => $value) { + foreach ($value as $k => $v) { + if (0 === $k) { + header($key.': '.$v); + } else { + header($key.': '.$v, false); + } + } + } + + $body = $response->getBody(); + if (null === $body) { + return; + } + + if (is_callable($body)) { + $body(); + + return; + } + + $contentLength = $response->getHeader('Content-Length'); + if (null !== $contentLength) { + $output = fopen('php://output', 'wb'); + if (is_resource($body) && 'stream' == get_resource_type($body)) { + if (PHP_INT_SIZE > 4) { + // use the dedicated function on 64 Bit systems + // a workaround to make PHP more possible to use mmap based copy, see https://github.com/sabre-io/http/pull/119 + $left = (int) $contentLength; + // copy with 4MiB chunks + $chunk_size = 4 * 1024 * 1024; + stream_set_chunk_size($output, $chunk_size); + // If this is a partial response, flush the beginning bytes until the first position that is a multiple of the page size. + $contentRange = $response->getHeader('Content-Range'); + // Matching "Content-Range: bytes 1234-5678/7890" + if (null !== $contentRange && preg_match('/^bytes\s([0-9]+)-([0-9]+)\//i', $contentRange, $matches)) { + // 4kB should be the default page size on most architectures + $pageSize = 4096; + $offset = (int) $matches[1]; + $delta = ($offset % $pageSize) > 0 ? ($pageSize - $offset % $pageSize) : 0; + if ($delta > 0) { + $left -= stream_copy_to_stream($body, $output, min($delta, $left)); + } + } + while ($left > 0) { + $copied = stream_copy_to_stream($body, $output, min($left, $chunk_size)); + // stream_copy_to_stream($src, $dest, $maxLength) must return the number of bytes copied or false in case of failure + // But when the $maxLength is greater than the total number of bytes remaining in the stream, + // It returns the negative number of bytes copied + // So break the loop in such cases. + if ($copied <= 0) { + break; + } + $left -= $copied; + } + } else { + // workaround for 32 Bit systems to avoid stream_copy_to_stream + while (!feof($body)) { + fwrite($output, fread($body, 8192)); + } + } + } else { + fwrite($output, $body, (int) $contentLength); + } + } else { + file_put_contents('php://output', $body); + } + + if (is_resource($body)) { + fclose($body); + } + } + + /** + * This static method will create a new Request object, based on a PHP + * $_SERVER array. + * + * REQUEST_URI and REQUEST_METHOD are required. + */ + public static function createFromServerArray(array $serverArray): Request + { + $headers = []; + $method = null; + $url = null; + $httpVersion = '1.1'; + + $protocol = 'http'; + $hostName = 'localhost'; + + foreach ($serverArray as $key => $value) { + switch ($key) { + case 'SERVER_PROTOCOL': + if ('HTTP/1.0' === $value) { + $httpVersion = '1.0'; + } elseif ('HTTP/2.0' === $value) { + $httpVersion = '2.0'; + } + break; + case 'REQUEST_METHOD': + $method = $value; + break; + case 'REQUEST_URI': + $url = $value; + break; + + // These sometimes show up without a HTTP_ prefix + case 'CONTENT_TYPE': + $headers['Content-Type'] = $value; + break; + case 'CONTENT_LENGTH': + $headers['Content-Length'] = $value; + break; + + // mod_php on apache will put credentials in these variables. + // (fast)cgi does not usually do this, however. + case 'PHP_AUTH_USER': + if (isset($serverArray['PHP_AUTH_PW'])) { + $headers['Authorization'] = 'Basic '.base64_encode($value.':'.$serverArray['PHP_AUTH_PW']); + } + break; + + // Similarly, mod_php may also screw around with digest auth. + case 'PHP_AUTH_DIGEST': + $headers['Authorization'] = 'Digest '.$value; + break; + + // Apache may prefix the HTTP_AUTHORIZATION header with + // REDIRECT_, if mod_rewrite was used. + case 'REDIRECT_HTTP_AUTHORIZATION': + $headers['Authorization'] = $value; + break; + + case 'HTTP_HOST': + $hostName = $value; + $headers['Host'] = $value; + break; + + case 'HTTPS': + if (!empty($value) && 'off' !== $value) { + $protocol = 'https'; + } + break; + + default: + if ('HTTP_' === substr($key, 0, 5)) { + // It's a HTTP header + + // Normalizing it to be prettier + $header = strtolower(substr($key, 5)); + + // Transforming dashes into spaces, and uppercasing + // every first letter. + $header = ucwords(str_replace('_', ' ', $header)); + + // Turning spaces into dashes. + $header = str_replace(' ', '-', $header); + $headers[$header] = $value; + } + break; + } + } + + if (null === $url) { + throw new InvalidArgumentException('The _SERVER array must have a REQUEST_URI key'); + } + + if (null === $method) { + throw new InvalidArgumentException('The _SERVER array must have a REQUEST_METHOD key'); + } + $r = new Request($method, $url, $headers); + $r->setHttpVersion($httpVersion); + $r->setRawServerData($serverArray); + $r->setAbsoluteUrl($protocol.'://'.$hostName.$url); + + return $r; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/Version.php b/plugins/panakour/backup/vendor/sabre/http/lib/Version.php new file mode 100644 index 0000000..8946a46 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/Version.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +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 = '5.1.0'; +} diff --git a/plugins/panakour/backup/vendor/sabre/http/lib/functions.php b/plugins/panakour/backup/vendor/sabre/http/lib/functions.php new file mode 100644 index 0000000..a23840a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/lib/functions.php @@ -0,0 +1,415 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +use DateTime; +use InvalidArgumentException; + +/** + * 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 + * + * @return bool|DateTime + */ +function parseDate(string $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 (false === strpos($dateString, ' GMT')) { + $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. + */ +function toDate(DateTime $dateTime): string +{ + // 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 + * + * @return string|null + */ +function negotiateContentType($acceptHeaderValue, array $availableOptions) +{ + if (!$acceptHeaderValue) { + // Grabbing the first in the list. + return reset($availableOptions); + } + + $proposals = array_map( + 'Sabre\HTTP\parseMimeType', + explode(',', $acceptHeaderValue) + ); + + // Ensuring array keys are reset. + $availableOptions = array_values($availableOptions); + + $options = array_map( + 'Sabre\HTTP\parseMimeType', + $availableOptions + ); + + $lastQuality = 0; + $lastSpecificity = 0; + $lastOptionIndex = 0; + $lastChoice = null; + + foreach ($proposals as $proposal) { + // Ignoring broken values. + if (null === $proposal) { + continue; + } + + // If the quality is lower we don't have to bother comparing. + if ($proposal['quality'] < $lastQuality) { + continue; + } + + foreach ($options as $optionIndex => $option) { + if ('*' !== $proposal['type'] && $proposal['type'] !== $option['type']) { + // no match on type. + continue; + } + if ('*' !== $proposal['subType'] && $proposal['subType'] !== $option['subType']) { + // no match on subtype. + continue; + } + + // Any parameters appearing on the options must appear on + // proposals. + foreach ($option['parameters'] as $paramName => $paramValue) { + if (!array_key_exists($paramName, $proposal['parameters'])) { + continue 2; + } + if ($paramValue !== $proposal['parameters'][$paramName]) { + continue 2; + } + } + + // If we got here, we have a match on parameters, type and + // subtype. We need to calculate a score for how specific the + // match was. + $specificity = + ('*' !== $proposal['type'] ? 20 : 0) + + ('*' !== $proposal['subType'] ? 10 : 0) + + count($option['parameters']); + + // Does this entry win? + if ( + ($proposal['quality'] > $lastQuality) || + ($proposal['quality'] === $lastQuality && $specificity > $lastSpecificity) || + ($proposal['quality'] === $lastQuality && $specificity === $lastSpecificity && $optionIndex < $lastOptionIndex) + ) { + $lastQuality = $proposal['quality']; + $lastSpecificity = $specificity; + $lastOptionIndex = $optionIndex; + $lastChoice = $availableOptions[$optionIndex]; + } + } + } + + return $lastChoice; +} + +/** + * Parses the Prefer header, as defined in RFC7240. + * + * Input can be given as a single header value (string) or multiple headers + * (array of string). + * + * This method will return a key->value array with the various Prefer + * parameters. + * + * Prefer: return=minimal will result in: + * + * [ 'return' => 'minimal' ] + * + * Prefer: foo, wait=10 will result in: + * + * [ 'foo' => true, 'wait' => '10'] + * + * This method also supports the formats from older drafts of RFC7240, and + * it will automatically map them to the new values, as the older values + * are still pretty common. + * + * Parameters are currently discarded. There's no known prefer value that + * uses them. + * + * @param string|string[] $input + */ +function parsePrefer($input): array +{ + $token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+'; + + // Work in progress + $word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )'; + + $regex = <<<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 + */ +function getHeaderValues($values, $values2 = null): array +{ + $values = (array) $values; + if ($values2) { + $values = array_merge($values, (array) $values2); + } + + $result = []; + foreach ($values as $l1) { + foreach (explode(',', $l1) as $l2) { + $result[] = trim($l2); + } + } + + return $result; +} + +/** + * Parses a mime-type and splits it into:. + * + * 1. type + * 2. subtype + * 3. quality + * 4. parameters + */ +function parseMimeType(string $str): array +{ + $parameters = []; + // If no q= parameter appears, then quality = 1. + $quality = 1; + + $parts = explode(';', $str); + + // The first part is the mime-type. + $mimeType = trim(array_shift($parts)); + + if ('*' === $mimeType) { + $mimeType = '*/*'; + } + + $mimeType = explode('/', $mimeType); + if (2 !== count($mimeType)) { + // Illegal value + var_dump($mimeType); + die(); + throw new InvalidArgumentException('Not a valid mime-type: '.$str); + } + list($type, $subType) = $mimeType; + + foreach ($parts as $part) { + $part = trim($part); + if (strpos($part, '=')) { + list($partName, $partValue) = + explode('=', $part, 2); + } else { + $partName = $part; + $partValue = null; + } + + // The quality parameter, if it appears, also marks the end of + // the parameter list. Anything after the q= counts as an + // 'accept extension' and could introduce new semantics in + // content-negotation. + if ('q' !== $partName) { + $parameters[$partName] = $part; + } else { + $quality = (float) $partValue; + break; // Stop parsing parts + } + } + + return [ + 'type' => $type, + 'subType' => $subType, + 'quality' => $quality, + 'parameters' => $parameters, + ]; +} + +/** + * Encodes the path of a url. + * + * slashes (/) are treated as path-separators. + */ +function encodePath(string $path): string +{ + return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:@])/', function ($match) { + return '%'.sprintf('%02x', ord($match[0])); + }, $path); +} + +/** + * Encodes a 1 segment of a path. + * + * Slashes are considered part of the name, and are encoded as %2f + */ +function encodePathSegment(string $pathSegment): string +{ + return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function ($match) { + return '%'.sprintf('%02x', ord($match[0])); + }, $pathSegment); +} + +/** + * Decodes a url-encoded path. + */ +function decodePath(string $path): string +{ + return decodePathSegment($path); +} + +/** + * Decodes a url-encoded path segment. + */ +function decodePathSegment(string $path): string +{ + $path = rawurldecode($path); + $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/plugins/panakour/backup/vendor/sabre/http/phpstan.neon b/plugins/panakour/backup/vendor/sabre/http/phpstan.neon new file mode 100644 index 0000000..213da6d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + level: 1 diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php new file mode 100644 index 0000000..f3b36cf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php @@ -0,0 +1,228 @@ +<?php + +declare(strict_types=1); + +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'; + + public function setUp(): void + { + $this->response = new Response(); + $this->request = new Request('GET', '/'); + $this->auth = new AWS(self::REALM, $this->request, $this->response); + } + + public function testNoHeader() + { + $this->request->setMethod('GET'); + $result = $this->auth->init(); + + $this->assertFalse($result, 'No AWS Authorization header was supplied, so we should have gotten false'); + $this->assertEquals(AWS::ERR_NOAWSHEADER, $this->auth->errorCode); + } + + public function testIncorrectContentMD5() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + + $this->request->setMethod('GET'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => 'garbage', + ]); + $this->request->setUrl('/'); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_MD5CHECKSUMWRONG, $this->auth->errorCode); + } + + public function testNoDate() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + $contentMD5 = base64_encode(md5($content, true)); + + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => $contentMD5, + ]); + $this->request->setUrl('/'); + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_INVALIDDATEFORMAT, $this->auth->errorCode); + } + + public function testFutureDate() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + $contentMD5 = base64_encode(md5($content, true)); + + $date = new \DateTime('@'.(time() + (60 * 20))); + $date->setTimeZone(new \DateTimeZone('GMT')); + $date = $date->format('D, d M Y H:i:s \\G\\M\\T'); + + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => $contentMD5, + 'Date' => $date, + ]); + + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_REQUESTTIMESKEWED, $this->auth->errorCode); + } + + public function testPastDate() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + $contentMD5 = base64_encode(md5($content, true)); + + $date = new \DateTime('@'.(time() - (60 * 20))); + $date->setTimeZone(new \DateTimeZone('GMT')); + $date = $date->format('D, d M Y H:i:s \\G\\M\\T'); + + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => $contentMD5, + 'Date' => $date, + ]); + + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_REQUESTTIMESKEWED, $this->auth->errorCode); + } + + public function testIncorrectSignature() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + + $contentMD5 = base64_encode(md5($content, true)); + + $date = new \DateTime('now'); + $date->setTimeZone(new \DateTimeZone('GMT')); + $date = $date->format('D, d M Y H:i:s \\G\\M\\T'); + + $this->request->setUrl('/'); + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => $contentMD5, + 'X-amz-date' => $date, + ]); + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_INVALIDSIGNATURE, $this->auth->errorCode); + } + + public function testValidRequest() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + $contentMD5 = base64_encode(md5($content, true)); + + $date = new \DateTime('now'); + $date->setTimeZone(new \DateTimeZone('GMT')); + $date = $date->format('D, d M Y H:i:s \\G\\M\\T'); + + $sig = base64_encode($this->hmacsha1($secretKey, + "POST\n$contentMD5\n\n$date\nx-amz-date:$date\n/evert" + )); + + $this->request->setUrl('/evert'); + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:$sig", + 'Content-MD5' => $contentMD5, + 'X-amz-date' => $date, + ]); + + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertTrue($result, 'Signature did not validate, got errorcode '.$this->auth->errorCode); + $this->assertEquals($accessKey, $this->auth->getAccessKey()); + } + + public function test401() + { + $this->auth->requireLogin(); + $test = preg_match('/^AWS$/', $this->response->getHeader('WWW-Authenticate'), $matches); + $this->assertTrue(true == $test, 'The WWW-Authenticate response didn\'t match our pattern'); + } + + /** + * Generates an HMAC-SHA1 signature. + * + * @param string $key + * @param string $message + * + * @return string + */ + private function hmacsha1($key, $message) + { + $blocksize = 64; + if (strlen($key) > $blocksize) { + $key = pack('H*', sha1($key)); + } + $key = str_pad($key, $blocksize, chr(0x00)); + $ipad = str_repeat(chr(0x36), $blocksize); + $opad = str_repeat(chr(0x5c), $blocksize); + $hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message)))); + + return $hmac; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php new file mode 100644 index 0000000..e4c06c9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +class BasicTest extends \PHPUnit\Framework\TestCase +{ + public 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()); + } + + public function testGetInvalidCredentialsColonMissing() + { + $request = new Request('GET', '/', [ + 'Authorization' => 'Basic '.base64_encode('userpass'), + ]); + + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + } + + public function testGetCredentialsNoheader() + { + $request = new Request('GET', '/', []); + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + } + + public function testGetCredentialsNotBasic() + { + $request = new Request('GET', '/', [ + 'Authorization' => 'QBasic '.base64_encode('user:pass:bla'), + ]); + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + } + + public function testRequireLogin() + { + $response = new Response(); + $request = new Request('GET', '/'); + + $basic = new Basic('Dagger', $request, $response); + + $basic->requireLogin(); + + $this->assertEquals('Basic realm="Dagger", charset="UTF-8"', $response->getHeader('WWW-Authenticate')); + $this->assertEquals(401, $response->getStatus()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php new file mode 100644 index 0000000..f3dad5c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +class BearerTest extends \PHPUnit\Framework\TestCase +{ + public function testGetToken() + { + $request = new Request('GET', '/', [ + 'Authorization' => 'Bearer 12345', + ]); + + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertEquals( + '12345', + $bearer->getToken() + ); + } + + public function testGetCredentialsNoheader() + { + $request = new Request('GET', '/', []); + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertNull($bearer->getToken()); + } + + public function testGetCredentialsNotBearer() + { + $request = new Request('GET', '/', [ + 'Authorization' => 'QBearer 12345', + ]); + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertNull($bearer->getToken()); + } + + public function testRequireLogin() + { + $response = new Response(); + $request = new Request('GET', '/'); + $bearer = new Bearer('Dagger', $request, $response); + + $bearer->requireLogin(); + + $this->assertEquals('Bearer realm="Dagger"', $response->getHeader('WWW-Authenticate')); + $this->assertEquals(401, $response->getStatus()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php new file mode 100644 index 0000000..7d54bd4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php @@ -0,0 +1,182 @@ +<?php + +declare(strict_types=1); + +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'; + + public function setUp(): void + { + $this->response = new Response(); + $this->request = new Request('GET', '/'); + $this->auth = new Digest(self::REALM, $this->request, $this->response); + } + + public function testDigest() + { + list($nonce, $opaque) = $this->getServerTokens(); + + $username = 'admin'; + $password = '12345'; + $nc = '00002'; + $cnonce = uniqid(); + + $digestHash = md5( + md5($username.':'.self::REALM.':'.$password).':'. + $nonce.':'. + $nc.':'. + $cnonce.':'. + 'auth:'. + md5('GET'.':'.'/') + ); + + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth,nc='.$nc.',cnonce="'.$cnonce.'"'); + + $this->auth->init(); + + $this->assertEquals($username, $this->auth->getUsername()); + $this->assertEquals(self::REALM, $this->auth->getRealm()); + $this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1'); + $this->assertTrue($this->auth->validatePassword($password), 'Authentication is deemed invalid through validatePassword'); + } + + public function testInvalidDigest() + { + list($nonce, $opaque) = $this->getServerTokens(); + + $username = 'admin'; + $password = 12345; + $nc = '00002'; + $cnonce = uniqid(); + + $digestHash = md5( + md5($username.':'.self::REALM.':'.$password).':'. + $nonce.':'. + $nc.':'. + $cnonce.':'. + 'auth:'. + md5('GET'.':'.'/') + ); + + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth,nc='.$nc.',cnonce="'.$cnonce.'"'); + + $this->auth->init(); + + $this->assertFalse($this->auth->validateA1(md5($username.':'.self::REALM.':'.($password.'randomness'))), 'Authentication is deemed invalid through validateA1'); + } + + public function testInvalidDigest2() + { + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'basic blablabla'); + + $this->auth->init(); + $this->assertFalse($this->auth->validateA1(md5('user:realm:password'))); + } + + public function testDigestAuthInt() + { + $this->auth->setQOP(Digest::QOP_AUTHINT); + list($nonce, $opaque) = $this->getServerTokens(Digest::QOP_AUTHINT); + + $username = 'admin'; + $password = 12345; + $nc = '00003'; + $cnonce = uniqid(); + + $digestHash = md5( + md5($username.':'.self::REALM.':'.$password).':'. + $nonce.':'. + $nc.':'. + $cnonce.':'. + 'auth-int:'. + md5('POST'.':'.'/'.':'.md5('body')) + ); + + $this->request->setMethod('POST'); + $this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth-int,nc='.$nc.',cnonce="'.$cnonce.'"'); + $this->request->setBody('body'); + + $this->auth->init(); + + $this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1'); + } + + public function testDigestAuthBoth() + { + $this->auth->setQOP(Digest::QOP_AUTHINT | Digest::QOP_AUTH); + list($nonce, $opaque) = $this->getServerTokens(Digest::QOP_AUTHINT | Digest::QOP_AUTH); + + $username = 'admin'; + $password = 12345; + $nc = '00003'; + $cnonce = uniqid(); + + $digestHash = md5( + md5($username.':'.self::REALM.':'.$password).':'. + $nonce.':'. + $nc.':'. + $cnonce.':'. + 'auth-int:'. + md5('POST'.':'.'/'.':'.md5('body')) + ); + + $this->request->setMethod('POST'); + $this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth-int,nc='.$nc.',cnonce="'.$cnonce.'"'); + $this->request->setBody('body'); + + $this->auth->init(); + + $this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1'); + } + + private function getServerTokens($qop = Digest::QOP_AUTH) + { + $this->auth->requireLogin(); + + switch ($qop) { + case Digest::QOP_AUTH: $qopstr = 'auth'; break; + case Digest::QOP_AUTHINT: $qopstr = 'auth-int'; break; + default: $qopstr = 'auth,auth-int'; break; + } + + $test = preg_match('/Digest realm="'.self::REALM.'",qop="'.$qopstr.'",nonce="([0-9a-f]*)",opaque="([0-9a-f]*)"/', + $this->response->getHeader('WWW-Authenticate'), $matches); + + $this->assertTrue(true == $test, 'The WWW-Authenticate response didn\'t match our pattern. We received: '.$this->response->getHeader('WWW-Authenticate')); + + $nonce = $matches[1]; + $opaque = $matches[2]; + + // Reset our environment + $this->setUp(); + $this->auth->setQOP($qop); + + return [$nonce, $opaque]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ClientTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ClientTest.php new file mode 100644 index 0000000..5a2910e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ClientTest.php @@ -0,0 +1,564 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class ClientTest extends \PHPUnit\Framework\TestCase +{ + public 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_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayHEAD() + { + $client = new ClientMock(); + $request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_NOBODY => true, + CURLOPT_CUSTOMREQUEST => 'HEAD', + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayGETAfterHEAD() + { + $client = new ClientMock(); + $request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']); + + // Parsing the settings for this method, and discarding the result. + // This will cause the client to automatically persist previous + // settings and will help us detect problems. + $client->createCurlSettingsArray($request); + + // This is the real request. + $request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']); + + $settings = [ + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_NOBODY => false, + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayPUTStream() + { + $client = new ClientMock(); + + $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 (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayPUTString() + { + $client = new ClientMock(); + $request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], 'boo'); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_NOBODY => false, + CURLOPT_POSTFIELDS => 'boo', + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testIssue89MultiplePutInfileGivesWarning() + { + $client = new ClientMock(); + $tmpFile = tmpfile(); + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], 'body'); + + $settings = $client->createCurlSettingsArray($request); + $this->assertArrayNotHasKey(CURLOPT_PUT, $settings); + $this->assertArrayNotHasKey(CURLOPT_INFILE, $settings); + + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], $tmpFile); + + $settings = $client->createCurlSettingsArray($request); + $this->assertEquals(true, $settings[CURLOPT_PUT]); + $this->assertEquals($tmpFile, $settings[CURLOPT_INFILE]); + + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], 'body'); + + $settings = $client->createCurlSettingsArray($request); + $this->assertArrayNotHasKey(CURLOPT_PUT, $settings); + $this->assertArrayNotHasKey(CURLOPT_INFILE, $settings); + } + + public function testSend() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(200); + }); + + $response = $client->send($request); + + $this->assertEquals(200, $response->getStatus()); + } + + protected function getAbsoluteUrl($path) + { + $baseUrl = getenv('BASEURL'); + if ($baseUrl) { + $path = ltrim($path, '/'); + + return "$baseUrl/$path"; + } + + return false; + } + + /** + * @group ci + */ + public function testSendToGetLargeContent() + { + $url = $this->getAbsoluteUrl('/large.php'); + if (!$url) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $request = new Request('GET', $url); + $client = new Client(); + $response = $client->send($request); + + $this->assertEquals(200, $response->getStatus()); + $this->assertGreaterThan(memory_get_peak_usage(), 40 * pow(1024, 2)); + } + + /** + * @group ci + */ + public function testSendAsync() + { + $url = $this->getAbsoluteUrl('/foo'); + if (!$url) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $client = new Client(); + + $request = new Request('GET', $url); + $client->sendAsync($request, function (ResponseInterface $response) { + $this->assertEquals("foo\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals(4, $response->getHeader('Content-Length')); + }, function ($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to GET $url"); + }); + + $client->wait(); + } + + /** + * @group ci + */ + public function testSendAsynConsecutively() + { + $url = $this->getAbsoluteUrl('/foo'); + if (!$url) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $client = new Client(); + + $request = new Request('GET', $url); + $client->sendAsync($request, function (ResponseInterface $response) { + $this->assertEquals("foo\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals(4, $response->getHeader('Content-Length')); + }, function ($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to get $url"); + }); + + $url = $this->getAbsoluteUrl('/bar.php'); + $request = new Request('GET', $url); + $client->sendAsync($request, function (ResponseInterface $response) { + $this->assertEquals("bar\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('Bar', $response->getHeader('X-Test')); + }, function ($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to get $url"); + }); + + $client->wait(); + } + + public function testSendClientError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + throw new ClientException('aaah', 1); + }); + $called = false; + $client->on('exception', function () use (&$called) { + $called = true; + }); + + try { + $client->send($request); + $this->fail('send() should have thrown an exception'); + } catch (ClientException $e) { + } + $this->assertTrue($called); + } + + public function testSendHttpError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(404); + }); + $called = 0; + $client->on('error', function () use (&$called) { + ++$called; + }); + $client->on('error:404', function () use (&$called) { + ++$called; + }); + + $client->send($request); + $this->assertEquals(2, $called); + } + + public function testSendRetry() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $called = 0; + $client->on('doRequest', function ($request, &$response) use (&$called) { + ++$called; + if ($called < 3) { + $response = new Response(404); + } else { + $response = new Response(200); + } + }); + + $errorCalled = 0; + $client->on('error', function ($request, $response, &$retry, $retryCount) use (&$errorCalled) { + ++$errorCalled; + $retry = true; + }); + + $response = $client->send($request); + $this->assertEquals(3, $called); + $this->assertEquals(2, $errorCalled); + $this->assertEquals(200, $response->getStatus()); + } + + public function testHttpErrorException() + { + $client = new ClientMock(); + $client->setThrowExceptions(true); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(404); + }); + + try { + $client->send($request); + $this->fail('An exception should have been thrown'); + } catch (ClientHttpException $e) { + $this->assertEquals(404, $e->getHttpStatus()); + $this->assertInstanceOf('Sabre\HTTP\Response', $e->getResponse()); + } + } + + public function testParseCurlResult() + { + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'header_size' => 33, + 'http_code' => 200, + ], + 0, + '', + ]; + }); + + $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; + $result = $client->parseCurlResult($body, 'foobar'); + + $this->assertEquals(Client::STATUS_SUCCESS, $result['status']); + $this->assertEquals(200, $result['http_code']); + $this->assertEquals(200, $result['response']->getStatus()); + $this->assertEquals(['Header1' => ['Val1']], $result['response']->getHeaders()); + $this->assertEquals('Foo', $result['response']->getBodyAsString()); + } + + public function testParseCurlResultEmptyBody() + { + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'header_size' => 33, + 'http_code' => 200, + ], + 0, + '', + ]; + }); + + $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\n"; + $result = $client->parseCurlResult($body, 'foobar'); + + $this->assertEquals(Client::STATUS_SUCCESS, $result['status']); + $this->assertEquals(200, $result['http_code']); + $this->assertEquals(200, $result['response']->getStatus()); + $this->assertEquals(['Header1' => ['Val1']], $result['response']->getHeaders()); + $this->assertEquals('', $result['response']->getBodyAsString()); + } + + public function testParseCurlError() + { + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [], + 1, + 'Curl error', + ]; + }); + + $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; + $result = $client->parseCurlResult($body, 'foobar'); + + $this->assertEquals(Client::STATUS_CURLERROR, $result['status']); + $this->assertEquals(1, $result['curl_errno']); + $this->assertEquals('Curl error', $result['curl_errmsg']); + } + + public function testDoRequest() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + $client->on('curlExec', function (&$return) { + $return = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; + }); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'header_size' => 33, + 'http_code' => 200, + ], + 0, + '', + ]; + }); + $response = $client->doRequest($request); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals(['Header1' => ['Val1']], $response->getHeaders()); + $this->assertEquals('Foo', $response->getBodyAsString()); + } + + public function testDoRequestCurlError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + $client->on('curlExec', function (&$return) { + $return = ''; + }); + $client->on('curlStuff', function (&$return) { + $return = [ + [], + 1, + 'Curl error', + ]; + }); + + try { + $response = $client->doRequest($request); + $this->fail('This should have thrown an exception'); + } catch (ClientException $e) { + $this->assertEquals(1, $e->getCode()); + $this->assertEquals('Curl error', $e->getMessage()); + } + } +} + +class ClientMock extends Client +{ + protected $persistedSettings = []; + + /** + * Making this method public. + */ + public function receiveCurlHeader($curlHandle, $headerLine) + { + return parent::receiveCurlHeader($curlHandle, $headerLine); + } + + /** + * Making this method public. + */ + public function createCurlSettingsArray(RequestInterface $request): array + { + return parent::createCurlSettingsArray($request); + } + + /** + * Making this method public. + */ + public function parseCurlResult(string $response, $curlHandle): array + { + return parent::parseCurlResult($response, $curlHandle); + } + + /** + * This method is responsible for performing a single request. + */ + public function doRequest(RequestInterface $request): ResponseInterface + { + $response = null; + $this->emit('doRequest', [$request, &$response]); + + // If nothing modified $response, we're using the default behavior. + if (is_null($response)) { + return parent::doRequest($request); + } else { + return $response; + } + } + + /** + * Returns a bunch of information about a curl request. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlStuff($curlHandle): array + { + $return = null; + $this->emit('curlStuff', [&$return]); + + // If nothing modified $return, we're using the default behavior. + if (is_null($return)) { + return parent::curlStuff($curlHandle); + } else { + return $return; + } + } + + /** + * Calls curl_exec. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlExec($curlHandle): string + { + $return = null; + $this->emit('curlExec', [&$return]); + + // If nothing modified $return, we're using the default behavior. + if (is_null($return)) { + return parent::curlExec($curlHandle); + } else { + return $return; + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/FunctionsTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/FunctionsTest.php new file mode 100644 index 0000000..f5e2e6e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/FunctionsTest.php @@ -0,0 +1,177 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class FunctionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @dataProvider getHeaderValuesData + */ + public function testGetHeaderValues($input, $output) + { + $this->assertEquals( + $output, + getHeaderValues($input) + ); + } + + public function getHeaderValuesData() + { + return [ + [ + 'a', + ['a'], + ], + [ + 'a,b', + ['a', 'b'], + ], + [ + 'a, b', + ['a', 'b'], + ], + [ + ['a, b'], + ['a', 'b'], + ], + [ + ['a, b', 'c', 'd,e'], + ['a', 'b', 'c', 'd', 'e'], + ], + ]; + } + + /** + * @dataProvider preferData + */ + public function testPrefer($input, $output) + { + $this->assertEquals( + $output, + parsePrefer($input) + ); + } + + public function preferData() + { + return [ + [ + 'foo; bar', + ['foo' => true], + ], + [ + 'foo; bar=""', + ['foo' => true], + ], + [ + 'foo=""; bar', + ['foo' => true], + ], + [ + 'FOO', + ['foo' => true], + ], + [ + 'respond-async', + ['respond-async' => true], + ], + [ + ['respond-async, wait=100', 'handling=lenient'], + ['respond-async' => true, 'wait' => 100, 'handling' => 'lenient'], + ], + [ + ['respond-async, wait=100, handling=lenient'], + ['respond-async' => true, 'wait' => 100, 'handling' => 'lenient'], + ], + // Old values + [ + 'return-asynch, return-representation', + ['respond-async' => true, 'return' => 'representation'], + ], + [ + 'return-minimal', + ['return' => 'minimal'], + ], + [ + 'strict', + ['handling' => 'strict'], + ], + [ + 'lenient', + ['handling' => 'lenient'], + ], + // Invalid token + [ + ['foo=%bar%'], + [], + ], + ]; + } + + public function testParseHTTPDate() + { + $times = [ + 'Wed, 13 Oct 2010 10:26:00 GMT', + 'Wednesday, 13-Oct-10 10:26:00 GMT', + 'Wed Oct 13 10:26:00 2010', + ]; + + $expected = 1286965560; + + foreach ($times as $time) { + $result = parseDate($time); + $this->assertEquals($expected, $result->format('U')); + } + + $result = parseDate('Wed Oct 6 10:26:00 2010'); + $this->assertEquals(1286360760, $result->format('U')); + } + + public function testParseHTTPDateFail() + { + $times = [ + //random string + 'NOW', + // not-GMT timezone + 'Wednesday, 13-Oct-10 10:26:00 UTC', + // No space before the 6 + 'Wed Oct 6 10:26:00 2010', + // Invalid day + 'Wed Oct 0 10:26:00 2010', + 'Wed Oct 32 10:26:00 2010', + 'Wed, 0 Oct 2010 10:26:00 GMT', + 'Wed, 32 Oct 2010 10:26:00 GMT', + 'Wednesday, 32-Oct-10 10:26:00 GMT', + // Invalid hour + 'Wed, 13 Oct 2010 24:26:00 GMT', + 'Wednesday, 13-Oct-10 24:26:00 GMT', + 'Wed Oct 13 24:26:00 2010', + ]; + + foreach ($times as $time) { + $this->assertFalse(parseDate($time), 'We used the string: '.$time); + } + } + + public function testTimezones() + { + $default = date_default_timezone_get(); + date_default_timezone_set('Europe/Amsterdam'); + + $this->testParseHTTPDate(); + + date_default_timezone_set($default); + } + + public function testToHTTPDate() + { + $dt = new \DateTime('2011-12-10 12:00:00 +0200'); + + $this->assertEquals( + 'Sat, 10 Dec 2011 10:00:00 GMT', + toDate($dt) + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php new file mode 100644 index 0000000..fe9e4a9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class MessageDecoratorTest extends \PHPUnit\Framework\TestCase +{ + protected $inner; + protected $outer; + + public function setUp(): void + { + $this->inner = new Request('GET', '/'); + $this->outer = new RequestDecorator($this->inner); + } + + public function testBody() + { + $this->outer->setBody('foo'); + $this->assertEquals('foo', stream_get_contents($this->inner->getBodyAsStream())); + $this->assertEquals('foo', stream_get_contents($this->outer->getBodyAsStream())); + $this->assertEquals('foo', $this->inner->getBodyAsString()); + $this->assertEquals('foo', $this->outer->getBodyAsString()); + $this->assertEquals('foo', $this->inner->getBody()); + $this->assertEquals('foo', $this->outer->getBody()); + } + + public function testHeaders() + { + $this->outer->setHeaders([ + 'a' => 'b', + ]); + + $this->assertEquals(['a' => ['b']], $this->inner->getHeaders()); + $this->assertEquals(['a' => ['b']], $this->outer->getHeaders()); + + $this->outer->setHeaders([ + 'c' => 'd', + ]); + + $this->assertEquals(['a' => ['b'], 'c' => ['d']], $this->inner->getHeaders()); + $this->assertEquals(['a' => ['b'], 'c' => ['d']], $this->outer->getHeaders()); + + $this->outer->addHeaders([ + 'e' => 'f', + ]); + + $this->assertEquals(['a' => ['b'], 'c' => ['d'], 'e' => ['f']], $this->inner->getHeaders()); + $this->assertEquals(['a' => ['b'], 'c' => ['d'], 'e' => ['f']], $this->outer->getHeaders()); + } + + public function testHeader() + { + $this->assertFalse($this->outer->hasHeader('a')); + $this->assertFalse($this->inner->hasHeader('a')); + $this->outer->setHeader('a', 'c'); + $this->assertTrue($this->outer->hasHeader('a')); + $this->assertTrue($this->inner->hasHeader('a')); + + $this->assertEquals('c', $this->inner->getHeader('A')); + $this->assertEquals('c', $this->outer->getHeader('A')); + + $this->outer->addHeader('A', 'd'); + + $this->assertEquals( + ['c', 'd'], + $this->inner->getHeaderAsArray('A') + ); + $this->assertEquals( + ['c', 'd'], + $this->outer->getHeaderAsArray('A') + ); + + $success = $this->outer->removeHeader('a'); + + $this->assertTrue($success); + $this->assertNull($this->inner->getHeader('A')); + $this->assertNull($this->outer->getHeader('A')); + + $this->assertFalse($this->outer->removeHeader('i-dont-exist')); + } + + public function testHttpVersion() + { + $this->outer->setHttpVersion('1.0'); + + $this->assertEquals('1.0', $this->inner->getHttpVersion()); + $this->assertEquals('1.0', $this->outer->getHttpVersion()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/MessageTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/MessageTest.php new file mode 100644 index 0000000..597067e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/MessageTest.php @@ -0,0 +1,281 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class MessageTest extends \PHPUnit\Framework\TestCase +{ + public function testConstruct() + { + $message = new MessageMock(); + $this->assertInstanceOf('Sabre\HTTP\Message', $message); + } + + public function testStreamBody() + { + $body = 'foo'; + $h = fopen('php://memory', 'r+'); + fwrite($h, $body); + rewind($h); + + $message = new MessageMock(); + $message->setBody($h); + + $this->assertEquals($body, $message->getBodyAsString()); + rewind($h); + $this->assertEquals($body, stream_get_contents($message->getBodyAsStream())); + rewind($h); + $this->assertEquals($body, stream_get_contents($message->getBody())); + } + + public function testStringBody() + { + $body = 'foo'; + + $message = new MessageMock(); + $message->setBody($body); + + $this->assertEquals($body, $message->getBodyAsString()); + $this->assertEquals($body, stream_get_contents($message->getBodyAsStream())); + $this->assertEquals($body, $message->getBody()); + } + + public function testCallbackBodyAsString() + { + $body = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($body); + + $string = $message->getBodyAsString(); + + $this->assertSame('foo', $string); + } + + public function testCallbackBodyAsStream() + { + $body = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($body); + + $stream = $message->getBodyAsStream(); + + $this->assertSame('foo', stream_get_contents($stream)); + } + + public function testGetBodyWhenCallback() + { + $callback = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($callback); + + $this->assertSame($callback, $message->getBody()); + } + + /** + * It's possible that streams contains more data than the Content-Length. + * + * The request object should make sure to never emit more than + * Content-Length, if Content-Length is set. + * + * This is in particular useful when responding to range requests with + * streams that represent files on the filesystem, as it's possible to just + * seek the stream to a certain point, set the content-length and let the + * request object do the rest. + */ + public function testLongStreamToStringBody() + { + $body = fopen('php://memory', 'r+'); + fwrite($body, 'abcdefg'); + fseek($body, 2); + + $message = new MessageMock(); + $message->setBody($body); + $message->setHeader('Content-Length', '4'); + + $this->assertEquals( + 'cdef', + $message->getBodyAsString() + ); + } + + /** + * Some clients include a content-length header, but the header is empty. + * This is definitely broken behavior, but we should support it. + */ + public function testEmptyContentLengthHeader() + { + $body = fopen('php://memory', 'r+'); + fwrite($body, 'abcdefg'); + fseek($body, 2); + + $message = new MessageMock(); + $message->setBody($body); + $message->setHeader('Content-Length', ''); + + $this->assertEquals( + 'cdefg', + $message->getBodyAsString() + ); + } + + public function testGetEmptyBodyStream() + { + $message = new MessageMock(); + $body = $message->getBodyAsStream(); + + $this->assertEquals('', stream_get_contents($body)); + } + + public function testGetEmptyBodyString() + { + $message = new MessageMock(); + $body = $message->getBodyAsString(); + + $this->assertEquals('', $body); + } + + public function testHeaders() + { + $message = new MessageMock(); + $message->setHeader('X-Foo', 'bar'); + + // Testing caselessness + $this->assertEquals('bar', $message->getHeader('X-Foo')); + $this->assertEquals('bar', $message->getHeader('x-fOO')); + + $this->assertTrue( + $message->removeHeader('X-FOO') + ); + $this->assertNull($message->getHeader('X-Foo')); + $this->assertFalse( + $message->removeHeader('X-FOO') + ); + } + + public function testSetHeaders() + { + $message = new MessageMock(); + + $headers = [ + 'X-Foo' => ['1'], + 'X-Bar' => ['2'], + ]; + + $message->setHeaders($headers); + $this->assertEquals($headers, $message->getHeaders()); + + $message->setHeaders([ + 'X-Foo' => ['3', '4'], + 'X-Bar' => '5', + ]); + + $expected = [ + 'X-Foo' => ['3', '4'], + 'X-Bar' => ['5'], + ]; + + $this->assertEquals($expected, $message->getHeaders()); + } + + public function testAddHeaders() + { + $message = new MessageMock(); + + $headers = [ + 'X-Foo' => ['1'], + 'X-Bar' => ['2'], + ]; + + $message->addHeaders($headers); + $this->assertEquals($headers, $message->getHeaders()); + + $message->addHeaders([ + 'X-Foo' => ['3', '4'], + 'X-Bar' => '5', + ]); + + $expected = [ + 'X-Foo' => ['1', '3', '4'], + 'X-Bar' => ['2', '5'], + ]; + + $this->assertEquals($expected, $message->getHeaders()); + } + + public function testSendBody() + { + $message = new MessageMock(); + + // String + $message->setBody('foo'); + + // Stream + $h = fopen('php://memory', 'r+'); + fwrite($h, 'bar'); + rewind($h); + $message->setBody($h); + + $body = $message->getBody(); + rewind($body); + + $this->assertEquals('bar', stream_get_contents($body)); + } + + public function testMultipleHeaders() + { + $message = new MessageMock(); + $message->setHeader('a', '1'); + $message->addHeader('A', '2'); + + $this->assertEquals( + '1,2', + $message->getHeader('A') + ); + $this->assertEquals( + '1,2', + $message->getHeader('a') + ); + + $this->assertEquals( + ['1', '2'], + $message->getHeaderAsArray('a') + ); + $this->assertEquals( + ['1', '2'], + $message->getHeaderAsArray('A') + ); + $this->assertEquals( + [], + $message->getHeaderAsArray('B') + ); + } + + public function testHasHeaders() + { + $message = new MessageMock(); + + $this->assertFalse($message->hasHeader('X-Foo')); + $message->setHeader('X-Foo', 'Bar'); + $this->assertTrue($message->hasHeader('X-Foo')); + } + + /** + * @param string $content + * + * @return \Closure Returns a callback printing $content to php://output stream + */ + private function createCallback($content) + { + return function () use ($content) { + echo $content; + }; + } +} + +class MessageMock extends Message +{ +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/NegotiateTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/NegotiateTest.php new file mode 100644 index 0000000..40c3ecd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/NegotiateTest.php @@ -0,0 +1,135 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class NegotiateTest extends \PHPUnit\Framework\TestCase +{ + /** + * @dataProvider negotiateData + */ + public function testNegotiate($acceptHeader, $available, $expected) + { + $this->assertEquals( + $expected, + negotiateContentType($acceptHeader, $available) + ); + } + + public function negotiateData() + { + return [ + [ // simple + 'application/xml', + ['application/xml'], + 'application/xml', + ], + [ // no header + null, + ['application/xml'], + 'application/xml', + ], + [ // 2 options + 'application/json', + ['application/xml', 'application/json'], + 'application/json', + ], + [ // 2 choices + 'application/json, application/xml', + ['application/xml'], + 'application/xml', + ], + [ // quality + 'application/xml;q=0.2, application/json', + ['application/xml', 'application/json'], + 'application/json', + ], + [ // wildcard + 'image/jpeg, image/png, */*', + ['application/xml', 'application/json'], + 'application/xml', + ], + [ // wildcard + quality + 'image/jpeg, image/png; q=0.5, */*', + ['application/xml', 'application/json', 'image/png'], + 'application/xml', + ], + [ // no match + 'image/jpeg', + ['application/xml'], + null, + ], + [ // This is used in sabre/dav + 'text/vcard; version=4.0', + [ + // Most often used mime-type. Version 3 + 'text/x-vcard', + // The correct standard mime-type. Defaults to version 3 as + // well. + 'text/vcard', + // vCard 4 + 'text/vcard; version=4.0', + // vCard 3 + 'text/vcard; version=3.0', + // jCard + 'application/vcard+json', + ], + 'text/vcard; version=4.0', + ], + [ // rfc7231 example 1 + 'audio/*; q=0.2, audio/basic', + [ + 'audio/pcm', + 'audio/basic', + ], + 'audio/basic', + ], + [ // Lower quality after + 'audio/pcm; q=0.2, audio/basic; q=0.1', + [ + 'audio/pcm', + 'audio/basic', + ], + 'audio/pcm', + ], + [ // Random parameter, should be ignored + 'audio/pcm; hello; q=0.2, audio/basic; q=0.1', + [ + 'audio/pcm', + 'audio/basic', + ], + 'audio/pcm', + ], + [ // No 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/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php new file mode 100644 index 0000000..f6f3a72 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class RequestDecoratorTest extends \PHPUnit\Framework\TestCase +{ + protected $inner; + protected $outer; + + public function setUp(): void + { + $this->inner = new Request('GET', '/'); + $this->outer = new RequestDecorator($this->inner); + } + + public function testMethod() + { + $this->outer->setMethod('FOO'); + $this->assertEquals('FOO', $this->inner->getMethod()); + $this->assertEquals('FOO', $this->outer->getMethod()); + } + + public function testUrl() + { + $this->outer->setUrl('/foo'); + $this->assertEquals('/foo', $this->inner->getUrl()); + $this->assertEquals('/foo', $this->outer->getUrl()); + } + + public function testAbsoluteUrl() + { + $this->outer->setAbsoluteUrl('http://example.org/foo'); + $this->assertEquals('http://example.org/foo', $this->inner->getAbsoluteUrl()); + $this->assertEquals('http://example.org/foo', $this->outer->getAbsoluteUrl()); + } + + public function testBaseUrl() + { + $this->outer->setBaseUrl('/foo'); + $this->assertEquals('/foo', $this->inner->getBaseUrl()); + $this->assertEquals('/foo', $this->outer->getBaseUrl()); + } + + public function testPath() + { + $this->outer->setBaseUrl('/foo'); + $this->outer->setUrl('/foo/bar'); + $this->assertEquals('bar', $this->inner->getPath()); + $this->assertEquals('bar', $this->outer->getPath()); + } + + public function testQueryParams() + { + $this->outer->setUrl('/foo?a=b&c=d&e'); + $expected = [ + 'a' => 'b', + 'c' => 'd', + 'e' => null, + ]; + + $this->assertEquals($expected, $this->inner->getQueryParameters()); + $this->assertEquals($expected, $this->outer->getQueryParameters()); + } + + public function testPostData() + { + $postData = [ + 'a' => 'b', + 'c' => 'd', + 'e' => null, + ]; + + $this->outer->setPostData($postData); + $this->assertEquals($postData, $this->inner->getPostData()); + $this->assertEquals($postData, $this->outer->getPostData()); + } + + public function testServerData() + { + $serverData = [ + 'HTTPS' => 'On', + ]; + + $this->outer->setRawServerData($serverData); + $this->assertEquals('On', $this->inner->getRawServerValue('HTTPS')); + $this->assertEquals('On', $this->outer->getRawServerValue('HTTPS')); + + $this->assertNull($this->inner->getRawServerValue('FOO')); + $this->assertNull($this->outer->getRawServerValue('FOO')); + } + + public function testToString() + { + $this->inner->setMethod('POST'); + $this->inner->setUrl('/foo/bar/'); + $this->inner->setBody('foo'); + $this->inner->setHeader('foo', 'bar'); + + $this->assertEquals((string) $this->inner, (string) $this->outer); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/RequestTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/RequestTest.php new file mode 100644 index 0000000..a810900 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/RequestTest.php @@ -0,0 +1,137 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class RequestTest extends \PHPUnit\Framework\TestCase +{ + public 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()); + } + + public function testGetQueryParameters() + { + $request = new Request('GET', '/foo?a=b&c&d=e'); + $this->assertEquals([ + 'a' => 'b', + 'c' => null, + 'd' => 'e', + ], $request->getQueryParameters()); + } + + public function testGetQueryParametersNoData() + { + $request = new Request('GET', '/foo'); + $this->assertEquals([], $request->getQueryParameters()); + } + + /** + * @backupGlobals + */ + public function testCreateFromPHPRequest() + { + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['REQUEST_METHOD'] = 'PUT'; + + $request = Sapi::getRequest(); + $this->assertEquals('PUT', $request->getMethod()); + } + + public function testGetAbsoluteUrl() + { + $r = new Request('GET', '/foo', [ + 'Host' => 'sabredav.org', + ]); + + $this->assertEquals('http://sabredav.org/foo', $r->getAbsoluteUrl()); + + $s = [ + 'HTTP_HOST' => 'sabredav.org', + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + 'HTTPS' => 'on', + ]; + + $r = Sapi::createFromServerArray($s); + + $this->assertEquals('https://sabredav.org/foo', $r->getAbsoluteUrl()); + } + + public function testGetPostData() + { + $post = [ + 'bla' => 'foo', + ]; + $r = new Request('POST', '/'); + $r->setPostData($post); + $this->assertEquals($post, $r->getPostData()); + } + + public function testGetPath() + { + $request = new Request('GET', '/foo/bar/'); + $request->setBaseUrl('/foo'); + $request->setUrl('/foo/bar/'); + + $this->assertEquals('bar', $request->getPath()); + } + + public function testGetPathStrippedQuery() + { + $request = new Request('GET', '/foo/bar?a=B'); + $request->setBaseUrl('/foo'); + + $this->assertEquals('bar', $request->getPath()); + } + + public function testGetPathMissingSlash() + { + $request = new Request('GET', '/foo'); + $request->setBaseUrl('/foo/'); + + $this->assertEquals('', $request->getPath()); + } + + public function testGetPathOutsideBaseUrl() + { + $this->expectException('LogicException'); + $request = new Request('GET', '/bar/'); + $request->setBaseUrl('/foo/'); + + $request->getPath(); + } + + public function testToString() + { + $request = new Request('PUT', '/foo/bar', ['Content-Type' => 'text/xml']); + $request->setBody('foo'); + + $expected = "PUT /foo/bar HTTP/1.1\r\n" + ."Content-Type: text/xml\r\n" + ."\r\n" + .'foo'; + $this->assertEquals($expected, (string) $request); + } + + public function testToStringAuthorization() + { + $request = new Request('PUT', '/foo/bar', ['Content-Type' => 'text/xml', 'Authorization' => 'Basic foobar']); + $request->setBody('foo'); + + $expected = "PUT /foo/bar HTTP/1.1\r\n" + ."Content-Type: text/xml\r\n" + ."Authorization: Basic REDACTED\r\n" + ."\r\n" + .'foo'; + $this->assertEquals($expected, (string) $request); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php new file mode 100644 index 0000000..a4ef31c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class ResponseDecoratorTest extends \PHPUnit\Framework\TestCase +{ + protected $inner; + protected $outer; + + public function setUp(): void + { + $this->inner = new Response(); + $this->outer = new ResponseDecorator($this->inner); + } + + public function testStatus() + { + $this->outer->setStatus(201); + $this->assertEquals(201, $this->inner->getStatus()); + $this->assertEquals(201, $this->outer->getStatus()); + $this->assertEquals('Created', $this->inner->getStatusText()); + $this->assertEquals('Created', $this->outer->getStatusText()); + } + + public function testToString() + { + $this->inner->setStatus(201); + $this->inner->setBody('foo'); + $this->inner->setHeader('foo', 'bar'); + + $this->assertEquals((string) $this->inner, (string) $this->outer); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ResponseTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ResponseTest.php new file mode 100644 index 0000000..ef21294 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/ResponseTest.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class ResponseTest extends \PHPUnit\Framework\TestCase +{ + public function testConstruct() + { + $response = new Response(200, ['Content-Type' => 'text/xml']); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('OK', $response->getStatusText()); + } + + public function testSetStatus() + { + $response = new Response(); + $response->setStatus('402 Where\'s my money?'); + $this->assertEquals(402, $response->getStatus()); + $this->assertEquals('Where\'s my money?', $response->getStatusText()); + } + + public function testInvalidStatus() + { + $this->expectException('InvalidArgumentException'); + $response = new Response(1000); + } + + public function testToString() + { + $response = new Response(200, ['Content-Type' => 'text/xml']); + $response->setBody('foo'); + + $expected = "HTTP/1.1 200 OK\r\n" + ."Content-Type: text/xml\r\n" + ."\r\n" + .'foo'; + $this->assertEquals($expected, (string) $response); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/SapiTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/SapiTest.php new file mode 100644 index 0000000..3c8e7f5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/SapiTest.php @@ -0,0 +1,262 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class SapiTest extends \PHPUnit\Framework\TestCase +{ + public 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')); + } + + public function testConstructPHPAuth() + { + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'pass', + ]); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Authorization' => ['Basic '.base64_encode('user:pass')], + ], $request->getHeaders()); + } + + public function testConstructPHPAuthDigest() + { + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + 'PHP_AUTH_DIGEST' => 'blabla', + ]); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Authorization' => ['Digest blabla'], + ], $request->getHeaders()); + } + + public function testConstructRedirectAuth() + { + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + 'REDIRECT_HTTP_AUTHORIZATION' => 'Basic bla', + ]); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Authorization' => ['Basic bla'], + ], $request->getHeaders()); + } + + /** + * @runInSeparateProcess + * + * Unfortunately we have no way of testing if the HTTP response code got + * changed. + */ + public function testSend() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('XDebug needs to be installed for this test to run'); + } + + $response = new Response(204, ['Content-Type' => 'text/xml;charset=UTF-8']); + + // Second Content-Type header. Normally this doesn't make sense. + $response->addHeader('Content-Type', 'application/xml'); + $response->setBody('foo'); + + ob_start(); + + Sapi::sendResponse($response); + $headers = xdebug_get_headers(); + + $result = ob_get_clean(); + header_remove(); + + $this->assertEquals( + [ + 'Content-Type: text/xml;charset=UTF-8', + 'Content-Type: application/xml', + ], + $headers + ); + + $this->assertEquals('foo', $result); + } + + /** + * @runInSeparateProcess + * @depends testSend + */ + public function testSendLimitedByContentLengthString() + { + $response = new Response(200); + + $response->addHeader('Content-Length', 19); + $response->setBody('Send this sentence. Ignore this one.'); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + header_remove(); + + $this->assertEquals('Send this sentence.', $result); + } + + /** + * Tests whether http2 is recognized. + */ + public function testRecognizeHttp2() + { + $request = Sapi::createFromServerArray([ + 'SERVER_PROTOCOL' => 'HTTP/2.0', + 'REQUEST_URI' => 'bla', + 'REQUEST_METHOD' => 'GET', + ]); + + $this->assertEquals('2.0', $request->getHttpVersion()); + } + + /** + * @runInSeparateProcess + * @depends testSend + */ + public function testSendLimitedByContentLengthStream() + { + $response = new Response(200, ['Content-Length' => 19]); + + $body = fopen('php://memory', 'w'); + fwrite($body, 'Ignore this. Send this sentence. Ignore this too.'); + rewind($body); + fread($body, 13); + $response->setBody($body); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + header_remove(); + + $this->assertEquals('Send this sentence.', $result); + } + + /** + * @runInSeparateProcess + * @depends testSend + * @dataProvider sendContentRangeStreamData + */ + public function testSendContentRangeStream($ignoreAtStart, $sendText, $multiplier, $ignoreAtEnd, $contentLength) + { + $partial = str_repeat($sendText, $multiplier); + $ignoreAtStartLength = strlen($ignoreAtStart); + $ignoreAtEndLength = strlen($ignoreAtEnd); + $body = fopen('php://memory', 'w'); + if (!$contentLength) { + $contentLength = strlen($partial); + } + fwrite($body, $ignoreAtStart); + fwrite($body, $partial); + if ($ignoreAtEndLength > 0) { + fwrite($body, $ignoreAtEnd); + } + rewind($body); + if ($ignoreAtStartLength > 0) { + fread($body, $ignoreAtStartLength); + } + $response = new Response(200, [ + 'Content-Length' => $contentLength, + 'Content-Range' => sprintf('bytes %d-%d/%d', $ignoreAtStartLength, $ignoreAtStartLength + strlen($partial) - 1, $ignoreAtStartLength + strlen($partial) + $ignoreAtEndLength), + ]); + $response->setBody($body); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + header_remove(); + + $this->assertEquals($partial, $result); + } + + public function sendContentRangeStreamData() + { + return [ + ['Ignore this. ', 'Send this.', 10, ' Ignore this at end.'], + ['Ignore this. ', 'Send this.', 1000, ' Ignore this at end.'], + ['Ignore this. ', 'S', 4096, ' Ignore this at end.'], + ['I', 'S', 4094, 'E'], + ['', 'Send this.', 10, ' Ignore this at end.'], + ['', 'Send this.', 1000, ' Ignore this at end.'], + ['', 'S', 4096, ' Ignore this at end.'], + ['', 'S', 4094, 'En'], + ['Ignore this. ', 'Send this.', 10, ''], + ['Ignore this. ', 'Send this.', 1000, ''], + ['Ignore this. ', 'S', 4096, ''], + ['Ig', 'S', 4094, ''], + + // Provide contentLength greater than the bytes remaining in the stream. + ['Ignore this. ', 'Send this.', 10, '', 101], + ['Ignore this. ', 'Send this.', 1000, '', 10001], + ['Ignore this. ', 'S', 4096, '', 5000000], + ['I', 'S', 4094, '', 8095], + // Provide contentLength equal to the bytes remaining in the stream. + ['', 'Send this.', 10, '', 100], + ['Ignore this. ', 'Send this.', 1000, '', 10000], + ]; + } + + /** + * @runInSeparateProcess + * @depends testSend + */ + public function testSendWorksWithCallbackAsBody() + { + $response = new Response(200, [], function () { + $fd = fopen('php://output', 'r+'); + fwrite($fd, 'foo'); + fclose($fd); + }); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + + $this->assertEquals('foo', $result); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/URLUtilTest.php b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/URLUtilTest.php new file mode 100644 index 0000000..6ad1d3c --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/HTTP/URLUtilTest.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); + +namespace Sabre\HTTP; + +class URLUtilTest extends \PHPUnit\Framework\TestCase +{ + public function testEncodePath() + { + $str = ''; + for ($i = 0; $i < 128; ++$i) { + $str .= chr($i); + } + + $newStr = 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, decodePath($newStr)); + } + + public function testEncodePathSegment() + { + $str = ''; + for ($i = 0; $i < 128; ++$i) { + $str .= chr($i); + } + + $newStr = encodePathSegment($str); + + // Note: almost exactly the same as the last test, with the + // exception of the encoding of / (ascii code 2f) + $this->assertEquals( + '%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f'. + '%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f'. + '%20%21%22%23%24%25%26%27()%2a%2b%2c-.%2f'. + '0123456789:%3b%3c%3d%3e%3f'. + '@ABCDEFGHIJKLMNO'. + 'PQRSTUVWXYZ%5b%5c%5d%5e_'. + '%60abcdefghijklmno'. + 'pqrstuvwxyz%7b%7c%7d~%7f', + $newStr); + + $this->assertEquals($str, decodePathSegment($newStr)); + } + + public function testDecode() + { + $str = 'Hello%20Test+Test2.txt'; + $newStr = decodePath($str); + $this->assertEquals('Hello Test+Test2.txt', $newStr); + } + + /** + * @depends testDecode + */ + public function testDecodeUmlaut() + { + $str = 'Hello%C3%BC.txt'; + $newStr = decodePath($str); + $this->assertEquals("Hello\xC3\xBC.txt", $newStr); + } + + /** + * @depends testDecodeUmlaut + */ + public function testDecodeUmlautLatin1() + { + $str = 'Hello%FC.txt'; + $newStr = decodePath($str); + $this->assertEquals("Hello\xC3\xBC.txt", $newStr); + } + + /** + * This testcase was sent by a bug reporter. + * + * @depends testDecode + */ + public function testDecodeAccentsWindows7() + { + $str = '/webdav/%C3%A0fo%C3%B3'; + $newStr = decodePath($str); + $this->assertEquals(strtolower($str), encodePath($newStr)); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/bootstrap.php b/plugins/panakour/backup/vendor/sabre/http/tests/bootstrap.php new file mode 100644 index 0000000..dafef92 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/bootstrap.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +date_default_timezone_set('UTC'); + +ini_set('error_reporting', (string) (E_ALL | E_STRICT | E_DEPRECATED)); + +// Composer autoloader +include __DIR__.'/../vendor/autoload.php'; diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/phpcs/ruleset.xml b/plugins/panakour/backup/vendor/sabre/http/tests/phpcs/ruleset.xml new file mode 100644 index 0000000..ec2c4c8 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/http/tests/phpunit.xml b/plugins/panakour/backup/vendor/sabre/http/tests/phpunit.xml new file mode 100644 index 0000000..d08f8b9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/phpunit.xml @@ -0,0 +1,27 @@ +<phpunit + colors="true" + beStrictAboutCoversAnnotation="true" + beStrictAboutOutputDuringTests="true" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutTodoAnnotatedTests="true" + bootstrap="bootstrap.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + > + <testsuites> + <testsuite name="Sabre_HTTP"> + <directory>HTTP/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist addUncoveredFilesFromWhitelist="true"> + <directory suffix=".php">../lib/</directory> + </whitelist> + </filter> + + <php> + <env name="BASEURL" value="http://localhost"/> + </php> +</phpunit> diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/www/bar.php b/plugins/panakour/backup/vendor/sabre/http/tests/www/bar.php new file mode 100644 index 0000000..05de39e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/www/bar.php @@ -0,0 +1,5 @@ +<?php + +header('X-Test: Bar'); +?> +bar diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/www/foo b/plugins/panakour/backup/vendor/sabre/http/tests/www/foo new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/www/foo @@ -0,0 +1 @@ +foo diff --git a/plugins/panakour/backup/vendor/sabre/http/tests/www/large.php b/plugins/panakour/backup/vendor/sabre/http/tests/www/large.php new file mode 100644 index 0000000..65fefb5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/http/tests/www/large.php @@ -0,0 +1,7 @@ +<?php + +$data = str_repeat('x', 32 * 1024 * 1024); +header('Content-Length: '.strlen($data)); +header('Content-Type: text/plain'); + +echo $data; diff --git a/plugins/panakour/backup/vendor/sabre/uri/LICENSE b/plugins/panakour/backup/vendor/sabre/uri/LICENSE new file mode 100644 index 0000000..ae2c992 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/uri/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2014-2019 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/panakour/backup/vendor/sabre/uri/composer.json b/plugins/panakour/backup/vendor/sabre/uri/composer.json new file mode 100644 index 0000000..29194c6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/uri/composer.json @@ -0,0 +1,46 @@ +{ + "name": "sabre/uri", + "description": "Functions for making sense out of URIs.", + "keywords": [ + "URI", + "URL", + "rfc3986" + ], + "homepage": "http://sabre.io/uri/", + "license": "BSD-3-Clause", + "require": { + "php": "^7.1" + }, + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "source": "https://github.com/fruux/sabre-uri" + }, + "autoload": { + "files" : [ + "lib/functions.php" + ], + "psr-4" : { + "Sabre\\Uri\\" : "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "Sabre\\Uri\\": "tests/" + } + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit" : "^7 || ^8" + }, + "config" : { + "bin-dir" : "bin/" + } +} diff --git a/plugins/panakour/backup/vendor/sabre/uri/lib/InvalidUriException.php b/plugins/panakour/backup/vendor/sabre/uri/lib/InvalidUriException.php new file mode 100644 index 0000000..7f37ca5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/uri/lib/InvalidUriException.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +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/plugins/panakour/backup/vendor/sabre/uri/lib/Version.php b/plugins/panakour/backup/vendor/sabre/uri/lib/Version.php new file mode 100644 index 0000000..ba7caf2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/uri/lib/Version.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +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 = '2.2.0'; +} diff --git a/plugins/panakour/backup/vendor/sabre/uri/lib/functions.php b/plugins/panakour/backup/vendor/sabre/uri/lib/functions.php new file mode 100644 index 0000000..329c862 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/uri/lib/functions.php @@ -0,0 +1,376 @@ +<?php + +declare(strict_types=1); + +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. + * + * @throws InvalidUriException + */ +function resolve(string $basePath, string $newPath): string +{ + $delta = parse($newPath); + + // If the new path defines a scheme, it's absolute and we can just return + // that. + if ($delta['scheme']) { + return build($delta); + } + + $base = parse($basePath); + $pick = function ($part) use ($base, $delta) { + if ($delta[$part]) { + return $delta[$part]; + } elseif ($base[$part]) { + return $base[$part]; + } + + return null; + }; + + $newParts = []; + + $newParts['scheme'] = $pick('scheme'); + $newParts['host'] = $pick('host'); + $newParts['port'] = $pick('port'); + + $path = ''; + if (is_string($delta['path']) and strlen($delta['path']) > 0) { + // If the path starts with a slash + if ('/' === $delta['path'][0]) { + $path = $delta['path']; + } else { + // Removing last component from base path. + $path = $base['path']; + $length = strrpos((string) $path, '/'); + if (false !== $length) { + $path = substr($path, 0, $length); + } + $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. + * + * @throws InvalidUriException + */ +function normalize(string $uri): string +{ + $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. + * + * @return array<string, string> + * + * @throws InvalidUriException + */ +function parse(string $uri): array +{ + // Normally a URI must be ASCII, however. However, often it's not and + // parse_url might corrupt these strings. + // + // For that reason we take any non-ascii characters from the uri and + // uriencode them first. + $uri = preg_replace_callback( + '/[^[:ascii:]]/u', + function ($matches) { + return rawurlencode($matches[0]); + }, + $uri + ); + + $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<string, string> $parts + */ +function build(array $parts): string +{ + $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']) && 'file' === $parts['scheme'])) { + // 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. + * + * @return array<int, mixed> + */ +function split(string $path): array +{ + $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. + * + * @return array<string, mixed> + * + * @throws InvalidUriException + */ +function _parse_fallback(string $uri): array +{ + // Normally a URI must be ASCII, however. However, often it's not and + // parse_url might corrupt these strings. + // + // For that reason we take any non-ascii characters from the uri and + // uriencode them first. + $uri = preg_replace_callback( + '/[^[:ascii:]]/u', + function ($matches) { + return rawurlencode($matches[0]); + }, + $uri + ); + + $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 (false !== strpos($uri, '#')) { + list($uri, $result['fragment']) = explode('#', $uri, 2); + } + // Taking off the query part + if (false !== strpos($uri, '?')) { + list($uri, $result['query']) = explode('?', $uri, 2); + } + + if ('///' === substr($uri, 0, 3)) { + // The triple slash uris are a bit unusual, but we have special handling + // for them. + $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 (isset($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/plugins/panakour/backup/vendor/sabre/uri/phpstan.neon b/plugins/panakour/backup/vendor/sabre/uri/phpstan.neon new file mode 100644 index 0000000..91375db --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/uri/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + level: 7 diff --git a/plugins/panakour/backup/vendor/sabre/vobject/CHANGELOG.md b/plugins/panakour/backup/vendor/sabre/vobject/CHANGELOG.md new file mode 100644 index 0000000..5126e04 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/CHANGELOG.md @@ -0,0 +1,850 @@ +ChangeLog +========= + +4.3.0 (2020-01-31) +------------------ + +* Added support for PHP 7.4, dropped support for PHP 7.0 (@phil-davis) +* #487: Added phpstan coverage, updated testsuite for phpunit8 (@phil-davis, @JeroenVanOort) +* #495: refactored maps to use ::class notation (@JeroenVanOort) + +4.2.2 (2020-01-14) +------------------ + +* #465: Add TZ in iTip REPLY iTip messages +* #486: Add PHONE-NUMBER value type (used for TEL in vCard 3.0) + +4.2.1 (2019-12-18) +------------------ + +* #469, #451: fix compat with php 7.4 +* #443: prevent running in indefinte loop +* #449: Preventing creating a component for a root document +* #450: Fix parse with option Forgiving with trailing equal +* #459: fixed typo in VCalendar which resulting in usage of the wrong TimeZone +* #462: Broker::parseEventForOrganizer copies DTSTAMP from $eventInfo that causes broken scheduling + +4.2.0 (2019-02-19) +------------------ + +* #432: DTSTAMP must be specified in UTC +* #435: ORGANIZER e-mail address are case-insensitive +* #441: Repairing BASE64 encoded vCard version 3 + +4.2.0-alpha1 (2018-09-26) +------------------------- + +* #412: Broker: add timezone to CANCEL messages +* #424: Support php7.3 + +4.1.6 (2018-04-20) +------------------ + +* #406, #407, #408, #409: Another round of performance improvements in serialization of properties (@gharlan, @staabm) +* #410: Fixes in iTip for handling `BYDAY=SA,SO` (@gharlan) +* #381: Fixes in iTip handling of `SCHEDULE-FORCE-SEND` (@alecpl) + +4.1.5 (2018-03-08) +------------------ + +* #404: Serialization: Performance boost for long properties (@gharlan) + +4.1.4 (2017-12-22) +------------------ + +* #383: Fix possible infinite loop in RRuleIterator, when the RRule FREQ + is YEARLY and it uses BYYEARDAY only (@mvdnes). +* #392: Improved significant change detection. This should reduce the number of + unneeded update emails in scheduling systems. (@alecpl). +* #395: Removed `Canada/East-Saskatchewan` timezone, as it got removed + from PHP as well. (@remicollet). + + +4.1.3 (2017-10-18) +------------------ + +* #363: Repair script and de-duplicate properties that are only allowed once, + but appear more than once. (@ddolcimascolo). +* #377: Added Pacific Time (US & Canada) as exchange timezone +* #384: Added fallback for VCards without `FN` + + +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/plugins/panakour/backup/vendor/sabre/vobject/LICENSE b/plugins/panakour/backup/vendor/sabre/vobject/LICENSE new file mode 100644 index 0000000..a99c8da --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2011-2016 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/panakour/backup/vendor/sabre/vobject/README.md b/plugins/panakour/backup/vendor/sabre/vobject/README.md new file mode 100644 index 0000000..5030cf2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/README.md @@ -0,0 +1,55 @@ +sabre/vobject +============= + +The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545) +and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP. + +The goal of the VObject library is to create a very complete library, with an easy to use API. + + +Installation +------------ + +Make sure you have [Composer][1] installed, and then run: + + composer require sabre/vobject "^4.0" + +This package requires PHP 5.5. If you need the PHP 5.3/5.4 version of this package instead, use: + + + composer require sabre/vobject "^3.4" + + +Usage +----- + +* [Working with vCards](http://sabre.io/vobject/vcard/) +* [Working with iCalendar](http://sabre.io/vobject/icalendar/) + + + +Build status +------------ + +| branch | status | +| ------ | ------ | +| master | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=master)](https://travis-ci.org/sabre-io/vobject) | +| 3.5 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.5)](https://travis-ci.org/sabre-io/vobject) | +| 3.4 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.4)](https://travis-ci.org/sabre-io/vobject) | +| 3.1 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.1)](https://travis-ci.org/sabre-io/vobject) | +| 2.1 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=2.1)](https://travis-ci.org/sabre-io/vobject) | +| 2.0 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=2.0)](https://travis-ci.org/sabre-io/vobject) | + + + +Support +------- + +Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions. + +Made at fruux +------------- + +This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support. + +[1]: https://getcomposer.org/ diff --git a/plugins/panakour/backup/vendor/sabre/vobject/bin/bench.php b/plugins/panakour/backup/vendor/sabre/vobject/bin/bench.php new file mode 100644 index 0000000..0a2736f --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/bin/bench_freebusygenerator.php b/plugins/panakour/backup/vendor/sabre/vobject/bin/bench_freebusygenerator.php new file mode 100644 index 0000000..1299c14 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/bin/bench_freebusygenerator.php @@ -0,0 +1,53 @@ +<?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/plugins/panakour/backup/vendor/sabre/vobject/bin/bench_manipulatevcard.php b/plugins/panakour/backup/vendor/sabre/vobject/bin/bench_manipulatevcard.php new file mode 100644 index 0000000..f229091 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/bin/bench_manipulatevcard.php @@ -0,0 +1,64 @@ +<?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/plugins/panakour/backup/vendor/sabre/vobject/bin/fetch_windows_zones.php b/plugins/panakour/backup/vendor/sabre/vobject/bin/fetch_windows_zones.php new file mode 100644 index 0000000..9c4e51a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/bin/fetch_windows_zones.php @@ -0,0 +1,49 @@ +#!/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/plugins/panakour/backup/vendor/sabre/vobject/bin/generate_vcards b/plugins/panakour/backup/vendor/sabre/vobject/bin/generate_vcards new file mode 100644 index 0000000..4663c3c --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/bin/generateicalendardata.php b/plugins/panakour/backup/vendor/sabre/vobject/bin/generateicalendardata.php new file mode 100644 index 0000000..62b6107 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/bin/generateicalendardata.php @@ -0,0 +1,87 @@ +#!/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/plugins/panakour/backup/vendor/sabre/vobject/bin/mergeduplicates.php b/plugins/panakour/backup/vendor/sabre/vobject/bin/mergeduplicates.php new file mode 100644 index 0000000..e6cde73 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/bin/mergeduplicates.php @@ -0,0 +1,160 @@ +#!/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 ('EMAIL' === $newProp->name || 'TEL' === $newProp->name) { + // We're going to make another attempt to find this + // property, this time just by value. If we find it, we + // consider it a success. + foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { + if ($compareProp->getValue() === $newProp->getValue()) { + $ok = true; + break; + } + } + + if (!$ok) { + // Merging the new value in the old vcard. + $collectedNames[$fn]->add(clone $newProp); + $ok = true; + ++$stats['Merged values']; + } + } + } + + if (!$ok) { + // echo $newProp->serialize() . " does not appear in earlier vcard!\n"; + ++$stats['Error']; + if ($debug) { + fwrite($debug, "Missing '".$newProp->name."' property in duplicate. Earlier vcard:\n".$collectedNames[$fn]->serialize()."\n\nLater:\n".$vcard->serialize()."\n\n"); + } + + $vcard->destroy(); + continue 2; + } + } + } + + $vcard->destroy(); + ++$stats['Ignored duplicates']; +} + +foreach ($collectedNames as $vcard) { + // Overwriting any old PRODID + $vcard->PRODID = '-//Sabre//Sabre VObject '.Version::VERSION.'//EN'; + write($vcard); + writeStats(); +} + +echo str_repeat("\n", count($stats)), "\nDone.\n"; diff --git a/plugins/panakour/backup/vendor/sabre/vobject/bin/rrulebench.php b/plugins/panakour/backup/vendor/sabre/vobject/bin/rrulebench.php new file mode 100644 index 0000000..583da57 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/bin/vobject b/plugins/panakour/backup/vendor/sabre/vobject/bin/vobject new file mode 100644 index 0000000..2aca7e7 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/composer.json b/plugins/panakour/backup/vendor/sabre/vobject/composer.json new file mode 100644 index 0000000..9e522f8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/composer.json @@ -0,0 +1,92 @@ +{ + "name": "sabre/vobject", + "description" : "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "keywords" : [ + "iCalendar", + "iCal", + "vCalendar", + "vCard", + "jCard", + "jCal", + "ics", + "vcf", + "xCard", + "xCal", + "freebusy", + "recurrence", + "availability", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868" + ], + "homepage" : "http://sabre.io/vobject/", + "license" : "BSD-3-Clause", + "require" : { + "php" : "^7.1", + "ext-mbstring" : "*", + "sabre/xml" : "^2.1" + }, + "require-dev" : { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit" : "^7 || ^8" + }, + "suggest" : { + "hoa/bench" : "If you would like to run the benchmark scripts" + }, + "authors" : [ + { + "name" : "Evert Pot", + "email" : "me@evertpot.com", + "homepage" : "http://evertpot.com/", + "role" : "Developer" + }, + { + "name" : "Dominik Tobschall", + "email" : "dominik@fruux.com", + "homepage" : "http://tobschall.de/", + "role" : "Developer" + }, + { + "name" : "Ivan Enderlin", + "email" : "ivan.enderlin@hoa-project.net", + "homepage" : "http://mnt.io/", + "role" : "Developer" + } + ], + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-vobject" + }, + "autoload" : { + "psr-4" : { + "Sabre\\VObject\\" : "lib/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\VObject\\" : "tests/VObject" + } + }, + "bin" : [ + "bin/vobject", + "bin/generate_vcards" + ], + "extra" : { + "branch-alias" : { + "dev-master" : "4.0.x-dev" + } + }, + "config" : { + "bin-dir" : "bin" + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php new file mode 100644 index 0000000..fade50e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php @@ -0,0 +1,172 @@ +<?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 + */ + public 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 + */ + public function setObjects($objects) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + + $this->objects = []; + foreach ($objects as $object) { + if (is_string($object)) { + $vObj = Reader::read($object); + if (!$vObj instanceof Component\VCard) { + throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects'); + } + + $this->objects[] = $vObj; + } elseif ($object instanceof Component\VCard) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects'); + } + } + } + + /** + * Sets the output format for the SUMMARY. + * + * @param string $format + */ + public function setFormat($format) + { + $this->format = $format; + } + + /** + * Parses the input data and returns a VCALENDAR. + * + * @return Component/VCalendar + */ + public function getResult() + { + $calendar = new VCalendar(); + + foreach ($this->objects as $object) { + // Skip if there is no BDAY property. + if (!$object->select('BDAY')) { + continue; + } + + // We've seen clients (ez-vcard) putting "BDAY:" properties + // without a value into vCards. If we come across those, we'll + // skip them. + if (empty($object->BDAY->getValue())) { + continue; + } + + // We're always converting to vCard 4.0 so we can rely on the + // VCardConverter handling the X-APPLE-OMIT-YEAR property for us. + $object = $object->convert(Document::VCARD40); + + // Skip if the card has no FN property. + if (!isset($object->FN)) { + continue; + } + + // Skip if the BDAY property is not of the right type. + if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) { + continue; + } + + // Skip if we can't parse the BDAY value. + try { + $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue()); + } catch (InvalidDataException $e) { + continue; + } + + // Set a year if it's not set. + $unknownYear = false; + + if (!$dateParts['year']) { + $object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date']; + + $unknownYear = true; + } + + // Create event. + $event = $calendar->add('VEVENT', [ + 'SUMMARY' => sprintf($this->format, $object->FN->getValue()), + 'DTSTART' => new \DateTime($object->BDAY->getValue()), + 'RRULE' => 'FREQ=YEARLY', + 'TRANSP' => 'TRANSPARENT', + ]); + + // add VALUE=date + $event->DTSTART['VALUE'] = 'DATE'; + + // Add X-SABRE-BDAY property. + if ($unknownYear) { + $event->add('X-SABRE-BDAY', 'BDAY', [ + 'X-SABRE-VCARD-UID' => $object->UID->getValue(), + 'X-SABRE-VCARD-FN' => $object->FN->getValue(), + 'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR, + ]); + } else { + $event->add('X-SABRE-BDAY', 'BDAY', [ + 'X-SABRE-VCARD-UID' => $object->UID->getValue(), + 'X-SABRE-VCARD-FN' => $object->FN->getValue(), + ]); + } + } + + return $calendar; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Cli.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Cli.php new file mode 100644 index 0000000..f3e419b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Cli.php @@ -0,0 +1,712 @@ +<?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; + + /** + * Whether 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 + */ + public 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 (0 === count($positional)) { + $this->showHelp(); + + return 1; + } + + if (1 === count($positional)) { + throw new InvalidArgumentException('Inputfile is a required argument'); + } + + if (count($positional) > 3) { + throw new InvalidArgumentException('Too many arguments'); + } + + if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { + throw new InvalidArgumentException('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 ('.json' === substr($this->inputPath, -5)) { + $this->inputFormat = 'json'; + } else { + $this->inputFormat = 'mimedir'; + } + } + if (!$this->format) { + if ('.json' === substr($this->outputPath, -5)) { + $this->format = 'json'; + } else { + $this->format = 'mimedir'; + } + } + + $realCode = 0; + + try { + while ($input = $this->readInput()) { + $returnCode = $this->$command($input); + if (0 !== $returnCode) { + $realCode = $returnCode; + } + } + } catch (EofException $e) { + // end of file + } catch (\Exception $e) { + $this->log('Error: '.$e->getMessage(), 'red'); + + return 2; + } + + return $realCode; + } + + /** + * Shows the help message. + */ + protected function showHelp() + { + $this->log('Usage:', 'yellow'); + $this->log(' vobject [options] command [arguments]'); + $this->log(''); + $this->log('Options:', 'yellow'); + $this->log($this->colorize('green', ' -q ')."Don't output anything."); + $this->log($this->colorize('green', ' -help -h ').'Display this help message.'); + $this->log($this->colorize('green', ' --format ').'Convert to a specific format. Must be one of: vcard, vcard21,'); + $this->log($this->colorize('green', ' --forgiving ').'Makes the parser less strict.'); + $this->log(' vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir.'); + $this->log($this->colorize('green', ' --inputformat ').'If the input format cannot be guessed from the extension, it'); + $this->log(' must be specified here.'); + // Only PHP 5.4 and up + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + $this->log($this->colorize('green', ' --pretty ').'json pretty-print.'); + } + $this->log(''); + $this->log('Commands:', 'yellow'); + $this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.'); + $this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.'); + $this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.'); + $this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debugging.'); + $this->log( + <<<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. + * + * @return int + */ + protected function validate(Component $vObj) + { + $returnCode = 0; + + switch ($vObj->name) { + case 'VCALENDAR': + $this->log('iCalendar: '.(string) $vObj->VERSION); + break; + case 'VCARD': + $this->log('vCard: '.(string) $vObj->VERSION); + break; + } + + $warnings = $vObj->validate(); + if (!count($warnings)) { + $this->log(' No warnings!'); + } else { + $levels = [ + 1 => 'REPAIRED', + 2 => 'WARNING', + 3 => 'ERROR', + ]; + $returnCode = 2; + foreach ($warnings as $warn) { + $extra = ''; + if ($warn['node'] instanceof Property) { + $extra = ' (property: "'.$warn['node']->name.'")'; + } + $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra); + } + } + + return $returnCode; + } + + /** + * Repairs a VObject file. + * + * @return int + */ + protected function repair(Component $vObj) + { + $returnCode = 0; + + switch ($vObj->name) { + case 'VCALENDAR': + $this->log('iCalendar: '.(string) $vObj->VERSION); + break; + case 'VCARD': + $this->log('vCard: '.(string) $vObj->VERSION); + break; + } + + $warnings = $vObj->validate(Node::REPAIR); + if (!count($warnings)) { + $this->log(' No warnings!'); + } else { + $levels = [ + 1 => 'REPAIRED', + 2 => 'WARNING', + 3 => 'ERROR', + ]; + $returnCode = 2; + foreach ($warnings as $warn) { + $extra = ''; + if ($warn['node'] instanceof Property) { + $extra = ' (property: "'.$warn['node']->name.'")'; + } + $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra); + } + } + fwrite($this->stdout, $vObj->serialize()); + + return $returnCode; + } + + /** + * Converts a vObject file to a new format. + * + * @param Component $vObj + * + * @return int + */ + protected function convert($vObj) + { + $json = false; + $convertVersion = null; + $forceInput = null; + + switch ($this->format) { + case 'json': + $json = true; + if ('VCARD' === $vObj->name) { + $convertVersion = Document::VCARD40; + } + break; + case 'jcard': + $json = true; + $forceInput = 'VCARD'; + $convertVersion = Document::VCARD40; + break; + case 'jcal': + $json = true; + $forceInput = 'VCALENDAR'; + break; + case 'mimedir': + case 'icalendar': + case 'icalendar20': + case 'vcard': + break; + case 'vcard21': + $convertVersion = Document::VCARD21; + break; + case 'vcard30': + $convertVersion = Document::VCARD30; + break; + case 'vcard40': + $convertVersion = Document::VCARD40; + break; + } + + if ($forceInput && $vObj->name !== $forceInput) { + throw new \Exception('You cannot convert a '.strtolower($vObj->name).' to '.$this->format); + } + if ($convertVersion) { + $vObj = $vObj->convert($convertVersion); + } + if ($json) { + $jsonOptions = 0; + if ($this->pretty) { + $jsonOptions = JSON_PRETTY_PRINT; + } + fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); + } else { + fwrite($this->stdout, $vObj->serialize()); + } + + return 0; + } + + /** + * Colorizes a file. + * + * @param Component $vObj + * + * @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 + */ + protected function cWrite($color, $str) + { + fwrite($this->stdout, $this->colorize($color, $str)); + } + + protected function serializeComponent(Component $vObj) + { + $this->cWrite('cyan', 'BEGIN'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name."\n"); + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accommodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * + * @return int + */ + $sortScore = function ($key, $array) { + if ($array[$key] instanceof Component) { + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ('VTIMEZONE' === $array[$key]->name) { + $score = 300000000; + + return $score + $key; + } else { + $score = 400000000; + + return $score + $key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ('VERSION' === $array[$key]->name) { + $score = 100000000; + + return $score + $key; + } else { + // All other properties + $score = 200000000; + + return $score + $key; + } + } + } + }; + + $children = $vObj->children(); + $tmp = $children; + uksort( + $children, + function ($a, $b) use ($sortScore, $tmp) { + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + } + ); + + foreach ($children as $child) { + if ($child instanceof Component) { + $this->serializeComponent($child); + } else { + $this->serializeProperty($child); + } + } + + $this->cWrite('cyan', 'END'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name."\n"); + } + + /** + * Colorizes a property. + */ + protected function serializeProperty(Property $property) + { + if ($property->group) { + $this->cWrite('default', $property->group); + $this->cWrite('red', '.'); + } + + $this->cWrite('yellow', $property->name); + + foreach ($property->parameters as $param) { + $this->cWrite('red', ';'); + $this->cWrite('blue', $param->serialize()); + } + $this->cWrite('red', ':'); + + if ($property instanceof Property\Binary) { + $this->cWrite('default', 'embedded binary stripped. ('.strlen($property->getValue()).' bytes)'); + } else { + $parts = $property->getParts(); + $first1 = true; + // Looping through property values + foreach ($parts as $part) { + if ($first1) { + $first1 = false; + } else { + $this->cWrite('red', $property->delimiter); + } + $first2 = true; + // Looping through property sub-values + foreach ((array) $part as $subPart) { + if ($first2) { + $first2 = false; + } else { + // The sub-value delimiter is always comma + $this->cWrite('red', ','); + } + + $subPart = strtr( + $subPart, + [ + '\\' => $this->colorize('purple', '\\\\', 'green'), + ';' => $this->colorize('purple', '\;', 'green'), + ',' => $this->colorize('purple', '\,', 'green'), + "\n" => $this->colorize('purple', "\\n\n\t", 'green'), + "\r" => '', + ] + ); + + $this->cWrite('green', $subPart); + } + } + } + $this->cWrite('default', "\n"); + } + + /** + * Parses the list of arguments. + */ + protected function parseArguments(array $argv) + { + $positional = []; + $options = []; + + for ($ii = 0; $ii < count($argv); ++$ii) { + // Skipping the first argument. + if (0 === $ii) { + continue; + } + + $v = $argv[$ii]; + + if ('--' === substr($v, 0, 2)) { + // This is a long-form option. + $optionName = substr($v, 2); + $optionValue = true; + if (strpos($optionName, '=')) { + list($optionName, $optionValue) = explode('=', $optionName); + } + $options[$optionName] = $optionValue; + } elseif ('-' === substr($v, 0, 1) && strlen($v) > 1) { + // This is a short-form option. + foreach (str_split(substr($v, 1)) as $option) { + $options[$option] = true; + } + } else { + $positional[] = $v; + } + } + + return [$options, $positional]; + } + + protected $parser; + + /** + * Reads the input file. + * + * @return Component + */ + protected function readInput() + { + if (!$this->parser) { + if ('-' !== $this->inputPath) { + $this->stdin = fopen($this->inputPath, 'r'); + } + + if ('mimedir' === $this->inputFormat) { + $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); + } else { + $this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); + } + } + + return $this->parser->parse(); + } + + /** + * Sends a message to STDERR. + * + * @param string $msg + */ + protected function log($msg, $color = 'default') + { + if (!$this->quiet) { + if ('default' !== $color) { + $msg = $this->colorize($color, $msg); + } + fwrite($this->stderr, $msg."\n"); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component.php new file mode 100644 index 0000000..58594ae --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component.php @@ -0,0 +1,671 @@ +<?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 string $name such as VCALENDAR, VEVENT + * @param bool $defaults + */ + public 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 + */ + public function add() + { + $arguments = func_get_args(); + + if ($arguments[0] instanceof Node) { + if (isset($arguments[1])) { + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); + } + $arguments[0]->parent = $this; + $newNode = $arguments[0]; + } elseif (is_string($arguments[0])) { + $newNode = call_user_func_array([$this->root, 'create'], $arguments); + } else { + throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); + } + + $name = $newNode->name; + if (isset($this->children[$name])) { + $this->children[$name][] = $newNode; + } else { + $this->children[$name] = [$newNode]; + } + + return $newNode; + } + + /** + * This method removes a component or property from this component. + * + * You can either specify the item by name (like DTSTART), in which case + * all properties/components with that name will be removed, or you can + * pass an instance of a property or component, in which case only that + * exact item will be removed. + * + * @param string|Property|Component $item + */ + public function remove($item) + { + if (is_string($item)) { + // If there's no dot in the name, it's an exact property name and + // we can just wipe out all those properties. + // + if (false === strpos($item, '.')) { + unset($this->children[strtoupper($item)]); + + return; + } + // If there was a dot, we need to ask select() to help us out and + // then we just call remove recursively. + foreach ($this->select($item) as $child) { + $this->remove($child); + } + } else { + foreach ($this->select($item->name) as $k => $child) { + if ($child === $item) { + unset($this->children[$item->name][$k]); + + return; + } + } + } + + throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component'); + } + + /** + * Returns a flat list of all the properties and components in this + * component. + * + * @return array + */ + public function children() + { + $result = []; + foreach ($this->children as $childGroup) { + $result = array_merge($result, $childGroup); + } + + return $result; + } + + /** + * This method only returns a list of sub-components. Properties are + * ignored. + * + * @return array + */ + public function getComponents() + { + $result = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $result[] = $child; + } + } + } + + return $result; + } + + /** + * Returns an array with elements that match the specified name. + * + * This function is also aware of MIME-Directory groups (as they appear in + * vcards). This means that if a property is grouped as "HOME.EMAIL", it + * will also be returned when searching for just "EMAIL". If you want to + * search for a property in a specific group, you can select on the entire + * string ("HOME.EMAIL"). If you want to search on a specific property that + * has not been assigned a group, specify ".EMAIL". + * + * @param string $name + * + * @return array + */ + public function select($name) + { + $group = null; + $name = strtoupper($name); + if (false !== strpos($name, '.')) { + list($group, $name) = explode('.', $name, 2); + } + if ('' === $name) { + $name = null; + } + + if (!is_null($name)) { + $result = isset($this->children[$name]) ? $this->children[$name] : []; + + if (is_null($group)) { + return $result; + } else { + // If we have a group filter as well, we need to narrow it down + // more. + return array_filter( + $result, + function ($child) use ($group) { + return $child instanceof Property && 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 + */ + public function serialize() + { + $str = 'BEGIN:'.$this->name."\r\n"; + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accommodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * + * @return int + */ + $sortScore = function ($key, $array) { + if ($array[$key] instanceof Component) { + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ('VTIMEZONE' === $array[$key]->name) { + $score = 300000000; + + return $score + $key; + } else { + $score = 400000000; + + return $score + $key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ('VERSION' === $array[$key]->name) { + $score = 100000000; + + return $score + $key; + } else { + // All other properties + $score = 200000000; + + return $score + $key; + } + } + } + }; + + $children = $this->children(); + $tmp = $children; + uksort( + $children, + function ($a, $b) use ($sortScore, $tmp) { + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + } + ); + + foreach ($children as $child) { + $str .= $child->serialize(); + } + $str .= 'END:'.$this->name."\r\n"; + + return $str; + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array + */ + public function jsonSerialize() + { + $components = []; + $properties = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $components[] = $child->jsonSerialize(); + } else { + $properties[] = $child->jsonSerialize(); + } + } + } + + return [ + strtolower($this->name), + $properties, + $components, + ]; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer) + { + $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 + */ + public function __get($name) + { + if ('children' === $name) { + throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead'); + } + + $matches = $this->select($name); + if (0 === count($matches)) { + return; + } else { + $firstMatch = current($matches); + /* @var $firstMatch Property */ + $firstMatch->setIterator(new ElementList(array_values($matches))); + + return $firstMatch; + } + } + + /** + * This method checks if a sub-element with the specified name exists. + * + * @param string $name + * + * @return bool + */ + public function __isset($name) + { + $matches = $this->select($name); + + return count($matches) > 0; + } + + /** + * Using the setter method you can add properties or subcomponents. + * + * You can either pass a Component, Property + * object, or a string to automatically create a Property. + * + * If the item already exists, it will be removed. If you want to add + * a new item with the same name, always use the add() method. + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + $name = strtoupper($name); + $this->remove($name); + if ($value instanceof self || $value instanceof Property) { + $this->add($value); + } else { + $this->add($name, $value); + } + } + + /** + * Removes all properties and components within this component with the + * specified name. + * + * @param string $name + */ + public function __unset($name) + { + $this->remove($name); + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + */ + public function __clone() + { + foreach ($this->children as $childName => $childGroup) { + foreach ($childGroup as $key => $child) { + $clonedChild = clone $child; + $clonedChild->parent = $this; + $clonedChild->root = $this->root; + $this->children[$childName][$key] = $clonedChild; + } + } + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * It is also possible to specify defaults and severity levels for + * violating the rule. + * + * See the VEVENT implementation for getValidationRules for a more complex + * example. + * + * @var array + */ + public function getValidationRules() + { + return []; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $rules = $this->getValidationRules(); + $defaults = $this->getDefaults(); + + $propertyCounters = []; + + $messages = []; + + foreach ($this->children() as $child) { + $name = strtoupper($child->name); + if (!isset($propertyCounters[$name])) { + $propertyCounters[$name] = 1; + } else { + ++$propertyCounters[$name]; + } + $messages = array_merge($messages, $child->validate($options)); + } + + foreach ($rules as $propName => $rule) { + switch ($rule) { + case '0': + if (isset($propertyCounters[$propName])) { + $messages[] = [ + 'level' => 3, + 'message' => $propName.' MUST NOT appear in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '1': + if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) { + $repaired = false; + if ($options & self::REPAIR && isset($defaults[$propName])) { + $this->add($propName, $defaults[$propName]); + $repaired = true; + } + $messages[] = [ + 'level' => $repaired ? 1 : 3, + 'message' => $propName.' MUST appear exactly once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '+': + if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) { + $messages[] = [ + 'level' => 3, + 'message' => $propName.' MUST appear at least once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '*': + break; + case '?': + if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) { + $level = 3; + + // We try to repair the same property appearing multiple times with the exact same value + // by removing the duplicates and keeping only one property + if ($options & self::REPAIR) { + $properties = array_unique($this->select($propName), SORT_REGULAR); + + if (1 === count($properties)) { + $this->remove($propName); + $this->add($properties[0]); + + $level = 1; + } + } + + $messages[] = [ + 'level' => $level, + 'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + } + } + + return $messages; + } + + /** + * Call this method on a document if you're done using it. + * + * It's intended to remove all circular references, so PHP can easily clean + * it up. + */ + public function destroy() + { + parent::destroy(); + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + $child->destroy(); + } + } + $this->children = []; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/Available.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/Available.php new file mode 100644 index 0000000..5510b9e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/Available.php @@ -0,0 +1,123 @@ +<?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 + */ + public 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 + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTART' => 1, + 'DTSTAMP' => 1, + + 'DTEND' => '?', + 'DURATION' => '?', + + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'LAST-MODIFIED' => '?', + 'RECURRENCE-ID' => '?', + 'RRULE' => '?', + 'SUMMARY' => '?', + + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'RDATE' => '*', + + 'AVAILABLE' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + + if (isset($this->DTEND) && isset($this->DURATION)) { + $result[] = [ + 'level' => 3, + 'message' => 'DTEND and DURATION cannot both be present', + 'node' => $this, + ]; + } + + return $result; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VAlarm.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VAlarm.php new file mode 100644 index 0000000..bd00eb6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VAlarm.php @@ -0,0 +1,138 @@ +<?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 + */ + public function getEffectiveTriggerTime() + { + $trigger = $this->TRIGGER; + if (!isset($trigger['VALUE']) || 'DURATION' === strtoupper($trigger['VALUE'])) { + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); + $related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START'; + + $parentComponent = $this->parent; + if ('START' === $related) { + if ('VTODO' === $parentComponent->name) { + $propName = 'DUE'; + } else { + $propName = 'DTSTART'; + } + + $effectiveTrigger = $parentComponent->$propName->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } else { + if ('VTODO' === $parentComponent->name) { + $endProp = 'DUE'; + } elseif ('VEVENT' === $parentComponent->name) { + $endProp = 'DTEND'; + } else { + throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); + } + + if (isset($parentComponent->$endProp)) { + $effectiveTrigger = $parentComponent->$endProp->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } elseif (isset($parentComponent->DURATION)) { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); + $effectiveTrigger = $effectiveTrigger->add($duration); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } else { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } + } + } else { + $effectiveTrigger = $trigger->getDateTime(); + } + + return $effectiveTrigger; + } + + /** + * Returns true or false depending on if the event falls in the specified + * time-range. This is used for filtering purposes. + * + * The rules used to determine if an event falls within the specified + * time-range is based on the CalDAV specification. + * + * @param DateTime $start + * @param DateTime $end + * + * @return bool + */ + public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) + { + $effectiveTrigger = $this->getEffectiveTriggerTime(); + + if (isset($this->DURATION)) { + $duration = VObject\DateTimeParser::parseDuration($this->DURATION); + $repeat = (string) $this->REPEAT; + if (!$repeat) { + $repeat = 1; + } + + $period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat); + + foreach ($period as $occurrence) { + if ($start <= $occurrence && $end > $occurrence) { + return true; + } + } + + return false; + } else { + return $start <= $effectiveTrigger && $end > $effectiveTrigger; + } + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'ACTION' => 1, + 'TRIGGER' => 1, + + 'DURATION' => '?', + 'REPEAT' => '?', + + 'ATTACH' => '?', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VAvailability.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VAvailability.php new file mode 100644 index 0000000..04ec38d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VAvailability.php @@ -0,0 +1,149 @@ +<?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 + * + * @return bool + */ + public 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 + */ + public function getEffectiveStartEnd() + { + $effectiveStart = null; + $effectiveEnd = null; + + if (isset($this->DTSTART)) { + $effectiveStart = $this->DTSTART->getDateTime(); + } + if (isset($this->DTEND)) { + $effectiveEnd = $this->DTEND->getDateTime(); + } elseif ($effectiveStart && isset($this->DURATION)) { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } + + return [$effectiveStart, $effectiveEnd]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'BUSYTYPE' => '?', + 'CLASS' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'DTSTART' => '?', + 'LAST-MODIFIED' => '?', + 'ORGANIZER' => '?', + 'PRIORITY' => '?', + 'SEQUENCE' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + 'DTEND' => '?', + 'DURATION' => '?', + + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + + if (isset($this->DTEND) && isset($this->DURATION)) { + $result[] = [ + 'level' => 3, + 'message' => 'DTEND and DURATION cannot both be present', + 'node' => $this, + ]; + } + + return $result; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VCalendar.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VCalendar.php new file mode 100644 index 0000000..40e09a1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VCalendar.php @@ -0,0 +1,528 @@ +<?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 + */ + public static $defaultName = 'VCALENDAR'; + + /** + * This is a list of components, and which classes they should map to. + * + * @var array + */ + public static $componentMap = [ + 'VCALENDAR' => self::class, + 'VALARM' => VAlarm::class, + 'VEVENT' => VEvent::class, + 'VFREEBUSY' => VFreeBusy::class, + 'VAVAILABILITY' => VAvailability::class, + 'AVAILABLE' => Available::class, + 'VJOURNAL' => VJournal::class, + 'VTIMEZONE' => VTimeZone::class, + 'VTODO' => VTodo::class, + ]; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + public static $valueMap = [ + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class, + 'DATE' => VObject\Property\ICalendar\Date::class, + 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class, + 'DURATION' => VObject\Property\ICalendar\Duration::class, + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'PERIOD' => VObject\Property\ICalendar\Period::class, + 'RECUR' => VObject\Property\ICalendar\Recur::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + ]; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + public static $propertyMap = [ + // Calendar properties + 'CALSCALE' => VObject\Property\FlatText::class, + 'METHOD' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, + + // Component properties + 'ATTACH' => VObject\Property\Uri::class, + 'CATEGORIES' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, + 'COMMENT' => VObject\Property\FlatText::class, + 'DESCRIPTION' => VObject\Property\FlatText::class, + 'GEO' => VObject\Property\FloatValue::class, + 'LOCATION' => VObject\Property\FlatText::class, + 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class, + 'PRIORITY' => VObject\Property\IntegerValue::class, + 'RESOURCES' => VObject\Property\Text::class, + 'STATUS' => VObject\Property\FlatText::class, + 'SUMMARY' => VObject\Property\FlatText::class, + + // Date and Time Component Properties + 'COMPLETED' => VObject\Property\ICalendar\DateTime::class, + 'DTEND' => VObject\Property\ICalendar\DateTime::class, + 'DUE' => VObject\Property\ICalendar\DateTime::class, + 'DTSTART' => VObject\Property\ICalendar\DateTime::class, + 'DURATION' => VObject\Property\ICalendar\Duration::class, + 'FREEBUSY' => VObject\Property\ICalendar\Period::class, + 'TRANSP' => VObject\Property\FlatText::class, + + // Time Zone Component Properties + 'TZID' => VObject\Property\FlatText::class, + 'TZNAME' => VObject\Property\FlatText::class, + 'TZOFFSETFROM' => VObject\Property\UtcOffset::class, + 'TZOFFSETTO' => VObject\Property\UtcOffset::class, + 'TZURL' => VObject\Property\Uri::class, + + // Relationship Component Properties + 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class, + 'CONTACT' => VObject\Property\FlatText::class, + 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class, + 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class, + 'RELATED-TO' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, + + // Recurrence Component Properties + 'EXDATE' => VObject\Property\ICalendar\DateTime::class, + 'RDATE' => VObject\Property\ICalendar\DateTime::class, + 'RRULE' => VObject\Property\ICalendar\Recur::class, + 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545 + + // Alarm Component Properties + 'ACTION' => VObject\Property\FlatText::class, + 'REPEAT' => VObject\Property\IntegerValue::class, + 'TRIGGER' => VObject\Property\ICalendar\Duration::class, + + // Change Management Component Properties + 'CREATED' => VObject\Property\ICalendar\DateTime::class, + 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class, + 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class, + 'SEQUENCE' => VObject\Property\IntegerValue::class, + + // Request Status + 'REQUEST-STATUS' => VObject\Property\Text::class, + + // Additions from draft-daboo-valarm-extensions-04 + 'ALARM-AGENT' => VObject\Property\Text::class, + 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class, + 'PROXIMITY' => VObject\Property\Text::class, + 'DEFAULT-ALARM' => VObject\Property\Boolean::class, + + // Additions from draft-daboo-calendar-availability-05 + 'BUSYTYPE' => VObject\Property\Text::class, + ]; + + /** + * Returns the current document type. + * + * @return int + */ + public function getDocumentType() + { + return self::ICALENDAR20; + } + + /** + * Returns a list of all 'base components'. For instance, if an Event has + * a recurrence rule, and one instance is overridden, the overridden event + * will have the same UID, but will be excluded from this list. + * + * VTIMEZONE components will always be excluded. + * + * @param string $componentName filter by component name + * + * @return VObject\Component[] + */ + public function getBaseComponents($componentName = null) + { + $isBaseComponent = function ($component) { + if (!$component instanceof VObject\Component) { + return false; + } + if ('VTIMEZONE' === $component->name) { + return false; + } + if (isset($component->{'RECURRENCE-ID'})) { + return false; + } + + return true; + }; + + if ($componentName) { + // Early exit + return array_filter( + $this->select($componentName), + $isBaseComponent + ); + } + + $components = []; + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if (!$child instanceof Component) { + // If one child is not a component, they all are so we skip + // the entire group. + continue 2; + } + if ($isBaseComponent($child)) { + $components[] = $child; + } + } + } + + return $components; + } + + /** + * Returns the first component that is not a VTIMEZONE, and does not have + * an RECURRENCE-ID. + * + * If there is no such component, null will be returned. + * + * @param string $componentName filter by component name + * + * @return VObject\Component|null + */ + public function getBaseComponent($componentName = null) + { + $isBaseComponent = function ($component) { + if (!$component instanceof VObject\Component) { + return false; + } + if ('VTIMEZONE' === $component->name) { + return false; + } + if (isset($component->{'RECURRENCE-ID'})) { + return false; + } + + return true; + }; + + if ($componentName) { + foreach ($this->select($componentName) as $child) { + if ($isBaseComponent($child)) { + return $child; + } + } + + return null; + } + + // Searching all components + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($isBaseComponent($child)) { + return $child; + } + } + } + + return null; + } + + /** + * Expand all events in this VCalendar object and return a new VCalendar + * with the expanded events. + * + * If this calendar object, has events with recurrence rules, this method + * can be used to expand the event into multiple sub-events. + * + * Each event will be stripped from its recurrence information, and only + * the instances of the event in the specified timerange will be left + * alone. + * + * In addition, this method will cause timezone information to be stripped, + * and normalized to UTC. + * + * @param DateTimeZone $timeZone reference timezone for floating dates and + * times + * + * @return VCalendar + */ + public function expand(DateTimeInterface $start, DateTimeInterface $end, DateTimeZone $timeZone = null) + { + $newChildren = []; + $recurringEvents = []; + + if (!$timeZone) { + $timeZone = new DateTimeZone('UTC'); + } + + $stripTimezones = function (Component $component) use ($timeZone, &$stripTimezones) { + foreach ($component->children() as $componentChild) { + if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) { + $dt = $componentChild->getDateTimes($timeZone); + // We only need to update the first timezone, because + // setDateTimes will match all other timezones to the + // first. + $dt[0] = $dt[0]->setTimeZone(new DateTimeZone('UTC')); + $componentChild->setDateTimes($dt); + } elseif ($componentChild instanceof Component) { + $stripTimezones($componentChild); + } + } + + return $component; + }; + + foreach ($this->children() as $child) { + if ($child instanceof Property && 'PRODID' !== $child->name) { + // We explictly want to ignore PRODID, because we want to + // overwrite it with our own. + $newChildren[] = clone $child; + } elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) { + // We're also stripping all VTIMEZONE objects because we're + // converting everything to UTC. + if ('VEVENT' === $child->name && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) { + // Handle these a bit later. + $uid = (string) $child->UID; + if (!$uid) { + throw new InvalidDataException('Every VEVENT object must have a UID property'); + } + if (isset($recurringEvents[$uid])) { + $recurringEvents[$uid][] = clone $child; + } else { + $recurringEvents[$uid] = [clone $child]; + } + } elseif ('VEVENT' === $child->name && $child->isInTimeRange($start, $end)) { + $newChildren[] = $stripTimezones(clone $child); + } + } + } + + foreach ($recurringEvents as $events) { + try { + $it = new EventIterator($events, null, $timeZone); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + continue; + } + $it->fastForward($start); + + while ($it->valid() && $it->getDTStart() < $end) { + if ($it->getDTEnd() > $start) { + $newChildren[] = $stripTimezones($it->getEventObject()); + } + $it->next(); + } + } + + return new self($newChildren); + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'VERSION' => '2.0', + 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', + 'CALSCALE' => 'GREGORIAN', + ]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'PRODID' => 1, + 'VERSION' => 1, + + 'CALSCALE' => '?', + 'METHOD' => '?', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = parent::validate($options); + + if ($ver = $this->VERSION) { + if ('2.0' !== (string) $ver) { + $warnings[] = [ + 'level' => 3, + 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', + 'node' => $this, + ]; + } + } + + $uidList = []; + $componentsFound = 0; + $componentTypes = []; + + foreach ($this->children() as $child) { + if ($child instanceof Component) { + ++$componentsFound; + + if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) { + continue; + } + $componentTypes[] = $child->name; + + $uid = (string) $child->UID; + $isMaster = isset($child->{'RECURRENCE-ID'}) ? 0 : 1; + if (isset($uidList[$uid])) { + ++$uidList[$uid]['count']; + if ($isMaster && $uidList[$uid]['hasMaster']) { + $warnings[] = [ + 'level' => 3, + 'message' => 'More than one master object was found for the object with UID '.$uid, + 'node' => $this, + ]; + } + $uidList[$uid]['hasMaster'] += $isMaster; + } else { + $uidList[$uid] = [ + 'count' => 1, + 'hasMaster' => $isMaster, + ]; + } + } + } + + if (0 === $componentsFound) { + $warnings[] = [ + 'level' => 3, + 'message' => 'An iCalendar object must have at least 1 component.', + 'node' => $this, + ]; + } + + if ($options & self::PROFILE_CALDAV) { + if (count($uidList) > 1) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server may only have components with the same UID.', + 'node' => $this, + ]; + } + if (0 === count($componentTypes)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).', + 'node' => $this, + ]; + } + if (count(array_unique($componentTypes)) > 1) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).', + 'node' => $this, + ]; + } + + if (isset($this->METHOD)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.', + 'node' => $this, + ]; + } + } + + return $warnings; + } + + /** + * Returns all components with a specific UID value. + * + * @return array + */ + public function getByUID($uid) + { + return array_filter($this->getComponents(), function ($item) use ($uid) { + if (!$itemUid = $item->select('UID')) { + return false; + } + $itemUid = current($itemUid)->getValue(); + + return $uid === $itemUid; + }); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VCard.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VCard.php new file mode 100644 index 0000000..5132194 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VCard.php @@ -0,0 +1,535 @@ +<?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 + */ + public 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 + */ + public static $componentMap = [ + 'VCARD' => VCard::class, + ]; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + public static $valueMap = [ + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only + 'DATE' => VObject\Property\VCard\Date::class, + 'DATE-TIME' => VObject\Property\VCard\DateTime::class, + 'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class, + 'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only + 'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'URL' => VObject\Property\Uri::class, // vCard 2.1 only + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + ]; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + public static $propertyMap = [ + // vCard 2.1 properties and up + 'N' => VObject\Property\Text::class, + 'FN' => VObject\Property\FlatText::class, + 'PHOTO' => VObject\Property\Binary::class, + 'BDAY' => VObject\Property\VCard\DateAndOrTime::class, + 'ADR' => VObject\Property\Text::class, + 'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'TEL' => VObject\Property\FlatText::class, + 'EMAIL' => VObject\Property\FlatText::class, + 'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'GEO' => VObject\Property\FlatText::class, + 'TITLE' => VObject\Property\FlatText::class, + 'ROLE' => VObject\Property\FlatText::class, + 'LOGO' => VObject\Property\Binary::class, + // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so + // not supported at the moment + 'ORG' => VObject\Property\Text::class, + 'NOTE' => VObject\Property\FlatText::class, + 'REV' => VObject\Property\VCard\TimeStamp::class, + 'SOUND' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, + 'KEY' => VObject\Property\FlatText::class, + 'TZ' => VObject\Property\Text::class, + + // vCard 3.0 properties + 'CATEGORIES' => VObject\Property\Text::class, + 'SORT-STRING' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'NICKNAME' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + + // rfc2739 properties + 'FBURL' => VObject\Property\Uri::class, + 'CAPURI' => VObject\Property\Uri::class, + 'CALURI' => VObject\Property\Uri::class, + 'CALADRURI' => VObject\Property\Uri::class, + + // rfc4770 properties + 'IMPP' => VObject\Property\Uri::class, + + // vCard 4.0 properties + 'SOURCE' => VObject\Property\Uri::class, + 'XML' => VObject\Property\FlatText::class, + 'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class, + 'CLIENTPIDMAP' => VObject\Property\Text::class, + 'LANG' => VObject\Property\VCard\LanguageTag::class, + 'GENDER' => VObject\Property\Text::class, + 'KIND' => VObject\Property\FlatText::class, + 'MEMBER' => VObject\Property\Uri::class, + 'RELATED' => VObject\Property\Uri::class, + + // rfc6474 properties + 'BIRTHPLACE' => VObject\Property\FlatText::class, + 'DEATHPLACE' => VObject\Property\FlatText::class, + 'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class, + + // rfc6715 properties + 'EXPERTISE' => VObject\Property\FlatText::class, + 'HOBBY' => VObject\Property\FlatText::class, + 'INTEREST' => VObject\Property\FlatText::class, + 'ORG-DIRECTORY' => VObject\Property\FlatText::class, + ]; + + /** + * Returns the current document type. + * + * @return int + */ + public function getDocumentType() + { + if (!$this->version) { + $version = (string) $this->VERSION; + + switch ($version) { + case '2.1': + $this->version = self::VCARD21; + break; + case '3.0': + $this->version = self::VCARD30; + break; + case '4.0': + $this->version = self::VCARD40; + break; + default: + // We don't want to cache the version if it's unknown, + // because we might get a version property in a bit. + return self::UNKNOWN; + } + } + + return $this->version; + } + + /** + * Converts the document to a different vcard version. + * + * Use one of the VCARD constants for the target. This method will return + * a copy of the vcard in the new version. + * + * At the moment the only supported conversion is from 3.0 to 4.0. + * + * If input and output version are identical, a clone is returned. + * + * @param int $target + * + * @return VCard + */ + public function convert($target) + { + $converter = new VObject\VCardConverter(); + + return $converter->convert($this, $target); + } + + /** + * VCards with version 2.1, 3.0 and 4.0 are found. + * + * If the VCARD doesn't know its version, 2.1 is assumed. + */ + const DEFAULT_VERSION = self::VCARD21; + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = []; + + $versionMap = [ + self::VCARD21 => '2.1', + self::VCARD30 => '3.0', + self::VCARD40 => '4.0', + ]; + + $version = $this->select('VERSION'); + if (1 === count($version)) { + $version = (string) $this->VERSION; + if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) { + $warnings[] = [ + 'level' => 3, + 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', + 'node' => $this, + ]; + if ($options & self::REPAIR) { + $this->VERSION = $versionMap[self::DEFAULT_VERSION]; + } + } + if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'CardDAV servers are not allowed to accept vCard 2.1.', + 'node' => $this, + ]; + } + } + $uid = $this->select('UID'); + if (0 === count($uid)) { + if ($options & self::PROFILE_CARDDAV) { + // Required for CardDAV + $warningLevel = 3; + $message = 'vCards on CardDAV servers MUST have a UID property.'; + } else { + // Not required for regular vcards + $warningLevel = 2; + $message = 'Adding a UID to a vCard property is recommended.'; + } + if ($options & self::REPAIR) { + $this->UID = VObject\UUIDUtil::getUUID(); + $warningLevel = 1; + } + $warnings[] = [ + 'level' => $warningLevel, + 'message' => $message, + 'node' => $this, + ]; + } + + $fn = $this->select('FN'); + if (1 !== count($fn)) { + $repaired = false; + if (($options & self::REPAIR) && 0 === count($fn)) { + // We're going to try to see if we can use the contents of the + // N property. + if (isset($this->N)) { + $value = explode(';', (string) $this->N); + if (isset($value[1]) && $value[1]) { + $this->FN = $value[1].' '.$value[0]; + } else { + $this->FN = $value[0]; + } + $repaired = true; + + // Otherwise, the ORG property may work + } elseif (isset($this->ORG)) { + $this->FN = (string) $this->ORG; + $repaired = true; + + // Otherwise, the EMAIL property may work + } elseif (isset($this->EMAIL)) { + $this->FN = (string) $this->EMAIL; + $repaired = true; + } + } + $warnings[] = [ + 'level' => $repaired ? 1 : 3, + 'message' => 'The FN property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ]; + } + + return array_merge( + parent::validate($options), + $warnings + ); + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'ADR' => '*', + 'ANNIVERSARY' => '?', + 'BDAY' => '?', + 'CALADRURI' => '*', + 'CALURI' => '*', + 'CATEGORIES' => '*', + 'CLIENTPIDMAP' => '*', + 'EMAIL' => '*', + 'FBURL' => '*', + 'IMPP' => '*', + 'GENDER' => '?', + 'GEO' => '*', + 'KEY' => '*', + 'KIND' => '?', + 'LANG' => '*', + 'LOGO' => '*', + 'MEMBER' => '*', + 'N' => '?', + 'NICKNAME' => '*', + 'NOTE' => '*', + 'ORG' => '*', + 'PHOTO' => '*', + 'PRODID' => '?', + 'RELATED' => '*', + 'REV' => '?', + 'ROLE' => '*', + 'SOUND' => '*', + 'SOURCE' => '*', + 'TEL' => '*', + 'TITLE' => '*', + 'TZ' => '*', + 'URL' => '*', + 'VERSION' => '1', + 'XML' => '*', + + // FN is commented out, because it's already handled by the + // validate function, which may also try to repair it. + // 'FN' => '+', + 'UID' => '?', + ]; + } + + /** + * Returns a preferred field. + * + * VCards can indicate 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 + */ + public function preferred($propertyName) + { + $preferred = null; + $lastPref = 101; + foreach ($this->select($propertyName) as $field) { + $pref = 101; + if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) { + $pref = 1; + } elseif (isset($field['PREF'])) { + $pref = $field['PREF']->getValue(); + } + + if ($pref < $lastPref || is_null($preferred)) { + $preferred = $field; + $lastPref = $pref; + } + } + + return $preferred; + } + + /** + * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL). + * + * This function will return null if the property does not exist. If there are + * multiple properties with the same TYPE value, only one will be returned. + * + * @param string $propertyName + * @param string $type + * + * @return VObject\Property|null + */ + public function getByType($propertyName, $type) + { + foreach ($this->select($propertyName) as $field) { + if (isset($field['TYPE']) && $field['TYPE']->has($type)) { + return $field; + } + } + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'VERSION' => '4.0', + 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + ]; + } + + /** + * This method returns an array, with the representation as it should be + * encoded in json. This is used to create jCard or jCal documents. + * + * @return array + */ + public function jsonSerialize() + { + // A vcard does not have sub-components, so we're overriding this + // method to remove that array element. + $properties = []; + + foreach ($this->children() as $child) { + $properties[] = $child->jsonSerialize(); + } + + return [ + strtolower($this->name), + $properties, + ]; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer) + { + $propertiesByGroup = []; + + foreach ($this->children() as $property) { + $group = $property->group; + + if (!isset($propertiesByGroup[$group])) { + $propertiesByGroup[$group] = []; + } + + $propertiesByGroup[$group][] = $property; + } + + $writer->startElement(strtolower($this->name)); + + foreach ($propertiesByGroup as $group => $properties) { + if (!empty($group)) { + $writer->startElement('group'); + $writer->writeAttribute('name', strtolower($group)); + } + + foreach ($properties as $property) { + switch ($property->name) { + case 'VERSION': + break; + + case 'XML': + $value = $property->getParts(); + $fragment = new Xml\Element\XmlFragment($value[0]); + $writer->write($fragment); + break; + + default: + $property->xmlSerialize($writer); + break; + } + } + + if (!empty($group)) { + $writer->endElement(); + } + } + + $writer->endElement(); + } + + /** + * Returns the default class for a property name. + * + * @param string $propertyName + * + * @return string + */ + public function getClassNameForPropertyName($propertyName) + { + $className = parent::getClassNameForPropertyName($propertyName); + + // In vCard 4, BINARY no longer exists, and we need URI instead. + if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) { + return VObject\Property\Uri::class; + } + + return $className; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VEvent.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VEvent.php new file mode 100644 index 0000000..6ea93ed --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VEvent.php @@ -0,0 +1,140 @@ +<?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. + * + * @return bool + */ + public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) + { + if ($this->RRULE) { + try { + $it = new EventIterator($this, null, $start->getTimezone()); + } catch (NoInstancesException $e) { + // If we've caught this exception, there are no instances + // for the event that fall into the specified time-range. + return false; + } + + $it->fastForward($start); + + // We fast-forwarded to a spot where the end-time of the + // recurrence instance exceeded the start of the requested + // time-range. + // + // If the starttime of the recurrence did not exceed the + // end of the time range as well, we have a match. + return $it->getDTStart() < $end && $it->getDTEnd() > $start; + } + + $effectiveStart = $this->DTSTART->getDateTime($start->getTimezone()); + if (isset($this->DTEND)) { + // The DTEND property is considered non inclusive. So for a 3 day + // event in july, dtstart and dtend would have to be July 1st and + // July 4th respectively. + // + // See: + // http://tools.ietf.org/html/rfc5545#page-54 + $effectiveEnd = $this->DTEND->getDateTime($end->getTimezone()); + } elseif (isset($this->DURATION)) { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } elseif (!$this->DTSTART->hasTime()) { + $effectiveEnd = $effectiveStart->modify('+1 day'); + } else { + $effectiveEnd = $effectiveStart; + } + + return + ($start < $effectiveEnd) && ($end > $effectiveStart) + ; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + $hasMethod = isset($this->parent->METHOD); + + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + 'DTSTART' => $hasMethod ? '?' : '1', + 'CLASS' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'GEO' => '?', + 'LAST-MODIFIED' => '?', + 'LOCATION' => '?', + 'ORGANIZER' => '?', + 'PRIORITY' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'TRANSP' => '?', + 'URL' => '?', + 'RECURRENCE-ID' => '?', + 'RRULE' => '?', + 'DTEND' => '?', + 'DURATION' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'REQUEST-STATUS' => '*', + 'RELATED-TO' => '*', + 'RESOURCES' => '*', + 'RDATE' => '*', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VFreeBusy.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VFreeBusy.php new file mode 100644 index 0000000..fef418b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VFreeBusy.php @@ -0,0 +1,93 @@ +<?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. + * + * @return bool + */ + public 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']) && 'BUSY' !== strtoupper(substr((string) $freebusy['FBTYPE'], 0, 4))) { + continue; + } + + // The freebusy component can hold more than 1 value, separated by + // commas. + $periods = explode(',', (string) $freebusy); + + foreach ($periods as $period) { + // Every period is formatted as [start]/[end]. The start is an + // absolute UTC time, the end may be an absolute UTC time, or + // duration (relative) value. + list($busyStart, $busyEnd) = explode('/', $period); + + $busyStart = VObject\DateTimeParser::parse($busyStart); + $busyEnd = VObject\DateTimeParser::parse($busyEnd); + if ($busyEnd instanceof \DateInterval) { + $busyEnd = $busyStart->add($busyEnd); + } + + if ($start < $busyEnd && $end > $busyStart) { + return false; + } + } + } + + return true; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CONTACT' => '?', + 'DTSTART' => '?', + 'DTEND' => '?', + 'ORGANIZER' => '?', + 'URL' => '?', + + 'ATTENDEE' => '*', + 'COMMENT' => '*', + 'FREEBUSY' => '*', + 'REQUEST-STATUS' => '*', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VJournal.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VJournal.php new file mode 100644 index 0000000..9b7f1b8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VJournal.php @@ -0,0 +1,101 @@ +<?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. + * + * @return bool + */ + public 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 + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CLASS' => '?', + 'CREATED' => '?', + 'DTSTART' => '?', + 'LAST-MODIFIED' => '?', + 'ORGANIZER' => '?', + 'RECURRENCE-ID' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + + 'RRULE' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'DESCRIPTION' => '*', + 'EXDATE' => '*', + 'RELATED-TO' => '*', + 'RDATE' => '*', + ]; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VTimeZone.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VTimeZone.php new file mode 100644 index 0000000..21c0623 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VTimeZone.php @@ -0,0 +1,63 @@ +<?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 + */ + public 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 + */ + public function getValidationRules() + { + return [ + 'TZID' => 1, + + 'LAST-MODIFIED' => '?', + 'TZURL' => '?', + + // At least 1 STANDARD or DAYLIGHT must appear. + // + // The validator is not specific yet to pick this up, so these + // rules are too loose. + 'STANDARD' => '*', + 'DAYLIGHT' => '*', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VTodo.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VTodo.php new file mode 100644 index 0000000..6f022ba --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Component/VTodo.php @@ -0,0 +1,181 @@ +<?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. + * + * @return bool + */ + public 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 + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CLASS' => '?', + 'COMPLETED' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'DTSTART' => '?', + 'GEO' => '?', + 'LAST-MODIFIED' => '?', + 'LOCATION' => '?', + 'ORGANIZER' => '?', + 'PERCENT' => '?', + 'PRIORITY' => '?', + 'RECURRENCE-ID' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + + 'RRULE' => '?', + 'DUE' => '?', + 'DURATION' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'REQUEST-STATUS' => '*', + 'RELATED-TO' => '*', + 'RESOURCES' => '*', + 'RDATE' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + if (isset($this->DUE) && isset($this->DTSTART)) { + $due = $this->DUE; + $dtStart = $this->DTSTART; + + if ($due->getValueType() !== $dtStart->getValueType()) { + $result[] = [ + 'level' => 3, + 'message' => 'The value type (DATE or DATE-TIME) must be identical for DUE and DTSTART', + 'node' => $due, + ]; + } elseif ($due->getDateTime() < $dtStart->getDateTime()) { + $result[] = [ + 'level' => 3, + 'message' => 'DUE must occur after DTSTART', + 'node' => $due, + ]; + } + } + + return $result; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => date('Ymd\\THis\\Z'), + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/DateTimeParser.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/DateTimeParser.php new file mode 100644 index 0000000..1c25343 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/DateTimeParser.php @@ -0,0 +1,560 @@ +<?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 + */ + public 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 ('Z' === $matches[7] || 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 + */ + public 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 + */ + public 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 ('P' === $duration) { + $duration = 'PT0S'; + } + + $iv = new DateInterval($duration); + + if ($invert) { + $iv->invert = true; + } + + return $iv; + } + + $parts = [ + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; + + $newDur = ''; + + foreach ($parts as $part) { + if (isset($matches[$part]) && $matches[$part]) { + $newDur .= ' '.$matches[$part].' '.$part.'s'; + } + } + + $newDur = ('-' === $matches['plusminus'] ? '-' : '+').trim($newDur); + + if ('+' === $newDur) { + $newDur = '+0 seconds'; + } + + return $newDur; + } + + /** + * Parses either a Date or DateTime, or Duration value. + * + * @param string $date + * @param DateTimeZone|string $referenceTz + * + * @return DateTimeImmutable|DateInterval + */ + public static function parse($date, $referenceTz = null) + { + if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) { + return self::parseDuration($date); + } elseif (8 === strlen($date)) { + return self::parseDate($date, $referenceTz); + } else { + return self::parseDateTime($date, $referenceTz); + } + } + + /** + * This method parses a vCard date and or time value. + * + * This can be used for the DATE, DATE-TIME, TIMESTAMP and + * DATE-AND-OR-TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * year, month, date, hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the year, etc. + * + * Timezone is either returned as 'Z' or as '+0800' + * + * For any non-specified values null is returned. + * + * List of date formats that are supported: + * YYYY + * YYYY-MM + * YYYYMMDD + * --MMDD + * ---DD + * + * YYYY-MM-DD + * --MM-DD + * ---DD + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format date-time string looks like : + * 20130603T133901 + * + * A full extended-format date-time string looks like : + * 2013-06-03T13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +1100. + * + * @param string $date + * + * @return array + */ + public static function parseVCardDateTime($date) + { + $regex = '/^ + (?: # date part + (?: + (?: (?<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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/lib/Document.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Document.php new file mode 100644 index 0000000..14a77c9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Document.php @@ -0,0 +1,264 @@ +<?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 + */ + public static $defaultName; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + public static $propertyMap = []; + + /** + * List of components, along with which classes they map to. + * + * @var array + */ + public static $componentMap = []; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + public 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) + */ + public function __construct() + { + $args = func_get_args(); + $name = static::$defaultName; + if (0 === count($args) || is_array($args[0])) { + $children = isset($args[0]) ? $args[0] : []; + $defaults = isset($args[1]) ? $args[1] : true; + } else { + $name = $args[0]; + $children = isset($args[1]) ? $args[1] : []; + $defaults = isset($args[2]) ? $args[2] : true; + } + parent::__construct($this, $name, $children, $defaults); + } + + /** + * Returns the current document type. + * + * @return int + */ + public 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 + */ + public 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 + */ + public function createComponent($name, array $children = null, $defaults = true) + { + $name = strtoupper($name); + $class = Component::class; + + if (isset(static::$componentMap[$name])) { + $class = static::$componentMap[$name]; + } + if (is_null($children)) { + $children = []; + } + + return new $class($this, $name, $children, $defaults); + } + + /** + * Factory method for creating new properties. + * + * This method automatically searches for the correct property class, based + * on its name. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param string $name + * @param mixed $value + * @param array $parameters + * @param string $valueType Force a specific valuetype, such as URI or TEXT + * + * @return Property + */ + public 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 (false !== ($i = strpos($name, '.'))) { + $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 + */ + public function getClassNameForPropertyValue($valueParam) + { + $valueParam = strtoupper($valueParam); + if (isset(static::$valueMap[$valueParam])) { + return static::$valueMap[$valueParam]; + } + } + + /** + * Returns the default class for a property name. + * + * @param string $propertyName + * + * @return string + */ + public function getClassNameForPropertyName($propertyName) + { + if (isset(static::$propertyMap[$propertyName])) { + return static::$propertyMap[$propertyName]; + } else { + return Property\Unknown::class; + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/ElementList.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/ElementList.php new file mode 100644 index 0000000..56058cb --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/ElementList.php @@ -0,0 +1,46 @@ +<?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 + */ + public function offsetSet($offset, $value) + { + throw new LogicException('You can not add new objects to an ElementList'); + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + public function offsetUnset($offset) + { + throw new LogicException('You can not remove objects from an ElementList'); + } + + /* }}} */ +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/EofException.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/EofException.php new file mode 100644 index 0000000..837af7e --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/lib/FreeBusyData.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/FreeBusyData.php new file mode 100644 index 0000000..d05dfc7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/FreeBusyData.php @@ -0,0 +1,185 @@ +<?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; + + public 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 + */ + public function add($start, $end, $type) + { + if ($start > $this->end || $end < $this->start) { + // This new data is outside our timerange. + return; + } + + if ($start < $this->start) { + // The item starts before our requested time range + $start = $this->start; + } + if ($end > $this->end) { + // The item ends after our requested time range + $end = $this->end; + } + + // Finding out where we need to insert the new item. + $currentIndex = 0; + while ($start > $this->data[$currentIndex]['end']) { + ++$currentIndex; + } + + // The standard insertion point will be one _after_ the first + // overlapping item. + $insertStartIndex = $currentIndex + 1; + + $newItem = [ + 'start' => $start, + 'end' => $end, + 'type' => $type, + ]; + + $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 (-1 === $itemsToDelete) { + $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] + ); + } + } + + public function getData() + { + return $this->data; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/FreeBusyGenerator.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/FreeBusyGenerator.php new file mode 100644 index 0000000..a1c2404 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/FreeBusyGenerator.php @@ -0,0 +1,550 @@ +<?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, its 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 + */ + public 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. + */ + public function setBaseObject(Document $vcalendar) + { + $this->baseObject = $vcalendar; + } + + /** + * Sets a VAVAILABILITY document. + */ + public function setVAvailability(Document $vcalendar) + { + $this->vavailability = $vcalendar; + } + + /** + * Sets the input objects. + * + * You must either specify a valendar object as a string, or as the parse + * Component. + * It's also possible to specify multiple objects as an array. + * + * @param mixed $objects + */ + public function setObjects($objects) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + + $this->objects = []; + foreach ($objects as $object) { + if (is_string($object) || is_resource($object)) { + $this->objects[] = Reader::read($object); + } elseif ($object instanceof Component) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects'); + } + } + } + + /** + * Sets the time range. + * + * Any freebusy object falling outside of this time range will be ignored. + * + * @param DateTimeInterface $start + * @param DateTimeInterface $end + */ + public function setTimeRange(DateTimeInterface $start = null, DateTimeInterface $end = null) + { + if (!$start) { + $start = new DateTimeImmutable(Settings::$minDate); + } + if (!$end) { + $end = new DateTimeImmutable(Settings::$maxDate); + } + $this->start = $start; + $this->end = $end; + } + + /** + * Sets the reference timezone for floating times. + */ + public function setTimeZone(DateTimeZone $timeZone) + { + $this->timeZone = $timeZone; + } + + /** + * Parses the input data and returns a correct VFREEBUSY object, wrapped in + * a VCALENDAR. + * + * @return Component + */ + public function getResult() + { + $fbData = new FreeBusyData( + $this->start->getTimeStamp(), + $this->end->getTimeStamp() + ); + if ($this->vavailability) { + $this->calculateAvailability($fbData, $this->vavailability); + } + + $this->calculateBusy($fbData, $this->objects); + + return $this->generateFreeBusyCalendar($fbData); + } + + /** + * This method takes a VAVAILABILITY component and figures out all the + * available times. + */ + protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability) + { + $vavailComps = iterator_to_array($vavailability->VAVAILABILITY); + usort( + $vavailComps, + function ($a, $b) { + // We need to order the components by priority. Priority 1 + // comes first, up until priority 9. Priority 0 comes after + // priority 9. No priority implies priority 0. + // + // Yes, I'm serious. + $priorityA = isset($a->PRIORITY) ? (int) $a->PRIORITY->getValue() : 0; + $priorityB = isset($b->PRIORITY) ? (int) $b->PRIORITY->getValue() : 0; + + if (0 === $priorityA) { + $priorityA = 10; + } + if (0 === $priorityB) { + $priorityB = 10; + } + + return $priorityA - $priorityB; + } + ); + + // Now we go over all the VAVAILABILITY components and figure if + // there's any we don't need to consider. + // + // This is can be because of one of two reasons: either the + // VAVAILABILITY component falls outside the time we are interested in, + // or a different VAVAILABILITY component with a higher priority has + // already completely covered the time-range. + $old = $vavailComps; + $new = []; + + foreach ($old as $vavail) { + list($compStart, $compEnd) = $vavail->getEffectiveStartEnd(); + + // We don't care about datetimes that are earlier or later than the + // start and end of the freebusy report, so this gets normalized + // first. + if (is_null($compStart) || $compStart < $this->start) { + $compStart = $this->start; + } + if (is_null($compEnd) || $compEnd > $this->end) { + $compEnd = $this->end; + } + + // If the item fell out of the timerange, we can just skip it. + if ($compStart > $this->end || $compEnd < $this->start) { + continue; + } + + // Going through our existing list of components to see if there's + // a higher priority component that already fully covers this one. + foreach ($new as $higherVavail) { + list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd(); + if ( + (is_null($higherStart) || $higherStart < $compStart) && + (is_null($higherEnd) || $higherEnd > $compEnd) + ) { + // Component is fully covered by a higher priority + // component. We can skip this component. + continue 2; + } + } + + // We're keeping it! + $new[] = $vavail; + } + + // Lastly, we need to traverse the remaining components and fill in the + // freebusydata slots. + // + // We traverse the components in reverse, because we want the higher + // priority components to override the lower ones. + foreach (array_reverse($new) as $vavail) { + $busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE'; + list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd(); + + // Making the component size no larger than the requested free-busy + // report range. + if (!$vavailStart || $vavailStart < $this->start) { + $vavailStart = $this->start; + } + if (!$vavailEnd || $vavailEnd > $this->end) { + $vavailEnd = $this->end; + } + + // Marking the entire time range of the VAVAILABILITY component as + // busy. + $fbData->add( + $vavailStart->getTimeStamp(), + $vavailEnd->getTimeStamp(), + $busyType + ); + + // Looping over the AVAILABLE components. + if (isset($vavail->AVAILABLE)) { + foreach ($vavail->AVAILABLE as $available) { + list($availStart, $availEnd) = $available->getEffectiveStartEnd(); + $fbData->add( + $availStart->getTimeStamp(), + $availEnd->getTimeStamp(), + 'FREE' + ); + + if ($available->RRULE) { + // Our favourite thing: recurrence!! + + $rruleIterator = new Recur\RRuleIterator( + $available->RRULE->getValue(), + $availStart + ); + $rruleIterator->fastForward($vavailStart); + + $startEndDiff = $availStart->diff($availEnd); + + while ($rruleIterator->valid()) { + $recurStart = $rruleIterator->current(); + $recurEnd = $recurStart->add($startEndDiff); + + if ($recurStart > $vavailEnd) { + // We're beyond the legal timerange. + break; + } + + if ($recurEnd > $vavailEnd) { + // Truncating the end if it exceeds the + // VAVAILABILITY end. + $recurEnd = $vavailEnd; + } + + $fbData->add( + $recurStart->getTimeStamp(), + $recurEnd->getTimeStamp(), + 'FREE' + ); + + $rruleIterator->next(); + } + } + } + } + } + } + + /** + * This method takes an array of iCalendar objects and applies its busy + * times on fbData. + * + * @param VCalendar[] $objects + */ + protected function calculateBusy(FreeBusyData $fbData, array $objects) + { + foreach ($objects as $key => $object) { + foreach ($object->getBaseComponents() as $component) { + switch ($component->name) { + case 'VEVENT': + + $FBTYPE = 'BUSY'; + if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) { + break; + } + if (isset($component->STATUS)) { + $status = strtoupper($component->STATUS); + if ('CANCELLED' === $status) { + break; + } + if ('TENTATIVE' === $status) { + $FBTYPE = 'BUSY-TENTATIVE'; + } + } + + $times = []; + + if ($component->RRULE) { + try { + $iterator = new EventIterator($object, (string) $component->UID, $this->timeZone); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + unset($this->objects[$key]); + break; + } + + if ($this->start) { + $iterator->fastForward($this->start); + } + + $maxRecurrences = Settings::$maxRecurrences; + + while ($iterator->valid() && --$maxRecurrences) { + $startTime = $iterator->getDTStart(); + if ($this->end && $startTime > $this->end) { + break; + } + $times[] = [ + $iterator->getDTStart(), + $iterator->getDTEnd(), + ]; + + $iterator->next(); + } + } else { + $startTime = $component->DTSTART->getDateTime($this->timeZone); + if ($this->end && $startTime > $this->end) { + break; + } + $endTime = null; + if (isset($component->DTEND)) { + $endTime = $component->DTEND->getDateTime($this->timeZone); + } elseif (isset($component->DURATION)) { + $duration = DateTimeParser::parseDuration((string) $component->DURATION); + $endTime = clone $startTime; + $endTime = $endTime->add($duration); + } elseif (!$component->DTSTART->hasTime()) { + $endTime = clone $startTime; + $endTime = $endTime->modify('+1 day'); + } else { + // The event had no duration (0 seconds) + break; + } + + $times[] = [$startTime, $endTime]; + } + + foreach ($times as $time) { + if ($this->end && $time[0] > $this->end) { + break; + } + if ($this->start && $time[1] < $this->start) { + break; + } + + $fbData->add( + $time[0]->getTimeStamp(), + $time[1]->getTimeStamp(), + $FBTYPE + ); + } + break; + + case 'VFREEBUSY': + foreach ($component->FREEBUSY as $freebusy) { + $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY'; + + // Skipping intervals marked as 'free' + if ('FREE' === $fbType) { + continue; + } + + $values = explode(',', $freebusy); + foreach ($values as $value) { + list($startTime, $endTime) = explode('/', $value); + $startTime = DateTimeParser::parseDateTime($startTime); + + if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) { + $duration = DateTimeParser::parseDuration($endTime); + $endTime = clone $startTime; + $endTime = $endTime->add($duration); + } else { + $endTime = DateTimeParser::parseDateTime($endTime); + } + + if ($this->start && $this->start > $endTime) { + continue; + } + if ($this->end && $this->end < $startTime) { + continue; + } + $fbData->add( + $startTime->getTimeStamp(), + $endTime->getTimeStamp(), + $fbType + ); + } + } + break; + } + } + } + } + + /** + * This method takes a FreeBusyData object and generates the VCALENDAR + * object associated with it. + * + * @return VCalendar + */ + protected function generateFreeBusyCalendar(FreeBusyData $fbData) + { + if ($this->baseObject) { + $calendar = $this->baseObject; + } else { + $calendar = new VCalendar(); + } + + $vfreebusy = $calendar->createComponent('VFREEBUSY'); + $calendar->add($vfreebusy); + + if ($this->start) { + $dtstart = $calendar->createProperty('DTSTART'); + $dtstart->setDateTime($this->start); + $vfreebusy->add($dtstart); + } + if ($this->end) { + $dtend = $calendar->createProperty('DTEND'); + $dtend->setDateTime($this->end); + $vfreebusy->add($dtend); + } + + $tz = new \DateTimeZone('UTC'); + $dtstamp = $calendar->createProperty('DTSTAMP'); + $dtstamp->setDateTime(new DateTimeImmutable('now', $tz)); + $vfreebusy->add($dtstamp); + + foreach ($fbData->getData() as $busyTime) { + $busyType = strtoupper($busyTime['type']); + + // Ignoring all the FREE parts, because those are already assumed. + if ('FREE' === $busyType) { + continue; + } + + $busyTime[0] = new \DateTimeImmutable('@'.$busyTime['start'], $tz); + $busyTime[1] = new \DateTimeImmutable('@'.$busyTime['end'], $tz); + + $prop = $calendar->createProperty( + 'FREEBUSY', + $busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z') + ); + + // Only setting FBTYPE if it's not BUSY, because BUSY is the + // default anyway. + if ('BUSY' !== $busyType) { + $prop['FBTYPE'] = $busyType; + } + $vfreebusy->add($prop); + } + + return $calendar; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/Broker.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/Broker.php new file mode 100644 index 0000000..c09cdf3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/Broker.php @@ -0,0 +1,964 @@ +<?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 VCalendar $existingObject + * + * @return VCalendar|null + */ + public function processMessage(Message $itipMessage, VCalendar $existingObject = null) + { + // We only support events at the moment. + if ('VEVENT' !== $itipMessage->component) { + return false; + } + + switch ($itipMessage->method) { + case 'REQUEST': + return $this->processMessageRequest($itipMessage, $existingObject); + + case 'CANCEL': + return $this->processMessageCancel($itipMessage, $existingObject); + + case 'REPLY': + return $this->processMessageReply($itipMessage, $existingObject); + + default: + // Unsupported iTip message + return; + } + + return $existingObject; + } + + /** + * This function parses a VCALENDAR object and figure out if any messages + * need to be sent. + * + * A VCALENDAR object will be created from the perspective of either an + * attendee, or an organizer. You must pass a string identifying the + * current user, so we can figure out who in the list of attendees or the + * organizer we are sending this message on behalf of. + * + * It's possible to specify the current user as an array, in case the user + * has more than one identifying href (such as multiple emails). + * + * It $oldCalendar is specified, it is assumed that the operation is + * updating an existing event, which means that we need to look at the + * differences between events, and potentially send old attendees + * cancellations, and current attendees updates. + * + * If $calendar is null, but $oldCalendar is specified, we treat the + * operation as if the user has deleted an event. If the user was an + * organizer, this means that we need to send cancellation notices to + * people. If the user was an attendee, we need to make sure that the + * organizer gets the 'declined' message. + * + * @param VCalendar|string $calendar + * @param string|array $userHref + * @param VCalendar|string $oldCalendar + * + * @return array + */ + public 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 VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null) + { + if (!$existingObject) { + // This is a new invite, and we're just going to copy over + // all the components from the invite. + $existingObject = new VCalendar(); + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + } else { + // We need to update an existing object with all the new + // information. We can just remove all existing components + // and create new ones. + foreach ($existingObject->getComponents() as $component) { + $existingObject->remove($component); + } + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + } + + return $existingObject; + } + + /** + * Processes incoming CANCEL messages. + * + * This is a message from an organizer, and means that either an + * attendee got removed from an event, or an event got cancelled + * altogether. + * + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null) + { + if (!$existingObject) { + // The event didn't exist in the first place, so we're just + // ignoring this message. + } else { + foreach ($existingObject->VEVENT as $vevent) { + $vevent->STATUS = 'CANCELLED'; + $vevent->SEQUENCE = $itipMessage->sequence; + } + } + + return $existingObject; + } + + /** + * Processes incoming REPLY messages. + * + * The message is a reply. This is for example an attendee telling + * an organizer he accepted the invite, or declined it. + * + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null) + { + // A reply can only be processed based on an existing object. + // If the object is not available, the reply is ignored. + if (!$existingObject) { + return; + } + $instances = []; + $requestStatus = '2.0'; + + // Finding all the instances the attendee replied to. + foreach ($itipMessage->message->VEVENT as $vevent) { + $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 ('master' === $recurId) { + $masterObject = $vevent; + } + if (isset($instances[$recurId])) { + $attendeeFound = false; + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $instances[$recurId]; + $attendee['SCHEDULE-STATUS'] = $requestStatus; + // Un-setting the RSVP status, because we now know + // that the attendee already replied. + unset($attendee['RSVP']); + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee. The iTip documentation calls this + // a party crasher. + $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $instances[$recurId], + ]); + if ($itipMessage->senderName) { + $attendee['CN'] = $itipMessage->senderName; + } + } + unset($instances[$recurId]); + } + } + + if (!$masterObject) { + // No master object, we can't add new instances. + return; + } + // If we got replies to instances that did not exist in the + // original list, it means that new exceptions must be created. + foreach ($instances as $recurId => $partstat) { + $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid); + $found = false; + $iterations = 1000; + do { + $newObject = $recurrenceIterator->getEventObject(); + $recurrenceIterator->next(); + + 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. + * + * @return array + */ + protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) + { + // Merging attendee lists. + $attendees = []; + foreach ($oldEventInfo['attendees'] as $attendee) { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => $attendee['instances'], + 'newInstances' => [], + 'name' => $attendee['name'], + 'forceSend' => null, + ]; + } + foreach ($eventInfo['attendees'] as $attendee) { + if (isset($attendees[$attendee['href']])) { + $attendees[$attendee['href']]['name'] = $attendee['name']; + $attendees[$attendee['href']]['newInstances'] = $attendee['instances']; + $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend']; + } else { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => [], + 'newInstances' => $attendee['instances'], + 'name' => $attendee['name'], + 'forceSend' => $attendee['forceSend'], + ]; + } + } + + $messages = []; + + foreach ($attendees as $attendee) { + // An organizer can also be an attendee. We should not generate any + // messages for those. + if ($attendee['href'] === $eventInfo['organizer']) { + continue; + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $eventInfo['organizer']; + $message->senderName = $eventInfo['organizerName']; + $message->recipient = $attendee['href']; + $message->recipientName = $attendee['name']; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + if (!$attendee['newInstances']) { + // 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'; + + $icalMsg->METHOD = $message->method; + + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]); + if (isset($calendar->VEVENT->SUMMARY)) { + $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue()); + } + $event->add(clone $calendar->VEVENT->DTSTART); + if (isset($calendar->VEVENT->DTEND)) { + $event->add(clone $calendar->VEVENT->DTEND); + } elseif (isset($calendar->VEVENT->DURATION)) { + $event->add(clone $calendar->VEVENT->DURATION); + } + $org = $event->add('ORGANIZER', $eventInfo['organizer']); + if ($eventInfo['organizerName']) { + $org['CN'] = $eventInfo['organizerName']; + } + $event->add('ATTENDEE', $attendee['href'], [ + 'CN' => $attendee['name'], + ]); + $message->significantChange = true; + } else { + // The attendee gets the updated event body + $message->method = 'REQUEST'; + + $icalMsg->METHOD = $message->method; + + // We need to find out that this change is significant. If it's + // not, systems may opt to not send messages. + // + // We do this based on the 'significantChangeHash' which is + // some value that changes if there's a certain set of + // properties changed in the event, or simply if there's a + // difference in instances that the attendee is invited to. + + $message->significantChange = + 'REQUEST' === $attendee['forceSend'] || + array_keys($attendee['oldInstances']) != array_keys($attendee['newInstances']) || + $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; + + foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) { + $currentEvent = clone $eventInfo['instances'][$instanceId]; + if ('master' === $instanceId) { + // We need to find a list of events that the attendee + // is not a part of to add to the list of exceptions. + $exceptions = []; + foreach ($eventInfo['instances'] as $instanceId => $vevent) { + if (!isset($attendee['newInstances'][$instanceId])) { + $exceptions[] = $instanceId; + } + } + + // If there were exceptions, we need to add it to an + // existing EXDATE property, if it exists. + if ($exceptions) { + if (isset($currentEvent->EXDATE)) { + $currentEvent->EXDATE->setParts(array_merge( + $currentEvent->EXDATE->getParts(), + $exceptions + )); + } else { + $currentEvent->EXDATE = $exceptions; + } + } + + // Cleaning up any scheduling information that + // shouldn't be sent along. + unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); + unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); + + foreach ($currentEvent->ATTENDEE as $attendee) { + unset($attendee['SCHEDULE-FORCE-SEND']); + unset($attendee['SCHEDULE-STATUS']); + + // We're adding PARTSTAT=NEEDS-ACTION to ensure that + // iOS shows an "Inbox Item" + if (!isset($attendee['PARTSTAT'])) { + $attendee['PARTSTAT'] = 'NEEDS-ACTION'; + } + } + } + + $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z'); + $icalMsg->add($currentEvent); + } + } + + $message->message = $icalMsg; + $messages[] = $message; + } + + return $messages; + } + + /** + * Parse an event update for an attendee. + * + * This function figures out if we need to send a reply to an organizer. + * + * @param string $attendee + * + * @return Message[] + */ + protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) + { + if ($this->scheduleAgentServerRules && 'CLIENT' === $eventInfo['organizerScheduleAgent']) { + return []; + } + + // Don't bother generating messages for events that have already been + // cancelled. + if ('CANCELLED' === $eventInfo['status']) { + return []; + } + + $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? + $oldEventInfo['attendees'][$attendee]['instances'] : + []; + + $instances = []; + foreach ($oldInstances as $instance) { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => $instance['partstat'], + 'newstatus' => null, + ]; + } + foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) { + if (isset($instances[$instance['id']])) { + $instances[$instance['id']]['newstatus'] = $instance['partstat']; + } else { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => null, + 'newstatus' => $instance['partstat'], + ]; + } + } + + // We need to also look for differences in EXDATE. If there are new + // items in EXDATE, it means that an attendee deleted instances of an + // event, which means we need to send DECLINED specifically for those + // instances. + // We only need to do that though, if the master event is not declined. + if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) { + foreach ($eventInfo['exdate'] as $exDate) { + if (!in_array($exDate, $oldEventInfo['exdate'])) { + if (isset($instances[$exDate])) { + $instances[$exDate]['newstatus'] = 'DECLINED'; + } else { + $instances[$exDate] = [ + 'id' => $exDate, + 'oldstatus' => null, + 'newstatus' => 'DECLINED', + ]; + } + } + } + } + + // Gathering a few extra properties for each instance. + foreach ($instances as $recurId => $instanceInfo) { + if (isset($eventInfo['instances'][$recurId])) { + $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART; + } else { + $instances[$recurId]['dtstart'] = $recurId; + } + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->method = 'REPLY'; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $attendee; + $message->senderName = $eventInfo['attendees'][$attendee]['name']; + $message->recipient = $eventInfo['organizer']; + $message->recipientName = $eventInfo['organizerName']; + + $icalMsg = new VCalendar(); + $icalMsg->METHOD = 'REPLY'; + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + $hasReply = false; + + foreach ($instances as $instance) { + if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) { + // Skip + continue; + } + + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + ]); + $summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : ''; + // Adding properties from the correct source instance + if (isset($eventInfo['instances'][$instance['id']])) { + $instanceObj = $eventInfo['instances'][$instance['id']]; + $event->add(clone $instanceObj->DTSTART); + if (isset($instanceObj->DTEND)) { + $event->add(clone $instanceObj->DTEND); + } elseif (isset($instanceObj->DURATION)) { + $event->add(clone $instanceObj->DURATION); + } + if (isset($instanceObj->SUMMARY)) { + $event->add('SUMMARY', $instanceObj->SUMMARY->getValue()); + } elseif ($summary) { + $event->add('SUMMARY', $summary); + } + } else { + // This branch of the code is reached, when a reply is + // generated for an instance of a recurring event, through the + // fact that the instance has disappeared by showing up in + // EXDATE + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('DTSTART', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('DTSTART', $dt); + } + if ($summary) { + $event->add('SUMMARY', $summary); + } + } + if ('master' !== $instance['id']) { + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('RECURRENCE-ID', $dt); + } + } + $organizer = $event->add('ORGANIZER', $message->recipient); + if ($message->recipientName) { + $organizer['CN'] = $message->recipientName; + } + $attendee = $event->add('ATTENDEE', $message->sender, [ + 'PARTSTAT' => $instance['newstatus'], + ]); + if ($message->senderName) { + $attendee['CN'] = $message->senderName; + } + $hasReply = true; + } + + if ($hasReply) { + $message->message = $icalMsg; + + return [$message]; + } else { + return []; + } + } + + /** + * Returns attendee information and information about instances of an + * event. + * + * Returns an array with the following keys: + * + * 1. uid + * 2. organizer + * 3. organizerName + * 4. organizerScheduleAgent + * 5. organizerForceSend + * 6. instances + * 7. attendees + * 8. sequence + * 9. exdate + * 10. timezone - strictly the timezone on which the recurrence rule is + * based on. + * 11. significantChangeHash + * 12. status + * + * @param VCalendar $calendar + * + * @return array + */ + protected function parseEventInfo(VCalendar $calendar = null) + { + $uid = null; + $organizer = null; + $organizerName = null; + $organizerForceSend = null; + $sequence = null; + $timezone = null; + $status = null; + $organizerScheduleAgent = 'SERVER'; + + $significantChangeHash = ''; + + // Now we need to collect a list of attendees, and which instances they + // are a part of. + $attendees = []; + + $instances = []; + $exdate = []; + + foreach ($calendar->VEVENT as $vevent) { + $rrule = []; + + if (is_null($uid)) { + $uid = $vevent->UID->getValue(); + } else { + if ($uid !== $vevent->UID->getValue()) { + throw new ITipException('If a calendar contained more than one event, they must have the same UID.'); + } + } + + if (!isset($vevent->DTSTART)) { + throw new ITipException('An event MUST have a DTSTART property.'); + } + + if (isset($vevent->ORGANIZER)) { + if (is_null($organizer)) { + $organizer = $vevent->ORGANIZER->getNormalizedValue(); + $organizerName = isset($vevent->ORGANIZER['CN']) ? $vevent->ORGANIZER['CN'] : null; + } else { + if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) { + throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); + } + } + $organizerForceSend = + isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ? + strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) : + null; + $organizerScheduleAgent = + isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ? + strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) : + 'SERVER'; + } + if (is_null($sequence) && isset($vevent->SEQUENCE)) { + $sequence = $vevent->SEQUENCE->getValue(); + } + if (isset($vevent->EXDATE)) { + foreach ($vevent->select('EXDATE') as $val) { + $exdate = array_merge($exdate, $val->getParts()); + } + sort($exdate); + } + if (isset($vevent->RRULE)) { + foreach ($vevent->select('RRULE') as $rr) { + foreach ($rr->getParts() as $key => $val) { + // ignore default values (https://github.com/sabre-io/vobject/issues/126) + if ('INTERVAL' === $key && 1 == $val) { + continue; + } + if (is_array($val)) { + $val = implode(',', $val); + } + $rrule[] = "$key=$val"; + } + } + sort($rrule); + } + if (isset($vevent->STATUS)) { + $status = strtoupper($vevent->STATUS->getValue()); + } + + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + if (is_null($timezone)) { + if ('master' === $recurId) { + $timezone = $vevent->DTSTART->getDateTime()->getTimeZone(); + } else { + $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); + } + } + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($this->scheduleAgentServerRules && + isset($attendee['SCHEDULE-AGENT']) && + 'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue()) + ) { + continue; + } + $partStat = + isset($attendee['PARTSTAT']) ? + strtoupper($attendee['PARTSTAT']) : + 'NEEDS-ACTION'; + + $forceSend = + isset($attendee['SCHEDULE-FORCE-SEND']) ? + strtoupper($attendee['SCHEDULE-FORCE-SEND']) : + null; + + if (isset($attendees[$attendee->getNormalizedValue()])) { + $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [ + 'id' => $recurId, + 'partstat' => $partStat, + 'forceSend' => $forceSend, + ]; + } else { + $attendees[$attendee->getNormalizedValue()] = [ + 'href' => $attendee->getNormalizedValue(), + 'instances' => [ + $recurId => [ + 'id' => $recurId, + 'partstat' => $partStat, + ], + ], + 'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null, + 'forceSend' => $forceSend, + ]; + } + } + $instances[$recurId] = $vevent; + } + + foreach ($this->significantChangeProperties as $prop) { + if (isset($vevent->$prop)) { + $propertyValues = $vevent->select($prop); + + $significantChangeHash .= $prop.':'; + + if ('EXDATE' === $prop) { + $significantChangeHash .= implode(',', $exdate).';'; + } elseif ('RRULE' === $prop) { + $significantChangeHash .= implode(',', $rrule).';'; + } 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/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/ITipException.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/ITipException.php new file mode 100644 index 0000000..9495636 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/ITipException.php @@ -0,0 +1,16 @@ +<?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/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/Message.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/Message.php new file mode 100644 index 0000000..43536f1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/Message.php @@ -0,0 +1,136 @@ +<?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 + */ + public function getScheduleStatus() + { + if (!$this->scheduleStatus) { + return false; + } else { + list($scheduleStatus) = explode(';', $this->scheduleStatus); + + return $scheduleStatus; + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php new file mode 100644 index 0000000..4c48625 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/lib/InvalidDataException.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/InvalidDataException.php new file mode 100644 index 0000000..1d8b675 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/InvalidDataException.php @@ -0,0 +1,15 @@ +<?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/plugins/panakour/backup/vendor/sabre/vobject/lib/Node.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Node.php new file mode 100644 index 0000000..4c0c04f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Node.php @@ -0,0 +1,245 @@ +<?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 public 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 public function jsonSerialize(); + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + abstract public 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. + */ + public function destroy() + { + $this->parent = null; + $this->root = null; + } + + /* {{{ IteratorAggregator interface */ + + /** + * Returns the iterator for this object. + * + * @return ElementList + */ + public function getIterator() + { + if (!is_null($this->iterator)) { + return $this->iterator; + } + + return new ElementList([$this]); + } + + /** + * Sets the overridden iterator. + * + * Note that this is not actually part of the iterator interface + */ + public function setIterator(ElementList $iterator) + { + $this->iterator = $iterator; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + return []; + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements. + * + * @return int + */ + public function count() + { + $it = $this->getIterator(); + + return $it->count(); + } + + /* }}} */ + + /* {{{ ArrayAccess Interface */ + + /** + * Checks if an item exists through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * + * @return bool + */ + public function offsetExists($offset) + { + $iterator = $this->getIterator(); + + return $iterator->offsetExists($offset); + } + + /** + * Gets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * + * @return mixed + */ + public function offsetGet($offset) + { + $iterator = $this->getIterator(); + + return $iterator->offsetGet($offset); + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + $iterator = $this->getIterator(); + $iterator->offsetSet($offset, $value); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + + // @codeCoverageIgnoreEnd + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + public function offsetUnset($offset) + { + $iterator = $this->getIterator(); + $iterator->offsetUnset($offset); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + + // @codeCoverageIgnoreEnd + + /* }}} */ +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/PHPUnitAssertions.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/PHPUnitAssertions.php new file mode 100644 index 0000000..45c0a21 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/PHPUnitAssertions.php @@ -0,0 +1,75 @@ +<?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 whether 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 + */ + public function assertVObjectEqualsVObject($expected, $actual, $message = '') + { + $getObj = function ($input) { + 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 && 'GREGORIAN' === (string) $input->CALSCALE) { + unset($input->CALSCALE); + } + + return $input; + }; + + $expected = $getObj($expected)->serialize(); + $actual = $getObj($actual)->serialize(); + + // Finding wildcards in expected. + preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $actual = preg_replace( + '|^'.preg_quote($match[1], '|').':(.*)\r$|m', + $match[1].':**ANY**'."\r", + $actual + ); + } + + $this->assertEquals( + $expected, + $actual, + $message + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Parameter.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parameter.php new file mode 100644 index 0000000..e39d320 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parameter.php @@ -0,0 +1,371 @@ +<?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 its 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 + */ + public 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 + */ + public static function guessParameterNameByValue($value) + { + switch (strtoupper($value)) { + // Encodings + case '7-BIT': + case 'QUOTED-PRINTABLE': + case 'BASE64': + $name = 'ENCODING'; + break; + + // Common types + case 'WORK': + case 'HOME': + case 'PREF': + + // Delivery Label Type + case 'DOM': + case 'INTL': + case 'POSTAL': + case 'PARCEL': + + // Telephone types + case 'VOICE': + case 'FAX': + case 'MSG': + case 'CELL': + case 'PAGER': + case 'BBS': + case 'MODEM': + case 'CAR': + case 'ISDN': + case 'VIDEO': + + // EMAIL types (lol) + case 'AOL': + case 'APPLELINK': + case 'ATTMAIL': + case 'CIS': + case 'EWORLD': + case 'INTERNET': + case 'IBMMAIL': + case 'MCIMAIL': + case 'POWERSHARE': + case 'PRODIGY': + case 'TLX': + case 'X400': + + // Photo / Logo format types + case 'GIF': + case 'CGM': + case 'WMF': + case 'BMP': + case 'DIB': + case 'PICT': + case 'TIFF': + case 'PDF': + case 'PS': + case 'JPEG': + case 'MPEG': + case 'MPEG2': + case 'AVI': + case 'QTIME': + + // Sound Digital Audio Type + case 'WAVE': + case 'PCM': + case 'AIFF': + + // Key types + case 'X509': + case 'PGP': + $name = 'TYPE'; + break; + + // Value types + case 'INLINE': + case 'URL': + case 'CONTENT-ID': + case 'CID': + $name = 'VALUE'; + break; + + default: + $name = ''; + } + + return $name; + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * Returns the current value. + * + * This method will always return a string, or null. If there were multiple + * values, it will automatically concatenate them (separated by comma). + * + * @return string|null + */ + public function getValue() + { + if (is_array($this->value)) { + return implode(',', $this->value); + } else { + return $this->value; + } + } + + /** + * Sets multiple values for this parameter. + */ + public function setParts(array $value) + { + $this->value = $value; + } + + /** + * Returns all values for this parameter. + * + * If there were no values, an empty array will be returned. + * + * @return array + */ + public function getParts() + { + if (is_array($this->value)) { + return $this->value; + } elseif (is_null($this->value)) { + return []; + } else { + return [$this->value]; + } + } + + /** + * Adds a value to this parameter. + * + * If the argument is specified as an array, all items will be added to the + * parameter value list. + * + * @param string|array $part + */ + public function addValue($part) + { + if (is_null($this->value)) { + $this->value = $part; + } else { + $this->value = array_merge((array) $this->value, (array) $part); + } + } + + /** + * Checks if this parameter contains the specified value. + * + * This is a case-insensitive match. It makes sense to call this for for + * instance the TYPE parameter, to see if it contains a keyword such as + * 'WORK' or 'FAX'. + * + * @param string $value + * + * @return bool + */ + public function has($value) + { + return in_array( + strtolower($value), + array_map('strtolower', (array) $this->value) + ); + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + $value = $this->getParts(); + + if (0 === count($value)) { + return $this->name.'='; + } + + if (Document::VCARD21 === $this->root->getDocumentType() && $this->noName) { + return implode(';', $value); + } + + return $this->name.'='.array_reduce( + $value, + function ($out, $item) { + if (!is_null($out)) { + $out .= ','; + } + + // If there's no special characters in the string, we'll use the simple + // format. + // + // The list of special characters is defined as: + // + // Any character except CONTROL, DQUOTE, ";", ":", "," + // + // by the iCalendar spec: + // https://tools.ietf.org/html/rfc5545#section-3.1 + // + // And we add ^ to that because of: + // https://tools.ietf.org/html/rfc6868 + // + // But we've found that iCal (7.0, shipped with OSX 10.9) + // 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 + */ + public function jsonSerialize() + { + return $this->value; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer) + { + foreach (explode(',', $this->value) as $value) { + $writer->writeElement('text', $value); + } + } + + /** + * Called when this object is being cast to a string. + * + * @return string + */ + public function __toString() + { + return (string) $this->getValue(); + } + + /** + * Returns the iterator for this object. + * + * @return ElementList + */ + public function getIterator() + { + if (!is_null($this->iterator)) { + return $this->iterator; + } + + return $this->iterator = new ArrayIterator((array) $this->value); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/ParseException.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/ParseException.php new file mode 100644 index 0000000..a8f497b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/ParseException.php @@ -0,0 +1,14 @@ +<?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/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/Json.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/Json.php new file mode 100644 index 0000000..f336032 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/Json.php @@ -0,0 +1,190 @@ +<?php + +namespace Sabre\VObject\Parser; + +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Document; +use Sabre\VObject\EofException; +use Sabre\VObject\ParseException; +use Sabre\VObject\Property\FlatText; +use Sabre\VObject\Property\Text; + +/** + * 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 + */ + public 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. + * + * @return \Sabre\VObject\Component + */ + public function parseComponent(array $jComp) + { + // We can remove $self from PHP 5.4 onward. + $self = $this; + + $properties = array_map( + function ($jProp) use ($self) { + return $self->parseProperty($jProp); + }, + $jComp[1] + ); + + if (isset($jComp[2])) { + $components = array_map( + function ($jComp) use ($self) { + return $self->parseComponent($jComp); + }, + $jComp[2] + ); + } else { + $components = []; + } + + return $this->root->createComponent( + $jComp[0], + array_merge($properties, $components), + $defaults = false + ); + } + + /** + * Parses properties. + * + * @return \Sabre\VObject\Property + */ + public function parseProperty(array $jProp) + { + list( + $propertyName, + $parameters, + $valueType + ) = $jProp; + + $propertyName = strtoupper($propertyName); + + // This is the default class we would be using if we didn't know the + // value type. We're using this value later in this function. + $defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName); + + $parameters = (array) $parameters; + + $value = array_slice($jProp, 3); + + $valueType = strtoupper($valueType); + + if (isset($parameters['group'])) { + $propertyName = $parameters['group'].'.'.$propertyName; + unset($parameters['group']); + } + + $prop = $this->root->createProperty($propertyName, null, $parameters, $valueType); + $prop->setJsonValue($value); + + // We have to do something awkward here. FlatText as well as Text + // represents TEXT values. We have to normalize these here. In the + // future we can get rid of FlatText once we're allowed to break BC + // again. + if (FlatText::class === $defaultPropertyClass) { + $defaultPropertyClass = Text::class; + } + + // If the value type we received (e.g.: TEXT) was not the default value + // type for the given property (e.g.: BDAY), we need to add a VALUE= + // parameter. + if ($defaultPropertyClass !== get_class($prop)) { + $prop['VALUE'] = $valueType; + } + + return $prop; + } + + /** + * Sets the input data. + * + * @param resource|string|array $input + */ + public function setInput($input) + { + if (is_resource($input)) { + $input = stream_get_contents($input); + } + if (is_string($input)) { + $input = json_decode($input); + } + $this->input = $input; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/MimeDir.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/MimeDir.php new file mode 100644 index 0000000..ea5ac03 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/MimeDir.php @@ -0,0 +1,671 @@ +<?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\Node; +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 + */ + public 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 + */ + public function setCharset($charset) + { + if (!in_array($charset, self::$SUPPORTED_CHARSETS)) { + throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')'); + } + $this->charset = $charset; + } + + /** + * Sets the input buffer. Must be a string or stream. + * + * @param resource|string $input + */ + public function setInput($input) + { + // Resetting the parser + $this->lineIndex = 0; + $this->startLine = 0; + + if (is_string($input)) { + // Converting to a stream. + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $input); + rewind($stream); + $this->input = $stream; + } elseif (is_resource($input)) { + $this->input = $input; + } else { + throw new \InvalidArgumentException('This parser can only read from strings or streams.'); + } + } + + /** + * Parses an entire document. + */ + protected function parseDocument() + { + $line = $this->readLine(); + + // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF). + // It's 0xEF 0xBB 0xBF in UTF-8 hex. + if (3 <= strlen($line) + && 0xef === ord($line[0]) + && 0xbb === ord($line[1]) + && 0xbf === ord($line[2])) { + $line = substr($line, 3); + } + + switch (strtoupper($line)) { + case 'BEGIN:VCALENDAR': + $class = VCalendar::$componentMap['VCALENDAR']; + break; + case 'BEGIN:VCARD': + $class = VCard::$componentMap['VCARD']; + break; + default: + throw new ParseException('This parser only supports VCARD and VCALENDAR files'); + } + + $this->root = new $class([], false); + + while (true) { + // Reading until we hit END: + $line = $this->readLine(); + if ('END:' === strtoupper(substr($line, 0, 4))) { + break; + } + $result = $this->parseLine($line); + if ($result) { + $this->root->add($result); + } + } + + $name = strtoupper(substr($line, 4)); + if ($name !== $this->root->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"'); + } + } + + /** + * Parses a line, and if it hits a component, it will also attempt to parse + * the entire component. + * + * @param string $line Unfolded line + * + * @return Node + */ + protected function parseLine($line) + { + // Start of a new component + if ('BEGIN:' === strtoupper(substr($line, 0, 6))) { + if (substr($line, 6) === $this->root->name) { + throw new ParseException('Invalid MimeDir file. Unexpected component: "'.$line.'" in document type '.$this->root->name); + } + $component = $this->root->createComponent(substr($line, 6), [], false); + + while (true) { + // Reading until we hit END: + $line = $this->readLine(); + if ('END:' === strtoupper(substr($line, 0, 4))) { + break; + } + $result = $this->parseLine($line); + if ($result) { + $component->add($result); + } + } + + $name = strtoupper(substr($line, 4)); + if ($name !== $component->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"'); + } + + return $component; + } else { + // Property reader + $property = $this->readProperty($line); + if (!$property) { + // Ignored line + return false; + } + + return $property; + } + } + + /** + * We need to look ahead 1 line every time to see if we need to 'unfold' + * the next line. + * + * If that was not the case, we store it here. + * + * @var string|null + */ + protected $lineBuffer; + + /** + * The real current line number. + */ + protected $lineIndex = 0; + + /** + * In the case of unfolded lines, this property holds the line number for + * the start of the line. + * + * @var int + */ + protected $startLine = 0; + + /** + * Contains a 'raw' representation of the current line. + * + * @var string + */ + protected $rawLine; + + /** + * Reads a single line from the buffer. + * + * This method strips any newlines and also takes care of unfolding. + * + * @throws \Sabre\VObject\EofException + * + * @return string + */ + protected function readLine() + { + if (!\is_null($this->lineBuffer)) { + $rawLine = $this->lineBuffer; + $this->lineBuffer = null; + } else { + do { + $eof = \feof($this->input); + + $rawLine = \fgets($this->input); + + if ($eof || (\feof($this->input) && false === $rawLine)) { + throw new EofException('End of document reached prematurely'); + } + if (false === $rawLine) { + throw new ParseException('Error reading from input stream'); + } + $rawLine = \rtrim($rawLine, "\r\n"); + } while ('' === $rawLine); // Skipping empty lines + ++$this->lineIndex; + } + $line = $rawLine; + + $this->startLine = $this->lineIndex; + + // Looking ahead for folded lines. + while (true) { + $nextLine = \rtrim(\fgets($this->input), "\r\n"); + ++$this->lineIndex; + if (!$nextLine) { + break; + } + if ("\t" === $nextLine[0] || ' ' === $nextLine[0]) { + $curLine = \substr($nextLine, 1); + $line .= $curLine; + $rawLine .= "\n ".$curLine; + } else { + $this->lineBuffer = $nextLine; + break; + } + } + $this->rawLine = $rawLine; + + return $line; + } + + /** + * Reads a property or component from a line. + */ + protected function readProperty($line) + { + if ($this->options & self::OPTION_FORGIVING) { + $propNameToken = 'A-Z0-9\-\._\\/'; + } else { + $propNameToken = 'A-Z0-9\-\.'; + } + + $paramNameToken = 'A-Z0-9\-'; + $safeChar = '^";:,'; + $qSafeChar = '^"'; + + $regex = "/ + ^(?P<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 its name. + // + // Our parser will get those as parameters without a value instead, so + // we're filtering these parameters out first. + $namedParameters = []; + $namelessParameters = []; + + foreach ($property['parameters'] as $name => $value) { + if (!is_null($value)) { + $namedParameters[$name] = $value; + } else { + $namelessParameters[] = $name; + } + } + + $propObj = $this->root->createProperty($property['name'], null, $namedParameters); + + foreach ($namelessParameters as $namelessParameter) { + $propObj->add(null, $namelessParameter); + } + + if ('QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) { + $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); + } else { + $charset = $this->charset; + if (Document::VCARD21 === $this->root->getDocumentType() && isset($propObj['CHARSET'])) { + // vCard 2.1 allows the character set to be specified per property. + $charset = (string) $propObj['CHARSET']; + } + switch (strtolower($charset)) { + case 'utf-8': + break; + case '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 delimiters in multiple values + * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped, + * as in some properties semi-colon is used for separators. + * * Properties using semi-colons: N, ADR, GEO, ORG + * * Both ADR and N's individual parts may be broken up further with a + * comma. + * * Properties using commas: NICKNAME, CATEGORIES + * + * vCard 4.0 (rfc6350) says: + * * Commas must be escaped. + * * Semi-colons may be escaped, an unescaped semi-colon _may_ be a + * delimiter, depending on the property. + * * Backslashes must be escaped + * * Newlines must be escaped as either \N or \n. + * * Some compound properties may contain multiple parts themselves, so a + * comma within a semi-colon delimited property may also be unescaped + * to denote multiple parts _within_ the compound property. + * * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP. + * * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID. + * + * Even though the spec says that commas must always be escaped, the + * example for GEO in Section 6.5.2 seems to violate this. + * + * iCalendar 2.0 (rfc5545) says: + * * Commas or semi-colons may be used as delimiters, depending on the + * property. + * * Commas, semi-colons, backslashes, newline (\N or \n) are always + * escaped, unless they are delimiters. + * * Colons shall not be escaped. + * * Commas can be considered the 'default delimiter' and is described as + * the delimiter in cases where the order of the multiple values is + * insignificant. + * * Semi-colons are described as the delimiter for 'structured values'. + * They are specifically used in Semi-colons are used as a delimiter in + * REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however. + * + * Now for the parameters + * + * If delimiter is not set (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[] + */ + public static function unescapeValue($input, $delimiter = ';') + { + $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )'; + if ($delimiter) { + $regex .= ' | ('.$delimiter.')'; + } + $regex .= ') #x'; + + $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + $resultArray = []; + $result = ''; + + foreach ($matches as $match) { + switch ($match) { + case '\\\\': + $result .= '\\'; + break; + case '\N': + case '\n': + $result .= "\n"; + break; + case '\;': + $result .= ';'; + break; + case '\,': + $result .= ','; + break; + case $delimiter: + $resultArray[] = $result; + $result = ''; + break; + default: + $result .= $match; + break; + } + } + + $resultArray[] = $result; + + return $delimiter ? $resultArray : $result; + } + + /** + * Unescapes a parameter value. + * + * vCard 2.1: + * * Does not mention a mechanism for this. In addition, double quotes + * are never used to wrap values. + * * This means that parameters can simply not contain colons or + * semi-colons. + * + * vCard 3.0 (rfc2425, rfc2426): + * * Parameters _may_ be surrounded by double quotes. + * * If this is not the case, semi-colon, colon and comma may simply not + * occur (the comma used for multiple parameter values though). + * * If it is surrounded by double-quotes, it may simply not contain + * double-quotes. + * * This means that a parameter can in no case encode double-quotes, or + * newlines. + * + * vCard 4.0 (rfc6350) + * * Behavior seems to be identical to vCard 3.0 + * + * iCalendar 2.0 (rfc5545) + * * Behavior seems to be identical to vCard 3.0 + * + * Parameter escaping mechanism (rfc6868) : + * * This rfc describes a new way to escape parameter values. + * * New-line is encoded as ^n + * * ^ is encoded as ^^. + * * " is encoded as ^' + * + * @param string $input + */ + private function unescapeParam($input) + { + return + preg_replace_callback( + '#(\^(\^|n|\'))#', + function ($matches) { + switch ($matches[2]) { + case 'n': + return "\n"; + case '^': + return '^'; + case '\'': + return '"'; + + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + }, + $input + ); + } + + /** + * Gets the full quoted printable value. + * + * We need a special method for this, because newlines have both a meaning + * in vCards, and in QuotedPrintable. + * + * This method does not do any decoding. + * + * @return string + */ + private function extractQuotedPrintableValue() + { + // We need to parse the raw line again to get the start of the value. + // + // We are basically looking for the first colon (:), but we need to + // skip over the parameters first, as they may contain one. + $regex = '/^ + (?: [^:])+ # Anything but a colon + (?: "[^"]")* # A parameter in double quotes + : # start of the value we really care about + (.*)$ + /xs'; + + preg_match($regex, $this->rawLine, $matches); + + $value = $matches[1]; + // Removing the first whitespace character from every line. Kind of + // like unfolding, but we keep the newline. + $value = str_replace("\n ", "\n", $value); + + // Microsoft products don't always correctly fold lines, they may be + // missing a whitespace. So if 'forgiving' is turned on, we will take + // those as well. + if ($this->options & self::OPTION_FORGIVING) { + while ('=' === substr($value, -1) && $this->lineBuffer) { + // Reading the line + $this->readLine(); + // Grabbing the raw form + $value .= "\n".$this->rawLine; + } + } + + return $value; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/Parser.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/Parser.php new file mode 100644 index 0000000..b7b6114 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/Parser.php @@ -0,0 +1,75 @@ +<?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) + */ + public 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 public function parse($input = null, $options = 0); + + /** + * Sets the input data. + * + * @param mixed $input + */ + abstract public function setInput($input); +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/XML.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/XML.php new file mode 100644 index 0000000..7877317 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/XML.php @@ -0,0 +1,377 @@ +<?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) + */ + public 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 + */ + public 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. + */ + protected function parseVCalendarComponents(Component $parentComponent) + { + foreach ($this->pointer['value'] ?: [] as $children) { + switch (static::getTagName($children['name'])) { + case 'properties': + $this->pointer = &$children['value']; + $this->parseProperties($parentComponent); + break; + + case 'components': + $this->pointer = &$children; + $this->parseComponent($parentComponent); + break; + } + } + } + + /** + * Parse a xCard component. + */ + protected function parseVCardComponents(Component $parentComponent) + { + $this->pointer = &$this->pointer['value']; + $this->parseProperties($parentComponent); + } + + /** + * Parse xCalendar and xCard properties. + * + * @param string $propertyNamePrefix + */ + protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '') + { + foreach ($this->pointer ?: [] as $xmlProperty) { + list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']); + + $propertyName = $tagName; + $propertyValue = []; + $propertyParameters = []; + $propertyType = 'text'; + + // A property which is not part of the standard. + if (self::XCAL_NAMESPACE !== $namespace + && self::XCARD_NAMESPACE !== $namespace) { + $propertyName = 'xml'; + $value = '<'.$tagName.' xmlns="'.$namespace.'"'; + + foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) { + $value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"'; + } + + $value .= '>'.$xmlProperty['value'].'</'.$tagName.'>'; + + $propertyValue = [$value]; + + $this->createProperty( + $parentComponent, + $propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + + continue; + } + + // xCard group. + if ('group' === $propertyName) { + if (!isset($xmlProperty['attributes']['name'])) { + continue; + } + + $this->pointer = &$xmlProperty['value']; + $this->parseProperties( + $parentComponent, + strtoupper($xmlProperty['attributes']['name']).'.' + ); + + continue; + } + + // Collect parameters. + foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) { + if (!is_array($xmlPropertyChild) + || 'parameters' !== static::getTagName($xmlPropertyChild['name'])) { + continue; + } + + $xmlParameters = $xmlPropertyChild['value']; + + foreach ($xmlParameters as $xmlParameter) { + $propertyParameterValues = []; + + foreach ($xmlParameter['value'] as $xmlParameterValues) { + $propertyParameterValues[] = $xmlParameterValues['value']; + } + + $propertyParameters[static::getTagName($xmlParameter['name'])] + = implode(',', $propertyParameterValues); + } + + array_splice($xmlProperty['value'], $i, 1); + } + + $propertyNameExtended = ($this->root instanceof VCalendar + ? 'xcal' + : 'xcard').':'.$propertyName; + + switch ($propertyNameExtended) { + case 'xcal:geo': + $propertyType = 'float'; + $propertyValue['latitude'] = 0; + $propertyValue['longitude'] = 0; + + foreach ($xmlProperty['value'] as $xmlRequestChild) { + $propertyValue[static::getTagName($xmlRequestChild['name'])] + = $xmlRequestChild['value']; + } + break; + + case 'xcal:request-status': + $propertyType = 'text'; + + foreach ($xmlProperty['value'] as $xmlRequestChild) { + $propertyValue[static::getTagName($xmlRequestChild['name'])] + = $xmlRequestChild['value']; + } + break; + + case 'xcal:freebusy': + $propertyType = 'freebusy'; + // We don't break because we only want to set + // another property type. + + // no break + case 'xcal:categories': + case 'xcal:resources': + case 'xcal:exdate': + foreach ($xmlProperty['value'] as $specialChild) { + $propertyValue[static::getTagName($specialChild['name'])] + = $specialChild['value']; + } + break; + + case 'xcal:rdate': + $propertyType = 'date-time'; + + foreach ($xmlProperty['value'] as $specialChild) { + $tagName = static::getTagName($specialChild['name']); + + if ('period' === $tagName) { + $propertyParameters['value'] = 'PERIOD'; + $propertyValue[] = implode('/', $specialChild['value']); + } else { + $propertyValue[] = $specialChild['value']; + } + } + break; + + default: + $propertyType = static::getTagName($xmlProperty['value'][0]['name']); + + foreach ($xmlProperty['value'] as $value) { + $propertyValue[] = $value['value']; + } + + if ('date' === $propertyType) { + $propertyParameters['value'] = 'DATE'; + } + break; + } + + $this->createProperty( + $parentComponent, + $propertyNamePrefix.$propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + } + } + + /** + * Parse a component. + */ + protected function parseComponent(Component $parentComponent) + { + $components = $this->pointer['value'] ?: []; + + foreach ($components as $component) { + $componentName = static::getTagName($component['name']); + $currentComponent = $this->root->createComponent( + $componentName, + null, + false + ); + + $this->pointer = &$component; + $this->parseVCalendarComponents($currentComponent); + + $parentComponent->add($currentComponent); + } + } + + /** + * Create a property. + * + * @param string $name + * @param array $parameters + * @param string $type + * @param mixed $value + */ + protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value) + { + $property = $this->root->createProperty( + $name, + null, + $parameters, + $type + ); + $parentComponent->add($property); + $property->setXmlValue($value); + } + + /** + * Sets the input data. + * + * @param resource|string $input + */ + public function setInput($input) + { + if (is_resource($input)) { + $input = stream_get_contents($input); + } + + if (is_string($input)) { + $reader = new SabreXml\Reader(); + $reader->elementMap['{'.self::XCAL_NAMESPACE.'}period'] + = XML\Element\KeyValue::class; + $reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur'] + = XML\Element\KeyValue::class; + $reader->xml($input); + $input = $reader->parse(); + } + + $this->input = $input; + } + + /** + * Get tag name from a Clark notation. + * + * @param string $clarkedTagName + * + * @return string + */ + protected static function getTagName($clarkedTagName) + { + list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName); + + return $tagName; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php new file mode 100644 index 0000000..c0bbf0d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php @@ -0,0 +1,65 @@ +<?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 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 XML\Reader $reader + * + * @return mixed + */ + public 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 (SabreXml\Reader::ELEMENT === $reader->nodeType) { + $name = $reader->localName; + $values[$name] = $reader->parseCurrentElement()['value']; + } else { + $reader->read(); + } + } while (SabreXml\Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property.php new file mode 100644 index 0000000..f9cf8e3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property.php @@ -0,0 +1,615 @@ +<?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 + */ + public 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 + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + * + * @return string + */ + public function getValue() + { + if (is_array($this->value)) { + if (0 == count($this->value)) { + return; + } elseif (1 === count($this->value)) { + return $this->value[0]; + } else { + return $this->getRawMimeDirValue(); + } + } else { + return $this->value; + } + } + + /** + * Sets a multi-valued property. + */ + public function setParts(array $parts) + { + $this->value = $parts; + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + * + * @return array + */ + public function getParts() + { + if (is_null($this->value)) { + return []; + } elseif (is_array($this->value)) { + return $this->value; + } else { + return [$this->value]; + } + } + + /** + * Adds a new parameter. + * + * If a parameter with same name already existed, the values will be + * combined. + * If nameless parameter is added, we try to guess its name. + * + * @param string $name + * @param string|array|null $value + */ + public function add($name, $value = null) + { + $noName = false; + if (null === $name) { + $name = Parameter::guessParameterNameByValue($value); + $noName = true; + } + + if (isset($this->parameters[strtoupper($name)])) { + $this->parameters[strtoupper($name)]->addValue($value); + } else { + $param = new Parameter($this->root, $name, $value); + $param->noName = $noName; + $this->parameters[$param->name] = $param; + } + } + + /** + * Returns an iterable list of children. + * + * @return array + */ + public function parameters() + { + return $this->parameters; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + abstract public function getValueType(); + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + abstract public function setRawMimeDirValue($val); + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + abstract public function getRawMimeDirValue(); + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + $str = $this->name; + if ($this->group) { + $str = $this->group.'.'.$this->name; + } + + foreach ($this->parameters() as $param) { + $str .= ';'.$param->serialize(); + } + + $str .= ':'.$this->getRawMimeDirValue(); + + $str = \preg_replace( + '/( + (?:^.)? # 1 additional byte in first line because of missing single space (see next line) + .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) + (?![\x80-\xbf]) # prevent splitting multibyte characters + )/x', + "$1\r\n ", + $str + ); + + // remove single space after last CRLF + return \substr($str, 0, -1); + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return $this->getParts(); + } + + /** + * Sets the JSON value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + if (1 === count($value)) { + $this->setValue(reset($value)); + } else { + $this->setValue($value); + } + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array + */ + public function jsonSerialize() + { + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ('VALUE' === $parameter->name) { + continue; + } + $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); + } + // In jCard, we need to encode the property-group as a separate 'group' + // parameter. + if ($this->group) { + $parameters['group'] = $this->group; + } + + return array_merge( + [ + strtolower($this->name), + (object) $parameters, + strtolower($this->getValueType()), + ], + $this->getJsonValue() + ); + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $this->setJsonValue($value); + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer) + { + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ('VALUE' === $parameter->name) { + continue; + } + + $parameters[] = $parameter; + } + + $writer->startElement(strtolower($this->name)); + + if (!empty($parameters)) { + $writer->startElement('parameters'); + + foreach ($parameters as $parameter) { + $writer->startElement(strtolower($parameter->name)); + $writer->write($parameter); + $writer->endElement(); + } + + $writer->endElement(); + } + + $this->xmlSerializeValue($writer); + $writer->endElement(); + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + + foreach ($this->getJsonValue() as $values) { + foreach ((array) $values as $value) { + $writer->writeElement($valueType, $value); + } + } + } + + /** + * Called when this object is being cast to a string. + * + * If the property only had a single value, you will get just that. In the + * case the property had multiple values, the contents will be escaped and + * combined with ,. + * + * @return string + */ + public function __toString() + { + return (string) $this->getValue(); + } + + /* ArrayAccess interface {{{ */ + + /** + * Checks if an array element exists. + * + * @param mixed $name + * + * @return bool + */ + public function offsetExists($name) + { + if (is_int($name)) { + return parent::offsetExists($name); + } + + $name = strtoupper($name); + + foreach ($this->parameters as $parameter) { + if ($parameter->name == $name) { + return true; + } + } + + return false; + } + + /** + * Returns a parameter. + * + * If the parameter does not exist, null is returned. + * + * @param string $name + * + * @return Node + */ + public function offsetGet($name) + { + if (is_int($name)) { + return parent::offsetGet($name); + } + $name = strtoupper($name); + + if (!isset($this->parameters[$name])) { + return; + } + + return $this->parameters[$name]; + } + + /** + * Creates a new parameter. + * + * @param string $name + * @param mixed $value + */ + public function offsetSet($name, $value) + { + if (is_int($name)) { + parent::offsetSet($name, $value); + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + $param = new Parameter($this->root, $name, $value); + $this->parameters[$param->name] = $param; + } + + /** + * Removes one or more parameters with the specified name. + * + * @param string $name + */ + public function offsetUnset($name) + { + if (is_int($name)) { + parent::offsetUnset($name); + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + unset($this->parameters[strtoupper($name)]); + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + */ + public function __clone() + { + foreach ($this->parameters as $key => $child) { + $this->parameters[$key] = clone $child; + $this->parameters[$key]->parent = $this; + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = []; + + // Checking if our value is UTF-8 + if (!StringUtil::isUTF8($this->getRawMimeDirValue())) { + $oldValue = $this->getRawMimeDirValue(); + $level = 3; + if ($options & self::REPAIR) { + $newValue = StringUtil::convertToUTF8($oldValue); + if (true || StringUtil::isUTF8($newValue)) { + $this->setRawMimeDirValue($newValue); + $level = 1; + } + } + + if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) { + $message = 'Property contained a control character (0x'.bin2hex($matches[1]).')'; + } else { + $message = 'Property is not valid UTF-8! '.$oldValue; + } + + $warnings[] = [ + 'level' => $level, + 'message' => $message, + 'node' => $this, + ]; + } + + // Checking if the propertyname does not contain any invalid bytes. + if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { + $warnings[] = [ + 'level' => $options & self::REPAIR ? 1 : 3, + 'message' => 'The propertyname: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed', + 'node' => $this, + ]; + if ($options & self::REPAIR) { + // Uppercasing and converting underscores to dashes. + $this->name = strtoupper( + str_replace('_', '-', $this->name) + ); + // Removing every other invalid character + $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name); + } + } + + if ($encoding = $this->offsetGet('ENCODING')) { + if (Document::VCARD40 === $this->root->getDocumentType()) { + $warnings[] = [ + 'level' => 3, + 'message' => 'ENCODING parameter is not valid in vCard 4.', + 'node' => $this, + ]; + } else { + $encoding = (string) $encoding; + + $allowedEncoding = []; + + switch ($this->root->getDocumentType()) { + case Document::ICALENDAR20: + $allowedEncoding = ['8BIT', 'BASE64']; + break; + case Document::VCARD21: + $allowedEncoding = ['QUOTED-PRINTABLE', 'BASE64', '8BIT']; + break; + case Document::VCARD30: + $allowedEncoding = ['B']; + //Repair vCard30 that use BASE64 encoding + if ($options & self::REPAIR) { + if ('BASE64' === strtoupper($encoding)) { + $encoding = 'B'; + $this['ENCODING'] = $encoding; + $warnings[] = [ + 'level' => 1, + 'message' => 'ENCODING=BASE64 has been transformed to ENCODING=B.', + 'node' => $this, + ]; + } + } + break; + } + if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.', + 'node' => $this, + ]; + } + } + } + + // Validating inner parameters + foreach ($this->parameters as $param) { + $warnings = array_merge($warnings, $param->validate($options)); + } + + return $warnings; + } + + /** + * Call this method on a document if you're done using it. + * + * It's intended to remove all circular references, so PHP can easily clean + * it up. + */ + public function destroy() + { + parent::destroy(); + foreach ($this->parameters as $param) { + $param->destroy(); + } + $this->parameters = []; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Binary.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Binary.php new file mode 100644 index 0000000..ec6713f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Binary.php @@ -0,0 +1,109 @@ +<?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 + */ + public function setValue($value) + { + if (is_array($value)) { + if (1 === count($value)) { + $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 + */ + public function setRawMimeDirValue($val) + { + $this->value = base64_decode($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return base64_encode($this->value); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'BINARY'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return [base64_encode($this->getValue())]; + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + $value = array_map('base64_decode', $value); + parent::setJsonValue($value); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Boolean.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Boolean.php new file mode 100644 index 0000000..9fb2bce --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Boolean.php @@ -0,0 +1,73 @@ +<?php + +namespace Sabre\VObject\Property; + +use + Sabre\VObject\Property; + +/** + * Boolean property. + * + * This object represents BOOLEAN values. These are always the case-insensitive + * 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 + */ + public function setRawMimeDirValue($val) + { + $val = 'TRUE' === strtoupper($val) ? true : false; + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->value ? 'TRUE' : 'FALSE'; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'BOOLEAN'; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map( + function ($value) { + return 'true' === $value; + }, + $value + ); + parent::setXmlValue($value); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/FlatText.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/FlatText.php new file mode 100644 index 0000000..d15cfe0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/FlatText.php @@ -0,0 +1,46 @@ +<?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 + */ + public function setQuotedPrintableValue($val) + { + $val = quoted_printable_decode($val); + $this->setValue($val); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/FloatValue.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/FloatValue.php new file mode 100644 index 0000000..0d03469 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/FloatValue.php @@ -0,0 +1,124 @@ +<?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 + */ + public 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 + */ + public function getRawMimeDirValue() + { + return implode( + $this->delimiter, + $this->getParts() + ); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'FLOAT'; + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $val = array_map('floatval', $this->getParts()); + + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2 + if ('GEO' === $this->name) { + return [$val]; + } + + return $val; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map('floatval', $value); + parent::setXmlValue($value); + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.2 + if ('GEO' === $this->name) { + $value = array_map('floatval', $this->getParts()); + + $writer->writeElement('latitude', $value[0]); + $writer->writeElement('longitude', $value[1]); + } else { + parent::xmlSerializeValue($writer); + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php new file mode 100644 index 0000000..e89bb31 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php @@ -0,0 +1,60 @@ +<?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 + */ + public 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 + */ + public function getNormalizedValue() + { + $input = $this->getValue(); + if (!strpos($input, ':')) { + return $input; + } + list($schema, $everythingElse) = explode(':', $input, 2); + + return strtolower($schema).':'.$everythingElse; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Date.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Date.php new file mode 100644 index 0000000..d8e86d1 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/DateTime.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/DateTime.php new file mode 100644 index 0000000..f2dbdeb --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/DateTime.php @@ -0,0 +1,363 @@ +<?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. + */ + public 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 + */ + public function setValue($value) + { + if (is_array($value) && isset($value[0]) && $value[0] instanceof DateTimeInterface) { + $this->setDateTimes($value); + } elseif ($value instanceof DateTimeInterface) { + $this->setDateTimes([$value]); + } else { + parent::setValue($value); + } + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns true if this is a DATE-TIME value, false if it's a DATE. + * + * @return bool + */ + public function hasTime() + { + return 'DATE' !== strtoupper((string) $this['VALUE']); + } + + /** + * Returns true if this is a floating DATE or DATE-TIME. + * + * Note that DATE is always floating. + */ + public function isFloating() + { + return + !$this->hasTime() || + ( + !isset($this['TZID']) && + false === strpos($this->getValue(), 'Z') + ); + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * If no timezone information is known, because it's either an all-day + * property or floating time, we will use the DateTimeZone argument to + * figure out the exact date. + * + * @param DateTimeZone $timeZone + * + * @return \DateTimeImmutable + */ + public function getDateTime(DateTimeZone $timeZone = null) + { + $dt = $this->getDateTimes($timeZone); + if (!$dt) { + return; + } + + return $dt[0]; + } + + /** + * Returns multiple date-time values. + * + * If no timezone information is known, because it's either an all-day + * property or floating time, we will use the DateTimeZone argument to + * figure out the exact date. + * + * @param DateTimeZone $timeZone + * + * @return \DateTimeImmutable[] + * @return \DateTime[] + */ + public function getDateTimes(DateTimeZone $timeZone = null) + { + // Does the property have a TZID? + $tzid = $this['TZID']; + + if ($tzid) { + $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root); + } + + $dts = []; + foreach ($this->getParts() as $part) { + $dts[] = DateTimeParser::parse($part, $timeZone); + } + + return $dts; + } + + /** + * Sets the property as a DateTime object. + * + * @param bool isFloating If set to true, timezones will be ignored + */ + public function setDateTime(DateTimeInterface $dt, $isFloating = false) + { + $this->setDateTimes([$dt], $isFloating); + } + + /** + * Sets the property as multiple date-time objects. + * + * The first value will be used as a reference for the timezones, and all + * the otehr values will be adjusted for that timezone + * + * @param DateTimeInterface[] $dt + * @param bool isFloating If set to true, timezones will be ignored + */ + public function setDateTimes(array $dt, $isFloating = false) + { + $values = []; + + if ($this->hasTime()) { + $tz = null; + $isUtc = false; + + foreach ($dt as $d) { + if ($isFloating) { + $values[] = $d->format('Ymd\\THis'); + continue; + } + if (is_null($tz)) { + $tz = $d->getTimeZone(); + $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']); + if (!$isUtc) { + $this->offsetSet('TZID', $tz->getName()); + } + } else { + $d = $d->setTimeZone($tz); + } + + if ($isUtc) { + $values[] = $d->format('Ymd\\THis\\Z'); + } else { + $values[] = $d->format('Ymd\\THis'); + } + } + if ($isUtc || $isFloating) { + $this->offsetUnset('TZID'); + } + } else { + foreach ($dt as $d) { + $values[] = $d->format('Ymd'); + } + $this->offsetUnset('TZID'); + } + + $this->value = $values; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return $this->hasTime() ? 'DATE-TIME' : 'DATE'; + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $dts = $this->getDateTimes(); + $hasTime = $this->hasTime(); + $isFloating = $this->isFloating(); + + $tz = $dts[0]->getTimeZone(); + $isUtc = $isFloating ? false : in_array($tz->getName(), ['UTC', 'GMT', 'Z']); + + return array_map( + function (DateTimeInterface $dt) use ($hasTime, $isUtc) { + if ($hasTime) { + return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : ''); + } else { + return $dt->format('Y-m-d'); + } + }, + $dts + ); + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + // dates and times in jCal have one difference to dates and times in + // iCalendar. In jCal date-parts are separated by dashes, and + // time-parts are separated by colons. It makes sense to just remove + // those. + $this->setValue( + array_map( + function ($item) { + return strtr($item, [':' => '', '-' => '']); + }, + $value + ) + ); + } + + /** + * We need to intercept offsetSet, because it may be used to alter the + * VALUE from DATE-TIME to DATE or vice-versa. + * + * @param string $name + * @param mixed $value + */ + public function offsetSet($name, $value) + { + parent::offsetSet($name, $value); + if ('VALUE' !== strtoupper($name)) { + return; + } + + // This will ensure that dates are correctly encoded. + $this->setDateTimes($this->getDateTimes()); + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $messages = parent::validate($options); + $valueType = $this->getValueType(); + $values = $this->getParts(); + foreach ($values as $value) { + try { + switch ($valueType) { + case 'DATE': + DateTimeParser::parseDate($value); + break; + case 'DATE-TIME': + DateTimeParser::parseDateTime($value); + break; + } + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, + 'node' => $this, + ]; + break; + } + } + + return $messages; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php new file mode 100644 index 0000000..87f0081 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php @@ -0,0 +1,79 @@ +<?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 + */ + public function setRawMimeDirValue($val) + { + $this->setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'DURATION'; + } + + /** + * Returns a DateInterval representation of the Duration property. + * + * If the property has more than one value, only the first is returned. + * + * @return \DateInterval + */ + public function getDateInterval() + { + $parts = $this->getParts(); + $value = $parts[0]; + + return DateTimeParser::parseDuration($value); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Period.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Period.php new file mode 100644 index 0000000..eb37527 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Period.php @@ -0,0 +1,135 @@ +<?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 + */ + public function setRawMimeDirValue($val) + { + $this->setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'PERIOD'; + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + $value = array_map( + function ($item) { + return strtr(implode('/', $item), [':' => '', '-' => '']); + }, + $value + ); + parent::setJsonValue($value); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $return = []; + foreach ($this->getParts() as $item) { + list($start, $end) = explode('/', $item, 2); + + $start = DateTimeParser::parseDateTime($start); + + // This is a duration value. + if ('P' === $end[0]) { + $return[] = [ + $start->format('Y-m-d\\TH:i:s'), + $end, + ]; + } else { + $end = DateTimeParser::parseDateTime($end); + $return[] = [ + $start->format('Y-m-d\\TH:i:s'), + $end->format('Y-m-d\\TH:i:s'), + ]; + } + } + + return $return; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $writer->startElement(strtolower($this->getValueType())); + $value = $this->getJsonValue(); + $writer->writeElement('start', $value[0][0]); + + if ('P' === $value[0][1][0]) { + $writer->writeElement('duration', $value[0][1]); + } else { + $writer->writeElement('end', $value[0][1]); + } + + $writer->endElement(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php new file mode 100644 index 0000000..3d632fe --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php @@ -0,0 +1,336 @@ +<?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 + */ + public function setValue($value) + { + // If we're getting the data from json, we'll be receiving an object + if ($value instanceof \StdClass) { + $value = (array) $value; + } + + if (is_array($value)) { + $newVal = []; + foreach ($value as $k => $v) { + if (is_string($v)) { + $v = strtoupper($v); + + // The value had multiple sub-values + if (false !== strpos($v, ',')) { + $v = explode(',', $v); + } + if (0 === strcmp($k, 'until')) { + $v = strtr($v, [':' => '', '-' => '']); + } + } elseif (is_array($v)) { + $v = array_map('strtoupper', $v); + } + + $newVal[strtoupper($k)] = $v; + } + $this->value = $newVal; + } elseif (is_string($value)) { + $this->value = self::stringToArray($value); + } else { + throw new \InvalidArgumentException('You must either pass a string, or a key=>value array'); + } + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + * + * @return string + */ + public function getValue() + { + $out = []; + foreach ($this->value as $key => $value) { + $out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value); + } + + return strtoupper(implode(';', $out)); + } + + /** + * Sets a multi-valued property. + */ + public function setParts(array $parts) + { + $this->setValue($parts); + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + * + * @return array + */ + public function getParts() + { + return $this->value; + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->getValue(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'RECUR'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $values = []; + foreach ($this->getParts() as $k => $v) { + if (0 === strcmp($k, 'UNTIL')) { + $date = new DateTime($this->root, null, $v); + $values[strtolower($k)] = $date->getJsonValue()[0]; + } elseif (0 === strcmp($k, 'COUNT')) { + $values[strtolower($k)] = intval($v); + } else { + $values[strtolower($k)] = $v; + } + } + + return [$values]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + + foreach ($this->getJsonValue() as $value) { + $writer->writeElement($valueType, $value); + } + } + + /** + * Parses an RRULE value string, and turns it into a struct-ish array. + * + * @param string $value + * + * @return array + */ + public static function stringToArray($value) + { + $value = strtoupper($value); + $newValue = []; + foreach (explode(';', $value) as $part) { + // Skipping empty parts. + if (empty($part)) { + continue; + } + list($partName, $partValue) = explode('=', $part); + + // The value itself had multiple values.. + if (false !== strpos($partValue, ',')) { + $partValue = explode(',', $partValue); + } + $newValue[$partName] = $partValue; + } + + return $newValue; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $repair = ($options & self::REPAIR); + + $warnings = parent::validate($options); + $values = $this->getParts(); + + foreach ($values as $key => $value) { + if ('' === $value) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'Invalid value for '.$key.' in '.$this->name, + 'node' => $this, + ]; + if ($repair) { + unset($values[$key]); + } + } elseif ('BYMONTH' == $key) { + $byMonth = (array) $value; + foreach ($byMonth as $i => $v) { + if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYMONTH in RRULE must have value(s) between 1 and 12!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } elseif ('BYWEEKNO' == $key) { + $byWeekNo = (array) $value; + foreach ($byWeekNo as $i => $v) { + if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } elseif ('BYYEARDAY' == $key) { + $byYearDay = (array) $value; + foreach ($byYearDay as $i => $v) { + if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } + } + if (!isset($values['FREQ'])) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'FREQ is required in '.$this->name, + 'node' => $this, + ]; + if ($repair) { + $this->parent->remove($this); + } + } + if ($repair) { + $this->setValue($values); + } + + return $warnings; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/IntegerValue.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/IntegerValue.php new file mode 100644 index 0000000..6f709bf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/IntegerValue.php @@ -0,0 +1,77 @@ +<?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 preceded 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 + */ + public function setRawMimeDirValue($val) + { + $this->setValue((int) $val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->value; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'INTEGER'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return [(int) $this->getValue()]; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map('intval', $value); + parent::setXmlValue($value); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Text.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Text.php new file mode 100644 index 0000000..ac8aa06 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Text.php @@ -0,0 +1,390 @@ +<?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 + */ + public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null) + { + // There's two types of multi-valued text properties: + // 1. multivalue properties. + // 2. structured value properties + // + // The former is always separated by a comma, the latter by semi-colon. + if (in_array($name, $this->structuredValues)) { + $this->delimiter = ';'; + } + + parent::__construct($root, $name, $value, $parameters, $group); + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); + } + + /** + * Sets the value as a quoted-printable encoded string. + * + * @param string $val + */ + public function setQuotedPrintableValue($val) + { + $val = quoted_printable_decode($val); + + // Quoted printable only appears in vCard 2.1, and the only character + // that may be escaped there is ;. So we are simply splitting on just + // that. + // + // We also don't have to unescape \\, so all we need to look for is a ; + // that's not preceded with a \. + $regex = '# (?<!\\\\) ; #x'; + $matches = preg_split($regex, $val); + $this->setValue($matches); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + foreach ($val as &$item) { + if (!is_array($item)) { + $item = [$item]; + } + + foreach ($item as &$subItem) { + $subItem = strtr( + $subItem, + [ + '\\' => '\\\\', + ';' => '\;', + ',' => '\,', + "\n" => '\n', + "\r" => '', + ] + ); + } + $item = implode(',', $item); + } + + return implode($this->delimiter, $val); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + // Structured text values should always be returned as a single + // array-item. Multi-value text should be returned as multiple items in + // the top-array. + if (in_array($this->name, $this->structuredValues)) { + return [$this->getParts()]; + } + + return $this->getParts(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'TEXT'; + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + // We need to kick in a special type of encoding, if it's a 2.1 vcard. + if (Document::VCARD21 !== $this->root->getDocumentType()) { + return parent::serialize(); + } + + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = \array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + // Imploding multiple parts into a single value, and splitting the + // values with ;. + if (\count($val) > 1) { + foreach ($val as $k => $v) { + $val[$k] = \str_replace(';', '\;', $v); + } + $val = \implode(';', $val); + } else { + $val = $val[0]; + } + + $str = $this->name; + if ($this->group) { + $str = $this->group.'.'.$this->name; + } + foreach ($this->parameters as $param) { + if ('QUOTED-PRINTABLE' === $param->getValue()) { + continue; + } + $str .= ';'.$param->serialize(); + } + + // If the resulting value contains a \n, we must encode it as + // quoted-printable. + if (false !== \strpos($val, "\n")) { + $str .= ';ENCODING=QUOTED-PRINTABLE:'; + $lastLine = $str; + $out = null; + + // The PHP built-in quoted-printable-encode does not correctly + // encode newlines for us. Specifically, the \r\n sequence must in + // vcards be encoded as =0D=OA and we must insert soft-newlines + // every 75 bytes. + for ($ii = 0; $ii < \strlen($val); ++$ii) { + $ord = \ord($val[$ii]); + // These characters are encoded as themselves. + if ($ord >= 32 && $ord <= 126) { + $lastLine .= $val[$ii]; + } else { + $lastLine .= '='.\strtoupper(\bin2hex($val[$ii])); + } + if (\strlen($lastLine) >= 75) { + // Soft line break + $out .= $lastLine."=\r\n "; + $lastLine = null; + } + } + if (!\is_null($lastLine)) { + $out .= $lastLine."\r\n"; + } + + return $out; + } else { + $str .= ':'.$val; + + $str = \preg_replace( + '/( + (?:^.)? # 1 additional byte in first line because of missing single space (see next line) + .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) + (?![\x80-\xbf]) # prevent splitting multibyte characters + )/x', + "$1\r\n ", + $str + ); + + // remove single space after last CRLF + return \substr($str, 0, -1); + } + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $values = $this->getParts(); + + $map = function ($items) use ($values, $writer) { + foreach ($items as $i => $item) { + $writer->writeElement( + $item, + !empty($values[$i]) ? $values[$i] : null + ); + } + }; + + switch ($this->name) { + // Special-casing the REQUEST-STATUS property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.3 + case 'REQUEST-STATUS': + $writer->writeElement('code', $values[0]); + $writer->writeElement('description', $values[1]); + + if (isset($values[2])) { + $writer->writeElement('data', $values[2]); + } + break; + + case 'N': + $map([ + 'surname', + 'given', + 'additional', + 'prefix', + 'suffix', + ]); + break; + + case 'GENDER': + $map([ + 'sex', + 'text', + ]); + break; + + case 'ADR': + $map([ + 'pobox', + 'ext', + 'street', + 'locality', + 'region', + 'code', + 'country', + ]); + break; + + case 'CLIENTPIDMAP': + $map([ + 'sourceid', + 'uri', + ]); + break; + + default: + parent::xmlSerializeValue($writer); + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = parent::validate($options); + + if (isset($this->minimumPropertyValues[$this->name])) { + $minimum = $this->minimumPropertyValues[$this->name]; + $parts = $this->getParts(); + if (count($parts) < $minimum) { + $warnings[] = [ + 'level' => $options & self::REPAIR ? 1 : 3, + 'message' => 'The '.$this->name.' property must have at least '.$minimum.' values. It only has '.count($parts), + 'node' => $this, + ]; + if ($options & self::REPAIR) { + $parts = array_pad($parts, $minimum, ''); + $this->setParts($parts); + } + } + } + + return $warnings; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Time.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Time.php new file mode 100644 index 0000000..544b5ce --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Time.php @@ -0,0 +1,131 @@ +<?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 + */ + public 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. + */ + public function setJsonValue(array $value) + { + // Removing colons from value. + $value = str_replace( + ':', + '', + $value + ); + + if (1 === count($value)) { + $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 + */ + public function getJsonValue() + { + $parts = DateTimeParser::parseVCardTime($this->getValue()); + $timeStr = ''; + + // Hour + if (!is_null($parts['hour'])) { + $timeStr .= $parts['hour']; + + if (!is_null($parts['minute'])) { + $timeStr .= ':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $timeStr .= '-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $timeStr .= $parts['minute']; + + if (!is_null($parts['second'])) { + $timeStr .= ':'; + } + } else { + if (isset($parts['second'])) { + // Dash for empty minute + $timeStr .= '-'; + } + } + + // Second + if (!is_null($parts['second'])) { + $timeStr .= $parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + if ('Z' === $parts['timezone']) { + $timeStr .= 'Z'; + } else { + $timeStr .= + preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); + } + } + + return [$timeStr]; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map( + function ($value) { + return str_replace(':', '', $value); + }, + $value + ); + parent::setXmlValue($value); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Unknown.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Unknown.php new file mode 100644 index 0000000..6f404c2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Unknown.php @@ -0,0 +1,41 @@ +<?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 + */ + public 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 + */ + public function getValueType() + { + return 'UNKNOWN'; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Uri.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Uri.php new file mode 100644 index 0000000..830cd3f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/Uri.php @@ -0,0 +1,116 @@ +<?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 + */ + public function getValueType() + { + return 'URI'; + } + + /** + * Returns an iterable list of children. + * + * @return array + */ + public 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 + */ + public function setRawMimeDirValue($val) + { + // Normally we don't need to do any type of unescaping for these + // properties, however.. we've noticed that Google Contacts + // specifically escapes the colon (:) with a backslash. While I have + // no clue why they thought that was a good idea, I'm unescaping it + // anyway. + // + // Good thing backslashes are not allowed in urls. Makes it easy to + // assume that a backslash is always intended as an escape character. + if ('URL' === $this->name) { + $regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x'; + $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $newVal = ''; + foreach ($matches as $match) { + switch ($match) { + case '\:': + $newVal .= ':'; + break; + default: + $newVal .= $match; + break; + } + } + $this->value = $newVal; + } else { + $this->value = strtr($val, ['\,' => ',']); + } + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + if (is_array($this->value)) { + $value = $this->value[0]; + } else { + $value = $this->value; + } + + return strtr($value, [',' => '\,']); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/UtcOffset.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/UtcOffset.php new file mode 100644 index 0000000..248ed40 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/UtcOffset.php @@ -0,0 +1,70 @@ +<?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 + */ + public 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. + */ + public 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 + */ + public function getJsonValue() + { + return array_map( + function ($value) { + return substr($value, 0, -2).':'. + substr($value, -2); + }, + parent::getJsonValue() + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/Date.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/Date.php new file mode 100644 index 0000000..fc679d5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/Date.php @@ -0,0 +1,36 @@ +<?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 + */ + public function getValueType() + { + return 'DATE'; + } + + /** + * Sets the property as a DateTime object. + */ + public function setDateTime(\DateTimeInterface $dt) + { + $this->value = $dt->format('Ymd'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php new file mode 100644 index 0000000..09918b3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php @@ -0,0 +1,367 @@ +<?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 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 + */ + public function getValueType() + { + return 'DATE-AND-OR-TIME'; + } + + /** + * Sets a multi-valued property. + * + * You may also specify DateTimeInterface objects here. + */ + public 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 + */ + public function setValue($value) + { + if ($value instanceof DateTimeInterface) { + $this->setDateTime($value); + } else { + parent::setValue($value); + } + } + + /** + * Sets the property as a DateTime object. + */ + public function setDateTime(DateTimeInterface $dt) + { + $tz = $dt->getTimeZone(); + $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']); + + if ($isUtc) { + $value = $dt->format('Ymd\\THis\\Z'); + } else { + // Calculating the offset. + $value = $dt->format('Ymd\\THisO'); + } + + $this->value = $value; + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * If no time was specified, we will always use midnight (in the default + * timezone) as the time. + * + * If parts of the date were omitted, such as the year, we will grab the + * current values for those. So at the time of writing, if the year was + * omitted, we would have filled in 2014. + * + * @return DateTimeImmutable + */ + public function getDateTime() + { + $now = new DateTime(); + + $tzFormat = 0 === $now->getTimezone()->getOffset($now) ? '\\Z' : 'O'; + $nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This'.$tzFormat)); + + $dateParts = DateTimeParser::parseVCardDateTime($this->getValue()); + + // This sets all the missing parts to the current date/time. + // So if the year was missing for a birthday, we're making it 'this + // year'. + foreach ($dateParts as $k => $v) { + if (is_null($v)) { + $dateParts[$k] = $nowParts[$k]; + } + } + + return new DateTimeImmutable("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]"); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $parts = DateTimeParser::parseVCardDateTime($this->getValue()); + + $dateStr = ''; + + // Year + if (!is_null($parts['year'])) { + $dateStr .= $parts['year']; + + if (!is_null($parts['month'])) { + // If a year and a month is set, we need to insert a separator + // dash. + $dateStr .= '-'; + } + } else { + if (!is_null($parts['month']) || !is_null($parts['date'])) { + // Inserting two dashes + $dateStr .= '--'; + } + } + + // Month + if (!is_null($parts['month'])) { + $dateStr .= $parts['month']; + + if (isset($parts['date'])) { + // If month and date are set, we need the separator dash. + $dateStr .= '-'; + } + } elseif (isset($parts['date'])) { + // If the month is empty, and a date is set, we need a 'empty + // dash' + $dateStr .= '-'; + } + + // Date + if (!is_null($parts['date'])) { + $dateStr .= $parts['date']; + } + + // Early exit if we don't have a time string. + if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) { + return [$dateStr]; + } + + $dateStr .= 'T'; + + // Hour + if (!is_null($parts['hour'])) { + $dateStr .= $parts['hour']; + + if (!is_null($parts['minute'])) { + $dateStr .= ':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $dateStr .= '-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $dateStr .= $parts['minute']; + + if (!is_null($parts['second'])) { + $dateStr .= ':'; + } + } elseif (isset($parts['second'])) { + // Dash for empty minute + $dateStr .= '-'; + } + + // Second + if (!is_null($parts['second'])) { + $dateStr .= $parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr .= $parts['timezone']; + } + + return [$dateStr]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + $parts = DateTimeParser::parseVCardDateAndOrTime($this->getValue()); + $value = ''; + + // $d = defined + $d = function ($part) use ($parts) { + return !is_null($parts[$part]); + }; + + // $r = read + $r = function ($part) use ($parts) { + return $parts[$part]; + }; + + // From the Relax NG Schema. + // + // # 4.3.1 + // value-date = element date { + // xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" } + // } + if (($d('year') || $d('month') || $d('date')) + && (!$d('hour') && !$d('minute') && !$d('second') && !$d('timezone'))) { + if ($d('year') && $d('month') && $d('date')) { + $value .= $r('year').$r('month').$r('date'); + } elseif ($d('year') && $d('month') && !$d('date')) { + $value .= $r('year').'-'.$r('month'); + } elseif (!$d('year') && $d('month')) { + $value .= '--'.$r('month').$r('date'); + } elseif (!$d('year') && !$d('month') && $d('date')) { + $value .= '---'.$r('date'); + } + + // # 4.3.2 + // value-time = element time { + // xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d?)|--\d\d)" + // ~ "(Z|[+\-]\d\d(\d\d)?)?" } + // } + } elseif ((!$d('year') && !$d('month') && !$d('date')) + && ($d('hour') || $d('minute') || $d('second'))) { + if ($d('hour')) { + $value .= $r('hour').$r('minute').$r('second'); + } elseif ($d('minute')) { + $value .= '-'.$r('minute').$r('second'); + } elseif ($d('second')) { + $value .= '--'.$r('second'); + } + + $value .= $r('timezone'); + + // # 4.3.3 + // value-date-time = element date-time { + // xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?" + // ~ "(Z|[+\-]\d\d(\d\d)?)?" } + // } + } elseif ($d('date') && $d('hour')) { + if ($d('year') && $d('month') && $d('date')) { + $value .= $r('year').$r('month').$r('date'); + } elseif (!$d('year') && $d('month') && $d('date')) { + $value .= '--'.$r('month').$r('date'); + } elseif (!$d('year') && !$d('month') && $d('date')) { + $value .= '---'.$r('date'); + } + + $value .= 'T'.$r('hour').$r('minute').$r('second'). + $r('timezone'); + } + + $writer->writeElement($valueType, $value); + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $messages = parent::validate($options); + $value = $this->getValue(); + + try { + DateTimeParser::parseVCardDateTime($value); + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property', + 'node' => $this, + ]; + } + + return $messages; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/DateTime.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/DateTime.php new file mode 100644 index 0000000..49c1f35 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/DateTime.php @@ -0,0 +1,28 @@ +<?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 + */ + public function getValueType() + { + return 'DATE-TIME'; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/LanguageTag.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/LanguageTag.php new file mode 100644 index 0000000..6972739 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/LanguageTag.php @@ -0,0 +1,54 @@ +<?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 + */ + public function setRawMimeDirValue($val) + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->getValue(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'LANGUAGE-TAG'; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php new file mode 100644 index 0000000..b714ffd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\VObject\Property\VCard; + +use Sabre\VObject\Property; + +/** + * PhoneNumber property. + * + * This object encodes PHONE-NUMBER values. + * + * @author Christian Kraus <christian@kraus.work> + */ +class PhoneNumber extends Property\Text +{ + protected $structuredValues = []; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'PHONE-NUMBER'; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php new file mode 100644 index 0000000..fccf2d6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php @@ -0,0 +1,81 @@ +<?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 + */ + public 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 + */ + public 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 + */ + 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/plugins/panakour/backup/vendor/sabre/vobject/lib/Reader.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Reader.php new file mode 100644 index 0000000..055d546 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Reader.php @@ -0,0 +1,95 @@ +<?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 + */ + public 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 its input. + * Specifying the array is useful if json_decode was already called on the + * input. + * + * @param string|resource|array $data + * @param int $options + * + * @return Document + */ + public static function readJson($data, $options = 0) + { + $parser = new Parser\Json(); + $result = $parser->parse($data, $options); + + return $result; + } + + /** + * Parses a xCard or xCal object, and returns the top component. + * + * The options argument is a bitfield. Pass any of the OPTIONS constant to + * alter the parsers' behaviour. + * + * You can either supply a string, or a readable stream for input. + * + * @param string|resource $data + * @param int $options + * + * @return Document + */ + public static function readXML($data, $options = 0) + { + $parser = new Parser\XML(); + $result = $parser->parse($data, $options); + + return $result; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/EventIterator.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/EventIterator.php new file mode 100644 index 0000000..fd904b3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/EventIterator.php @@ -0,0 +1,484 @@ +<?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 + */ + public 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 + */ + public function current() + { + if ($this->currentDate) { + return clone $this->currentDate; + } + } + + /** + * This method returns the start date for the current iteration of the + * event. + * + * @return DateTimeImmutable + */ + public function getDtStart() + { + if ($this->currentDate) { + return clone $this->currentDate; + } + } + + /** + * This method returns the end date for the current iteration of the + * event. + * + * @return DateTimeImmutable + */ + public function getDtEnd() + { + if (!$this->valid()) { + return; + } + $end = clone $this->currentDate; + + return $end->modify('+'.$this->eventDuration.' seconds'); + } + + /** + * Returns a VEVENT for the current iterations of the event. + * + * This VEVENT will have a recurrence id, and its DTSTART and DTEND + * altered. + * + * @return VEvent + */ + public function getEventObject() + { + if ($this->currentOverriddenEvent) { + return $this->currentOverriddenEvent; + } + + $event = clone $this->masterEvent; + + // Ignoring the following block, because PHPUnit's code coverage + // ignores most of these lines, and this messes with our stats. + // + // @codeCoverageIgnoreStart + unset( + $event->RRULE, + $event->EXDATE, + $event->RDATE, + $event->EXRULE, + $event->{'RECURRENCE-ID'} + ); + // @codeCoverageIgnoreEnd + + $event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating()); + if (isset($event->DTEND)) { + $event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating()); + } + $recurid = clone $event->DTSTART; + $recurid->name = 'RECURRENCE-ID'; + $event->add($recurid); + + return $event; + } + + /** + * Returns the current position of the iterator. + * + * This is for us simply a 0-based index. + * + * @return int + */ + public function key() + { + // The counter is always 1 ahead. + return $this->counter - 1; + } + + /** + * This is called after next, to see if the iterator is still at a valid + * position, or if it's at the end. + * + * @return bool + */ + public function valid() + { + if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) { + throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences); + } + + return (bool) $this->currentDate; + } + + /** + * Sets the iterator back to the starting point. + */ + public function rewind() + { + $this->recurIterator->rewind(); + // re-creating overridden event index. + $index = []; + foreach ($this->overriddenEvents as $key => $event) { + $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp(); + $index[$stamp][] = $key; + } + krsort($index); + $this->counter = 0; + $this->overriddenEventsIndex = $index; + $this->currentOverriddenEvent = null; + + $this->nextDate = null; + $this->currentDate = clone $this->startDate; + + $this->next(); + } + + /** + * Advances the iterator with one step. + */ + public function next() + { + $this->currentOverriddenEvent = null; + ++$this->counter; + if ($this->nextDate) { + // We had a stored value. + $nextDate = $this->nextDate; + $this->nextDate = null; + } else { + // We need to ask rruleparser for the next date. + // We need to do this until we find a date that's not in the + // exception list. + do { + if (!$this->recurIterator->valid()) { + $nextDate = null; + break; + } + $nextDate = $this->recurIterator->current(); + $this->recurIterator->next(); + } while (isset($this->exceptions[$nextDate->getTimeStamp()])); + } + + // $nextDate now contains what rrule thinks is the next one, but an + // overridden event may cut ahead. + if ($this->overriddenEventsIndex) { + $offsets = end($this->overriddenEventsIndex); + $timestamp = key($this->overriddenEventsIndex); + $offset = end($offsets); + if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) { + // Overridden event comes first. + $this->currentOverriddenEvent = $this->overriddenEvents[$offset]; + + // Putting the rrule next date aside. + $this->nextDate = $nextDate; + $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone); + + // Ensuring that this item will only be used once. + array_pop($this->overriddenEventsIndex[$timestamp]); + if (!$this->overriddenEventsIndex[$timestamp]) { + array_pop($this->overriddenEventsIndex); + } + + // Exit point! + return; + } + } + + $this->currentDate = $nextDate; + } + + /** + * Quickly jump to a date in the future. + */ + public function fastForward(DateTimeInterface $dateTime) + { + while ($this->valid() && $this->getDtEnd() <= $dateTime) { + $this->next(); + } + } + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return $this->recurIterator->isInfinite(); + } + + /** + * RRULE parser. + * + * @var RRuleIterator + */ + protected $recurIterator; + + /** + * The duration, in seconds, of the master event. + * + * We use this to calculate the DTEND for subsequent events. + */ + protected $eventDuration; + + /** + * A reference to the main (master) event. + * + * @var VEVENT + */ + protected $masterEvent; + + /** + * List of overridden events. + * + * @var array + */ + protected $overriddenEvents = []; + + /** + * Overridden event index. + * + * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent + * property. + * + * @var array + */ + protected $overriddenEventsIndex; + + /** + * A list of recurrence-id's that are either part of EXDATE, or are + * overridden. + * + * @var array + */ + protected $exceptions = []; + + /** + * Internal event counter. + * + * @var int + */ + protected $counter; + + /** + * The very start of the iteration process. + * + * @var DateTimeImmutable + */ + protected $startDate; + + /** + * Where we are currently in the iteration process. + * + * @var DateTimeImmutable + */ + protected $currentDate; + + /** + * The next date from the rrule parser. + * + * Sometimes we need to temporary store the next date, because an + * overridden event came before. + * + * @var DateTimeImmutable + */ + protected $nextDate; + + /** + * The event that overwrites the current iteration. + * + * @var VEVENT + */ + protected $currentOverriddenEvent; +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php new file mode 100644 index 0000000..cb08358 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php @@ -0,0 +1,17 @@ +<?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/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/NoInstancesException.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/NoInstancesException.php new file mode 100644 index 0000000..348c023 --- /dev/null +++ b/plugins/panakour/backup/vendor/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 occurrence 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/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/RDateIterator.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/RDateIterator.php new file mode 100644 index 0000000..d117e15 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/RDateIterator.php @@ -0,0 +1,166 @@ +<?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 + */ + public function __construct($rrule, DateTimeInterface $start) + { + $this->startDate = $start; + $this->parseRDate($rrule); + $this->currentDate = clone $this->startDate; + } + + /* Implementation of the Iterator interface {{{ */ + + public function current() + { + if (!$this->valid()) { + return; + } + + return clone $this->currentDate; + } + + /** + * Returns the current item number. + * + * @return int + */ + public function key() + { + return $this->counter; + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. + * + * @return bool + */ + public function valid() + { + return $this->counter <= count($this->dates); + } + + /** + * Resets the iterator. + */ + public function rewind() + { + $this->currentDate = clone $this->startDate; + $this->counter = 0; + } + + /** + * Goes on to the next iteration. + */ + public function next() + { + ++$this->counter; + if (!$this->valid()) { + return; + } + + $this->currentDate = + DateTimeParser::parse( + $this->dates[$this->counter - 1], + $this->startDate->getTimezone() + ); + } + + /* End of Iterator implementation }}} */ + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return false; + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + */ + public function fastForward(DateTimeInterface $dt) + { + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * The reference start date/time for the rrule. + * + * All calculations are based on this initial date. + * + * @var DateTimeInterface + */ + protected $startDate; + + /** + * The date of the current iteration. You can get this by calling + * ->current(). + * + * @var DateTimeInterface + */ + protected $currentDate; + + /** + * The current item in the list. + * + * You can get this number with the key() method. + * + * @var int + */ + protected $counter = 0; + + /* }}} */ + + /** + * This method receives a string from an RRULE property, and populates this + * class with all the values. + * + * @param string|array $rrule + */ + protected function parseRDate($rdate) + { + if (is_string($rdate)) { + $rdate = explode(',', $rdate); + } + + $this->dates = $rdate; + } + + /** + * Array with the RRULE dates. + * + * @var array + */ + protected $dates = []; +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/RRuleIterator.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/RRuleIterator.php new file mode 100644 index 0000000..55581e9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Recur/RRuleIterator.php @@ -0,0 +1,973 @@ +<?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 + */ + public function __construct($rrule, DateTimeInterface $start) + { + $this->startDate = $start; + $this->parseRRule($rrule); + $this->currentDate = clone $this->startDate; + } + + /* Implementation of the Iterator interface {{{ */ + + public function current() + { + if (!$this->valid()) { + return; + } + + return clone $this->currentDate; + } + + /** + * Returns the current item number. + * + * @return int + */ + public function key() + { + return $this->counter; + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. This will return false if we've gone beyond the UNTIL or COUNT + * statements. + * + * @return bool + */ + public function valid() + { + if (null === $this->currentDate) { + return false; + } + if (!is_null($this->count)) { + return $this->counter < $this->count; + } + + return is_null($this->until) || $this->currentDate <= $this->until; + } + + /** + * Resets the iterator. + */ + public function rewind() + { + $this->currentDate = clone $this->startDate; + $this->counter = 0; + } + + /** + * Goes on to the next iteration. + */ + public function next() + { + // Otherwise, we find the next event in the normal RRULE + // sequence. + switch ($this->frequency) { + case 'hourly': + $this->nextHourly(); + break; + + case 'daily': + $this->nextDaily(); + break; + + case 'weekly': + $this->nextWeekly(); + break; + + case 'monthly': + $this->nextMonthly(); + break; + + case 'yearly': + $this->nextYearly(); + break; + } + ++$this->counter; + } + + /* End of Iterator implementation }}} */ + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return !$this->count && !$this->until; + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + */ + public function fastForward(DateTimeInterface $dt) + { + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * The reference start date/time for the rrule. + * + * All calculations are based on this initial date. + * + * @var DateTimeInterface + */ + protected $startDate; + + /** + * The date of the current iteration. You can get this by calling + * ->current(). + * + * @var DateTimeInterface + */ + protected $currentDate; + + /** + * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, + * yearly. + * + * @var string + */ + protected $frequency; + + /** + * The number of recurrences, or 'null' if infinitely recurring. + * + * @var int + */ + protected $count; + + /** + * The interval. + * + * If for example frequency is set to daily, interval = 2 would mean every + * 2 days. + * + * @var int + */ + protected $interval = 1; + + /** + * The last instance of this recurrence, inclusively. + * + * @var DateTimeInterface|null + */ + protected $until; + + /** + * Which seconds to recur. + * + * This is an array of integers (between 0 and 60) + * + * @var array + */ + protected $bySecond; + + /** + * Which minutes to recur. + * + * This is an array of integers (between 0 and 59) + * + * @var array + */ + protected $byMinute; + + /** + * Which hours to recur. + * + * This is an array of integers (between 0 and 23) + * + * @var array + */ + protected $byHour; + + /** + * The current item in the list. + * + * You can get this number with the key() method. + * + * @var int + */ + protected $counter = 0; + + /** + * Which weekdays to recur. + * + * This is an array of weekdays + * + * This may also be preceded by a positive or negative integer. If present, + * this indicates the nth occurrence of a specific day within the monthly or + * yearly rrule. For instance, -2TU indicates the second-last tuesday of + * the month, or year. + * + * @var array + */ + protected $byDay; + + /** + * Which days of the month to recur. + * + * This is an array of days of the months (1-31). The value can also be + * negative. -5 for instance means the 5th last day of the month. + * + * @var array + */ + protected $byMonthDay; + + /** + * Which days of the year to recur. + * + * This is an array with days of the year (1 to 366). The values can also + * be negative. For instance, -1 will always represent the last day of the + * year. (December 31st). + * + * @var array + */ + protected $byYearDay; + + /** + * Which week numbers to recur. + * + * This is an array of integers from 1 to 53. The values can also be + * negative. -1 will always refer to the last week of the year. + * + * @var array + */ + protected $byWeekNo; + + /** + * Which months to recur. + * + * This is an array of integers from 1 to 12. + * + * @var array + */ + protected $byMonth; + + /** + * Which items in an existing st to recur. + * + * These numbers work together with an existing by* rule. It specifies + * exactly which items of the existing by-rule to filter. + * + * Valid values are 1 to 366 and -1 to -366. As an example, this can be + * used to recur the last workday of the month. + * + * This would be done by setting frequency to 'monthly', byDay to + * 'MO,TU,WE,TH,FR' and bySetPos to -1. + * + * @var array + */ + protected $bySetPos; + + /** + * When the week starts. + * + * @var string + */ + protected $weekStart = 'MO'; + + /* Functions that advance the iterator {{{ */ + + /** + * Does the processing for advancing the iterator for hourly frequency. + */ + protected function nextHourly() + { + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + } + + /** + * Does the processing for advancing the iterator for daily frequency. + */ + protected function nextDaily() + { + if (!$this->byHour && !$this->byDay) { + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + + return; + } + + $recurrenceHours = []; + if (!empty($this->byHour)) { + $recurrenceHours = $this->getHours(); + } + + $recurrenceDays = []; + if (!empty($this->byDay)) { + $recurrenceDays = $this->getDays(); + } + + $recurrenceMonths = []; + if (!empty($this->byMonth)) { + $recurrenceMonths = $this->getMonths(); + } + + do { + if ($this->byHour) { + if ('23' == $this->currentDate->format('G')) { + // to obey the interval rule + $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days'); + } + + $this->currentDate = $this->currentDate->modify('+1 hours'); + } else { + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + } + + // Current month of the year + $currentMonth = $this->currentDate->format('n'); + + // Current day of the week + $currentDay = $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = $this->currentDate->format('G'); + } while ( + ($this->byDay && !in_array($currentDay, $recurrenceDays)) || + ($this->byHour && !in_array($currentHour, $recurrenceHours)) || + ($this->byMonth && !in_array($currentMonth, $recurrenceMonths)) + ); + } + + /** + * Does the processing for advancing the iterator for weekly frequency. + */ + protected function nextWeekly() + { + if (!$this->byHour && !$this->byDay) { + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks'); + + return; + } + + $recurrenceHours = []; + if ($this->byHour) { + $recurrenceHours = $this->getHours(); + } + + $recurrenceDays = []; + if ($this->byDay) { + $recurrenceDays = $this->getDays(); + } + + // First day of the week: + $firstDay = $this->dayMap[$this->weekStart]; + + do { + if ($this->byHour) { + $this->currentDate = $this->currentDate->modify('+1 hours'); + } else { + $this->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 || '0' == $currentHour)) { + $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks'); + + // We need to go to the first day of this week, but only if we + // are not already on this first day of this week. + if ($this->currentDate->format('w') != $firstDay) { + $this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]); + } + } + + // We have a match + } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); + } + + /** + * Does the processing for advancing the iterator for monthly frequency. + */ + protected function nextMonthly() + { + $currentDayOfMonth = $this->currentDate->format('j'); + if (!$this->byMonthDay && !$this->byDay) { + // If the current day is higher than the 28th, rollover can + // occur to the next month. We Must skip these invalid + // entries. + if ($currentDayOfMonth < 29) { + $this->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; + } + + $occurrence = -1; + while (true) { + $occurrences = $this->getMonthlyOccurrences(); + + foreach ($occurrences as $occurrence) { + // The first occurrence thats higher than the current + // day of the month wins. + if ($occurrence > $currentDayOfMonth) { + break 2; + } + } + + // If we made it all the way here, it means there were no + // valid occurrences, and we need to advance to the next + // month. + // + // This line does not currently work in hhvm. Temporary workaround + // follows: + // $this->currentDate->modify('first day of this month'); + $this->currentDate = new DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone()); + // end of workaround + $this->currentDate = $this->currentDate->modify('+ '.$this->interval.' months'); + + // This goes to 0 because we need to start counting at the + // beginning. + $currentDayOfMonth = 0; + + // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply + // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... + if ($this->currentDate->getTimestamp() > 253402300799) { + $this->currentDate = null; + + return; + } + } + + $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. + */ + protected function nextYearly() + { + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + // No sub-rules, so we just advance by year + if (empty($this->byMonth)) { + // Unless it was a leap day! + if (2 == $currentMonth && 29 == $currentDayOfMonth) { + $counter = 0; + do { + ++$counter; + // Here we increase the year count by the interval, until + // we hit a date that's also in a leap year. + // + // We could just find the next interval that's dividable by + // 4, but that would ignore the rule that there's no leap + // year every year that's dividable by a 100, but not by + // 400. (1800, 1900, 2100). So we just rely on the datetime + // functions instead. + $nextDate = clone $this->currentDate; + $nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years'); + } while (2 != $nextDate->format('n')); + + $this->currentDate = $nextDate; + + return; + } + + if (null !== $this->byWeekNo) { // byWeekNo is an array with values from -53 to -1, or 1 to 53 + $dayOffsets = []; + if ($this->byDay) { + foreach ($this->byDay as $byDay) { + $dayOffsets[] = $this->dayMap[$byDay]; + } + } else { // default is Monday + $dayOffsets[] = 1; + } + + $currentYear = $this->currentDate->format('Y'); + + while (true) { + $checkDates = []; + + // loop through all WeekNo and Days to check all the combinations + foreach ($this->byWeekNo as $byWeekNo) { + foreach ($dayOffsets as $dayOffset) { + $date = clone $this->currentDate; + $date->setISODate($currentYear, $byWeekNo, $dayOffset); + + if ($date > $this->currentDate) { + $checkDates[] = $date; + } + } + } + + if (count($checkDates) > 0) { + $this->currentDate = min($checkDates); + + return; + } + + // if there is no date found, check the next year + $currentYear += $this->interval; + } + } + + if (null !== $this->byYearDay) { // byYearDay is an array with values from -366 to -1, or 1 to 366 + $dayOffsets = []; + if ($this->byDay) { + foreach ($this->byDay as $byDay) { + $dayOffsets[] = $this->dayMap[$byDay]; + } + } else { // default is Monday-Sunday + $dayOffsets = [1, 2, 3, 4, 5, 6, 7]; + } + + $currentYear = $this->currentDate->format('Y'); + + while (true) { + $checkDates = []; + + // loop through all YearDay and Days to check all the combinations + foreach ($this->byYearDay as $byYearDay) { + $date = clone $this->currentDate; + $date = $date->setDate($currentYear, 1, 1); + if ($byYearDay > 0) { + $date = $date->add(new \DateInterval('P'.$byYearDay.'D')); + } else { + $date = $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) { + $occurrence = -1; + while (true) { + $occurrences = $this->getMonthlyOccurrences(); + + foreach ($occurrences as $occurrence) { + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { + 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 + */ + protected function parseRRule($rrule) + { + if (is_string($rrule)) { + $rrule = Property\ICalendar\Recur::stringToArray($rrule); + } + + foreach ($rrule as $key => $value) { + $key = strtoupper($key); + switch ($key) { + case 'FREQ': + $value = strtolower($value); + if (!in_array( + $value, + ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'] + )) { + throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value)); + } + $this->frequency = $value; + break; + + case 'UNTIL': + $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone()); + + // In some cases events are generated with an UNTIL= + // parameter before the actual start of the event. + // + // Not sure why this is happening. We assume that the + // intention was that the event only recurs once. + // + // So we are modifying the parameter so our code doesn't + // break. + if ($this->until < $this->startDate) { + $this->until = $this->startDate; + } + break; + + case 'INTERVAL': + + case 'COUNT': + $val = (int) $value; + if ($val < 1) { + throw new InvalidDataException(strtoupper($key).' in RRULE must be a positive integer!'); + } + $key = strtolower($key); + $this->$key = $val; + break; + + case 'BYSECOND': + $this->bySecond = (array) $value; + break; + + case 'BYMINUTE': + $this->byMinute = (array) $value; + break; + + case 'BYHOUR': + $this->byHour = (array) $value; + break; + + case 'BYDAY': + $value = (array) $value; + foreach ($value as $part) { + if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) { + throw new InvalidDataException('Invalid part in BYDAY clause: '.$part); + } + } + $this->byDay = $value; + break; + + case 'BYMONTHDAY': + $this->byMonthDay = (array) $value; + break; + + case 'BYYEARDAY': + $this->byYearDay = (array) $value; + foreach ($this->byYearDay as $byYearDay) { + if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 == (int) $byYearDay || (int) $byYearDay > 366) { + throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!'); + } + } + break; + + case 'BYWEEKNO': + $this->byWeekNo = (array) $value; + foreach ($this->byWeekNo as $byWeekNo) { + if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 == (int) $byWeekNo || (int) $byWeekNo > 53) { + throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!'); + } + } + break; + + case 'BYMONTH': + $this->byMonth = (array) $value; + foreach ($this->byMonth as $byMonth) { + if (!is_numeric($byMonth) || (int) $byMonth < 1 || (int) $byMonth > 12) { + throw new InvalidDataException('BYMONTH in RRULE must have value(s) between 1 and 12!'); + } + } + break; + + case 'BYSETPOS': + $this->bySetPos = (array) $value; + break; + + case 'WKST': + $this->weekStart = strtoupper($value); + break; + + default: + throw new InvalidDataException('Not supported: '.strtoupper($key)); + } + } + } + + /** + * Mappings between the day number and english day name. + * + * @var array + */ + protected $dayNames = [ + 0 => 'Sunday', + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + ]; + + /** + * Returns all the occurrences for a monthly frequency with a 'byDay' or + * 'byMonthDay' expansion for the current month. + * + * The returned list is an array of integers with the day of month (1-31). + * + * @return array + */ + protected function getMonthlyOccurrences() + { + $startDate = clone $this->currentDate; + + $byDayResults = []; + + // Our strategy is to simply go through the byDays, advance the date to + // that point and add it to the results. + if ($this->byDay) { + foreach ($this->byDay as $day) { + $dayName = $this->dayNames[$this->dayMap[substr($day, -2)]]; + + // Dayname will be something like 'wednesday'. Now we need to find + // all wednesdays in this month. + $dayHits = []; + + // workaround for missing 'first day of the month' support in hhvm + $checkDate = new \DateTime($startDate->format('Y-m-1')); + // workaround modify always advancing the date even if the current day is a $dayName in hhvm + if ($checkDate->format('l') !== $dayName) { + $checkDate = $checkDate->modify($dayName); + } + + do { + $dayHits[] = $checkDate->format('j'); + $checkDate = $checkDate->modify('next '.$dayName); + } while ($checkDate->format('n') === $startDate->format('n')); + + // So now we have 'all wednesdays' for month. It is however + // possible that the user only really wanted the 1st, 2nd or last + // wednesday. + if (strlen($day) > 2) { + $offset = (int) substr($day, 0, -2); + + if ($offset > 0) { + // It is possible that the day does not exist, such as a + // 5th or 6th wednesday of the month. + if (isset($dayHits[$offset - 1])) { + $byDayResults[] = $dayHits[$offset - 1]; + } + } else { + // if it was negative we count from the end of the array + // might not exist, fx. -5th tuesday + if (isset($dayHits[count($dayHits) + $offset])) { + $byDayResults[] = $dayHits[count($dayHits) + $offset]; + } + } + } else { + // There was no counter (first, second, last wednesdays), so we + // just need to add the all to the list). + $byDayResults = array_merge($byDayResults, $dayHits); + } + } + } + + $byMonthDayResults = []; + if ($this->byMonthDay) { + foreach ($this->byMonthDay as $monthDay) { + // Removing values that are out of range for this month + if ($monthDay > $startDate->format('t') || + $monthDay < 0 - $startDate->format('t')) { + continue; + } + if ($monthDay > 0) { + $byMonthDayResults[] = $monthDay; + } else { + // Negative values + $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; + } + } + } + + // If there was just byDay or just byMonthDay, they just specify our + // (almost) final list. If both were provided, then byDay limits the + // list. + if ($this->byMonthDay && $this->byDay) { + $result = array_intersect($byMonthDayResults, $byDayResults); + } elseif ($this->byMonthDay) { + $result = $byMonthDayResults; + } else { + $result = $byDayResults; + } + $result = array_unique($result); + sort($result, SORT_NUMERIC); + + // The last thing that needs checking is the BYSETPOS. If it's set, it + // means only certain items in the set survive the filter. + if (!$this->bySetPos) { + return $result; + } + + $filteredResult = []; + foreach ($this->bySetPos as $setPos) { + if ($setPos < 0) { + $setPos = count($result) + ($setPos + 1); + } + if (isset($result[$setPos - 1])) { + $filteredResult[] = $result[$setPos - 1]; + } + } + + sort($filteredResult, SORT_NUMERIC); + + return $filteredResult; + } + + /** + * Simple mapping from iCalendar day names to day numbers. + * + * @var array + */ + protected $dayMap = [ + 'SU' => 0, + 'MO' => 1, + 'TU' => 2, + 'WE' => 3, + 'TH' => 4, + 'FR' => 5, + 'SA' => 6, + ]; + + protected function getHours() + { + $recurrenceHours = []; + foreach ($this->byHour as $byHour) { + $recurrenceHours[] = $byHour; + } + + return $recurrenceHours; + } + + protected function getDays() + { + $recurrenceDays = []; + foreach ($this->byDay as $byDay) { + // The day may be preceded with a positive (+n) or + // negative (-n) integer. However, this does not make + // sense in 'weekly' so we ignore it here. + $recurrenceDays[] = $this->dayMap[substr($byDay, -2)]; + } + + return $recurrenceDays; + } + + protected function getMonths() + { + $recurrenceMonths = []; + foreach ($this->byMonth as $byMonth) { + $recurrenceMonths[] = $byMonth; + } + + return $recurrenceMonths; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Settings.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Settings.php new file mode 100644 index 0000000..b0bb80a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Settings.php @@ -0,0 +1,55 @@ +<?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. + */ + public 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. + */ + public 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 daily event that goes on for 10 + * years, which is hopefully long enough for most. + * + * Set this value to -1 to disable this control altogether. + */ + public static $maxRecurrences = 3500; +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/ICalendar.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/ICalendar.php new file mode 100644 index 0000000..d425661 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/ICalendar.php @@ -0,0 +1,106 @@ +<?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 its input. + * + * @param resource $input + * @param int $options parser options, see the OPTIONS constants + */ + public 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 ('VTIMEZONE' === $component->name) { + $this->vtimezones[(string) $component->TZID] = $component; + continue; + } + + // Get component UID for recurring Events search + if (!$component->UID) { + $component->UID = sha1(microtime()).'-vobjectimport'; + } + $uid = (string) $component->UID; + + // Take care of recurring events + if (!array_key_exists($uid, $this->objects)) { + $this->objects[$uid] = new VCalendar(); + } + + $this->objects[$uid]->add(clone $component); + } + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return \Sabre\VObject\Component|null + */ + public function getNext() + { + if ($object = array_shift($this->objects)) { + // create our baseobject + $object->version = '2.0'; + $object->prodid = '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN'; + $object->calscale = 'GREGORIAN'; + + // add vtimezone information to obj (if we have it) + foreach ($this->vtimezones as $vtimezone) { + $object->add($vtimezone); + } + + return $object; + } else { + return; + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php new file mode 100644 index 0000000..c845ac5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php @@ -0,0 +1,38 @@ +<?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 its input. + * + * @param resource $input + */ + public 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 + */ + public function getNext(); +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/VCard.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/VCard.php new file mode 100644 index 0000000..a20f5c2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Splitter/VCard.php @@ -0,0 +1,74 @@ +<?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 its input. + * + * @param resource $input + * @param int $options parser options, see the OPTIONS constants + */ + public 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 + */ + public function getNext() + { + try { + $object = $this->parser->parse(); + + if (!$object instanceof VObject\Component\VCard) { + throw new VObject\ParseException('The supplied input contained non-VCARD data.'); + } + } catch (VObject\EofException $e) { + return; + } + + return $object; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/StringUtil.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/StringUtil.php new file mode 100644 index 0000000..2333d6a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/StringUtil.php @@ -0,0 +1,62 @@ +<?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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/lib/TimeZoneUtil.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/TimeZoneUtil.php new file mode 100644 index 0000000..2c407fe --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/TimeZoneUtil.php @@ -0,0 +1,265 @@ +<?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 +{ + public static $map = null; + + /** + * List of microsoft exchange timezone ids. + * + * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx + */ + public static $microsoftExchangeMap = [ + 0 => 'UTC', + 31 => 'Africa/Casablanca', + + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding.. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Luanda', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Harare', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Kuwait', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Muscat', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Calcutta', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Rangoon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Godthab', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Midway', + 39 => 'Pacific/Kwajalein', + ]; + + /** + * This method will 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 + */ + public 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 successfully 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 ('SystemV/' === substr($lic, 0, 8)) { + $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 (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { + 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. + */ + public static function loadTzMaps() + { + if (!is_null(self::$map)) { + return; + } + + self::$map = array_merge( + include __DIR__.'/timezonedata/windowszones.php', + include __DIR__.'/timezonedata/lotuszones.php', + include __DIR__.'/timezonedata/exchangezones.php', + include __DIR__.'/timezonedata/php-workaround.php' + ); + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + */ + public static function getIdentifiersBC() + { + return include __DIR__.'/timezonedata/php-bc.php'; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/UUIDUtil.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/UUIDUtil.php new file mode 100644 index 0000000..066af62 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/UUIDUtil.php @@ -0,0 +1,66 @@ +<?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 + */ + public 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 + */ + public static function validateUUID($uuid) + { + return 0 !== preg_match( + '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', + $uuid + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/VCardConverter.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/VCardConverter.php new file mode 100644 index 0000000..04932fe --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/VCardConverter.php @@ -0,0 +1,416 @@ +<?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 int $targetVersion + */ + public 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 = Document::VCARD40 === $targetVersion ? '4.0' : '3.0'; + + $output = new Component\VCard([ + 'VERSION' => $newVersion, + ]); + + // We might have generated a default UID. Remove it! + unset($output->UID); + + foreach ($input->children() as $property) { + $this->convertProperty($input, $output, $property, $targetVersion); + } + + return $output; + } + + /** + * Handles conversion of a single property. + * + * @param int $targetVersion + */ + protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) + { + // Skipping these, those are automatically added. + if (in_array($property->name, ['VERSION', 'PRODID'])) { + return; + } + + $parameters = $property->parameters(); + $valueType = null; + if (isset($parameters['VALUE'])) { + $valueType = $parameters['VALUE']->getValue(); + unset($parameters['VALUE']); + } + if (!$valueType) { + $valueType = $property->getValueType(); + } + if (Document::VCARD30 !== $targetVersion && 'PHONE-NUMBER' === $valueType) { + $valueType = null; + } + $newProperty = $output->createProperty( + $property->name, + $property->getParts(), + [], // parameters will get added a bit later. + $valueType + ); + + if (Document::VCARD30 === $targetVersion) { + if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { + $newProperty = $this->convertUriToBinary($output, $newProperty); + } elseif ($property instanceof Property\VCard\DateAndOrTime) { + // In vCard 4, the birth year may be optional. This is not the + // case for vCard 3. Apple has a workaround for this that + // allows applications that support Apple's extension still + // omit birthyears in vCard 3, but applications that do not + // support this, will just use a random birthyear. We're + // choosing 1604 for the birthyear, because that's what apple + // uses. + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); + if (is_null($parts['year'])) { + $newValue = '1604-'.$parts['month'].'-'.$parts['date']; + $newProperty->setValue($newValue); + $newProperty['X-APPLE-OMIT-YEAR'] = '1604'; + } + + if ('ANNIVERSARY' == $newProperty->name) { + // Microsoft non-standard anniversary + $newProperty->name = 'X-ANNIVERSARY'; + + // We also need to add a new apple property for the same + // purpose. This apple property needs a 'label' in the same + // group, so we first need to find a groupname that doesn't + // exist yet. + $x = 1; + while ($output->select('ITEM'.$x.'.')) { + ++$x; + } + $output->add('ITEM'.$x.'.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']); + $output->add('ITEM'.$x.'.X-ABLABEL', '_$!<Anniversary>!$_'); + } + } elseif ('KIND' === $property->name) { + switch (strtolower($property->getValue())) { + case 'org': + // vCard 3.0 does not have an equivalent to KIND:ORG, + // but apple has an extension that means the same + // thing. + $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY'); + break; + + case 'individual': + // Individual is implicit, so we skip it. + return; + + case 'group': + // OS X addressbook property + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP'); + break; + } + } + } elseif (Document::VCARD40 === $targetVersion) { + // These properties were removed in vCard 4.0 + if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) { + return; + } + + if ($property instanceof Property\Binary) { + $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); + } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { + // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', + // then we're stripping the year from the vcard 4 value. + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); + if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) { + $newValue = '--'.$parts['month'].'-'.$parts['date']; + $newProperty->setValue($newValue); + } + + // Regardless if the year matched or not, we do need to strip + // X-APPLE-OMIT-YEAR. + unset($parameters['X-APPLE-OMIT-YEAR']); + } + switch ($property->name) { + case 'X-ABSHOWAS': + if ('COMPANY' === strtoupper($property->getValue())) { + $newProperty = $output->createProperty('KIND', 'ORG'); + } + break; + case 'X-ADDRESSBOOKSERVER-KIND': + if ('GROUP' === strtoupper($property->getValue())) { + $newProperty = $output->createProperty('KIND', 'GROUP'); + } + break; + case 'X-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 || '_$!<Anniversary>!$_' !== $label->getValue()) { + break; + } + + // If we already have an anniversary property with the same + // value, ignore. + foreach ($output->select('ANNIVERSARY') as $anniversary) { + if ($anniversary->getValue() === $newProperty->getValue()) { + return; + } + } + $newProperty->name = 'ANNIVERSARY'; + break; + // Apple's per-property label system. + case 'X-ABLABEL': + if ('_$!<Anniversary>!$_' === $newProperty->getValue()) { + // We can safely remove these, as they are converted to + // ANNIVERSARY properties. + return; + } + break; + } + } + + // set property group + $newProperty->group = $property->group; + + if (Document::VCARD40 === $targetVersion) { + $this->convertParameters40($newProperty, $parameters); + } else { + $this->convertParameters30($newProperty, $parameters); + } + + // Lastly, we need to see if there's a need for a VALUE parameter. + // + // We can do that by instantiating a empty property with that name, and + // seeing if the default valueType is identical to the current one. + $tempProperty = $output->createProperty($newProperty->name); + if ($tempProperty->getValueType() !== $newProperty->getValueType()) { + $newProperty['VALUE'] = $newProperty->getValueType(); + } + + $output->add($newProperty); + } + + /** + * Converts a BINARY property to a URI property. + * + * vCard 4.0 no longer supports BINARY properties. + * + * @param Property\Uri $property the input property + * @param $parameters list of parameters that will eventually be added to + * the new property + * + * @return Property\Uri + */ + protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters) + { + $value = $newProperty->getValue(); + $newProperty = $output->createProperty( + $newProperty->name, + null, // no value + [], // no parameters yet + 'URI' // Forcing the BINARY type + ); + + $mimeType = 'application/octet-stream'; + + // See if we can find a better mimetype. + if (isset($parameters['TYPE'])) { + $newTypes = []; + foreach ($parameters['TYPE']->getParts() as $typePart) { + if (in_array( + strtoupper($typePart), + ['JPEG', 'PNG', 'GIF'] + )) { + $mimeType = 'image/'.strtolower($typePart); + } else { + $newTypes[] = $typePart; + } + } + + // If there were any parameters we're not converting to a + // mime-type, we need to keep them. + if ($newTypes) { + $parameters['TYPE']->setParts($newTypes); + } else { + unset($parameters['TYPE']); + } + } + + $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value)); + + return $newProperty; + } + + /** + * Converts a URI property to a BINARY property. + * + * In vCard 4.0 attachments are encoded as data: uri. Even though these may + * be valid in vCard 3.0 as well, we should convert those to BINARY if + * possible, to improve compatibility. + * + * @param Property\Uri $property the input property + * + * @return Property\Binary|null + */ + protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty) + { + $value = $newProperty->getValue(); + + // Only converting data: uris + if ('data:' !== substr($value, 0, 5)) { + return $newProperty; + } + + $newProperty = $output->createProperty( + $newProperty->name, + null, // no value + [], // no parameters yet + 'BINARY' + ); + + $mimeType = substr($value, 5, strpos($value, ',') - 5); + if (strpos($mimeType, ';')) { + $mimeType = substr($mimeType, 0, strpos($mimeType, ';')); + $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1))); + } else { + $newProperty->setValue(substr($value, strpos($value, ',') + 1)); + } + unset($value); + + $newProperty['ENCODING'] = 'b'; + switch ($mimeType) { + case 'image/jpeg': + $newProperty['TYPE'] = 'JPEG'; + break; + case 'image/png': + $newProperty['TYPE'] = 'PNG'; + break; + case 'image/gif': + $newProperty['TYPE'] = 'GIF'; + break; + } + + return $newProperty; + } + + /** + * Adds parameters to a new property for vCard 4.0. + */ + protected function convertParameters40(Property $newProperty, array $parameters) + { + // Adding all parameters. + foreach ($parameters as $param) { + // vCard 2.1 allowed parameters with no name + if ($param->noName) { + $param->noName = false; + } + + switch ($param->name) { + // We need to see if there's any TYPE=PREF, because in vCard 4 + // that's now PREF=1. + case 'TYPE': + foreach ($param->getParts() as $paramPart) { + if ('PREF' === strtoupper($paramPart)) { + $newProperty->add('PREF', '1'); + } else { + $newProperty->add($param->name, $paramPart); + } + } + break; + // These no longer exist in vCard 4 + case 'ENCODING': + case 'CHARSET': + break; + + default: + $newProperty->add($param->name, $param->getParts()); + break; + } + } + } + + /** + * Adds parameters to a new property for vCard 3.0. + */ + protected function convertParameters30(Property $newProperty, array $parameters) + { + // Adding all parameters. + foreach ($parameters as $param) { + // vCard 2.1 allowed parameters with no name + if ($param->noName) { + $param->noName = false; + } + + switch ($param->name) { + case 'ENCODING': + // This value only existed in vCard 2.1, and should be + // removed for anything else. + if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) { + $newProperty->add($param->name, $param->getParts()); + } + break; + + /* + * Converting PREF=1 to TYPE=PREF. + * + * Any other PREF numbers we'll drop. + */ + case 'PREF': + if ('1' == $param->getValue()) { + $newProperty->add('TYPE', 'PREF'); + } + break; + + default: + $newProperty->add($param->name, $param->getParts()); + break; + } + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Version.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Version.php new file mode 100644 index 0000000..883d202 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Version.php @@ -0,0 +1,18 @@ +<?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.3.0'; +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/Writer.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/Writer.php new file mode 100644 index 0000000..cbd2202 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/Writer.php @@ -0,0 +1,68 @@ +<?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. + * + * @return string + */ + public static function write(Component $component) + { + return $component->serialize(); + } + + /** + * Serializes a jCal or jCard object. + * + * @param int $options + * + * @return string + */ + public static function writeJson(Component $component, $options = 0) + { + return json_encode($component, $options); + } + + /** + * Serializes a xCal or xCard object. + * + * @return string + */ + public static function writeXml(Component $component) + { + $writer = new Xml\Writer(); + $writer->openMemory(); + $writer->setIndent(true); + + $writer->startDocument('1.0', 'utf-8'); + + if ($component instanceof Component\VCalendar) { + $writer->startElement('icalendar'); + $writer->writeAttribute('xmlns', Parser\XML::XCAL_NAMESPACE); + } else { + $writer->startElement('vcards'); + $writer->writeAttribute('xmlns', Parser\XML::XCARD_NAMESPACE); + } + + $component->xmlSerialize($writer); + + $writer->endElement(); + + return $writer->outputMemory(); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/exchangezones.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/exchangezones.php new file mode 100644 index 0000000..89bddc2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/exchangezones.php @@ -0,0 +1,94 @@ +<?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)' => 'America/Los_Angeles', // Best guess + 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess + 'Alaska' => 'America/Anchorage', + 'Hawaii' => 'Pacific/Honolulu', + 'Midway Island, Samoa' => 'Pacific/Midway', + 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', +]; diff --git a/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/lotuszones.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/lotuszones.php new file mode 100644 index 0000000..4b50808 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/php-bc.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/php-bc.php new file mode 100644 index 0000000..83f38f5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/php-bc.php @@ -0,0 +1,153 @@ +<?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/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/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/php-workaround.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/php-workaround.php new file mode 100644 index 0000000..13ff4b3 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/windowszones.php b/plugins/panakour/backup/vendor/sabre/vobject/lib/timezonedata/windowszones.php new file mode 100644 index 0000000..af3904b --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/phpstan.neon b/plugins/panakour/backup/vendor/sabre/vobject/phpstan.neon new file mode 100644 index 0000000..c705178 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 1 + universalObjectCratesClasses: + - \Sabre\VObject\Component diff --git a/plugins/panakour/backup/vendor/sabre/vobject/resources/schema/xcal.rng b/plugins/panakour/backup/vendor/sabre/vobject/resources/schema/xcal.rng new file mode 100644 index 0000000..4a51460 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/resources/schema/xcal.rng @@ -0,0 +1,1192 @@ +# RELAX NG Schema for iCalendar in XML +# Extract from RFC6321. +# Erratum 3042 applied. +# Erratum 3050 applied. +# Erratum 3314 applied. + +default namespace = "urn:ietf:params:xml:ns:icalendar-2.0" + +# 3.2 Property Parameters + +# 3.2.1 Alternate Text Representation + +altrepparam = element altrep { + value-uri +} + +# 3.2.2 Common Name + +cnparam = element cn { + value-text +} + +# 3.2.3 Calendar User Type + +cutypeparam = element cutype { + element text { + "INDIVIDUAL" | + "GROUP" | + "RESOURCE" | + "ROOM" | + "UNKNOWN" + } +} + +# 3.2.4 Delegators + +delfromparam = element delegated-from { + value-cal-address+ +} + +# 3.2.5 Delegatees + +deltoparam = element delegated-to { + value-cal-address+ +} + +# 3.2.6 Directory Entry Reference + +dirparam = element dir { + value-uri +} + +# 3.2.7 Inline Encoding + +encodingparam = element encoding { + element text { + "8BIT" | + "BASE64" + } +} + +# 3.2.8 Format Type + +fmttypeparam = element fmttype { + value-text +} + +# 3.2.9 Free/Busy Time Type + +fbtypeparam = element fbtype { + element text { + "FREE" | + "BUSY" | + "BUSY-UNAVAILABLE" | + "BUSY-TENTATIVE" + } +} + +# 3.2.10 Language + +languageparam = element language { + value-text +} + +# 3.2.11 Group or List Membership + +memberparam = element member { + value-cal-address+ +} + +# 3.2.12 Participation Status + +partstatparam = element partstat { + type-partstat-event | + type-partstat-todo | + type-partstat-jour +} + +type-partstat-event = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" | + "TENTATIVE" | + "DELEGATED" + } +) + +type-partstat-todo = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" | + "TENTATIVE" | + "DELEGATED" | + "COMPLETED" | + "IN-PROCESS" + } +) + +type-partstat-jour = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" + } +) + +# 3.2.13 Recurrence Identifier Range + +rangeparam = element range { + element text { + "THISANDFUTURE" + } +} + +# 3.2.14 Alarm Trigger Relationship + +trigrelparam = element related { + element text { + "START" | + "END" + } +} + +# 3.2.15 Relationship Type + +reltypeparam = element reltype { + element text { + "PARENT" | + "CHILD" | + "SIBLING" + } +} + +# 3.2.16 Participation Role + +roleparam = element role { + element text { + "CHAIR" | + "REQ-PARTICIPANT" | + "OPT-PARTICIPANT" | + "NON-PARTICIPANT" + } +} + +# 3.2.17 RSVP Expectation + +rsvpparam = element rsvp { + value-boolean +} + +# 3.2.18 Sent By + +sentbyparam = element sent-by { + value-cal-address +} + +# 3.2.19 Time Zone Identifier + +tzidparam = element tzid { + value-text +} + +# 3.3 Property Value Data Types + +# 3.3.1 BINARY + +value-binary = element binary { + xsd:string +} + +# 3.3.2 BOOLEAN + +value-boolean = element boolean { + xsd:boolean +} + +# 3.3.3 CAL-ADDRESS + +value-cal-address = element cal-address { + xsd:anyURI +} + +# 3.3.4 DATE + +pattern-date = xsd:string { + pattern = "\d\d\d\d-\d\d-\d\d" +} + +value-date = element date { + pattern-date +} + +# 3.3.5 DATE-TIME + +pattern-date-time = xsd:string { + pattern = "\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ?" +} + +value-date-time = element date-time { + pattern-date-time +} + +# 3.3.6 DURATION + +pattern-duration = xsd:string { + pattern = "(+|-)?P(\d+W)|(\d+D)?" + ~ "(T(\d+H(\d+M)?(\d+S)?)|" + ~ "(\d+M(\d+S)?)|" + ~ "(\d+S))?" +} + +value-duration = element duration { + pattern-duration +} + +# 3.3.7 FLOAT + +value-float = element float { + xsd:float +} + +# 3.3.8 INTEGER + +value-integer = element integer { + xsd:integer +} + +# 3.3.9 PERIOD + +value-period = element period { + element start { + pattern-date-time + }, + ( + element end { + pattern-date-time + } | + element duration { + pattern-duration + } + ) +} + +# 3.3.10 RECUR + +value-recur = element recur { + type-freq, + (type-until | type-count)?, + element interval { + xsd:positiveInteger + }?, + type-bysecond*, + type-byminute*, + type-byhour*, + type-byday*, + type-bymonthday*, + type-byyearday*, + type-byweekno*, + type-bymonth*, + type-bysetpos*, + element wkst { type-weekday }? +} + +type-freq = element freq { + "SECONDLY" | + "MINUTELY" | + "HOURLY" | + "DAILY" | + "WEEKLY" | + "MONTHLY" | + "YEARLY" +} + +type-until = element until { + type-date | + type-date-time +} + +type-count = element count { + xsd:positiveInteger +} + +type-bysecond = element bysecond { + xsd:nonNegativeInteger +} + +type-byminute = element byminute { + xsd:nonNegativeInteger +} + +type-byhour = element byhour { + xsd:nonNegativeInteger +} + +type-weekday = ( + "SU" | + "MO" | + "TU" | + "WE" | + "TH" | + "FR" | + "SA" +) + +type-byday = element byday { + xsd:integer?, + type-weekday +} + +type-bymonthday = element bymonthday { + xsd:integer +} + +type-byyearday = element byyearday { + xsd:integer +} + +type-byweekno = element byweekno { + xsd:integer +} + +type-bymonth = element bymonth { + xsd:positiveInteger +} + +type-bysetpos = element bysetpos { + xsd:integer +} + +# 3.3.11 TEXT + +value-text = element text { + xsd:string +} + +# 3.3.12 TIME + +pattern-time = xsd:string { + pattern = "\d\d:\d\d:\d\dZ?" +} + +value-time = element time { + pattern-time +} + +# 3.3.13 URI + +value-uri = element uri { + xsd:anyURI +} + +# 3.3.14 UTC-OFFSET + +value-utc-offset = element utc-offset { + xsd:string { pattern = "(+|-)\d\d:\d\d(:\d\d)?" } +} + +# UNKNOWN + +value-unknown = element unknown { + xsd:string +} + +# 3.4 iCalendar Stream + +start = element icalendar { + vcalendar+ +} + +# 3.6 Calendar Components + +vcalendar = element vcalendar { + type-calprops, + type-component +} + +type-calprops = element properties { + property-prodid & + property-version & + property-calscale? & + property-method? +} + +type-component = element components { + ( + component-vevent | + component-vtodo | + component-vjournal | + component-vfreebusy | + component-vtimezone + )* +} + +# 3.6.1 Event Component + +component-vevent = element vevent { + type-eventprop, + element components { + component-valarm+ + }? +} + +type-eventprop = element properties { + property-dtstamp & + property-dtstart & + property-uid & + + property-class? & + property-created? & + property-description? & + property-geo? & + property-last-mod? & + property-location? & + property-organizer? & + property-priority? & + property-seq? & + property-status-event? & + property-summary? & + property-transp? & + property-url? & + property-recurid? & + + property-rrule? & + + (property-dtend | property-duration)? & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-exdate* & + property-rstatus* & + property-related* & + property-resources* & + property-rdate* +} + +# 3.6.2 To-do Component + +component-vtodo = element vtodo { + type-todoprop, + element components { + component-valarm+ + }? +} + +type-todoprop = element properties { + property-dtstamp & + property-uid & + + property-class? & + property-completed? & + property-created? & + property-description? & + property-geo? & + property-last-mod? & + property-location? & + property-organizer? & + property-percent? & + property-priority? & + property-recurid? & + property-seq? & + property-status-todo? & + property-summary? & + property-url? & + + property-rrule? & + + ( + (property-dtstart?, property-dtend? ) | + (property-dtstart, property-duration)? + ) & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-exdate* & + property-rstatus* & + property-related* & + property-resources* & + property-rdate* +} + +# 3.6.3 Journal Component + +component-vjournal = element vjournal { + type-jourprop +} + +type-jourprop = element properties { + property-dtstamp & + property-uid & + + property-class? & + property-created? & + property-dtstart? & + property-last-mod? & + property-organizer? & + property-recurid? & + property-seq? & + property-status-jour? & + property-summary? & + property-url? & + + property-rrule? & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-description? & + property-exdate* & + property-related* & + property-rdate* & + property-rstatus* +} + +# 3.6.4 Free/Busy Component + +component-vfreebusy = element vfreebusy { + type-fbprop +} + +type-fbprop = element properties { + property-dtstamp & + property-uid & + + property-contact? & + property-dtstart? & + property-dtend? & + property-duration? & + property-organizer? & + property-url? & + + property-attendee* & + property-comment* & + property-freebusy* & + property-rstatus* +} + +# 3.6.5 Time Zone Component + +component-vtimezone = element vtimezone { + element properties { + property-tzid & + + property-last-mod? & + property-tzurl? + }, + element components { + (component-standard | component-daylight) & + component-standard* & + component-daylight* + } +} + +component-standard = element standard { + type-tzprop +} + +component-daylight = element daylight { + type-tzprop +} + +type-tzprop = element properties { + property-dtstart & + property-tzoffsetto & + property-tzoffsetfrom & + + property-rrule? & + + property-comment* & + property-rdate* & + property-tzname* +} + +# 3.6.6 Alarm Component + +component-valarm = element valarm { + type-audioprop | type-dispprop | type-emailprop +} + +type-audioprop = element properties { + property-action & + + property-trigger & + + (property-duration, property-repeat)? & + + property-attach? +} + +type-emailprop = element properties { + property-action & + property-description & + property-trigger & + property-summary & + + property-attendee+ & + + (property-duration, property-repeat)? & + + property-attach* +} + +type-dispprop = element properties { + property-action & + property-description & + property-trigger & + + (property-duration, property-repeat)? +} + +# 3.7 Calendar Properties + +# 3.7.1 Calendar Scale + +property-calscale = element calscale { + + element parameters { empty }?, + + element text { "GREGORIAN" } +} + +# 3.7.2 Method + +property-method = element method { + + element parameters { empty }?, + + value-text +} + +# 3.7.3 Product Identifier + +property-prodid = element prodid { + + element parameters { empty }?, + + value-text +} + +# 3.7.4 Version + +property-version = element version { + + element parameters { empty }?, + + element text { "2.0" } +} + +# 3.8 Component Properties + +# 3.8.1 Descriptive Component Properties + +# 3.8.1.1 Attachment + +property-attach = element attach { + + element parameters { + fmttypeparam? & + encodingparam? + }?, + + value-uri | value-binary +} + +# 3.8.1.2 Categories + +property-categories = element categories { + + element parameters { + languageparam? & + }?, + + value-text+ +} + +# 3.8.1.3 Classification + +property-class = element class { + + element parameters { empty }?, + + element text { + "PUBLIC" | + "PRIVATE" | + "CONFIDENTIAL" + } +} + +# 3.8.1.4 Comment + +property-comment = element comment { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.5 Description + +property-description = element description { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.6 Geographic Position + +property-geo = element geo { + + element parameters { empty }?, + + element latitude { xsd:float }, + element longitude { xsd:float } +} + +# 3.8.1.7 Location + +property-location = element location { + + element parameters { + + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.8 Percent Complete + +property-percent = element percent-complete { + + element parameters { empty }?, + + value-integer +} + +# 3.8.1.9 Priority + +property-priority = element priority { + + element parameters { empty }?, + + value-integer +} + +# 3.8.1.10 Resources + +property-resources = element resources { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text+ +} + +# 3.8.1.11 Status + +property-status-event = element status { + + element parameters { empty }?, + + element text { + "TENTATIVE" | + "CONFIRMED" | + "CANCELLED" + } +} + +property-status-todo = element status { + + element parameters { empty }?, + + element text { + "NEEDS-ACTION" | + "COMPLETED" | + "IN-PROCESS" | + "CANCELLED" + } +} + +property-status-jour = element status { + + element parameters { empty }?, + + element text { + "DRAFT" | + "FINAL" | + "CANCELLED" + } +} + +# 3.8.1.12 Summary + +property-summary = element summary { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.2 Date and Time Component Properties + +# 3.8.2.1 Date/Time Completed + +property-completed = element completed { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.2.2 Date/Time End + +property-dtend = element dtend { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.3 Date/Time Due + +property-due = element due { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.4 Date/Time Start + +property-dtstart = element dtstart { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.5 Duration + +property-duration = element duration { + + element parameters { empty }?, + + value-duration +} + +# 3.8.2.6 Free/Busy Time + +property-freebusy = element freebusy { + + element parameters { + fbtypeparam? + }?, + + + value-period+ +} + +# 3.8.2.7 Time Transparency + +property-transp = element transp { + + element parameters { empty }?, + + element text { + "OPAQUE" | + "TRANSPARENT" + } +} + +# 3.8.3 Time Zone Component Properties + +# 3.8.3.1 Time Zone Identifier + +property-tzid = element tzid { + + element parameters { empty }?, + + value-text +} + +# 3.8.3.2 Time Zone Name + +property-tzname = element tzname { + + element parameters { + languageparam? + }?, + + value-text +} + +# 3.8.3.3 Time Zone Offset From + +property-tzoffsetfrom = element tzoffsetfrom { + + element parameters { empty }?, + + value-utc-offset +} + +# 3.8.3.4 Time Zone Offset To + +property-tzoffsetto = element tzoffsetto { + + element parameters { empty }?, + + value-utc-offset +} + +# 3.8.3.5 Time Zone URL + +property-tzurl = element tzurl { + + element parameters { empty }?, + + value-uri +} + +# 3.8.4 Relationship Component Properties + +# 3.8.4.1 Attendee + +property-attendee = element attendee { + + element parameters { + cutypeparam? & + memberparam? & + roleparam? & + partstatparam? & + rsvpparam? & + deltoparam? & + delfromparam? & + sentbyparam? & + cnparam? & + dirparam? & + languageparam? + }?, + + value-cal-address +} + +# 3.8.4.2 Contact + +property-contact = element contact { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.4.3 Organizer + +property-organizer = element organizer { + + element parameters { + cnparam? & + dirparam? & + sentbyparam? & + languageparam? + }?, + + value-cal-address +} + +# 3.8.4.4 Recurrence ID + +property-recurid = element recurrence-id { + + element parameters { + tzidparam? & + rangeparam? + }?, + + value-date-time | + value-date +} + +# 3.8.4.5 Related-To + +property-related = element related-to { + + element parameters { + reltypeparam? + }?, + + value-text +} + +# 3.8.4.6 Uniform Resource Locator + +property-url = element url { + + element parameters { empty }?, + + value-uri +} + +# 3.8.4.7 Unique Identifier + +property-uid = element uid { + + element parameters { empty }?, + + value-text +} + +# 3.8.5 Recurrence Component Properties + +# 3.8.5.1 Exception Date/Times + +property-exdate = element exdate { + + element parameters { + tzidparam? + }?, + + value-date-time+ | + value-date+ +} + +# 3.8.5.2 Recurrence Date/Times + +property-rdate = element rdate { + + element parameters { + tzidparam? + }?, + + value-date-time+ | + value-date+ | + value-period+ +} + +# 3.8.5.3 Recurrence Rule + +property-rrule = element rrule { + + element parameters { empty }?, + + value-recur +} + +# 3.8.6 Alarm Component Properties + +# 3.8.6.1 Action + +property-action = element action { + + element parameters { empty }?, + + element text { + "AUDIO" | + "DISPLAY" | + "EMAIL" + } +} + +# 3.8.6.2 Repeat Count + +property-repeat = element repeat { + + element parameters { empty }?, + + value-integer +} + +# 3.8.6.3 Trigger + +property-trigger = element trigger { + + ( + element parameters { + trigrelparam? + }?, + + value-duration + ) | + ( + element parameters { empty }?, + + value-date-time + ) +} + +# 3.8.7 Change Management Component Properties + +# 3.8.7.1 Date/Time Created + +property-created = element created { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.2 Date/Time Stamp + +property-dtstamp = element dtstamp { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.3 Last Modified + +property-last-mod = element last-modified { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.4 Sequence Number + +property-seq = element sequence { + + element parameters { empty }?, + + value-integer +} + +# 3.8.8 Miscellaneous Component Properties + +# 3.8.8.3 Request Status + +property-rstatus = element request-status { + + element parameters { + languageparam? + }?, + + element code { xsd:string }, + element description { xsd:string }, + element data { xsd:string }? +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/resources/schema/xcard.rng b/plugins/panakour/backup/vendor/sabre/vobject/resources/schema/xcard.rng new file mode 100644 index 0000000..c0b7cfb --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/resources/schema/xcard.rng @@ -0,0 +1,388 @@ +# RELAX NG Schema for vCard in XML +# Extract from RFC6351. +# Erratum 2994 applied. +# Erratum 3047 applied. +# Erratum 3008 applied. +# Erratum 4247 applied. + +default namespace = "urn:ietf:params:xml:ns:vcard-4.0" + +### Section 3.3: vCard Format Specification +# +# 3.3 +iana-token = xsd:string { pattern = "[a-zA-Z0-9\-]+" } +x-name = xsd:string { pattern = "x-[a-zA-Z0-9\-]+" } + +### Section 4: Value types +# +# 4.1 +value-text = element text { text } +value-text-list = value-text+ + +# 4.2 +value-uri = element uri { xsd:anyURI } + +# 4.3.1 +value-date = element date { + xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" } + } + +# 4.3.2 +value-time = element time { + xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)" + ~ "(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.3.3 +value-date-time = element date-time { + xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?" + ~ "(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.3.4 +value-date-and-or-time = value-date | value-date-time | value-time + +# 4.3.5 +value-timestamp = element timestamp { + xsd:string { pattern = "\d{8}T\d{6}(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.4 +value-boolean = element boolean { xsd:boolean } + +# 4.5 +value-integer = element integer { xsd:integer } + +# 4.6 +value-float = element float { xsd:float } + +# 4.7 +value-utc-offset = element utc-offset { + xsd:string { pattern = "[+\-]\d\d(\d\d)?" } + } + +# 4.8 +value-language-tag = element language-tag { + xsd:string { pattern = "([a-z]{2,3}((-[a-z]{3}){0,3})?|[a-z]{4,8})" + ~ "(-[a-z]{4})?(-([a-z]{2}|\d{3}))?" + ~ "(-([0-9a-z]{5,8}|\d[0-9a-z]{3}))*" + ~ "(-[0-9a-wyz](-[0-9a-z]{2,8})+)*" + ~ "(-x(-[0-9a-z]{1,8})+)?|x(-[0-9a-z]{1,8})+|" + ~ "[a-z]{1,3}(-[0-9a-z]{2,8}){1,2}" } + } + +### Section 5: Parameters +# +# 5.1 +param-language = element language { value-language-tag }? + +# 5.2 +param-pref = element pref { + element integer { + xsd:integer { minInclusive = "1" maxInclusive = "100" } + } + }? + +# 5.4 +param-altid = element altid { value-text }? + +# 5.5 +param-pid = element pid { + element text { xsd:string { pattern = "\d+(\.\d+)?" } }+ + }? + +# 5.6 +param-type = element type { element text { "work" | "home" }+ }? + +# 5.7 +param-mediatype = element mediatype { value-text }? + +# 5.8 +param-calscale = element calscale { element text { "gregorian" } }? + +# 5.9 +param-sort-as = element sort-as { value-text+ }? + +# 5.10 +param-geo = element geo { value-uri }? + +# 5.11 +param-tz = element tz { value-text | value-uri }? + +### Section 6: Properties +# +# 6.1.3 +property-source = element source { + element parameters { param-altid, param-pid, param-pref, + param-mediatype }?, + value-uri + } + +# 6.1.4 +property-kind = element kind { + element text { "individual" | "group" | "org" | "location" | + x-name | iana-token }* + } + +# 6.2.1 +property-fn = element fn { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.2.2 +property-n = element n { + element parameters { param-language, param-sort-as, param-altid }?, + element surname { text }+, + element given { text }+, + element additional { text }+, + element prefix { text }+, + element suffix { text }+ + } + +# 6.2.3 +property-nickname = element nickname { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text-list + } + +# 6.2.4 +property-photo = element photo { + element parameters { param-altid, param-pid, param-pref, param-type, + param-mediatype }?, + value-uri + } + +# 6.2.5 +property-bday = element bday { + element parameters { param-altid, param-calscale }?, + (value-date-and-or-time | value-text) + } + +# 6.2.6 +property-anniversary = element anniversary { + element parameters { param-altid, param-calscale }?, + (value-date-and-or-time | value-text) + } + +# 6.2.7 +property-gender = element gender { + element sex { "" | "M" | "F" | "O" | "N" | "U" }, + element identity { text }? + } + +# 6.3.1 +param-label = element label { value-text }? +property-adr = element adr { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-geo, param-tz, + param-label }?, + element pobox { text }+, + element ext { text }+, + element street { text }+, + element locality { text }+, + element region { text }+, + element code { text }+, + element country { text }+ + } + +# 6.4.1 +property-tel = element tel { + element parameters { + param-altid, + param-pid, + param-pref, + element type { + element text { "work" | "home" | "text" | "voice" + | "fax" | "cell" | "video" | "pager" + | "textphone" | x-name | iana-token }+ + }?, + param-mediatype + }?, + (value-text | value-uri) + } + +# 6.4.2 +property-email = element email { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-text + } + +# 6.4.3 +property-impp = element impp { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.4.4 +property-lang = element lang { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-language-tag + } + +# 6.5.1 +property-tz = element tz { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + (value-text | value-uri | value-utc-offset) + } + +# 6.5.2 +property-geo = element geo { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.6.1 +property-title = element title { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.6.2 +property-role = element role { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.6.3 +property-logo = element logo { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-mediatype }?, + value-uri + } + +# 6.6.4 +property-org = element org { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-sort-as }?, + value-text-list + } + +# 6.6.5 +property-member = element member { + element parameters { param-altid, param-pid, param-pref, + param-mediatype }?, + value-uri + } + +# 6.6.6 +property-related = element related { + element parameters { + param-altid, + param-pid, + param-pref, + element type { + element text { + "work" | "home" | "contact" | "acquaintance" | + "friend" | "met" | "co-worker" | "colleague" | "co-resident" | + "neighbor" | "child" | "parent" | "sibling" | "spouse" | + "kin" | "muse" | "crush" | "date" | "sweetheart" | "me" | + "agent" | "emergency" + }+ + }?, + param-mediatype + }?, + (value-uri | value-text) + } + +# 6.7.1 +property-categories = element categories { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-text-list + } + +# 6.7.2 +property-note = element note { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.7.3 +property-prodid = element prodid { value-text } + +# 6.7.4 +property-rev = element rev { value-timestamp } + +# 6.7.5 +property-sound = element sound { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-mediatype }?, + value-uri + } + +# 6.7.6 +property-uid = element uid { value-uri } + +# 6.7.7 +property-clientpidmap = element clientpidmap { + element sourceid { xsd:positiveInteger }, + value-uri + } + +# 6.7.8 +property-url = element url { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.8.1 +property-key = element key { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + (value-uri | value-text) + } + +# 6.9.1 +property-fburl = element fburl { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.9.2 +property-caladruri = element caladruri { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.9.3 +property-caluri = element caluri { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# Top-level grammar +property = property-adr | property-anniversary | property-bday + | property-caladruri | property-caluri | property-categories + | property-clientpidmap | property-email | property-fburl + | property-fn | property-geo | property-impp | property-key + | property-kind | property-lang | property-logo + | property-member | property-n | property-nickname + | property-note | property-org | property-photo + | property-prodid | property-related | property-rev + | property-role | property-gender | property-sound + | property-source | property-tel | property-title + | property-tz | property-uid | property-url +start = element vcards { + element vcard { + (property + | element group { + attribute name { text }, + property* + })+ + }+ + } diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/AttachIssueTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/AttachIssueTest.php new file mode 100644 index 0000000..fb2b8b0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/AttachIssueTest.php @@ -0,0 +1,22 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class AttachIssueTest extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/BirthdayCalendarGeneratorTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/BirthdayCalendarGeneratorTest.php new file mode 100644 index 0000000..d273628 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/BirthdayCalendarGeneratorTest.php @@ -0,0 +1,542 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class BirthdayCalendarGeneratorTest extends TestCase +{ + use PHPUnitAssertions; + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public function testParseException() + { + $this->expectException(ParseException::class); + $generator = new BirthdayCalendarGenerator(); + $input = <<<EOT +BEGIN:FOO +FOO:Bar +END:FOO +EOT; + + $generator->setObjects($input); + } + + public function testInvalidArgumentException() + { + $this->expectException(\InvalidArgumentException::class); + $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); + } + + public function testInvalidArgumentExceptionForPartiallyInvalidArray() + { + $this->expectException(\InvalidArgumentException::class); + $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); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/CliTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/CliTest.php new file mode 100644 index 0000000..5736f9a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/CliTest.php @@ -0,0 +1,626 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +/** + * Tests the cli. + * + * Warning: these tests are very rudimentary. + */ +class CliTest extends TestCase +{ + /** @var CliMock */ + private $cli; + + private $sabreTempDir = __DIR__.'/../temp/'; + + public function setUp(): void + { + if (!file_exists($this->sabreTempDir)) { + mkdir($this->sabreTempDir); + } + + $this->cli = new CliMock(); + $this->cli->stderr = fopen('php://memory', 'r+'); + $this->cli->stdout = fopen('php://memory', 'r+'); + } + + public function testInvalidArg() + { + $this->assertEquals( + 1, + $this->cli->main(['vobject', '--hi']) + ); + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + } + + public 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))); + } + + public function testHelp() + { + $this->assertEquals( + 0, + $this->cli->main(['vobject', '-h']) + ); + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public function testNoInputFile() + { + $this->assertEquals( + 1, + $this->cli->main(['vobject', 'color']) + ); + + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + } + + public function testTooManyArgs() + { + $this->assertEquals( + 1, + $this->cli->main(['vobject', 'color', 'a', 'b', 'c']) + ); + } + + public function testUnknownCommand() + { + $this->assertEquals( + 1, + $this->cli->main(['vobject', 'foo', '-']) + ); + } + + public 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) + ); + } + + public 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) + ); + } + + public 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', '-']) + ); + } + + public 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) + ); + } + + public function testConvertDefaultFormats() + { + $outputFile = $this->sabreTempDir.'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); + } + + public function testConvertDefaultFormats2() + { + $outputFile = $this->sabreTempDir.'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); + } + + public 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) + ); + } + + public 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) + ); + } + + public 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', '-']) + ); + } + + public 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 + ); + } + + public 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', '-']) + ); + } + + public 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', '-']) + ); + } + + public 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)); + } + + public 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. + */ + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/AvailableTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/AvailableTest.php new file mode 100644 index 0000000..bf0a671 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/AvailableTest.php @@ -0,0 +1,71 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeImmutable; +use DateTimeZone; +use PHPUnit\Framework\TestCase; +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 TestCase +{ + public function testAvailableComponent() + { + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:AVAILABLE +END:AVAILABLE +END:VCALENDAR +VCAL; + $document = Reader::read($vcal); + $this->assertInstanceOf(Available::class, $document->AVAILABLE); + } + + public 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() + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VAlarmTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VAlarmTest.php new file mode 100644 index 0000000..2823d16 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VAlarmTest.php @@ -0,0 +1,171 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTime; +use PHPUnit\Framework\TestCase; +use Sabre\VObject\InvalidDataException; +use Sabre\VObject\Reader; + +class VAlarmTest extends TestCase +{ + /** + * @dataProvider timeRangeTestData + */ + public function testInTimeRange(VAlarm $valarm, $start, $end, $outcome) + { + $this->assertEquals($outcome, $valarm->isInTimeRange($start, $end)); + } + + public 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; + } + + public function testInTimeRangeInvalidComponent() + { + $this->expectException(InvalidDataException::class); + $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. + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VAvailabilityTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VAvailabilityTest.php new file mode 100644 index 0000000..2fd9c0d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VAvailabilityTest.php @@ -0,0 +1,474 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeImmutable; +use DateTimeZone; +use PHPUnit\Framework\TestCase; +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 TestCase +{ + public function testVAvailabilityComponent() + { + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +END:VAVAILABILITY +END:VCALENDAR +VCAL; + $document = Reader::read($vcal); + + $this->assertInstanceOf(VAvailability::class, $document->VAVAILABILITY); + } + + public 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() + ); + } + + public 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() + ); + } + + public function testGetEffectiveStartEndUnbound() + { + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +END:VAVAILABILITY +END:VCALENDAR +VCAL; + + $document = Reader::read($vcal); + $this->assertEquals( + [ + null, + null, + ], + $document->VAVAILABILITY->getEffectiveStartEnd() + ); + } + + public 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')) + ); + } + + public 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')) + ); + } + + public 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 + )); + } + + public 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, + ]))); + } + } + + public 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', + ]))); + } + + public function testAvailableSubComponent() + { + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +BEGIN:AVAILABLE +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +VCAL; + $document = Reader::read($vcal); + + $this->assertInstanceOf(Available::class, $document->VAVAILABILITY->AVAILABLE); + } + + public 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 + )); + } + + public 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', + ]))); + } + + public 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, + ]))); + } + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VCalendarTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VCalendarTest.php new file mode 100644 index 0000000..c2f0ce9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VCalendarTest.php @@ -0,0 +1,757 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeZone; +use PHPUnit\Framework\TestCase; +use Sabre\VObject; +use Sabre\VObject\InvalidDataException; + +class VCalendarTest extends TestCase +{ + use VObject\PHPUnitAssertions; + + /** + * @dataProvider expandData + */ + public 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()); + } + + public 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; + } + + public function testBrokenEventExpand() + { + $this->expectException(InvalidDataException::class); + $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') + ); + } + + public function testGetDocumentType() + { + $vcard = new VCalendar(); + $vcard->VERSION = '2.0'; + $this->assertEquals(VCalendar::ICALENDAR20, $vcard->getDocumentType()); + } + + public 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'); + } + + public 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())); + } + + public 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())); + } + + public 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())); + } + + public 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())); + } + + public 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())); + } + + public 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())); + } + + public 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())); + } + + public 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()); + } + + public 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); + } + + public 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()); + } + + public 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); + } + + public 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.' + ); + } + + public 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).' + ); + } + + public 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.' + ); + } + + public 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).' + ); + } + + public 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.' + ); + } + + public function assertValidate($ics, $options, $expectedLevel, $expectedMessage = null) + { + $vcal = VObject\Reader::read($ics); + $result = $vcal->validate($options); + + $this->assertValidateResult($result, $expectedLevel, $expectedMessage); + } + + public function assertValidateResult($input, $expectedLevel, $expectedMessage = null) + { + $messages = []; + foreach ($input as $warning) { + $messages[] = $warning['message']; + } + + if (0 === $expectedLevel) { + $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VCardTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VCardTest.php new file mode 100644 index 0000000..3124fec --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VCardTest.php @@ -0,0 +1,302 @@ +<?php + +namespace Sabre\VObject\Component; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject; + +class VCardTest extends TestCase +{ + /** + * @dataProvider validateData + */ + public 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() + ); + } + + public 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", + ]; + // No FN, EMAIL fallback + $tests[] = [ + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nEMAIL:1@example.org\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\nEMAIL:1@example.org\r\nFN:1@example.org\r\nEND:VCARD\r\n", + ]; + + return $tests; + } + + public 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()); + } + + public 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')); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public function testPreferredNotFound() + { + $vcard = <<<VCF +BEGIN:VCARD +VERSION:4.0 +END:VCARD +VCF; + + $vcard = VObject\Reader::read($vcard); + $this->assertNull($vcard->preferred('EMAIL')); + } + + public 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.' + ); + } + + public 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.' + ); + } + + public 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.' + ); + } + + public 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.' + ); + } + + public function testVCard21NoCardDAV() + { + $vcard = <<<VCF +BEGIN:VCARD +VERSION:2.1 +FN:John Doe +UID:foo +END:VCARD +VCF; + $this->assertValidate( + $vcard, + 0, + 0 + ); + } + + public function assertValidate($vcf, $options, $expectedLevel, $expectedMessage = null) + { + $vcal = VObject\Reader::read($vcf); + $result = $vcal->validate($options); + + $this->assertValidateResult($result, $expectedLevel, $expectedMessage); + } + + public function assertValidateResult($input, $expectedLevel, $expectedMessage = null) + { + $messages = []; + foreach ($input as $warning) { + $messages[] = $warning['message']; + } + + if (0 === $expectedLevel) { + $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VEventTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VEventTest.php new file mode 100644 index 0000000..635ae5a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VEventTest.php @@ -0,0 +1,94 @@ +<?php + +namespace Sabre\VObject\Component; + +use PHPUnit\Framework\TestCase; + +class VEventTest extends TestCase +{ + /** + * @dataProvider timeRangeTestData + */ + public function testInTimeRange(VEvent $vevent, $start, $end, $outcome) + { + $this->assertEquals($outcome, $vevent->isInTimeRange($start, $end)); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VFreeBusyTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VFreeBusyTest.php new file mode 100644 index 0000000..2aa4351 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VFreeBusyTest.php @@ -0,0 +1,64 @@ +<?php + +namespace Sabre\VObject\Component; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject; +use Sabre\VObject\Reader; + +class VFreeBusyTest extends TestCase +{ + public 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))); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VJournalTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VJournalTest.php new file mode 100644 index 0000000..67901b7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VJournalTest.php @@ -0,0 +1,94 @@ +<?php + +namespace Sabre\VObject\Component; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject\Reader; + +class VJournalTest extends TestCase +{ + /** + * @dataProvider timeRangeTestData + */ + public function testInTimeRange(VJournal $vtodo, $start, $end, $outcome) + { + $this->assertEquals($outcome, $vtodo->isInTimeRange($start, $end)); + } + + public 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); + } + + public 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 + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VTimeZoneTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VTimeZoneTest.php new file mode 100644 index 0000000..ead22b8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VTimeZoneTest.php @@ -0,0 +1,54 @@ +<?php + +namespace Sabre\VObject\Component; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject\Reader; + +class VTimeZoneTest extends TestCase +{ + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VTodoTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VTodoTest.php new file mode 100644 index 0000000..d543445 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Component/VTodoTest.php @@ -0,0 +1,171 @@ +<?php + +namespace Sabre\VObject\Component; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject\Reader; + +class VTodoTest extends TestCase +{ + /** + * @dataProvider timeRangeTestData + */ + public function testInTimeRange(VTodo $vtodo, $start, $end, $outcome) + { + $this->assertEquals($outcome, $vtodo->isInTimeRange($start, $end)); + } + + public 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; + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ComponentTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ComponentTest.php new file mode 100644 index 0000000..f56d555 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ComponentTest.php @@ -0,0 +1,542 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VCard; + +class ComponentTest extends TestCase +{ + public 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(Component::class, $subcomponent); + + if (2 === $count) { + $this->assertEquals(1, $key); + } + } + $this->assertEquals(2, $count); + } + + public 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(Component::class, $event); + $this->assertEquals('VEVENT', $event->name); + + $this->assertNull($comp->vjournal); + } + + public 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); + } + + public 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)); + } + + public function testMagicSetScalar() + { + $comp = new VCalendar(); + $comp->myProp = 'myValue'; + + $this->assertInstanceOf(Property::class, $comp->MYPROP); + $this->assertEquals('myValue', (string) $comp->MYPROP); + } + + public function testMagicSetScalarTwice() + { + $comp = new VCalendar([], false); + $comp->myProp = 'myValue'; + $comp->myProp = 'myValue'; + + $this->assertEquals(1, count($comp->children())); + $this->assertInstanceOf(Property::class, $comp->MYPROP); + $this->assertEquals('myValue', (string) $comp->MYPROP); + } + + public function testMagicSetArray() + { + $comp = new VCalendar(); + $comp->ORG = ['Acme Inc', 'Section 9']; + + $this->assertInstanceOf(Property::class, $comp->ORG); + $this->assertEquals(['Acme Inc', 'Section 9'], $comp->ORG->getParts()); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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])); + } + + public function testArrayAccessSet() + { + $this->expectException(\LogicException::class); + $comp = new VCalendar(); + $comp['hey'] = 'hi there'; + } + + public function testArrayAccessUnset() + { + $this->expectException(\LogicException::class); + $comp = new VCalendar(); + unset($comp[0]); + } + + public 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); + } + + public function testAddScalarParams() + { + $comp = new VCalendar([], false); + + $comp->add('myprop', 'value', ['param1' => 'value1']); + + $this->assertEquals(1, count($comp->children())); + + $bla = $comp->children()[0]; + + $this->assertInstanceOf(Property::class, $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()); + } + + public function testAddComponent() + { + $comp = new VCalendar([], false); + + $comp->add($comp->createComponent('VEVENT')); + + $this->assertEquals(1, count($comp->children())); + + $this->assertEquals('VEVENT', $comp->VEVENT->name); + } + + public 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); + } + + public function testAddArgFail() + { + $this->expectException(\InvalidArgumentException::class); + $comp = new VCalendar(); + $comp->add($comp->createComponent('VEVENT'), 'hello'); + } + + public function testAddArgFail2() + { + $this->expectException(\InvalidArgumentException::class); + $comp = new VCalendar(); + $comp->add([]); + } + + public function testMagicUnset() + { + $comp = new VCalendar([], false); + $comp->add($comp->createComponent('VEVENT')); + + unset($comp->vevent); + + $this->assertEquals(0, count($comp->children())); + } + + public function testCount() + { + $comp = new VCalendar(); + $this->assertEquals(1, $comp->count()); + } + + public 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->assertIsArray($r); + $this->assertEquals(2, count($r)); + } + + public function testGetComponents() + { + $comp = new VCalendar(); + + $comp->add($comp->createProperty('FOO', 'BAR')); + $comp->add($comp->createComponent('VTODO')); + + $r = $comp->getComponents(); + $this->assertIsArray($r); + $this->assertEquals(1, count($r)); + $this->assertEquals('VTODO', $r[0]->name); + } + + public function testSerialize() + { + $comp = new VCalendar([], false); + $this->assertEquals("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n", $comp->serialize()); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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()); + } + + public function testInstantiateSubComponent() + { + $comp = new VCalendar(); + $event = $comp->createComponent('VEVENT', [ + $comp->createProperty('UID', '12345'), + ]); + $comp->add($event); + + $this->assertEquals('12345', $comp->VEVENT->UID->getValue()); + } + + public 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)); + } + + public 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)); + } + + public function testRemoveNotFound() + { + $this->expectException(\InvalidArgumentException::class); + $comp = new VCalendar([], false); + $prop = $comp->createProperty('A', 'B'); + $comp->remove($prop); + } + + /** + * @dataProvider ruleData + */ + public 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())); + } + + public function testValidateRepair() + { + $vcard = new Component\VCard(); + + $component = new FakeComponent($vcard, 'Hi', [], $defaults = false); + $component->validate(Component::REPAIR); + $this->assertEquals('yow', $component->BAR->getValue()); + } + + public function testValidateRepairShouldNotDeduplicatePropertiesWhenValuesDiffer() + { + $vcard = new Component\VCard(); + + $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); + $component->add('BAZ', 'BAZ'); + $component->add('GIR', 'VALUE1'); + $component->add('GIR', 'VALUE2'); // Different values + + $messages = $component->validate(Component::REPAIR); + + $this->assertEquals(1, count($messages)); + $this->assertEquals(3, $messages[0]['level']); + $this->assertEquals(2, count($component->GIR)); + } + + public function testValidateRepairShouldNotDeduplicatePropertiesWhenParametersDiffer() + { + $vcard = new Component\VCard(); + + $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); + $component->add('BAZ', 'BAZ'); + $component->add('GIR', 'VALUE')->add('PARAM', '1'); + $component->add('GIR', 'VALUE')->add('PARAM', '2'); // Same value but different parameters + + $messages = $component->validate(Component::REPAIR); + + $this->assertEquals(1, count($messages)); + $this->assertEquals(3, $messages[0]['level']); + $this->assertEquals(2, count($component->GIR)); + } + + public function testValidateRepairShouldDeduplicatePropertiesWhenValuesAndParametersAreEqual() + { + $vcard = new Component\VCard(); + + $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); + $component->add('BAZ', 'BAZ'); + $component->add('GIR', 'VALUE')->add('PARAM', 'P'); + $component->add('GIR', 'VALUE')->add('PARAM', 'P'); + + $messages = $component->validate(Component::REPAIR); + + $this->assertEquals(1, count($messages)); + $this->assertEquals(1, $messages[0]['level']); + $this->assertEquals(1, count($component->GIR)); + } + + public function testValidateRepairShouldDeduplicatePropertiesWhenValuesAreEqual() + { + $vcard = new Component\VCard(); + + $component = new FakeComponent($vcard, 'WithDuplicateGIR', []); + $component->add('BAZ', 'BAZ'); + $component->add('GIR', 'VALUE'); + $component->add('GIR', 'VALUE'); + + $messages = $component->validate(Component::REPAIR); + + $this->assertEquals(1, count($messages)); + $this->assertEquals(1, $messages[0]['level']); + $this->assertEquals(1, count($component->GIR)); + } + + public 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 +{ + public function getValidationRules() + { + return [ + 'FOO' => '0', + 'BAR' => '1', + 'BAZ' => '+', + 'ZIM' => '*', + 'GIR' => '?', + ]; + } + + public function getDefaults() + { + return [ + 'BAR' => 'yow', + ]; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/DateTimeParserTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/DateTimeParserTest.php new file mode 100644 index 0000000..b20a432 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/DateTimeParserTest.php @@ -0,0 +1,654 @@ +<?php + +namespace Sabre\VObject; + +use DateInterval; +use DateTimeImmutable; +use DateTimeZone; +use PHPUnit\Framework\TestCase; + +class DateTimeParserTest extends TestCase +{ + public 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')); + } + + public 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')); + } + + public function testParseICalendarDurationFail() + { + $this->expectException(InvalidDataException::class); + DateTimeParser::parseDuration('P1X', true); + } + + public function testParseICalendarDateTime() + { + $dateTime = DateTimeParser::parseDateTime('20100316T141405'); + + $compare = new DateTimeImmutable('2010-03-16 14:14:05', new DateTimeZone('UTC')); + + $this->assertEquals($compare, $dateTime); + } + + /** + * @depends testParseICalendarDateTime + */ + public function testParseICalendarDateTimeBadFormat() + { + $this->expectException(InvalidDataException::class); + $dateTime = DateTimeParser::parseDateTime('20100316T141405 '); + } + + /** + * @depends testParseICalendarDateTime + */ + public function testParseICalendarDateTimeInvalidTime() + { + $this->expectException(InvalidDataException::class); + $dateTime = DateTimeParser::parseDateTime('20100316T251405'); + } + + /** + * @depends testParseICalendarDateTime + */ + public function testParseICalendarDateTimeUTC() + { + $dateTime = DateTimeParser::parseDateTime('20100316T141405Z'); + + $compare = new DateTimeImmutable('2010-03-16 14:14:05', new DateTimeZone('UTC')); + $this->assertEquals($compare, $dateTime); + } + + /** + * @depends testParseICalendarDateTime + */ + public function testParseICalendarDateTimeUTC2() + { + $dateTime = DateTimeParser::parseDateTime('20101211T160000Z'); + + $compare = new DateTimeImmutable('2010-12-11 16:00:00', new DateTimeZone('UTC')); + $this->assertEquals($compare, $dateTime); + } + + /** + * @depends testParseICalendarDateTime + */ + public 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); + } + + public 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. + */ + public 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. + */ + public 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 + */ + public function testParseICalendarDateBadFormat() + { + $this->expectException(InvalidDataException::class); + $dateTime = DateTimeParser::parseDate('20100316T141405'); + } + + /** + * @depends testParseICalendarDate + */ + public function testParseICalendarDateInvalidDate() + { + $this->expectException(InvalidDataException::class); + $dateTime = DateTimeParser::parseDate('20101331'); + } + + /** + * @dataProvider vcardDates + */ + public function testVCardDate($input, $output) + { + $this->assertEquals( + $output, + DateTimeParser::parseVCardDateTime($input) + ); + } + + public function testBadVCardDate() + { + $this->expectException(InvalidDataException::class); + DateTimeParser::parseVCardDateTime('1985---01'); + } + + public function testBadVCardTime() + { + $this->expectException(InvalidDataException::class); + DateTimeParser::parseVCardTime('23:12:166'); + } + + public 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', + ], + ], + ]; + } + + public function testDateAndOrTime_DateWithYearMonthDay() + { + $this->assertDateAndOrTimeEqualsTo( + '20150128', + [ + 'year' => '2015', + 'month' => '01', + 'date' => '28', + ] + ); + } + + public function testDateAndOrTime_DateWithYearMonth() + { + $this->assertDateAndOrTimeEqualsTo( + '2015-01', + [ + 'year' => '2015', + 'month' => '01', + ] + ); + } + + public function testDateAndOrTime_DateWithMonth() + { + $this->assertDateAndOrTimeEqualsTo( + '--01', + [ + 'month' => '01', + ] + ); + } + + public function testDateAndOrTime_DateWithMonthDay() + { + $this->assertDateAndOrTimeEqualsTo( + '--0128', + [ + 'month' => '01', + 'date' => '28', + ] + ); + } + + public function testDateAndOrTime_DateWithDay() + { + $this->assertDateAndOrTimeEqualsTo( + '---28', + [ + 'date' => '28', + ] + ); + } + + public function testDateAndOrTime_TimeWithHour() + { + $this->assertDateAndOrTimeEqualsTo( + '13', + [ + 'hour' => '13', + ] + ); + } + + public function testDateAndOrTime_TimeWithHourMinute() + { + $this->assertDateAndOrTimeEqualsTo( + '1353', + [ + 'hour' => '13', + 'minute' => '53', + ] + ); + } + + public function testDateAndOrTime_TimeWithHourSecond() + { + $this->assertDateAndOrTimeEqualsTo( + '135301', + [ + 'hour' => '13', + 'minute' => '53', + 'second' => '01', + ] + ); + } + + public function testDateAndOrTime_TimeWithMinute() + { + $this->assertDateAndOrTimeEqualsTo( + '-53', + [ + 'minute' => '53', + ] + ); + } + + public function testDateAndOrTime_TimeWithMinuteSecond() + { + $this->assertDateAndOrTimeEqualsTo( + '-5301', + [ + 'minute' => '53', + 'second' => '01', + ] + ); + } + + public 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. + */ + } + + public function testDateAndOrTime_TimeWithSecondZ() + { + $this->assertDateAndOrTimeEqualsTo( + '--01Z', + [ + 'second' => '01', + 'timezone' => 'Z', + ] + ); + } + + public function testDateAndOrTime_TimeWithSecondTZ() + { + $this->assertDateAndOrTimeEqualsTo( + '--01+1234', + [ + 'second' => '01', + 'timezone' => '+1234', + ] + ); + } + + public function testDateAndOrTime_DateTimeWithYearMonthDayHour() + { + $this->assertDateAndOrTimeEqualsTo( + '20150128T13', + [ + 'year' => '2015', + 'month' => '01', + 'date' => '28', + 'hour' => '13', + ] + ); + } + + public function testDateAndOrTime_DateTimeWithMonthDayHour() + { + $this->assertDateAndOrTimeEqualsTo( + '--0128T13', + [ + 'month' => '01', + 'date' => '28', + 'hour' => '13', + ] + ); + } + + public function testDateAndOrTime_DateTimeWithDayHour() + { + $this->assertDateAndOrTimeEqualsTo( + '---28T13', + [ + 'date' => '28', + 'hour' => '13', + ] + ); + } + + public function testDateAndOrTime_DateTimeWithDayHourMinute() + { + $this->assertDateAndOrTimeEqualsTo( + '---28T1353', + [ + 'date' => '28', + 'hour' => '13', + 'minute' => '53', + ] + ); + } + + public function testDateAndOrTime_DateTimeWithDayHourMinuteSecond() + { + $this->assertDateAndOrTimeEqualsTo( + '---28T135301', + [ + 'date' => '28', + 'hour' => '13', + 'minute' => '53', + 'second' => '01', + ] + ); + } + + public function testDateAndOrTime_DateTimeWithDayHourZ() + { + $this->assertDateAndOrTimeEqualsTo( + '---28T13Z', + [ + 'date' => '28', + 'hour' => '13', + 'timezone' => 'Z', + ] + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/DocumentTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/DocumentTest.php new file mode 100644 index 0000000..f2698f6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/DocumentTest.php @@ -0,0 +1,84 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class DocumentTest extends TestCase +{ + public function testGetDocumentType() + { + $doc = new MockDocument(); + $this->assertEquals(Document::UNKNOWN, $doc->getDocumentType()); + } + + public function testConstruct() + { + $doc = new MockDocument('VLIST'); + $this->assertEquals('VLIST', $doc->name); + } + + public function testCreateComponent() + { + $vcal = new Component\VCalendar([], false); + + $event = $vcal->createComponent('VEVENT'); + + $this->assertInstanceOf(Component\VEvent::class, $event); + $vcal->add($event); + + $prop = $vcal->createProperty('X-PROP', '1234256', ['X-PARAM' => '3']); + $this->assertInstanceOf(Property::class, $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); + } + + public function testCreate() + { + $vcal = new Component\VCalendar([], false); + + $event = $vcal->create('VEVENT'); + $this->assertInstanceOf(Component\VEvent::class, $event); + + $prop = $vcal->create('CALSCALE'); + $this->assertInstanceOf(Property\Text::class, $prop); + } + + public function testGetClassNameForPropertyValue() + { + $vcal = new Component\VCalendar([], false); + $this->assertEquals(Property\Text::class, $vcal->getClassNameForPropertyValue('TEXT')); + $this->assertNull($vcal->getClassNameForPropertyValue('FOO')); + } + + public function testDestroy() + { + $vcal = new Component\VCalendar([], false); + $event = $vcal->createComponent('VEVENT'); + + $this->assertInstanceOf(Component\VEvent::class, $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ElementListTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ElementListTest.php new file mode 100644 index 0000000..f3bb8f2 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ElementListTest.php @@ -0,0 +1,33 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class ElementListTest extends TestCase +{ + public 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(Component::class, $subcomponent); + + if (3 === $count) { + $this->assertEquals(2, $key); + } + } + $this->assertEquals(3, $count); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmClientTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmClientTest.php new file mode 100644 index 0000000..bb586ba --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmClientTest.php @@ -0,0 +1,55 @@ +<?php + +namespace Sabre\VObject; + +use DateTimeImmutable; +use PHPUnit\Framework\TestCase; + +class EmClientTest extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmptyParameterTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmptyParameterTest.php new file mode 100644 index 0000000..52fe878 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmptyParameterTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class EmptyParameterTest extends TestCase +{ + public 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(Component\VCard::class, $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)); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmptyValueIssueTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmptyValueIssueTest.php new file mode 100644 index 0000000..91a4d84 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/EmptyValueIssueTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +/** + * This test is written for Issue 68:. + * + * https://github.com/fruux/sabre-vobject/issues/68 + */ +class EmptyValueIssueTest extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/FreeBusyDataTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/FreeBusyDataTest.php new file mode 100644 index 0000000..9e5143f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/FreeBusyDataTest.php @@ -0,0 +1,311 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class FreeBusyDataTest extends TestCase +{ + public function testGetData() + { + $fb = new FreeBusyData(100, 200); + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 200, + 'type' => 'FREE', + ], + ], + $fb->getData() + ); + } + + /** + * @depends testGetData + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/FreeBusyGeneratorTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/FreeBusyGeneratorTest.php new file mode 100644 index 0000000..4700a28 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/FreeBusyGeneratorTest.php @@ -0,0 +1,719 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class FreeBusyGeneratorTest extends TestCase +{ + use PHPUnitAssertions; + + public 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()); + } + + public function testInvalidArg() + { + $this->expectException(\InvalidArgumentException::class); + $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 + */ + public 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); + } + + public 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 + ); + } + + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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') + ); + } + + public 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 + ); + } + + public 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') + ); + } + + public 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. + */ + public 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. + */ + public 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. + */ + public 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). + */ + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/GoogleColonEscapingTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/GoogleColonEscapingTest.php new file mode 100644 index 0000000..48bbde0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/GoogleColonEscapingTest.php @@ -0,0 +1,31 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +/** + * 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 TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ICalendar/AttachParseTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ICalendar/AttachParseTest.php new file mode 100644 index 0000000..e6e3d86 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ICalendar/AttachParseTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\VObject\ICalendar; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject\Property\Uri; +use Sabre\VObject\Reader; + +class AttachParseTest extends TestCase +{ + /** + * See issue #128 for more info. + */ + public 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(Uri::class, $prop); + $this->assertEquals('ftp://example.com/pub/reports/r-960812.ps', $prop->getValue()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerAttendeeReplyTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerAttendeeReplyTest.php new file mode 100644 index 0000000..71008c6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerAttendeeReplyTest.php @@ -0,0 +1,1213 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerAttendeeReplyTest extends BrokerTester +{ + public 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); + } + + public function testAcceptedWithTz() + { + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:(UTC+01:00) Brussels\, Copenhagen\, Madrid\, Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +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;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T120000Z +DTEND;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:(UTC+01:00) Brussels\, Copenhagen\, Madrid\, Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +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;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T120000Z +DTEND;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T130000Z +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:VTIMEZONE +TZID:(UTC+01:00) Brussels\, Copenhagen\, Madrid\, Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T120000Z +DTEND;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T130000Z +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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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. + */ + public 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 + */ + public 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 + */ + public 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); + } + + public 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); + } + + public 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. + */ + public 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); + } + + public 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); + } + + public 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. + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerDeleteEventTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerDeleteEventTest.php new file mode 100644 index 0000000..88f30ec --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerDeleteEventTest.php @@ -0,0 +1,326 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerDeleteEventTest extends BrokerTester +{ + public 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'); + } + + public 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'); + } + + public 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'); + } + + public 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'); + } + + public 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'); + } + + public function testNoCalendar() + { + $this->parse(null, null, [], 'mailto:one@example.org'); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerNewEventTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerNewEventTest.php new file mode 100644 index 0000000..f5b89b0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerNewEventTest.php @@ -0,0 +1,578 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerNewEventTest extends BrokerTester +{ + public function testNoAttendee() + { + $message = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART:20140811T220000Z +DTEND:20140811T230000Z +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->parse(null, $message, []); + } + + public function testVTODO() + { + $message = <<<ICS +BEGIN:VCALENDAR +BEGIN:VTODO +UID:foobar +END:VTODO +END:VCALENDAR +ICS; + + $result = $this->parse(null, $message, []); + } + + public 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 +DTSTAMP:**ANY** +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'); + } + + public function testBrokenEventUIDMisMatch() + { + $this->expectException(ITipException::class); + $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'); + } + + public function testBrokenEventOrganizerMisMatch() + { + $this->expectException(ITipException::class); + $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'); + } + + public 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 +DTSTAMP:**ANY** +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 +DTSTAMP:**ANY** +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 +DTSTAMP:**ANY** +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 +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse(null, $message, $expected, 'mailto:strunk@example.org'); + } + + public 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 +DTSTAMP:**ANY** +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 +DTSTAMP:**ANY** +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 +DTSTAMP:**ANY** +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 +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse(null, $message, $expected, 'mailto:strunk@example.org'); + } + + public function testRecurrenceInvite3() + { + // This method tests a complex rrule + $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 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;BYDAY=SA,SU +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 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;BYDAY=SA,SU +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse(null, $message, $expected, 'mailto:strunk@example.org'); + } + + public 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'); + } + + public function testMultipleUID() + { + $this->expectException(ITipException::class); + $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'); + } + + public function testChangingOrganizers() + { + $this->expectException(SameOrganizerForAllComponentsException::class); + $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; + + $this->parse(null, $message, [], 'mailto:strunk@example.org'); + } + + public function testCaseInsensitiveOrganizers() + { + $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; + + $this->parse(null, $message, [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + ], + ['uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + ], + ], 'mailto:strunk@example.org'); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerProcessMessageTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerProcessMessageTest.php new file mode 100644 index 0000000..3327be0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerProcessMessageTest.php @@ -0,0 +1,157 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerProcessMessageTest extends BrokerTester +{ + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerProcessReplyTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerProcessReplyTest.php new file mode 100644 index 0000000..1cb6850 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerProcessReplyTest.php @@ -0,0 +1,583 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerProcessReplyTest extends BrokerTester +{ + public 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); + } + + public 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); + } + + public function testReplyWithTz() + { + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VTIMEZONE +TZID:(UTC+01:00) Brussels\, Copenhagen\, Madrid\, Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +UID:foobar +DTSTART;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T120000Z +DTEND;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:(UTC+01:00) Brussels\, Copenhagen\, Madrid\, Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +DTSTART;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T120000Z +DTEND;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:(UTC+01:00) Brussels\, Copenhagen\, Madrid\, Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +DTSTART;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T120000Z +DTEND;TZID="(UTC+01:00) Brussels, Copenhagen, Madrid, Paris":20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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 + */ + public 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerSignificantChangesTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerSignificantChangesTest.php new file mode 100644 index 0000000..a225cb9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerSignificantChangesTest.php @@ -0,0 +1,108 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerSignificantChangesTest extends BrokerTester +{ + /** + * Check significant changes detection (no change). + */ + public function testSignificantChangesNoChange() + { + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//Ximian//NONSGML Evolution Calendar//EN +BEGIN:VEVENT +UID:20140813T153116Z-12176-1000-1065-6@johnny-lubuntu +DTSTAMP:20140813T142829Z +DTSTART;TZID=America/Toronto:20140815T110000 +SEQUENCE:2 +SUMMARY:Evo makes a Meeting +LOCATION:fruux HQ +CLASS:PUBLIC +RRULE:FREQ=WEEKLY +ORGANIZER: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; + + $new = $old; + $expected = [['significantChange' => false]]; + + $this->parse($old, $new, $expected, 'mailto:martin@fruux.com'); + } + + /** + * Check significant changes detection (no change). + */ + public function testSignificantChangesRRuleNoChange() + { + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//Ximian//NONSGML Evolution Calendar//EN +BEGIN:VEVENT +UID:20140813T153116Z-12176-1000-1065-6@johnny-lubuntu +DTSTAMP:20140813T142829Z +DTSTART;TZID=America/Toronto:20140815T110000 +SEQUENCE:2 +SUMMARY:Evo makes a Meeting +LOCATION:fruux HQ +CLASS:PUBLIC +RRULE:FREQ=WEEKLY +ORGANIZER: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; + + $new = str_replace('FREQ=WEEKLY', 'FREQ=WEEKLY;INTERVAL=1', $old); + $expected = [['significantChange' => false]]; + + $this->parse($old, $new, $expected, 'mailto:martin@fruux.com'); + } + + /** + * Check significant changes detection (no change). + */ + public function testSignificantChangesRRuleOrderNoChange() + { + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//Ximian//NONSGML Evolution Calendar//EN +BEGIN:VEVENT +UID:20140813T153116Z-12176-1000-1065-6@johnny-lubuntu +DTSTAMP:20140813T142829Z +DTSTART;TZID=America/Toronto:20140815T110000 +SEQUENCE:2 +SUMMARY:Evo makes a Meeting +LOCATION:fruux HQ +CLASS:PUBLIC +RRULE:FREQ=WEEKLY;BYDAY=MO +ORGANIZER: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; + + $new = str_replace('FREQ=WEEKLY;BYDAY=MO', 'BYDAY=MO;FREQ=WEEKLY', $old); + $expected = [['significantChange' => false]]; + + $this->parse($old, $new, $expected, 'mailto:martin@fruux.com'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerTester.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerTester.php new file mode 100644 index 0000000..9e9956e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerTester.php @@ -0,0 +1,93 @@ +<?php + +namespace Sabre\VObject\ITip; + +use PHPUnit\Framework\TestCase; +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 TestCase +{ + use \Sabre\VObject\PHPUnitAssertions; + + public 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 ('message' === $key) { + $this->assertVObjectEqualsVObject( + $val, + $message->message->serialize() + ); + } else { + $this->assertEquals($val, $message->$key); + } + } + } + } + + public function process($input, $existingObject = null, $expected = false) + { + $version = \Sabre\VObject\Version::VERSION; + + $vcal = Reader::read($input); + + $mainComponent = new \Sabre\VObject\Component\VEvent($vcal, 'VEVENT'); + foreach ($vcal->getComponents() as $mainComponent) { + if ('VEVENT' === $mainComponent->name) { + 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 ('REPLY' === $message->method) { + $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerTimezoneInParseEventInfoWithoutMasterTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerTimezoneInParseEventInfoWithoutMasterTest.php new file mode 100644 index 0000000..1635be7 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerTimezoneInParseEventInfoWithoutMasterTest.php @@ -0,0 +1,78 @@ +<?php + +namespace Sabre\VObject\ITip; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject\Reader; + +class BrokerTimezoneInParseEventInfoWithoutMasterTest extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerUpdateEventTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerUpdateEventTest.php new file mode 100644 index 0000000..e93f896 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/BrokerUpdateEventTest.php @@ -0,0 +1,817 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerUpdateEventTest extends BrokerTester +{ + public 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 +DTSTAMP:**ANY** +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 +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } + + public 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 +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } + + public 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'); + } + + public 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'); + } + + public 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 +DTSTAMP:**ANY** +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, + */ + public 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 +DTSTAMP:**ANY** +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 +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } + + public 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 +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } + + public 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 +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } + + public 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'); + } + + public 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 +DTSTAMP:**ANY** +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/EvolutionTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/EvolutionTest.php new file mode 100644 index 0000000..686a94d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/EvolutionTest.php @@ -0,0 +1,2647 @@ +<?php + +namespace Sabre\VObject\ITip; + +class EvolutionTest extends BrokerTester +{ + /** + * Evolution does things as usual a little bit differently. + * + * We're adding a separate test just for it. + */ + public 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 +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 +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 +DTSTAMP:**ANY** +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. + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/MessageTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/MessageTest.php new file mode 100644 index 0000000..56fcf82 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ITip/MessageTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\VObject\ITip; + +use PHPUnit\Framework\TestCase; + +class MessageTest extends TestCase +{ + public function testNoScheduleStatus() + { + $message = new Message(); + $this->assertFalse($message->getScheduleStatus()); + } + + public function testScheduleStatus() + { + $message = new Message(); + $message->scheduleStatus = '1.2;Delivered'; + + $this->assertEquals('1.2', $message->getScheduleStatus()); + } + + public function testUnexpectedScheduleStatus() + { + $message = new Message(); + $message->scheduleStatus = '9.9.9'; + + $this->assertEquals('9.9.9', $message->getScheduleStatus()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue153Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue153Test.php new file mode 100644 index 0000000..3090250 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue153Test.php @@ -0,0 +1,14 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class Issue153Test extends TestCase +{ + public function testRead() + { + $obj = Reader::read(file_get_contents(dirname(__FILE__).'/issue153.vcf')); + $this->assertEquals('Test Benutzer', (string) $obj->FN); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue259Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue259Test.php new file mode 100644 index 0000000..3227fcd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue259Test.php @@ -0,0 +1,23 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class Issue259Test extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue36WorkAroundTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue36WorkAroundTest.php new file mode 100644 index 0000000..1afd3d1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue36WorkAroundTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class Issue36WorkAroundTest extends TestCase +{ + public 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(Recur\EventIterator::class, $it); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue40Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue40Test.php new file mode 100644 index 0000000..1cf9986 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue40Test.php @@ -0,0 +1,32 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +/** + * This test is created to handle the issues brought forward by issue 40. + * + * https://github.com/fruux/sabre-vobject/issues/40 + */ +class Issue40Test extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue64Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue64Test.php new file mode 100644 index 0000000..2e623ba --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue64Test.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class Issue64Test extends TestCase +{ + public 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(Component\VCard::class, $converted); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue96Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue96Test.php new file mode 100644 index 0000000..88803a3 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Issue96Test.php @@ -0,0 +1,24 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class Issue96Test extends TestCase +{ + public 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(Component\VCard::class, $vcard); + $this->assertEquals('http://www.example.org', $vcard->URL->getValue()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/IssueUndefinedIndexTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/IssueUndefinedIndexTest.php new file mode 100644 index 0000000..7ed214a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/IssueUndefinedIndexTest.php @@ -0,0 +1,27 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class IssueUndefinedIndexTest extends TestCase +{ + public function testRead() + { + $this->expectException(ParseException::class); + $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/JCalTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/JCalTest.php new file mode 100644 index 0000000..9332b64 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/JCalTest.php @@ -0,0 +1,149 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class JCalTest extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/JCardTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/JCardTest.php new file mode 100644 index 0000000..1864f66 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/JCardTest.php @@ -0,0 +1,194 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class JCardTest extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/LineFoldingIssueTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/LineFoldingIssueTest.php new file mode 100644 index 0000000..3d4f26a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/LineFoldingIssueTest.php @@ -0,0 +1,23 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class LineFoldingIssueTest extends TestCase +{ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ParameterTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ParameterTest.php new file mode 100644 index 0000000..0e23b06 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ParameterTest.php @@ -0,0 +1,124 @@ +<?php + +namespace Sabre\VObject; + +use PHPUnit\Framework\TestCase; + +class ParameterTest extends TestCase +{ + public function testSetup() + { + $cal = new Component\VCalendar(); + + $param = new Parameter($cal, 'name', 'value'); + $this->assertEquals('NAME', $param->name); + $this->assertEquals('value', $param->getValue()); + } + + public 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); + } + + public 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()); + } + + public function testCastToString() + { + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', 'value'); + $this->assertEquals('value', $param->__toString()); + $this->assertEquals('value', (string) $param); + } + + public function testCastNullToString() + { + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', null); + $this->assertEquals('', $param->__toString()); + $this->assertEquals('', (string) $param); + } + + public function testSerialize() + { + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', 'value'); + $this->assertEquals('NAME=value', $param->serialize()); + } + + public function testSerializeEmpty() + { + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', null); + $this->assertEquals('NAME=', $param->serialize()); + } + + public 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. + */ + public function testSerializePlusSign() + { + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'EMAIL', 'user+something@example.org'); + $this->assertEquals('EMAIL="user+something@example.org"', $param->serialize()); + } + + public 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); + } + + public function testSerializeColon() + { + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', 'va:lue'); + $this->assertEquals('NAME="va:lue"', $param->serialize()); + } + + public function testSerializeSemiColon() + { + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', 'va;lue'); + $this->assertEquals('NAME="va;lue"', $param->serialize()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/JsonTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/JsonTest.php new file mode 100644 index 0000000..e1c7014 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/JsonTest.php @@ -0,0 +1,390 @@ +<?php + +namespace Sabre\VObject\Parser; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject; +use Sabre\VObject\ParseException; + +class JsonTest extends TestCase +{ + public 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() + ); + } + + public 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() + ); + } + + public 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()); + } + + public function testParseInvalidData() + { + $this->expectException(ParseException::class); + $json = new Json(); + $input = [ + 'vlist', + [ + [ + 'FN', new \stdClass(), 'text', 'foo', + ], + ], + ]; + + $json->parse(json_encode($input), 0); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/MimeDirTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/MimeDirTest.php new file mode 100644 index 0000000..183c9ce --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/MimeDirTest.php @@ -0,0 +1,148 @@ +<?php + +namespace Sabre\VObject\Parser; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject\ParseException; + +/** + * Note that most MimeDir related tests can actually be found in the ReaderTest + * class one level up. + */ +class MimeDirTest extends TestCase +{ + public function testParseError() + { + $this->expectException(ParseException::class); + $mimeDir = new MimeDir(); + $mimeDir->parse(fopen(__FILE__, 'a+')); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public function testDecodeUnsupportedCharset() + { + $this->expectException(\InvalidArgumentException::class); + $mimeDir = new MimeDir(); + $mimeDir->setCharset('foobar'); + } + + public function testDecodeUnsupportedInlineCharset() + { + $this->expectException(ParseException::class); + $vcard = <<<VCF +BEGIN:VCARD +VERSION:2.1 +FN;CHARSET=foobar:nothing +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $mimeDir->parse($vcard); + } + + public 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()); + } + + public 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()); + } + + public function testCaseInsensitiveInlineCharset() + { + $vcard = <<<VCF +BEGIN:VCARD +VERSION:2.1 +FN;CHARSET=iSo-8859-1:Euro +N;CHARSET=utf-8:Test2 +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $vcard = $mimeDir->parse($vcard); + // we can do a simple assertion here. As long as we don't get an exception, everything is thing + $this->assertEquals('Euro', $vcard->FN->getValue()); + $this->assertEquals('Test2', $vcard->N->getValue()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/QuotedPrintableTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/QuotedPrintableTest.php new file mode 100644 index 0000000..7cadb46 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/QuotedPrintableTest.php @@ -0,0 +1,102 @@ +<?php + +namespace Sabre\VObject\Parser; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject\Component; +use Sabre\VObject\Reader; + +class QuotedPrintableTest extends TestCase +{ + public function testReadQuotedPrintableSimple() + { + $data = "BEGIN:VCARD\r\nLABEL;ENCODING=QUOTED-PRINTABLE:Aach=65n\r\nEND:VCARD"; + + $result = Reader::read($data); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCARD', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertEquals('Aachen', $this->getPropertyValue($result->LABEL)); + } + + public 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(Component::class, $result); + $this->assertEquals('VCARD', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertEquals('Aachen', $this->getPropertyValue($result->LABEL)); + } + + public 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(Component::class, $result); + $this->assertEquals('VCARD', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertEquals("Aachen\r\nGermany", $this->getPropertyValue($result->LABEL)); + } + + public 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(Component::class, $result); + $this->assertEquals('VCARD', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertEquals("Aachen\r\nDeutschland:okay", $this->getPropertyValue($result->LABEL)); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/XmlTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/XmlTest.php new file mode 100644 index 0000000..e520185 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Parser/XmlTest.php @@ -0,0 +1,2808 @@ +<?php + +namespace Sabre\VObject\Parser; + +use PHPUnit\Framework\TestCase; +use Sabre\VObject; + +class XmlTest extends TestCase +{ + use VObject\PHPUnitAssertions; + + public 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" + ); + } + + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public function testRFC6321Section3_6_7() + { + // GEO uses <float /> with a positive and a non-negative numbers. + $this->testRFC6321Section3_4_1_2(); + } + + /** + * Values, Integer. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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" + ); + } + + public 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" + ); + } + + public 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" + ); + } + + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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" + ); + } + + public 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" + ); + } + + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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" + ); + } + + public function testRFC6350Section6_7_4() + { + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + 19951031T222710Z + + + +XML +, + 'BEGIN:VCARD'."\n". + 'VERSION:4.0'."\n". + 'REV:19951031T222710Z'."\n". + 'END:VCARD'."\n" + ); + } + + /** + * Property: SOUND. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public function testRFC6350Section6_7_9() + { + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + +XML +, + 'BEGIN:VCARD'."\n". + 'VERSION:4.0'."\n". + 'END:VCARD'."\n" + ); + } + + /** + * Property: KEY. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/BinaryTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/BinaryTest.php new file mode 100644 index 0000000..b1c2dad --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/BinaryTest.php @@ -0,0 +1,16 @@ +expectException(\InvalidArgumentException::class); + $vcard = new VObject\Component\VCard(['VERSION' => '3.0']); + $vcard->add('PHOTO', ['a', 'b']); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/BooleanTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/BooleanTest.php new file mode 100644 index 0000000..ad2ccbc --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/BooleanTest.php @@ -0,0 +1,21 @@ +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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/CompoundTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/CompoundTest.php new file mode 100644 index 0000000..bfa1ed6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/CompoundTest.php @@ -0,0 +1,48 @@ +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]); + } + + public 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]); + } + + public function testGetPartsNull() + { + $vcard = new VCard(); + $elem = $vcard->createProperty('ORG', null); + + $this->assertEquals(0, count($elem->getParts())); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/FloatTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/FloatTest.php new file mode 100644 index 0000000..0ba0247 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/FloatTest.php @@ -0,0 +1,29 @@ +parse($input); + + $this->assertInstanceOf(FloatValue::class, $result->{'X-FLOAT'}); + + $this->assertEquals([ + 0.234, + 1.245, + ], $result->{'X-FLOAT'}->getParts()); + + $this->assertEquals( + $input, + $result->serialize() + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/CalAddressTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/CalAddressTest.php new file mode 100644 index 0000000..a907daf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/CalAddressTest.php @@ -0,0 +1,31 @@ +add('ATTENDEE', $input); + + $this->assertEquals( + $expected, + $property->getNormalizedValue() + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/DateTimeTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/DateTimeTest.php new file mode 100644 index 0000000..40c6e2e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/DateTimeTest.php @@ -0,0 +1,347 @@ +vcal = new VCalendar(); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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); + } + + public function testGetDateTimeDateNULL() + { + $elem = $this->vcal->createProperty('DTSTART'); + $dt = $elem->getDateTime(); + + $this->assertNull($dt); + } + + public 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')); + } + + public 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')); + } + + public 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')); + } + + public 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')); + } + + public 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()); + } + + public 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()); + } + + public function testGetDateTimeDateInvalid() + { + $this->expectException(InvalidDataException::class); + $elem = $this->vcal->createProperty('DTSTART', 'bla'); + $dt = $elem->getDateTime(); + } + + public 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()); + } + + public 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); + } + + public 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()); + } + + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/DurationTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/DurationTest.php new file mode 100644 index 0000000..2003ad6 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/RecurTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/RecurTest.php new file mode 100644 index 0000000..3902a6e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/ICalendar/RecurTest.php @@ -0,0 +1,430 @@ +add('RRULE', 'FREQ=Daily'); + + $this->assertInstanceOf(Recur::class, $recur); + + $this->assertEquals(['FREQ' => 'DAILY'], $recur->getParts()); + $recur->setParts(['freq' => 'MONTHLY']); + + $this->assertEquals(['FREQ' => 'MONTHLY'], $recur->getParts()); + } + + public function testSetValueBadVal() + { + $this->expectException(\InvalidArgumentException::class); + $vcal = new VCalendar(); + $recur = $vcal->add('RRULE', 'FREQ=Daily'); + $recur->setValue(new \Exception()); + } + + public function testSetValueWithCount() + { + $vcal = new VCalendar(); + $recur = $vcal->add('RRULE', 'FREQ=Daily'); + $recur->setValue(['COUNT' => 3]); + $this->assertEquals($recur->getParts()['COUNT'], 3); + } + + public 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); + } + + public 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()); + } + + public 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); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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. + */ + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/TextTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/TextTest.php new file mode 100644 index 0000000..eea21b1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/TextTest.php @@ -0,0 +1,87 @@ + '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); + } + + public function testSerializeVCard21() + { + $this->assertVCard21Serialization( + 'f;oo', + 'PROP;P1=V1:f;oo' + ); + } + + public function testSerializeVCard21Array() + { + $this->assertVCard21Serialization( + ['f;oo', 'bar'], + 'PROP;P1=V1:f\;oo;bar' + ); + } + + public function testSerializeVCard21Fold() + { + $this->assertVCard21Serialization( + str_repeat('x', 80), + 'PROP;P1=V1:'.str_repeat('x', 64)."\r\n ".str_repeat('x', 16) + ); + } + + public function testSerializeQuotedPrintable() + { + $this->assertVCard21Serialization( + "foo\r\nbar", + 'PROP;P1=V1;ENCODING=QUOTED-PRINTABLE:foo=0D=0Abar' + ); + } + + public function testSerializeQuotedPrintableFold() + { + $this->assertVCard21Serialization( + "foo\r\nbarxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "PROP;P1=V1;ENCODING=QUOTED-PRINTABLE:foo=0D=0Abarxxxxxxxxxxxxxxxxxxxxxxxxxx=\r\n xxx" + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/UriTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/UriTest.php new file mode 100644 index 0000000..d3d0e0f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/UriTest.php @@ -0,0 +1,26 @@ +serialize(); + $this->assertStringContainsString('URL;VALUE=URI:http://example.org/', $output); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/DateAndOrTimeTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/DateAndOrTimeTest.php new file mode 100644 index 0000000..f21b408 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/DateAndOrTimeTest.php @@ -0,0 +1,253 @@ +createProperty('BDAY', $input); + + $this->assertEquals([$output], $prop->getJsonValue()); + } + + public 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', + ], + ]; + } + + public 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()); + } + + public 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()); + } + + public function testSetPartsTooMany() + { + $this->expectException(\InvalidArgumentException::class); + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setParts([ + 1, + 2, + ]); + } + + public function testSetPartsString() + { + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setParts([ + '20140402T183700Z', + ]); + + $this->assertEquals('20140402T183700Z', $prop->getValue()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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()); + } + + public 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())); + } + + public 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')); + } + + public 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')); + } + + public function testValidate() + { + $datetime = '--0407'; + + $vcard = new VObject\Component\VCard(); + $prop = $vcard->add('BDAY', $datetime); + + $this->assertEquals([], $prop->validate()); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/LanguageTagTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/LanguageTagTest.php new file mode 100644 index 0000000..54106ff --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/LanguageTagTest.php @@ -0,0 +1,47 @@ +parse($input); + + $this->assertInstanceOf(LanguageTag::class, $result->LANG); + + $this->assertEquals('nl', $result->LANG->getValue()); + + $this->assertEquals( + $input, + $result->serialize() + ); + } + + public 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(LanguageTag::class, $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/PhoneNumberTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/PhoneNumberTest.php new file mode 100644 index 0000000..668bc7e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Property/VCard/PhoneNumberTest.php @@ -0,0 +1,19 @@ +assertInstanceOf(PhoneNumber::class, $vCard->TEL); + $this->assertEquals('PHONE-NUMBER', $vCard->TEL->getValueType()); + $this->assertEquals($input, $vCard->serialize()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/PropertyTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/PropertyTest.php new file mode 100644 index 0000000..1f6e070 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/PropertyTest.php @@ -0,0 +1,388 @@ +createProperty('propname', 'propvalue'); + $this->assertEquals('PROPNAME', $property->name); + $this->assertEquals('propvalue', $property->__toString()); + $this->assertEquals('propvalue', (string) $property); + $this->assertEquals('propvalue', $property->getValue()); + } + + public 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()); + } + + public function testSetValue() + { + $cal = new VCalendar(); + + $property = $cal->createProperty('propname', 'propvalue'); + $property->setValue('value2'); + + $this->assertEquals('PROPNAME', $property->name); + $this->assertEquals('value2', $property->__toString()); + } + + public 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'])); + } + + public function testParameterGet() + { + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + $this->assertInstanceOf(Parameter::class, $property['paramname']); + } + + public function testParameterNotExists() + { + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + $this->assertNull($property['foo']); + } + + public function testParameterMultiple() + { + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + $property->add('paramname', 'paramvalue'); + + $this->assertInstanceOf(Parameter::class, $property['paramname']); + $this->assertEquals(2, count($property['paramname']->getParts())); + } + + public function testSetParameterAsString() + { + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + $this->assertEquals(1, count($property->parameters())); + $this->assertInstanceOf(Parameter::class, $property->parameters['PARAMNAME']); + $this->assertEquals('PARAMNAME', $property->parameters['PARAMNAME']->name); + $this->assertEquals('paramvalue', $property->parameters['PARAMNAME']->getValue()); + } + + public function testUnsetParameter() + { + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + unset($property['PARAMNAME']); + $this->assertEquals(0, count($property->parameters())); + } + + public function testSerialize() + { + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + + $this->assertEquals("PROPNAME:propvalue\r\n", $property->serialize()); + } + + public 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()); + } + + public function testSerializeNewLine() + { + $cal = new VCalendar(); + $property = $cal->createProperty('SUMMARY', "line1\nline2"); + + $this->assertEquals("SUMMARY:line1\\nline2\r\n", $property->serialize()); + } + + public 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()); + } + + public function testSerializeUTF8LineFold() + { + $cal = new VCalendar(); + $value = str_repeat('!', 65)."\xc3\xa4bla".str_repeat('!', 142)."\xc3\xa4foo"; // inserted umlaut-a + $property = $cal->createProperty('propname', $value); + + // PROPNAME:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ("PROPNAME:" + 65x"!" = 74 bytes) + // äbla!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! (" äbla" + 69x"!" = 75 bytes) + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! (" " + 73x"!" = 74 bytes) + // äfoo + $expected = 'PROPNAME:'.str_repeat('!', 65)."\r\n \xc3\xa4bla".str_repeat('!', 69)."\r\n ".str_repeat('!', 73)."\r\n \xc3\xa4foo\r\n"; + $this->assertEquals($expected, $property->serialize()); + } + + public function testGetIterator() + { + $cal = new VCalendar(); + $it = new ElementList([]); + $property = $cal->createProperty('propname', 'propvalue'); + $property->setIterator($it); + $this->assertEquals($it, $property->getIterator()); + } + + public function testGetIteratorDefault() + { + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $it = $property->getIterator(); + $this->assertTrue($it instanceof ElementList); + $this->assertEquals(1, count($it)); + } + + public 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()); + } + + public 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); + } + + public 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); + } + + public 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']); + } + + public 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())); + } + + public 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()); + } + + public 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()); + } + + public 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); + } + + public 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. + */ + public function testArrayAccessSetInt() + { + $this->expectException(\LogicException::class); + $calendar = new VCalendar(); + $property = $calendar->createProperty('X-PROP', null); + + $calendar->add($property); + $calendar->{'X-PROP'}[0] = 'Something!'; + } + + /** + * ElementList should reject this. + */ + public function testArrayAccessUnsetInt() + { + $this->expectException(\LogicException::class); + $calendar = new VCalendar(); + $property = $calendar->createProperty('X-PROP', null); + + $calendar->add($property); + unset($calendar->{'X-PROP'}[0]); + } + + public 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']); + } + + public 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']); + } + + public 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']); + + //Validate the reparation of BASE64 formatted vCard v3 + $result = $property->validate(Property::REPAIR); + + $this->assertEquals('ENCODING=BASE64 has been transformed to ENCODING=B.', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ReaderTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ReaderTest.php new file mode 100644 index 0000000..0992806 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/ReaderTest.php @@ -0,0 +1,453 @@ +assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + } + + public function testReadStream() + { + $data = "BEGIN:VCALENDAR\r\nEND:VCALENDAR"; + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $data); + rewind($stream); + + $result = Reader::read($stream); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + } + + public function testReadComponentUnixNewLine() + { + $data = "BEGIN:VCALENDAR\nEND:VCALENDAR"; + + $result = Reader::read($data); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + } + + public function testReadComponentLineFold() + { + $data = "BEGIN:\r\n\tVCALENDAR\r\nE\r\n ND:VCALENDAR"; + + $result = Reader::read($data); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + } + + public function testReadCorruptComponent() + { + $this->expectException(ParseException::class); + $data = "BEGIN:VCALENDAR\r\nEND:FOO"; + + $result = Reader::read($data); + } + + public function testReadCorruptSubComponent() + { + $this->expectException(ParseException::class); + $data = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:FOO\r\nEND:VCALENDAR"; + + $result = Reader::read($data); + } + + public function testReadProperty() + { + $data = "BEGIN:VCALENDAR\r\nSUMMARY:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->SUMMARY; + $this->assertInstanceOf(Property::class, $result); + $this->assertEquals('SUMMARY', $result->name); + $this->assertEquals('propValue', $result->getValue()); + } + + public 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(Property::class, $result); + $this->assertEquals('SUMMARY', $result->name); + $this->assertEquals("Line1\nLine2\nLine3\\Not the 4th line!", $result->getValue()); + } + + public function testReadMappedProperty() + { + $data = "BEGIN:VCALENDAR\r\nDTSTART:20110529\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->DTSTART; + $this->assertInstanceOf(Property\ICalendar\DateTime::class, $result); + $this->assertEquals('DTSTART', $result->name); + $this->assertEquals('20110529', $result->getValue()); + } + + public function testReadMappedPropertyGrouped() + { + $data = "BEGIN:VCALENDAR\r\nfoo.DTSTART:20110529\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->DTSTART; + $this->assertInstanceOf(Property\ICalendar\DateTime::class, $result); + $this->assertEquals('DTSTART', $result->name); + $this->assertEquals('20110529', $result->getValue()); + } + + public function testReadBrokenLine() + { + $this->expectException(ParseException::class); + $data = "BEGIN:VCALENDAR\r\nPROPNAME;propValue"; + $result = Reader::read($data); + } + + public function testReadPropertyInComponent() + { + $data = [ + 'BEGIN:VCALENDAR', + 'PROPNAME:propValue', + 'END:VCALENDAR', + ]; + + $result = Reader::read(implode("\r\n", $data)); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertInstanceOf(Property::class, $result->children()[0]); + $this->assertEquals('PROPNAME', $result->children()[0]->name); + $this->assertEquals('propValue', $result->children()[0]->getValue()); + } + + public function testReadNestedComponent() + { + $data = [ + 'BEGIN:VCALENDAR', + 'BEGIN:VTIMEZONE', + 'BEGIN:DAYLIGHT', + 'END:DAYLIGHT', + 'END:VTIMEZONE', + 'END:VCALENDAR', + ]; + + $result = Reader::read(implode("\r\n", $data)); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertInstanceOf(Component::class, $result->children()[0]); + $this->assertEquals('VTIMEZONE', $result->children()[0]->name); + $this->assertEquals(1, count($result->children()[0]->children())); + $this->assertInstanceOf(Component::class, $result->children()[0]->children()[0]); + $this->assertEquals('DAYLIGHT', $result->children()[0]->children()[0]->name); + } + + public function testReadPropertyParameter() + { + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=paramvalue:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf(Property::class, $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()); + } + + public 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(Property::class, $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()); + } + + public function testReadPropertyRepeatingNamelessGuessedParameter() + { + $data = "BEGIN:VCALENDAR\r\nPROPNAME;WORK;VOICE;PREF:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf(Property::class, $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()); + } + + public function testReadPropertyNoName() + { + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PRODIGY:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf(Property::class, $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']); + } + + public function testReadPropertyParameterExtraColon() + { + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=paramvalue:propValue:anotherrandomstring\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf(Property::class, $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()); + } + + public function testReadProperty2Parameters() + { + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=paramvalue;PARAMNAME2=paramvalue2:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf(Property::class, $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()); + } + + public function testReadPropertyParameterQuoted() + { + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=\"paramvalue\":propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf(Property::class, $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()); + } + + public function testReadPropertyParameterNewLines() + { + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=paramvalue1^nvalue2^^nvalue3:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf(Property::class, $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()); + } + + public function testReadPropertyParameterQuotedColon() + { + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=\"param:value\":propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + $result = $result->PROPNAME; + + $this->assertInstanceOf(Property::class, $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()); + } + + public 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()); + } + + public 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. + */ + public function testReadIncompleteFile() + { + $this->expectException(ParseException::class); + $input = <<expectException(\InvalidArgumentException::class); + Reader::read(false); + } + + public function testReadBOM() + { + $data = chr(0xef).chr(0xbb).chr(0xbf)."BEGIN:VCALENDAR\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + } + + public function testReadXMLComponent() + { + $data = << + + + + +XML; + + $result = Reader::readXML($data); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + } + + public function testReadXMLStream() + { + $data = << + + + + +XML; + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $data); + rewind($stream); + + $result = Reader::readXML($stream); + + $this->assertInstanceOf(Component::class, $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php new file mode 100644 index 0000000..bd1eeb9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php @@ -0,0 +1,60 @@ +assertInstanceOf(VCalendar::class, $vcal); + + $vcal = $vcal->expand(new DateTime('2013-09-28'), new DateTime('2014-09-11')); + + $dates = []; + 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/BySetPosHangTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/BySetPosHangTest.php new file mode 100644 index 0000000..555f2ff --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/BySetPosHangTest.php @@ -0,0 +1,62 @@ +assertInstanceOf(VCalendar::class, $vcal); + + $vcal = $vcal->expand(new DateTime('2015-01-01'), new DateTime('2016-01-01')); + + $dates = []; + 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php new file mode 100644 index 0000000..605e10d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php @@ -0,0 +1,121 @@ +assertInstanceOf(VCalendar::class, $vcal); + + $vcal = $vcal->expand(new DateTime('2015-01-01'), new DateTime('2015-01-31')); + $output = <<assertVObjectEqualsVObject($output, $vcal); + } + + public function testExpandWithReferenceTimezone() + { + $input = <<assertInstanceOf(VCalendar::class, $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/FifthTuesdayProblemTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/FifthTuesdayProblemTest.php new file mode 100644 index 0000000..3db97ed --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/FifthTuesdayProblemTest.php @@ -0,0 +1,53 @@ +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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php new file mode 100644 index 0000000..5488201 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php @@ -0,0 +1,60 @@ +assertInstanceOf(VCalendar::class, $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php new file mode 100644 index 0000000..f9fcda4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php @@ -0,0 +1,62 @@ +assertInstanceOf(VCalendar::class, $vcal); + + $vcal = $vcal->expand(new DateTime('2011-01-01'), new DateTime('2014-01-01')); + + $output = <<assertVObjectEqualsVObject($output, $vcal); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php new file mode 100644 index 0000000..5546c50 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php @@ -0,0 +1,95 @@ +vcal = new VCalendar(); + } + + /** + * This bug came from a Fruux customer. This would result in a never-ending + * request. + */ + public 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. + */ + public 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. + */ + public function testZeroInterval() + { + $this->expectException(InvalidDataException::class); + $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue26Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue26Test.php new file mode 100644 index 0000000..3313c3e --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue26Test.php @@ -0,0 +1,33 @@ +expectException(InvalidDataException::class); + $input = <<assertInstanceOf(VCalendar::class, $vcal); + + $it = new EventIterator($vcal, 'bae5d57a98'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue48Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue48Test.php new file mode 100644 index 0000000..aef5925 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue48Test.php @@ -0,0 +1,50 @@ +assertInstanceOf(VCalendar::class, $vcal); + + $it = new 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue50Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue50Test.php new file mode 100644 index 0000000..5c476e6 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/Issue50Test.php @@ -0,0 +1,127 @@ +assertInstanceOf(VCalendar::class, $vcal); + + $it = new 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MainTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MainTest.php new file mode 100644 index 0000000..1a019a5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MainTest.php @@ -0,0 +1,1411 @@ +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()); + } + + /** + * @depends testValues + */ + public function testInvalidFreq() + { + $this->expectException(InvalidDataException::class); + $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); + } + + public function testVCalendarNoUID() + { + $this->expectException(\InvalidArgumentException::class); + $vcal = new VCalendar(); + $it = new EventIterator($vcal); + } + + public function testVCalendarInvalidUID() + { + $this->expectException(\InvalidArgumentException::class); + $vcal = new VCalendar(); + $it = new EventIterator($vcal, 'foo'); + } + + /** + * @depends testValues + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public 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 + */ + public function testNoMasterBadUID() + { + $this->expectException(\InvalidArgumentException::class); + $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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MaxInstancesTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MaxInstancesTest.php new file mode 100644 index 0000000..6314b3b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MaxInstancesTest.php @@ -0,0 +1,37 @@ +expectException(MaxInstancesExceededException::class); + $input = <<expand(new DateTime('2014-08-01'), new DateTime('2014-09-01')); + } finally { + Settings::$maxRecurrences = $temp; + } + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MissingOverriddenTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MissingOverriddenTest.php new file mode 100644 index 0000000..69af8a8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/MissingOverriddenTest.php @@ -0,0 +1,62 @@ +assertInstanceOf(VCalendar::class, $vcal); + + $vcal = $vcal->expand(new DateTime('2011-01-01'), new DateTime('2015-01-01')); + + $output = <<assertVObjectEqualsVObject($output, $vcal); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/NoInstancesTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/NoInstancesTest.php new file mode 100644 index 0000000..d89afd1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/NoInstancesTest.php @@ -0,0 +1,39 @@ +expectException(NoInstancesException::class); + $input = <<assertInstanceOf(VCalendar::class, $vcal); + + $it = new EventIterator($vcal, 'foo'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php new file mode 100644 index 0000000..150a139 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php @@ -0,0 +1,119 @@ +expand(new DateTime('2014-08-01'), new DateTime('2014-09-01')); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $vcal + ); + } + + public function testRemoveFirstEvent() + { + $input = <<expand(new DateTime('2014-08-01'), new DateTime('2014-08-19')); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $vcal + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/SameDateForRecurringEventsTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/SameDateForRecurringEventsTest.php new file mode 100644 index 0000000..1cbd979 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/EventIterator/SameDateForRecurringEventsTest.php @@ -0,0 +1,56 @@ +getComponents()); + + $this->assertEquals(4, iterator_count($eventIterator), 'in ICS 4 events'); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/RDateIteratorTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/RDateIteratorTest.php new file mode 100644 index 0000000..453d8cf --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/RDateIteratorTest.php @@ -0,0 +1,74 @@ +assertEquals( + $expected, + iterator_to_array($it) + ); + + $this->assertFalse($it->isInfinite()); + } + + public 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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/RRuleIteratorTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/RRuleIteratorTest.php new file mode 100644 index 0000000..0b3e6b8 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Recur/RRuleIteratorTest.php @@ -0,0 +1,956 @@ +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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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' + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public function testYearlyByMonthInvalidValue1() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0', + '2011-04-07 00:00:00', + [] + ); + } + + public function testYearlyByMonthInvalidValue2() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=bla', + '2011-04-07 00:00:00', + [] + ); + } + + public function testYearlyByMonthManyInvalidValues() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0,bla', + '2011-04-07 00:00:00', + [] + ); + } + + public function testYearlyByMonthEmptyValue() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=', + '2011-04-07 00:00:00', + [] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + /* + * Regression test for #383 + * $parser->next() used to cause an infinite loop. + */ + public function testYearlyByYearDayImmutable() + { + $start = '2011-07-10 03:07:00'; + $rule = 'FREQ=YEARLY;COUNT=7;INTERVAL=2;BYYEARDAY=190'; + $tz = 'UTC'; + + $dt = new DateTimeImmutable($start, new DateTimeZone($tz)); + $parser = new RRuleIterator($rule, $dt); + + $parser->next(); + + $item = $parser->current(); + $this->assertEquals($item->format('Y-m-d H:i:s'), '2013-07-10 03:07:00'); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public function testYearlyByYearDayInvalid390() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYYEARDAY=390', + '2011-04-07 00:00:00', + [ + ] + ); + } + + public function testYearlyByYearDayInvalid0() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYYEARDAY=0', + '2011-04-07 00:00:00', + [ + ] + ); + } + + public 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 + */ + public 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. + */ + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public function testInvalidByWeekNo() + { + $this->expectException(InvalidDataException::class); + $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. + */ + public 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. + */ + public function testZeroInterval() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=YEARLY;INTERVAL=0', + '2012-08-24 14:57:00', + [], + '2013-01-01 23:00:00' + ); + } + + public function testInvalidFreq() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=SMONTHLY;INTERVAL=3;UNTIL=20111025T000000Z', + '2011-10-07', + [] + ); + } + + public function testByDayBadOffset() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=WEEKLY;INTERVAL=1;COUNT=4;BYDAY=0MO;WKST=SA', + '2014-08-01 00:00:00', + [] + ); + } + + public 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' + ); + } + + public function testUntilBeforeDtStart() + { + $this->parse( + 'FREQ=DAILY;UNTIL=20140101T000000Z', + '2014-08-02 00:15:00', + [ + '2014-08-02 00:15:00', + ] + ); + } + + public 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', + ] + ); + } + + public 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', + ] + ); + } + + public function testNeverEnding() + { + $this->parse( + 'FREQ=MONTHLY;BYDAY=2TU;BYSETPOS=2', + '2015-01-01 00:15:00', + [ + '2015-01-01 00:15:00', + ], + null, + 'UTC', + true + ); + } + + public function testUnsupportedPart() + { + $this->expectException(InvalidDataException::class); + $this->parse( + 'FREQ=DAILY;BYWODAN=1', + '2014-08-02 00:15:00', + [] + ); + } + + public 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() + ); + } + + public function parse($rule, $start, $expected, $fastForward = null, $tz = 'UTC', $runTillTheEnd = false) + { + $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 (!$runTillTheEnd && $parser->isInfinite() && count($result) >= count($expected)) { + break; + } + $parser->next(); + } + + $this->assertEquals( + $expected, + $result + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/RecurrenceIterator/UntilRespectsTimezoneTest.ics b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/RecurrenceIterator/UntilRespectsTimezoneTest.ics new file mode 100644 index 0000000..1663c78 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/SlashRTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/SlashRTest.php new file mode 100644 index 0000000..688563a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/SlashRTest.php @@ -0,0 +1,19 @@ +add('test', "abc\r\ndef"); + $this->assertEquals("TEST:abc\\ndef\r\n", $prop->serialize()); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Splitter/ICalendarTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Splitter/ICalendarTest.php new file mode 100644 index 0000000..2788b96 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Splitter/ICalendarTest.php @@ -0,0 +1,320 @@ +version = VObject\Version::VERSION; + } + + public function createStream($data) + { + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $data); + rewind($stream); + + return $stream; + } + + public function testICalendarImportValidEvent() + { + $data = <<createStream($data); + + $objects = new ICalendar($tempFile); + + $return = ''; + while ($object = $objects->getNext()) { + $return .= $object->serialize(); + } + $this->assertEquals([], VObject\Reader::read($return)->validate()); + } + + public function testICalendarImportWrongType() + { + $this->expectException(ParseException::class); + $data = <<createStream($data); + + $objects = new ICalendar($tempFile); + } + + public function testICalendarImportEndOfData() + { + $data = <<createStream($data); + + $objects = new ICalendar($tempFile); + + $return = ''; + while ($object = $objects->getNext()) { + $return .= $object->serialize(); + } + $this->assertNull($object = $objects->getNext()); + } + + public function testICalendarImportInvalidEvent() + { + $this->expectException(ParseException::class); + $data = <<createStream($data); + $objects = new ICalendar($tempFile); + } + + public 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()); + } + + public 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); + } + } + + public 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()); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Splitter/VCardTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Splitter/VCardTest.php new file mode 100644 index 0000000..1402b50 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/Splitter/VCardTest.php @@ -0,0 +1,237 @@ +createStream($data); + + $objects = new VCard($tempFile); + + $count = 0; + while ($objects->getNext()) { + ++$count; + } + $this->assertEquals(1, $count); + } + + public function testVCardImportWrongType() + { + $this->expectException(ParseException::class); + $event[] = <<createStream($data); + + $splitter = new VCard($tempFile); + + while ($object = $splitter->getNext()) { + } + } + + public function testVCardImportValidVCardsWithCategories() + { + $data = <<createStream($data); + + $splitter = new VCard($tempFile); + + $count = 0; + while ($object = $splitter->getNext()) { + ++$count; + } + $this->assertEquals(4, $count); + } + + public function testVCardImportVCardNoComponent() + { + $this->expectException(ParseException::class); + $data = <<createStream($data); + + $splitter = new VCard($tempFile); + + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Invalid MimeDir file. Unexpected component: "BEGIN:VCARD" in document type VCARD'); + while ($object = $splitter->getNext()) { + } + } + + public function testVCardImportQuotedPrintableOptionForgivingLeading() + { + $data = <<createStream($data); + + $splitter = new VCard($tempFile, \Sabre\VObject\Parser\Parser::OPTION_FORGIVING); + + $count = 0; + while ($object = $splitter->getNext()) { + ++$count; + } + $this->assertEquals(2, $count); + } + + public function testVCardImportEndOfData() + { + $data = <<createStream($data); + + $objects = new VCard($tempFile); + $object = $objects->getNext(); + + $this->assertNull($objects->getNext()); + } + + public function testVCardImportCheckInvalidArgumentException() + { + $this->expectException(ParseException::class); + $data = <<createStream($data); + + $objects = new VCard($tempFile); + while ($objects->getNext()) { + } + } + + public function testVCardImportMultipleValidVCards() + { + $data = <<createStream($data); + + $objects = new VCard($tempFile); + + $count = 0; + while ($objects->getNext()) { + ++$count; + } + $this->assertEquals(2, $count); + } + + public function testImportMultipleSeparatedWithNewLines() + { + $data = <<createStream($data); + $objects = new VCard($tempFile); + + $count = 0; + while ($objects->getNext()) { + ++$count; + } + $this->assertEquals(2, $count); + } + + public function testVCardImportVCardWithoutUID() + { + $data = <<createStream($data); + + $objects = new VCard($tempFile); + + $count = 0; + while ($objects->getNext()) { + ++$count; + } + + $this->assertEquals(1, $count); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/StringUtilTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/StringUtilTest.php new file mode 100644 index 0000000..c614f6a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/StringUtilTest.php @@ -0,0 +1,50 @@ +assertEquals(false, $string); + } + + public function testIsUTF8() + { + $string = StringUtil::isUTF8('I 💚 SabreDAV'); + + $this->assertEquals(true, $string); + } + + public function testUTF8ControlChar() + { + $string = StringUtil::isUTF8(chr(0x00)); + + $this->assertEquals(false, $string); + } + + public function testConvertToUTF8nonUTF8() + { + $string = StringUtil::convertToUTF8(chr(0xbf)); + + $this->assertEquals(utf8_encode(chr(0xbf)), $string); + } + + public function testConvertToUTF8IsUTF8() + { + $string = StringUtil::convertToUTF8('I 💚 SabreDAV'); + + $this->assertEquals('I 💚 SabreDAV', $string); + } + + public function testConvertToUTF8ControlChar() + { + $string = StringUtil::convertToUTF8(chr(0x00)); + + $this->assertEquals('', $string); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/TimeZoneUtilTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/TimeZoneUtilTest.php new file mode 100644 index 0000000..619abc1 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/TimeZoneUtilTest.php @@ -0,0 +1,357 @@ +assertInstanceOf('DateTimeZone', $tz); + } catch (\Exception $e) { + if (false !== strpos($e->getMessage(), 'Unknown or bad timezone')) { + $this->markTestSkipped($timezoneName.' is not (yet) supported in this PHP version. Update pecl/timezonedb'); + } else { + throw $e; + } + } + } + + public function getMapping() + { + TimeZoneUtil::loadTzMaps(); + + // PHPUNit requires an array of arrays + return array_map( + function ($value) { + return [$value]; + }, + TimeZoneUtil::$map + ); + } + + public function testExchangeMap() + { + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + } + + public function testWetherMicrosoftIsStillInsane() + { + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + } + + public function testUnknownExchangeId() + { + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + } + + public function testWindowsTimeZone() + { + $tz = TimeZoneUtil::getTimeZone('Eastern Standard Time'); + $ex = new \DateTimeZone('America/New_York'); + $this->assertEquals($ex->getName(), $tz->getName()); + } + + /** + * @dataProvider getPHPTimeZoneIdentifiers + */ + public function testTimeZoneIdentifiers($tzid) + { + $tz = TimeZoneUtil::getTimeZone($tzid); + $ex = new \DateTimeZone($tzid); + + $this->assertEquals($ex->getName(), $tz->getName()); + } + + /** + * @dataProvider getPHPTimeZoneBCIdentifiers + */ + public function testTimeZoneBCIdentifiers($tzid) + { + $tz = TimeZoneUtil::getTimeZone($tzid); + $ex = new \DateTimeZone($tzid); + + $this->assertEquals($ex->getName(), $tz->getName()); + } + + public function getPHPTimeZoneIdentifiers() + { + // PHPUNit requires an array of arrays + return array_map( + function ($value) { + return [$value]; + }, + \DateTimeZone::listIdentifiers() + ); + } + + public function getPHPTimeZoneBCIdentifiers() + { + // PHPUNit requires an array of arrays + return array_map( + function ($value) { + return [$value]; + }, + TimeZoneUtil::getIdentifiersBC() + ); + } + + public 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()); + } + + public function testTimezoneFail() + { + $this->expectException(\InvalidArgumentException::class); + $tz = TimeZoneUtil::getTimeZone('FooBar', null, true); + } + + public function testFallBack() + { + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + } + + public function testLjubljanaBug() + { + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + } + + public function testWeirdSystemVLICs() + { + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + } + + public 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/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/UUIDUtilTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/UUIDUtilTest.php new file mode 100644 index 0000000..579928f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/UUIDUtilTest.php @@ -0,0 +1,36 @@ +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 + */ + public function testGetUUID() + { + $this->assertTrue( + UUIDUtil::validateUUID( + UUIDUtil::getUUID() + ) + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VCard21Test.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VCard21Test.php new file mode 100644 index 0000000..9dd0728 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VCard21Test.php @@ -0,0 +1,51 @@ +serialize(); + + $this->assertEquals($input, $output); + } + + public function testPropertyPadValueCount() + { + $input = <<serialize(); + + $expected = <<assertEquals($expected, $output); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VCardConverterTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VCardConverterTest.php new file mode 100644 index 0000000..ae2b10b --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VCardConverterTest.php @@ -0,0 +1,547 @@ +convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } + + public function testConvert40to40() + { + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } + + public function testConvert21to40() + { + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } + + public function testConvert30to30() + { + $input = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } + + public function testConvert40to30() + { + $input = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } + + public function testConvertGroupCard() + { + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + $input = $output; + $output = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } + + public function testBDAYConversion() + { + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + $input = $output; + $output = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } + + public function testUnknownSourceVCardVersion() + { + $this->expectException(\InvalidArgumentException::class); + $input = <<convert(Document::VCARD40); + } + + public function testUnknownTargetVCardVersion() + { + $this->expectException(\InvalidArgumentException::class); + $input = <<convert(Document::VCARD21); + } + + public function testConvertIndividualCard() + { + $input = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + $input = $output; + $output = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public function testNoLabel() + { + $input = <<assertInstanceOf(Component\VCard::class, $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)); + } + + public function testPhoneNumberValueTypeGetsRemoved() + { + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VersionTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VersionTest.php new file mode 100644 index 0000000..0ad9627 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/VersionTest.php @@ -0,0 +1,14 @@ +assertEquals(-1, version_compare('2.0.0', $v)); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/WriterTest.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/WriterTest.php new file mode 100644 index 0000000..e30fac5 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/WriterTest.php @@ -0,0 +1,39 @@ +getComponent()); + $this->assertEquals("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n", $result); + } + + public function testWriteToJson() + { + $result = Writer::writeJson($this->getComponent()); + $this->assertEquals('["vcalendar",[],[]]', $result); + } + + public function testWriteToXml() + { + $result = Writer::writeXml($this->getComponent()); + $this->assertEquals( + ''."\n". + ''."\n". + ' '."\n". + ''."\n", + $result + ); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/issue153.vcf b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/issue153.vcf new file mode 100644 index 0000000..180949c --- /dev/null +++ b/plugins/panakour/backup/vendor/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 + 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 + zbVbk4/OrNpefLsnyyg5UUAf/9k= +END:VCARD diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/issue64.vcf b/plugins/panakour/backup/vendor/sabre/vobject/tests/VObject/issue64.vcf new file mode 100644 index 0000000..6110529 --- /dev/null +++ b/plugins/panakour/backup/vendor/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 + 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 + zbVbk4/OrNpefLsnyyg5UUAf/9k= + +END:VCARD diff --git a/plugins/panakour/backup/vendor/sabre/vobject/tests/bootstrap.php b/plugins/panakour/backup/vendor/sabre/vobject/tests/bootstrap.php new file mode 100644 index 0000000..2496aa4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/vobject/tests/bootstrap.php @@ -0,0 +1,15 @@ + + + + VObject/ + + + + + + ../lib/ + + ../lib/Sabre/VObject/includes.php + + + + diff --git a/plugins/panakour/backup/vendor/sabre/xml/CHANGELOG.md b/plugins/panakour/backup/vendor/sabre/xml/CHANGELOG.md new file mode 100644 index 0000000..bbfc8dd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/CHANGELOG.md @@ -0,0 +1,273 @@ +ChangeLog +========= + +2.2.0 (2020-01-31) +------------------ + +* #171: Added Support for PHP 7.4, dropped Support for PHP 7.0 (@staabm, @phil-davis) +* #174: Update testsuite to phpunit8 (@phil-davis) +* Added phpstan coverage (@phil-davis) +* #144: Added a new `functionCaller` deserializer function for executing a callable when reading a XML +element (@vsouz4) + + +2.1.3 (2019-08-14) +------------------ + +* #166: Throw exception when empty inputs found + + +2.1.2 (2019-01-09) +------------------ + +* #161: Prevent infinite loop on empty xml elements + + +2.1.1 (2018-10-09) +------------------ + +* #149: Properly detect xml parse errors in `parseCurrentElement()` edge-cases + + +2.1.0 (2018-02-08) +------------------ + +* #112: Added a `mixedContent` deserializer function, which might be useful + if you're parsing HTML-like documents with elements that contain both text + and other elements as siblings. (@staabm). + + +2.0.0 (2016-11-15) +------------------ + +* Now requires PHP 7. +* Uses typehints everywhere. +* Fixed some minor strict typing-related issues. +* Removed workaround for PHP bug [64230](https://bugs.php.net/bug.php?id=64230). + + +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/plugins/panakour/backup/vendor/sabre/xml/LICENSE b/plugins/panakour/backup/vendor/sabre/xml/LICENSE new file mode 100644 index 0000000..c9faf40 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/panakour/backup/vendor/sabre/xml/README.md b/plugins/panakour/backup/vendor/sabre/xml/README.md new file mode 100644 index 0000000..55af24f --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/README.md @@ -0,0 +1,25 @@ +sabre/xml +========= + +[![Build Status](https://secure.travis-ci.org/sabre-io/xml.svg?branch=master)](http://travis-ci.org/sabre-io/xml) + +The sabre/xml library is a specialized XML reader and writer. + +Documentation +------------- + +* [Introduction](http://sabre.io/xml/). +* [Installation](http://sabre.io/xml/install/). +* [Reading XML](http://sabre.io/xml/reading/). +* [Writing XML](http://sabre.io/xml/writing/). + + +Support +------- + +Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions. + +Made at fruux +------------- + +This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support. diff --git a/plugins/panakour/backup/vendor/sabre/xml/composer.json b/plugins/panakour/backup/vendor/sabre/xml/composer.json new file mode 100644 index 0000000..2af0dd4 --- /dev/null +++ b/plugins/panakour/backup/vendor/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" : "^7.1", + "ext-xmlwriter" : "*", + "ext-xmlreader" : "*", + "ext-dom" : "*", + "lib-libxml" : ">=2.6.20", + "sabre/uri" : ">=1.0,<3.0.0" + }, + "authors" : [ + { + "name" : "Evert Pot", + "email" : "me@evertpot.com", + "homepage" : "http://evertpot.com/", + "role" : "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role" : "Developer" + } + ], + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-xml" + }, + "autoload" : { + "psr-4" : { + "Sabre\\Xml\\" : "lib/" + }, + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ] + }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\Xml\\" : "tests/Sabre/Xml/" + } + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit" : "^7 || ^8" + }, + "config" : { + "bin-dir" : "bin/" + } +} diff --git a/plugins/panakour/backup/vendor/sabre/xml/lib/ContextStackTrait.php b/plugins/panakour/backup/vendor/sabre/xml/lib/ContextStackTrait.php new file mode 100644 index 0000000..7570888 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/lib/ContextStackTrait.php @@ -0,0 +1,118 @@ +contextStack[] = [ + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap, + ]; + } + + /** + * Restore the previous "context". + */ + public function popContext() + { + list( + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap + ) = array_pop($this->contextStack); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/xml/lib/Deserializer/functions.php b/plugins/panakour/backup/vendor/sabre/xml/lib/Deserializer/functions.php new file mode 100644 index 0000000..c4f2409 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/lib/Deserializer/functions.php @@ -0,0 +1,359 @@ +value" array. + * + * For example, keyvalue will parse: + * + * + * + * value1 + * value2 + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1" => "value1", + * "{http://sabredav.org/ns}elem2" => "value2", + * "{http://sabredav.org/ns}elem3" => null, + * ]; + * + * If you specify the 'namespace' argument, the deserializer will remove + * the namespaces of the keys that match that namespace. + * + * For example, if you call keyValue like this: + * + * keyValue($reader, 'http://sabredav.org/ns') + * + * it's output will instead be: + * + * [ + * "elem1" => "value1", + * "elem2" => "value2", + * "elem3" => null, + * ]; + * + * Attributes will be removed from the top-level elements. If elements with + * the same name appear twice in the list, only the last one will be kept. + */ +function keyValue(Reader $reader, string $namespace = null): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + + $values = []; + + do { + if (Reader::ELEMENT === $reader->nodeType) { + if (null !== $namespace && $reader->namespaceURI === $namespace) { + $values[$reader->localName] = $reader->parseCurrentElement()['value']; + } else { + $clark = $reader->getClark(); + $values[$clark] = $reader->parseCurrentElement()['value']; + } + } else { + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; +} + +/** + * The 'enum' deserializer parses elements into a simple list + * without values or attributes. + * + * For example, Elements will parse: + * + * + * + * + * + * + * content + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1", + * "{http://sabredav.org/ns}elem2", + * "{http://sabredav.org/ns}elem3", + * "{http://sabredav.org/ns}elem4", + * "{http://sabredav.org/ns}elem5", + * ]; + * + * This is useful for 'enum'-like structures. + * + * If the $namespace argument is specified, it will strip the namespace + * for all elements that match that. + * + * For example, + * + * enum($reader, 'http://sabredav.org/ns') + * + * would return: + * + * [ + * "elem1", + * "elem2", + * "elem3", + * "elem4", + * "elem5", + * ]; + * + * @return string[] + */ +function enum(Reader $reader, string $namespace = null): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + $currentDepth = $reader->depth; + + $values = []; + do { + if (Reader::ELEMENT !== $reader->nodeType) { + continue; + } + if (!is_null($namespace) && $namespace === $reader->namespaceURI) { + $values[] = $reader->localName; + } else { + $values[] = (string) $reader->getClark(); + } + } while ($reader->depth >= $currentDepth && $reader->next()); + + $reader->next(); + + return $values; +} + +/** + * The valueObject deserializer turns an xml element into a PHP object of + * a specific class. + * + * This is primarily used by the mapValueObject function from the Service + * class, but it can also easily be used for more specific situations. + * + * @return object + */ +function valueObject(Reader $reader, string $className, string $namespace) +{ + $valueObject = new $className(); + if ($reader->isEmptyElement) { + $reader->next(); + + return $valueObject; + } + + $defaultProperties = get_class_vars($className); + + $reader->read(); + do { + if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { + if (property_exists($valueObject, $reader->localName)) { + if (is_array($defaultProperties[$reader->localName])) { + $valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value']; + } else { + $valueObject->{$reader->localName} = $reader->parseCurrentElement()['value']; + } + } else { + // Ignore property + $reader->next(); + } + } else { + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $valueObject; +} + +/** + * This deserializer helps you deserialize xml structures that look like + * this:. + * + * + * ... + * ... + * ... + * + * + * Many XML documents use patterns like that, and this deserializer + * allow you to get all the 'items' as an array. + * + * In that previous example, you would register the deserializer as such: + * + * $reader->elementMap['{}collection'] = function($reader) { + * return repeatingElements($reader, '{}item'); + * } + * + * The repeatingElements deserializer simply returns everything as an array. + * + * $childElementName must either be a a clark-notation element name, or if no + * namespace is used, the bare element name. + */ +function repeatingElements(Reader $reader, string $childElementName): array +{ + if ('{' !== $childElementName[0]) { + $childElementName = '{}'.$childElementName; + } + $result = []; + + foreach ($reader->parseGetElements() as $element) { + if ($element['name'] === $childElementName) { + $result[] = $element['value']; + } + } + + return $result; +} + +/** + * This deserializer helps you to deserialize structures which contain mixed content like this:. + * + *

    some text and a inline tagand even more text

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

    This is some text and a inline tagand even more text

    +XML; + + $result = $service->parse($xml); + + $expected = [ + 'This is some text ', + [ + 'name' => '{}extref', + 'value' => 'and a inline tag', + 'attributes' => [], + ], + 'and even more text', + ]; + + $this->assertEquals($expected, $result); + } +} diff --git a/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/RepeatingElementsTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/RepeatingElementsTest.php new file mode 100644 index 0000000..d5f96b3 --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/ValueObjectTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/ValueObjectTest.php new file mode 100644 index 0000000..04f77a4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Deserializer/ValueObjectTest.php @@ -0,0 +1,165 @@ + + + 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 + ); + } + + public 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 + ); + } + + public 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 + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/CDataTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/CDataTest.php new file mode 100644 index 0000000..d4ae8bd --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/CDataTest.php @@ -0,0 +1,54 @@ +expectException(\LogicException::class); + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}blabla' => 'Sabre\\Xml\\Element\\Cdata', + ]; + $reader->xml($input); + + $output = $reader->parse(); + } + + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/Eater.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/Eater.php new file mode 100644 index 0000000..880e317 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/Eater.php @@ -0,0 +1,71 @@ +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. + * + * @return mixed + */ + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/ElementsTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/ElementsTest.php new file mode 100644 index 0000000..d50b7b4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/ElementsTest.php @@ -0,0 +1,127 @@ + + + + + + + + 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); + } + + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/KeyValueTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/KeyValueTest.php new file mode 100644 index 0000000..dc6085a --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/KeyValueTest.php @@ -0,0 +1,206 @@ + + + + + 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. + */ + public 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()); + } + + public 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. + */ + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/Mock.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/Mock.php new file mode 100644 index 0000000..73f5cc0 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/Mock.php @@ -0,0 +1,56 @@ +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. + * + * @return mixed + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + $reader->next(); + + return 'foobar'; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/UriTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/UriTest.php new file mode 100644 index 0000000..3c51318 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/UriTest.php @@ -0,0 +1,74 @@ + + + /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 + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/XmlFragmentTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/XmlFragmentTest.php new file mode 100644 index 0000000..bb803f9 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Element/XmlFragmentTest.php @@ -0,0 +1,139 @@ + + + $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. + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/InfiteLoopTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/InfiteLoopTest.php new file mode 100644 index 0000000..404eaae --- /dev/null +++ b/plugins/panakour/backup/vendor/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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ReaderTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ReaderTest.php new file mode 100644 index 0000000..07f19e4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ReaderTest.php @@ -0,0 +1,550 @@ + + +BLA; + $reader = new Reader(); + $reader->xml($input); + + $reader->next(); + + $this->assertEquals('{http://sabredav.org/ns}root', $reader->getClark()); + } + + public function testGetClarkNoNS() + { + $input = << + +BLA; + $reader = new Reader(); + $reader->xml($input); + + $reader->next(); + + $this->assertEquals('{}root', $reader->getClark()); + } + + public function testGetClarkNotOnAnElement() + { + $input = << + +BLA; + $reader = new Reader(); + $reader->xml($input); + + $this->assertNull($reader->getClark()); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public function testMappedElementBadClass() + { + $this->expectException(\LogicException::class); + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => new \StdClass(), + ]; + $reader->xml($input); + + $reader->parse(); + } + + /** + * @depends testMappedElement + */ + public 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 + */ + public 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 + */ + public 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); + } + + public 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->assertIsArray($e->getErrors()); + } + } + + public function testBrokenParserClass() + { + $this->expectException(ParseException::class); + $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. + */ + public function testBrokenXml() + { + $this->expectException(LibXMLException::class); + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->xml($input); + $reader->parse(); + } + + /** + * Test was added for Issue #45. + */ + public function testBrokenXml2() + { + $this->expectException(LibXMLException::class); + $input = << + + + + + + ""Administrative w"> + + + + xml($input); + $reader->parse(); + } + + /** + * @depends testMappedElement + */ + public 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 + */ + public 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 + */ + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Serializer/EnumTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Serializer/EnumTest.php new file mode 100644 index 0000000..3852495 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Serializer/EnumTest.php @@ -0,0 +1,33 @@ +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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Serializer/RepeatingElementsTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Serializer/RepeatingElementsTest.php new file mode 100644 index 0000000..6cf5495 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/Serializer/RepeatingElementsTest.php @@ -0,0 +1,32 @@ +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/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ServiceTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ServiceTest.php new file mode 100644 index 0000000..792e76d --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/ServiceTest.php @@ -0,0 +1,418 @@ + 'Test!', + ]; + + $util = new Service(); + $util->elementMap = $elems; + + $reader = $util->getReader(); + $this->assertInstanceOf('Sabre\\Xml\\Reader', $reader); + $this->assertEquals($elems, $reader->elementMap); + } + + public 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); + } + + public function testEmptyInputParse() + { + $resource = fopen('php://input', 'r'); + $util = new Service(); + $this->expectException('\Sabre\Xml\ParseException'); + $this->expectExceptionMessage('The input element to parse is empty. Do not attempt to parse'); + $util->parse($resource, '/sabre.io/ns'); + } + + /** + * @depends testGetReader + */ + public 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 + */ + public 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 + ); + } + + public function testEmptyInputExpect() + { + //$resource = \fopen('') + $resource = fopen('php://input', 'r'); + $util = new Service(); + $this->expectException('\Sabre\Xml\ParseException'); + $this->expectExceptionMessage('The input element to parse is empty. Do not attempt to parse'); + $util->expect('foo', $resource, '/sabre.io/ns'); + } + + /** + * @depends testGetReader + */ + public 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 + ); + } + + public function testInvalidNameSpace() + { + $this->expectException(LibXMLException::class); + $xml = ''; + + $util = new Service(); + $util->elementMap = [ + '{DAV:}propfind' => PropFindTestAsset::class, + ]; + $util->namespaceMap = [ + 'http://sabre.io/ns' => 's', + ]; + $result = $util->expect('{DAV:}propfind', $xml); + } + + /** + * @dataProvider providesEmptyPropfinds + */ + public function testEmptyPropfind($xml) + { + $util = new Service(); + $util->elementMap = [ + '{DAV:}propfind' => PropFindTestAsset::class, + ]; + $util->namespaceMap = [ + 'http://sabre.io/ns' => 's', + ]; + $result = $util->expect('{DAV:}propfind', $xml); + $this->assertEquals(false, $result->allProp); + $this->assertEquals([], $result->properties); + } + + /** + * @depends testGetReader + */ + public 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 + */ + public function testExpectWrong() + { + $this->expectException(ParseException::class); + $xml = << + value + +XML; + $util = new Service(); + $util->expect('{http://sabre.io/ns}error', $xml); + } + + /** + * @depends testGetWriter + */ + public 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 + ); + } + + public 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); + } + + public 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); + } + + public function testWriteVoNotFound() + { + $this->expectException(\InvalidArgumentException::class); + $service = new Service(); + $service->writeValueObject(new \StdClass()); + } + + public function testParseClarkNotation() + { + $this->assertEquals([ + 'http://sabredav.org/ns', + 'elem', + ], Service::parseClarkNotation('{http://sabredav.org/ns}elem')); + } + + public function testParseClarkNotationFail() + { + $this->expectException(\InvalidArgumentException::class); + Service::parseClarkNotation('http://sabredav.org/ns}elem'); + } + + public function providesEmptyPropfinds() + { + return [ + [''], + [''], + [''], + [''], + [' '], + ]; + } +} + +/** + * 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; +} + +/** + * asset for testInvalidNameSpace. + * + * @internal + */ +class PropFindTestAsset implements XmlDeserializable +{ + public $allProp = false; + + public $properties; + + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements'; + + foreach (KeyValue::xmlDeserialize($reader) as $k => $v) { + switch ($k) { + case '{DAV:}prop': + $self->properties = $v; + break; + case '{DAV:}allprop': + $self->allProp = true; + } + } + + $reader->popContext(); + + return $self; + } +} diff --git a/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/WriterTest.php b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/WriterTest.php new file mode 100644 index 0000000..766c072 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/Sabre/Xml/WriterTest.php @@ -0,0 +1,412 @@ +writer = new Writer(); + $this->writer->namespaceMap = [ + 'http://sabredav.org/ns' => 's', + ]; + $this->writer->openMemory(); + $this->writer->setIndent(true); + $this->writer->startDocument(); + } + + public function compare($input, $output) + { + $this->writer->write($input); + $this->assertEquals($output, $this->writer->outputMemory()); + } + + public function testSimple() + { + $this->compare([ + '{http://sabredav.org/ns}root' => 'text', + ], << +text + +HI + ); + } + + /** + * @depends testSimple + */ + public function testSimpleQuotes() + { + $this->compare([ + '{http://sabredav.org/ns}root' => '"text"', + ], << +"text" + +HI + ); + } + + public function testSimpleAttributes() + { + $this->compare([ + '{http://sabredav.org/ns}root' => [ + 'value' => 'text', + 'attributes' => [ + 'attr1' => 'attribute value', + ], + ], + ], << +text + +HI + ); + } + + public 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 + ); + } + + public function testNull() + { + $this->compare([ + '{http://sabredav.org/ns}root' => null, + ], << + + +HI + ); + } + + public function testArrayFormat2() + { + $this->compare([ + '{http://sabredav.org/ns}root' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => 'text', + 'attributes' => [ + 'attr1' => 'attribute value', + ], + ], + ], + ], << + + text + + +HI + ); + } + + public function testArrayOfValues() + { + $this->compare([ + '{http://sabredav.org/ns}root' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => [ + 'foo', + 'bar', + 'baz', + ], + ], + ], + ], << + + foobarbaz + + +HI + ); + } + + /** + * @depends testArrayFormat2 + */ + public function testArrayFormat2NoValue() + { + $this->compare([ + '{http://sabredav.org/ns}root' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'attributes' => [ + 'attr1' => 'attribute value', + ], + ], + ], + ], << + + + + +HI + ); + } + + public function testCustomNamespace() + { + $this->compare([ + '{http://sabredav.org/ns}root' => [ + '{urn:foo}elem1' => 'bar', + ], + ], << + + bar + + +HI + ); + } + + public function testEmptyNamespace() + { + // Empty namespaces are allowed, so we should support this. + $this->compare([ + '{http://sabredav.org/ns}root' => [ + '{}elem1' => 'bar', + ], + ], << + + bar + + +HI + ); + } + + public 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 + ); + } + + public function testBaseElement() + { + $this->compare([ + '{http://sabredav.org/ns}root' => new Element\Base('hello'), + ], << +hello + +HI + ); + } + + public function testElementObj() + { + $this->compare([ + '{http://sabredav.org/ns}root' => new Element\Mock(), + ], << + + hiiii! + + +HI + ); + } + + public function testEmptyNamespacePrefix() + { + $this->writer->namespaceMap['http://sabredav.org/ns'] = null; + $this->compare([ + '{http://sabredav.org/ns}root' => new Element\Mock(), + ], << + + hiiii! + + +HI + ); + } + + public function testEmptyNamespacePrefixEmptyString() + { + $this->writer->namespaceMap['http://sabredav.org/ns'] = ''; + $this->compare([ + '{http://sabredav.org/ns}root' => new Element\Mock(), + ], << + + hiiii! + + +HI + ); + } + + public function testWriteElement() + { + $this->writer->writeElement('{http://sabredav.org/ns}foo', 'content'); + + $output = << +content + +HI; + + $this->assertEquals($output, $this->writer->outputMemory()); + } + + public 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()); + } + + public function testWriteBadObject() + { + $this->expectException(\InvalidArgumentException::class); + $this->writer->write(new \StdClass()); + } + + public function testStartElementSimple() + { + $this->writer->startElement('foo'); + $this->writer->endElement(); + + $output = << + + +HI; + + $this->assertEquals($output, $this->writer->outputMemory()); + } + + public function testCallback() + { + $this->compare([ + '{http://sabredav.org/ns}root' => function (Writer $writer) { + $writer->text('deferred writer'); + }, + ], << +deferred writer + +HI + ); + } + + public function testResource() + { + $this->expectException(\InvalidArgumentException::class); + $this->compare([ + '{http://sabredav.org/ns}root' => fopen('php://memory', 'r'), + ], << +deferred writer + +HI + ); + } + + public 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/plugins/panakour/backup/vendor/sabre/xml/tests/phpunit.xml b/plugins/panakour/backup/vendor/sabre/xml/tests/phpunit.xml new file mode 100644 index 0000000..7cab0c4 --- /dev/null +++ b/plugins/panakour/backup/vendor/sabre/xml/tests/phpunit.xml @@ -0,0 +1,21 @@ + + + + Sabre/ + + + + + + ../lib/ + + + diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/CHANGELOG.md b/plugins/panakour/backup/vendor/spatie/db-dumper/CHANGELOG.md new file mode 100644 index 0000000..467f5b1 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/CHANGELOG.md @@ -0,0 +1,174 @@ +# Changelog + +All notable changes to `db-dumper` will be documented in this file + +## 2.13.1 - 2019-03-01 + +- remove pipefail operator when compressing dump + +## 2.13.0 - 2019-03-01 + +- add ability to specify all databases as MySQL option + +## 2.12.0 - 2018-12-10 + +- add `doNotCreateTables` + +## 2.11.1 - 2018-09-27 + +- add `useExtension` + +## 2.11.0 - 2018-09-26 + +- add `Compressor` + +## 2.10.1 - 2018-08-30 + +- allow destination paths to have a space character + +## 2.10.0 - 2018-04-27 + +- add support for compressing dumps + +## 2.9.0 - 2018-03-05 + +- add support for setting `--set-gtid-purged` + +## 2.8.2 - 2018-01-20 + +- add support for Symfony 4 + +## 2.8.1 - 2017-11-24 + +- fix SQLite dump + +## 2.8.0 - 2017-11-13 + +- add `setAuthenticationDatabase` + +## 2.7.4 - 2017-11-07 + +- fix for dumping a MongoDB without username or password + +## 2.7.3 - 2017-09-09 + +- allow empty passwords for MongoDB dumps + +## 2.7.2 - 2017-09-07 + +- make `--databases` optional + +## 2.7.1 - 2017-08-18 + +- made option passing more flexible by adding `--databases` option to the MySQL dumper + +## 2.7.0 - 2017-04-13 + +- `MongoDb` dumps won't be compressed by default anymore +- add `enableCompression` on `MongoDb` + +## 2.6.1 - 2017-04-13 + +- fix sqlite dumper + +## 2.6.0 - 2017-04-13 + +- add support for MongoDB + +## 2.5.1 - 2017-04-07 + +- prefix excluded tables with database name when dumping a MySql db + +## 2.5.0 - 2017-04-05 + +- add `--default-character-set` option for MySql +- improve the preservation of the used charset when dumping a MySql db + +## 2.4.1 - 2016-12-31 + +- fix bug where custom binary path with spaces on linux would not process correctly + +## 2.4.0 - 2016-12-30 + +- add `skipComments` + +## 2.3.0 - 2016-11-21 + +- add support for SQLite + +## 2.1.1 - 2016-11-19 + +- made a change so the package can be used on Windows + +## 2.1.0 - 2016-10-21 + +- added `getHost` + +## 2.0.1 - 2016-09-17 + +- fix for dump paths with spaces + +## 2.0.0 - 2016-09-07 + +- refactored all classes +- added the ability to add artribrary options + +## 1.5.1 - 2016-06-14 + +- Removed -d flag from pg_dump for compability with pgsql 7.3+ + +## 1.5.0 - 2016-06-01 +- Added `includeTables` and `excludeTables` + +## 1.4.0 - 2016-04-27 + +- Added --single-transaction option to Mysql dump command + +## 1.3.0 - 2016-04-03 + +- Added the ability to use insert when dumping a PostgreSQL db + +## 1.2.4 - 2016-03-24 + +- Added more details about a dump failure in the error message + +## 1.2.3 - 2016-03-18 + +- Fixed an issue where paths containing spaces would cause problems + +## 1.2.2 - 2016-03-16 + +- Added an option to set a timeout + +## 1.2.1 - 2016-03-14 + +- Fixed PostgreSQL dump + +## 1.2.0 - 2016-03-13 + +- Added support for PostgreSQL + +## 1.1.0 - 2016-02-21 + +- Lowered PHP and symfony requirements + +## 1.0.4 - 2016-02-14 + +- Fixed a bug when the backup has failed. + +## 1.0.3 - 2016-02-01 + +- Added missing abstract `getDbName`-method + +## 1.0.2 - 2016-02-01 + +- Added missing abstract `dumpToFile`-method + +## 1.0.1 - 2016-01-19 + +- Fixed typo in `checkIfDumpWasSuccessFul`-method name +- Fixed bug running Process + +## 1.0.0 - 2016-01-19 + +- Initial release diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/CONTRIBUTING.md b/plugins/panakour/backup/vendor/spatie/db-dumper/CONTRIBUTING.md new file mode 100644 index 0000000..5b711d3 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/spatie/db-dumper). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + + +## Running Tests + +``` bash +$ composer test +``` + + +**Happy coding**! diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/LICENSE.md b/plugins/panakour/backup/vendor/spatie/db-dumper/LICENSE.md new file mode 100644 index 0000000..59e5ec5 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Spatie bvba + +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/plugins/panakour/backup/vendor/spatie/db-dumper/README.md b/plugins/panakour/backup/vendor/spatie/db-dumper/README.md new file mode 100644 index 0000000..034efca --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/README.md @@ -0,0 +1,285 @@ +# Dump the contents of a database + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/db-dumper.svg?style=flat-square)](https://packagist.org/packages/spatie/db-dumper) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/spatie/db-dumper/master.svg?style=flat-square)](https://travis-ci.org/spatie/db-dumper) +[![Quality Score](https://img.shields.io/scrutinizer/g/spatie/db-dumper.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/db-dumper) +[![StyleCI](https://styleci.io/repos/49829051/shield?branch=master)](https://styleci.io/repos/49829051) +[![Total Downloads](https://img.shields.io/packagist/dt/spatie/db-dumper.svg?style=flat-square)](https://packagist.org/packages/spatie/db-dumper) + +This repo contains an easy to use class to dump a database using PHP. Currently MySQL, PostgreSQL, SQLite and MongoDB are supported. Behind +the scenes `mysqldump`, `pg_dump`, `sqlite3` and `mongodump` are used. + +Here's are simple examples of how to create a database dump with different drivers: + +**MySQL** + +```php +Spatie\DbDumper\Databases\MySql::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->dumpToFile('dump.sql'); +``` + +**PostgreSQL** + +```php +Spatie\DbDumper\Databases\PostgreSql::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->dumpToFile('dump.sql'); +``` + +**SQLite** + +```php +Spatie\DbDumper\Databases\Sqlite::create() + ->setDbName($pathToDatabaseFile) + ->dumpToFile('dump.sql'); +``` + +**MongoDB** + +```php +Spatie\DbDumper\Databases\MongoDb::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->dumpToFile('dump.gz'); +``` + +Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). + +## Requirements +For dumping MySQL-db's `mysqldump` should be installed. + +For dumping PostgreSQL-db's `pg_dump` should be installed. + +For dumping SQLite-db's `sqlite3` should be installed. + +For dumping MongoDB-db's `mongodump` should be installed. + +## Installation + +You can install the package via composer: +``` bash +$ composer require spatie/db-dumper +``` + +## Usage + +This is the simplest way to create a dump of a MySql db: + +```php +Spatie\DbDumper\Databases\MySql::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->dumpToFile('dump.sql'); +``` + +If you're working with PostgreSQL just use that dumper, most methods are available on both the MySql. and PostgreSql-dumper. + +```php +Spatie\DbDumper\Databases\PostgreSql::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->dumpToFile('dump.sql'); +``` + +If the `mysqldump` (or `pg_dump`) binary is installed in a non default location you can let the package know by using the`setDumpBinaryPath()`-function: + +```php +Spatie\DbDumper\Databases\MySql::create() + ->setDumpBinaryPath('/custom/location') + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->dumpToFile('dump.sql'); +``` + +### Dump specific tables + +Using an array: + +```php +Spatie\DbDumper\Databases\MySql::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->includeTables(['table1', 'table2', 'table3']) + ->dumpToFile('dump.sql'); +``` +Using a string: + +```php +Spatie\DbDumper\Databases\MySql::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->includeTables('table1, table2, table3') + ->dumpToFile('dump.sql'); +``` + +### Excluding tables from the dump + +Using an array: + +```php +Spatie\DbDumper\Databases\MySql::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->excludeTables(['table1', 'table2', 'table3']) + ->dumpToFile('dump.sql'); +``` +Using a string: + +```php +Spatie\DbDumper\Databases\MySql::create() + ->setDbName($databaseName) + ->setUserName($userName) + ->setPassword($password) + ->excludeTables('table1, table2, table3') + ->dumpToFile('dump.sql'); +``` + +### Do not write CREATE TABLE statements that create each dumped table. +```php +$dumpCommand = MySql::create() + ->setDbName('dbname') + ->setUserName('username') + ->setPassword('password') + ->doNotCreateTables() + ->getDumpCommand('dump.sql', 'credentials.txt'); +``` + +### Adding extra options +If you want to add an arbitrary option to the dump command you can use `addExtraOption` + +```php +$dumpCommand = MySql::create() + ->setDbName('dbname') + ->setUserName('username') + ->setPassword('password') + ->addExtraOption('--xml') + ->getDumpCommand('dump.sql', 'credentials.txt'); +``` + +If you're working with MySql you can set the database name using `--databases` as an extra option. This is particularly useful when used in conjunction with the `--add-drop-database` `mysqldump` option (see the [mysqldump docs](https://dev.mysql.com/doc/refman/5.7/en/mysqldump.html#option_mysqldump_add-drop-database)). + +```php +$dumpCommand = MySql::create() + ->setUserName('username') + ->setPassword('password') + ->addExtraOption('--databases dbname') + ->addExtraOption('--add-drop-database') + ->getDumpCommand('dump.sql', 'credentials.txt'); +``` + +With MySql, you also have the option to use the `--all-databases` extra option. This is useful when you want to run a full backup of all the databases in the specified MySQL connection. + +```php +$dumpCommand = MySql::create() + ->setUserName('username') + ->setPassword('password') + ->addExtraOption('--all-databases') + ->getDumpCommand('dump.sql', 'credentials.txt'); +``` + +Please note that using the `->addExtraOption('--databases dbname')` or `->addExtraOption('--all-databases')` will override the database name set on a previous `->setDbName()` call. + +### Using compression +If you want to compress the outputted file, you can use one of the compressors and the resulted dump file will be compressed. + +There is one compressor that comes out of the box: `GzipCompressor`. It will compress your db dump with `gzip`. Make sure `gzip` is installed on your system before using this. + +```php +$dumpCommand = MySql::create() + ->setDbName('dbname') + ->setUserName('username') + ->setPassword('password') + ->useCompressor(new GzipCompressor()) + ->dumpToFile('dump.sql.gz'); +``` + +### Creating your own compressor + +You can create you own compressor implementing the `Compressor` interface. Here's how that interface looks like: + +```php +namespace Spatie\DbDumper\Compressors; + +interface Compressor +{ + public function useCommand(): string; + + public function useExtension(): string; +} +``` + +The `useCommand` should simply return the compression command the db dump will get pumped to. Here's the implementation of `GzipCompression`. + +```php +namespace Spatie\DbDumper\Compressors; + +class GzipCompressor implements Compressor +{ + public function useCommand(): string + { + return 'gzip'; + } + + public function useExtension(): string + { + return 'gz'; + } +} +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Testing + +``` bash +$ composer test +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Security + +If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. + +## Postcardware + +You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. + +Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. + +We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). + +## Credits + +- [Freek Van der Herten](https://github.com/freekmurze) +- [All Contributors](../../contributors) + +Initial PostgreSQL support was contributed by [Adriano Machado](https://github.com/ammachado). SQlite support was contributed by [Peter Matseykanets](https://twitter.com/pmatseykanets). + +## Support us + +Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). + +Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). +All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/composer.json b/plugins/panakour/backup/vendor/spatie/db-dumper/composer.json new file mode 100644 index 0000000..3bf67c1 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/composer.json @@ -0,0 +1,41 @@ +{ + "name": "spatie/db-dumper", + "description": "Dump databases", + "keywords": [ + "spatie", + "dump", + "database", + "mysqldump", + "db-dumper" + ], + "homepage": "https://github.com/spatie/db-dumper", + "license": "MIT", + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "require": { + "php" : "^7.0", + "symfony/process": "^3.0|^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "autoload": { + "psr-4": { + "Spatie\\DbDumper\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Spatie\\DbDumper\\Test\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit" + } +} diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/src/Compressors/Compressor.php b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Compressors/Compressor.php new file mode 100644 index 0000000..ddeb252 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Compressors/Compressor.php @@ -0,0 +1,10 @@ +guardAgainstIncompleteCredentials(); + + $command = $this->getDumpCommand($dumpFile); + + $process = new Process($command); + + if (! is_null($this->timeout)) { + $process->setTimeout($this->timeout); + } + + $process->run(); + + $this->checkIfDumpWasSuccessFul($process, $dumpFile); + } + + /** + * Verifies if the dbname and host options are set. + * + * @throws \Spatie\DbDumper\Exceptions\CannotStartDump + * @return void + */ + protected function guardAgainstIncompleteCredentials() + { + foreach (['dbName', 'host'] as $requiredProperty) { + if (strlen($this->$requiredProperty) === 0) { + throw CannotStartDump::emptyParameter($requiredProperty); + } + } + } + + /** + * @param string $collection + * + * @return \Spatie\DbDumper\Databases\MongoDb + */ + public function setCollection(string $collection) + { + $this->collection = $collection; + + return $this; + } + + /** + * @param string $authenticationDatabase + * + * @return \Spatie\DbDumper\Databases\MongoDb + */ + public function setAuthenticationDatabase(string $authenticationDatabase) + { + $this->authenticationDatabase = $authenticationDatabase; + + return $this; + } + + /** + * Generate the dump command for MongoDb. + * + * @param string $filename + * + * @return string + */ + public function getDumpCommand(string $filename) : string + { + $command = [ + "'{$this->dumpBinaryPath}mongodump'", + "--db {$this->dbName}", + '--archive', + ]; + + if ($this->userName) { + $command[] = "--username '{$this->userName}'"; + } + + if ($this->password) { + $command[] = "--password '{$this->password}'"; + } + + if (isset($this->host)) { + $command[] = "--host {$this->host}"; + } + + if (isset($this->port)) { + $command[] = "--port {$this->port}"; + } + + if (isset($this->collection)) { + $command[] = "--collection {$this->collection}"; + } + + if ($this->authenticationDatabase) { + $command[] = "--authenticationDatabase {$this->authenticationDatabase}"; + } + + return $this->echoToFile(implode(' ', $command), $filename); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/MySql.php b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/MySql.php new file mode 100644 index 0000000..6395e9d --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/MySql.php @@ -0,0 +1,269 @@ +port = 3306; + } + + /** + * @return $this + */ + public function skipComments() + { + $this->skipComments = true; + + return $this; + } + + /** + * @return $this + */ + public function dontSkipComments() + { + $this->skipComments = false; + + return $this; + } + + /** + * @return $this + */ + public function useExtendedInserts() + { + $this->useExtendedInserts = true; + + return $this; + } + + /** + * @return $this + */ + public function dontUseExtendedInserts() + { + $this->useExtendedInserts = false; + + return $this; + } + + /** + * @return $this + */ + public function useSingleTransaction() + { + $this->useSingleTransaction = true; + + return $this; + } + + /** + * @return $this + */ + public function dontUseSingleTransaction() + { + $this->useSingleTransaction = false; + + return $this; + } + + /** + * @param string $characterSet + * + * @return $this + */ + public function setDefaultCharacterSet(string $characterSet) + { + $this->defaultCharacterSet = $characterSet; + + return $this; + } + + /** + * @return $this + */ + public function setGtidPurged(string $setGtidPurged) + { + $this->setGtidPurged = $setGtidPurged; + + return $this; + } + + /** + * Dump the contents of the database to the given file. + * + * @param string $dumpFile + * + * @throws \Spatie\DbDumper\Exceptions\CannotStartDump + * @throws \Spatie\DbDumper\Exceptions\DumpFailed + */ + public function dumpToFile(string $dumpFile) + { + $this->guardAgainstIncompleteCredentials(); + + $tempFileHandle = tmpfile(); + fwrite($tempFileHandle, $this->getContentsOfCredentialsFile()); + $temporaryCredentialsFile = stream_get_meta_data($tempFileHandle)['uri']; + + $command = $this->getDumpCommand($dumpFile, $temporaryCredentialsFile); + + $process = new Process($command); + + if (! is_null($this->timeout)) { + $process->setTimeout($this->timeout); + } + + $process->run(); + + $this->checkIfDumpWasSuccessFul($process, $dumpFile); + } + + public function addExtraOption(string $extraOption) + { + if (strpos($extraOption, '--all-databases') !== false) { + $this->dbNameWasSetAsExtraOption = true; + $this->allDatabasesWasSetAsExtraOption = true; + } + + if (preg_match('/^--databases (\S+)/', $extraOption, $matches) === 1) { + $this->setDbName($matches[1]); + $this->dbNameWasSetAsExtraOption = true; + } + + return parent::addExtraOption($extraOption); + } + + /** + * @return $this + */ + public function doNotCreateTables() + { + $this->createTables = false; + + return $this; + } + + /** + * Get the command that should be performed to dump the database. + * + * @param string $dumpFile + * @param string $temporaryCredentialsFile + * + * @return string + */ + public function getDumpCommand(string $dumpFile, string $temporaryCredentialsFile): string + { + $quote = $this->determineQuote(); + + $command = [ + "{$quote}{$this->dumpBinaryPath}mysqldump{$quote}", + "--defaults-extra-file=\"{$temporaryCredentialsFile}\"", + ]; + + if (! $this->createTables) { + $command[] = '--no-create-info'; + } + + if ($this->skipComments) { + $command[] = '--skip-comments'; + } + + $command[] = $this->useExtendedInserts ? '--extended-insert' : '--skip-extended-insert'; + + if ($this->useSingleTransaction) { + $command[] = '--single-transaction'; + } + + if ($this->socket !== '') { + $command[] = "--socket={$this->socket}"; + } + + foreach ($this->excludeTables as $tableName) { + $command[] = "--ignore-table={$this->dbName}.{$tableName}"; + } + + if (! empty($this->defaultCharacterSet)) { + $command[] = '--default-character-set='.$this->defaultCharacterSet; + } + + foreach ($this->extraOptions as $extraOption) { + $command[] = $extraOption; + } + + if ($this->setGtidPurged !== 'AUTO') { + $command[] = '--set-gtid-purged='.$this->setGtidPurged; + } + + if (! $this->dbNameWasSetAsExtraOption) { + $command[] = $this->dbName; + } + + if (! empty($this->includeTables)) { + $includeTables = implode(' ', $this->includeTables); + $command[] = "--tables {$includeTables}"; + } + + return $this->echoToFile(implode(' ', $command), $dumpFile); + } + + public function getContentsOfCredentialsFile(): string + { + $contents = [ + '[client]', + "user = '{$this->userName}'", + "password = '{$this->password}'", + "host = '{$this->host}'", + "port = '{$this->port}'", + ]; + + return implode(PHP_EOL, $contents); + } + + protected function guardAgainstIncompleteCredentials() + { + foreach (['userName', 'host'] as $requiredProperty) { + if (strlen($this->$requiredProperty) === 0) { + throw CannotStartDump::emptyParameter($requiredProperty); + } + } + + if (strlen('dbName') === 0 && ! $this->allDatabasesWasSetAsExtraOption) { + throw CannotStartDump::emptyParameter($requiredProperty); + } + } + + protected function determineQuote(): string + { + return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? '"' : "'"; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/PostgreSql.php b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/PostgreSql.php new file mode 100644 index 0000000..469da2e --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/PostgreSql.php @@ -0,0 +1,122 @@ +port = 5432; + } + + /** + * @return $this + */ + public function useInserts() + { + $this->useInserts = true; + + return $this; + } + + /** + * Dump the contents of the database to the given file. + * + * @param string $dumpFile + * + * @throws \Spatie\DbDumper\Exceptions\CannotStartDump + * @throws \Spatie\DbDumper\Exceptions\DumpFailed + */ + public function dumpToFile(string $dumpFile) + { + $this->guardAgainstIncompleteCredentials(); + + $command = $this->getDumpCommand($dumpFile); + + $tempFileHandle = tmpfile(); + fwrite($tempFileHandle, $this->getContentsOfCredentialsFile()); + $temporaryCredentialsFile = stream_get_meta_data($tempFileHandle)['uri']; + + $process = new Process($command, null, $this->getEnvironmentVariablesForDumpCommand($temporaryCredentialsFile)); + + if (! is_null($this->timeout)) { + $process->setTimeout($this->timeout); + } + + $process->run(); + + $this->checkIfDumpWasSuccessFul($process, $dumpFile); + } + + /** + * Get the command that should be performed to dump the database. + * + * @param string $dumpFile + * + * @return string + */ + public function getDumpCommand(string $dumpFile): string + { + $command = [ + "'{$this->dumpBinaryPath}pg_dump'", + "-U {$this->userName}", + '-h '.($this->socket === '' ? $this->host : $this->socket), + "-p {$this->port}", + ]; + + if ($this->useInserts) { + $command[] = '--inserts'; + } + + foreach ($this->extraOptions as $extraOption) { + $command[] = $extraOption; + } + + if (! empty($this->includeTables)) { + $command[] = '-t '.implode(' -t ', $this->includeTables); + } + + if (! empty($this->excludeTables)) { + $command[] = '-T '.implode(' -T ', $this->excludeTables); + } + + return $this->echoToFile(implode(' ', $command), $dumpFile); + } + + public function getContentsOfCredentialsFile(): string + { + $contents = [ + $this->host, + $this->port, + $this->dbName, + $this->userName, + $this->password, + ]; + + return implode(':', $contents); + } + + protected function guardAgainstIncompleteCredentials() + { + foreach (['userName', 'dbName', 'host'] as $requiredProperty) { + if (empty($this->$requiredProperty)) { + throw CannotStartDump::emptyParameter($requiredProperty); + } + } + } + + protected function getEnvironmentVariablesForDumpCommand(string $temporaryCredentialsFile): array + { + return [ + 'PGPASSFILE' => $temporaryCredentialsFile, + 'PGDATABASE' => $this->dbName, + ]; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/Sqlite.php b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/Sqlite.php new file mode 100644 index 0000000..004abe9 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Databases/Sqlite.php @@ -0,0 +1,49 @@ +getDumpCommand($dumpFile); + + $process = new Process($command); + + if (! is_null($this->timeout)) { + $process->setTimeout($this->timeout); + } + + $process->run(); + + $this->checkIfDumpWasSuccessFul($process, $dumpFile); + } + + /** + * Get the command that should be performed to dump the database. + * + * @param string $dumpFile + * + * @return string + */ + public function getDumpCommand(string $dumpFile): string + { + $command = sprintf( + "echo 'BEGIN IMMEDIATE;\n.dump' | '%ssqlite3' --bail '%s'", + $this->dumpBinaryPath, + $this->dbName + ); + + return $this->echoToFile($command, $dumpFile); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/src/DbDumper.php b/plugins/panakour/backup/vendor/spatie/db-dumper/src/DbDumper.php new file mode 100644 index 0000000..f977cce --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/src/DbDumper.php @@ -0,0 +1,268 @@ +dbName; + } + + /** + * @param string $dbName + * + * @return $this + */ + public function setDbName(string $dbName) + { + $this->dbName = $dbName; + + return $this; + } + + /** + * @param string $userName + * + * @return $this + */ + public function setUserName(string $userName) + { + $this->userName = $userName; + + return $this; + } + + /** + * @param string $password + * + * @return $this + */ + public function setPassword(string $password) + { + $this->password = $password; + + return $this; + } + + /** + * @param string $host + * + * @return $this + */ + public function setHost(string $host) + { + $this->host = $host; + + return $this; + } + + public function getHost(): string + { + return $this->host; + } + + /** + * @param int $port + * + * @return $this + */ + public function setPort(int $port) + { + $this->port = $port; + + return $this; + } + + /** + * @param string $socket + * + * @return $this + */ + public function setSocket(string $socket) + { + $this->socket = $socket; + + return $this; + } + + /** + * @param int $timeout + * + * @return $this + */ + public function setTimeout(int $timeout) + { + $this->timeout = $timeout; + + return $this; + } + + public function setDumpBinaryPath(string $dumpBinaryPath) + { + if ($dumpBinaryPath !== '' && substr($dumpBinaryPath, -1) !== '/') { + $dumpBinaryPath .= '/'; + } + + $this->dumpBinaryPath = $dumpBinaryPath; + + return $this; + } + + /** + * @deprecated + * + * @return $this + */ + public function enableCompression() + { + $this->compressor = new GzipCompressor(); + + return $this; + } + + public function getCompressorExtension(): string + { + return $this->compressor->useExtension(); + } + + public function useCompressor(Compressor $compressor) + { + $this->compressor = $compressor; + + return $this; + } + + /** + * @param string|array $includeTables + * + * @return $this + * + * @throws \Spatie\DbDumper\Exceptions\CannotSetParameter + */ + public function includeTables($includeTables) + { + if (! empty($this->excludeTables)) { + throw CannotSetParameter::conflictingParameters('includeTables', 'excludeTables'); + } + + if (! is_array($includeTables)) { + $includeTables = explode(', ', $includeTables); + } + + $this->includeTables = $includeTables; + + return $this; + } + + /** + * @param string|array $excludeTables + * + * @return $this + * + * @throws \Spatie\DbDumper\Exceptions\CannotSetParameter + */ + public function excludeTables($excludeTables) + { + if (! empty($this->includeTables)) { + throw CannotSetParameter::conflictingParameters('excludeTables', 'includeTables'); + } + + if (! is_array($excludeTables)) { + $excludeTables = explode(', ', $excludeTables); + } + + $this->excludeTables = $excludeTables; + + return $this; + } + + /** + * @param string $extraOption + * + * @return $this + */ + public function addExtraOption(string $extraOption) + { + if (! empty($extraOption)) { + $this->extraOptions[] = $extraOption; + } + + return $this; + } + + abstract public function dumpToFile(string $dumpFile); + + protected function checkIfDumpWasSuccessFul(Process $process, string $outputFile) + { + if (! $process->isSuccessful()) { + throw DumpFailed::processDidNotEndSuccessfully($process); + } + + if (! file_exists($outputFile)) { + throw DumpFailed::dumpfileWasNotCreated(); + } + + if (filesize($outputFile) === 0) { + throw DumpFailed::dumpfileWasEmpty(); + } + } + + protected function echoToFile(string $command, string $dumpFile): string + { + $compressor = $this->compressor + ? ' | '.$this->compressor->useCommand() + : ''; + + $dumpFile = '"'.addcslashes($dumpFile, '\\"').'"'; + + return $command.$compressor.' > '.$dumpFile; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/db-dumper/src/Exceptions/CannotSetParameter.php b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Exceptions/CannotSetParameter.php new file mode 100644 index 0000000..33ab68d --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/db-dumper/src/Exceptions/CannotSetParameter.php @@ -0,0 +1,19 @@ +getExitCode()} : {$process->getExitCodeText()} : {$process->getErrorOutput()}"); + } + + /** + * @return \Spatie\DbDumper\Exceptions\DumpFailed + */ + public static function dumpfileWasNotCreated() + { + return new static('The dumpfile could not be created'); + } + + /** + * @return \Spatie\DbDumper\Exceptions\DumpFailed + */ + public static function dumpfileWasEmpty() + { + return new static('The created dumpfile is empty'); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/dropbox-api/CHANGELOG.md b/plugins/panakour/backup/vendor/spatie/dropbox-api/CHANGELOG.md new file mode 100644 index 0000000..6d41e1a --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/dropbox-api/CHANGELOG.md @@ -0,0 +1,96 @@ +# Changelog + +All notable changes to `dropbox-api` will be documented in this file + +## 1.12.0 - 2020-02-04 + +- add `search` method + +## 1.11.1 - 2019-12-12 + +- make compatible with PHP 7.4 + +## 1.11.0 - 2019-07-04 + +- add `$response` to `BadRequest` + +## 1.10.0 - 2019-07-01 + +- move retry stuff to package + +## 1.9.0 - 2019-05-21 + +- make guzzle retry 5xx and 429 responses + +## 1.8.0 - 2019-04-13 + +- add `getEndpointUrl` +- drop support for PHP 7.0 + +## 1.7.1 - 2019-02-13 + +- fix for `createSharedLinkWithSettings` with empty settings + +## 1.7.0 - 2019-02-06 + +- add getter and setter for the access token + +## 1.6.6 - 2018-07-19 + +- fix for piped streams + +## 1.6.5 - 2018-01-15 + +- adjust `normalizePath` to allow id/rev/ns to be queried + +## 1.6.4 - 2017-12-05 + +- fix max chunk size + +## 1.6.1 - 2017-07-28 + +- fix for finishing upload session + +## 1.6.0 - 2017-07-28 + +- add various new methods to enable chuncked uploads + +## 1.5.3 - 2017-07-28 + +- use recommended `move_v2` method to move files + +## 1.5.2 - 2017-07-17 + +- add missing parameters to `listSharedLinks` method + +## 1.5.1 - 2017-07-17 + +- fix broken `revokeToken` and `getAccountInfo` + +## 1.5.0 - 2017-07-11 + +- add `revokeToken` and `getAccountInfo` + +## 1.4.0 - 2017-07-11 + +- add `listSharedLinks` + +## 1.3.0 - 2017-07-04 + +- add error code to thrown exception + +## 1.2.0 - 2017-04-29 + +- added `createSharedLinkWithSettings` + +## 1.1.0 - 2017-04-22 + +- added `listFolderContinue` + +## 1.0.1 - 2017-04-19 + +- Bugfix: set default value for request body + +## 1.0.0 - 2017-04-19 + +- initial release diff --git a/plugins/panakour/backup/vendor/spatie/dropbox-api/CONTRIBUTING.md b/plugins/panakour/backup/vendor/spatie/dropbox-api/CONTRIBUTING.md new file mode 100644 index 0000000..4da74e3 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/dropbox-api/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/plugins/panakour/backup/vendor/spatie/dropbox-api/LICENSE.md b/plugins/panakour/backup/vendor/spatie/dropbox-api/LICENSE.md new file mode 100644 index 0000000..59e5ec5 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/dropbox-api/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Spatie bvba + +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/plugins/panakour/backup/vendor/spatie/dropbox-api/README.md b/plugins/panakour/backup/vendor/spatie/dropbox-api/README.md new file mode 100644 index 0000000..aadc4dc --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/dropbox-api/README.md @@ -0,0 +1,108 @@ +# A minimal implementation of Dropbox API v2 + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/dropbox-api.svg?style=flat-square)](https://packagist.org/packages/spatie/dropbox-api) +[![Build Status](https://img.shields.io/travis/spatie/dropbox-api/master.svg?style=flat-square)](https://travis-ci.org/spatie/dropbox-api) +[![StyleCI](https://styleci.io/repos/88621289/shield?branch=master)](https://styleci.io/repos/88621289) +[![Quality Score](https://img.shields.io/scrutinizer/g/spatie/dropbox-api.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/dropbox-api) +[![Total Downloads](https://img.shields.io/packagist/dt/spatie/dropbox-api.svg?style=flat-square)](https://packagist.org/packages/spatie/dropbox-api) + +This is a minimal PHP implementation of the [Dropbox API v2](https://www.dropbox.com/developers/documentation/http/overview). It contains only the methods needed for [our flysystem-dropbox adapter](https://github.com/spatie/flysystem-dropbox). We are open however to PRs that add extra methods to the client. + +Here are a few examples on how you can use the package: + +```php +$client = new Spatie\Dropbox\Client($authorizationToken); + +//create a folder +$client->createFolder($path); + +//list a folder +$client->listFolder($path); + +//get a temporary link +$client->getTemporaryLink($path); +``` +## Installation + +You can install the package via composer: + +``` bash +composer require spatie/dropbox-api +``` + +## Usage + +The first thing you need to do is get an authorization token at Dropbox. Unlike [other companies](https://google.com) Dropbox has made this very easy. You can just generate a token in the [App Console](https://www.dropbox.com/developers/apps) for any Dropbox API app. You'll find more info at [the Dropbox Developer Blog](https://blogs.dropbox.com/developers/2014/05/generate-an-access-token-for-your-own-account/). + +With an authorization token you can instantiate a `Spatie\Dropbox\Client`. + +```php +$client = new Spatie\Dropbox\Client($authorizationToken); +``` + +Look in [the source code of `Spatie\Dropbox\Client`](https://github.com/spatie/dropbox-api/blob/master/src/Client.php) to discover the methods you can use. + +If you do not find your favorite method, you can directly use the `contentEndpointRequest` and `rpcEndpointRequest` functions. + +```php +public function contentEndpointRequest(string $endpoint, array $arguments, $body): ResponseInterface + +public function rpcEndpointRequest(string $endpoint, array $parameters): array +``` + +Here's an example: + +```php +$client->rpcEndpointRequest('search', ['path' => '', 'query' => 'bat cave']); +``` + +If you need to change the subdomain of the endpoint URL used in the API request, you can prefix the endpoint path with `subdomain::`. + +Here's an example: + +```php +$client->rpcEndpointRequest('content::files/get_thumbnail_batch', $parameters); +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Testing + +``` bash +composer test +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Security + +If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. + +## Postcardware + +You're free to use this package (it's [MIT-licensed](LICENSE.md)), but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. + +Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. + +We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). + +## Credits + +- [Alex Vanderbist](https://github.com/AlexVanderbist) +- [Freek Van der Herten](https://github.com/freekmurze) +- [All Contributors](../../contributors) + +## Support us + +Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). + +Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). +All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/plugins/panakour/backup/vendor/spatie/dropbox-api/composer.json b/plugins/panakour/backup/vendor/spatie/dropbox-api/composer.json new file mode 100644 index 0000000..d276ae4 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/dropbox-api/composer.json @@ -0,0 +1,51 @@ +{ + "name": "spatie/dropbox-api", + "description": "A minimal implementation of Dropbox API v2", + "keywords": [ + "spatie", + "dropbox-api", + "dropbox", + "api", + "v2" + ], + "homepage": "https://github.com/spatie/dropbox-api", + "license": "MIT", + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex.vanderbist@gmail.com", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "require": { + "php": "^7.1", + "graham-campbell/guzzle-factory": "^3.0", + "guzzlehttp/guzzle": "^6.2" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.15|^8.5" + }, + "autoload": { + "psr-4": { + "Spatie\\Dropbox\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Spatie\\Dropbox\\Test\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit" + }, + "config": { + "sort-packages": true + } +} diff --git a/plugins/panakour/backup/vendor/spatie/dropbox-api/src/Client.php b/plugins/panakour/backup/vendor/spatie/dropbox-api/src/Client.php new file mode 100644 index 0000000..5ed9fe3 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/dropbox-api/src/Client.php @@ -0,0 +1,671 @@ +accessToken = $accessToken; + + $this->client = $client ?? new GuzzleClient(['handler' => GuzzleFactory::handler()]); + + $this->maxChunkSize = ($maxChunkSize < self::MAX_CHUNK_SIZE ? ($maxChunkSize > 1 ? $maxChunkSize : 1) : self::MAX_CHUNK_SIZE); + $this->maxUploadChunkRetries = $maxUploadChunkRetries; + } + + /** + * Copy a file or folder to a different location in the user's Dropbox. + * + * If the source path is a folder all its contents will be copied. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-copy_v2 + */ + public function copy(string $fromPath, string $toPath): array + { + $parameters = [ + 'from_path' => $this->normalizePath($fromPath), + 'to_path' => $this->normalizePath($toPath), + ]; + + return $this->rpcEndpointRequest('files/copy_v2', $parameters); + } + + /** + * Create a folder at a given path. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-create_folder + */ + public function createFolder(string $path): array + { + $parameters = [ + 'path' => $this->normalizePath($path), + ]; + + $object = $this->rpcEndpointRequest('files/create_folder', $parameters); + + $object['.tag'] = 'folder'; + + return $object; + } + + /** + * Create a shared link with custom settings. + * + * If no settings are given then the default visibility is RequestedVisibility.public. + * The resolved visibility, though, may depend on other aspects such as team and + * shared folder settings). Only for paid users. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-create_shared_link_with_settings + */ + public function createSharedLinkWithSettings(string $path, array $settings = []) + { + $parameters = [ + 'path' => $this->normalizePath($path), + ]; + + if (count($settings)) { + $parameters = array_merge(compact('settings'), $parameters); + } + + return $this->rpcEndpointRequest('sharing/create_shared_link_with_settings', $parameters); + } + + /** + * Search a file or folder in the user's Dropbox. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-search + */ + public function search(string $query, bool $includeHighlights = false) + { + $parameters = [ + 'query' => $query, + 'include_highlights' => $includeHighlights, + ]; + + return $this->rpcEndpointRequest('files/search_v2', $parameters); + } + + /** + * List shared links. + * + * For empty path returns a list of all shared links. For non-empty path + * returns a list of all shared links with access to the given path. + * + * If direct_only is set true, only direct links to the path will be returned, otherwise + * it may return link to the path itself and parent folders as described on docs. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-list_shared_links + */ + public function listSharedLinks(string $path = null, bool $direct_only = false, string $cursor = null): array + { + $parameters = [ + 'path' => $path ? $this->normalizePath($path) : null, + 'cursor' => $cursor, + 'direct_only' => $direct_only, + ]; + + $body = $this->rpcEndpointRequest('sharing/list_shared_links', $parameters); + + return $body['links']; + } + + /** + * Delete the file or folder at a given path. + * + * If the path is a folder, all its contents will be deleted too. + * A successful response indicates that the file or folder was deleted. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-delete + */ + public function delete(string $path): array + { + $parameters = [ + 'path' => $this->normalizePath($path), + ]; + + return $this->rpcEndpointRequest('files/delete', $parameters); + } + + /** + * Download a file from a user's Dropbox. + * + * @param string $path + * + * @return resource + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-download + */ + public function download(string $path) + { + $arguments = [ + 'path' => $this->normalizePath($path), + ]; + + $response = $this->contentEndpointRequest('files/download', $arguments); + + return StreamWrapper::getResource($response->getBody()); + } + + /** + * Returns the metadata for a file or folder. + * + * Note: Metadata for the root folder is unsupported. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_metadata + */ + public function getMetadata(string $path): array + { + $parameters = [ + 'path' => $this->normalizePath($path), + ]; + + return $this->rpcEndpointRequest('files/get_metadata', $parameters); + } + + /** + * Get a temporary link to stream content of a file. + * + * This link will expire in four hours and afterwards you will get 410 Gone. + * Content-Type of the link is determined automatically by the file's mime type. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link + */ + public function getTemporaryLink(string $path): string + { + $parameters = [ + 'path' => $this->normalizePath($path), + ]; + + $body = $this->rpcEndpointRequest('files/get_temporary_link', $parameters); + + return $body['link']; + } + + /** + * Get a thumbnail for an image. + * + * This method currently supports files with the following file extensions: + * jpg, jpeg, png, tiff, tif, gif and bmp. + * + * Photos that are larger than 20MB in size won't be converted to a thumbnail. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail + */ + public function getThumbnail(string $path, string $format = 'jpeg', string $size = 'w64h64'): string + { + $arguments = [ + 'path' => $this->normalizePath($path), + 'format' => $format, + 'size' => $size, + ]; + + $response = $this->contentEndpointRequest('files/get_thumbnail', $arguments); + + return (string) $response->getBody(); + } + + /** + * Starts returning the contents of a folder. + * + * If the result's ListFolderResult.has_more field is true, call + * list_folder/continue with the returned ListFolderResult.cursor to retrieve more entries. + * + * Note: auth.RateLimitError may be returned if multiple list_folder or list_folder/continue calls + * with same parameters are made simultaneously by same API app for same user. If your app implements + * retry logic, please hold off the retry until the previous request finishes. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder + */ + public function listFolder(string $path = '', bool $recursive = false): array + { + $parameters = [ + 'path' => $this->normalizePath($path), + 'recursive' => $recursive, + ]; + + return $this->rpcEndpointRequest('files/list_folder', $parameters); + } + + /** + * Once a cursor has been retrieved from list_folder, use this to paginate through all files and + * retrieve updates to the folder, following the same rules as documented for list_folder. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder-continue + */ + public function listFolderContinue(string $cursor = ''): array + { + return $this->rpcEndpointRequest('files/list_folder/continue', compact('cursor')); + } + + /** + * Move a file or folder to a different location in the user's Dropbox. + * + * If the source path is a folder all its contents will be moved. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-move_v2 + */ + public function move(string $fromPath, string $toPath): array + { + $parameters = [ + 'from_path' => $this->normalizePath($fromPath), + 'to_path' => $this->normalizePath($toPath), + ]; + + return $this->rpcEndpointRequest('files/move_v2', $parameters); + } + + /** + * The file should be uploaded in chunks if it size exceeds the 150 MB threshold + * or if the resource size could not be determined (eg. a popen() stream). + * + * @param string|resource $contents + * + * @return bool + */ + protected function shouldUploadChunked($contents): bool + { + $size = is_string($contents) ? strlen($contents) : fstat($contents)['size']; + + if ($this->isPipe($contents)) { + return true; + } + + if ($size === null) { + return true; + } + + return $size > $this->maxChunkSize; + } + + /** + * Check if the contents is a pipe stream (not seekable, no size defined). + * + * @param string|resource $contents + * + * @return bool + */ + protected function isPipe($contents): bool + { + return is_resource($contents) ? (fstat($contents)['mode'] & 010000) != 0 : false; + } + + /** + * Create a new file with the contents provided in the request. + * + * Do not use this to upload a file larger than 150 MB. Instead, create an upload session with upload_session/start. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload + * + * @param string $path + * @param string|resource $contents + * @param string $mode + * + * @return array + */ + public function upload(string $path, $contents, $mode = 'add'): array + { + if ($this->shouldUploadChunked($contents)) { + return $this->uploadChunked($path, $contents, $mode); + } + + $arguments = [ + 'path' => $this->normalizePath($path), + 'mode' => $mode, + ]; + + $response = $this->contentEndpointRequest('files/upload', $arguments, $contents); + + $metadata = json_decode($response->getBody(), true); + + $metadata['.tag'] = 'file'; + + return $metadata; + } + + /** + * Upload file split in chunks. This allows uploading large files, since + * Dropbox API v2 limits the content size to 150MB. + * + * The chunk size will affect directly the memory usage, so be careful. + * Large chunks tends to speed up the upload, while smaller optimizes memory usage. + * + * @param string $path + * @param string|resource $contents + * @param string $mode + * @param int $chunkSize + * + * @return array + */ + public function uploadChunked(string $path, $contents, $mode = 'add', $chunkSize = null): array + { + if ($chunkSize === null || $chunkSize > $this->maxChunkSize) { + $chunkSize = $this->maxChunkSize; + } + + $stream = $this->getStream($contents); + + $cursor = $this->uploadChunk(self::UPLOAD_SESSION_START, $stream, $chunkSize, null); + + while (! $stream->eof()) { + $cursor = $this->uploadChunk(self::UPLOAD_SESSION_APPEND, $stream, $chunkSize, $cursor); + } + + return $this->uploadSessionFinish('', $cursor, $path, $mode); + } + + /** + * @param int $type + * @param Psr7\Stream $stream + * @param int $chunkSize + * @param \Spatie\Dropbox\UploadSessionCursor|null $cursor + * @return \Spatie\Dropbox\UploadSessionCursor + * @throws Exception + */ + protected function uploadChunk($type, &$stream, $chunkSize, $cursor = null): UploadSessionCursor + { + $maximumTries = $stream->isSeekable() ? $this->maxUploadChunkRetries : 0; + $pos = $stream->tell(); + + $tries = 0; + + tryUpload: + try { + $tries++; + + $chunkStream = new Psr7\LimitStream($stream, $chunkSize, $stream->tell()); + + if ($type === self::UPLOAD_SESSION_START) { + return $this->uploadSessionStart($chunkStream); + } + + if ($type === self::UPLOAD_SESSION_APPEND && $cursor !== null) { + return $this->uploadSessionAppend($chunkStream, $cursor); + } + + throw new Exception('Invalid type'); + } catch (RequestException $exception) { + if ($tries < $maximumTries) { + // rewind + $stream->seek($pos, SEEK_SET); + goto tryUpload; + } + throw $exception; + } + } + + /** + * Upload sessions allow you to upload a single file in one or more requests, + * for example where the size of the file is greater than 150 MB. + * This call starts a new upload session with the given data. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-start + * + * @param string|StreamInterface $contents + * @param bool $close + * + * @return UploadSessionCursor + */ + public function uploadSessionStart($contents, bool $close = false): UploadSessionCursor + { + $arguments = compact('close'); + + $response = json_decode( + $this->contentEndpointRequest('files/upload_session/start', $arguments, $contents)->getBody(), + true + ); + + return new UploadSessionCursor($response['session_id'], ($contents instanceof StreamInterface ? $contents->tell() : strlen($contents))); + } + + /** + * Append more data to an upload session. + * When the parameter close is set, this call will close the session. + * A single request should not upload more than 150 MB. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-append_v2 + * + * @param string|StreamInterface $contents + * @param UploadSessionCursor $cursor + * @param bool $close + * + * @return \Spatie\Dropbox\UploadSessionCursor + */ + public function uploadSessionAppend($contents, UploadSessionCursor $cursor, bool $close = false): UploadSessionCursor + { + $arguments = compact('cursor', 'close'); + + $pos = $contents instanceof StreamInterface ? $contents->tell() : 0; + $this->contentEndpointRequest('files/upload_session/append_v2', $arguments, $contents); + + $cursor->offset += $contents instanceof StreamInterface ? ($contents->tell() - $pos) : strlen($contents); + + return $cursor; + } + + /** + * Finish an upload session and save the uploaded data to the given file path. + * A single request should not upload more than 150 MB. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-finish + * + * @param string|StreamInterface $contents + * @param \Spatie\Dropbox\UploadSessionCursor $cursor + * @param string $path + * @param string|array $mode + * @param bool $autorename + * @param bool $mute + * + * @return array + */ + public function uploadSessionFinish($contents, UploadSessionCursor $cursor, string $path, $mode = 'add', $autorename = false, $mute = false): array + { + $arguments = compact('cursor'); + $arguments['commit'] = compact('path', 'mode', 'autorename', 'mute'); + + $response = $this->contentEndpointRequest( + 'files/upload_session/finish', + $arguments, + ($contents == '') ? null : $contents + ); + + $metadata = json_decode($response->getBody(), true); + + $metadata['.tag'] = 'file'; + + return $metadata; + } + + /** + * Get Account Info for current authenticated user. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account + * + * @return array + */ + public function getAccountInfo(): array + { + return $this->rpcEndpointRequest('users/get_current_account'); + } + + /** + * Revoke current access token. + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#auth-token-revoke + */ + public function revokeToken() + { + $this->rpcEndpointRequest('auth/token/revoke'); + } + + protected function normalizePath(string $path): string + { + if (preg_match("/^id:.*|^rev:.*|^(ns:[0-9]+(\/.*)?)/", $path) === 1) { + return $path; + } + + $path = trim($path, '/'); + + return ($path === '') ? '' : '/'.$path; + } + + protected function getEndpointUrl(string $subdomain, string $endpoint): string + { + if (count($parts = explode('::', $endpoint)) === 2) { + [$subdomain, $endpoint] = $parts; + } + + return "https://{$subdomain}.dropboxapi.com/2/{$endpoint}"; + } + + /** + * @param string $endpoint + * @param array $arguments + * @param string|resource|StreamInterface $body + * + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \Exception + */ + public function contentEndpointRequest(string $endpoint, array $arguments, $body = ''): ResponseInterface + { + $headers = ['Dropbox-API-Arg' => json_encode($arguments)]; + + if ($body !== '') { + $headers['Content-Type'] = 'application/octet-stream'; + } + + try { + $response = $this->client->post($this->getEndpointUrl('content', $endpoint), [ + 'headers' => $this->getHeaders($headers), + 'body' => $body, + ]); + } catch (ClientException $exception) { + throw $this->determineException($exception); + } + + return $response; + } + + public function rpcEndpointRequest(string $endpoint, array $parameters = null): array + { + try { + $options = ['headers' => $this->getHeaders()]; + + if ($parameters) { + $options['json'] = $parameters; + } + + $response = $this->client->post($this->getEndpointUrl('api', $endpoint), $options); + } catch (ClientException $exception) { + throw $this->determineException($exception); + } + + $response = json_decode($response->getBody(), true); + + return $response ?? []; + } + + protected function determineException(ClientException $exception): Exception + { + if (in_array($exception->getResponse()->getStatusCode(), [400, 409])) { + return new BadRequest($exception->getResponse()); + } + + return $exception; + } + + /** + * @param $contents + * + * @return \GuzzleHttp\Psr7\PumpStream|\GuzzleHttp\Psr7\Stream + */ + protected function getStream($contents) + { + if ($this->isPipe($contents)) { + /* @var resource $contents */ + return new PumpStream(function ($length) use ($contents) { + $data = fread($contents, $length); + if (strlen($data) === 0) { + return false; + } + + return $data; + }); + } + + return Psr7\stream_for($contents); + } + + /** + * Get the access token. + */ + public function getAccessToken(): string + { + return $this->accessToken; + } + + /** + * Set the access token. + */ + public function setAccessToken(string $accessToken): self + { + $this->accessToken = $accessToken; + + return $this; + } + + /** + * Get the HTTP headers. + */ + protected function getHeaders(array $headers = []): array + { + return array_merge([ + 'Authorization' => "Bearer {$this->accessToken}", + ], $headers); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/dropbox-api/src/Exceptions/BadRequest.php b/plugins/panakour/backup/vendor/spatie/dropbox-api/src/Exceptions/BadRequest.php new file mode 100644 index 0000000..20696be --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/dropbox-api/src/Exceptions/BadRequest.php @@ -0,0 +1,36 @@ +response = $response; + + $body = json_decode($response->getBody(), true); + + if ($body !== null) { + if (isset($body['error']['.tag'])) { + $this->dropboxCode = $body['error']['.tag']; + } + + parent::__construct($body['error_summary']); + } + } +} diff --git a/plugins/panakour/backup/vendor/spatie/dropbox-api/src/UploadSessionCursor.php b/plugins/panakour/backup/vendor/spatie/dropbox-api/src/UploadSessionCursor.php new file mode 100644 index 0000000..eba74a3 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/dropbox-api/src/UploadSessionCursor.php @@ -0,0 +1,26 @@ +session_id = $session_id; + $this->offset = $offset; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/CHANGELOG.md b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/CHANGELOG.md new file mode 100644 index 0000000..99d5360 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +All notable changes to `flysystem-dropbox` will be documented in this file + +## 1.2.2 - 2019-12-04 + +- fix `createSharedLinkWithSettings` + +## 1.2.1 - 2019-09-14 + +- fix minimum dep + +## 1.2.0 - 2019-09-13 + +- add `getUrl` method + +## 1.1.0 - 2019-05-31 + +- add `createSharedLinkWithSettings` + +## 1.0.6 - 2017-11-18 + +- determine mimetype from filename + +## 1.0.5 - 2017-10-21 + +- do not throw an exception when listing a non-existing directory + +## 1.0.4 - 2017-10-19 + +- make sure all files are retrieved when calling `listContents` + +## 1.0.3 - 2017-05-18 + +- reverts changes made in 1.0.2 + +## 1.0.2 - 2017-05-18 + +- fix for files with different casings not showing up + +## 1.0.1 - 2017-05-09 + +- add `size` key. `bytes` is deprecated and will be removed in the next major version. + + +## 1.0.0 - 2017-04-19 + +- initial release diff --git a/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/CONTRIBUTING.md b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/CONTRIBUTING.md new file mode 100644 index 0000000..4da74e3 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/LICENSE.md b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/LICENSE.md new file mode 100644 index 0000000..59e5ec5 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Spatie bvba + +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/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/README.md b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/README.md new file mode 100644 index 0000000..9618e7b --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/README.md @@ -0,0 +1,85 @@ +# Flysystem adapter for the Dropbox v2 API + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/flysystem-dropbox.svg?style=flat-square)](https://packagist.org/packages/spatie/flysystem-dropbox) +[![Build Status](https://img.shields.io/travis/spatie/flysystem-dropbox/master.svg?style=flat-square)](https://travis-ci.org/spatie/flysystem-dropbox) +[![StyleCI](https://styleci.io/repos/88596787/shield?branch=master)](https://styleci.io/repos/88596787) +[![Quality Score](https://img.shields.io/scrutinizer/g/spatie/flysystem-dropbox.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/flysystem-dropbox) +[![Total Downloads](https://img.shields.io/packagist/dt/spatie/flysystem-dropbox.svg?style=flat-square)](https://packagist.org/packages/spatie/flysystem-dropbox) + +This package contains a [Flysystem](https://flysystem.thephpleague.com/) adapter for Dropbox. Under the hood, the [Dropbox API v2](https://www.dropbox.com/developers/documentation/http/overview) is used. + +## Installation + +You can install the package via composer: + +``` bash +composer require spatie/flysystem-dropbox +``` + +## Upgrading from thephpleague/flysystem + +[thephpleague/flysystem](https://github.com/thephpleague/flysystem) under the hood uses v1 of the Dropbox API which will be turned off soon. Luckily upgrading is easy. Read [this blogpost](https://murze.be/2017/04/dropbox-will-turn-off-v1-of-their-api-soon-its-time-to-update-your-php-application/) to learn how to upgrade. + +## Usage + +The first thing you need to do is get an authorization token at Dropbox. A token can be generated in the [App Console](https://www.dropbox.com/developers/apps) for any Dropbox API app. You'll find more info at [the Dropbox Developer Blog](https://blogs.dropbox.com/developers/2014/05/generate-an-access-token-for-your-own-account/). + +``` php +use League\Flysystem\Filesystem; +use Spatie\Dropbox\Client; +use Spatie\FlysystemDropbox\DropboxAdapter; + +$client = new Client($authorizationToken); + +$adapter = new DropboxAdapter($client); + +$filesystem = new Filesystem($adapter, ['case_sensitive' => false]); +``` +Note: Because Dropbox is not case-sensitive you’ll need to set the 'case_sensitive' option to false. + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Testing + +``` bash +$ composer test +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Security + +If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. + +## Postcardware + +You're free to use this package (it's [MIT-licensed](LICENSE.md)), but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. + +Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. + +We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). + +## Stuck on PHP 5? + +This package is PHP 7 only. If you need PHP5 support take a look at [this fork](https://github.com/srmklive/flysystem-dropbox-v2). + +## Credits + +- [Alex Vanderbist](https://github.com/AlexVanderbist) +- [Freek Van der Herten](https://github.com/freekmurze) +- [All Contributors](../../contributors) + +## Support us + +Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). + +Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). +All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/composer.json b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/composer.json new file mode 100644 index 0000000..19fb0f5 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/composer.json @@ -0,0 +1,46 @@ +{ + "name": "spatie/flysystem-dropbox", + "description": "Flysystem Adapter for the Dropbox v2 API", + "keywords": [ + "spatie", + "flysystem-dropbox", + "flysystem", + "dropbox", + "v2", + "api" + ], + "homepage": "https://github.com/spatie/flysystem-dropbox", + "license": "MIT", + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex.vanderbist@gmail.com", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "require": { + "php": "^7.0", + "league/flysystem": "^1.0.20", + "spatie/dropbox-api": "^1.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "autoload": { + "psr-4": { + "Spatie\\FlysystemDropbox\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Spatie\\FlysystemDropbox\\Test\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit" + }, + "config": { + "sort-packages": true + } +} diff --git a/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/src/DropboxAdapter.php b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/src/DropboxAdapter.php new file mode 100644 index 0000000..82a4941 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/flysystem-dropbox/src/DropboxAdapter.php @@ -0,0 +1,323 @@ +client = $client; + + $this->setPathPrefix($prefix); + } + + /** + * {@inheritdoc} + */ + public function write($path, $contents, Config $config) + { + return $this->upload($path, $contents, 'add'); + } + + /** + * {@inheritdoc} + */ + public function writeStream($path, $resource, Config $config) + { + return $this->upload($path, $resource, 'add'); + } + + /** + * {@inheritdoc} + */ + public function update($path, $contents, Config $config) + { + return $this->upload($path, $contents, 'overwrite'); + } + + /** + * {@inheritdoc} + */ + public function updateStream($path, $resource, Config $config) + { + return $this->upload($path, $resource, 'overwrite'); + } + + /** + * {@inheritdoc} + */ + public function rename($path, $newPath): bool + { + $path = $this->applyPathPrefix($path); + $newPath = $this->applyPathPrefix($newPath); + + try { + $this->client->move($path, $newPath); + } catch (BadRequest $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function copy($path, $newpath): bool + { + $path = $this->applyPathPrefix($path); + $newpath = $this->applyPathPrefix($newpath); + + try { + $this->client->copy($path, $newpath); + } catch (BadRequest $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function delete($path): bool + { + $location = $this->applyPathPrefix($path); + + try { + $this->client->delete($location); + } catch (BadRequest $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function deleteDir($dirname): bool + { + return $this->delete($dirname); + } + + /** + * {@inheritdoc} + */ + public function createDir($dirname, Config $config) + { + $path = $this->applyPathPrefix($dirname); + + try { + $object = $this->client->createFolder($path); + } catch (BadRequest $e) { + return false; + } + + return $this->normalizeResponse($object); + } + + /** + * {@inheritdoc} + */ + public function has($path) + { + return $this->getMetadata($path); + } + + /** + * {@inheritdoc} + */ + public function read($path) + { + if (! $object = $this->readStream($path)) { + return false; + } + + $object['contents'] = stream_get_contents($object['stream']); + fclose($object['stream']); + unset($object['stream']); + + return $object; + } + + /** + * {@inheritdoc} + */ + public function readStream($path) + { + $path = $this->applyPathPrefix($path); + + try { + $stream = $this->client->download($path); + } catch (BadRequest $e) { + return false; + } + + return compact('stream'); + } + + /** + * {@inheritdoc} + */ + public function listContents($directory = '', $recursive = false): array + { + $location = $this->applyPathPrefix($directory); + + try { + $result = $this->client->listFolder($location, $recursive); + } catch (BadRequest $e) { + return []; + } + + $entries = $result['entries']; + + while ($result['has_more']) { + $result = $this->client->listFolderContinue($result['cursor']); + $entries = array_merge($entries, $result['entries']); + } + + if (! count($entries)) { + return []; + } + + return array_map(function ($entry) { + $path = $this->removePathPrefix($entry['path_display']); + + return $this->normalizeResponse($entry, $path); + }, $entries); + } + + /** + * {@inheritdoc} + */ + public function getMetadata($path) + { + $path = $this->applyPathPrefix($path); + + try { + $object = $this->client->getMetadata($path); + } catch (BadRequest $e) { + return false; + } + + return $this->normalizeResponse($object); + } + + /** + * {@inheritdoc} + */ + public function getSize($path) + { + return $this->getMetadata($path); + } + + /** + * {@inheritdoc} + */ + public function getMimetype($path) + { + return ['mimetype' => MimeType::detectByFilename($path)]; + } + + /** + * {@inheritdoc} + */ + public function getTimestamp($path) + { + return $this->getMetadata($path); + } + + public function getTemporaryLink(string $path): string + { + return $this->client->getTemporaryLink($path); + } + + public function getTemporaryUrl(string $path): string + { + return $this->getTemporaryLink($path); + } + + public function getUrl(string $path): string + { + return $this->getTemporaryLink($path); + } + + public function getThumbnail(string $path, string $format = 'jpeg', string $size = 'w64h64') + { + return $this->client->getThumbnail($path, $format, $size); + } + + public function createSharedLinkWithSettings($path, $settings) + { + return $this->client->createSharedLinkWithSettings($path, $settings); + } + + /** + * {@inheritdoc} + */ + public function applyPathPrefix($path): string + { + $path = parent::applyPathPrefix($path); + + return '/'.trim($path, '/'); + } + + public function getClient(): Client + { + return $this->client; + } + + /** + * @param string $path + * @param resource|string $contents + * @param string $mode + * + * @return array|false file metadata + */ + protected function upload(string $path, $contents, string $mode) + { + $path = $this->applyPathPrefix($path); + + try { + $object = $this->client->upload($path, $contents, $mode); + } catch (BadRequest $e) { + return false; + } + + return $this->normalizeResponse($object); + } + + protected function normalizeResponse(array $response): array + { + $normalizedPath = ltrim($this->removePathPrefix($response['path_display']), '/'); + + $normalizedResponse = ['path' => $normalizedPath]; + + if (isset($response['server_modified'])) { + $normalizedResponse['timestamp'] = strtotime($response['server_modified']); + } + + if (isset($response['size'])) { + $normalizedResponse['size'] = $response['size']; + $normalizedResponse['bytes'] = $response['size']; + } + + $type = ($response['.tag'] === 'folder' ? 'dir' : 'file'); + $normalizedResponse['type'] = $type; + + return $normalizedResponse; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/CHANGELOG.md b/plugins/panakour/backup/vendor/spatie/laravel-backup/CHANGELOG.md new file mode 100644 index 0000000..79eff1f --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/CHANGELOG.md @@ -0,0 +1,597 @@ +# Changelog + +All notable changes to `laravel-backup` will be documented in this file. + +## 5.12.0 - 2018-11-28 + +- added the ability to use `backup_options` on a disk defined in the `filesystems` config file + +## 5.11.4 - 2018-11-27 + +- restore lumen compatibility + +## 5.11.3 - 2018-11-03 + +- fix bugs regarding cleaning backups + +## 5.11.2 - 2018-10-30 + +- make sure the backup size is correctly displayed + +## 5.11.1 - 2018-10-20 + +- fix bug where compressor config option would not be respected + +## 5.11.0 - 2018-10-18 + +- add support for database compressors + +## 5.10.1 - 2018-08-24 + +- allow Laravel 5.7 + +## 5.10.0 - 2018-08-09 + +- add `stream()` on `Backup` + +## 5.9.3 - 2018-08-04 + +- set default for `disableNotifications` on `CleanupJob` + +## 5.9.2 - 2018-08-04 + +- resolve `CleanupStrategy` out of the container + +## 5.9.1 - 2018-06-19 + +- set default when `temporary_directory` config option is not set + +## 5.9.0 - 2018-06-18 + +**THIS VERSION IS BROKEN, DO NOT USE** + +- add `temporary_directory` config option + +## 5.9.0 - 2018-08-09 + +- add `stream()` to `Backup` + +## 5.8.0 - 2018-06-09 + +- add Polish translation + +## 5.7.0 - 2018-05-11 + +- add Persian translation + +## 5.6.6 - 2018-05-05 + +- fix composer requirements + +## 5.6.5 - 2018-05-01 + +**THIS RELEASE WAS DELETED BECAUSE IT COULD GET PULLED IN WITH ONLY PHP7.0 INSTALLED** + +- only zip files will get threated as backup files +- drop support for PHP 7.0 + +## 5.6.4 - 2018-04-30 + +- gzipping is now handled by db-dumper + +## 5.6.3 - 2018-04-26 + +- fix wrong import + +## 5.6.2 - 2018-04-24 + +- lower storage requirements by removing the dumped database file after gzipping it + +## 5.6.1 - 2018-04-13 + +- improved compatiblity with MariaDB +- improved compatiblity with Google Drive + +## 5.6.0 - 2018-04-03 +- add `icon` and `username` to slack config + +## 5.5.1 - 2018-03-17 +- fix French translation + +## 5.5.0 - 2018-03-17 +- add Hindi translation + +## 5.4.1 - 2018-03-04 +- fix typo + +## 5.4.0 - 2018-03-01 +- add turkish translation + +## 5.3.0 - 2018-02-26 +- allow filtering on db name + +## 5.2.2 - 2018-02-23 +- fix typos in exception messages + +## 5.2.1 - 2018-02-08 +- add support for L5.6 + +## 5.2.0 - 2018-02-06 +- add indonesian translation + +## 5.1.5 - 2018-01-20 +- more improvements to use correct exit codes + +## 5.1.4 - 2018-01-18 +- use correct exit codes + +## 5.1.3 - 2018-01-09 +- fix for apps using multiple dbs + +## 5.1.2 - 2017-11-26 +- use `config` instead of `env` to get the app name + +## 5.1.1 - 2017-11-03 +- fix deleting all backups when using maximum storage + +## 5.1.0 - 2017-11-01 +- add Italian translations + +## 5.0.5 - 2017-10-15 +- use all configuration keys when using `read` database connections + +## 5.0.4 - 2017-10-01 +- fix CleanupHasFailed application_name translations + +## 5.0.3 - 2017-09-29 +- use `APP_NAME` instead of `APP_URL` to name the backup + +## 5.0.2 - 2017-09-29 +- renamed temporary directory + +## 5.0.1 - 2017-09-26 +- type hint config contract instead of concreate config class on `EventHandler` + +## 5.0.0 - 2017-08-30 +- added support for Laravel 5.5, dropped support for older versions of the framework +- renamed config file from `laravel-backup` to `backup` + +## 4.19.2 - 2017-08-29 +- make sure the temp directory is empty before starting the backup + +## 4.19.1 - 2017-08-03 + - fix bug in default cleaning strategy + +## 4.19.0 - 2017-08-02 + - add Spanish translations + +## 4.18.1 - 2017-07-13 +- close resource in backup destination if this was not already done by Flysystem + +## 4.18.0 - 2017-06-15 + - add `disable-notifications` option to `backup` and `clean` commands + +## 4.17.0 - 2017-06-01 + - add Danish translation + +## 4.16.0 - 2017-05-23 + - add French translation + +## 4.15.0 - 2017-05-20 + - add Romanian translation + +## 4.14.2 - 2017-05-18 +- fix for empty backup when trying to back up a single file + +## 4.14.1 - 2017-05-09 +- prevent overwriting of dump files when two databases with the same name (but other driver) are dumped + +## 4.14.0 - 2017-05-09 +- add support for MongoDB. + +## 4.13.1 - 2017-05-01 +- fix call to undefined method getFilesystemName + +## 4.13.0 - 2017-04-26 +- add support for gzipping database dumps + +## 4.12.1 - 2017-04-19 +- optimise `backup:list` for external file systems + +## 4.12.0 - 2017-04-14 +- add Russian translation + +## 4.11.0 - 2017-04-14 +- add Ukranian translation + +## 4.10.0 - 2017-04-11 +- add ability to override the Slack channel in the config file + +## 4.9.0 - 2017-04-11 +- add pt-BR translation + +## 4.8.1 - 2017-04-06 +- dump mysql databases in the configured charset + +## 4.8.0 - 2017-04-02 +- add Arabic translation + +## 4.7.2 - 2017-03-31 +- fix bug where a file that was already closed by Flysystem would be closed again + +## 4.7.1 - 2017-03-14 +- do not send mail notification when config for notification contains an empty string + +## 4.7.0 - 2017-03-14 +- added German translations + +## 4.6.6 - 2017-02-22 +- fix for `File is busy` error + +## 4.6.5 - 2017-02-19 +- added `backupName` to `backupDestinationProperties` of notifications + +## 4.6.4 - 2017-02-17 +- fix `unhealthy_backup_found_full` translation + +## 4.6.3 - 2017-02-17 +- fix `unhealthy_backup_found_full` translation + +## 4.6.2 - 2017-02-17 +- fixed translation for `UnhealthyBackupWasFound` notification +- fixed support for floating point numbers for maximum allow storage + +## 4.6.1 - 2017-02-16 +- fixed translations for notifications + +## 4.6.0 - 2017-02-15 +- add translations for notifications + +## 4.5.0 - 2017-02-12 +- add SQLite support + +## 4.4.9 - 2017-02-06 +- fix the dumping of DB's on Windows systems + +## 4.4.8 - 2017-02-06 +- avoid empty directories in zips on Windows systems + +## 4.4.7 - 2017-02-04 +- improve the creation of db dumper subdirectory in the temporary directory + +## 4.4.6 - 2017-02-04 +- force creation of temporary directory + +## 4.4.5 - 2017-02-03 +- force `BackupDestinationStatus::maximumAllowedUsageInBytes()` to return an integer + +## 4.4.4 - 2017-02-02 + +- fix constraints so the latest version of `spatie/temporary-backup` can be pulled in + +## 4.4.3 - 2017-02-02 + +- fix bug where entire backup disk would be ignored for backups + +## 4.4.2 - 2017-02-01 + +- improve handling of temporary directory + +## 4.4.1 - 2017-01-26 + +- fix typehint of `setMaximumStorageUsageInMegabytes` + +## 4.4.0 - 2017-01-23 + +- add compatibility for Laravel 5.4 + +## 4.3.4 - 2017-01-22 + +- fix bugs in passing values from the database dump config to the db dumpers + +## 4.3.3 - 2017-01-19 + +- fix error where `filename` option would not be respected in the `BackupCommand` + +## 4.3.2 - 2017-01-02 + +- fix errors when `app.name` is empty + +## 4.3.1 - 2016-12-11 + +- fix excluding paths of symlinked directories + +## 4.3.0 - 2016-11-26 + +- added `filename_prefix` to config file + +## 4.2.0 - 2016-11-19 + +- added `BackupZipCreated` event + +## 4.1.0 - 2016-10-21 + +- added the ability to use a read-only host for db backups + +## 4.0.4 - 2016-10-19 + +- use 24h clock when determining names for the zipfile. + +## 4.0.3 - 2016-10-02 + +- fix for performance problems when backing up a large number of files + +## 4.0.2 - 2016-09-21 + +- various bugfixes for the backup monitor + +## 4.0.1 - 2016-09-20 + +- fix for dumping of databases than run on custom ports + +## 4.0.0 - 2016-09-17 + +- removed custom notification system in favor of Laravel 5.3's native notifications +- made it easier to pass custom arguments to the database dumpers +- refactored most classes +- dropped PHP 5 support + +## 3.10.2 - 2016-08-24 + +- added L5.3 compatibility + +## 3.10.1 - 2016-08-16 + +- refactored some code so backing up only writes to a disk without reading from it + +## 3.10.0 - 2016-08-16 + +- made backup filename configurable + +## 3.9.0 - 2016-08-07 + +- added telegram sender + +## 3.8.2 - 2016-07-27 + +- fixed wrong comment in the config file + +## 3.8.1 - 2016-07-06 + +- vastly reduce memory usage and speed up backup + +## 3.8.0 - 2016-06-16 + +- the backup:list command now highlights the problems with a backupdestination when it is unhealty + +## 3.7.2 - 2016-05-28 + +- refactor `FileSelection` in an attempt to reduce memory usage + +## 3.7.1 - 2016-05-13 + +- fix for missing `followLinks` option after running `composer update` + +## 3.7.0 - 2016-05-12 + +- added an option to determine if symlinks should be followed when selecting files + +## 3.6.1 - 2016-05-10 + +- refactored wildcard support + +## 3.6.0 - 2016-05-10 + +- add support for wildcards in excluding paths + +## 3.5.0 - 2016-04-27 + +- add support for dumping a mysql db using a single transaction + +## 3.4.4 - 2016-04-18 + +- fixed the capitalization of `CleanupWasSuccessful` + +## 3.4.3 - 2016-04-18 + +- the `port` configuration of a postgresql db will now be used when dumping the db + +## 3.4.2 - 2016-04-13 + +- the `port` configuration of a mysql db will now be used when dumping the db + +## 3.4.1 - 2016-04-07 + +- fixed the `--only-to-disk` option in `backup:run` + +## 3.4.0 - 2016-04-03 + +- added the ability to use inserts when dumping a PostgreSQL db + +## 3.3.3 - 2016-04-01 + +- fixed a bug where the error events would not hold the exceptions in the right variable + +## 3.3.2 - 2016-03-30 + +- excluded node_modules in default backup configuration + +## 3.3.1 - 2016-03-29 + +- fix bug in service provider + +## 3.3.0 - 2016-03-29 + +## This version contains a bug in the service provider. Please upgrade to 3.3.1 + +- made the pushover sounds configurable + +## 3.2.2 - 2016-03-16 + +- made sure that, when a notifier fails, the other notifiers wil still get called + +## 3.2.1 - 2016-03-16 + +- fixed a typo in the config file + +## 3.2.0 - 2016-03-16 + +- added pushover sender + +## 3.1.4 - 2016-03-16 + +- added an option to specify a timeout for the database dumpers +- fixed a bug where notifications for certain events would not be sent + +## 3.1.3 - 2016-03-16 + +- added an option to specify a custom mysqldump or pg_dump path, by adding `dump_command_path` in the database configuration file, for that particular database + +## 3.1.2 - 2016-03-14 + +- upped the required version of db-dumper to a bug free version + +## 3.1.1 - 2016-03-13 + +- fixed `backup:list`-command + +## 3.1.0 - 2016-03-13 + +**This version contains a bug, that pops up when running `backup:list`. Please upgrade to 3.1.1** + +- added support for PostgreSQL +- added an option to the backup command to backup only to a specified diskname +- renamed `filesystems` to `disks` in the config file, console output, events and error messages (in a non-breaking way, the old "filesystems" key will still work) + +## 3.0.5 - 2016-03-09 + +- improve the console output + +## 3.0.4 - 2016-03-08 + +- fixed the monitor command in Laravel 5.1 apps + +## 3.0.3 - 2016-03-08 + +- make backup destinations more robust when using non existing file systems + +## 3.0.2 - 2016-03-08 + +- added console output when a backup command fails + +## 3.0.1 - 2016-03-08 + +- fixed a bug in the mail and slack notification senders + +## 3.0.0 - 2016-03-08 + +Complete rewrite with lots of new features: + +- added a new strategy to clean up old backups +- added a monitor to check the health of the backups +- added notifications to keep you informed about the status of the backups +- databases will now be dumped using the separate spatie/db-dumper package +- full documentation is now provided on https://docs.spatie.be/laravel-backup + + +## 2.10.0 +- Add `list`-command +- Make the `dump_command_path`-option a bit more robust + +## 2.9.2 +- Fix installation error when using Symfony 3 + +## 2.9.1 +- Fixed a bug that prevented to write directly into the root of an S3 bucket + +## 2.9.0 +- Added support for PostgreSQL. + +## 2.8.3 +- Further improve the clean up of temporary files. + +## 2.8.2 +- Improve the clean up of temporary files. + +## 2.8.1 +- Fixed determining the driver of the database. + +## 2.8.0 +- The temp backup file will now be explicitly deleted. + +## 2.7.0 +- Add `only-files`-option + +## 2.6.0 +- Display warning when backupping zero bytes + +## 2.5.1 +- Fix tests + +## 2.5.0 +- Added option to specify the timeout of the mysqldump command + +## 2.4.2 +- Fixed an issue where the incorrect backup filename would be displayed + +## 2.4.1 +- Changed github repo location + +## 2.4.0 +- Add option to enable mysqldump's extended insert + +## 2.3.2 +- Fixed a bug that caused a failure when backing up a large db + +## 2.3.1 +- Fixed a bug where the backups would not be stored in the right directory + +## 2.3.0 +- Add options to specifify a suffix and a prefix for the backup-zip-file +- Add support for laravel installation that have seperate hosts for reading a writing a db + +## 2.2.1 +- Fixes issues where not the whole db gets backed up when not using a socket + +## 2.2.0 (Warning: this version contains a critical bug that could cause an incomplete backup of the database. This issue has been fixed in version 2.2.1) +- Add support for custom sockets + +## 2.1.2 +- Package is now compatible with php 5.4 + +## 2.1.1 +- Fixed a bug where the specified path in the config file is not respected during clean up + +## 2.1.0 +- Added a command to clean up old backups + +## 2.0.6 +- Added an option to only backup the db + +## 2.0.4 +- Fixed a [bug](https://github.com/freekmurze/laravel-backup/issues/10) that caused dot files not being included in the backup + +## 2.0.3 +- Moved orchestra/testbench to dev-dependencies + +## 2.0.2 +- Fixed a [security issue](https://github.com/freekmurze/laravel-backup/issues/6) where, on shared hosting environments, +the username and password show up in the processlist + +## 2.0.1 +- Fixed a bug that caused excluded files to still end up in the backup +- Added an exception when the database dump returns an empty string + +## 2.0.0 +- Added support to backup directories and individual files +- Configuration file changed +- Refactored all classes + +## 1.2.0 +- Added support to backup to multiple filesystems at once + +## 1.1.0 +- Added support for L5's filesystem service + +## 1.0.0 +- Initial release diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/CONTRIBUTING.md b/plugins/panakour/backup/vendor/spatie/laravel-backup/CONTRIBUTING.md new file mode 100644 index 0000000..a4834d6 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/spatie/laravel-backup). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + + +## Running Tests + +``` bash +$ composer test +``` + + +**Happy coding**! diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/LICENSE.md b/plugins/panakour/backup/vendor/spatie/laravel-backup/LICENSE.md new file mode 100644 index 0000000..59e5ec5 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Spatie bvba + +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/plugins/panakour/backup/vendor/spatie/laravel-backup/README.md b/plugins/panakour/backup/vendor/spatie/laravel-backup/README.md new file mode 100644 index 0000000..b9e49a3 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/README.md @@ -0,0 +1,77 @@ +# A modern backup solution for Laravel apps + +[![Latest Stable Version](https://poser.pugx.org/spatie/laravel-backup/v/stable?format=flat-square)](https://packagist.org/packages/spatie/laravel-backup) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/spatie/laravel-backup/master.svg?style=flat-square)](https://travis-ci.org/spatie/laravel-backup) +[![Quality Score](https://img.shields.io/scrutinizer/g/spatie/laravel-backup.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/laravel-backup) +[![StyleCI](https://styleci.io/repos/30915528/shield)](https://styleci.io/repos/30915528) +[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-backup.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-backup) + +This Laravel package [creates a backup of your application](https://docs.spatie.be/laravel-backup/v5/taking-backups/overview). The backup is a zip file that contains all files in the directories you specify along with a dump of your database. The backup can be stored on [any of the filesystems you have configured in Laravel 5](http://laravel.com/docs/filesystem). + +Feeling paranoid about backups? No problem! You can backup your application to multiple filesystems at once. + +Once installed taking a backup of your files and databases is very easy. Just issue this artisan command: + +``` bash +php artisan backup:run +``` + +But we didn't stop there. The package also provides [a backup monitor to check the health of your backups](https://docs.spatie.be/laravel-backup/v5/monitoring-the-health-of-all-backups/overview). You can be [notified via several channels](https://docs.spatie.be/laravel-backup/v5/sending-notifications/overview) when a problem with one of your backups is found. +To avoid using excessive disk space, the package can also [clean up old backups](https://docs.spatie.be/laravel-backup/v5/cleaning-up-old-backups/overview). + +Spatie is a web design agency in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). + +## Installation and usage + +This package requires PHP 7 and Laravel 5.5 or higher. You'll find installation instructions and full documentation on https://docs.spatie.be/laravel-backup/v5. + +## Using an older version of PHP / Laravel? + +If you're not on PHP 7 or Laravel 5.5 just use version 3 or version 4 of this package. + +Read the extensive [documentation on version 3](https://docs.spatie.be/laravel-backup/v3) and [on version 4](https://docs.spatie.be/laravel-backup/v4). We won't introduce new features to v3 and v4 anymore but we will still fix bugs. + +## Testing + +Run the tests with: + +``` bash +composer test +``` + +### Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Security + +If you discover any security-related issues, please email freek@spatie.be instead of using the issue tracker. + +## Postcardware + +You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. + +Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. + +We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). + +## Credits + +- [Freek Van der Herten](https://github.com/freekmurze) +- [All Contributors](../../contributors) + +## Support us + +Spatie is a web design agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). + +Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). +All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/UPGRADING.md b/plugins/panakour/backup/vendor/spatie/laravel-backup/UPGRADING.md new file mode 100644 index 0000000..ed1d63d --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/UPGRADING.md @@ -0,0 +1,3 @@ +# From v4 to v5 + +The config file has been renamed. Change the name of the config file from `laravel-backup.php` to `backup.php` \ No newline at end of file diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/composer.json b/plugins/panakour/backup/vendor/spatie/laravel-backup/composer.json new file mode 100644 index 0000000..3757bae --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/composer.json @@ -0,0 +1,67 @@ +{ + "name": "spatie/laravel-backup", + "description": "A Laravel 5 package to backup your application", + "keywords": [ + "spatie", + "backup", + "database", + "laravel-backup" + ], + "homepage": "https://github.com/spatie/laravel-backup", + "license": "MIT", + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "require": { + "php": "^7.1", + "illuminate/console": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/contracts": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/events": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/filesystem": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/support": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "illuminate/notifications": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", + "league/flysystem": "^1.0.27", + "spatie/db-dumper": "^2.11.1", + "spatie/temporary-directory": "^1.1", + "symfony/finder": "^3.3|^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.3", + "orchestra/testbench" : "~3.5.0|~3.6.0|~3.7.0|~3.8.0", + "mockery/mockery": "^1.0" + }, + "autoload": { + "psr-4": { + "Spatie\\Backup\\": "src" + }, + "files": [ + "src/Helpers/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Spatie\\Backup\\Test\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit" + }, + "suggest": { + "guzzlehttp/guzzle": "Allows notifications to be sent via Slack" + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "Spatie\\Backup\\BackupServiceProvider" + ] + } + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/config/backup.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/config/backup.php new file mode 100644 index 0000000..a8b14d9 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/config/backup.php @@ -0,0 +1,210 @@ + [ + + /* + * The name of this application. You can use this name to monitor + * the backups. + */ + 'name' => config('app.name'), + + 'source' => [ + + 'files' => [ + + /* + * The list of directories and files that will be included in the backup. + */ + 'include' => [ + base_path(), + ], + + /* + * These directories and files will be excluded from the backup. + * + * Directories used by the backup process will automatically be excluded. + */ + 'exclude' => [ + base_path('vendor'), + base_path('node_modules'), + ], + + /* + * Determines if symlinks should be followed. + */ + 'followLinks' => false, + ], + + /* + * The names of the connections to the databases that should be backed up + * MySQL, PostgreSQL, SQLite and Mongo databases are supported. + * + * The content of the database dump may be customized for each connection + * by adding a 'dump' key to the connection settings in config/database.php. + * E.g. + * 'mysql' => [ + * ... + * 'dump' => [ + * 'excludeTables' => [ + * 'table_to_exclude_from_backup', + * 'another_table_to_exclude' + * ] + * ] + * ], + * + * For a complete list of available customization options, see https://github.com/spatie/db-dumper + */ + 'databases' => [ + 'mysql', + ], + ], + + /* + * The database dump can be compressed to decrease diskspace usage. + * + * Out of the box Laravel-backup supplies + * Spatie\DbDumper\Compressors\GzipCompressor::class. + * + * You can also create custom compressor. More info on that here: + * https://github.com/spatie/db-dumper#using-compression + * + * If you do not want any compressor at all, set it to null. + */ + 'database_dump_compressor' => null, + + 'destination' => [ + + /* + * The filename prefix used for the backup zip file. + */ + 'filename_prefix' => '', + + /* + * The disk names on which the backups will be stored. + */ + 'disks' => [ + 'local', + ], + ], + + /* + * The directory where the temporary files will be stored. + */ + 'temporary_directory' => storage_path('app/backup-temp'), + ], + + /* + * You can get notified when specific events occur. Out of the box you can use 'mail' and 'slack'. + * For Slack you need to install guzzlehttp/guzzle. + * + * You can also use your own notification classes, just make sure the class is named after one of + * the `Spatie\Backup\Events` classes. + */ + 'notifications' => [ + + 'notifications' => [ + \Spatie\Backup\Notifications\Notifications\BackupHasFailed::class => ['mail'], + \Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFound::class => ['mail'], + \Spatie\Backup\Notifications\Notifications\CleanupHasFailed::class => ['mail'], + \Spatie\Backup\Notifications\Notifications\BackupWasSuccessful::class => ['mail'], + \Spatie\Backup\Notifications\Notifications\HealthyBackupWasFound::class => ['mail'], + \Spatie\Backup\Notifications\Notifications\CleanupWasSuccessful::class => ['mail'], + ], + + /* + * Here you can specify the notifiable to which the notifications should be sent. The default + * notifiable will use the variables specified in this config file. + */ + 'notifiable' => \Spatie\Backup\Notifications\Notifiable::class, + + 'mail' => [ + 'to' => 'your@example.com', + ], + + 'slack' => [ + 'webhook_url' => '', + + /* + * If this is set to null the default channel of the webhook will be used. + */ + 'channel' => null, + + 'username' => null, + + 'icon' => null, + + ], + ], + + /* + * Here you can specify which backups should be monitored. + * If a backup does not meet the specified requirements the + * UnHealthyBackupWasFound event will be fired. + */ + 'monitorBackups' => [ + [ + 'name' => config('app.name'), + 'disks' => ['local'], + 'newestBackupsShouldNotBeOlderThanDays' => 1, + 'storageUsedMayNotBeHigherThanMegabytes' => 5000, + ], + + /* + [ + 'name' => 'name of the second app', + 'disks' => ['local', 's3'], + 'newestBackupsShouldNotBeOlderThanDays' => 1, + 'storageUsedMayNotBeHigherThanMegabytes' => 5000, + ], + */ + ], + + 'cleanup' => [ + /* + * The strategy that will be used to cleanup old backups. The default strategy + * will keep all backups for a certain amount of days. After that period only + * a daily backup will be kept. After that period only weekly backups will + * be kept and so on. + * + * No matter how you configure it the default strategy will never + * delete the newest backup. + */ + 'strategy' => \Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy::class, + + 'defaultStrategy' => [ + + /* + * The number of days for which backups must be kept. + */ + 'keepAllBackupsForDays' => 7, + + /* + * The number of days for which daily backups must be kept. + */ + 'keepDailyBackupsForDays' => 16, + + /* + * The number of weeks for which one weekly backup must be kept. + */ + 'keepWeeklyBackupsForWeeks' => 8, + + /* + * The number of months for which one monthly backup must be kept. + */ + 'keepMonthlyBackupsForMonths' => 4, + + /* + * The number of years for which one yearly backup must be kept. + */ + 'keepYearlyBackupsForYears' => 2, + + /* + * After cleaning up the backups remove the oldest backup until + * this amount of megabytes has been reached. + */ + 'deleteOldestBackupsWhenUsingMoreMegabytesThan' => 5000, + ], + ], +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ar/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ar/notifications.php new file mode 100644 index 0000000..f84de9c --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ar/notifications.php @@ -0,0 +1,35 @@ + 'رسالة استثناء: :message', + 'exception_trace' => 'تتبع الإستثناء: :trace', + 'exception_message_title' => 'رسالة استثناء', + 'exception_trace_title' => 'تتبع الإستثناء', + + 'backup_failed_subject' => 'أخفق النسخ الاحتياطي لل :application_name', + 'backup_failed_body' => 'مهم: حدث خطأ أثناء النسخ الاحتياطي :application_name', + + 'backup_successful_subject' => 'نسخ احتياطي جديد ناجح ل :application_name', + 'backup_successful_subject_title' => 'نجاح النسخ الاحتياطي الجديد!', + 'backup_successful_body' => 'أخبار عظيمة، نسخة احتياطية جديدة ل :application_name تم إنشاؤها بنجاح على القرص المسمى :disk_name.', + + 'cleanup_failed_subject' => 'فشل تنظيف النسخ الاحتياطي للتطبيق :application_name .', + 'cleanup_failed_body' => 'حدث خطأ أثناء تنظيف النسخ الاحتياطية ل :application_name', + + 'cleanup_successful_subject' => 'تنظيف النسخ الاحتياطية ل :application_name تمت بنجاح', + 'cleanup_successful_subject_title' => 'تنظيف النسخ الاحتياطية تم بنجاح!', + 'cleanup_successful_body' => 'تنظيف النسخ الاحتياطية ل :application_name على القرص المسمى :disk_name تم بنجاح.', + + 'healthy_backup_found_subject' => 'النسخ الاحتياطية ل :application_name على القرص :disk_name صحية', + 'healthy_backup_found_subject_title' => 'النسخ الاحتياطية ل :application_name صحية', + 'healthy_backup_found_body' => 'تعتبر النسخ الاحتياطية ل :application_name صحية. عمل جيد!', + + 'unhealthy_backup_found_subject' => 'مهم: النسخ الاحتياطية ل :application_name غير صحية', + 'unhealthy_backup_found_subject_title' => 'مهم: النسخ الاحتياطية ل :application_name غير صحية. :problem', + 'unhealthy_backup_found_body' => 'النسخ الاحتياطية ل :application_name على القرص :disk_name غير صحية.', + 'unhealthy_backup_found_not_reachable' => 'لا يمكن الوصول إلى وجهة النسخ الاحتياطي. :error', + 'unhealthy_backup_found_empty' => 'لا توجد نسخ احتياطية لهذا التطبيق على الإطلاق.', + 'unhealthy_backup_found_old' => 'تم إنشاء أحدث النسخ الاحتياطية في :date وتعتبر قديمة جدا.', + 'unhealthy_backup_found_unknown' => 'عذرا، لا يمكن تحديد سبب دقيق.', + 'unhealthy_backup_found_full' => 'النسخ الاحتياطية تستخدم الكثير من التخزين. الاستخدام الحالي هو :disk_usage وهو أعلى من الحد المسموح به من :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/da/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/da/notifications.php new file mode 100644 index 0000000..e7b95fc --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/da/notifications.php @@ -0,0 +1,35 @@ + 'Fejlbesked: :message', + 'exception_trace' => 'Fejl trace: :trace', + 'exception_message_title' => 'Fejlbesked', + 'exception_trace_title' => 'Fejl trace', + + 'backup_failed_subject' => 'Backup af :application_name fejlede', + 'backup_failed_body' => 'Vigtigt: Der skete en fejl under backup af :application_name', + + 'backup_successful_subject' => 'Ny backup af :application_name oprettet', + 'backup_successful_subject_title' => 'Ny backup!', + 'backup_successful_body' => 'Gode nyheder - der blev oprettet en ny backup af :application_name på disken :disk_name.', + + 'cleanup_failed_subject' => 'Oprydning af backups for :application_name fejlede.', + 'cleanup_failed_body' => 'Der skete en fejl under oprydning af backups for :application_name', + + 'cleanup_successful_subject' => 'Oprydning af backups for :application_name gennemført', + 'cleanup_successful_subject_title' => 'Backup oprydning gennemført!', + 'cleanup_successful_body' => 'Oprydningen af backups for :application_name på disken :disk_name er gennemført.', + + 'healthy_backup_found_subject' => 'Alle backups for :application_name på disken :disk_name er OK', + 'healthy_backup_found_subject_title' => 'Alle backups for :application_name er OK', + 'healthy_backup_found_body' => 'Alle backups for :application_name er ok. Godt gået!', + + 'unhealthy_backup_found_subject' => 'Vigtigt: Backups for :application_name fejlbehæftede', + 'unhealthy_backup_found_subject_title' => 'Vigtigt: Backups for :application_name er fejlbehæftede. :problem', + 'unhealthy_backup_found_body' => 'Backups for :application_name på disken :disk_name er fejlbehæftede.', + 'unhealthy_backup_found_not_reachable' => 'Backup destinationen kunne ikke findes. :error', + 'unhealthy_backup_found_empty' => 'Denne applikation har ingen backups overhovedet.', + 'unhealthy_backup_found_old' => 'Den seneste backup fra :date er for gammel.', + 'unhealthy_backup_found_unknown' => 'Beklager, en præcis årsag kunne ikke findes.', + 'unhealthy_backup_found_full' => 'Backups bruger for meget plads. Nuværende disk forbrug er :disk_usage, hvilket er mere end den tilladte grænse på :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/de/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/de/notifications.php new file mode 100644 index 0000000..2d87d8f --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/de/notifications.php @@ -0,0 +1,35 @@ + 'Fehlermeldung: :message', + 'exception_trace' => 'Fehlerverfolgung: :trace', + 'exception_message_title' => 'Fehlermeldung', + 'exception_trace_title' => 'Fehlerverfolgung', + + 'backup_failed_subject' => 'Backup von :application_name konnte nicht erstellt werden', + 'backup_failed_body' => 'Wichtig: Beim Backup von :application_name ist ein Fehler aufgetreten', + + 'backup_successful_subject' => 'Erfolgreiches neues Backup von :application_name', + 'backup_successful_subject_title' => 'Erfolgreiches neues Backup!', + 'backup_successful_body' => 'Gute Nachrichten, ein neues Backup von :application_name wurde erfolgreich erstellt und in :disk_name gepeichert.', + + 'cleanup_failed_subject' => 'Aufräumen der Backups von :application_name schlug fehl.', + 'cleanup_failed_body' => 'Beim aufräumen der Backups von :application_name ist ein Fehler aufgetreten', + + 'cleanup_successful_subject' => 'Aufräumen der Backups von :application_name backups erfolgreich', + 'cleanup_successful_subject_title' => 'Aufräumen der Backups erfolgreich!', + 'cleanup_successful_body' => 'Aufräumen der Backups von :application_name in :disk_name war erfolgreich.', + + 'healthy_backup_found_subject' => 'Die Backups von :application_name in :disk_name sind gesund', + 'healthy_backup_found_subject_title' => 'Die Backups von :application_name sind Gesund', + 'healthy_backup_found_body' => 'Die Backups von :application_name wurden als gesund eingestuft. Gute Arbeit!', + + 'unhealthy_backup_found_subject' => 'Wichtig: Die Backups für :application_name sind nicht gesund', + 'unhealthy_backup_found_subject_title' => 'Wichtig: Die Backups für :application_name sind ungesund. :problem', + 'unhealthy_backup_found_body' => 'Die Backups für :application_name in :disk_name sind ungesund.', + 'unhealthy_backup_found_not_reachable' => 'Das Backup Ziel konnte nicht erreicht werden. :error', + 'unhealthy_backup_found_empty' => 'Es gibt für die Anwendung noch gar keine Backups.', + 'unhealthy_backup_found_old' => 'Das letzte Backup am :date ist zu lange her.', + 'unhealthy_backup_found_unknown' => 'Sorry, ein genauer Grund konnte nicht gefunden werden.', + 'unhealthy_backup_found_full' => 'Die Backups verbrauchen zu viel Platz. Aktuell wird :disk_usage belegt, dass ist höher als das erlaubte Limit von :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/en/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/en/notifications.php new file mode 100644 index 0000000..369ef94 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/en/notifications.php @@ -0,0 +1,35 @@ + 'Exception message: :message', + 'exception_trace' => 'Exception trace: :trace', + 'exception_message_title' => 'Exception message', + 'exception_trace_title' => 'Exception trace', + + 'backup_failed_subject' => 'Failed back up of :application_name', + 'backup_failed_body' => 'Important: An error occurred while backing up :application_name', + + 'backup_successful_subject' => 'Successful new backup of :application_name', + 'backup_successful_subject_title' => 'Successful new backup!', + 'backup_successful_body' => 'Great news, a new backup of :application_name was successfully created on the disk named :disk_name.', + + 'cleanup_failed_subject' => 'Cleaning up the backups of :application_name failed.', + 'cleanup_failed_body' => 'An error occurred while cleaning up the backups of :application_name', + + 'cleanup_successful_subject' => 'Clean up of :application_name backups successful', + 'cleanup_successful_subject_title' => 'Clean up of backups successful!', + 'cleanup_successful_body' => 'The clean up of the :application_name backups on the disk named :disk_name was successful.', + + 'healthy_backup_found_subject' => 'The backups for :application_name on disk :disk_name are healthy', + 'healthy_backup_found_subject_title' => 'The backups for :application_name are healthy', + 'healthy_backup_found_body' => 'The backups for :application_name are considered healthy. Good job!', + + 'unhealthy_backup_found_subject' => 'Important: The backups for :application_name are unhealthy', + 'unhealthy_backup_found_subject_title' => 'Important: The backups for :application_name are unhealthy. :problem', + 'unhealthy_backup_found_body' => 'The backups for :application_name on disk :disk_name are unhealthy.', + 'unhealthy_backup_found_not_reachable' => 'The backup destination cannot be reached. :error', + 'unhealthy_backup_found_empty' => 'There are no backups of this application at all.', + 'unhealthy_backup_found_old' => 'The latest backup made on :date is considered too old.', + 'unhealthy_backup_found_unknown' => 'Sorry, an exact reason cannot be determined.', + 'unhealthy_backup_found_full' => 'The backups are using too much storage. Current usage is :disk_usage which is higher than the allowed limit of :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/es/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/es/notifications.php new file mode 100644 index 0000000..4f4900f --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/es/notifications.php @@ -0,0 +1,35 @@ + 'Mensaje de la excepción: :message', + 'exception_trace' => 'Traza de la excepción: :trace', + 'exception_message_title' => 'Mensaje de la excepción', + 'exception_trace_title' => 'Traza de la excepción', + + 'backup_failed_subject' => 'Copia de seguridad de :application_name fallida', + 'backup_failed_body' => 'Importante: Ocurrió un error al realizar la copia de seguridad de :application_name', + + 'backup_successful_subject' => 'Se completó con éxito la copia de seguridad de :application_name', + 'backup_successful_subject_title' => '¡Nueva copia de seguridad creada con éxito!', + 'backup_successful_body' => 'Buenas noticias, una nueva copia de seguridad de :application_name fue creada con éxito en el disco llamado :disk_name.', + + 'cleanup_failed_subject' => 'La limpieza de copias de seguridad de :application_name falló.', + 'cleanup_failed_body' => 'Ocurrió un error mientras se realizaba la limpieza de copias de seguridad de :application_name', + + 'cleanup_successful_subject' => 'La limpieza de copias de seguridad de :application_name se completó con éxito', + 'cleanup_successful_subject_title' => '!Limpieza de copias de seguridad completada con éxito!', + 'cleanup_successful_body' => 'La limpieza de copias de seguridad de :application_name en el disco llamado :disk_name se completo con éxito.', + + 'healthy_backup_found_subject' => 'Las copias de seguridad de :application_name en el disco :disk_name están en buen estado', + 'healthy_backup_found_subject_title' => 'Las copias de seguridad de :application_name están en buen estado', + 'healthy_backup_found_body' => 'Las copias de seguridad de :application_name se consideran en buen estado. ¡Buen trabajo!', + + 'unhealthy_backup_found_subject' => 'Importante: Las copias de seguridad de :application_name están en mal estado', + 'unhealthy_backup_found_subject_title' => 'Importante: Las copias de seguridad de :application_name están en mal estado. :problem', + 'unhealthy_backup_found_body' => 'Las copias de seguridad de :application_name en el disco :disk_name están en mal estado.', + 'unhealthy_backup_found_not_reachable' => 'No se puede acceder al destino de la copia de seguridad. :error', + 'unhealthy_backup_found_empty' => 'No existe ninguna copia de seguridad de esta aplicación.', + 'unhealthy_backup_found_old' => 'La última copia de seguriad hecha en :date es demasiado antigua.', + 'unhealthy_backup_found_unknown' => 'Lo siento, no es posible determinar la razón exacta.', + 'unhealthy_backup_found_full' => 'Las copias de seguridad están ocupando demasiado espacio. El espacio utilizado actualmente es :disk_usage el cual es mayor que el límite permitido de :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/fa/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/fa/notifications.php new file mode 100644 index 0000000..33cbe33 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/fa/notifications.php @@ -0,0 +1,35 @@ + 'پیغام خطا: :message', + 'exception_trace' => 'جزییات خطا: :trace', + 'exception_message_title' => 'پیغام خطا', + 'exception_trace_title' => 'جزییات خطا', + + 'backup_failed_subject' => 'پشتیبان‌گیری :application_name با خطا مواجه شد.', + 'backup_failed_body' => 'پیغام مهم: هنگام پشتیبان‌گیری از :application_name خطایی رخ داده است. ', + + 'backup_successful_subject' => 'نسخه پشتیبان جدید :application_name با موفقیت ساخته شد.', + 'backup_successful_subject_title' => 'پشتیبان‌گیری موفق!', + 'backup_successful_body' => 'خبر خوب, به تازگی نسخه پشتیبان :application_name بر روی دیسک :disk_name با موفقیت ساخته شد. ', + + 'cleanup_failed_subject' => 'پاک‌‌سازی نسخه پشتیبان :application_name انجام نشد.', + 'cleanup_failed_body' => 'هنگام پاک‌سازی نسخه پشتیبان :application_name خطایی رخ داده است.', + + 'cleanup_successful_subject' => 'پاک‌سازی نسخه پشتیبان :application_name با موفقیت انجام شد.', + 'cleanup_successful_subject_title' => 'پاک‌سازی نسخه پشتیبان!', + 'cleanup_successful_body' => 'پاک‌سازی نسخه پشتیبان :application_name بر روی دیسک :disk_name با موفقیت انجام شد.', + + 'healthy_backup_found_subject' => 'نسخه پشتیبان :application_name بر روی دیسک :disk_name سالم بود.', + 'healthy_backup_found_subject_title' => 'نسخه پشتیبان :application_name سالم بود.', + 'healthy_backup_found_body' => 'نسخه پشتیبان :application_name به نظر سالم میاد. دمت گرم!', + + 'unhealthy_backup_found_subject' => 'خبر مهم: نسخه پشتیبان :application_name سالم نبود.', + 'unhealthy_backup_found_subject_title' => 'خبر مهم: نسخه پشتیبان :application_name سالم نبود. :problem', + 'unhealthy_backup_found_body' => 'نسخه پشتیبان :application_name بر روی دیسک :disk_name سالم نبود.', + 'unhealthy_backup_found_not_reachable' => 'مقصد پشتیبان‌گیری در دسترس نبود. :error', + 'unhealthy_backup_found_empty' => 'برای این برنامه هیچ نسخه پشتیبانی وجود ندارد.', + 'unhealthy_backup_found_old' => 'آخرین نسخه پشتیبان برای تاریخ :date است. که به نظر خیلی قدیمی میاد. ', + 'unhealthy_backup_found_unknown' => 'متاسفانه دلیل دقیق مشخص نشده است.', + 'unhealthy_backup_found_full' => 'نسخه‌های پشتیبانی که تهیه کرده اید حجم زیادی اشغال کرده اند. میزان دیسک استفاده شده :disk_usage است که از میزان مجاز :disk_limit فراتر رفته است. ', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/fr/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/fr/notifications.php new file mode 100644 index 0000000..57a98c2 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/fr/notifications.php @@ -0,0 +1,35 @@ + 'Message de l\'exception : :message', + 'exception_trace' => 'Trace de l\'exception : :trace', + 'exception_message_title' => 'Message de l\'exception', + 'exception_trace_title' => 'Trace de l\'exception', + + 'backup_failed_subject' => 'Échec de la sauvegarde de :application_name', + 'backup_failed_body' => 'Important : Une erreur est survenue lors de la sauvegarde de :application_name', + + 'backup_successful_subject' => 'Succès de la sauvegarde de :application_name', + 'backup_successful_subject_title' => 'Sauvegarde créée avec succès !', + 'backup_successful_body' => 'Bonne nouvelle, une nouvelle sauvegarde de :application_name a été créée avec succès sur le disque nommé :disk_name.', + + 'cleanup_failed_subject' => 'Le nettoyage des sauvegardes de :application_name a echoué.', + 'cleanup_failed_body' => 'Une erreur est survenue lors du nettoyage des sauvegardes de :application_name', + + 'cleanup_successful_subject' => 'Succès du nettoyage des sauvegardes de :application_name', + 'cleanup_successful_subject_title' => 'Sauvegardes nettoyées avec succès !', + 'cleanup_successful_body' => 'Le nettoyage des sauvegardes de :application_name sur le disque nommé :disk_name a été effectué avec succès.', + + 'healthy_backup_found_subject' => 'Les sauvegardes pour :application_name sur le disque :disk_name sont saines', + 'healthy_backup_found_subject_title' => 'Les sauvegardes pour :application_name sont saines', + 'healthy_backup_found_body' => 'Les sauvegardes pour :application_name sont considérées saines. Bon travail !', + + 'unhealthy_backup_found_subject' => 'Important : Les sauvegardes pour :application_name sont corrompues', + 'unhealthy_backup_found_subject_title' => 'Important : Les sauvegardes pour :application_name sont corrompues. :problem', + 'unhealthy_backup_found_body' => 'Les sauvegardes pour :application_name sur le disque :disk_name sont corrompues.', + 'unhealthy_backup_found_not_reachable' => 'La destination de la sauvegarde n\'est pas accessible. :error', + 'unhealthy_backup_found_empty' => 'Il n\'y a aucune sauvegarde pour cette application.', + 'unhealthy_backup_found_old' => 'La dernière sauvegarde du :date est considérée trop vieille.', + 'unhealthy_backup_found_unknown' => 'Désolé, une raison exacte ne peut être déterminée.', + 'unhealthy_backup_found_full' => 'Les sauvegardes utilisent trop d\'espace disque. L\'utilisation actuelle est de :disk_usage alors que la limite autorisée est de :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/hi/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/hi/notifications.php new file mode 100644 index 0000000..74a188d --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/hi/notifications.php @@ -0,0 +1,35 @@ + 'गलती संदेश: :message', + 'exception_trace' => 'गलती निशान: :trace', + 'exception_message_title' => 'गलती संदेश', + 'exception_trace_title' => 'गलती निशान', + + 'backup_failed_subject' => ':application_name का बैकअप असफल रहा', + 'backup_failed_body' => 'जरूरी सुचना: :application_name का बैकअप लेते समय असफल रहे', + + 'backup_successful_subject' => ':application_name का बैकअप सफल रहा', + 'backup_successful_subject_title' => 'बैकअप सफल रहा!', + 'backup_successful_body' => 'खुशखबरी, :application_name का बैकअप :disk_name पर संग्रहित करने मे सफल रहे.', + + 'cleanup_failed_subject' => ':application_name के बैकअप की सफाई असफल रही.', + 'cleanup_failed_body' => ':application_name के बैकअप की सफाई करते समय कुछ बाधा आयी है.', + + 'cleanup_successful_subject' => ':application_name के बैकअप की सफाई सफल रही', + 'cleanup_successful_subject_title' => 'बैकअप की सफाई सफल रही!', + 'cleanup_successful_body' => ':application_name का बैकअप जो :disk_name नाम की डिस्क पर संग्रहित है, उसकी सफाई सफल रही.', + + 'healthy_backup_found_subject' => ':disk_name नाम की डिस्क पर संग्रहित :application_name के बैकअप स्वस्थ है', + 'healthy_backup_found_subject_title' => ':application_name के सभी बैकअप स्वस्थ है', + 'healthy_backup_found_body' => 'बहुत बढ़िया! :application_name के सभी बैकअप स्वस्थ है.', + + 'unhealthy_backup_found_subject' => 'जरूरी सुचना : :application_name के बैकअप अस्वस्थ है', + 'unhealthy_backup_found_subject_title' => 'जरूरी सुचना : :application_name के बैकअप :problem के बजेसे अस्वस्थ है', + 'unhealthy_backup_found_body' => ':disk_name नाम की डिस्क पर संग्रहित :application_name के बैकअप अस्वस्थ है', + 'unhealthy_backup_found_not_reachable' => ':error के बजेसे बैकअप की मंजिल तक पोहोच नहीं सकते.', + 'unhealthy_backup_found_empty' => 'इस एप्लीकेशन का कोई भी बैकअप नहीं है.', + 'unhealthy_backup_found_old' => 'हालहीमें :date को लिया हुआ बैकअप बहुत पुराना है.', + 'unhealthy_backup_found_unknown' => 'माफ़ कीजिये, सही कारण निर्धारित नहीं कर सकते.', + 'unhealthy_backup_found_full' => 'सभी बैकअप बहुत ज्यादा जगह का उपयोग कर रहे है. फ़िलहाल सभी बैकअप :disk_usage जगह का उपयोग कर रहे है, जो की :disk_limit अनुमति सीमा से अधिक का है.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/id/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/id/notifications.php new file mode 100644 index 0000000..971322a --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/id/notifications.php @@ -0,0 +1,35 @@ + 'Pesan pengecualian: :message', + 'exception_trace' => 'Jejak pengecualian: :trace', + 'exception_message_title' => 'Pesan pengecualian', + 'exception_trace_title' => 'Jejak pengecualian', + + 'backup_failed_subject' => 'Gagal backup :application_name', + 'backup_failed_body' => 'Penting: Sebuah error terjadi ketika membackup :application_name', + + 'backup_successful_subject' => 'Backup baru sukses dari :application_name', + 'backup_successful_subject_title' => 'Backup baru sukses!', + 'backup_successful_body' => 'Kabar baik, sebuah backup baru dari :application_name sukses dibuat pada disk bernama :disk_name.', + + 'cleanup_failed_subject' => 'Membersihkan backup dari :application_name yang gagal.', + 'cleanup_failed_body' => 'Sebuah error teradi ketika membersihkan backup dari :application_name', + + 'cleanup_successful_subject' => 'Sukses membersihkan backup :application_name', + 'cleanup_successful_subject_title' => 'Sukses membersihkan backup!', + 'cleanup_successful_body' => 'Pembersihan backup :application_name pada disk bernama :disk_name telah sukses.', + + 'healthy_backup_found_subject' => 'Backup untuk :application_name pada disk :disk_name sehat', + 'healthy_backup_found_subject_title' => 'Backup untuk :application_name sehat', + 'healthy_backup_found_body' => 'Backup untuk :application_name dipertimbangkan sehat. Kerja bagus!', + + 'unhealthy_backup_found_subject' => 'Penting: Backup untuk :application_name tidak sehat', + 'unhealthy_backup_found_subject_title' => 'Penting: Backup untuk :application_name tidak sehat. :problem', + 'unhealthy_backup_found_body' => 'Backup untuk :application_name pada disk :disk_name tidak sehat.', + 'unhealthy_backup_found_not_reachable' => 'Tujuan backup tidak dapat terjangkau. :error', + 'unhealthy_backup_found_empty' => 'Tidak ada backup pada aplikasi ini sama sekali.', + 'unhealthy_backup_found_old' => 'Backup terakhir dibuat pada :date dimana dipertimbahkan sudah sangat lama.', + 'unhealthy_backup_found_unknown' => 'Maaf, sebuah alasan persisnya tidak dapat ditentukan.', + 'unhealthy_backup_found_full' => 'Backup menggunakan terlalu banyak kapasitas penyimpanan. Penggunaan terkini adalah :disk_usage dimana lebih besar dari batas yang diperbolehkan yaitu :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/it/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/it/notifications.php new file mode 100644 index 0000000..43ad38e --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/it/notifications.php @@ -0,0 +1,35 @@ + 'Messaggio dell\'eccezione: :message', + 'exception_trace' => 'Traccia dell\'eccezione: :trace', + 'exception_message_title' => 'Messaggio dell\'eccezione', + 'exception_trace_title' => 'Traccia dell\'eccezione', + + 'backup_failed_subject' => 'Fallito il backup di :application_name', + 'backup_failed_body' => 'Importante: Si è verificato un errore durante il backup di :application_name', + + 'backup_successful_subject' => 'Creato nuovo backup di :application_name', + 'backup_successful_subject_title' => 'Nuovo backup creato!', + 'backup_successful_body' => 'Grande notizia, un nuovo backup di :application_name è stato creato con successo sul disco :disk_name.', + + 'cleanup_failed_subject' => 'Pulizia dei backup di :application_name fallita.', + 'cleanup_failed_body' => 'Si è verificato un errore durante la pulizia dei backup di :application_name', + + 'cleanup_successful_subject' => 'Pulizia dei backup di :application_name avvenuta con successo', + 'cleanup_successful_subject_title' => 'Pulizia dei backup avvenuta con successo!', + 'cleanup_successful_body' => 'La pulizia dei backup di :application_name sul disco :disk_name è avvenuta con successo.', + + 'healthy_backup_found_subject' => 'I backup per :application_name sul disco :disk_name sono sani', + 'healthy_backup_found_subject_title' => 'I backup per :application_name sono sani', + 'healthy_backup_found_body' => 'I backup per :application_name sono considerati sani. Bel Lavoro!', + + 'unhealthy_backup_found_subject' => 'Importante: i backup per :application_name sono corrotti', + 'unhealthy_backup_found_subject_title' => 'Importante: i backup per :application_name sono corrotti. :problem', + 'unhealthy_backup_found_body' => 'I backup per :application_name sul disco :disk_name sono corrotti.', + 'unhealthy_backup_found_not_reachable' => 'Impossibile raggiungere la destinazione di backup. :error', + 'unhealthy_backup_found_empty' => 'Non esiste alcun backup di questa applicazione.', + 'unhealthy_backup_found_old' => 'L\'ultimo backup fatto il :date è considerato troppo vecchio.', + 'unhealthy_backup_found_unknown' => 'Spiacenti, non è possibile determinare una ragione esatta.', + 'unhealthy_backup_found_full' => 'I backup utilizzano troppa memoria. L\'utilizzo corrente è :disk_usage che è superiore al limite consentito di :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/pl/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/pl/notifications.php new file mode 100644 index 0000000..86f5539 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/pl/notifications.php @@ -0,0 +1,35 @@ + 'Błąd: :message', + 'exception_trace' => 'Zrzut błędu: :trace', + 'exception_message_title' => 'Błąd', + 'exception_trace_title' => 'Zrzut błędu', + + 'backup_failed_subject' => 'Tworzenie kopii zapasowej aplikacji :application_name nie powiodło się', + 'backup_failed_body' => 'Ważne: Wystąpił błąd podczas tworzenia kopii zapasowej aplikacji :application_name', + + 'backup_successful_subject' => 'Pomyślnie utworzono kopię zapasową aplikacji :application_name', + 'backup_successful_subject_title' => 'Nowa kopia zapasowa!', + 'backup_successful_body' => 'Wspaniała wiadomość, nowa kopia zapasowa aplikacji :application_name została pomyślnie utworzona na dysku o nazwie :disk_name.', + + 'cleanup_failed_subject' => 'Czyszczenie kopii zapasowych aplikacji :application_name nie powiodło się.', + 'cleanup_failed_body' => 'Wystąpił błąd podczas czyszczenia kopii zapasowej aplikacji :application_name', + + 'cleanup_successful_subject' => 'Kopie zapasowe aplikacji :application_name zostały pomyślnie wyczyszczone', + 'cleanup_successful_subject_title' => 'Kopie zapasowe zostały pomyślnie wyczyszczone!', + 'cleanup_successful_body' => 'Czyszczenie kopii zapasowych aplikacji :application_name na dysku :disk_name zakończone sukcecem.', + + 'healthy_backup_found_subject' => 'Kopie zapasowe aplikacji :application_name na dysku :disk_name są poprawne', + 'healthy_backup_found_subject_title' => 'Kopie zapasowe aplikacji :application_name są poprawne', + 'healthy_backup_found_body' => 'Kopie zapasowe aplikacji :application_name są poprawne. Dobra robota!', + + 'unhealthy_backup_found_subject' => 'Ważne: Kopie zapasowe aplikacji :application_name są niepoprawne', + 'unhealthy_backup_found_subject_title' => 'Ważne: Kopie zapasowe aplikacji :application_name są niepoprawne. :problem', + 'unhealthy_backup_found_body' => 'Kopie zapasowe aplikacji :application_name na dysku :disk_name są niepoprawne.', + 'unhealthy_backup_found_not_reachable' => 'Miejsce docelowe kopii zapasowej nie jest osiągalne. :error', + 'unhealthy_backup_found_empty' => 'W aplikacji nie ma żadnej kopii zapasowych tej aplikacji.', + 'unhealthy_backup_found_old' => 'Ostatnia kopia zapasowa wykonania dnia :date jest zbyt stara.', + 'unhealthy_backup_found_unknown' => 'Niestety, nie można ustalić dokładnego błędu.', + 'unhealthy_backup_found_full' => 'Kopie zapasowe zajmują zbyt dużo miejsca. Obecne użycie dysku :disk_usage jest większe od ustalonego limitu :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/pt-BR/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/pt-BR/notifications.php new file mode 100644 index 0000000..d22ebf4 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/pt-BR/notifications.php @@ -0,0 +1,35 @@ + 'Exception message: :message', + 'exception_trace' => 'Exception trace: :trace', + 'exception_message_title' => 'Exception message', + 'exception_trace_title' => 'Exception trace', + + 'backup_failed_subject' => 'Falha no backup da aplicação :application_name', + 'backup_failed_body' => 'Importante: Ocorreu um erro ao fazer o backup da aplicação :application_name', + + 'backup_successful_subject' => 'Backup realizado com sucesso: :application_name', + 'backup_successful_subject_title' => 'Backup Realizado com sucesso!', + 'backup_successful_body' => 'Boas notícias, um novo backup da aplicação :application_name foi criado no disco :disk_name.', + + 'cleanup_failed_subject' => 'Falha na limpeza dos backups da aplicação :application_name.', + 'cleanup_failed_body' => 'Um erro ocorreu ao fazer a limpeza dos backups da aplicação :application_name', + + 'cleanup_successful_subject' => 'Limpeza dos backups da aplicação :application_name concluída!', + 'cleanup_successful_subject_title' => 'Limpeza dos backups concluída!', + 'cleanup_successful_body' => 'A limpeza dos backups da aplicação :application_name no disco :disk_name foi concluída.', + + 'healthy_backup_found_subject' => 'Os backups da aplicação :application_name no disco :disk_name estão em dia', + 'healthy_backup_found_subject_title' => 'Os backups da aplicação :application_name estão em dia', + 'healthy_backup_found_body' => 'Os backups da aplicação :application_name estão em dia. Bom trabalho!', + + 'unhealthy_backup_found_subject' => 'Importante: Os backups da aplicação :application_name não estão em dia', + 'unhealthy_backup_found_subject_title' => 'Importante: Os backups da aplicação :application_name não estão em dia. :problem', + 'unhealthy_backup_found_body' => 'Os backups da aplicação :application_name no disco :disk_name não estão em dia.', + 'unhealthy_backup_found_not_reachable' => 'O destino dos backups não pode ser alcançado. :error', + 'unhealthy_backup_found_empty' => 'Não existem backups para essa aplicação.', + 'unhealthy_backup_found_old' => 'O último backup realizado em :date é considerado muito antigo.', + 'unhealthy_backup_found_unknown' => 'Desculpe, a exata razão não pode ser encontrada.', + 'unhealthy_backup_found_full' => 'Os backups estão usando muito espaço de armazenamento. A utilização atual é de :disk_usage, o que é maior que o limite permitido de :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ro/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ro/notifications.php new file mode 100644 index 0000000..cc0322d --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ro/notifications.php @@ -0,0 +1,35 @@ + 'Cu excepția mesajului: :message', + 'exception_trace' => 'Urmă excepţie: :trace', + 'exception_message_title' => 'Mesaj de excepție', + 'exception_trace_title' => 'Urmă excepţie', + + 'backup_failed_subject' => 'Nu s-a putut face copie de rezervă pentru :application_name', + 'backup_failed_body' => 'Important: A apărut o eroare în timpul generării copiei de rezervă pentru :application_name', + + 'backup_successful_subject' => 'Copie de rezervă efectuată cu succes pentru :application_name', + 'backup_successful_subject_title' => 'O nouă copie de rezervă a fost efectuată cu succes!', + 'backup_successful_body' => 'Vești bune, o nouă copie de rezervă pentru :application_name a fost creată cu succes pe discul cu numele :disk_name.', + + 'cleanup_failed_subject' => 'Curățarea copiilor de rezervă pentru :application_name nu a reușit.', + 'cleanup_failed_body' => 'A apărut o eroare în timpul curățirii copiilor de rezervă pentru :application_name', + + 'cleanup_successful_subject' => 'Curățarea copiilor de rezervă pentru :application_name a fost făcută cu succes', + 'cleanup_successful_subject_title' => 'Curățarea copiilor de rezervă a fost făcută cu succes!', + 'cleanup_successful_body' => 'Curățarea copiilor de rezervă pentru :application_name de pe discul cu numele :disk_name a fost făcută cu succes.', + + 'healthy_backup_found_subject' => 'Copiile de rezervă pentru :application_name de pe discul :disk_name sunt în regulă', + 'healthy_backup_found_subject_title' => 'Copiile de rezervă pentru :application_name sunt în regulă', + 'healthy_backup_found_body' => 'Copiile de rezervă pentru :application_name sunt considerate în regulă. Bună treabă!', + + 'unhealthy_backup_found_subject' => 'Important: Copiile de rezervă pentru :application_name nu sunt în regulă', + 'unhealthy_backup_found_subject_title' => 'Important: Copiile de rezervă pentru :application_name nu sunt în regulă. :problem', + 'unhealthy_backup_found_body' => 'Copiile de rezervă pentru :application_name de pe discul :disk_name nu sunt în regulă.', + 'unhealthy_backup_found_not_reachable' => 'Nu se poate ajunge la destinația copiilor de rezervă. :error', + 'unhealthy_backup_found_empty' => 'Nu există copii de rezervă ale acestei aplicații.', + 'unhealthy_backup_found_old' => 'Cea mai recentă copie de rezervă făcută la :date este considerată prea veche.', + 'unhealthy_backup_found_unknown' => 'Ne pare rău, un motiv exact nu poate fi determinat.', + 'unhealthy_backup_found_full' => 'Copiile de rezervă folosesc prea mult spațiu de stocare. Utilizarea curentă este de :disk_usage care este mai mare decât limita permisă de :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ru/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ru/notifications.php new file mode 100644 index 0000000..875633c --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/ru/notifications.php @@ -0,0 +1,35 @@ + 'Сообщение об ошибке: :message', + 'exception_trace' => 'Сведения об ошибке: :trace', + 'exception_message_title' => 'Сообщение об ошибке', + 'exception_trace_title' => 'Сведения об ошибке', + + 'backup_failed_subject' => 'Не удалось сделать резервную копию :application_name', + 'backup_failed_body' => 'Внимание: Произошла ошибка во время резервного копирования :application_name', + + 'backup_successful_subject' => 'Успешно создана новая резервная копия :application_name', + 'backup_successful_subject_title' => 'Успешно создана новая резервная копия!', + 'backup_successful_body' => 'Отличная новость, новая резервная копия :application_name успешно создана и сохранена на диск :disk_name.', + + 'cleanup_failed_subject' => 'Не удалось очистить резервные копии :application_name', + 'cleanup_failed_body' => 'Произошла ошибка при очистке резервных копий :application_name', + + 'cleanup_successful_subject' => 'Очистка от резервных копий :application_name прошла успешно', + 'cleanup_successful_subject_title' => 'Очистка резервных копий прошла удачно!', + 'cleanup_successful_body' => 'Очистка от старых резервных копий :application_name на диске :disk_name прошла удачно.', + + 'healthy_backup_found_subject' => 'Резервная копия :application_name с диска :disk_name установлена', + 'healthy_backup_found_subject_title' => 'Резервная копия :application_name установлена', + 'healthy_backup_found_body' => 'Резервная копия :application_name успешно установлена. Хорошая работа!', + + 'unhealthy_backup_found_subject' => 'Внимание: резервная копия :application_name не установилась', + 'unhealthy_backup_found_subject_title' => 'Внимание: резервная копия для :application_name не установилась. :problem', + 'unhealthy_backup_found_body' => 'Резервная копия для :application_name на диске :disk_name не установилась.', + 'unhealthy_backup_found_not_reachable' => 'Резервная копия не смогла установиться. :error', + 'unhealthy_backup_found_empty' => 'Резервные копии для этого приложения отсутствуют.', + 'unhealthy_backup_found_old' => 'Последнее резервное копирование создано :date является устаревшим.', + 'unhealthy_backup_found_unknown' => 'Извините, точная причина не может быть определена.', + 'unhealthy_backup_found_full' => 'Резервные копии используют слишком много памяти. Используется :disk_usage что выше допустимого предела: :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/tr/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/tr/notifications.php new file mode 100644 index 0000000..298b0ec --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/tr/notifications.php @@ -0,0 +1,35 @@ + 'Hata mesajı: :message', + 'exception_trace' => 'Hata izleri: :trace', + 'exception_message_title' => 'Hata mesajı', + 'exception_trace_title' => 'Hata izleri', + + 'backup_failed_subject' => 'Yedeklenemedi :application_name', + 'backup_failed_body' => 'Önemli: Yedeklenirken bir hata oluştu :application_name', + + 'backup_successful_subject' => 'Başarılı :application_name yeni yedeklemesi', + 'backup_successful_subject_title' => 'Başarılı bir yeni yedekleme!', + 'backup_successful_body' => 'Harika bir haber, :application_name âit yeni bir yedekleme :disk_name adlı diskte başarıyla oluşturuldu.', + + 'cleanup_failed_subject' => ':application_name yedeklemeleri temizlenmesi başarısız.', + 'cleanup_failed_body' => ':application_name yedeklerini temizlerken bir hata oluştu ', + + 'cleanup_successful_subject' => ':application_name yedeklemeleri temizlenmesi başarılı.', + 'cleanup_successful_subject_title' => 'Yedeklerin temizlenmesi başarılı!', + 'cleanup_successful_body' => ':application_name yedeklemeleri temizlenmesi ,:disk_name diskinden silindi', + + 'healthy_backup_found_subject' => ':application_name yedeklenmesi ,:disk_name adlı diskte sağlıklı', + 'healthy_backup_found_subject_title' => ':application_name yedeklenmesi sağlıklı', + 'healthy_backup_found_body' => ':application_name için yapılan yedeklemeler sağlıklı sayılır. Aferin!', + + 'unhealthy_backup_found_subject' => 'Önemli: :application_name için yedeklemeler sağlıksız', + 'unhealthy_backup_found_subject_title' => 'Önemli: :application_name için yedeklemeler sağlıksız. :problem', + 'unhealthy_backup_found_body' => 'Yedeklemeler: :application_name disk: :disk_name sağlıksız.', + 'unhealthy_backup_found_not_reachable' => 'Yedekleme hedefine ulaşılamıyor. :error', + 'unhealthy_backup_found_empty' => 'Bu uygulamanın yedekleri yok.', + 'unhealthy_backup_found_old' => ':date tarihinde yapılan en son yedekleme çok eski kabul ediliyor.', + 'unhealthy_backup_found_unknown' => 'Üzgünüm, kesin bir sebep belirlenemiyor.', + 'unhealthy_backup_found_full' => 'Yedeklemeler çok fazla depolama alanı kullanıyor. Şu anki kullanım: :disk_usage, izin verilen sınırdan yüksek: :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/uk/notifications.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/uk/notifications.php new file mode 100644 index 0000000..a39c90a --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/resources/lang/uk/notifications.php @@ -0,0 +1,35 @@ + 'Повідомлення про помилку: :message', + 'exception_trace' => 'Деталі помилки: :trace', + 'exception_message_title' => 'Повідомлення помилки', + 'exception_trace_title' => 'Деталі помилки', + + 'backup_failed_subject' => 'Не вдалось зробити резервну копію :application_name', + 'backup_failed_body' => 'Увага: Трапилась помилка під час резервного копіювання :application_name', + + 'backup_successful_subject' => 'Успішне резервне копіювання :application_name', + 'backup_successful_subject_title' => 'Успішно створена резервна копія!', + 'backup_successful_body' => 'Чудова новина, нова резервна копія :application_name успішно створена і збережена на диск :disk_name.', + + 'cleanup_failed_subject' => 'Не вдалось очистити резервні копії :application_name', + 'cleanup_failed_body' => 'Сталася помилка під час очищення резервних копій :application_name', + + 'cleanup_successful_subject' => 'Успішне очищення від резервних копій :application_name', + 'cleanup_successful_subject_title' => 'Очищення резервних копій пройшло вдало!', + 'cleanup_successful_body' => 'Очищенно від старих резервних копій :application_name на диску :disk_name пойшло успішно.', + + 'healthy_backup_found_subject' => 'Резервна копія :application_name з диску :disk_name установлена', + 'healthy_backup_found_subject_title' => 'Резервна копія :application_name установлена', + 'healthy_backup_found_body' => 'Резервна копія :application_name успішно установлена. Хороша робота!', + + 'unhealthy_backup_found_subject' => 'Увага: резервна копія :application_name не установилась', + 'unhealthy_backup_found_subject_title' => 'Увага: резервна копія для :application_name не установилась. :problem', + 'unhealthy_backup_found_body' => 'Резервна копія для :application_name на диску :disk_name не установилась.', + 'unhealthy_backup_found_not_reachable' => 'Резервна копія не змогла установитись. :error', + 'unhealthy_backup_found_empty' => 'Резервні копії для цього додатку відсутні.', + 'unhealthy_backup_found_old' => 'Останнє резервне копіювання створено :date є застарілим.', + 'unhealthy_backup_found_unknown' => 'Вибачте, але ми не змогли визначити точну причину.', + 'unhealthy_backup_found_full' => 'Резервні копії використовують занадто багато пам`яті. Використовується :disk_usage що вище за допустиму межу :disk_limit.', +]; diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/Backup.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/Backup.php new file mode 100644 index 0000000..3a1811f --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/Backup.php @@ -0,0 +1,61 @@ +disk = $disk; + + $this->path = $path; + } + + public function path(): string + { + return $this->path; + } + + public function exists(): bool + { + return $this->disk->exists($this->path); + } + + public function date(): Carbon + { + return Carbon::createFromTimestamp($this->disk->lastModified($this->path)); + } + + /** + * Get the size in bytes. + */ + public function size(): int + { + if (! $this->exists()) { + return 0; + } + + return $this->disk->size($this->path); + } + + public function stream() + { + return $this->disk->readStream($this->path); + } + + public function delete() + { + $this->disk->delete($this->path); + + consoleOutput()->info("Deleted backup `{$this->path}`."); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupCollection.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupCollection.php new file mode 100644 index 0000000..fe68fa2 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupCollection.php @@ -0,0 +1,49 @@ +filter(function ($path) use ($disk) { + return (new File)->isZipFile($disk, $path); + }) + ->map(function ($path) use ($disk) { + return new Backup($disk, $path); + }) + ->sortByDesc(function (Backup $backup) { + return $backup->date()->timestamp; + }) + ->values(); + } + + public function newest(): ?Backup + { + return $this->first(); + } + + public function oldest(): ?Backup + { + return $this + ->filter->exists() + ->last(); + } + + public function size(): int + { + if ($this->sizeCache !== null) { + return $this->sizeCache; + } + + return $this->sizeCache = $this->sum->size(); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupDestination.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupDestination.php new file mode 100644 index 0000000..b8a892c --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupDestination.php @@ -0,0 +1,174 @@ +disk = $disk; + + $this->diskName = $diskName; + + $this->backupName = preg_replace('/[^a-zA-Z0-9.]/', '-', $backupName); + } + + public function disk(): Filesystem + { + return $this->disk; + } + + public function diskName(): string + { + return $this->diskName; + } + + public function filesystemType(): string + { + if (is_null($this->disk)) { + return 'unknown'; + } + + $adapterClass = get_class($this->disk->getDriver()->getAdapter()); + + $filesystemType = last(explode('\\', $adapterClass)); + + return strtolower($filesystemType); + } + + public static function create(string $diskName, string $backupName): self + { + try { + $disk = app(Factory::class)->disk($diskName); + + return new static($disk, $backupName, $diskName); + } catch (Exception $exception) { + $backupDestination = new static(null, $backupName, $diskName); + + $backupDestination->connectionError = $exception; + + return $backupDestination; + } + } + + public function write(string $file) + { + if (is_null($this->disk)) { + throw InvalidBackupDestination::diskNotSet(); + } + + $destination = $this->backupName.'/'.pathinfo($file, PATHINFO_BASENAME); + + $handle = fopen($file, 'r+'); + + $this->disk->getDriver()->writeStream( + $destination, + $handle, + $this->getDiskOptions() + ); + + if (is_resource($handle)) { + fclose($handle); + } + } + + public function backupName(): string + { + return $this->backupName; + } + + public function backups(): BackupCollection + { + if ($this->backupCollectionCache) { + return $this->backupCollectionCache; + } + + $files = is_null($this->disk) ? [] : $this->disk->allFiles($this->backupName); + + return $this->backupCollectionCache = BackupCollection::createFromFiles( + $this->disk, + $files + ); + } + + public function connectionError(): Exception + { + return $this->connectionError; + } + + public function getDiskOptions(): array + { + return config("filesystems.disks.{$this->diskName()}.backup_options") ?? []; + } + + public function isReachable(): bool + { + if (is_null($this->disk)) { + return false; + } + + try { + $this->disk->allFiles($this->backupName); + + return true; + } catch (Exception $exception) { + $this->connectionError = $exception; + + return false; + } + } + + public function usedStorage(): int + { + return $this->backups()->size(); + } + + public function newestBackup(): ?Backup + { + return $this->backups()->newest(); + } + + public function oldestBackup(): ?Backup + { + return $this->backups()->oldest(); + } + + public function newestBackupIsOlderThan(Carbon $date): bool + { + $newestBackup = $this->newestBackup(); + + if (is_null($newestBackup)) { + return true; + } + + return $newestBackup->date()->gt($date); + } + + public function fresh(): self + { + $this->backupCollectionCache = null; + + return $this; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupDestinationFactory.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupDestinationFactory.php new file mode 100644 index 0000000..6060df3 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupDestination/BackupDestinationFactory.php @@ -0,0 +1,16 @@ +map(function ($filesystemName) use ($config) { + return BackupDestination::create($filesystemName, $config['name']); + }); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupServiceProvider.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupServiceProvider.php new file mode 100644 index 0000000..6cb4b65 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/BackupServiceProvider.php @@ -0,0 +1,51 @@ +publishes([ + __DIR__.'/../config/backup.php' => config_path('backup.php'), + ], 'config'); + + $this->publishes([ + __DIR__.'/../resources/lang' => resource_path('lang/vendor/backup'), + ]); + + $this->loadTranslationsFrom(__DIR__.'/../resources/lang/', 'backup'); + } + + public function register() + { + $this->mergeConfigFrom(__DIR__.'/../config/backup.php', 'backup'); + + $this->app['events']->subscribe(EventHandler::class); + + $this->app->bind('command.backup:run', BackupCommand::class); + $this->app->bind('command.backup:clean', CleanupCommand::class); + $this->app->bind('command.backup:list', ListCommand::class); + $this->app->bind('command.backup:monitor', MonitorCommand::class); + + $this->app->bind(CleanupStrategy::class, config('backup.cleanup.strategy')); + + $this->commands([ + 'command.backup:run', + 'command.backup:clean', + 'command.backup:list', + 'command.backup:monitor', + ]); + + $this->app->singleton(ConsoleOutput::class); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/BackupCommand.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/BackupCommand.php new file mode 100644 index 0000000..cfae6d1 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/BackupCommand.php @@ -0,0 +1,72 @@ +comment('Starting backup...'); + + $disableNotifications = $this->option('disable-notifications'); + + try { + $this->guardAgainstInvalidOptions(); + + $backupJob = BackupJobFactory::createFromArray(config('backup')); + + if ($this->option('only-db')) { + $backupJob->dontBackupFilesystem(); + } + if ($this->option('db-name')) { + $backupJob->onlyDbName($this->option('db-name')); + } + + if ($this->option('only-files')) { + $backupJob->dontBackupDatabases(); + } + + if ($this->option('only-to-disk')) { + $backupJob->onlyBackupTo($this->option('only-to-disk')); + } + + if ($this->option('filename')) { + $backupJob->setFilename($this->option('filename')); + } + + if ($disableNotifications) { + $backupJob->disableNotifications(); + } + + $backupJob->run(); + + consoleOutput()->comment('Backup completed!'); + } catch (Exception $exception) { + consoleOutput()->error("Backup failed because: {$exception->getMessage()}."); + + if (! $disableNotifications) { + event(new BackupHasFailed($exception)); + } + + return 1; + } + } + + protected function guardAgainstInvalidOptions() + { + if ($this->option('only-db') && $this->option('only-files')) { + throw InvalidCommand::create('Cannot use `only-db` and `only-files` together'); + } + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/BaseCommand.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/BaseCommand.php new file mode 100644 index 0000000..827fe47 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/BaseCommand.php @@ -0,0 +1,18 @@ +setOutput($this); + + return parent::run($input, $output); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/CleanupCommand.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/CleanupCommand.php new file mode 100644 index 0000000..7a7a195 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/CleanupCommand.php @@ -0,0 +1,53 @@ +strategy = $strategy; + } + + public function handle() + { + consoleOutput()->comment('Starting cleanup...'); + + $disableNotifications = $this->option('disable-notifications'); + + try { + $config = config('backup'); + + $backupDestinations = BackupDestinationFactory::createFromArray($config['backup']); + + $cleanupJob = new CleanupJob($backupDestinations, $this->strategy, $disableNotifications); + + $cleanupJob->run(); + + consoleOutput()->comment('Cleanup completed!'); + } catch (Exception $exception) { + if (! $disableNotifications) { + event(new CleanupHasFailed($exception)); + } + + return 1; + } + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/ListCommand.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/ListCommand.php new file mode 100644 index 0000000..ff0be2e --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/ListCommand.php @@ -0,0 +1,97 @@ +displayOverview($statuses); + + $this->displayConnectionErrors($statuses); + } + + protected function displayOverview(Collection $backupDestinationStatuses) + { + $headers = ['Name', 'Disk', 'Reachable', 'Healthy', '# of backups', 'Newest backup', 'Used storage']; + + $rows = $backupDestinationStatuses->map(function (BackupDestinationStatus $backupDestinationStatus) { + return $this->convertToRow($backupDestinationStatus); + }); + + $this->table($headers, $rows); + } + + public function convertToRow(BackupDestinationStatus $backupDestinationStatus): array + { + $row = [ + $backupDestinationStatus->backupName(), + $backupDestinationStatus->diskName(), + Format::emoji($backupDestinationStatus->isReachable()), + Format::emoji($backupDestinationStatus->isHealthy()), + 'amount' => $backupDestinationStatus->amountOfBackups(), + 'newest' => $backupDestinationStatus->dateOfNewestBackup() + ? Format::ageInDays($backupDestinationStatus->dateOfNewestBackup()) + : 'No backups present', + 'usedStorage' => $backupDestinationStatus->humanReadableUsedStorage(), + ]; + + if (! $backupDestinationStatus->isReachable()) { + foreach (['amount', 'newest', 'usedStorage'] as $propertyName) { + $row[$propertyName] = '/'; + } + } + + $row = $this->applyStylingToRow($row, $backupDestinationStatus); + + return $row; + } + + protected function applyStylingToRow(array $row, BackupDestinationStatus $backupDestinationStatus): array + { + if ($backupDestinationStatus->newestBackupIsTooOld() || (! $backupDestinationStatus->dateOfNewestBackup())) { + $row['newest'] = "{$row['newest']}"; + } + + if ($backupDestinationStatus->usesTooMuchStorage()) { + $row['usedStorage'] = "{$row['usedStorage']} "; + } + + return $row; + } + + protected function displayConnectionErrors(Collection $backupDestinationStatuses) + { + $unreachableBackupDestinationStatuses = $backupDestinationStatuses + ->reject(function (BackupDestinationStatus $backupDestinationStatus) { + return $backupDestinationStatus->isReachable(); + }); + + if ($unreachableBackupDestinationStatuses->isEmpty()) { + return; + } + + $this->warn(''); + $this->warn('Unreachable backup destinations'); + $this->warn('-------------------------------'); + + $unreachableBackupDestinationStatuses->each(function (BackupDestinationStatus $backupStatus) { + $this->warn("Could not reach backups for {$backupStatus->backupName()} on disk {$backupStatus->diskName()} because:"); + $this->warn($backupStatus->connectionError()->getMessage()); + $this->warn(''); + }); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/MonitorCommand.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/MonitorCommand.php new file mode 100644 index 0000000..b3f8bd9 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Commands/MonitorCommand.php @@ -0,0 +1,34 @@ +each(function (BackupDestinationStatus $backupDestinationStatus) { + if ($backupDestinationStatus->isHealthy()) { + $this->info("The backups on {$backupDestinationStatus->diskName()} are considered healthy."); + event(new HealthyBackupWasFound($backupDestinationStatus)); + + return; + } + + $this->error("The backups on {$backupDestinationStatus->diskName()} are considered unhealthy!"); + event(new UnHealthyBackupWasFound($backupDestinationStatus)); + }); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupHasFailed.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupHasFailed.php new file mode 100644 index 0000000..fd94161 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupHasFailed.php @@ -0,0 +1,22 @@ +exception = $exception; + + $this->backupDestination = $backupDestination; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupManifestWasCreated.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupManifestWasCreated.php new file mode 100644 index 0000000..19f8c64 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupManifestWasCreated.php @@ -0,0 +1,16 @@ +manifest = $manifest; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupWasSuccessful.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupWasSuccessful.php new file mode 100644 index 0000000..0920099 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupWasSuccessful.php @@ -0,0 +1,16 @@ +backupDestination = $backupDestination; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupZipWasCreated.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupZipWasCreated.php new file mode 100644 index 0000000..eac1223 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/BackupZipWasCreated.php @@ -0,0 +1,14 @@ +pathToZip = $pathToZip; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/CleanupHasFailed.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/CleanupHasFailed.php new file mode 100644 index 0000000..0bbacb2 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/CleanupHasFailed.php @@ -0,0 +1,22 @@ +exception = $exception; + + $this->backupDestination = $backupDestination; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/CleanupWasSuccessful.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/CleanupWasSuccessful.php new file mode 100644 index 0000000..a994d54 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/CleanupWasSuccessful.php @@ -0,0 +1,16 @@ +backupDestination = $backupDestination; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/HealthyBackupWasFound.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/HealthyBackupWasFound.php new file mode 100644 index 0000000..e1ea823 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/HealthyBackupWasFound.php @@ -0,0 +1,16 @@ +backupDestinationStatus = $backupDestinationStatus; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/UnhealthyBackupWasFound.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/UnhealthyBackupWasFound.php new file mode 100644 index 0000000..749d149 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Events/UnhealthyBackupWasFound.php @@ -0,0 +1,16 @@ +backupDestinationStatus = $backupDestinationStatus; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/CannotCreateDbDumper.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/CannotCreateDbDumper.php new file mode 100644 index 0000000..7d6937b --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Exceptions/CannotCreateDbDumper.php @@ -0,0 +1,13 @@ +output = $output; + } + + public function __call(string $method, array $arguments) + { + $consoleOutput = app(static::class); + + if (! $consoleOutput->output) { + return; + } + + $consoleOutput->output->$method($arguments[0]); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/File.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/File.php new file mode 100644 index 0000000..358f143 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/File.php @@ -0,0 +1,49 @@ +hasZipExtension($path)) { + return true; + } + + return $this->hasAllowedMimeType($disk, $path); + } + + protected function hasZipExtension(string $path): bool + { + return pathinfo($path, PATHINFO_EXTENSION) === 'zip'; + } + + protected function hasAllowedMimeType(?Filesystem $disk, string $path) + { + return in_array($this->mimeType($disk, $path), self::$allowedMimeTypes); + } + + protected function mimeType(?Filesystem $disk, string $path) + { + try { + if ($disk && method_exists($disk, 'mimeType')) { + return $disk->mimeType($path) ?: false; + } + } catch (Exception $exception) { + // Some drivers throw exceptions when checking mime types, we'll + // just fallback to `false`. + } + + return false; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/Format.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/Format.php new file mode 100644 index 0000000..bc9b4f0 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/Format.php @@ -0,0 +1,36 @@ + 1024; $i++) { + $sizeInBytes /= 1024; + } + + return round($sizeInBytes, 2).' '.$units[$i]; + } + + public static function emoji(bool $bool): string + { + if ($bool) { + return '✅'; + } + + return '❌'; + } + + public static function ageInDays(Carbon $date): string + { + return number_format(round($date->diffInMinutes() / (24 * 60), 2), 2).' ('.$date->diffForHumans().')'; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/functions.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/functions.php new file mode 100644 index 0000000..0a70256 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Helpers/functions.php @@ -0,0 +1,8 @@ +backupDestination()->backupName(); + } + + public function diskName(): string + { + return $this->backupDestination()->diskName(); + } + + protected function backupDestinationProperties(): Collection + { + $backupDestination = $this->backupDestination(); + + if (! $backupDestination) { + return collect(); + } + + $backupDestination->fresh(); + + $newestBackup = $backupDestination->newestBackup(); + $oldestBackup = $backupDestination->oldestBackup(); + + return collect([ + 'Application name' => $this->applicationName(), + 'Backup name' => $this->backupName(), + 'Disk' => $backupDestination->diskName(), + 'Newest backup size' => $newestBackup ? Format::humanReadableSize($newestBackup->size()) : 'No backups were made yet', + 'Amount of backups' => (string) $backupDestination->backups()->count(), + 'Total storage used' => Format::humanReadableSize($backupDestination->backups()->size()), + 'Newest backup date' => $newestBackup ? $newestBackup->date()->format('Y/m/d H:i:s') : 'No backups were made yet', + 'Oldest backup date' => $oldestBackup ? $oldestBackup->date()->format('Y/m/d H:i:s') : 'No backups were made yet', + ])->filter(); + } + + public function backupDestination(): ?BackupDestination + { + if (isset($this->event->backupDestination)) { + return $this->event->backupDestination; + } + + if (isset($this->event->backupDestinationStatus)) { + return $this->event->backupDestinationStatus->backupDestination(); + } + + return null; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/EventHandler.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/EventHandler.php new file mode 100644 index 0000000..644c215 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/EventHandler.php @@ -0,0 +1,74 @@ +config = $config; + } + + public function subscribe(Dispatcher $events) + { + $events->listen($this->allBackupEventClasses(), function ($event) { + $notifiable = $this->determineNotifiable(); + + $notification = $this->determineNotification($event); + + $notifiable->notify($notification); + }); + } + + protected function determineNotifiable() + { + $notifiableClass = $this->config->get('backup.notifications.notifiable'); + + return app($notifiableClass); + } + + protected function determineNotification($event): Notification + { + $eventName = class_basename($event); + + $notificationClass = collect($this->config->get('backup.notifications.notifications')) + ->keys() + ->first(function ($notificationClass) use ($eventName) { + $notificationName = class_basename($notificationClass); + + return $notificationName === $eventName; + }); + + if (! $notificationClass) { + throw NotificationCouldNotBeSent::noNotifcationClassForEvent($event); + } + + return app($notificationClass)->setEvent($event); + } + + protected function allBackupEventClasses(): array + { + return [ + BackupHasFailed::class, + BackupWasSuccessful::class, + CleanupHasFailed::class, + CleanupWasSuccessful::class, + HealthyBackupWasFound::class, + UnhealthyBackupWasFound::class, + ]; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifiable.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifiable.php new file mode 100644 index 0000000..2c50339 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifiable.php @@ -0,0 +1,25 @@ +error() + ->subject(trans('backup::notifications.backup_failed_subject', ['application_name' => $this->applicationName()])) + ->line(trans('backup::notifications.backup_failed_body', ['application_name' => $this->applicationName()])) + ->line(trans('backup::notifications.exception_message', ['message' => $this->event->exception->getMessage()])) + ->line(trans('backup::notifications.exception_trace', ['trace' => $this->event->exception->getTraceAsString()])); + + $this->backupDestinationProperties()->each(function ($value, $name) use ($mailMessage) { + $mailMessage->line("{$name}: $value"); + }); + + return $mailMessage; + } + + public function toSlack(): SlackMessage + { + return (new SlackMessage) + ->error() + ->from(config('backup.notifications.slack.username'), config('backup.notifications.slack.icon')) + ->to(config('backup.notifications.slack.channel')) + ->content(trans('backup::notifications.backup_failed_subject', ['application_name' => $this->applicationName()])) + ->attachment(function (SlackAttachment $attachment) { + $attachment + ->title(trans('backup::notifications.exception_message_title')) + ->content($this->event->exception->getMessage()); + }) + ->attachment(function (SlackAttachment $attachment) { + $attachment + ->title(trans('backup::notifications.exception_trace_title')) + ->content($this->event->exception->getTraceAsString()); + }) + ->attachment(function (SlackAttachment $attachment) { + $attachment->fields($this->backupDestinationProperties()->toArray()); + }); + } + + public function setEvent(BackupHasFailedEvent $event) + { + $this->event = $event; + + return $this; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/BackupWasSuccessful.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/BackupWasSuccessful.php new file mode 100644 index 0000000..b04df24 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/BackupWasSuccessful.php @@ -0,0 +1,47 @@ +subject(trans('backup::notifications.backup_successful_subject', ['application_name' => $this->applicationName()])) + ->line(trans('backup::notifications.backup_successful_body', ['application_name' => $this->applicationName(), 'disk_name' => $this->diskName()])); + + $this->backupDestinationProperties()->each(function ($value, $name) use ($mailMessage) { + $mailMessage->line("{$name}: $value"); + }); + + return $mailMessage; + } + + public function toSlack(): SlackMessage + { + return (new SlackMessage) + ->success() + ->from(config('backup.notifications.slack.username'), config('backup.notifications.slack.icon')) + ->to(config('backup.notifications.slack.channel')) + ->content(trans('backup::notifications.backup_successful_subject_title')) + ->attachment(function (SlackAttachment $attachment) { + $attachment->fields($this->backupDestinationProperties()->toArray()); + }); + } + + public function setEvent(BackupWasSuccessfulEvent $event) + { + $this->event = $event; + + return $this; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/CleanupHasFailed.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/CleanupHasFailed.php new file mode 100644 index 0000000..6b3816b --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/CleanupHasFailed.php @@ -0,0 +1,60 @@ +error() + ->subject(trans('backup::notifications.cleanup_failed_subject', ['application_name' => $this->applicationName()])) + ->line(trans('backup::notifications.cleanup_failed_body', ['application_name' => $this->applicationName()])) + ->line(trans('backup::notifications.exception_message', ['message' => $this->event->exception->getMessage()])) + ->line(trans('backup::notifications.exception_trace', ['trace' => $this->event->exception->getTraceAsString()])); + + $this->backupDestinationProperties()->each(function ($value, $name) use ($mailMessage) { + $mailMessage->line("{$name}: $value"); + }); + + return $mailMessage; + } + + public function toSlack(): SlackMessage + { + return (new SlackMessage) + ->error() + ->from(config('backup.notifications.slack.username'), config('backup.notifications.slack.icon')) + ->to(config('backup.notifications.slack.channel')) + ->content(trans('backup::notifications.cleanup_failed_subject', ['application_name' => $this->applicationName()])) + ->attachment(function (SlackAttachment $attachment) { + $attachment + ->title(trans('backup::notifications.exception_message_title')) + ->content($this->event->exception->getMessage()); + }) + ->attachment(function (SlackAttachment $attachment) { + $attachment + ->title(trans('backup::notifications.exception_message_trace')) + ->content($this->event->exception->getTraceAsString()); + }) + ->attachment(function (SlackAttachment $attachment) { + $attachment->fields($this->backupDestinationProperties()->toArray()); + }); + } + + public function setEvent(CleanupHasFailedEvent $event) + { + $this->event = $event; + + return $this; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/CleanupWasSuccessful.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/CleanupWasSuccessful.php new file mode 100644 index 0000000..8745be5 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/CleanupWasSuccessful.php @@ -0,0 +1,47 @@ +subject(trans('backup::notifications.cleanup_successful_subject', ['application_name' => $this->applicationName()])) + ->line(trans('backup::notifications.cleanup_successful_body', ['application_name' => $this->applicationName(), 'disk_name' => $this->diskName()])); + + $this->backupDestinationProperties()->each(function ($value, $name) use ($mailMessage) { + $mailMessage->line("{$name}: $value"); + }); + + return $mailMessage; + } + + public function toSlack(): SlackMessage + { + return (new SlackMessage) + ->success() + ->from(config('backup.notifications.slack.username'), config('backup.notifications.slack.icon')) + ->to(config('backup.notifications.slack.channel')) + ->content(trans('backup::notifications.cleanup_successful_subject_title')) + ->attachment(function (SlackAttachment $attachment) { + $attachment->fields($this->backupDestinationProperties()->toArray()); + }); + } + + public function setEvent(CleanupWasSuccessfulEvent $event) + { + $this->event = $event; + + return $this; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/HealthyBackupWasFound.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/HealthyBackupWasFound.php new file mode 100644 index 0000000..4dca066 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/HealthyBackupWasFound.php @@ -0,0 +1,47 @@ +subject(trans('backup::notifications.healthy_backup_found_subject', ['application_name' => $this->applicationName(), 'disk_name' => $this->diskName()])) + ->line(trans('backup::notifications.healthy_backup_found_body', ['application_name' => $this->applicationName()])); + + $this->backupDestinationProperties()->each(function ($value, $name) use ($mailMessage) { + $mailMessage->line("{$name}: $value"); + }); + + return $mailMessage; + } + + public function toSlack(): SlackMessage + { + return (new SlackMessage) + ->success() + ->from(config('backup.notifications.slack.username'), config('backup.notifications.slack.icon')) + ->to(config('backup.notifications.slack.channel')) + ->content(trans('backup::notifications.healthy_backup_found_subject_title', ['application_name' => $this->applicationName()])) + ->attachment(function (SlackAttachment $attachment) { + $attachment->fields($this->backupDestinationProperties()->toArray()); + }); + } + + public function setEvent(HealthyBackupWasFoundEvent $event) + { + $this->event = $event; + + return $this; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/UnhealthyBackupWasFound.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/UnhealthyBackupWasFound.php new file mode 100644 index 0000000..a432fbc --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Notifications/Notifications/UnhealthyBackupWasFound.php @@ -0,0 +1,72 @@ +error() + ->subject(trans('backup::notifications.unhealthy_backup_found_subject', ['application_name' => $this->applicationName()])) + ->line(trans('backup::notifications.unhealthy_backup_found_body', ['application_name' => $this->applicationName(), 'disk_name' => $this->diskName()])) + ->line($this->problemDescription()); + + $this->backupDestinationProperties()->each(function ($value, $name) use ($mailMessage) { + $mailMessage->line("{$name}: $value"); + }); + + return $mailMessage; + } + + public function toSlack(): SlackMessage + { + return (new SlackMessage) + ->error() + ->from(config('backup.notifications.slack.username'), config('backup.notifications.slack.icon')) + ->to(config('backup.notifications.slack.channel')) + ->content(trans('backup::notifications.unhealthy_backup_found_subject_title', ['application_name' => $this->applicationName(), 'problem' => $this->problemDescription()])) + ->attachment(function (SlackAttachment $attachment) { + $attachment->fields($this->backupDestinationProperties()->toArray()); + }); + } + + protected function problemDescription(): string + { + $backupStatus = $this->event->backupDestinationStatus; + + if (! $backupStatus->isReachable()) { + return trans('backup::notification.unhealthy_backup_found_not_reachable', ['error' => $backupStatus->connectionError()]); + } + + if ($backupStatus->amountOfBackups() === 0) { + return trans('backup::notifications.unhealthy_backup_found_empty'); + } + + if ($backupStatus->usesTooMuchStorage()) { + return trans('backup::notifications.unhealthy_backup_found_full', ['disk_usage' => $backupStatus->humanReadableUsedStorage(), 'disk_limit' => $backupStatus->humanReadableAllowedStorage()]); + } + + if ($backupStatus->newestBackupIsTooOld()) { + return trans('backup::notifications.unhealthy_backup_found_old', ['date' => $backupStatus->dateOfNewestBackup()->format('Y/m/d h:i:s')]); + } + + return trans('backup::notifications.unhealthy_backup_found_unknown'); + } + + public function setEvent(UnhealthyBackupWasFoundEvent $event) + { + $this->event = $event; + + return $this; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/BackupJob.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/BackupJob.php new file mode 100644 index 0000000..eb0c3b1 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/BackupJob.php @@ -0,0 +1,277 @@ +dontBackupFilesystem(); + $this->dontBackupDatabases(); + $this->setDefaultFilename(); + + $this->backupDestinations = new Collection(); + } + + public function dontBackupFilesystem(): self + { + $this->fileSelection = FileSelection::create(); + + return $this; + } + + public function onlyDbName(array $allowedDbNames): self + { + $this->dbDumpers = $this->dbDumpers->filter( + function (DbDumper $dbDumper, string $connectionName) use ($allowedDbNames) { + return in_array($connectionName, $allowedDbNames); + }); + + return $this; + } + + public function dontBackupDatabases(): self + { + $this->dbDumpers = new Collection(); + + return $this; + } + + public function disableNotifications(): self + { + $this->sendNotifications = false; + + return $this; + } + + public function setDefaultFilename(): self + { + $this->filename = Carbon::now()->format('Y-m-d-H-i-s').'.zip'; + + return $this; + } + + public function setFileSelection(FileSelection $fileSelection): self + { + $this->fileSelection = $fileSelection; + + return $this; + } + + public function setDbDumpers(Collection $dbDumpers): self + { + $this->dbDumpers = $dbDumpers; + + return $this; + } + + public function setFilename(string $filename): self + { + $this->filename = $filename; + + return $this; + } + + public function onlyBackupTo(string $diskName): self + { + $this->backupDestinations = $this->backupDestinations->filter(function (BackupDestination $backupDestination) use ($diskName) { + return $backupDestination->diskName() === $diskName; + }); + + if (! count($this->backupDestinations)) { + throw InvalidBackupJob::destinationDoesNotExist($diskName); + } + + return $this; + } + + public function setBackupDestinations(Collection $backupDestinations): self + { + $this->backupDestinations = $backupDestinations; + + return $this; + } + + public function run() + { + $temporaryDirectoryPath = config('backup.backup.temporary_directory') ?? storage_path('app/backup-temp'); + + $this->temporaryDirectory = (new TemporaryDirectory($temporaryDirectoryPath)) + ->name('temp') + ->force() + ->create() + ->empty(); + + try { + if (! count($this->backupDestinations)) { + throw InvalidBackupJob::noDestinationsSpecified(); + } + + $manifest = $this->createBackupManifest(); + + if (! $manifest->count()) { + throw InvalidBackupJob::noFilesToBeBackedUp(); + } + + $zipFile = $this->createZipContainingEveryFileInManifest($manifest); + + $this->copyToBackupDestinations($zipFile); + } catch (Exception $exception) { + consoleOutput()->error("Backup failed because {$exception->getMessage()}.".PHP_EOL.$exception->getTraceAsString()); + + $this->sendNotification(new BackupHasFailed($exception)); + + $this->temporaryDirectory->delete(); + + throw $exception; + } + + $this->temporaryDirectory->delete(); + } + + protected function createBackupManifest(): Manifest + { + $databaseDumps = $this->dumpDatabases(); + + consoleOutput()->info('Determining files to backup...'); + + $manifest = Manifest::create($this->temporaryDirectory->path('manifest.txt')) + ->addFiles($databaseDumps) + ->addFiles($this->filesToBeBackedUp()); + + $this->sendNotification(new BackupManifestWasCreated($manifest)); + + return $manifest; + } + + public function filesToBeBackedUp() + { + $this->fileSelection->excludeFilesFrom($this->directoriesUsedByBackupJob()); + + return $this->fileSelection->selectedFiles(); + } + + protected function directoriesUsedByBackupJob(): array + { + return $this->backupDestinations + ->filter(function (BackupDestination $backupDestination) { + return $backupDestination->filesystemType() === 'local'; + }) + ->map(function (BackupDestination $backupDestination) { + return $backupDestination->disk()->getDriver()->getAdapter()->applyPathPrefix('').$backupDestination->backupName(); + }) + ->each(function (string $backupDestinationDirectory) { + $this->fileSelection->excludeFilesFrom($backupDestinationDirectory); + }) + ->push($this->temporaryDirectory->path()) + ->toArray(); + } + + protected function createZipContainingEveryFileInManifest(Manifest $manifest) + { + consoleOutput()->info("Zipping {$manifest->count()} files..."); + + $pathToZip = $this->temporaryDirectory->path(config('backup.backup.destination.filename_prefix').$this->filename); + + $zip = Zip::createForManifest($manifest, $pathToZip); + + consoleOutput()->info("Created zip containing {$zip->count()} files. Size is {$zip->humanReadableSize()}"); + + $this->sendNotification(new BackupZipWasCreated($pathToZip)); + + return $pathToZip; + } + + /** + * Dumps the databases to the given directory. + * Returns an array with paths to the dump files. + * + * @return array + */ + protected function dumpDatabases(): array + { + return $this->dbDumpers->map(function (DbDumper $dbDumper) { + consoleOutput()->info("Dumping database {$dbDumper->getDbName()}..."); + + $dbType = mb_strtolower(basename(str_replace('\\', '/', get_class($dbDumper)))); + + $dbName = $dbDumper instanceof Sqlite ? 'database' : $dbDumper->getDbName(); + + $fileName = "{$dbType}-{$dbName}.sql"; + + if (config('backup.backup.gzip_database_dump')) { + $dbDumper->useCompressor(new GzipCompressor()); + $fileName .= '.'.$dbDumper->getCompressorExtension(); + } + + if ($compressor = config('backup.backup.database_dump_compressor')) { + $dbDumper->useCompressor(new $compressor()); + $fileName .= '.'.$dbDumper->getCompressorExtension(); + } + + $temporaryFilePath = $this->temporaryDirectory->path('db-dumps'.DIRECTORY_SEPARATOR.$fileName); + + $dbDumper->dumpToFile($temporaryFilePath); + + return $temporaryFilePath; + })->toArray(); + } + + protected function copyToBackupDestinations(string $path) + { + $this->backupDestinations->each(function (BackupDestination $backupDestination) use ($path) { + try { + consoleOutput()->info("Copying zip to disk named {$backupDestination->diskName()}..."); + + $backupDestination->write($path); + + consoleOutput()->info("Successfully copied zip to disk named {$backupDestination->diskName()}."); + + $this->sendNotification(new BackupWasSuccessful($backupDestination)); + } catch (Exception $exception) { + consoleOutput()->error("Copying zip failed because: {$exception->getMessage()}."); + + $this->sendNotification(new BackupHasFailed($exception, $backupDestination ?? null)); + } + }); + } + + protected function sendNotification($notification) + { + if ($this->sendNotifications) { + event($notification); + } + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/BackupJobFactory.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/BackupJobFactory.php new file mode 100644 index 0000000..f16ba59 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/BackupJobFactory.php @@ -0,0 +1,31 @@ +setFileSelection(static::createFileSelection($config['backup']['source']['files'])) + ->setDbDumpers(static::createDbDumpers($config['backup']['source']['databases'])) + ->setBackupDestinations(BackupDestinationFactory::createFromArray($config['backup'])); + } + + protected static function createFileSelection(array $sourceFiles): FileSelection + { + return FileSelection::create($sourceFiles['include']) + ->excludeFilesFrom($sourceFiles['exclude']) + ->shouldFollowLinks(isset($sourceFiles['followLinks']) && $sourceFiles['followLinks']); + } + + protected static function createDbDumpers(array $dbConnectionNames): Collection + { + return collect($dbConnectionNames)->mapWithKeys(function (string $dbConnectionName) { + return [$dbConnectionName=>DbDumperFactory::createFromConnection($dbConnectionName)]; + }); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/DbDumperFactory.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/DbDumperFactory.php new file mode 100644 index 0000000..3856d49 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/DbDumperFactory.php @@ -0,0 +1,105 @@ +setHost(array_first(array_wrap($dbConfig['host'] ?? ''))) + ->setDbName($dbConfig['database']) + ->setUserName($dbConfig['username'] ?? '') + ->setPassword($dbConfig['password'] ?? ''); + + if ($dbDumper instanceof MySql) { + $dbDumper->setDefaultCharacterSet($dbConfig['charset'] ?? ''); + } + + if (isset($dbConfig['port'])) { + $dbDumper = $dbDumper->setPort($dbConfig['port']); + } + + if (isset($dbConfig['dump'])) { + $dbDumper = static::processExtraDumpParameters($dbConfig['dump'], $dbDumper); + } + + return $dbDumper; + } + + protected static function forDriver($dbDriver): DbDumper + { + $driver = strtolower($dbDriver); + + if ($driver === 'mysql' || $driver === 'mariadb') { + return new MySql(); + } + + if ($driver === 'pgsql') { + return new PostgreSql(); + } + + if ($driver === 'sqlite') { + return new Sqlite(); + } + + if ($driver === 'mongodb') { + return new MongoDb(); + } + + throw CannotCreateDbDumper::unsupportedDriver($driver); + } + + protected static function processExtraDumpParameters(array $dumpConfiguration, DbDumper $dbDumper): DbDumper + { + collect($dumpConfiguration)->each(function ($configValue, $configName) use ($dbDumper) { + $methodName = lcfirst(studly_case(is_numeric($configName) ? $configValue : $configName)); + $methodValue = is_numeric($configName) ? null : $configValue; + + $methodName = static::determineValidMethodName($dbDumper, $methodName); + + if (method_exists($dbDumper, $methodName)) { + static::callMethodOnDumper($dbDumper, $methodName, $methodValue); + } + }); + + return $dbDumper; + } + + protected static function callMethodOnDumper(DbDumper $dbDumper, string $methodName, $methodValue): DbDumper + { + if (! $methodValue) { + $dbDumper->$methodName(); + + return $dbDumper; + } + + $dbDumper->$methodName($methodValue); + + return $dbDumper; + } + + protected static function determineValidMethodName(DbDumper $dbDumper, string $methodName): string + { + return collect([$methodName, 'set'.ucfirst($methodName)]) + ->first(function (string $methodName) use ($dbDumper) { + return method_exists($dbDumper, $methodName); + }, ''); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/FileSelection.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/FileSelection.php new file mode 100644 index 0000000..81e42fc --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/FileSelection.php @@ -0,0 +1,143 @@ +includeFilesAndDirectories = collect($includeFilesAndDirectories); + + $this->excludeFilesAndDirectories = collect(); + } + + /** + * Do not included the given files and directories. + * + * @param array|string $excludeFilesAndDirectories + * + * @return \Spatie\Backup\Tasks\Backup\FileSelection + */ + public function excludeFilesFrom($excludeFilesAndDirectories): self + { + $this->excludeFilesAndDirectories = $this->excludeFilesAndDirectories->merge($this->sanitize($excludeFilesAndDirectories)); + + return $this; + } + + public function shouldFollowLinks(bool $shouldFollowLinks): self + { + $this->shouldFollowLinks = $shouldFollowLinks; + + return $this; + } + + /** + * @return \Generator|string[] + */ + public function selectedFiles() + { + if ($this->includeFilesAndDirectories->isEmpty()) { + return []; + } + + $finder = (new Finder()) + ->ignoreDotFiles(false) + ->ignoreVCS(false) + ->files(); + + if ($this->shouldFollowLinks) { + $finder->followLinks(); + } + + foreach ($this->includedFiles() as $includedFile) { + yield $includedFile; + } + + if (! count($this->includedDirectories())) { + return; + } + + $finder->in($this->includedDirectories()); + + foreach ($finder->getIterator() as $file) { + if ($this->shouldExclude($file)) { + continue; + } + + yield $file->getPathname(); + } + } + + protected function includedFiles(): array + { + return $this->includeFilesAndDirectories->filter(function ($path) { + return is_file($path); + })->toArray(); + } + + protected function includedDirectories(): array + { + return $this->includeFilesAndDirectories->reject(function ($path) { + return is_file($path); + })->toArray(); + } + + protected function shouldExclude(string $path): bool + { + foreach ($this->excludeFilesAndDirectories as $excludedPath) { + if (starts_with(realpath($path), $excludedPath)) { + return true; + } + } + + return false; + } + + /** + * @param string|array $paths + * + * @return \Illuminate\Support\Collection + */ + protected function sanitize($paths): Collection + { + return collect($paths) + ->reject(function ($path) { + return $path === ''; + }) + ->flatMap(function ($path) { + return glob($path); + }) + ->map(function ($path) { + return realpath($path); + }) + ->reject(function ($path) { + return $path === false; + }); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/Manifest.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/Manifest.php new file mode 100644 index 0000000..c77282a --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/Manifest.php @@ -0,0 +1,74 @@ +manifestPath = $manifestPath; + + touch($manifestPath); + } + + public function path(): string + { + return $this->manifestPath; + } + + /** + * @param array|string $filePaths + * + * @return $this + */ + public function addFiles($filePaths): self + { + if (is_string($filePaths)) { + $filePaths = [$filePaths]; + } + + foreach ($filePaths as $filePath) { + if (! empty($filePath)) { + file_put_contents($this->manifestPath, $filePath.PHP_EOL, FILE_APPEND); + } + } + + return $this; + } + + /** + * @return \Generator|string[] + */ + public function files() + { + $file = new SplFileObject($this->path()); + + while (! $file->eof()) { + $filePath = $file->fgets(); + + if (! empty($filePath)) { + yield trim($filePath); + } + } + } + + public function count(): int + { + $file = new SplFileObject($this->manifestPath, 'r'); + + $file->seek(PHP_INT_MAX); + + return $file->key(); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/Zip.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/Zip.php new file mode 100644 index 0000000..4f8d987 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Backup/Zip.php @@ -0,0 +1,115 @@ +open(); + + foreach ($manifest->files() as $file) { + $zip->add($file, self::determineNameOfFileInZip($file, $pathToZip)); + } + + $zip->close(); + + return $zip; + } + + protected static function determineNameOfFileInZip(string $pathToFile, string $pathToZip) + { + $zipDirectory = pathinfo($pathToZip, PATHINFO_DIRNAME); + + $fileDirectory = pathinfo($pathToFile, PATHINFO_DIRNAME); + + if (starts_with($fileDirectory, $zipDirectory)) { + return str_replace($zipDirectory, '', $pathToFile); + } + + return $pathToFile; + } + + public function __construct(string $pathToZip) + { + $this->zipFile = new ZipArchive(); + + $this->pathToZip = $pathToZip; + + $this->open(); + } + + public function path(): string + { + return $this->pathToZip; + } + + public function size(): int + { + if ($this->fileCount === 0) { + return 0; + } + + return filesize($this->pathToZip); + } + + public function humanReadableSize(): string + { + return Format::humanReadableSize($this->size()); + } + + public function open() + { + $this->zipFile->open($this->pathToZip, ZipArchive::CREATE); + } + + public function close() + { + $this->zipFile->close(); + } + + /** + * @param string|array $files + * @param string $nameInZip + * + * @return \Spatie\Backup\Tasks\Backup\Zip + */ + public function add($files, string $nameInZip = null): self + { + if (is_array($files)) { + $nameInZip = null; + } + + if (is_string($files)) { + $files = [$files]; + } + + foreach ($files as $file) { + if (file_exists($file)) { + $this->zipFile->addFile($file, ltrim($nameInZip, DIRECTORY_SEPARATOR)).PHP_EOL; + } + $this->fileCount++; + } + + return $this; + } + + public function count(): int + { + return $this->fileCount; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/CleanupJob.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/CleanupJob.php new file mode 100644 index 0000000..eac94ed --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/CleanupJob.php @@ -0,0 +1,63 @@ +backupDestinations = $backupDestinations; + + $this->strategy = $strategy; + + $this->sendNotifications = ! $disableNotifications; + } + + public function run() + { + $this->backupDestinations->each(function (BackupDestination $backupDestination) { + try { + if (! $backupDestination->isReachable()) { + throw new Exception("Could not connect to disk {$backupDestination->diskName()} because: {$backupDestination->connectionError()}"); + } + + consoleOutput()->info("Cleaning backups of {$backupDestination->backupName()} on disk {$backupDestination->diskName()}..."); + + $this->strategy->deleteOldBackups($backupDestination->backups()); + $this->sendNotification(new CleanupWasSuccessful($backupDestination)); + + $usedStorage = Format::humanReadableSize($backupDestination->fresh()->usedStorage()); + consoleOutput()->info("Used storage after cleanup: {$usedStorage}."); + } catch (Exception $exception) { + consoleOutput()->error("Cleanup failed because: {$exception->getMessage()}."); + + $this->sendNotification(new CleanupHasFailed($exception)); + + throw $exception; + } + }); + } + + protected function sendNotification($notification) + { + if ($this->sendNotifications) { + event($notification); + } + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/CleanupStrategy.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/CleanupStrategy.php new file mode 100644 index 0000000..8851129 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/CleanupStrategy.php @@ -0,0 +1,19 @@ +config = $config; + } + + abstract public function deleteOldBackups(BackupCollection $backups); +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/Period.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/Period.php new file mode 100644 index 0000000..23cece6 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/Period.php @@ -0,0 +1,31 @@ +startDate = $startDate; + + $this->endDate = $endDate; + } + + public function startDate(): Carbon + { + return $this->startDate->copy(); + } + + public function endDate(): Carbon + { + return $this->endDate->copy(); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/Strategies/DefaultStrategy.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/Strategies/DefaultStrategy.php new file mode 100644 index 0000000..58aee4e --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Cleanup/Strategies/DefaultStrategy.php @@ -0,0 +1,122 @@ +newestBackup = $backups->shift(); + + $dateRanges = $this->calculateDateRanges(); + + $backupsPerPeriod = $dateRanges->map(function (Period $period) use ($backups) { + return $backups->filter(function (Backup $backup) use ($period) { + return $backup->date()->between($period->startDate(), $period->endDate()); + }); + }); + + $backupsPerPeriod['daily'] = $this->groupByDateFormat($backupsPerPeriod['daily'], 'Ymd'); + $backupsPerPeriod['weekly'] = $this->groupByDateFormat($backupsPerPeriod['weekly'], 'YW'); + $backupsPerPeriod['monthly'] = $this->groupByDateFormat($backupsPerPeriod['monthly'], 'Ym'); + $backupsPerPeriod['yearly'] = $this->groupByDateFormat($backupsPerPeriod['yearly'], 'Y'); + + $this->removeBackupsForAllPeriodsExceptOne($backupsPerPeriod); + + $this->removeBackupsOlderThan($dateRanges['yearly']->endDate(), $backups); + + $this->removeOldBackupsUntilUsingLessThanMaximumStorage($backups); + } + + protected function calculateDateRanges(): Collection + { + $config = $this->config->get('backup.cleanup.defaultStrategy'); + + $daily = new Period( + Carbon::now()->subDays($config['keepAllBackupsForDays']), + Carbon::now() + ->subDays($config['keepAllBackupsForDays']) + ->subDays($config['keepDailyBackupsForDays']) + ); + + $weekly = new Period( + $daily->endDate(), + $daily->endDate() + ->subWeeks($config['keepWeeklyBackupsForWeeks']) + ); + + $monthly = new Period( + $weekly->endDate(), + $weekly->endDate() + ->subMonths($config['keepMonthlyBackupsForMonths']) + ); + + $yearly = new Period( + $monthly->endDate(), + $monthly->endDate() + ->subYears($config['keepYearlyBackupsForYears']) + ); + + return collect(compact('daily', 'weekly', 'monthly', 'yearly')); + } + + protected function groupByDateFormat(Collection $backups, string $dateFormat): Collection + { + return $backups->groupBy(function (Backup $backup) use ($dateFormat) { + return $backup->date()->format($dateFormat); + }); + } + + protected function removeBackupsForAllPeriodsExceptOne(Collection $backupsPerPeriod) + { + $backupsPerPeriod->each(function (Collection $groupedBackupsByDateProperty, string $periodName) { + $groupedBackupsByDateProperty->each(function (Collection $group) { + $group->shift(); + + $group->each(function (Backup $backup) { + $backup->delete(); + }); + }); + }); + } + + protected function removeBackupsOlderThan(Carbon $endDate, BackupCollection $backups) + { + $backups->filter(function (Backup $backup) use ($endDate) { + return $backup->exists() && $backup->date()->lt($endDate); + })->each(function (Backup $backup) { + $backup->delete(); + }); + } + + protected function removeOldBackupsUntilUsingLessThanMaximumStorage(BackupCollection $backups) + { + if (! $oldest = $backups->oldest()) { + return; + } + + $maximumSize = $this->config->get('backup.cleanup.defaultStrategy.deleteOldestBackupsWhenUsingMoreMegabytesThan') + * 1024 * 1024; + + if (($backups->size() + $this->newestBackup->size()) <= $maximumSize) { + return; + } + + $oldest->delete(); + + $backups = $backups->filter->exists(); + + $this->removeOldBackupsUntilUsingLessThanMaximumStorage($backups); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Monitor/BackupDestinationStatus.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Monitor/BackupDestinationStatus.php new file mode 100644 index 0000000..48abe51 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Monitor/BackupDestinationStatus.php @@ -0,0 +1,159 @@ +backupDestination = $backupDestination; + $this->diskName = $diskName; + + $this->reachable = $this->backupDestination->isReachable(); + } + + public function setMaximumAgeOfNewestBackupInDays(int $days): self + { + $this->maximumAgeOfNewestBackupInDays = $days; + + return $this; + } + + public function maximumAgeOfNewestBackupInDays(): int + { + return $this->maximumAgeOfNewestBackupInDays; + } + + public function setMaximumStorageUsageInMegabytes(float $megabytes): self + { + $this->maximumStorageUsageInMegabytes = $megabytes; + + return $this; + } + + public function backupName(): string + { + return $this->backupDestination->backupName(); + } + + public function diskName(): string + { + return $this->diskName; + } + + public function amountOfBackups(): int + { + return $this->backupDestination->backups()->count(); + } + + public function dateOfNewestBackup(): ?Carbon + { + $newestBackup = $this->backupDestination->newestBackup(); + + if (is_null($newestBackup)) { + return null; + } + + return $newestBackup->date(); + } + + public function newestBackupIsTooOld(): bool + { + if (! count($this->backupDestination->backups())) { + return true; + } + + $maximumDate = Carbon::now()->subDays($this->maximumAgeOfNewestBackupInDays); + + return ! $this->backupDestination->newestBackupIsOlderThan($maximumDate); + } + + public function usedStorage(): int + { + return $this->backupDestination->usedStorage(); + } + + public function connectionError(): Exception + { + return $this->backupDestination->connectionError(); + } + + public function isReachable(): bool + { + return $this->reachable; + } + + public function maximumAllowedUsageInBytes(): int + { + return (int) ($this->maximumStorageUsageInMegabytes * 1024 * 1024); + } + + public function usesTooMuchStorage(): bool + { + $maximumInBytes = $this->maximumAllowedUsageInBytes(); + + if ($maximumInBytes === 0) { + return false; + } + + return $this->usedStorage() > $maximumInBytes; + } + + public function isHealthy(): bool + { + if (! $this->isReachable()) { + return false; + } + + if ($this->usesTooMuchStorage()) { + return false; + } + + if ($this->newestBackupIsTooOld()) { + return false; + } + + return true; + } + + public function humanReadableAllowedStorage(): string + { + $maximumInBytes = $this->maximumAllowedUsageInBytes(); + + if ($maximumInBytes === 0) { + return 'unlimited'; + } + + return Format::humanReadableSize($maximumInBytes); + } + + public function humanReadableUsedStorage(): string + { + return Format::humanReadableSize($this->usedStorage()); + } + + public function backupDestination(): BackupDestination + { + return $this->backupDestination; + } +} diff --git a/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Monitor/BackupDestinationStatusFactory.php b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Monitor/BackupDestinationStatusFactory.php new file mode 100644 index 0000000..ced238e --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/laravel-backup/src/Tasks/Monitor/BackupDestinationStatusFactory.php @@ -0,0 +1,29 @@ +flatMap(function (array $monitorProperties) { + return BackupDestinationStatusFactory::createForSingleMonitor($monitorProperties); + })->sortBy(function (BackupDestinationStatus $backupDestinationStatus) { + return "{$backupDestinationStatus->backupName()}-{$backupDestinationStatus->diskName()}"; + }); + } + + public static function createForSingleMonitor(array $monitorConfig): Collection + { + return collect($monitorConfig['disks'])->map(function ($diskName) use ($monitorConfig) { + $backupDestination = BackupDestination::create($diskName, $monitorConfig['name']); + + return (new BackupDestinationStatus($backupDestination, $diskName)) + ->setMaximumAgeOfNewestBackupInDays($monitorConfig['newestBackupsShouldNotBeOlderThanDays']) + ->setMaximumStorageUsageInMegabytes($monitorConfig['storageUsedMayNotBeHigherThanMegabytes']); + }); + } +} diff --git a/plugins/panakour/backup/vendor/spatie/temporary-directory/CHANGELOG.md b/plugins/panakour/backup/vendor/spatie/temporary-directory/CHANGELOG.md new file mode 100644 index 0000000..14604a0 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/temporary-directory/CHANGELOG.md @@ -0,0 +1,51 @@ +# Changelog + +All notable changes to `temporary-directory` will be documented in this file + +## 1.2.2 - 2019-12-15 + +- create dir with 0777 permissions and allow recursive in empty function (#38) + +## 1.2.1 - 2019-08-28 + +- delete directories using `FilesystemIterator` + +## 1.2.0 - 2019-06-16 + +- drop support for PHP 7.1 and below + +## 1.1.5 - 2019-06-16 + +- make sure unique directories are created under heavy load + +## 1.1.4 - 2018-04-13 + +- use return types instead of php-doc-tags + +## 1.1.2 - 2017-02-02 + +- do not use periods when generating a name for the temporary directory + +## 1.1.1 - 2017-02-02 + +- do not use spaces when generating a name for the temporary directory + +## 1.1.0 - 2017-02-01 + +- added optional $location argument in `TemporaryDirectory` constructor + +## 1.0.0 - 2017-02-01 + +- initial release + +## 0.0.3 - 2017-02-01 + +- added chainable methods for creating a temporary directory + +## 0.0.2 - 2017-01-30 + +- removed create method, use constructor instead + +## 0.0.1 - 2017-01-31 + +- experimental release diff --git a/plugins/panakour/backup/vendor/spatie/temporary-directory/CONTRIBUTING.md b/plugins/panakour/backup/vendor/spatie/temporary-directory/CONTRIBUTING.md new file mode 100644 index 0000000..4da74e3 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/temporary-directory/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/plugins/panakour/backup/vendor/spatie/temporary-directory/LICENSE.md b/plugins/panakour/backup/vendor/spatie/temporary-directory/LICENSE.md new file mode 100644 index 0000000..59e5ec5 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/temporary-directory/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Spatie bvba + +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/plugins/panakour/backup/vendor/spatie/temporary-directory/README.md b/plugins/panakour/backup/vendor/spatie/temporary-directory/README.md new file mode 100644 index 0000000..926b0ae --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/temporary-directory/README.md @@ -0,0 +1,145 @@ +# Quickly create, use and delete temporary directories + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/temporary-directory.svg?style=flat-square)](https://packagist.org/packages/spatie/temporary-directory) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/spatie/temporary-directory/master.svg?style=flat-square)](https://travis-ci.org/spatie/temporary-directory) +[![Quality Score](https://img.shields.io/scrutinizer/g/spatie/temporary-directory.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/temporary-directory) +[![StyleCI](https://styleci.io/repos/80403728/shield?branch=master)](https://styleci.io/repos/80403728) +[![Total Downloads](https://img.shields.io/packagist/dt/spatie/temporary-directory.svg?style=flat-square)](https://packagist.org/packages/spatie/temporary-directory) + +This package allows you to quickly create, use and delete a temporary directory in the system's temporary directory. + +Here's a quick example on how to create a temporary directory and delete it: + +```php +use Spatie\TemporaryDirectory\TemporaryDirectory; + +$temporaryDirectory = (new TemporaryDirectory())->create(); + +// Get a path inside the temporary directory +$temporaryDirectory->path('temporaryfile.txt'); + +// Delete the temporary directory and all the files inside it +$temporaryDirectory->delete(); +``` + +## Installation + +You can install the package via composer: + +```bash +composer require spatie/temporary-directory +``` + +## Usage + +### Creating a temporary directory + +To create a temporary directory simply call the `create` method on a `TemporaryDirectory` object. By default the temporary directory will be created in a timestamped directory in your system's temporary directory (usually `/tmp`). + +```php +(new TemporaryDirectory())->create(); +``` + +### Naming your temporary directory + +If you want to use a custom name for your temporary directory instead of the timestamp call the `name` method with a string `$name` argument before the `create` method. + +```php +(new TemporaryDirectory()) + ->name($name) + ->create(); +``` + +By default an exception will be thrown if a directory already exists with the given argument. You can override this behaviour by calling the `force` method in combination with the `name` method. + +```php +(new TemporaryDirectory()) + ->name($name) + ->force() + ->create(); +``` + +### Setting a custom location for a temporary directory + +You can set a custom location in which your temporary directory will be created by passing a string `$location` argument to the `TemporaryDirectory` constructor. + +```php +(new TemporaryDirectory($location)) + ->create(); +``` + +Optionally you can call the `location` method with a `$location` argument. + +```php +(new TemporaryDirectory()) + ->location($location) + ->create(); +``` + +### Determining paths within the temporary directory + +You can use the `path` method to determine the full path to a file or directory in the temporary directory: + +```php +$temporaryDirectory = (new TemporaryDirectory())->create(); +$temporaryDirectory->path('dumps/datadump.dat'); // return /tmp/1485941876276/dumps/datadump.dat +``` + +### Emptying a temporary directory + +Use the `empty` method to delete all the files inside the temporary directory. + +```php +$temporaryDirectory->empty(); +``` + +### Deleting a temporary directory + +Once you're done processing your temporary data you can delete the entire temporary directory using the `delete` method. All files inside of it will be deleted. + +```php +$temporaryDirectory->delete(); +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Testing + +``` bash +composer test +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Security + +If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. + +## Postcardware + +You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. + +Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. + +We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). + +## Credits + +- [Alex Vanderbist](https://github.com/AlexVanderbist) +- [All Contributors](../../contributors) + +## Support us + +Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). + +Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). +All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/plugins/panakour/backup/vendor/spatie/temporary-directory/composer.json b/plugins/panakour/backup/vendor/spatie/temporary-directory/composer.json new file mode 100644 index 0000000..597aa5b --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/temporary-directory/composer.json @@ -0,0 +1,40 @@ +{ + "name": "spatie/temporary-directory", + "description": "Easily create, use and destroy temporary directories", + "keywords": [ + "spatie", + "temporary-directory" + ], + "homepage": "https://github.com/spatie/temporary-directory", + "license": "MIT", + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "require": { + "php": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.0" + }, + "autoload": { + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Spatie\\TemporaryDirectory\\Test\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit" + }, + "config": { + "sort-packages": true + } +} diff --git a/plugins/panakour/backup/vendor/spatie/temporary-directory/src/TemporaryDirectory.php b/plugins/panakour/backup/vendor/spatie/temporary-directory/src/TemporaryDirectory.php new file mode 100644 index 0000000..68c35e9 --- /dev/null +++ b/plugins/panakour/backup/vendor/spatie/temporary-directory/src/TemporaryDirectory.php @@ -0,0 +1,162 @@ +location = $this->sanitizePath($location); + } + + public function create(): self + { + if (empty($this->location)) { + $this->location = $this->getSystemTemporaryDirectory(); + } + + if (empty($this->name)) { + $this->name = mt_rand().'-'.str_replace([' ', '.'], '', microtime()); + } + + if ($this->forceCreate && file_exists($this->getFullPath())) { + $this->deleteDirectory($this->getFullPath()); + } + + if (file_exists($this->getFullPath())) { + throw new InvalidArgumentException("Path `{$this->getFullPath()}` already exists."); + } + + mkdir($this->getFullPath(), 0777, true); + + return $this; + } + + public function force(): self + { + $this->forceCreate = true; + + return $this; + } + + public function name(string $name): self + { + $this->name = $this->sanitizeName($name); + + return $this; + } + + public function location(string $location): self + { + $this->location = $this->sanitizePath($location); + + return $this; + } + + public function path(string $pathOrFilename = ''): string + { + if (empty($pathOrFilename)) { + return $this->getFullPath(); + } + + $path = $this->getFullPath().DIRECTORY_SEPARATOR.trim($pathOrFilename, '/'); + + $directoryPath = $this->removeFilenameFromPath($path); + + if (! file_exists($directoryPath)) { + mkdir($directoryPath, 0777, true); + } + + return $path; + } + + public function empty(): self + { + $this->deleteDirectory($this->getFullPath()); + mkdir($this->getFullPath(), 0777, true); + + return $this; + } + + public function delete(): bool + { + return $this->deleteDirectory($this->getFullPath()); + } + + protected function getFullPath(): string + { + return $this->location.($this->name ? DIRECTORY_SEPARATOR.$this->name : ''); + } + + protected function isValidDirectoryName(string $directoryName): bool + { + return strpbrk($directoryName, '\\/?%*:|"<>') === false; + } + + protected function getSystemTemporaryDirectory(): string + { + return rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR); + } + + protected function sanitizePath(string $path): string + { + $path = rtrim($path); + + return rtrim($path, DIRECTORY_SEPARATOR); + } + + protected function sanitizeName(string $name): string + { + if (! $this->isValidDirectoryName($name)) { + throw new Exception("The directory name `$name` contains invalid characters."); + } + + return trim($name); + } + + protected function removeFilenameFromPath(string $path): string + { + if (! $this->isFilePath($path)) { + return $path; + } + + return substr($path, 0, strrpos($path, DIRECTORY_SEPARATOR)); + } + + protected function isFilePath(string $path): bool + { + return strpos($path, '.') !== false; + } + + protected function deleteDirectory(string $path): bool + { + if (! file_exists($path)) { + return true; + } + + if (! is_dir($path)) { + return unlink($path); + } + + foreach (new FilesystemIterator($path) as $item) { + if (! $this->deleteDirectory($item)) { + return false; + } + } + + return rmdir($path); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/CHANGELOG.md b/plugins/panakour/backup/vendor/symfony/finder/CHANGELOG.md new file mode 100644 index 0000000..2045184 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/CHANGELOG.md @@ -0,0 +1,74 @@ +CHANGELOG +========= + +4.3.0 +----- + + * added Finder::ignoreVCSIgnored() to ignore files based on rules listed in .gitignore + +4.2.0 +----- + + * added $useNaturalSort option to Finder::sortByName() method + * the `Finder::sortByName()` method will have a new `$useNaturalSort` + argument in version 5.0, not defining it is deprecated + * added `Finder::reverseSorting()` to reverse the sorting + +4.0.0 +----- + + * removed `ExceptionInterface` + * removed `Symfony\Component\Finder\Iterator\FilterIterator` + +3.4.0 +----- + + * deprecated `Symfony\Component\Finder\Iterator\FilterIterator` + * added Finder::hasResults() method to check if any results were found + +3.3.0 +----- + + * added double-star matching to Glob::toRegex() + +3.0.0 +----- + + * removed deprecated classes + +2.8.0 +----- + + * deprecated adapters and related classes + +2.5.0 +----- + * added support for GLOB_BRACE in the paths passed to Finder::in() + +2.3.0 +----- + + * added a way to ignore unreadable directories (via Finder::ignoreUnreadableDirs()) + * unified the way subfolders that are not executable are handled by always throwing an AccessDeniedException exception + +2.2.0 +----- + + * added Finder::path() and Finder::notPath() methods + * added finder adapters to improve performance on specific platforms + * added support for wildcard characters (glob patterns) in the paths passed + to Finder::in() + +2.1.0 +----- + + * added Finder::sortByAccessedTime(), Finder::sortByChangedTime(), and + Finder::sortByModifiedTime() + * added Countable to Finder + * added support for an array of directories as an argument to + Finder::exclude() + * added searching based on the file content via Finder::contains() and + Finder::notContains() + * added support for the != operator in the Comparator + * [BC BREAK] filter expressions (used for file name and content) are no more + considered as regexps but glob patterns when they are enclosed in '*' or '?' diff --git a/plugins/panakour/backup/vendor/symfony/finder/Comparator/Comparator.php b/plugins/panakour/backup/vendor/symfony/finder/Comparator/Comparator.php new file mode 100644 index 0000000..6aee21c --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Comparator/Comparator.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Comparator; + +/** + * Comparator. + * + * @author Fabien Potencier + */ +class Comparator +{ + private $target; + private $operator = '=='; + + /** + * Gets the target value. + * + * @return string The target value + */ + public function getTarget() + { + return $this->target; + } + + /** + * Sets the target value. + * + * @param string $target The target value + */ + public function setTarget($target) + { + $this->target = $target; + } + + /** + * Gets the comparison operator. + * + * @return string The operator + */ + public function getOperator() + { + return $this->operator; + } + + /** + * Sets the comparison operator. + * + * @param string $operator A valid operator + * + * @throws \InvalidArgumentException + */ + public function setOperator($operator) + { + if (!$operator) { + $operator = '=='; + } + + if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) { + throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator)); + } + + $this->operator = $operator; + } + + /** + * Tests against the target. + * + * @param mixed $test A test value + * + * @return bool + */ + public function test($test) + { + switch ($this->operator) { + case '>': + return $test > $this->target; + case '>=': + return $test >= $this->target; + case '<': + return $test < $this->target; + case '<=': + return $test <= $this->target; + case '!=': + return $test != $this->target; + } + + return $test == $this->target; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Comparator/DateComparator.php b/plugins/panakour/backup/vendor/symfony/finder/Comparator/DateComparator.php new file mode 100644 index 0000000..d17c77a --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Comparator/DateComparator.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Comparator; + +/** + * DateCompare compiles date comparisons. + * + * @author Fabien Potencier + */ +class DateComparator extends Comparator +{ + /** + * @param string $test A comparison string + * + * @throws \InvalidArgumentException If the test is not understood + */ + public function __construct(string $test) + { + if (!preg_match('#^\s*(==|!=|[<>]=?|after|since|before|until)?\s*(.+?)\s*$#i', $test, $matches)) { + throw new \InvalidArgumentException(sprintf('Don\'t understand "%s" as a date test.', $test)); + } + + try { + $date = new \DateTime($matches[2]); + $target = $date->format('U'); + } catch (\Exception $e) { + throw new \InvalidArgumentException(sprintf('"%s" is not a valid date.', $matches[2])); + } + + $operator = isset($matches[1]) ? $matches[1] : '=='; + if ('since' === $operator || 'after' === $operator) { + $operator = '>'; + } + + if ('until' === $operator || 'before' === $operator) { + $operator = '<'; + } + + $this->setOperator($operator); + $this->setTarget($target); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Comparator/NumberComparator.php b/plugins/panakour/backup/vendor/symfony/finder/Comparator/NumberComparator.php new file mode 100644 index 0000000..80667c9 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Comparator/NumberComparator.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Comparator; + +/** + * NumberComparator compiles a simple comparison to an anonymous + * subroutine, which you can call with a value to be tested again. + * + * Now this would be very pointless, if NumberCompare didn't understand + * magnitudes. + * + * The target value may use magnitudes of kilobytes (k, ki), + * megabytes (m, mi), or gigabytes (g, gi). Those suffixed + * with an i use the appropriate 2**n version in accordance with the + * IEC standard: http://physics.nist.gov/cuu/Units/binary.html + * + * Based on the Perl Number::Compare module. + * + * @author Fabien Potencier PHP port + * @author Richard Clamp Perl version + * @copyright 2004-2005 Fabien Potencier + * @copyright 2002 Richard Clamp + * + * @see http://physics.nist.gov/cuu/Units/binary.html + */ +class NumberComparator extends Comparator +{ + /** + * @param string|int $test A comparison string or an integer + * + * @throws \InvalidArgumentException If the test is not understood + */ + public function __construct(?string $test) + { + if (!preg_match('#^\s*(==|!=|[<>]=?)?\s*([0-9\.]+)\s*([kmg]i?)?\s*$#i', $test, $matches)) { + throw new \InvalidArgumentException(sprintf('Don\'t understand "%s" as a number test.', $test)); + } + + $target = $matches[2]; + if (!is_numeric($target)) { + throw new \InvalidArgumentException(sprintf('Invalid number "%s".', $target)); + } + if (isset($matches[3])) { + // magnitude + switch (strtolower($matches[3])) { + case 'k': + $target *= 1000; + break; + case 'ki': + $target *= 1024; + break; + case 'm': + $target *= 1000000; + break; + case 'mi': + $target *= 1024 * 1024; + break; + case 'g': + $target *= 1000000000; + break; + case 'gi': + $target *= 1024 * 1024 * 1024; + break; + } + } + + $this->setTarget($target); + $this->setOperator(isset($matches[1]) ? $matches[1] : '=='); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Exception/AccessDeniedException.php b/plugins/panakour/backup/vendor/symfony/finder/Exception/AccessDeniedException.php new file mode 100644 index 0000000..ee195ea --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Exception/AccessDeniedException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Exception; + +/** + * @author Jean-François Simon + */ +class AccessDeniedException extends \UnexpectedValueException +{ +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Exception/DirectoryNotFoundException.php b/plugins/panakour/backup/vendor/symfony/finder/Exception/DirectoryNotFoundException.php new file mode 100644 index 0000000..c6cc0f2 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Exception/DirectoryNotFoundException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Exception; + +/** + * @author Andreas Erhard + */ +class DirectoryNotFoundException extends \InvalidArgumentException +{ +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Finder.php b/plugins/panakour/backup/vendor/symfony/finder/Finder.php new file mode 100644 index 0000000..e1f27a8 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Finder.php @@ -0,0 +1,812 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder; + +use Symfony\Component\Finder\Comparator\DateComparator; +use Symfony\Component\Finder\Comparator\NumberComparator; +use Symfony\Component\Finder\Exception\DirectoryNotFoundException; +use Symfony\Component\Finder\Iterator\CustomFilterIterator; +use Symfony\Component\Finder\Iterator\DateRangeFilterIterator; +use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator; +use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator; +use Symfony\Component\Finder\Iterator\FilecontentFilterIterator; +use Symfony\Component\Finder\Iterator\FilenameFilterIterator; +use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator; +use Symfony\Component\Finder\Iterator\SortableIterator; + +/** + * Finder allows to build rules to find files and directories. + * + * It is a thin wrapper around several specialized iterator classes. + * + * All rules may be invoked several times. + * + * All methods return the current Finder object to allow chaining: + * + * $finder = Finder::create()->files()->name('*.php')->in(__DIR__); + * + * @author Fabien Potencier + */ +class Finder implements \IteratorAggregate, \Countable +{ + const IGNORE_VCS_FILES = 1; + const IGNORE_DOT_FILES = 2; + const IGNORE_VCS_IGNORED_FILES = 4; + + private $mode = 0; + private $names = []; + private $notNames = []; + private $exclude = []; + private $filters = []; + private $depths = []; + private $sizes = []; + private $followLinks = false; + private $reverseSorting = false; + private $sort = false; + private $ignore = 0; + private $dirs = []; + private $dates = []; + private $iterators = []; + private $contains = []; + private $notContains = []; + private $paths = []; + private $notPaths = []; + private $ignoreUnreadableDirs = false; + + private static $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg']; + + public function __construct() + { + $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES; + } + + /** + * Creates a new Finder. + * + * @return static + */ + public static function create() + { + return new static(); + } + + /** + * Restricts the matching to directories only. + * + * @return $this + */ + public function directories() + { + $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES; + + return $this; + } + + /** + * Restricts the matching to files only. + * + * @return $this + */ + public function files() + { + $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES; + + return $this; + } + + /** + * Adds tests for the directory depth. + * + * Usage: + * + * $finder->depth('> 1') // the Finder will start matching at level 1. + * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point. + * $finder->depth(['>= 1', '< 3']) + * + * @param string|int|string[]|int[] $levels The depth level expression or an array of depth levels + * + * @return $this + * + * @see DepthRangeFilterIterator + * @see NumberComparator + */ + public function depth($levels) + { + foreach ((array) $levels as $level) { + $this->depths[] = new Comparator\NumberComparator($level); + } + + return $this; + } + + /** + * Adds tests for file dates (last modified). + * + * The date must be something that strtotime() is able to parse: + * + * $finder->date('since yesterday'); + * $finder->date('until 2 days ago'); + * $finder->date('> now - 2 hours'); + * $finder->date('>= 2005-10-15'); + * $finder->date(['>= 2005-10-15', '<= 2006-05-27']); + * + * @param string|string[] $dates A date range string or an array of date ranges + * + * @return $this + * + * @see strtotime + * @see DateRangeFilterIterator + * @see DateComparator + */ + public function date($dates) + { + foreach ((array) $dates as $date) { + $this->dates[] = new Comparator\DateComparator($date); + } + + return $this; + } + + /** + * Adds rules that files must match. + * + * You can use patterns (delimited with / sign), globs or simple strings. + * + * $finder->name('*.php') + * $finder->name('/\.php$/') // same as above + * $finder->name('test.php') + * $finder->name(['test.py', 'test.php']) + * + * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns + * + * @return $this + * + * @see FilenameFilterIterator + */ + public function name($patterns) + { + $this->names = array_merge($this->names, (array) $patterns); + + return $this; + } + + /** + * Adds rules that files must not match. + * + * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns + * + * @return $this + * + * @see FilenameFilterIterator + */ + public function notName($patterns) + { + $this->notNames = array_merge($this->notNames, (array) $patterns); + + return $this; + } + + /** + * Adds tests that file contents must match. + * + * Strings or PCRE patterns can be used: + * + * $finder->contains('Lorem ipsum') + * $finder->contains('/Lorem ipsum/i') + * $finder->contains(['dolor', '/ipsum/i']) + * + * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns + * + * @return $this + * + * @see FilecontentFilterIterator + */ + public function contains($patterns) + { + $this->contains = array_merge($this->contains, (array) $patterns); + + return $this; + } + + /** + * Adds tests that file contents must not match. + * + * Strings or PCRE patterns can be used: + * + * $finder->notContains('Lorem ipsum') + * $finder->notContains('/Lorem ipsum/i') + * $finder->notContains(['lorem', '/dolor/i']) + * + * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns + * + * @return $this + * + * @see FilecontentFilterIterator + */ + public function notContains($patterns) + { + $this->notContains = array_merge($this->notContains, (array) $patterns); + + return $this; + } + + /** + * Adds rules that filenames must match. + * + * You can use patterns (delimited with / sign) or simple strings. + * + * $finder->path('some/special/dir') + * $finder->path('/some\/special\/dir/') // same as above + * $finder->path(['some dir', 'another/dir']) + * + * Use only / as dirname separator. + * + * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns + * + * @return $this + * + * @see FilenameFilterIterator + */ + public function path($patterns) + { + $this->paths = array_merge($this->paths, (array) $patterns); + + return $this; + } + + /** + * Adds rules that filenames must not match. + * + * You can use patterns (delimited with / sign) or simple strings. + * + * $finder->notPath('some/special/dir') + * $finder->notPath('/some\/special\/dir/') // same as above + * $finder->notPath(['some/file.txt', 'another/file.log']) + * + * Use only / as dirname separator. + * + * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns + * + * @return $this + * + * @see FilenameFilterIterator + */ + public function notPath($patterns) + { + $this->notPaths = array_merge($this->notPaths, (array) $patterns); + + return $this; + } + + /** + * Adds tests for file sizes. + * + * $finder->size('> 10K'); + * $finder->size('<= 1Ki'); + * $finder->size(4); + * $finder->size(['> 10K', '< 20K']) + * + * @param string|int|string[]|int[] $sizes A size range string or an integer or an array of size ranges + * + * @return $this + * + * @see SizeRangeFilterIterator + * @see NumberComparator + */ + public function size($sizes) + { + foreach ((array) $sizes as $size) { + $this->sizes[] = new Comparator\NumberComparator($size); + } + + return $this; + } + + /** + * Excludes directories. + * + * Directories passed as argument must be relative to the ones defined with the `in()` method. For example: + * + * $finder->in(__DIR__)->exclude('ruby'); + * + * @param string|array $dirs A directory path or an array of directories + * + * @return $this + * + * @see ExcludeDirectoryFilterIterator + */ + public function exclude($dirs) + { + $this->exclude = array_merge($this->exclude, (array) $dirs); + + return $this; + } + + /** + * Excludes "hidden" directories and files (starting with a dot). + * + * This option is enabled by default. + * + * @param bool $ignoreDotFiles Whether to exclude "hidden" files or not + * + * @return $this + * + * @see ExcludeDirectoryFilterIterator + */ + public function ignoreDotFiles($ignoreDotFiles) + { + if ($ignoreDotFiles) { + $this->ignore |= static::IGNORE_DOT_FILES; + } else { + $this->ignore &= ~static::IGNORE_DOT_FILES; + } + + return $this; + } + + /** + * Forces the finder to ignore version control directories. + * + * This option is enabled by default. + * + * @param bool $ignoreVCS Whether to exclude VCS files or not + * + * @return $this + * + * @see ExcludeDirectoryFilterIterator + */ + public function ignoreVCS($ignoreVCS) + { + if ($ignoreVCS) { + $this->ignore |= static::IGNORE_VCS_FILES; + } else { + $this->ignore &= ~static::IGNORE_VCS_FILES; + } + + return $this; + } + + /** + * Forces Finder to obey .gitignore and ignore files based on rules listed there. + * + * This option is disabled by default. + * + * @return $this + */ + public function ignoreVCSIgnored(bool $ignoreVCSIgnored) + { + if ($ignoreVCSIgnored) { + $this->ignore |= static::IGNORE_VCS_IGNORED_FILES; + } else { + $this->ignore &= ~static::IGNORE_VCS_IGNORED_FILES; + } + + return $this; + } + + /** + * Adds VCS patterns. + * + * @see ignoreVCS() + * + * @param string|string[] $pattern VCS patterns to ignore + */ + public static function addVCSPattern($pattern) + { + foreach ((array) $pattern as $p) { + self::$vcsPatterns[] = $p; + } + + self::$vcsPatterns = array_unique(self::$vcsPatterns); + } + + /** + * Sorts files and directories by an anonymous function. + * + * The anonymous function receives two \SplFileInfo instances to compare. + * + * This can be slow as all the matching files and directories must be retrieved for comparison. + * + * @return $this + * + * @see SortableIterator + */ + public function sort(\Closure $closure) + { + $this->sort = $closure; + + return $this; + } + + /** + * Sorts files and directories by name. + * + * This can be slow as all the matching files and directories must be retrieved for comparison. + * + * @param bool $useNaturalSort Whether to use natural sort or not, disabled by default + * + * @return $this + * + * @see SortableIterator + */ + public function sortByName(/* bool $useNaturalSort = false */) + { + if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "bool $useNaturalSort = false" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + } + $useNaturalSort = 0 < \func_num_args() && func_get_arg(0); + + $this->sort = $useNaturalSort ? Iterator\SortableIterator::SORT_BY_NAME_NATURAL : Iterator\SortableIterator::SORT_BY_NAME; + + return $this; + } + + /** + * Sorts files and directories by type (directories before files), then by name. + * + * This can be slow as all the matching files and directories must be retrieved for comparison. + * + * @return $this + * + * @see SortableIterator + */ + public function sortByType() + { + $this->sort = Iterator\SortableIterator::SORT_BY_TYPE; + + return $this; + } + + /** + * Sorts files and directories by the last accessed time. + * + * This is the time that the file was last accessed, read or written to. + * + * This can be slow as all the matching files and directories must be retrieved for comparison. + * + * @return $this + * + * @see SortableIterator + */ + public function sortByAccessedTime() + { + $this->sort = Iterator\SortableIterator::SORT_BY_ACCESSED_TIME; + + return $this; + } + + /** + * Reverses the sorting. + * + * @return $this + */ + public function reverseSorting() + { + $this->reverseSorting = true; + + return $this; + } + + /** + * Sorts files and directories by the last inode changed time. + * + * This is the time that the inode information was last modified (permissions, owner, group or other metadata). + * + * On Windows, since inode is not available, changed time is actually the file creation time. + * + * This can be slow as all the matching files and directories must be retrieved for comparison. + * + * @return $this + * + * @see SortableIterator + */ + public function sortByChangedTime() + { + $this->sort = Iterator\SortableIterator::SORT_BY_CHANGED_TIME; + + return $this; + } + + /** + * Sorts files and directories by the last modified time. + * + * This is the last time the actual contents of the file were last modified. + * + * This can be slow as all the matching files and directories must be retrieved for comparison. + * + * @return $this + * + * @see SortableIterator + */ + public function sortByModifiedTime() + { + $this->sort = Iterator\SortableIterator::SORT_BY_MODIFIED_TIME; + + return $this; + } + + /** + * Filters the iterator with an anonymous function. + * + * The anonymous function receives a \SplFileInfo and must return false + * to remove files. + * + * @return $this + * + * @see CustomFilterIterator + */ + public function filter(\Closure $closure) + { + $this->filters[] = $closure; + + return $this; + } + + /** + * Forces the following of symlinks. + * + * @return $this + */ + public function followLinks() + { + $this->followLinks = true; + + return $this; + } + + /** + * Tells finder to ignore unreadable directories. + * + * By default, scanning unreadable directories content throws an AccessDeniedException. + * + * @param bool $ignore + * + * @return $this + */ + public function ignoreUnreadableDirs($ignore = true) + { + $this->ignoreUnreadableDirs = (bool) $ignore; + + return $this; + } + + /** + * Searches files and directories which match defined rules. + * + * @param string|string[] $dirs A directory path or an array of directories + * + * @return $this + * + * @throws DirectoryNotFoundException if one of the directories does not exist + */ + public function in($dirs) + { + $resolvedDirs = []; + + foreach ((array) $dirs as $dir) { + if (is_dir($dir)) { + $resolvedDirs[] = $this->normalizeDir($dir); + } elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? GLOB_BRACE : 0) | GLOB_ONLYDIR | GLOB_NOSORT)) { + sort($glob); + $resolvedDirs = array_merge($resolvedDirs, array_map([$this, 'normalizeDir'], $glob)); + } else { + throw new DirectoryNotFoundException(sprintf('The "%s" directory does not exist.', $dir)); + } + } + + $this->dirs = array_merge($this->dirs, $resolvedDirs); + + return $this; + } + + /** + * Returns an Iterator for the current Finder configuration. + * + * This method implements the IteratorAggregate interface. + * + * @return \Iterator|SplFileInfo[] An iterator + * + * @throws \LogicException if the in() method has not been called + */ + public function getIterator() + { + if (0 === \count($this->dirs) && 0 === \count($this->iterators)) { + throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.'); + } + + if (1 === \count($this->dirs) && 0 === \count($this->iterators)) { + return $this->searchInDirectory($this->dirs[0]); + } + + $iterator = new \AppendIterator(); + foreach ($this->dirs as $dir) { + $iterator->append($this->searchInDirectory($dir)); + } + + foreach ($this->iterators as $it) { + $iterator->append($it); + } + + return $iterator; + } + + /** + * Appends an existing set of files/directories to the finder. + * + * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array. + * + * @param iterable $iterator + * + * @return $this + * + * @throws \InvalidArgumentException when the given argument is not iterable + */ + public function append($iterator) + { + if ($iterator instanceof \IteratorAggregate) { + $this->iterators[] = $iterator->getIterator(); + } elseif ($iterator instanceof \Iterator) { + $this->iterators[] = $iterator; + } elseif ($iterator instanceof \Traversable || \is_array($iterator)) { + $it = new \ArrayIterator(); + foreach ($iterator as $file) { + $it->append($file instanceof \SplFileInfo ? $file : new \SplFileInfo($file)); + } + $this->iterators[] = $it; + } else { + throw new \InvalidArgumentException('Finder::append() method wrong argument type.'); + } + + return $this; + } + + /** + * Check if the any results were found. + * + * @return bool + */ + public function hasResults() + { + foreach ($this->getIterator() as $_) { + return true; + } + + return false; + } + + /** + * Counts all the results collected by the iterators. + * + * @return int + */ + public function count() + { + return iterator_count($this->getIterator()); + } + + private function searchInDirectory(string $dir): \Iterator + { + $exclude = $this->exclude; + $notPaths = $this->notPaths; + + if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { + $exclude = array_merge($exclude, self::$vcsPatterns); + } + + if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) { + $notPaths[] = '#(^|/)\..+(/|$)#'; + } + + if (static::IGNORE_VCS_IGNORED_FILES === (static::IGNORE_VCS_IGNORED_FILES & $this->ignore)) { + $gitignoreFilePath = sprintf('%s/.gitignore', $dir); + if (!is_readable($gitignoreFilePath)) { + throw new \RuntimeException(sprintf('The "ignoreVCSIgnored" option cannot be used by the Finder as the "%s" file is not readable.', $gitignoreFilePath)); + } + $notPaths = array_merge($notPaths, [Gitignore::toRegex(file_get_contents($gitignoreFilePath))]); + } + + $minDepth = 0; + $maxDepth = PHP_INT_MAX; + + foreach ($this->depths as $comparator) { + switch ($comparator->getOperator()) { + case '>': + $minDepth = $comparator->getTarget() + 1; + break; + case '>=': + $minDepth = $comparator->getTarget(); + break; + case '<': + $maxDepth = $comparator->getTarget() - 1; + break; + case '<=': + $maxDepth = $comparator->getTarget(); + break; + default: + $minDepth = $maxDepth = $comparator->getTarget(); + } + } + + $flags = \RecursiveDirectoryIterator::SKIP_DOTS; + + if ($this->followLinks) { + $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; + } + + $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs); + + if ($exclude) { + $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $exclude); + } + + $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); + + if ($minDepth > 0 || $maxDepth < PHP_INT_MAX) { + $iterator = new Iterator\DepthRangeFilterIterator($iterator, $minDepth, $maxDepth); + } + + if ($this->mode) { + $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); + } + + if ($this->names || $this->notNames) { + $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames); + } + + if ($this->contains || $this->notContains) { + $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains); + } + + if ($this->sizes) { + $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes); + } + + if ($this->dates) { + $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates); + } + + if ($this->filters) { + $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); + } + + if ($this->paths || $notPaths) { + $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $notPaths); + } + + if ($this->sort || $this->reverseSorting) { + $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort, $this->reverseSorting); + $iterator = $iteratorAggregate->getIterator(); + } + + return $iterator; + } + + /** + * Normalizes given directory names by removing trailing slashes. + * + * Excluding: (s)ftp:// or ssh2.(s)ftp:// wrapper + */ + private function normalizeDir(string $dir): string + { + if ('/' === $dir) { + return $dir; + } + + $dir = rtrim($dir, '/'.\DIRECTORY_SEPARATOR); + + if (preg_match('#^(ssh2\.)?s?ftp://#', $dir)) { + $dir .= '/'; + } + + return $dir; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Gitignore.php b/plugins/panakour/backup/vendor/symfony/finder/Gitignore.php new file mode 100644 index 0000000..5ffe585 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Gitignore.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder; + +/** + * Gitignore matches against text. + * + * @author Ahmed Abdou + */ +class Gitignore +{ + /** + * Returns a regexp which is the equivalent of the gitignore pattern. + * + * @return string The regexp + */ + public static function toRegex(string $gitignoreFileContent): string + { + $gitignoreFileContent = preg_replace('/^[^\\\r\n]*#.*/m', '', $gitignoreFileContent); + $gitignoreLines = preg_split('/\r\n|\r|\n/', $gitignoreFileContent); + $gitignoreLines = array_map('trim', $gitignoreLines); + $gitignoreLines = array_filter($gitignoreLines); + + $ignoreLinesPositive = array_filter($gitignoreLines, function (string $line) { + return !preg_match('/^!/', $line); + }); + + $ignoreLinesNegative = array_filter($gitignoreLines, function (string $line) { + return preg_match('/^!/', $line); + }); + + $ignoreLinesNegative = array_map(function (string $line) { + return preg_replace('/^!(.*)/', '${1}', $line); + }, $ignoreLinesNegative); + $ignoreLinesNegative = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesNegative); + + $ignoreLinesPositive = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesPositive); + if (empty($ignoreLinesPositive)) { + return '/^$/'; + } + + if (empty($ignoreLinesNegative)) { + return sprintf('/%s/', implode('|', $ignoreLinesPositive)); + } + + return sprintf('/(?=^(?:(?!(%s)).)*$)(%s)/', implode('|', $ignoreLinesNegative), implode('|', $ignoreLinesPositive)); + } + + private static function getRegexFromGitignore(string $gitignorePattern): string + { + $regex = '('; + if (0 === strpos($gitignorePattern, '/')) { + $gitignorePattern = substr($gitignorePattern, 1); + $regex .= '^'; + } else { + $regex .= '(^|\/)'; + } + + if ('/' === $gitignorePattern[\strlen($gitignorePattern) - 1]) { + $gitignorePattern = substr($gitignorePattern, 0, -1); + } + + $iMax = \strlen($gitignorePattern); + for ($i = 0; $i < $iMax; ++$i) { + $doubleChars = substr($gitignorePattern, $i, 2); + if ('**' === $doubleChars) { + $regex .= '.+'; + ++$i; + continue; + } + + $c = $gitignorePattern[$i]; + switch ($c) { + case '*': + $regex .= '[^\/]+'; + break; + case '/': + case '.': + case ':': + case '(': + case ')': + case '{': + case '}': + $regex .= '\\'.$c; + break; + default: + $regex .= $c; + } + } + + $regex .= '($|\/)'; + $regex .= ')'; + + return $regex; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Glob.php b/plugins/panakour/backup/vendor/symfony/finder/Glob.php new file mode 100644 index 0000000..ea76d51 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Glob.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder; + +/** + * Glob matches globbing patterns against text. + * + * if match_glob("foo.*", "foo.bar") echo "matched\n"; + * + * // prints foo.bar and foo.baz + * $regex = glob_to_regex("foo.*"); + * for (['foo.bar', 'foo.baz', 'foo', 'bar'] as $t) + * { + * if (/$regex/) echo "matched: $car\n"; + * } + * + * Glob implements glob(3) style matching that can be used to match + * against text, rather than fetching names from a filesystem. + * + * Based on the Perl Text::Glob module. + * + * @author Fabien Potencier PHP port + * @author Richard Clamp Perl version + * @copyright 2004-2005 Fabien Potencier + * @copyright 2002 Richard Clamp + */ +class Glob +{ + /** + * Returns a regexp which is the equivalent of the glob pattern. + * + * @param string $glob The glob pattern + * @param bool $strictLeadingDot + * @param bool $strictWildcardSlash + * @param string $delimiter Optional delimiter + * + * @return string regex The regexp + */ + public static function toRegex($glob, $strictLeadingDot = true, $strictWildcardSlash = true, $delimiter = '#') + { + $firstByte = true; + $escaping = false; + $inCurlies = 0; + $regex = ''; + $sizeGlob = \strlen($glob); + for ($i = 0; $i < $sizeGlob; ++$i) { + $car = $glob[$i]; + if ($firstByte && $strictLeadingDot && '.' !== $car) { + $regex .= '(?=[^\.])'; + } + + $firstByte = '/' === $car; + + if ($firstByte && $strictWildcardSlash && isset($glob[$i + 2]) && '**' === $glob[$i + 1].$glob[$i + 2] && (!isset($glob[$i + 3]) || '/' === $glob[$i + 3])) { + $car = '[^/]++/'; + if (!isset($glob[$i + 3])) { + $car .= '?'; + } + + if ($strictLeadingDot) { + $car = '(?=[^\.])'.$car; + } + + $car = '/(?:'.$car.')*'; + $i += 2 + isset($glob[$i + 3]); + + if ('/' === $delimiter) { + $car = str_replace('/', '\\/', $car); + } + } + + if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) { + $regex .= "\\$car"; + } elseif ('*' === $car) { + $regex .= $escaping ? '\\*' : ($strictWildcardSlash ? '[^/]*' : '.*'); + } elseif ('?' === $car) { + $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.'); + } elseif ('{' === $car) { + $regex .= $escaping ? '\\{' : '('; + if (!$escaping) { + ++$inCurlies; + } + } elseif ('}' === $car && $inCurlies) { + $regex .= $escaping ? '}' : ')'; + if (!$escaping) { + --$inCurlies; + } + } elseif (',' === $car && $inCurlies) { + $regex .= $escaping ? ',' : '|'; + } elseif ('\\' === $car) { + if ($escaping) { + $regex .= '\\\\'; + $escaping = false; + } else { + $escaping = true; + } + + continue; + } else { + $regex .= $car; + } + $escaping = false; + } + + return $delimiter.'^'.$regex.'$'.$delimiter; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/CustomFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/CustomFilterIterator.php new file mode 100644 index 0000000..a30bbd0 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/CustomFilterIterator.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * CustomFilterIterator filters files by applying anonymous functions. + * + * The anonymous function receives a \SplFileInfo and must return false + * to remove files. + * + * @author Fabien Potencier + */ +class CustomFilterIterator extends \FilterIterator +{ + private $filters = []; + + /** + * @param \Iterator $iterator The Iterator to filter + * @param callable[] $filters An array of PHP callbacks + * + * @throws \InvalidArgumentException + */ + public function __construct(\Iterator $iterator, array $filters) + { + foreach ($filters as $filter) { + if (!\is_callable($filter)) { + throw new \InvalidArgumentException('Invalid PHP callback.'); + } + } + $this->filters = $filters; + + parent::__construct($iterator); + } + + /** + * Filters the iterator values. + * + * @return bool true if the value should be kept, false otherwise + */ + public function accept() + { + $fileinfo = $this->current(); + + foreach ($this->filters as $filter) { + if (false === $filter($fileinfo)) { + return false; + } + } + + return true; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/DateRangeFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/DateRangeFilterIterator.php new file mode 100644 index 0000000..2e97e00 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/DateRangeFilterIterator.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +use Symfony\Component\Finder\Comparator\DateComparator; + +/** + * DateRangeFilterIterator filters out files that are not in the given date range (last modified dates). + * + * @author Fabien Potencier + */ +class DateRangeFilterIterator extends \FilterIterator +{ + private $comparators = []; + + /** + * @param \Iterator $iterator The Iterator to filter + * @param DateComparator[] $comparators An array of DateComparator instances + */ + public function __construct(\Iterator $iterator, array $comparators) + { + $this->comparators = $comparators; + + parent::__construct($iterator); + } + + /** + * Filters the iterator values. + * + * @return bool true if the value should be kept, false otherwise + */ + public function accept() + { + $fileinfo = $this->current(); + + if (!file_exists($fileinfo->getPathname())) { + return false; + } + + $filedate = $fileinfo->getMTime(); + foreach ($this->comparators as $compare) { + if (!$compare->test($filedate)) { + return false; + } + } + + return true; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php new file mode 100644 index 0000000..436a66d --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * DepthRangeFilterIterator limits the directory depth. + * + * @author Fabien Potencier + */ +class DepthRangeFilterIterator extends \FilterIterator +{ + private $minDepth = 0; + + /** + * @param \RecursiveIteratorIterator $iterator The Iterator to filter + * @param int $minDepth The min depth + * @param int $maxDepth The max depth + */ + public function __construct(\RecursiveIteratorIterator $iterator, int $minDepth = 0, int $maxDepth = PHP_INT_MAX) + { + $this->minDepth = $minDepth; + $iterator->setMaxDepth(PHP_INT_MAX === $maxDepth ? -1 : $maxDepth); + + parent::__construct($iterator); + } + + /** + * Filters the iterator values. + * + * @return bool true if the value should be kept, false otherwise + */ + public function accept() + { + return $this->getInnerIterator()->getDepth() >= $this->minDepth; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php new file mode 100644 index 0000000..6a1b291 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * ExcludeDirectoryFilterIterator filters out directories. + * + * @author Fabien Potencier + */ +class ExcludeDirectoryFilterIterator extends \FilterIterator implements \RecursiveIterator +{ + private $iterator; + private $isRecursive; + private $excludedDirs = []; + private $excludedPattern; + + /** + * @param \Iterator $iterator The Iterator to filter + * @param string[] $directories An array of directories to exclude + */ + public function __construct(\Iterator $iterator, array $directories) + { + $this->iterator = $iterator; + $this->isRecursive = $iterator instanceof \RecursiveIterator; + $patterns = []; + foreach ($directories as $directory) { + $directory = rtrim($directory, '/'); + if (!$this->isRecursive || false !== strpos($directory, '/')) { + $patterns[] = preg_quote($directory, '#'); + } else { + $this->excludedDirs[$directory] = true; + } + } + if ($patterns) { + $this->excludedPattern = '#(?:^|/)(?:'.implode('|', $patterns).')(?:/|$)#'; + } + + parent::__construct($iterator); + } + + /** + * Filters the iterator values. + * + * @return bool True if the value should be kept, false otherwise + */ + public function accept() + { + if ($this->isRecursive && isset($this->excludedDirs[$this->getFilename()]) && $this->isDir()) { + return false; + } + + if ($this->excludedPattern) { + $path = $this->isDir() ? $this->current()->getRelativePathname() : $this->current()->getRelativePath(); + $path = str_replace('\\', '/', $path); + + return !preg_match($this->excludedPattern, $path); + } + + return true; + } + + /** + * @return bool + */ + public function hasChildren() + { + return $this->isRecursive && $this->iterator->hasChildren(); + } + + public function getChildren() + { + $children = new self($this->iterator->getChildren(), []); + $children->excludedDirs = $this->excludedDirs; + $children->excludedPattern = $this->excludedPattern; + + return $children; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/FileTypeFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/FileTypeFilterIterator.php new file mode 100644 index 0000000..a4c4eec --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/FileTypeFilterIterator.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * FileTypeFilterIterator only keeps files, directories, or both. + * + * @author Fabien Potencier + */ +class FileTypeFilterIterator extends \FilterIterator +{ + const ONLY_FILES = 1; + const ONLY_DIRECTORIES = 2; + + private $mode; + + /** + * @param \Iterator $iterator The Iterator to filter + * @param int $mode The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES) + */ + public function __construct(\Iterator $iterator, int $mode) + { + $this->mode = $mode; + + parent::__construct($iterator); + } + + /** + * Filters the iterator values. + * + * @return bool true if the value should be kept, false otherwise + */ + public function accept() + { + $fileinfo = $this->current(); + if (self::ONLY_DIRECTORIES === (self::ONLY_DIRECTORIES & $this->mode) && $fileinfo->isFile()) { + return false; + } elseif (self::ONLY_FILES === (self::ONLY_FILES & $this->mode) && $fileinfo->isDir()) { + return false; + } + + return true; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/FilecontentFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/FilecontentFilterIterator.php new file mode 100644 index 0000000..81594b8 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/FilecontentFilterIterator.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * FilecontentFilterIterator filters files by their contents using patterns (regexps or strings). + * + * @author Fabien Potencier + * @author Włodzimierz Gajda + */ +class FilecontentFilterIterator extends MultiplePcreFilterIterator +{ + /** + * Filters the iterator values. + * + * @return bool true if the value should be kept, false otherwise + */ + public function accept() + { + if (!$this->matchRegexps && !$this->noMatchRegexps) { + return true; + } + + $fileinfo = $this->current(); + + if ($fileinfo->isDir() || !$fileinfo->isReadable()) { + return false; + } + + $content = $fileinfo->getContents(); + if (!$content) { + return false; + } + + return $this->isAccepted($content); + } + + /** + * Converts string to regexp if necessary. + * + * @param string $str Pattern: string or regexp + * + * @return string regexp corresponding to a given string or regexp + */ + protected function toRegex($str) + { + return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/FilenameFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/FilenameFilterIterator.php new file mode 100644 index 0000000..e168cd8 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/FilenameFilterIterator.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +use Symfony\Component\Finder\Glob; + +/** + * FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string). + * + * @author Fabien Potencier + */ +class FilenameFilterIterator extends MultiplePcreFilterIterator +{ + /** + * Filters the iterator values. + * + * @return bool true if the value should be kept, false otherwise + */ + public function accept() + { + return $this->isAccepted($this->current()->getFilename()); + } + + /** + * Converts glob to regexp. + * + * PCRE patterns are left unchanged. + * Glob strings are transformed with Glob::toRegex(). + * + * @param string $str Pattern: glob or regexp + * + * @return string regexp corresponding to a given glob or regexp + */ + protected function toRegex($str) + { + return $this->isRegex($str) ? $str : Glob::toRegex($str); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php new file mode 100644 index 0000000..18b082e --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * MultiplePcreFilterIterator filters files using patterns (regexps, globs or strings). + * + * @author Fabien Potencier + */ +abstract class MultiplePcreFilterIterator extends \FilterIterator +{ + protected $matchRegexps = []; + protected $noMatchRegexps = []; + + /** + * @param \Iterator $iterator The Iterator to filter + * @param array $matchPatterns An array of patterns that need to match + * @param array $noMatchPatterns An array of patterns that need to not match + */ + public function __construct(\Iterator $iterator, array $matchPatterns, array $noMatchPatterns) + { + foreach ($matchPatterns as $pattern) { + $this->matchRegexps[] = $this->toRegex($pattern); + } + + foreach ($noMatchPatterns as $pattern) { + $this->noMatchRegexps[] = $this->toRegex($pattern); + } + + parent::__construct($iterator); + } + + /** + * Checks whether the string is accepted by the regex filters. + * + * If there is no regexps defined in the class, this method will accept the string. + * Such case can be handled by child classes before calling the method if they want to + * apply a different behavior. + * + * @param string $string The string to be matched against filters + * + * @return bool + */ + protected function isAccepted($string) + { + // should at least not match one rule to exclude + foreach ($this->noMatchRegexps as $regex) { + if (preg_match($regex, $string)) { + return false; + } + } + + // should at least match one rule + if ($this->matchRegexps) { + foreach ($this->matchRegexps as $regex) { + if (preg_match($regex, $string)) { + return true; + } + } + + return false; + } + + // If there is no match rules, the file is accepted + return true; + } + + /** + * Checks whether the string is a regex. + * + * @param string $str + * + * @return bool Whether the given string is a regex + */ + protected function isRegex($str) + { + if (preg_match('/^(.{3,}?)[imsxuADU]*$/', $str, $m)) { + $start = substr($m[1], 0, 1); + $end = substr($m[1], -1); + + if ($start === $end) { + return !preg_match('/[*?[:alnum:] \\\\]/', $start); + } + + foreach ([['{', '}'], ['(', ')'], ['[', ']'], ['<', '>']] as $delimiters) { + if ($start === $delimiters[0] && $end === $delimiters[1]) { + return true; + } + } + } + + return false; + } + + /** + * Converts string into regexp. + * + * @param string $str Pattern + * + * @return string regexp corresponding to a given string + */ + abstract protected function toRegex($str); +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/PathFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/PathFilterIterator.php new file mode 100644 index 0000000..3fda557 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/PathFilterIterator.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * PathFilterIterator filters files by path patterns (e.g. some/special/dir). + * + * @author Fabien Potencier + * @author Włodzimierz Gajda + */ +class PathFilterIterator extends MultiplePcreFilterIterator +{ + /** + * Filters the iterator values. + * + * @return bool true if the value should be kept, false otherwise + */ + public function accept() + { + $filename = $this->current()->getRelativePathname(); + + if ('\\' === \DIRECTORY_SEPARATOR) { + $filename = str_replace('\\', '/', $filename); + } + + return $this->isAccepted($filename); + } + + /** + * Converts strings to regexp. + * + * PCRE patterns are left unchanged. + * + * Default conversion: + * 'lorem/ipsum/dolor' ==> 'lorem\/ipsum\/dolor/' + * + * Use only / as directory separator (on Windows also). + * + * @param string $str Pattern: regexp or dirname + * + * @return string regexp corresponding to a given string or regexp + */ + protected function toRegex($str) + { + return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php new file mode 100644 index 0000000..7616b14 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +use Symfony\Component\Finder\Exception\AccessDeniedException; +use Symfony\Component\Finder\SplFileInfo; + +/** + * Extends the \RecursiveDirectoryIterator to support relative paths. + * + * @author Victor Berchet + */ +class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator +{ + /** + * @var bool + */ + private $ignoreUnreadableDirs; + + /** + * @var bool + */ + private $rewindable; + + // these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations + private $rootPath; + private $subPath; + private $directorySeparator = '/'; + + /** + * @throws \RuntimeException + */ + public function __construct(string $path, int $flags, bool $ignoreUnreadableDirs = false) + { + if ($flags & (self::CURRENT_AS_PATHNAME | self::CURRENT_AS_SELF)) { + throw new \RuntimeException('This iterator only support returning current as fileinfo.'); + } + + parent::__construct($path, $flags); + $this->ignoreUnreadableDirs = $ignoreUnreadableDirs; + $this->rootPath = $path; + if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) { + $this->directorySeparator = \DIRECTORY_SEPARATOR; + } + } + + /** + * Return an instance of SplFileInfo with support for relative paths. + * + * @return SplFileInfo File information + */ + public function current() + { + // the logic here avoids redoing the same work in all iterations + + if (null === $subPathname = $this->subPath) { + $subPathname = $this->subPath = (string) $this->getSubPath(); + } + if ('' !== $subPathname) { + $subPathname .= $this->directorySeparator; + } + $subPathname .= $this->getFilename(); + + if ('/' !== $basePath = $this->rootPath) { + $basePath .= $this->directorySeparator; + } + + return new SplFileInfo($basePath.$subPathname, $this->subPath, $subPathname); + } + + /** + * @return \RecursiveIterator + * + * @throws AccessDeniedException + */ + public function getChildren() + { + try { + $children = parent::getChildren(); + + if ($children instanceof self) { + // parent method will call the constructor with default arguments, so unreadable dirs won't be ignored anymore + $children->ignoreUnreadableDirs = $this->ignoreUnreadableDirs; + + // performance optimization to avoid redoing the same work in all children + $children->rewindable = &$this->rewindable; + $children->rootPath = $this->rootPath; + } + + return $children; + } catch (\UnexpectedValueException $e) { + if ($this->ignoreUnreadableDirs) { + // If directory is unreadable and finder is set to ignore it, a fake empty content is returned. + return new \RecursiveArrayIterator([]); + } else { + throw new AccessDeniedException($e->getMessage(), $e->getCode(), $e); + } + } + } + + /** + * Do nothing for non rewindable stream. + */ + public function rewind() + { + if (false === $this->isRewindable()) { + return; + } + + parent::rewind(); + } + + /** + * Checks if the stream is rewindable. + * + * @return bool true when the stream is rewindable, false otherwise + */ + public function isRewindable() + { + if (null !== $this->rewindable) { + return $this->rewindable; + } + + if (false !== $stream = @opendir($this->getPath())) { + $infos = stream_get_meta_data($stream); + closedir($stream); + + if ($infos['seekable']) { + return $this->rewindable = true; + } + } + + return $this->rewindable = false; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/SizeRangeFilterIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/SizeRangeFilterIterator.php new file mode 100644 index 0000000..2aeef67 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/SizeRangeFilterIterator.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +use Symfony\Component\Finder\Comparator\NumberComparator; + +/** + * SizeRangeFilterIterator filters out files that are not in the given size range. + * + * @author Fabien Potencier + */ +class SizeRangeFilterIterator extends \FilterIterator +{ + private $comparators = []; + + /** + * @param \Iterator $iterator The Iterator to filter + * @param NumberComparator[] $comparators An array of NumberComparator instances + */ + public function __construct(\Iterator $iterator, array $comparators) + { + $this->comparators = $comparators; + + parent::__construct($iterator); + } + + /** + * Filters the iterator values. + * + * @return bool true if the value should be kept, false otherwise + */ + public function accept() + { + $fileinfo = $this->current(); + if (!$fileinfo->isFile()) { + return true; + } + + $filesize = $fileinfo->getSize(); + foreach ($this->comparators as $compare) { + if (!$compare->test($filesize)) { + return false; + } + } + + return true; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/Iterator/SortableIterator.php b/plugins/panakour/backup/vendor/symfony/finder/Iterator/SortableIterator.php new file mode 100644 index 0000000..8f0090c --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/Iterator/SortableIterator.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * SortableIterator applies a sort on a given Iterator. + * + * @author Fabien Potencier + */ +class SortableIterator implements \IteratorAggregate +{ + const SORT_BY_NONE = 0; + const SORT_BY_NAME = 1; + const SORT_BY_TYPE = 2; + const SORT_BY_ACCESSED_TIME = 3; + const SORT_BY_CHANGED_TIME = 4; + const SORT_BY_MODIFIED_TIME = 5; + const SORT_BY_NAME_NATURAL = 6; + + private $iterator; + private $sort; + + /** + * @param \Traversable $iterator The Iterator to filter + * @param int|callable $sort The sort type (SORT_BY_NAME, SORT_BY_TYPE, or a PHP callback) + * + * @throws \InvalidArgumentException + */ + public function __construct(\Traversable $iterator, $sort, bool $reverseOrder = false) + { + $this->iterator = $iterator; + $order = $reverseOrder ? -1 : 1; + + if (self::SORT_BY_NAME === $sort) { + $this->sort = static function ($a, $b) use ($order) { + return $order * strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); + }; + } elseif (self::SORT_BY_NAME_NATURAL === $sort) { + $this->sort = static function ($a, $b) use ($order) { + return $order * strnatcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); + }; + } elseif (self::SORT_BY_TYPE === $sort) { + $this->sort = static function ($a, $b) use ($order) { + if ($a->isDir() && $b->isFile()) { + return -$order; + } elseif ($a->isFile() && $b->isDir()) { + return $order; + } + + return $order * strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); + }; + } elseif (self::SORT_BY_ACCESSED_TIME === $sort) { + $this->sort = static function ($a, $b) use ($order) { + return $order * ($a->getATime() - $b->getATime()); + }; + } elseif (self::SORT_BY_CHANGED_TIME === $sort) { + $this->sort = static function ($a, $b) use ($order) { + return $order * ($a->getCTime() - $b->getCTime()); + }; + } elseif (self::SORT_BY_MODIFIED_TIME === $sort) { + $this->sort = static function ($a, $b) use ($order) { + return $order * ($a->getMTime() - $b->getMTime()); + }; + } elseif (self::SORT_BY_NONE === $sort) { + $this->sort = $order; + } elseif (\is_callable($sort)) { + $this->sort = $reverseOrder ? static function ($a, $b) use ($sort) { return -$sort($a, $b); } : $sort; + } else { + throw new \InvalidArgumentException('The SortableIterator takes a PHP callable or a valid built-in sort algorithm as an argument.'); + } + } + + /** + * @return \Traversable + */ + public function getIterator() + { + if (1 === $this->sort) { + return $this->iterator; + } + + $array = iterator_to_array($this->iterator, true); + + if (-1 === $this->sort) { + $array = array_reverse($array); + } else { + uasort($array, $this->sort); + } + + return new \ArrayIterator($array); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/LICENSE b/plugins/panakour/backup/vendor/symfony/finder/LICENSE new file mode 100644 index 0000000..9e936ec --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2020 Fabien Potencier + +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/plugins/panakour/backup/vendor/symfony/finder/README.md b/plugins/panakour/backup/vendor/symfony/finder/README.md new file mode 100644 index 0000000..0b19c75 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/README.md @@ -0,0 +1,14 @@ +Finder Component +================ + +The Finder component finds files and directories via an intuitive fluent +interface. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/finder.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/plugins/panakour/backup/vendor/symfony/finder/SplFileInfo.php b/plugins/panakour/backup/vendor/symfony/finder/SplFileInfo.php new file mode 100644 index 0000000..65d7423 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/SplFileInfo.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder; + +/** + * Extends \SplFileInfo to support relative paths. + * + * @author Fabien Potencier + */ +class SplFileInfo extends \SplFileInfo +{ + private $relativePath; + private $relativePathname; + + /** + * @param string $file The file name + * @param string $relativePath The relative path + * @param string $relativePathname The relative path name + */ + public function __construct(string $file, string $relativePath, string $relativePathname) + { + parent::__construct($file); + $this->relativePath = $relativePath; + $this->relativePathname = $relativePathname; + } + + /** + * Returns the relative path. + * + * This path does not contain the file name. + * + * @return string the relative path + */ + public function getRelativePath() + { + return $this->relativePath; + } + + /** + * Returns the relative path name. + * + * This path contains the file name. + * + * @return string the relative path name + */ + public function getRelativePathname() + { + return $this->relativePathname; + } + + public function getFilenameWithoutExtension(): string + { + $filename = $this->getFilename(); + + return pathinfo($filename, PATHINFO_FILENAME); + } + + /** + * Returns the contents of the file. + * + * @return string the contents of the file + * + * @throws \RuntimeException + */ + public function getContents() + { + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + $content = file_get_contents($this->getPathname()); + restore_error_handler(); + if (false === $content) { + throw new \RuntimeException($error); + } + + return $content; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/finder/composer.json b/plugins/panakour/backup/vendor/symfony/finder/composer.json new file mode 100644 index 0000000..0b1408c --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/finder/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/finder", + "type": "library", + "description": "Symfony Finder Component", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^7.1.3" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Finder\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/Idn.php b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/Idn.php new file mode 100644 index 0000000..adb718d --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/Idn.php @@ -0,0 +1,283 @@ + + * @author Sebastian Kroczek + * @author Dmitry Lukashin + * @author Laurent Bassin + * + * @internal + */ +final class Idn +{ + const INTL_IDNA_VARIANT_2003 = 0; + const INTL_IDNA_VARIANT_UTS46 = 1; + + private static $encodeTable = array( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', + 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + ); + + private static $decodeTable = array( + 'a' => 0, 'b' => 1, 'c' => 2, 'd' => 3, 'e' => 4, 'f' => 5, + 'g' => 6, 'h' => 7, 'i' => 8, 'j' => 9, 'k' => 10, 'l' => 11, + 'm' => 12, 'n' => 13, 'o' => 14, 'p' => 15, 'q' => 16, 'r' => 17, + 's' => 18, 't' => 19, 'u' => 20, 'v' => 21, 'w' => 22, 'x' => 23, + 'y' => 24, 'z' => 25, '0' => 26, '1' => 27, '2' => 28, '3' => 29, + '4' => 30, '5' => 31, '6' => 32, '7' => 33, '8' => 34, '9' => 35, + ); + + public static function idn_to_ascii($domain, $options, $variant, &$idna_info = array()) + { + if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) { + @trigger_error('idn_to_ascii(): INTL_IDNA_VARIANT_2003 is deprecated', E_USER_DEPRECATED); + } + + if (self::INTL_IDNA_VARIANT_UTS46 === $variant) { + $domain = mb_strtolower($domain, 'utf-8'); + } + + $parts = explode('.', $domain); + + foreach ($parts as $i => &$part) { + if ('' === $part && \count($parts) > 1 + $i) { + return false; + } + if (false === $part = self::encodePart($part)) { + return false; + } + } + + $output = implode('.', $parts); + + $idna_info = array( + 'result' => \strlen($output) > 255 ? false : $output, + 'isTransitionalDifferent' => false, + 'errors' => 0, + ); + + return $idna_info['result']; + } + + public static function idn_to_utf8($domain, $options, $variant, &$idna_info = array()) + { + if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) { + @trigger_error('idn_to_utf8(): INTL_IDNA_VARIANT_2003 is deprecated', E_USER_DEPRECATED); + } + + $parts = explode('.', $domain); + + foreach ($parts as &$part) { + $length = \strlen($part); + if ($length < 1 || 63 < $length) { + continue; + } + if (0 !== strpos($part, 'xn--')) { + continue; + } + + $part = substr($part, 4); + $part = self::decodePart($part); + } + + $output = implode('.', $parts); + + $idna_info = array( + 'result' => \strlen($output) > 255 ? false : $output, + 'isTransitionalDifferent' => false, + 'errors' => 0, + ); + + return $idna_info['result']; + } + + private static function encodePart($input) + { + $codePoints = self::listCodePoints($input); + + $n = 128; + $bias = 72; + $delta = 0; + $h = $b = \count($codePoints['basic']); + + $output = ''; + foreach ($codePoints['basic'] as $code) { + $output .= mb_chr($code, 'utf-8'); + } + if ($input === $output) { + return $output; + } + if ($b > 0) { + $output .= '-'; + } + + $codePoints['nonBasic'] = array_unique($codePoints['nonBasic']); + sort($codePoints['nonBasic']); + + $i = 0; + $length = mb_strlen($input, 'utf-8'); + while ($h < $length) { + $m = $codePoints['nonBasic'][$i++]; + $delta += ($m - $n) * ($h + 1); + $n = $m; + + foreach ($codePoints['all'] as $c) { + if ($c < $n || $c < 128) { + ++$delta; + } + if ($c === $n) { + $q = $delta; + for ($k = 36;; $k += 36) { + $t = self::calculateThreshold($k, $bias); + if ($q < $t) { + break; + } + + $code = $t + (($q - $t) % (36 - $t)); + $output .= self::$encodeTable[$code]; + + $q = ($q - $t) / (36 - $t); + } + + $output .= self::$encodeTable[$q]; + $bias = self::adapt($delta, $h + 1, ($h === $b)); + $delta = 0; + ++$h; + } + } + + ++$delta; + ++$n; + } + + $output = 'xn--'.$output; + + return \strlen($output) < 1 || 63 < \strlen($output) ? false : strtolower($output); + } + + private static function listCodePoints($input) + { + $codePoints = array( + 'all' => array(), + 'basic' => array(), + 'nonBasic' => array(), + ); + + $length = mb_strlen($input, 'utf-8'); + for ($i = 0; $i < $length; ++$i) { + $char = mb_substr($input, $i, 1, 'utf-8'); + $code = mb_ord($char, 'utf-8'); + if ($code < 128) { + $codePoints['all'][] = $codePoints['basic'][] = $code; + } else { + $codePoints['all'][] = $codePoints['nonBasic'][] = $code; + } + } + + return $codePoints; + } + + private static function calculateThreshold($k, $bias) + { + if ($k <= $bias + 1) { + return 1; + } + if ($k >= $bias + 26) { + return 26; + } + + return $k - $bias; + } + + private static function adapt($delta, $numPoints, $firstTime) + { + $delta = (int) ($firstTime ? $delta / 700 : $delta / 2); + $delta += (int) ($delta / $numPoints); + + $k = 0; + while ($delta > 35 * 13) { + $delta = (int) ($delta / 35); + $k = $k + 36; + } + + return $k + (int) (36 * $delta / ($delta + 38)); + } + + private static function decodePart($input) + { + $n = 128; + $i = 0; + $bias = 72; + $output = ''; + + $pos = strrpos($input, '-'); + if (false !== $pos) { + $output = substr($input, 0, $pos++); + } else { + $pos = 0; + } + + $outputLength = \strlen($output); + $inputLength = \strlen($input); + + while ($pos < $inputLength) { + $oldi = $i; + $w = 1; + + for ($k = 36;; $k += 36) { + $digit = self::$decodeTable[$input[$pos++]]; + $i += $digit * $w; + $t = self::calculateThreshold($k, $bias); + + if ($digit < $t) { + break; + } + + $w *= 36 - $t; + } + + $bias = self::adapt($i - $oldi, ++$outputLength, 0 === $oldi); + $n = $n + (int) ($i / $outputLength); + $i = $i % $outputLength; + $output = mb_substr($output, 0, $i, 'utf-8').mb_chr($n, 'utf-8').mb_substr($output, $i, $outputLength - 1, 'utf-8'); + + ++$i; + } + + return $output; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/LICENSE b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/LICENSE new file mode 100644 index 0000000..3f853aa --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2019 Fabien Potencier + +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/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/README.md b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/README.md new file mode 100644 index 0000000..2e75f2e --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/README.md @@ -0,0 +1,12 @@ +Symfony Polyfill / Intl: Idn +============================ + +This component provides [`idn_to_ascii`](https://php.net/idn-to-ascii) and [`idn_to_utf8`](https://php.net/idn-to-utf8) functions to users who run php versions without the [Intl](https://php.net/intl) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/bootstrap.php b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/bootstrap.php new file mode 100644 index 0000000..b29d4e5 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/bootstrap.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Idn as p; + +if (!defined('IDNA_DEFAULT')) { + define('U_IDNA_PROHIBITED_ERROR', 66560); + define('U_IDNA_ERROR_START', 66560); + define('U_IDNA_UNASSIGNED_ERROR', 66561); + define('U_IDNA_CHECK_BIDI_ERROR', 66562); + define('U_IDNA_STD3_ASCII_RULES_ERROR', 66563); + define('U_IDNA_ACE_PREFIX_ERROR', 66564); + define('U_IDNA_VERIFICATION_ERROR', 66565); + define('U_IDNA_LABEL_TOO_LONG_ERROR', 66566); + define('U_IDNA_ZERO_LENGTH_LABEL_ERROR', 66567); + define('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR', 66568); + define('U_IDNA_ERROR_LIMIT', 66569); + define('U_STRINGPREP_PROHIBITED_ERROR', 66560); + define('U_STRINGPREP_UNASSIGNED_ERROR', 66561); + define('U_STRINGPREP_CHECK_BIDI_ERROR', 66562); + define('IDNA_DEFAULT', 0); + define('IDNA_ALLOW_UNASSIGNED', 1); + define('IDNA_USE_STD3_RULES', 2); + define('IDNA_CHECK_BIDI', 4); + define('IDNA_CHECK_CONTEXTJ', 8); + define('IDNA_NONTRANSITIONAL_TO_ASCII', 16); + define('IDNA_NONTRANSITIONAL_TO_UNICODE', 32); + define('INTL_IDNA_VARIANT_2003', 0); + define('INTL_IDNA_VARIANT_UTS46', 1); + define('IDNA_ERROR_EMPTY_LABEL', 1); + define('IDNA_ERROR_LABEL_TOO_LONG', 2); + define('IDNA_ERROR_DOMAIN_NAME_TOO_LONG', 4); + define('IDNA_ERROR_LEADING_HYPHEN', 8); + define('IDNA_ERROR_TRAILING_HYPHEN', 16); + define('IDNA_ERROR_HYPHEN_3_4', 32); + define('IDNA_ERROR_LEADING_COMBINING_MARK', 64); + define('IDNA_ERROR_DISALLOWED', 128); + define('IDNA_ERROR_PUNYCODE', 256); + define('IDNA_ERROR_LABEL_HAS_DOT', 512); + define('IDNA_ERROR_INVALID_ACE_LABEL', 1024); + define('IDNA_ERROR_BIDI', 2048); + define('IDNA_ERROR_CONTEXTJ', 4096); +} + +if (!function_exists('idn_to_ascii')) { + if (PHP_VERSION_ID < 70400) { + function idn_to_ascii($domain, $options = IDNA_DEFAULT, $variant = INTL_IDNA_VARIANT_2003, &$idna_info = array()) { return p\Idn::idn_to_ascii($domain, $options, $variant, $idna_info); } + function idn_to_utf8($domain, $options = IDNA_DEFAULT, $variant = INTL_IDNA_VARIANT_2003, &$idna_info = array()) { return p\Idn::idn_to_utf8($domain, $options, $variant, $idna_info); } + } else { + function idn_to_ascii($domain, $options = IDNA_DEFAULT, $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info = array()) { return p\Idn::idn_to_ascii($domain, $options, $variant, $idna_info); } + function idn_to_utf8($domain, $options = IDNA_DEFAULT, $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info = array()) { return p\Idn::idn_to_utf8($domain, $options, $variant, $idna_info); } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/composer.json b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/composer.json new file mode 100644 index 0000000..ef2c006 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-intl-idn/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/polyfill-intl-idn", + "type": "library", + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "idn"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php72": "^1.10" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Intl\\Idn\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-intl": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/LICENSE b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/LICENSE new file mode 100644 index 0000000..4cd8bdd --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2019 Fabien Potencier + +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/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Mbstring.php b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Mbstring.php new file mode 100644 index 0000000..15503bc --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Mbstring.php @@ -0,0 +1,847 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Mbstring; + +/** + * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. + * + * Implemented: + * - mb_chr - Returns a specific character from its Unicode code point + * - mb_convert_encoding - Convert character encoding + * - mb_convert_variables - Convert character code in variable(s) + * - mb_decode_mimeheader - Decode string in MIME header field + * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED + * - mb_decode_numericentity - Decode HTML numeric string reference to character + * - mb_encode_numericentity - Encode character to HTML numeric string reference + * - mb_convert_case - Perform case folding on a string + * - mb_detect_encoding - Detect character encoding + * - mb_get_info - Get internal settings of mbstring + * - mb_http_input - Detect HTTP input character encoding + * - mb_http_output - Set/Get HTTP output character encoding + * - mb_internal_encoding - Set/Get internal character encoding + * - mb_list_encodings - Returns an array of all supported encodings + * - mb_ord - Returns the Unicode code point of a character + * - mb_output_handler - Callback function converts character encoding in output buffer + * - mb_scrub - Replaces ill-formed byte sequences with substitute characters + * - mb_strlen - Get string length + * - mb_strpos - Find position of first occurrence of string in a string + * - mb_strrpos - Find position of last occurrence of a string in a string + * - mb_str_split - Convert a string to an array + * - mb_strtolower - Make a string lowercase + * - mb_strtoupper - Make a string uppercase + * - mb_substitute_character - Set/Get substitution character + * - mb_substr - Get part of string + * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive + * - mb_stristr - Finds first occurrence of a string within another, case insensitive + * - mb_strrchr - Finds the last occurrence of a character in a string within another + * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive + * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive + * - mb_strstr - Finds first occurrence of a string within another + * - mb_strwidth - Return width of string + * - mb_substr_count - Count the number of substring occurrences + * + * Not implemented: + * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) + * - mb_ereg_* - Regular expression with multibyte support + * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable + * - mb_preferred_mime_name - Get MIME charset string + * - mb_regex_encoding - Returns current encoding for multibyte regex as string + * - mb_regex_set_options - Set/Get the default options for mbregex functions + * - mb_send_mail - Send encoded mail + * - mb_split - Split multibyte string using regular expression + * - mb_strcut - Get part of string + * - mb_strimwidth - Get truncated string with specified width + * + * @author Nicolas Grekas + * + * @internal + */ +final class Mbstring +{ + const MB_CASE_FOLD = PHP_INT_MAX; + + private static $encodingList = array('ASCII', 'UTF-8'); + private static $language = 'neutral'; + private static $internalEncoding = 'UTF-8'; + private static $caseFold = array( + array('µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"), + array('μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'), + ); + + public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) + { + if (\is_array($fromEncoding) || false !== strpos($fromEncoding, ',')) { + $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); + } else { + $fromEncoding = self::getEncoding($fromEncoding); + } + + $toEncoding = self::getEncoding($toEncoding); + + if ('BASE64' === $fromEncoding) { + $s = base64_decode($s); + $fromEncoding = $toEncoding; + } + + if ('BASE64' === $toEncoding) { + return base64_encode($s); + } + + if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { + if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { + $fromEncoding = 'Windows-1252'; + } + if ('UTF-8' !== $fromEncoding) { + $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); + } + + return preg_replace_callback('/[\x80-\xFF]+/', array(__CLASS__, 'html_encoding_callback'), $s); + } + + if ('HTML-ENTITIES' === $fromEncoding) { + $s = html_entity_decode($s, ENT_COMPAT, 'UTF-8'); + $fromEncoding = 'UTF-8'; + } + + return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); + } + + public static function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) + { + $vars = array(&$a, &$b, &$c, &$d, &$e, &$f); + + $ok = true; + array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { + if (false === $v = Mbstring::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { + $ok = false; + } + }); + + return $ok ? $fromEncoding : false; + } + + public static function mb_decode_mimeheader($s) + { + return iconv_mime_decode($s, 2, self::$internalEncoding); + } + + public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) + { + trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', E_USER_WARNING); + } + + public static function mb_decode_numericentity($s, $convmap, $encoding = null) + { + if (null !== $s && !\is_scalar($s) && !(\is_object($s) && \method_exists($s, '__toString'))) { + trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || !$convmap) { + return false; + } + + if (null !== $encoding && !\is_scalar($encoding)) { + trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', E_USER_WARNING); + + return ''; // Instead of null (cf. mb_encode_numericentity). + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $cnt = floor(\count($convmap) / 4) * 4; + + for ($i = 0; $i < $cnt; $i += 4) { + // collector_decode_htmlnumericentity ignores $convmap[$i + 3] + $convmap[$i] += $convmap[$i + 2]; + $convmap[$i + 1] += $convmap[$i + 2]; + } + + $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { + $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; + for ($i = 0; $i < $cnt; $i += 4) { + if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { + return Mbstring::mb_chr($c - $convmap[$i + 2]); + } + } + + return $m[0]; + }, $s); + + if (null === $encoding) { + return $s; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) + { + if (null !== $s && !\is_scalar($s) && !(\is_object($s) && \method_exists($s, '__toString'))) { + trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', E_USER_WARNING); + + return null; + } + + if (!\is_array($convmap) || !$convmap) { + return false; + } + + if (null !== $encoding && !\is_scalar($encoding)) { + trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', E_USER_WARNING); + + return null; // Instead of '' (cf. mb_decode_numericentity). + } + + if (null !== $is_hex && !\is_scalar($is_hex)) { + trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', E_USER_WARNING); + + return null; + } + + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); + + $cnt = floor(\count($convmap) / 4) * 4; + $i = 0; + $len = \strlen($s); + $result = ''; + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + $c = self::mb_ord($uchr); + + for ($j = 0; $j < $cnt; $j += 4) { + if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { + $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; + $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; + continue 2; + } + } + $result .= $uchr; + } + + if (null === $encoding) { + return $result; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $result); + } + + public static function mb_convert_case($s, $mode, $encoding = null) + { + $s = (string) $s; + if ('' === $s) { + return ''; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding) { + $encoding = null; + if (!preg_match('//u', $s)) { + $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); + } + } else { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + if (MB_CASE_TITLE == $mode) { + static $titleRegexp = null; + if (null === $titleRegexp) { + $titleRegexp = self::getData('titleCaseRegexp'); + } + $s = preg_replace_callback($titleRegexp, array(__CLASS__, 'title_case'), $s); + } else { + if (MB_CASE_UPPER == $mode) { + static $upper = null; + if (null === $upper) { + $upper = self::getData('upperCase'); + } + $map = $upper; + } else { + if (self::MB_CASE_FOLD === $mode) { + $s = str_replace(self::$caseFold[0], self::$caseFold[1], $s); + } + + static $lower = null; + if (null === $lower) { + $lower = self::getData('lowerCase'); + } + $map = $lower; + } + + static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); + + $i = 0; + $len = \strlen($s); + + while ($i < $len) { + $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + + if (isset($map[$uchr])) { + $uchr = $map[$uchr]; + $nlen = \strlen($uchr); + + if ($nlen == $ulen) { + $nlen = $i; + do { + $s[--$nlen] = $uchr[--$ulen]; + } while ($ulen); + } else { + $s = substr_replace($s, $uchr, $i - $ulen, $ulen); + $len += $nlen - $ulen; + $i += $nlen - $ulen; + } + } + } + } + + if (null === $encoding) { + return $s; + } + + return iconv('UTF-8', $encoding.'//IGNORE', $s); + } + + public static function mb_internal_encoding($encoding = null) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + $encoding = self::getEncoding($encoding); + + if ('UTF-8' === $encoding || false !== @iconv($encoding, $encoding, ' ')) { + self::$internalEncoding = $encoding; + + return true; + } + + return false; + } + + public static function mb_language($lang = null) + { + if (null === $lang) { + return self::$language; + } + + switch ($lang = strtolower($lang)) { + case 'uni': + case 'neutral': + self::$language = $lang; + + return true; + } + + return false; + } + + public static function mb_list_encodings() + { + return array('UTF-8'); + } + + public static function mb_encoding_aliases($encoding) + { + switch (strtoupper($encoding)) { + case 'UTF8': + case 'UTF-8': + return array('utf8'); + } + + return false; + } + + public static function mb_check_encoding($var = null, $encoding = null) + { + if (null === $encoding) { + if (null === $var) { + return false; + } + $encoding = self::$internalEncoding; + } + + return self::mb_detect_encoding($var, array($encoding)) || false !== @iconv($encoding, $encoding, $var); + } + + public static function mb_detect_encoding($str, $encodingList = null, $strict = false) + { + if (null === $encodingList) { + $encodingList = self::$encodingList; + } else { + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + } + + foreach ($encodingList as $enc) { + switch ($enc) { + case 'ASCII': + if (!preg_match('/[\x80-\xFF]/', $str)) { + return $enc; + } + break; + + case 'UTF8': + case 'UTF-8': + if (preg_match('//u', $str)) { + return 'UTF-8'; + } + break; + + default: + if (0 === strncmp($enc, 'ISO-8859-', 9)) { + return $enc; + } + } + } + + return false; + } + + public static function mb_detect_order($encodingList = null) + { + if (null === $encodingList) { + return self::$encodingList; + } + + if (!\is_array($encodingList)) { + $encodingList = array_map('trim', explode(',', $encodingList)); + } + $encodingList = array_map('strtoupper', $encodingList); + + foreach ($encodingList as $enc) { + switch ($enc) { + default: + if (strncmp($enc, 'ISO-8859-', 9)) { + return false; + } + // no break + case 'ASCII': + case 'UTF8': + case 'UTF-8': + } + } + + self::$encodingList = $encodingList; + + return true; + } + + public static function mb_strlen($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return \strlen($s); + } + + return @iconv_strlen($s, $encoding); + } + + public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strpos($haystack, $needle, $offset); + } + + $needle = (string) $needle; + if ('' === $needle) { + trigger_error(__METHOD__.': Empty delimiter', E_USER_WARNING); + + return false; + } + + return iconv_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strrpos($haystack, $needle, $offset); + } + + if ($offset != (int) $offset) { + $offset = 0; + } elseif ($offset = (int) $offset) { + if ($offset < 0) { + if (0 > $offset += self::mb_strlen($needle)) { + $haystack = self::mb_substr($haystack, 0, $offset, $encoding); + } + $offset = 0; + } else { + $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); + } + } + + $pos = iconv_strrpos($haystack, $needle, $encoding); + + return false !== $pos ? $offset + $pos : false; + } + + public static function mb_str_split($string, $split_length = 1, $encoding = null) + { + if (null !== $string && !\is_scalar($string) && !(\is_object($string) && \method_exists($string, '__toString'))) { + trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', E_USER_WARNING); + + return null; + } + + if (1 > $split_length = (int) $split_length) { + trigger_error('The length of each segment must be greater than zero', E_USER_WARNING); + + return false; + } + + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + if ('UTF-8' === $encoding = self::getEncoding($encoding)) { + $rx = '/('; + while (65535 < $split_length) { + $rx .= '.{65535}'; + $split_length -= 65535; + } + $rx .= '.{'.$split_length.'})/us'; + + return preg_split($rx, $string, null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + } + + $result = array(); + $length = mb_strlen($string, $encoding); + + for ($i = 0; $i < $length; $i += $split_length) { + $result[] = mb_substr($string, $i, $split_length, $encoding); + } + + return $result; + } + + public static function mb_strtolower($s, $encoding = null) + { + return self::mb_convert_case($s, MB_CASE_LOWER, $encoding); + } + + public static function mb_strtoupper($s, $encoding = null) + { + return self::mb_convert_case($s, MB_CASE_UPPER, $encoding); + } + + public static function mb_substitute_character($c = null) + { + if (0 === strcasecmp($c, 'none')) { + return true; + } + + return null !== $c ? false : 'none'; + } + + public static function mb_substr($s, $start, $length = null, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return (string) substr($s, $start, null === $length ? 2147483647 : $length); + } + + if ($start < 0) { + $start = iconv_strlen($s, $encoding) + $start; + if ($start < 0) { + $start = 0; + } + } + + if (null === $length) { + $length = 2147483647; + } elseif ($length < 0) { + $length = iconv_strlen($s, $encoding) + $length - $start; + if ($length < 0) { + return ''; + } + } + + return (string) iconv_substr($s, $start, $length, $encoding); + } + + public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) + { + $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); + $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); + + return self::mb_strpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) + { + $pos = self::mb_stripos($haystack, $needle, 0, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) + { + $encoding = self::getEncoding($encoding); + if ('CP850' === $encoding || 'ASCII' === $encoding) { + return strrchr($haystack, $needle, $part); + } + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = iconv_strrpos($haystack, $needle, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) + { + $needle = self::mb_substr($needle, 0, 1, $encoding); + $pos = self::mb_strripos($haystack, $needle, $encoding); + + return self::getSubpart($pos, $part, $haystack, $encoding); + } + + public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) + { + $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); + $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); + + return self::mb_strrpos($haystack, $needle, $offset, $encoding); + } + + public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) + { + $pos = strpos($haystack, $needle); + if (false === $pos) { + return false; + } + if ($part) { + return substr($haystack, 0, $pos); + } + + return substr($haystack, $pos); + } + + public static function mb_get_info($type = 'all') + { + $info = array( + 'internal_encoding' => self::$internalEncoding, + 'http_output' => 'pass', + 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', + 'func_overload' => 0, + 'func_overload_list' => 'no overload', + 'mail_charset' => 'UTF-8', + 'mail_header_encoding' => 'BASE64', + 'mail_body_encoding' => 'BASE64', + 'illegal_chars' => 0, + 'encoding_translation' => 'Off', + 'language' => self::$language, + 'detect_order' => self::$encodingList, + 'substitute_character' => 'none', + 'strict_detection' => 'Off', + ); + + if ('all' === $type) { + return $info; + } + if (isset($info[$type])) { + return $info[$type]; + } + + return false; + } + + public static function mb_http_input($type = '') + { + return false; + } + + public static function mb_http_output($encoding = null) + { + return null !== $encoding ? 'pass' === $encoding : 'pass'; + } + + public static function mb_strwidth($s, $encoding = null) + { + $encoding = self::getEncoding($encoding); + + if ('UTF-8' !== $encoding) { + $s = iconv($encoding, 'UTF-8//IGNORE', $s); + } + + $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); + + return ($wide << 1) + iconv_strlen($s, 'UTF-8'); + } + + public static function mb_substr_count($haystack, $needle, $encoding = null) + { + return substr_count($haystack, $needle); + } + + public static function mb_output_handler($contents, $status) + { + return $contents; + } + + public static function mb_chr($code, $encoding = null) + { + if (0x80 > $code %= 0x200000) { + $s = \chr($code); + } elseif (0x800 > $code) { + $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } else { + $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } + + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, $encoding, 'UTF-8'); + } + + return $s; + } + + public static function mb_ord($s, $encoding = null) + { + if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { + $s = mb_convert_encoding($s, 'UTF-8', $encoding); + } + + if (1 === \strlen($s)) { + return \ord($s); + } + + $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; + if (0xF0 <= $code) { + return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; + } + if (0xE0 <= $code) { + return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; + } + if (0xC0 <= $code) { + return (($code - 0xC0) << 6) + $s[2] - 0x80; + } + + return $code; + } + + private static function getSubpart($pos, $part, $haystack, $encoding) + { + if (false === $pos) { + return false; + } + if ($part) { + return self::mb_substr($haystack, 0, $pos, $encoding); + } + + return self::mb_substr($haystack, $pos, null, $encoding); + } + + private static function html_encoding_callback(array $m) + { + $i = 1; + $entities = ''; + $m = unpack('C*', htmlentities($m[0], ENT_COMPAT, 'UTF-8')); + + while (isset($m[$i])) { + if (0x80 > $m[$i]) { + $entities .= \chr($m[$i++]); + continue; + } + if (0xF0 <= $m[$i]) { + $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } elseif (0xE0 <= $m[$i]) { + $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; + } else { + $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; + } + + $entities .= '&#'.$c.';'; + } + + return $entities; + } + + private static function title_case(array $s) + { + return self::mb_convert_case($s[1], MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], MB_CASE_LOWER, 'UTF-8'); + } + + private static function getData($file) + { + if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { + return require $file; + } + + return false; + } + + private static function getEncoding($encoding) + { + if (null === $encoding) { + return self::$internalEncoding; + } + + if ('UTF-8' === $encoding) { + return 'UTF-8'; + } + + $encoding = strtoupper($encoding); + + if ('8BIT' === $encoding || 'BINARY' === $encoding) { + return 'CP850'; + } + + if ('UTF8' === $encoding) { + return 'UTF-8'; + } + + return $encoding; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/README.md b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/README.md new file mode 100644 index 0000000..4efb599 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/README.md @@ -0,0 +1,13 @@ +Symfony Polyfill / Mbstring +=========================== + +This component provides a partial, native PHP implementation for the +[Mbstring](https://php.net/mbstring) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php new file mode 100644 index 0000000..e6fbfa6 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php @@ -0,0 +1,1096 @@ + 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + 'À' => 'à', + 'Á' => 'á', + 'Â' => 'â', + 'Ã' => 'ã', + 'Ä' => 'ä', + 'Å' => 'å', + 'Æ' => 'æ', + 'Ç' => 'ç', + 'È' => 'è', + 'É' => 'é', + 'Ê' => 'ê', + 'Ë' => 'ë', + 'Ì' => 'ì', + 'Í' => 'í', + 'Î' => 'î', + 'Ï' => 'ï', + 'Ð' => 'ð', + 'Ñ' => 'ñ', + 'Ò' => 'ò', + 'Ó' => 'ó', + 'Ô' => 'ô', + 'Õ' => 'õ', + 'Ö' => 'ö', + 'Ø' => 'ø', + 'Ù' => 'ù', + 'Ú' => 'ú', + 'Û' => 'û', + 'Ü' => 'ü', + 'Ý' => 'ý', + 'Þ' => 'þ', + 'Ā' => 'ā', + 'Ă' => 'ă', + 'Ą' => 'ą', + 'Ć' => 'ć', + 'Ĉ' => 'ĉ', + 'Ċ' => 'ċ', + 'Č' => 'č', + 'Ď' => 'ď', + 'Đ' => 'đ', + 'Ē' => 'ē', + 'Ĕ' => 'ĕ', + 'Ė' => 'ė', + 'Ę' => 'ę', + 'Ě' => 'ě', + 'Ĝ' => 'ĝ', + 'Ğ' => 'ğ', + 'Ġ' => 'ġ', + 'Ģ' => 'ģ', + 'Ĥ' => 'ĥ', + 'Ħ' => 'ħ', + 'Ĩ' => 'ĩ', + 'Ī' => 'ī', + 'Ĭ' => 'ĭ', + 'Į' => 'į', + 'İ' => 'i', + 'IJ' => 'ij', + 'Ĵ' => 'ĵ', + 'Ķ' => 'ķ', + 'Ĺ' => 'ĺ', + 'Ļ' => 'ļ', + 'Ľ' => 'ľ', + 'Ŀ' => 'ŀ', + 'Ł' => 'ł', + 'Ń' => 'ń', + 'Ņ' => 'ņ', + 'Ň' => 'ň', + 'Ŋ' => 'ŋ', + 'Ō' => 'ō', + 'Ŏ' => 'ŏ', + 'Ő' => 'ő', + 'Œ' => 'œ', + 'Ŕ' => 'ŕ', + 'Ŗ' => 'ŗ', + 'Ř' => 'ř', + 'Ś' => 'ś', + 'Ŝ' => 'ŝ', + 'Ş' => 'ş', + 'Š' => 'š', + 'Ţ' => 'ţ', + 'Ť' => 'ť', + 'Ŧ' => 'ŧ', + 'Ũ' => 'ũ', + 'Ū' => 'ū', + 'Ŭ' => 'ŭ', + 'Ů' => 'ů', + 'Ű' => 'ű', + 'Ų' => 'ų', + 'Ŵ' => 'ŵ', + 'Ŷ' => 'ŷ', + 'Ÿ' => 'ÿ', + 'Ź' => 'ź', + 'Ż' => 'ż', + 'Ž' => 'ž', + 'Ɓ' => 'ɓ', + 'Ƃ' => 'ƃ', + 'Ƅ' => 'ƅ', + 'Ɔ' => 'ɔ', + 'Ƈ' => 'ƈ', + 'Ɖ' => 'ɖ', + 'Ɗ' => 'ɗ', + 'Ƌ' => 'ƌ', + 'Ǝ' => 'ǝ', + 'Ə' => 'ə', + 'Ɛ' => 'ɛ', + 'Ƒ' => 'ƒ', + 'Ɠ' => 'ɠ', + 'Ɣ' => 'ɣ', + 'Ɩ' => 'ɩ', + 'Ɨ' => 'ɨ', + 'Ƙ' => 'ƙ', + 'Ɯ' => 'ɯ', + 'Ɲ' => 'ɲ', + 'Ɵ' => 'ɵ', + 'Ơ' => 'ơ', + 'Ƣ' => 'ƣ', + 'Ƥ' => 'ƥ', + 'Ʀ' => 'ʀ', + 'Ƨ' => 'ƨ', + 'Ʃ' => 'ʃ', + 'Ƭ' => 'ƭ', + 'Ʈ' => 'ʈ', + 'Ư' => 'ư', + 'Ʊ' => 'ʊ', + 'Ʋ' => 'ʋ', + 'Ƴ' => 'ƴ', + 'Ƶ' => 'ƶ', + 'Ʒ' => 'ʒ', + 'Ƹ' => 'ƹ', + 'Ƽ' => 'ƽ', + 'DŽ' => 'dž', + 'Dž' => 'dž', + 'LJ' => 'lj', + 'Lj' => 'lj', + 'NJ' => 'nj', + 'Nj' => 'nj', + 'Ǎ' => 'ǎ', + 'Ǐ' => 'ǐ', + 'Ǒ' => 'ǒ', + 'Ǔ' => 'ǔ', + 'Ǖ' => 'ǖ', + 'Ǘ' => 'ǘ', + 'Ǚ' => 'ǚ', + 'Ǜ' => 'ǜ', + 'Ǟ' => 'ǟ', + 'Ǡ' => 'ǡ', + 'Ǣ' => 'ǣ', + 'Ǥ' => 'ǥ', + 'Ǧ' => 'ǧ', + 'Ǩ' => 'ǩ', + 'Ǫ' => 'ǫ', + 'Ǭ' => 'ǭ', + 'Ǯ' => 'ǯ', + 'DZ' => 'dz', + 'Dz' => 'dz', + 'Ǵ' => 'ǵ', + 'Ƕ' => 'ƕ', + 'Ƿ' => 'ƿ', + 'Ǹ' => 'ǹ', + 'Ǻ' => 'ǻ', + 'Ǽ' => 'ǽ', + 'Ǿ' => 'ǿ', + 'Ȁ' => 'ȁ', + 'Ȃ' => 'ȃ', + 'Ȅ' => 'ȅ', + 'Ȇ' => 'ȇ', + 'Ȉ' => 'ȉ', + 'Ȋ' => 'ȋ', + 'Ȍ' => 'ȍ', + 'Ȏ' => 'ȏ', + 'Ȑ' => 'ȑ', + 'Ȓ' => 'ȓ', + 'Ȕ' => 'ȕ', + 'Ȗ' => 'ȗ', + 'Ș' => 'ș', + 'Ț' => 'ț', + 'Ȝ' => 'ȝ', + 'Ȟ' => 'ȟ', + 'Ƞ' => 'ƞ', + 'Ȣ' => 'ȣ', + 'Ȥ' => 'ȥ', + 'Ȧ' => 'ȧ', + 'Ȩ' => 'ȩ', + 'Ȫ' => 'ȫ', + 'Ȭ' => 'ȭ', + 'Ȯ' => 'ȯ', + 'Ȱ' => 'ȱ', + 'Ȳ' => 'ȳ', + 'Ⱥ' => 'ⱥ', + 'Ȼ' => 'ȼ', + 'Ƚ' => 'ƚ', + 'Ⱦ' => 'ⱦ', + 'Ɂ' => 'ɂ', + 'Ƀ' => 'ƀ', + 'Ʉ' => 'ʉ', + 'Ʌ' => 'ʌ', + 'Ɇ' => 'ɇ', + 'Ɉ' => 'ɉ', + 'Ɋ' => 'ɋ', + 'Ɍ' => 'ɍ', + 'Ɏ' => 'ɏ', + 'Ͱ' => 'ͱ', + 'Ͳ' => 'ͳ', + 'Ͷ' => 'ͷ', + 'Ϳ' => 'ϳ', + 'Ά' => 'ά', + 'Έ' => 'έ', + 'Ή' => 'ή', + 'Ί' => 'ί', + 'Ό' => 'ό', + 'Ύ' => 'ύ', + 'Ώ' => 'ώ', + 'Α' => 'α', + 'Β' => 'β', + 'Γ' => 'γ', + 'Δ' => 'δ', + 'Ε' => 'ε', + 'Ζ' => 'ζ', + 'Η' => 'η', + 'Θ' => 'θ', + 'Ι' => 'ι', + 'Κ' => 'κ', + 'Λ' => 'λ', + 'Μ' => 'μ', + 'Ν' => 'ν', + 'Ξ' => 'ξ', + 'Ο' => 'ο', + 'Π' => 'π', + 'Ρ' => 'ρ', + 'Σ' => 'σ', + 'Τ' => 'τ', + 'Υ' => 'υ', + 'Φ' => 'φ', + 'Χ' => 'χ', + 'Ψ' => 'ψ', + 'Ω' => 'ω', + 'Ϊ' => 'ϊ', + 'Ϋ' => 'ϋ', + 'Ϗ' => 'ϗ', + 'Ϙ' => 'ϙ', + 'Ϛ' => 'ϛ', + 'Ϝ' => 'ϝ', + 'Ϟ' => 'ϟ', + 'Ϡ' => 'ϡ', + 'Ϣ' => 'ϣ', + 'Ϥ' => 'ϥ', + 'Ϧ' => 'ϧ', + 'Ϩ' => 'ϩ', + 'Ϫ' => 'ϫ', + 'Ϭ' => 'ϭ', + 'Ϯ' => 'ϯ', + 'ϴ' => 'θ', + 'Ϸ' => 'ϸ', + 'Ϲ' => 'ϲ', + 'Ϻ' => 'ϻ', + 'Ͻ' => 'ͻ', + 'Ͼ' => 'ͼ', + 'Ͽ' => 'ͽ', + 'Ѐ' => 'ѐ', + 'Ё' => 'ё', + 'Ђ' => 'ђ', + 'Ѓ' => 'ѓ', + 'Є' => 'є', + 'Ѕ' => 'ѕ', + 'І' => 'і', + 'Ї' => 'ї', + 'Ј' => 'ј', + 'Љ' => 'љ', + 'Њ' => 'њ', + 'Ћ' => 'ћ', + 'Ќ' => 'ќ', + 'Ѝ' => 'ѝ', + 'Ў' => 'ў', + 'Џ' => 'џ', + 'А' => 'а', + 'Б' => 'б', + 'В' => 'в', + 'Г' => 'г', + 'Д' => 'д', + 'Е' => 'е', + 'Ж' => 'ж', + 'З' => 'з', + 'И' => 'и', + 'Й' => 'й', + 'К' => 'к', + 'Л' => 'л', + 'М' => 'м', + 'Н' => 'н', + 'О' => 'о', + 'П' => 'п', + 'Р' => 'р', + 'С' => 'с', + 'Т' => 'т', + 'У' => 'у', + 'Ф' => 'ф', + 'Х' => 'х', + 'Ц' => 'ц', + 'Ч' => 'ч', + 'Ш' => 'ш', + 'Щ' => 'щ', + 'Ъ' => 'ъ', + 'Ы' => 'ы', + 'Ь' => 'ь', + 'Э' => 'э', + 'Ю' => 'ю', + 'Я' => 'я', + 'Ѡ' => 'ѡ', + 'Ѣ' => 'ѣ', + 'Ѥ' => 'ѥ', + 'Ѧ' => 'ѧ', + 'Ѩ' => 'ѩ', + 'Ѫ' => 'ѫ', + 'Ѭ' => 'ѭ', + 'Ѯ' => 'ѯ', + 'Ѱ' => 'ѱ', + 'Ѳ' => 'ѳ', + 'Ѵ' => 'ѵ', + 'Ѷ' => 'ѷ', + 'Ѹ' => 'ѹ', + 'Ѻ' => 'ѻ', + 'Ѽ' => 'ѽ', + 'Ѿ' => 'ѿ', + 'Ҁ' => 'ҁ', + 'Ҋ' => 'ҋ', + 'Ҍ' => 'ҍ', + 'Ҏ' => 'ҏ', + 'Ґ' => 'ґ', + 'Ғ' => 'ғ', + 'Ҕ' => 'ҕ', + 'Җ' => 'җ', + 'Ҙ' => 'ҙ', + 'Қ' => 'қ', + 'Ҝ' => 'ҝ', + 'Ҟ' => 'ҟ', + 'Ҡ' => 'ҡ', + 'Ң' => 'ң', + 'Ҥ' => 'ҥ', + 'Ҧ' => 'ҧ', + 'Ҩ' => 'ҩ', + 'Ҫ' => 'ҫ', + 'Ҭ' => 'ҭ', + 'Ү' => 'ү', + 'Ұ' => 'ұ', + 'Ҳ' => 'ҳ', + 'Ҵ' => 'ҵ', + 'Ҷ' => 'ҷ', + 'Ҹ' => 'ҹ', + 'Һ' => 'һ', + 'Ҽ' => 'ҽ', + 'Ҿ' => 'ҿ', + 'Ӏ' => 'ӏ', + 'Ӂ' => 'ӂ', + 'Ӄ' => 'ӄ', + 'Ӆ' => 'ӆ', + 'Ӈ' => 'ӈ', + 'Ӊ' => 'ӊ', + 'Ӌ' => 'ӌ', + 'Ӎ' => 'ӎ', + 'Ӑ' => 'ӑ', + 'Ӓ' => 'ӓ', + 'Ӕ' => 'ӕ', + 'Ӗ' => 'ӗ', + 'Ә' => 'ә', + 'Ӛ' => 'ӛ', + 'Ӝ' => 'ӝ', + 'Ӟ' => 'ӟ', + 'Ӡ' => 'ӡ', + 'Ӣ' => 'ӣ', + 'Ӥ' => 'ӥ', + 'Ӧ' => 'ӧ', + 'Ө' => 'ө', + 'Ӫ' => 'ӫ', + 'Ӭ' => 'ӭ', + 'Ӯ' => 'ӯ', + 'Ӱ' => 'ӱ', + 'Ӳ' => 'ӳ', + 'Ӵ' => 'ӵ', + 'Ӷ' => 'ӷ', + 'Ӹ' => 'ӹ', + 'Ӻ' => 'ӻ', + 'Ӽ' => 'ӽ', + 'Ӿ' => 'ӿ', + 'Ԁ' => 'ԁ', + 'Ԃ' => 'ԃ', + 'Ԅ' => 'ԅ', + 'Ԇ' => 'ԇ', + 'Ԉ' => 'ԉ', + 'Ԋ' => 'ԋ', + 'Ԍ' => 'ԍ', + 'Ԏ' => 'ԏ', + 'Ԑ' => 'ԑ', + 'Ԓ' => 'ԓ', + 'Ԕ' => 'ԕ', + 'Ԗ' => 'ԗ', + 'Ԙ' => 'ԙ', + 'Ԛ' => 'ԛ', + 'Ԝ' => 'ԝ', + 'Ԟ' => 'ԟ', + 'Ԡ' => 'ԡ', + 'Ԣ' => 'ԣ', + 'Ԥ' => 'ԥ', + 'Ԧ' => 'ԧ', + 'Ԩ' => 'ԩ', + 'Ԫ' => 'ԫ', + 'Ԭ' => 'ԭ', + 'Ԯ' => 'ԯ', + 'Ա' => 'ա', + 'Բ' => 'բ', + 'Գ' => 'գ', + 'Դ' => 'դ', + 'Ե' => 'ե', + 'Զ' => 'զ', + 'Է' => 'է', + 'Ը' => 'ը', + 'Թ' => 'թ', + 'Ժ' => 'ժ', + 'Ի' => 'ի', + 'Լ' => 'լ', + 'Խ' => 'խ', + 'Ծ' => 'ծ', + 'Կ' => 'կ', + 'Հ' => 'հ', + 'Ձ' => 'ձ', + 'Ղ' => 'ղ', + 'Ճ' => 'ճ', + 'Մ' => 'մ', + 'Յ' => 'յ', + 'Ն' => 'ն', + 'Շ' => 'շ', + 'Ո' => 'ո', + 'Չ' => 'չ', + 'Պ' => 'պ', + 'Ջ' => 'ջ', + 'Ռ' => 'ռ', + 'Ս' => 'ս', + 'Վ' => 'վ', + 'Տ' => 'տ', + 'Ր' => 'ր', + 'Ց' => 'ց', + 'Ւ' => 'ւ', + 'Փ' => 'փ', + 'Ք' => 'ք', + 'Օ' => 'օ', + 'Ֆ' => 'ֆ', + 'Ⴀ' => 'ⴀ', + 'Ⴁ' => 'ⴁ', + 'Ⴂ' => 'ⴂ', + 'Ⴃ' => 'ⴃ', + 'Ⴄ' => 'ⴄ', + 'Ⴅ' => 'ⴅ', + 'Ⴆ' => 'ⴆ', + 'Ⴇ' => 'ⴇ', + 'Ⴈ' => 'ⴈ', + 'Ⴉ' => 'ⴉ', + 'Ⴊ' => 'ⴊ', + 'Ⴋ' => 'ⴋ', + 'Ⴌ' => 'ⴌ', + 'Ⴍ' => 'ⴍ', + 'Ⴎ' => 'ⴎ', + 'Ⴏ' => 'ⴏ', + 'Ⴐ' => 'ⴐ', + 'Ⴑ' => 'ⴑ', + 'Ⴒ' => 'ⴒ', + 'Ⴓ' => 'ⴓ', + 'Ⴔ' => 'ⴔ', + 'Ⴕ' => 'ⴕ', + 'Ⴖ' => 'ⴖ', + 'Ⴗ' => 'ⴗ', + 'Ⴘ' => 'ⴘ', + 'Ⴙ' => 'ⴙ', + 'Ⴚ' => 'ⴚ', + 'Ⴛ' => 'ⴛ', + 'Ⴜ' => 'ⴜ', + 'Ⴝ' => 'ⴝ', + 'Ⴞ' => 'ⴞ', + 'Ⴟ' => 'ⴟ', + 'Ⴠ' => 'ⴠ', + 'Ⴡ' => 'ⴡ', + 'Ⴢ' => 'ⴢ', + 'Ⴣ' => 'ⴣ', + 'Ⴤ' => 'ⴤ', + 'Ⴥ' => 'ⴥ', + 'Ⴧ' => 'ⴧ', + 'Ⴭ' => 'ⴭ', + 'Ḁ' => 'ḁ', + 'Ḃ' => 'ḃ', + 'Ḅ' => 'ḅ', + 'Ḇ' => 'ḇ', + 'Ḉ' => 'ḉ', + 'Ḋ' => 'ḋ', + 'Ḍ' => 'ḍ', + 'Ḏ' => 'ḏ', + 'Ḑ' => 'ḑ', + 'Ḓ' => 'ḓ', + 'Ḕ' => 'ḕ', + 'Ḗ' => 'ḗ', + 'Ḙ' => 'ḙ', + 'Ḛ' => 'ḛ', + 'Ḝ' => 'ḝ', + 'Ḟ' => 'ḟ', + 'Ḡ' => 'ḡ', + 'Ḣ' => 'ḣ', + 'Ḥ' => 'ḥ', + 'Ḧ' => 'ḧ', + 'Ḩ' => 'ḩ', + 'Ḫ' => 'ḫ', + 'Ḭ' => 'ḭ', + 'Ḯ' => 'ḯ', + 'Ḱ' => 'ḱ', + 'Ḳ' => 'ḳ', + 'Ḵ' => 'ḵ', + 'Ḷ' => 'ḷ', + 'Ḹ' => 'ḹ', + 'Ḻ' => 'ḻ', + 'Ḽ' => 'ḽ', + 'Ḿ' => 'ḿ', + 'Ṁ' => 'ṁ', + 'Ṃ' => 'ṃ', + 'Ṅ' => 'ṅ', + 'Ṇ' => 'ṇ', + 'Ṉ' => 'ṉ', + 'Ṋ' => 'ṋ', + 'Ṍ' => 'ṍ', + 'Ṏ' => 'ṏ', + 'Ṑ' => 'ṑ', + 'Ṓ' => 'ṓ', + 'Ṕ' => 'ṕ', + 'Ṗ' => 'ṗ', + 'Ṙ' => 'ṙ', + 'Ṛ' => 'ṛ', + 'Ṝ' => 'ṝ', + 'Ṟ' => 'ṟ', + 'Ṡ' => 'ṡ', + 'Ṣ' => 'ṣ', + 'Ṥ' => 'ṥ', + 'Ṧ' => 'ṧ', + 'Ṩ' => 'ṩ', + 'Ṫ' => 'ṫ', + 'Ṭ' => 'ṭ', + 'Ṯ' => 'ṯ', + 'Ṱ' => 'ṱ', + 'Ṳ' => 'ṳ', + 'Ṵ' => 'ṵ', + 'Ṷ' => 'ṷ', + 'Ṹ' => 'ṹ', + 'Ṻ' => 'ṻ', + 'Ṽ' => 'ṽ', + 'Ṿ' => 'ṿ', + 'Ẁ' => 'ẁ', + 'Ẃ' => 'ẃ', + 'Ẅ' => 'ẅ', + 'Ẇ' => 'ẇ', + 'Ẉ' => 'ẉ', + 'Ẋ' => 'ẋ', + 'Ẍ' => 'ẍ', + 'Ẏ' => 'ẏ', + 'Ẑ' => 'ẑ', + 'Ẓ' => 'ẓ', + 'Ẕ' => 'ẕ', + 'ẞ' => 'ß', + 'Ạ' => 'ạ', + 'Ả' => 'ả', + 'Ấ' => 'ấ', + 'Ầ' => 'ầ', + 'Ẩ' => 'ẩ', + 'Ẫ' => 'ẫ', + 'Ậ' => 'ậ', + 'Ắ' => 'ắ', + 'Ằ' => 'ằ', + 'Ẳ' => 'ẳ', + 'Ẵ' => 'ẵ', + 'Ặ' => 'ặ', + 'Ẹ' => 'ẹ', + 'Ẻ' => 'ẻ', + 'Ẽ' => 'ẽ', + 'Ế' => 'ế', + 'Ề' => 'ề', + 'Ể' => 'ể', + 'Ễ' => 'ễ', + 'Ệ' => 'ệ', + 'Ỉ' => 'ỉ', + 'Ị' => 'ị', + 'Ọ' => 'ọ', + 'Ỏ' => 'ỏ', + 'Ố' => 'ố', + 'Ồ' => 'ồ', + 'Ổ' => 'ổ', + 'Ỗ' => 'ỗ', + 'Ộ' => 'ộ', + 'Ớ' => 'ớ', + 'Ờ' => 'ờ', + 'Ở' => 'ở', + 'Ỡ' => 'ỡ', + 'Ợ' => 'ợ', + 'Ụ' => 'ụ', + 'Ủ' => 'ủ', + 'Ứ' => 'ứ', + 'Ừ' => 'ừ', + 'Ử' => 'ử', + 'Ữ' => 'ữ', + 'Ự' => 'ự', + 'Ỳ' => 'ỳ', + 'Ỵ' => 'ỵ', + 'Ỷ' => 'ỷ', + 'Ỹ' => 'ỹ', + 'Ỻ' => 'ỻ', + 'Ỽ' => 'ỽ', + 'Ỿ' => 'ỿ', + 'Ἀ' => 'ἀ', + 'Ἁ' => 'ἁ', + 'Ἂ' => 'ἂ', + 'Ἃ' => 'ἃ', + 'Ἄ' => 'ἄ', + 'Ἅ' => 'ἅ', + 'Ἆ' => 'ἆ', + 'Ἇ' => 'ἇ', + 'Ἐ' => 'ἐ', + 'Ἑ' => 'ἑ', + 'Ἒ' => 'ἒ', + 'Ἓ' => 'ἓ', + 'Ἔ' => 'ἔ', + 'Ἕ' => 'ἕ', + 'Ἠ' => 'ἠ', + 'Ἡ' => 'ἡ', + 'Ἢ' => 'ἢ', + 'Ἣ' => 'ἣ', + 'Ἤ' => 'ἤ', + 'Ἥ' => 'ἥ', + 'Ἦ' => 'ἦ', + 'Ἧ' => 'ἧ', + 'Ἰ' => 'ἰ', + 'Ἱ' => 'ἱ', + 'Ἲ' => 'ἲ', + 'Ἳ' => 'ἳ', + 'Ἴ' => 'ἴ', + 'Ἵ' => 'ἵ', + 'Ἶ' => 'ἶ', + 'Ἷ' => 'ἷ', + 'Ὀ' => 'ὀ', + 'Ὁ' => 'ὁ', + 'Ὂ' => 'ὂ', + 'Ὃ' => 'ὃ', + 'Ὄ' => 'ὄ', + 'Ὅ' => 'ὅ', + 'Ὑ' => 'ὑ', + 'Ὓ' => 'ὓ', + 'Ὕ' => 'ὕ', + 'Ὗ' => 'ὗ', + 'Ὠ' => 'ὠ', + 'Ὡ' => 'ὡ', + 'Ὢ' => 'ὢ', + 'Ὣ' => 'ὣ', + 'Ὤ' => 'ὤ', + 'Ὥ' => 'ὥ', + 'Ὦ' => 'ὦ', + 'Ὧ' => 'ὧ', + 'ᾈ' => 'ᾀ', + 'ᾉ' => 'ᾁ', + 'ᾊ' => 'ᾂ', + 'ᾋ' => 'ᾃ', + 'ᾌ' => 'ᾄ', + 'ᾍ' => 'ᾅ', + 'ᾎ' => 'ᾆ', + 'ᾏ' => 'ᾇ', + 'ᾘ' => 'ᾐ', + 'ᾙ' => 'ᾑ', + 'ᾚ' => 'ᾒ', + 'ᾛ' => 'ᾓ', + 'ᾜ' => 'ᾔ', + 'ᾝ' => 'ᾕ', + 'ᾞ' => 'ᾖ', + 'ᾟ' => 'ᾗ', + 'ᾨ' => 'ᾠ', + 'ᾩ' => 'ᾡ', + 'ᾪ' => 'ᾢ', + 'ᾫ' => 'ᾣ', + 'ᾬ' => 'ᾤ', + 'ᾭ' => 'ᾥ', + 'ᾮ' => 'ᾦ', + 'ᾯ' => 'ᾧ', + 'Ᾰ' => 'ᾰ', + 'Ᾱ' => 'ᾱ', + 'Ὰ' => 'ὰ', + 'Ά' => 'ά', + 'ᾼ' => 'ᾳ', + 'Ὲ' => 'ὲ', + 'Έ' => 'έ', + 'Ὴ' => 'ὴ', + 'Ή' => 'ή', + 'ῌ' => 'ῃ', + 'Ῐ' => 'ῐ', + 'Ῑ' => 'ῑ', + 'Ὶ' => 'ὶ', + 'Ί' => 'ί', + 'Ῠ' => 'ῠ', + 'Ῡ' => 'ῡ', + 'Ὺ' => 'ὺ', + 'Ύ' => 'ύ', + 'Ῥ' => 'ῥ', + 'Ὸ' => 'ὸ', + 'Ό' => 'ό', + 'Ὼ' => 'ὼ', + 'Ώ' => 'ώ', + 'ῼ' => 'ῳ', + 'Ω' => 'ω', + 'K' => 'k', + 'Å' => 'å', + 'Ⅎ' => 'ⅎ', + 'Ⅰ' => 'ⅰ', + 'Ⅱ' => 'ⅱ', + 'Ⅲ' => 'ⅲ', + 'Ⅳ' => 'ⅳ', + 'Ⅴ' => 'ⅴ', + 'Ⅵ' => 'ⅵ', + 'Ⅶ' => 'ⅶ', + 'Ⅷ' => 'ⅷ', + 'Ⅸ' => 'ⅸ', + 'Ⅹ' => 'ⅹ', + 'Ⅺ' => 'ⅺ', + 'Ⅻ' => 'ⅻ', + 'Ⅼ' => 'ⅼ', + 'Ⅽ' => 'ⅽ', + 'Ⅾ' => 'ⅾ', + 'Ⅿ' => 'ⅿ', + 'Ↄ' => 'ↄ', + 'Ⓐ' => 'ⓐ', + 'Ⓑ' => 'ⓑ', + 'Ⓒ' => 'ⓒ', + 'Ⓓ' => 'ⓓ', + 'Ⓔ' => 'ⓔ', + 'Ⓕ' => 'ⓕ', + 'Ⓖ' => 'ⓖ', + 'Ⓗ' => 'ⓗ', + 'Ⓘ' => 'ⓘ', + 'Ⓙ' => 'ⓙ', + 'Ⓚ' => 'ⓚ', + 'Ⓛ' => 'ⓛ', + 'Ⓜ' => 'ⓜ', + 'Ⓝ' => 'ⓝ', + 'Ⓞ' => 'ⓞ', + 'Ⓟ' => 'ⓟ', + 'Ⓠ' => 'ⓠ', + 'Ⓡ' => 'ⓡ', + 'Ⓢ' => 'ⓢ', + 'Ⓣ' => 'ⓣ', + 'Ⓤ' => 'ⓤ', + 'Ⓥ' => 'ⓥ', + 'Ⓦ' => 'ⓦ', + 'Ⓧ' => 'ⓧ', + 'Ⓨ' => 'ⓨ', + 'Ⓩ' => 'ⓩ', + 'Ⰰ' => 'ⰰ', + 'Ⰱ' => 'ⰱ', + 'Ⰲ' => 'ⰲ', + 'Ⰳ' => 'ⰳ', + 'Ⰴ' => 'ⰴ', + 'Ⰵ' => 'ⰵ', + 'Ⰶ' => 'ⰶ', + 'Ⰷ' => 'ⰷ', + 'Ⰸ' => 'ⰸ', + 'Ⰹ' => 'ⰹ', + 'Ⰺ' => 'ⰺ', + 'Ⰻ' => 'ⰻ', + 'Ⰼ' => 'ⰼ', + 'Ⰽ' => 'ⰽ', + 'Ⰾ' => 'ⰾ', + 'Ⰿ' => 'ⰿ', + 'Ⱀ' => 'ⱀ', + 'Ⱁ' => 'ⱁ', + 'Ⱂ' => 'ⱂ', + 'Ⱃ' => 'ⱃ', + 'Ⱄ' => 'ⱄ', + 'Ⱅ' => 'ⱅ', + 'Ⱆ' => 'ⱆ', + 'Ⱇ' => 'ⱇ', + 'Ⱈ' => 'ⱈ', + 'Ⱉ' => 'ⱉ', + 'Ⱊ' => 'ⱊ', + 'Ⱋ' => 'ⱋ', + 'Ⱌ' => 'ⱌ', + 'Ⱍ' => 'ⱍ', + 'Ⱎ' => 'ⱎ', + 'Ⱏ' => 'ⱏ', + 'Ⱐ' => 'ⱐ', + 'Ⱑ' => 'ⱑ', + 'Ⱒ' => 'ⱒ', + 'Ⱓ' => 'ⱓ', + 'Ⱔ' => 'ⱔ', + 'Ⱕ' => 'ⱕ', + 'Ⱖ' => 'ⱖ', + 'Ⱗ' => 'ⱗ', + 'Ⱘ' => 'ⱘ', + 'Ⱙ' => 'ⱙ', + 'Ⱚ' => 'ⱚ', + 'Ⱛ' => 'ⱛ', + 'Ⱜ' => 'ⱜ', + 'Ⱝ' => 'ⱝ', + 'Ⱞ' => 'ⱞ', + 'Ⱡ' => 'ⱡ', + 'Ɫ' => 'ɫ', + 'Ᵽ' => 'ᵽ', + 'Ɽ' => 'ɽ', + 'Ⱨ' => 'ⱨ', + 'Ⱪ' => 'ⱪ', + 'Ⱬ' => 'ⱬ', + 'Ɑ' => 'ɑ', + 'Ɱ' => 'ɱ', + 'Ɐ' => 'ɐ', + 'Ɒ' => 'ɒ', + 'Ⱳ' => 'ⱳ', + 'Ⱶ' => 'ⱶ', + 'Ȿ' => 'ȿ', + 'Ɀ' => 'ɀ', + 'Ⲁ' => 'ⲁ', + 'Ⲃ' => 'ⲃ', + 'Ⲅ' => 'ⲅ', + 'Ⲇ' => 'ⲇ', + 'Ⲉ' => 'ⲉ', + 'Ⲋ' => 'ⲋ', + 'Ⲍ' => 'ⲍ', + 'Ⲏ' => 'ⲏ', + 'Ⲑ' => 'ⲑ', + 'Ⲓ' => 'ⲓ', + 'Ⲕ' => 'ⲕ', + 'Ⲗ' => 'ⲗ', + 'Ⲙ' => 'ⲙ', + 'Ⲛ' => 'ⲛ', + 'Ⲝ' => 'ⲝ', + 'Ⲟ' => 'ⲟ', + 'Ⲡ' => 'ⲡ', + 'Ⲣ' => 'ⲣ', + 'Ⲥ' => 'ⲥ', + 'Ⲧ' => 'ⲧ', + 'Ⲩ' => 'ⲩ', + 'Ⲫ' => 'ⲫ', + 'Ⲭ' => 'ⲭ', + 'Ⲯ' => 'ⲯ', + 'Ⲱ' => 'ⲱ', + 'Ⲳ' => 'ⲳ', + 'Ⲵ' => 'ⲵ', + 'Ⲷ' => 'ⲷ', + 'Ⲹ' => 'ⲹ', + 'Ⲻ' => 'ⲻ', + 'Ⲽ' => 'ⲽ', + 'Ⲿ' => 'ⲿ', + 'Ⳁ' => 'ⳁ', + 'Ⳃ' => 'ⳃ', + 'Ⳅ' => 'ⳅ', + 'Ⳇ' => 'ⳇ', + 'Ⳉ' => 'ⳉ', + 'Ⳋ' => 'ⳋ', + 'Ⳍ' => 'ⳍ', + 'Ⳏ' => 'ⳏ', + 'Ⳑ' => 'ⳑ', + 'Ⳓ' => 'ⳓ', + 'Ⳕ' => 'ⳕ', + 'Ⳗ' => 'ⳗ', + 'Ⳙ' => 'ⳙ', + 'Ⳛ' => 'ⳛ', + 'Ⳝ' => 'ⳝ', + 'Ⳟ' => 'ⳟ', + 'Ⳡ' => 'ⳡ', + 'Ⳣ' => 'ⳣ', + 'Ⳬ' => 'ⳬ', + 'Ⳮ' => 'ⳮ', + 'Ⳳ' => 'ⳳ', + 'Ꙁ' => 'ꙁ', + 'Ꙃ' => 'ꙃ', + 'Ꙅ' => 'ꙅ', + 'Ꙇ' => 'ꙇ', + 'Ꙉ' => 'ꙉ', + 'Ꙋ' => 'ꙋ', + 'Ꙍ' => 'ꙍ', + 'Ꙏ' => 'ꙏ', + 'Ꙑ' => 'ꙑ', + 'Ꙓ' => 'ꙓ', + 'Ꙕ' => 'ꙕ', + 'Ꙗ' => 'ꙗ', + 'Ꙙ' => 'ꙙ', + 'Ꙛ' => 'ꙛ', + 'Ꙝ' => 'ꙝ', + 'Ꙟ' => 'ꙟ', + 'Ꙡ' => 'ꙡ', + 'Ꙣ' => 'ꙣ', + 'Ꙥ' => 'ꙥ', + 'Ꙧ' => 'ꙧ', + 'Ꙩ' => 'ꙩ', + 'Ꙫ' => 'ꙫ', + 'Ꙭ' => 'ꙭ', + 'Ꚁ' => 'ꚁ', + 'Ꚃ' => 'ꚃ', + 'Ꚅ' => 'ꚅ', + 'Ꚇ' => 'ꚇ', + 'Ꚉ' => 'ꚉ', + 'Ꚋ' => 'ꚋ', + 'Ꚍ' => 'ꚍ', + 'Ꚏ' => 'ꚏ', + 'Ꚑ' => 'ꚑ', + 'Ꚓ' => 'ꚓ', + 'Ꚕ' => 'ꚕ', + 'Ꚗ' => 'ꚗ', + 'Ꚙ' => 'ꚙ', + 'Ꚛ' => 'ꚛ', + 'Ꜣ' => 'ꜣ', + 'Ꜥ' => 'ꜥ', + 'Ꜧ' => 'ꜧ', + 'Ꜩ' => 'ꜩ', + 'Ꜫ' => 'ꜫ', + 'Ꜭ' => 'ꜭ', + 'Ꜯ' => 'ꜯ', + 'Ꜳ' => 'ꜳ', + 'Ꜵ' => 'ꜵ', + 'Ꜷ' => 'ꜷ', + 'Ꜹ' => 'ꜹ', + 'Ꜻ' => 'ꜻ', + 'Ꜽ' => 'ꜽ', + 'Ꜿ' => 'ꜿ', + 'Ꝁ' => 'ꝁ', + 'Ꝃ' => 'ꝃ', + 'Ꝅ' => 'ꝅ', + 'Ꝇ' => 'ꝇ', + 'Ꝉ' => 'ꝉ', + 'Ꝋ' => 'ꝋ', + 'Ꝍ' => 'ꝍ', + 'Ꝏ' => 'ꝏ', + 'Ꝑ' => 'ꝑ', + 'Ꝓ' => 'ꝓ', + 'Ꝕ' => 'ꝕ', + 'Ꝗ' => 'ꝗ', + 'Ꝙ' => 'ꝙ', + 'Ꝛ' => 'ꝛ', + 'Ꝝ' => 'ꝝ', + 'Ꝟ' => 'ꝟ', + 'Ꝡ' => 'ꝡ', + 'Ꝣ' => 'ꝣ', + 'Ꝥ' => 'ꝥ', + 'Ꝧ' => 'ꝧ', + 'Ꝩ' => 'ꝩ', + 'Ꝫ' => 'ꝫ', + 'Ꝭ' => 'ꝭ', + 'Ꝯ' => 'ꝯ', + 'Ꝺ' => 'ꝺ', + 'Ꝼ' => 'ꝼ', + 'Ᵹ' => 'ᵹ', + 'Ꝿ' => 'ꝿ', + 'Ꞁ' => 'ꞁ', + 'Ꞃ' => 'ꞃ', + 'Ꞅ' => 'ꞅ', + 'Ꞇ' => 'ꞇ', + 'Ꞌ' => 'ꞌ', + 'Ɥ' => 'ɥ', + 'Ꞑ' => 'ꞑ', + 'Ꞓ' => 'ꞓ', + 'Ꞗ' => 'ꞗ', + 'Ꞙ' => 'ꞙ', + 'Ꞛ' => 'ꞛ', + 'Ꞝ' => 'ꞝ', + 'Ꞟ' => 'ꞟ', + 'Ꞡ' => 'ꞡ', + 'Ꞣ' => 'ꞣ', + 'Ꞥ' => 'ꞥ', + 'Ꞧ' => 'ꞧ', + 'Ꞩ' => 'ꞩ', + 'Ɦ' => 'ɦ', + 'Ɜ' => 'ɜ', + 'Ɡ' => 'ɡ', + 'Ɬ' => 'ɬ', + 'Ʞ' => 'ʞ', + 'Ʇ' => 'ʇ', + 'A' => 'a', + 'B' => 'b', + 'C' => 'c', + 'D' => 'd', + 'E' => 'e', + 'F' => 'f', + 'G' => 'g', + 'H' => 'h', + 'I' => 'i', + 'J' => 'j', + 'K' => 'k', + 'L' => 'l', + 'M' => 'm', + 'N' => 'n', + 'O' => 'o', + 'P' => 'p', + 'Q' => 'q', + 'R' => 'r', + 'S' => 's', + 'T' => 't', + 'U' => 'u', + 'V' => 'v', + 'W' => 'w', + 'X' => 'x', + 'Y' => 'y', + 'Z' => 'z', + '𐐀' => '𐐨', + '𐐁' => '𐐩', + '𐐂' => '𐐪', + '𐐃' => '𐐫', + '𐐄' => '𐐬', + '𐐅' => '𐐭', + '𐐆' => '𐐮', + '𐐇' => '𐐯', + '𐐈' => '𐐰', + '𐐉' => '𐐱', + '𐐊' => '𐐲', + '𐐋' => '𐐳', + '𐐌' => '𐐴', + '𐐍' => '𐐵', + '𐐎' => '𐐶', + '𐐏' => '𐐷', + '𐐐' => '𐐸', + '𐐑' => '𐐹', + '𐐒' => '𐐺', + '𐐓' => '𐐻', + '𐐔' => '𐐼', + '𐐕' => '𐐽', + '𐐖' => '𐐾', + '𐐗' => '𐐿', + '𐐘' => '𐑀', + '𐐙' => '𐑁', + '𐐚' => '𐑂', + '𐐛' => '𐑃', + '𐐜' => '𐑄', + '𐐝' => '𐑅', + '𐐞' => '𐑆', + '𐐟' => '𐑇', + '𐐠' => '𐑈', + '𐐡' => '𐑉', + '𐐢' => '𐑊', + '𐐣' => '𐑋', + '𐐤' => '𐑌', + '𐐥' => '𐑍', + '𐐦' => '𐑎', + '𐐧' => '𐑏', + '𑢠' => '𑣀', + '𑢡' => '𑣁', + '𑢢' => '𑣂', + '𑢣' => '𑣃', + '𑢤' => '𑣄', + '𑢥' => '𑣅', + '𑢦' => '𑣆', + '𑢧' => '𑣇', + '𑢨' => '𑣈', + '𑢩' => '𑣉', + '𑢪' => '𑣊', + '𑢫' => '𑣋', + '𑢬' => '𑣌', + '𑢭' => '𑣍', + '𑢮' => '𑣎', + '𑢯' => '𑣏', + '𑢰' => '𑣐', + '𑢱' => '𑣑', + '𑢲' => '𑣒', + '𑢳' => '𑣓', + '𑢴' => '𑣔', + '𑢵' => '𑣕', + '𑢶' => '𑣖', + '𑢷' => '𑣗', + '𑢸' => '𑣘', + '𑢹' => '𑣙', + '𑢺' => '𑣚', + '𑢻' => '𑣛', + '𑢼' => '𑣜', + '𑢽' => '𑣝', + '𑢾' => '𑣞', + '𑢿' => '𑣟', +); diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php new file mode 100644 index 0000000..2a8f6e7 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php @@ -0,0 +1,5 @@ + 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + 'µ' => 'Μ', + 'à' => 'À', + 'á' => 'Á', + 'â' => 'Â', + 'ã' => 'Ã', + 'ä' => 'Ä', + 'å' => 'Å', + 'æ' => 'Æ', + 'ç' => 'Ç', + 'è' => 'È', + 'é' => 'É', + 'ê' => 'Ê', + 'ë' => 'Ë', + 'ì' => 'Ì', + 'í' => 'Í', + 'î' => 'Î', + 'ï' => 'Ï', + 'ð' => 'Ð', + 'ñ' => 'Ñ', + 'ò' => 'Ò', + 'ó' => 'Ó', + 'ô' => 'Ô', + 'õ' => 'Õ', + 'ö' => 'Ö', + 'ø' => 'Ø', + 'ù' => 'Ù', + 'ú' => 'Ú', + 'û' => 'Û', + 'ü' => 'Ü', + 'ý' => 'Ý', + 'þ' => 'Þ', + 'ÿ' => 'Ÿ', + 'ā' => 'Ā', + 'ă' => 'Ă', + 'ą' => 'Ą', + 'ć' => 'Ć', + 'ĉ' => 'Ĉ', + 'ċ' => 'Ċ', + 'č' => 'Č', + 'ď' => 'Ď', + 'đ' => 'Đ', + 'ē' => 'Ē', + 'ĕ' => 'Ĕ', + 'ė' => 'Ė', + 'ę' => 'Ę', + 'ě' => 'Ě', + 'ĝ' => 'Ĝ', + 'ğ' => 'Ğ', + 'ġ' => 'Ġ', + 'ģ' => 'Ģ', + 'ĥ' => 'Ĥ', + 'ħ' => 'Ħ', + 'ĩ' => 'Ĩ', + 'ī' => 'Ī', + 'ĭ' => 'Ĭ', + 'į' => 'Į', + 'ı' => 'I', + 'ij' => 'IJ', + 'ĵ' => 'Ĵ', + 'ķ' => 'Ķ', + 'ĺ' => 'Ĺ', + 'ļ' => 'Ļ', + 'ľ' => 'Ľ', + 'ŀ' => 'Ŀ', + 'ł' => 'Ł', + 'ń' => 'Ń', + 'ņ' => 'Ņ', + 'ň' => 'Ň', + 'ŋ' => 'Ŋ', + 'ō' => 'Ō', + 'ŏ' => 'Ŏ', + 'ő' => 'Ő', + 'œ' => 'Œ', + 'ŕ' => 'Ŕ', + 'ŗ' => 'Ŗ', + 'ř' => 'Ř', + 'ś' => 'Ś', + 'ŝ' => 'Ŝ', + 'ş' => 'Ş', + 'š' => 'Š', + 'ţ' => 'Ţ', + 'ť' => 'Ť', + 'ŧ' => 'Ŧ', + 'ũ' => 'Ũ', + 'ū' => 'Ū', + 'ŭ' => 'Ŭ', + 'ů' => 'Ů', + 'ű' => 'Ű', + 'ų' => 'Ų', + 'ŵ' => 'Ŵ', + 'ŷ' => 'Ŷ', + 'ź' => 'Ź', + 'ż' => 'Ż', + 'ž' => 'Ž', + 'ſ' => 'S', + 'ƀ' => 'Ƀ', + 'ƃ' => 'Ƃ', + 'ƅ' => 'Ƅ', + 'ƈ' => 'Ƈ', + 'ƌ' => 'Ƌ', + 'ƒ' => 'Ƒ', + 'ƕ' => 'Ƕ', + 'ƙ' => 'Ƙ', + 'ƚ' => 'Ƚ', + 'ƞ' => 'Ƞ', + 'ơ' => 'Ơ', + 'ƣ' => 'Ƣ', + 'ƥ' => 'Ƥ', + 'ƨ' => 'Ƨ', + 'ƭ' => 'Ƭ', + 'ư' => 'Ư', + 'ƴ' => 'Ƴ', + 'ƶ' => 'Ƶ', + 'ƹ' => 'Ƹ', + 'ƽ' => 'Ƽ', + 'ƿ' => 'Ƿ', + 'Dž' => 'DŽ', + 'dž' => 'DŽ', + 'Lj' => 'LJ', + 'lj' => 'LJ', + 'Nj' => 'NJ', + 'nj' => 'NJ', + 'ǎ' => 'Ǎ', + 'ǐ' => 'Ǐ', + 'ǒ' => 'Ǒ', + 'ǔ' => 'Ǔ', + 'ǖ' => 'Ǖ', + 'ǘ' => 'Ǘ', + 'ǚ' => 'Ǚ', + 'ǜ' => 'Ǜ', + 'ǝ' => 'Ǝ', + 'ǟ' => 'Ǟ', + 'ǡ' => 'Ǡ', + 'ǣ' => 'Ǣ', + 'ǥ' => 'Ǥ', + 'ǧ' => 'Ǧ', + 'ǩ' => 'Ǩ', + 'ǫ' => 'Ǫ', + 'ǭ' => 'Ǭ', + 'ǯ' => 'Ǯ', + 'Dz' => 'DZ', + 'dz' => 'DZ', + 'ǵ' => 'Ǵ', + 'ǹ' => 'Ǹ', + 'ǻ' => 'Ǻ', + 'ǽ' => 'Ǽ', + 'ǿ' => 'Ǿ', + 'ȁ' => 'Ȁ', + 'ȃ' => 'Ȃ', + 'ȅ' => 'Ȅ', + 'ȇ' => 'Ȇ', + 'ȉ' => 'Ȉ', + 'ȋ' => 'Ȋ', + 'ȍ' => 'Ȍ', + 'ȏ' => 'Ȏ', + 'ȑ' => 'Ȑ', + 'ȓ' => 'Ȓ', + 'ȕ' => 'Ȕ', + 'ȗ' => 'Ȗ', + 'ș' => 'Ș', + 'ț' => 'Ț', + 'ȝ' => 'Ȝ', + 'ȟ' => 'Ȟ', + 'ȣ' => 'Ȣ', + 'ȥ' => 'Ȥ', + 'ȧ' => 'Ȧ', + 'ȩ' => 'Ȩ', + 'ȫ' => 'Ȫ', + 'ȭ' => 'Ȭ', + 'ȯ' => 'Ȯ', + 'ȱ' => 'Ȱ', + 'ȳ' => 'Ȳ', + 'ȼ' => 'Ȼ', + 'ȿ' => 'Ȿ', + 'ɀ' => 'Ɀ', + 'ɂ' => 'Ɂ', + 'ɇ' => 'Ɇ', + 'ɉ' => 'Ɉ', + 'ɋ' => 'Ɋ', + 'ɍ' => 'Ɍ', + 'ɏ' => 'Ɏ', + 'ɐ' => 'Ɐ', + 'ɑ' => 'Ɑ', + 'ɒ' => 'Ɒ', + 'ɓ' => 'Ɓ', + 'ɔ' => 'Ɔ', + 'ɖ' => 'Ɖ', + 'ɗ' => 'Ɗ', + 'ə' => 'Ə', + 'ɛ' => 'Ɛ', + 'ɜ' => 'Ɜ', + 'ɠ' => 'Ɠ', + 'ɡ' => 'Ɡ', + 'ɣ' => 'Ɣ', + 'ɥ' => 'Ɥ', + 'ɦ' => 'Ɦ', + 'ɨ' => 'Ɨ', + 'ɩ' => 'Ɩ', + 'ɫ' => 'Ɫ', + 'ɬ' => 'Ɬ', + 'ɯ' => 'Ɯ', + 'ɱ' => 'Ɱ', + 'ɲ' => 'Ɲ', + 'ɵ' => 'Ɵ', + 'ɽ' => 'Ɽ', + 'ʀ' => 'Ʀ', + 'ʃ' => 'Ʃ', + 'ʇ' => 'Ʇ', + 'ʈ' => 'Ʈ', + 'ʉ' => 'Ʉ', + 'ʊ' => 'Ʊ', + 'ʋ' => 'Ʋ', + 'ʌ' => 'Ʌ', + 'ʒ' => 'Ʒ', + 'ʞ' => 'Ʞ', + 'ͅ' => 'Ι', + 'ͱ' => 'Ͱ', + 'ͳ' => 'Ͳ', + 'ͷ' => 'Ͷ', + 'ͻ' => 'Ͻ', + 'ͼ' => 'Ͼ', + 'ͽ' => 'Ͽ', + 'ά' => 'Ά', + 'έ' => 'Έ', + 'ή' => 'Ή', + 'ί' => 'Ί', + 'α' => 'Α', + 'β' => 'Β', + 'γ' => 'Γ', + 'δ' => 'Δ', + 'ε' => 'Ε', + 'ζ' => 'Ζ', + 'η' => 'Η', + 'θ' => 'Θ', + 'ι' => 'Ι', + 'κ' => 'Κ', + 'λ' => 'Λ', + 'μ' => 'Μ', + 'ν' => 'Ν', + 'ξ' => 'Ξ', + 'ο' => 'Ο', + 'π' => 'Π', + 'ρ' => 'Ρ', + 'ς' => 'Σ', + 'σ' => 'Σ', + 'τ' => 'Τ', + 'υ' => 'Υ', + 'φ' => 'Φ', + 'χ' => 'Χ', + 'ψ' => 'Ψ', + 'ω' => 'Ω', + 'ϊ' => 'Ϊ', + 'ϋ' => 'Ϋ', + 'ό' => 'Ό', + 'ύ' => 'Ύ', + 'ώ' => 'Ώ', + 'ϐ' => 'Β', + 'ϑ' => 'Θ', + 'ϕ' => 'Φ', + 'ϖ' => 'Π', + 'ϗ' => 'Ϗ', + 'ϙ' => 'Ϙ', + 'ϛ' => 'Ϛ', + 'ϝ' => 'Ϝ', + 'ϟ' => 'Ϟ', + 'ϡ' => 'Ϡ', + 'ϣ' => 'Ϣ', + 'ϥ' => 'Ϥ', + 'ϧ' => 'Ϧ', + 'ϩ' => 'Ϩ', + 'ϫ' => 'Ϫ', + 'ϭ' => 'Ϭ', + 'ϯ' => 'Ϯ', + 'ϰ' => 'Κ', + 'ϱ' => 'Ρ', + 'ϲ' => 'Ϲ', + 'ϳ' => 'Ϳ', + 'ϵ' => 'Ε', + 'ϸ' => 'Ϸ', + 'ϻ' => 'Ϻ', + 'а' => 'А', + 'б' => 'Б', + 'в' => 'В', + 'г' => 'Г', + 'д' => 'Д', + 'е' => 'Е', + 'ж' => 'Ж', + 'з' => 'З', + 'и' => 'И', + 'й' => 'Й', + 'к' => 'К', + 'л' => 'Л', + 'м' => 'М', + 'н' => 'Н', + 'о' => 'О', + 'п' => 'П', + 'р' => 'Р', + 'с' => 'С', + 'т' => 'Т', + 'у' => 'У', + 'ф' => 'Ф', + 'х' => 'Х', + 'ц' => 'Ц', + 'ч' => 'Ч', + 'ш' => 'Ш', + 'щ' => 'Щ', + 'ъ' => 'Ъ', + 'ы' => 'Ы', + 'ь' => 'Ь', + 'э' => 'Э', + 'ю' => 'Ю', + 'я' => 'Я', + 'ѐ' => 'Ѐ', + 'ё' => 'Ё', + 'ђ' => 'Ђ', + 'ѓ' => 'Ѓ', + 'є' => 'Є', + 'ѕ' => 'Ѕ', + 'і' => 'І', + 'ї' => 'Ї', + 'ј' => 'Ј', + 'љ' => 'Љ', + 'њ' => 'Њ', + 'ћ' => 'Ћ', + 'ќ' => 'Ќ', + 'ѝ' => 'Ѝ', + 'ў' => 'Ў', + 'џ' => 'Џ', + 'ѡ' => 'Ѡ', + 'ѣ' => 'Ѣ', + 'ѥ' => 'Ѥ', + 'ѧ' => 'Ѧ', + 'ѩ' => 'Ѩ', + 'ѫ' => 'Ѫ', + 'ѭ' => 'Ѭ', + 'ѯ' => 'Ѯ', + 'ѱ' => 'Ѱ', + 'ѳ' => 'Ѳ', + 'ѵ' => 'Ѵ', + 'ѷ' => 'Ѷ', + 'ѹ' => 'Ѹ', + 'ѻ' => 'Ѻ', + 'ѽ' => 'Ѽ', + 'ѿ' => 'Ѿ', + 'ҁ' => 'Ҁ', + 'ҋ' => 'Ҋ', + 'ҍ' => 'Ҍ', + 'ҏ' => 'Ҏ', + 'ґ' => 'Ґ', + 'ғ' => 'Ғ', + 'ҕ' => 'Ҕ', + 'җ' => 'Җ', + 'ҙ' => 'Ҙ', + 'қ' => 'Қ', + 'ҝ' => 'Ҝ', + 'ҟ' => 'Ҟ', + 'ҡ' => 'Ҡ', + 'ң' => 'Ң', + 'ҥ' => 'Ҥ', + 'ҧ' => 'Ҧ', + 'ҩ' => 'Ҩ', + 'ҫ' => 'Ҫ', + 'ҭ' => 'Ҭ', + 'ү' => 'Ү', + 'ұ' => 'Ұ', + 'ҳ' => 'Ҳ', + 'ҵ' => 'Ҵ', + 'ҷ' => 'Ҷ', + 'ҹ' => 'Ҹ', + 'һ' => 'Һ', + 'ҽ' => 'Ҽ', + 'ҿ' => 'Ҿ', + 'ӂ' => 'Ӂ', + 'ӄ' => 'Ӄ', + 'ӆ' => 'Ӆ', + 'ӈ' => 'Ӈ', + 'ӊ' => 'Ӊ', + 'ӌ' => 'Ӌ', + 'ӎ' => 'Ӎ', + 'ӏ' => 'Ӏ', + 'ӑ' => 'Ӑ', + 'ӓ' => 'Ӓ', + 'ӕ' => 'Ӕ', + 'ӗ' => 'Ӗ', + 'ә' => 'Ә', + 'ӛ' => 'Ӛ', + 'ӝ' => 'Ӝ', + 'ӟ' => 'Ӟ', + 'ӡ' => 'Ӡ', + 'ӣ' => 'Ӣ', + 'ӥ' => 'Ӥ', + 'ӧ' => 'Ӧ', + 'ө' => 'Ө', + 'ӫ' => 'Ӫ', + 'ӭ' => 'Ӭ', + 'ӯ' => 'Ӯ', + 'ӱ' => 'Ӱ', + 'ӳ' => 'Ӳ', + 'ӵ' => 'Ӵ', + 'ӷ' => 'Ӷ', + 'ӹ' => 'Ӹ', + 'ӻ' => 'Ӻ', + 'ӽ' => 'Ӽ', + 'ӿ' => 'Ӿ', + 'ԁ' => 'Ԁ', + 'ԃ' => 'Ԃ', + 'ԅ' => 'Ԅ', + 'ԇ' => 'Ԇ', + 'ԉ' => 'Ԉ', + 'ԋ' => 'Ԋ', + 'ԍ' => 'Ԍ', + 'ԏ' => 'Ԏ', + 'ԑ' => 'Ԑ', + 'ԓ' => 'Ԓ', + 'ԕ' => 'Ԕ', + 'ԗ' => 'Ԗ', + 'ԙ' => 'Ԙ', + 'ԛ' => 'Ԛ', + 'ԝ' => 'Ԝ', + 'ԟ' => 'Ԟ', + 'ԡ' => 'Ԡ', + 'ԣ' => 'Ԣ', + 'ԥ' => 'Ԥ', + 'ԧ' => 'Ԧ', + 'ԩ' => 'Ԩ', + 'ԫ' => 'Ԫ', + 'ԭ' => 'Ԭ', + 'ԯ' => 'Ԯ', + 'ա' => 'Ա', + 'բ' => 'Բ', + 'գ' => 'Գ', + 'դ' => 'Դ', + 'ե' => 'Ե', + 'զ' => 'Զ', + 'է' => 'Է', + 'ը' => 'Ը', + 'թ' => 'Թ', + 'ժ' => 'Ժ', + 'ի' => 'Ի', + 'լ' => 'Լ', + 'խ' => 'Խ', + 'ծ' => 'Ծ', + 'կ' => 'Կ', + 'հ' => 'Հ', + 'ձ' => 'Ձ', + 'ղ' => 'Ղ', + 'ճ' => 'Ճ', + 'մ' => 'Մ', + 'յ' => 'Յ', + 'ն' => 'Ն', + 'շ' => 'Շ', + 'ո' => 'Ո', + 'չ' => 'Չ', + 'պ' => 'Պ', + 'ջ' => 'Ջ', + 'ռ' => 'Ռ', + 'ս' => 'Ս', + 'վ' => 'Վ', + 'տ' => 'Տ', + 'ր' => 'Ր', + 'ց' => 'Ց', + 'ւ' => 'Ւ', + 'փ' => 'Փ', + 'ք' => 'Ք', + 'օ' => 'Օ', + 'ֆ' => 'Ֆ', + 'ᵹ' => 'Ᵹ', + 'ᵽ' => 'Ᵽ', + 'ḁ' => 'Ḁ', + 'ḃ' => 'Ḃ', + 'ḅ' => 'Ḅ', + 'ḇ' => 'Ḇ', + 'ḉ' => 'Ḉ', + 'ḋ' => 'Ḋ', + 'ḍ' => 'Ḍ', + 'ḏ' => 'Ḏ', + 'ḑ' => 'Ḑ', + 'ḓ' => 'Ḓ', + 'ḕ' => 'Ḕ', + 'ḗ' => 'Ḗ', + 'ḙ' => 'Ḙ', + 'ḛ' => 'Ḛ', + 'ḝ' => 'Ḝ', + 'ḟ' => 'Ḟ', + 'ḡ' => 'Ḡ', + 'ḣ' => 'Ḣ', + 'ḥ' => 'Ḥ', + 'ḧ' => 'Ḧ', + 'ḩ' => 'Ḩ', + 'ḫ' => 'Ḫ', + 'ḭ' => 'Ḭ', + 'ḯ' => 'Ḯ', + 'ḱ' => 'Ḱ', + 'ḳ' => 'Ḳ', + 'ḵ' => 'Ḵ', + 'ḷ' => 'Ḷ', + 'ḹ' => 'Ḹ', + 'ḻ' => 'Ḻ', + 'ḽ' => 'Ḽ', + 'ḿ' => 'Ḿ', + 'ṁ' => 'Ṁ', + 'ṃ' => 'Ṃ', + 'ṅ' => 'Ṅ', + 'ṇ' => 'Ṇ', + 'ṉ' => 'Ṉ', + 'ṋ' => 'Ṋ', + 'ṍ' => 'Ṍ', + 'ṏ' => 'Ṏ', + 'ṑ' => 'Ṑ', + 'ṓ' => 'Ṓ', + 'ṕ' => 'Ṕ', + 'ṗ' => 'Ṗ', + 'ṙ' => 'Ṙ', + 'ṛ' => 'Ṛ', + 'ṝ' => 'Ṝ', + 'ṟ' => 'Ṟ', + 'ṡ' => 'Ṡ', + 'ṣ' => 'Ṣ', + 'ṥ' => 'Ṥ', + 'ṧ' => 'Ṧ', + 'ṩ' => 'Ṩ', + 'ṫ' => 'Ṫ', + 'ṭ' => 'Ṭ', + 'ṯ' => 'Ṯ', + 'ṱ' => 'Ṱ', + 'ṳ' => 'Ṳ', + 'ṵ' => 'Ṵ', + 'ṷ' => 'Ṷ', + 'ṹ' => 'Ṹ', + 'ṻ' => 'Ṻ', + 'ṽ' => 'Ṽ', + 'ṿ' => 'Ṿ', + 'ẁ' => 'Ẁ', + 'ẃ' => 'Ẃ', + 'ẅ' => 'Ẅ', + 'ẇ' => 'Ẇ', + 'ẉ' => 'Ẉ', + 'ẋ' => 'Ẋ', + 'ẍ' => 'Ẍ', + 'ẏ' => 'Ẏ', + 'ẑ' => 'Ẑ', + 'ẓ' => 'Ẓ', + 'ẕ' => 'Ẕ', + 'ẛ' => 'Ṡ', + 'ạ' => 'Ạ', + 'ả' => 'Ả', + 'ấ' => 'Ấ', + 'ầ' => 'Ầ', + 'ẩ' => 'Ẩ', + 'ẫ' => 'Ẫ', + 'ậ' => 'Ậ', + 'ắ' => 'Ắ', + 'ằ' => 'Ằ', + 'ẳ' => 'Ẳ', + 'ẵ' => 'Ẵ', + 'ặ' => 'Ặ', + 'ẹ' => 'Ẹ', + 'ẻ' => 'Ẻ', + 'ẽ' => 'Ẽ', + 'ế' => 'Ế', + 'ề' => 'Ề', + 'ể' => 'Ể', + 'ễ' => 'Ễ', + 'ệ' => 'Ệ', + 'ỉ' => 'Ỉ', + 'ị' => 'Ị', + 'ọ' => 'Ọ', + 'ỏ' => 'Ỏ', + 'ố' => 'Ố', + 'ồ' => 'Ồ', + 'ổ' => 'Ổ', + 'ỗ' => 'Ỗ', + 'ộ' => 'Ộ', + 'ớ' => 'Ớ', + 'ờ' => 'Ờ', + 'ở' => 'Ở', + 'ỡ' => 'Ỡ', + 'ợ' => 'Ợ', + 'ụ' => 'Ụ', + 'ủ' => 'Ủ', + 'ứ' => 'Ứ', + 'ừ' => 'Ừ', + 'ử' => 'Ử', + 'ữ' => 'Ữ', + 'ự' => 'Ự', + 'ỳ' => 'Ỳ', + 'ỵ' => 'Ỵ', + 'ỷ' => 'Ỷ', + 'ỹ' => 'Ỹ', + 'ỻ' => 'Ỻ', + 'ỽ' => 'Ỽ', + 'ỿ' => 'Ỿ', + 'ἀ' => 'Ἀ', + 'ἁ' => 'Ἁ', + 'ἂ' => 'Ἂ', + 'ἃ' => 'Ἃ', + 'ἄ' => 'Ἄ', + 'ἅ' => 'Ἅ', + 'ἆ' => 'Ἆ', + 'ἇ' => 'Ἇ', + 'ἐ' => 'Ἐ', + 'ἑ' => 'Ἑ', + 'ἒ' => 'Ἒ', + 'ἓ' => 'Ἓ', + 'ἔ' => 'Ἔ', + 'ἕ' => 'Ἕ', + 'ἠ' => 'Ἠ', + 'ἡ' => 'Ἡ', + 'ἢ' => 'Ἢ', + 'ἣ' => 'Ἣ', + 'ἤ' => 'Ἤ', + 'ἥ' => 'Ἥ', + 'ἦ' => 'Ἦ', + 'ἧ' => 'Ἧ', + 'ἰ' => 'Ἰ', + 'ἱ' => 'Ἱ', + 'ἲ' => 'Ἲ', + 'ἳ' => 'Ἳ', + 'ἴ' => 'Ἴ', + 'ἵ' => 'Ἵ', + 'ἶ' => 'Ἶ', + 'ἷ' => 'Ἷ', + 'ὀ' => 'Ὀ', + 'ὁ' => 'Ὁ', + 'ὂ' => 'Ὂ', + 'ὃ' => 'Ὃ', + 'ὄ' => 'Ὄ', + 'ὅ' => 'Ὅ', + 'ὑ' => 'Ὑ', + 'ὓ' => 'Ὓ', + 'ὕ' => 'Ὕ', + 'ὗ' => 'Ὗ', + 'ὠ' => 'Ὠ', + 'ὡ' => 'Ὡ', + 'ὢ' => 'Ὢ', + 'ὣ' => 'Ὣ', + 'ὤ' => 'Ὤ', + 'ὥ' => 'Ὥ', + 'ὦ' => 'Ὦ', + 'ὧ' => 'Ὧ', + 'ὰ' => 'Ὰ', + 'ά' => 'Ά', + 'ὲ' => 'Ὲ', + 'έ' => 'Έ', + 'ὴ' => 'Ὴ', + 'ή' => 'Ή', + 'ὶ' => 'Ὶ', + 'ί' => 'Ί', + 'ὸ' => 'Ὸ', + 'ό' => 'Ό', + 'ὺ' => 'Ὺ', + 'ύ' => 'Ύ', + 'ὼ' => 'Ὼ', + 'ώ' => 'Ώ', + 'ᾀ' => 'ᾈ', + 'ᾁ' => 'ᾉ', + 'ᾂ' => 'ᾊ', + 'ᾃ' => 'ᾋ', + 'ᾄ' => 'ᾌ', + 'ᾅ' => 'ᾍ', + 'ᾆ' => 'ᾎ', + 'ᾇ' => 'ᾏ', + 'ᾐ' => 'ᾘ', + 'ᾑ' => 'ᾙ', + 'ᾒ' => 'ᾚ', + 'ᾓ' => 'ᾛ', + 'ᾔ' => 'ᾜ', + 'ᾕ' => 'ᾝ', + 'ᾖ' => 'ᾞ', + 'ᾗ' => 'ᾟ', + 'ᾠ' => 'ᾨ', + 'ᾡ' => 'ᾩ', + 'ᾢ' => 'ᾪ', + 'ᾣ' => 'ᾫ', + 'ᾤ' => 'ᾬ', + 'ᾥ' => 'ᾭ', + 'ᾦ' => 'ᾮ', + 'ᾧ' => 'ᾯ', + 'ᾰ' => 'Ᾰ', + 'ᾱ' => 'Ᾱ', + 'ᾳ' => 'ᾼ', + 'ι' => 'Ι', + 'ῃ' => 'ῌ', + 'ῐ' => 'Ῐ', + 'ῑ' => 'Ῑ', + 'ῠ' => 'Ῠ', + 'ῡ' => 'Ῡ', + 'ῥ' => 'Ῥ', + 'ῳ' => 'ῼ', + 'ⅎ' => 'Ⅎ', + 'ⅰ' => 'Ⅰ', + 'ⅱ' => 'Ⅱ', + 'ⅲ' => 'Ⅲ', + 'ⅳ' => 'Ⅳ', + 'ⅴ' => 'Ⅴ', + 'ⅵ' => 'Ⅵ', + 'ⅶ' => 'Ⅶ', + 'ⅷ' => 'Ⅷ', + 'ⅸ' => 'Ⅸ', + 'ⅹ' => 'Ⅹ', + 'ⅺ' => 'Ⅺ', + 'ⅻ' => 'Ⅻ', + 'ⅼ' => 'Ⅼ', + 'ⅽ' => 'Ⅽ', + 'ⅾ' => 'Ⅾ', + 'ⅿ' => 'Ⅿ', + 'ↄ' => 'Ↄ', + 'ⓐ' => 'Ⓐ', + 'ⓑ' => 'Ⓑ', + 'ⓒ' => 'Ⓒ', + 'ⓓ' => 'Ⓓ', + 'ⓔ' => 'Ⓔ', + 'ⓕ' => 'Ⓕ', + 'ⓖ' => 'Ⓖ', + 'ⓗ' => 'Ⓗ', + 'ⓘ' => 'Ⓘ', + 'ⓙ' => 'Ⓙ', + 'ⓚ' => 'Ⓚ', + 'ⓛ' => 'Ⓛ', + 'ⓜ' => 'Ⓜ', + 'ⓝ' => 'Ⓝ', + 'ⓞ' => 'Ⓞ', + 'ⓟ' => 'Ⓟ', + 'ⓠ' => 'Ⓠ', + 'ⓡ' => 'Ⓡ', + 'ⓢ' => 'Ⓢ', + 'ⓣ' => 'Ⓣ', + 'ⓤ' => 'Ⓤ', + 'ⓥ' => 'Ⓥ', + 'ⓦ' => 'Ⓦ', + 'ⓧ' => 'Ⓧ', + 'ⓨ' => 'Ⓨ', + 'ⓩ' => 'Ⓩ', + 'ⰰ' => 'Ⰰ', + 'ⰱ' => 'Ⰱ', + 'ⰲ' => 'Ⰲ', + 'ⰳ' => 'Ⰳ', + 'ⰴ' => 'Ⰴ', + 'ⰵ' => 'Ⰵ', + 'ⰶ' => 'Ⰶ', + 'ⰷ' => 'Ⰷ', + 'ⰸ' => 'Ⰸ', + 'ⰹ' => 'Ⰹ', + 'ⰺ' => 'Ⰺ', + 'ⰻ' => 'Ⰻ', + 'ⰼ' => 'Ⰼ', + 'ⰽ' => 'Ⰽ', + 'ⰾ' => 'Ⰾ', + 'ⰿ' => 'Ⰿ', + 'ⱀ' => 'Ⱀ', + 'ⱁ' => 'Ⱁ', + 'ⱂ' => 'Ⱂ', + 'ⱃ' => 'Ⱃ', + 'ⱄ' => 'Ⱄ', + 'ⱅ' => 'Ⱅ', + 'ⱆ' => 'Ⱆ', + 'ⱇ' => 'Ⱇ', + 'ⱈ' => 'Ⱈ', + 'ⱉ' => 'Ⱉ', + 'ⱊ' => 'Ⱊ', + 'ⱋ' => 'Ⱋ', + 'ⱌ' => 'Ⱌ', + 'ⱍ' => 'Ⱍ', + 'ⱎ' => 'Ⱎ', + 'ⱏ' => 'Ⱏ', + 'ⱐ' => 'Ⱐ', + 'ⱑ' => 'Ⱑ', + 'ⱒ' => 'Ⱒ', + 'ⱓ' => 'Ⱓ', + 'ⱔ' => 'Ⱔ', + 'ⱕ' => 'Ⱕ', + 'ⱖ' => 'Ⱖ', + 'ⱗ' => 'Ⱗ', + 'ⱘ' => 'Ⱘ', + 'ⱙ' => 'Ⱙ', + 'ⱚ' => 'Ⱚ', + 'ⱛ' => 'Ⱛ', + 'ⱜ' => 'Ⱜ', + 'ⱝ' => 'Ⱝ', + 'ⱞ' => 'Ⱞ', + 'ⱡ' => 'Ⱡ', + 'ⱥ' => 'Ⱥ', + 'ⱦ' => 'Ⱦ', + 'ⱨ' => 'Ⱨ', + 'ⱪ' => 'Ⱪ', + 'ⱬ' => 'Ⱬ', + 'ⱳ' => 'Ⱳ', + 'ⱶ' => 'Ⱶ', + 'ⲁ' => 'Ⲁ', + 'ⲃ' => 'Ⲃ', + 'ⲅ' => 'Ⲅ', + 'ⲇ' => 'Ⲇ', + 'ⲉ' => 'Ⲉ', + 'ⲋ' => 'Ⲋ', + 'ⲍ' => 'Ⲍ', + 'ⲏ' => 'Ⲏ', + 'ⲑ' => 'Ⲑ', + 'ⲓ' => 'Ⲓ', + 'ⲕ' => 'Ⲕ', + 'ⲗ' => 'Ⲗ', + 'ⲙ' => 'Ⲙ', + 'ⲛ' => 'Ⲛ', + 'ⲝ' => 'Ⲝ', + 'ⲟ' => 'Ⲟ', + 'ⲡ' => 'Ⲡ', + 'ⲣ' => 'Ⲣ', + 'ⲥ' => 'Ⲥ', + 'ⲧ' => 'Ⲧ', + 'ⲩ' => 'Ⲩ', + 'ⲫ' => 'Ⲫ', + 'ⲭ' => 'Ⲭ', + 'ⲯ' => 'Ⲯ', + 'ⲱ' => 'Ⲱ', + 'ⲳ' => 'Ⲳ', + 'ⲵ' => 'Ⲵ', + 'ⲷ' => 'Ⲷ', + 'ⲹ' => 'Ⲹ', + 'ⲻ' => 'Ⲻ', + 'ⲽ' => 'Ⲽ', + 'ⲿ' => 'Ⲿ', + 'ⳁ' => 'Ⳁ', + 'ⳃ' => 'Ⳃ', + 'ⳅ' => 'Ⳅ', + 'ⳇ' => 'Ⳇ', + 'ⳉ' => 'Ⳉ', + 'ⳋ' => 'Ⳋ', + 'ⳍ' => 'Ⳍ', + 'ⳏ' => 'Ⳏ', + 'ⳑ' => 'Ⳑ', + 'ⳓ' => 'Ⳓ', + 'ⳕ' => 'Ⳕ', + 'ⳗ' => 'Ⳗ', + 'ⳙ' => 'Ⳙ', + 'ⳛ' => 'Ⳛ', + 'ⳝ' => 'Ⳝ', + 'ⳟ' => 'Ⳟ', + 'ⳡ' => 'Ⳡ', + 'ⳣ' => 'Ⳣ', + 'ⳬ' => 'Ⳬ', + 'ⳮ' => 'Ⳮ', + 'ⳳ' => 'Ⳳ', + 'ⴀ' => 'Ⴀ', + 'ⴁ' => 'Ⴁ', + 'ⴂ' => 'Ⴂ', + 'ⴃ' => 'Ⴃ', + 'ⴄ' => 'Ⴄ', + 'ⴅ' => 'Ⴅ', + 'ⴆ' => 'Ⴆ', + 'ⴇ' => 'Ⴇ', + 'ⴈ' => 'Ⴈ', + 'ⴉ' => 'Ⴉ', + 'ⴊ' => 'Ⴊ', + 'ⴋ' => 'Ⴋ', + 'ⴌ' => 'Ⴌ', + 'ⴍ' => 'Ⴍ', + 'ⴎ' => 'Ⴎ', + 'ⴏ' => 'Ⴏ', + 'ⴐ' => 'Ⴐ', + 'ⴑ' => 'Ⴑ', + 'ⴒ' => 'Ⴒ', + 'ⴓ' => 'Ⴓ', + 'ⴔ' => 'Ⴔ', + 'ⴕ' => 'Ⴕ', + 'ⴖ' => 'Ⴖ', + 'ⴗ' => 'Ⴗ', + 'ⴘ' => 'Ⴘ', + 'ⴙ' => 'Ⴙ', + 'ⴚ' => 'Ⴚ', + 'ⴛ' => 'Ⴛ', + 'ⴜ' => 'Ⴜ', + 'ⴝ' => 'Ⴝ', + 'ⴞ' => 'Ⴞ', + 'ⴟ' => 'Ⴟ', + 'ⴠ' => 'Ⴠ', + 'ⴡ' => 'Ⴡ', + 'ⴢ' => 'Ⴢ', + 'ⴣ' => 'Ⴣ', + 'ⴤ' => 'Ⴤ', + 'ⴥ' => 'Ⴥ', + 'ⴧ' => 'Ⴧ', + 'ⴭ' => 'Ⴭ', + 'ꙁ' => 'Ꙁ', + 'ꙃ' => 'Ꙃ', + 'ꙅ' => 'Ꙅ', + 'ꙇ' => 'Ꙇ', + 'ꙉ' => 'Ꙉ', + 'ꙋ' => 'Ꙋ', + 'ꙍ' => 'Ꙍ', + 'ꙏ' => 'Ꙏ', + 'ꙑ' => 'Ꙑ', + 'ꙓ' => 'Ꙓ', + 'ꙕ' => 'Ꙕ', + 'ꙗ' => 'Ꙗ', + 'ꙙ' => 'Ꙙ', + 'ꙛ' => 'Ꙛ', + 'ꙝ' => 'Ꙝ', + 'ꙟ' => 'Ꙟ', + 'ꙡ' => 'Ꙡ', + 'ꙣ' => 'Ꙣ', + 'ꙥ' => 'Ꙥ', + 'ꙧ' => 'Ꙧ', + 'ꙩ' => 'Ꙩ', + 'ꙫ' => 'Ꙫ', + 'ꙭ' => 'Ꙭ', + 'ꚁ' => 'Ꚁ', + 'ꚃ' => 'Ꚃ', + 'ꚅ' => 'Ꚅ', + 'ꚇ' => 'Ꚇ', + 'ꚉ' => 'Ꚉ', + 'ꚋ' => 'Ꚋ', + 'ꚍ' => 'Ꚍ', + 'ꚏ' => 'Ꚏ', + 'ꚑ' => 'Ꚑ', + 'ꚓ' => 'Ꚓ', + 'ꚕ' => 'Ꚕ', + 'ꚗ' => 'Ꚗ', + 'ꚙ' => 'Ꚙ', + 'ꚛ' => 'Ꚛ', + 'ꜣ' => 'Ꜣ', + 'ꜥ' => 'Ꜥ', + 'ꜧ' => 'Ꜧ', + 'ꜩ' => 'Ꜩ', + 'ꜫ' => 'Ꜫ', + 'ꜭ' => 'Ꜭ', + 'ꜯ' => 'Ꜯ', + 'ꜳ' => 'Ꜳ', + 'ꜵ' => 'Ꜵ', + 'ꜷ' => 'Ꜷ', + 'ꜹ' => 'Ꜹ', + 'ꜻ' => 'Ꜻ', + 'ꜽ' => 'Ꜽ', + 'ꜿ' => 'Ꜿ', + 'ꝁ' => 'Ꝁ', + 'ꝃ' => 'Ꝃ', + 'ꝅ' => 'Ꝅ', + 'ꝇ' => 'Ꝇ', + 'ꝉ' => 'Ꝉ', + 'ꝋ' => 'Ꝋ', + 'ꝍ' => 'Ꝍ', + 'ꝏ' => 'Ꝏ', + 'ꝑ' => 'Ꝑ', + 'ꝓ' => 'Ꝓ', + 'ꝕ' => 'Ꝕ', + 'ꝗ' => 'Ꝗ', + 'ꝙ' => 'Ꝙ', + 'ꝛ' => 'Ꝛ', + 'ꝝ' => 'Ꝝ', + 'ꝟ' => 'Ꝟ', + 'ꝡ' => 'Ꝡ', + 'ꝣ' => 'Ꝣ', + 'ꝥ' => 'Ꝥ', + 'ꝧ' => 'Ꝧ', + 'ꝩ' => 'Ꝩ', + 'ꝫ' => 'Ꝫ', + 'ꝭ' => 'Ꝭ', + 'ꝯ' => 'Ꝯ', + 'ꝺ' => 'Ꝺ', + 'ꝼ' => 'Ꝼ', + 'ꝿ' => 'Ꝿ', + 'ꞁ' => 'Ꞁ', + 'ꞃ' => 'Ꞃ', + 'ꞅ' => 'Ꞅ', + 'ꞇ' => 'Ꞇ', + 'ꞌ' => 'Ꞌ', + 'ꞑ' => 'Ꞑ', + 'ꞓ' => 'Ꞓ', + 'ꞗ' => 'Ꞗ', + 'ꞙ' => 'Ꞙ', + 'ꞛ' => 'Ꞛ', + 'ꞝ' => 'Ꞝ', + 'ꞟ' => 'Ꞟ', + 'ꞡ' => 'Ꞡ', + 'ꞣ' => 'Ꞣ', + 'ꞥ' => 'Ꞥ', + 'ꞧ' => 'Ꞧ', + 'ꞩ' => 'Ꞩ', + 'a' => 'A', + 'b' => 'B', + 'c' => 'C', + 'd' => 'D', + 'e' => 'E', + 'f' => 'F', + 'g' => 'G', + 'h' => 'H', + 'i' => 'I', + 'j' => 'J', + 'k' => 'K', + 'l' => 'L', + 'm' => 'M', + 'n' => 'N', + 'o' => 'O', + 'p' => 'P', + 'q' => 'Q', + 'r' => 'R', + 's' => 'S', + 't' => 'T', + 'u' => 'U', + 'v' => 'V', + 'w' => 'W', + 'x' => 'X', + 'y' => 'Y', + 'z' => 'Z', + '𐐨' => '𐐀', + '𐐩' => '𐐁', + '𐐪' => '𐐂', + '𐐫' => '𐐃', + '𐐬' => '𐐄', + '𐐭' => '𐐅', + '𐐮' => '𐐆', + '𐐯' => '𐐇', + '𐐰' => '𐐈', + '𐐱' => '𐐉', + '𐐲' => '𐐊', + '𐐳' => '𐐋', + '𐐴' => '𐐌', + '𐐵' => '𐐍', + '𐐶' => '𐐎', + '𐐷' => '𐐏', + '𐐸' => '𐐐', + '𐐹' => '𐐑', + '𐐺' => '𐐒', + '𐐻' => '𐐓', + '𐐼' => '𐐔', + '𐐽' => '𐐕', + '𐐾' => '𐐖', + '𐐿' => '𐐗', + '𐑀' => '𐐘', + '𐑁' => '𐐙', + '𐑂' => '𐐚', + '𐑃' => '𐐛', + '𐑄' => '𐐜', + '𐑅' => '𐐝', + '𐑆' => '𐐞', + '𐑇' => '𐐟', + '𐑈' => '𐐠', + '𐑉' => '𐐡', + '𐑊' => '𐐢', + '𐑋' => '𐐣', + '𐑌' => '𐐤', + '𐑍' => '𐐥', + '𐑎' => '𐐦', + '𐑏' => '𐐧', + '𑣀' => '𑢠', + '𑣁' => '𑢡', + '𑣂' => '𑢢', + '𑣃' => '𑢣', + '𑣄' => '𑢤', + '𑣅' => '𑢥', + '𑣆' => '𑢦', + '𑣇' => '𑢧', + '𑣈' => '𑢨', + '𑣉' => '𑢩', + '𑣊' => '𑢪', + '𑣋' => '𑢫', + '𑣌' => '𑢬', + '𑣍' => '𑢭', + '𑣎' => '𑢮', + '𑣏' => '𑢯', + '𑣐' => '𑢰', + '𑣑' => '𑢱', + '𑣒' => '𑢲', + '𑣓' => '𑢳', + '𑣔' => '𑢴', + '𑣕' => '𑢵', + '𑣖' => '𑢶', + '𑣗' => '𑢷', + '𑣘' => '𑢸', + '𑣙' => '𑢹', + '𑣚' => '𑢺', + '𑣛' => '𑢻', + '𑣜' => '𑢼', + '𑣝' => '𑢽', + '𑣞' => '𑢾', + '𑣟' => '𑢿', +); diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/bootstrap.php b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/bootstrap.php new file mode 100644 index 0000000..8c8225c --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/bootstrap.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Mbstring as p; + +if (!defined('MB_CASE_UPPER')) { + define('MB_CASE_UPPER', 0); + define('MB_CASE_LOWER', 1); + define('MB_CASE_TITLE', 2); +} + +if (!function_exists('mb_strlen')) { + function mb_convert_encoding($s, $to, $from = null) { return p\Mbstring::mb_convert_encoding($s, $to, $from); } + function mb_decode_mimeheader($s) { return p\Mbstring::mb_decode_mimeheader($s); } + function mb_encode_mimeheader($s, $charset = null, $transferEnc = null, $lf = null, $indent = null) { return p\Mbstring::mb_encode_mimeheader($s, $charset, $transferEnc, $lf, $indent); } + function mb_decode_numericentity($s, $convmap, $enc = null) { return p\Mbstring::mb_decode_numericentity($s, $convmap, $enc); } + function mb_encode_numericentity($s, $convmap, $enc = null, $is_hex = false) { return p\Mbstring::mb_encode_numericentity($s, $convmap, $enc, $is_hex); } + function mb_convert_case($s, $mode, $enc = null) { return p\Mbstring::mb_convert_case($s, $mode, $enc); } + function mb_internal_encoding($enc = null) { return p\Mbstring::mb_internal_encoding($enc); } + function mb_language($lang = null) { return p\Mbstring::mb_language($lang); } + function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } + function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } + function mb_check_encoding($var = null, $encoding = null) { return p\Mbstring::mb_check_encoding($var, $encoding); } + function mb_detect_encoding($str, $encodingList = null, $strict = false) { return p\Mbstring::mb_detect_encoding($str, $encodingList, $strict); } + function mb_detect_order($encodingList = null) { return p\Mbstring::mb_detect_order($encodingList); } + function mb_parse_str($s, &$result = array()) { parse_str($s, $result); } + function mb_strlen($s, $enc = null) { return p\Mbstring::mb_strlen($s, $enc); } + function mb_strpos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strpos($s, $needle, $offset, $enc); } + function mb_strtolower($s, $enc = null) { return p\Mbstring::mb_strtolower($s, $enc); } + function mb_strtoupper($s, $enc = null) { return p\Mbstring::mb_strtoupper($s, $enc); } + function mb_substitute_character($char = null) { return p\Mbstring::mb_substitute_character($char); } + function mb_substr($s, $start, $length = 2147483647, $enc = null) { return p\Mbstring::mb_substr($s, $start, $length, $enc); } + function mb_stripos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_stripos($s, $needle, $offset, $enc); } + function mb_stristr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_stristr($s, $needle, $part, $enc); } + function mb_strrchr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strrchr($s, $needle, $part, $enc); } + function mb_strrichr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strrichr($s, $needle, $part, $enc); } + function mb_strripos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strripos($s, $needle, $offset, $enc); } + function mb_strrpos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strrpos($s, $needle, $offset, $enc); } + function mb_strstr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strstr($s, $needle, $part, $enc); } + function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } + function mb_http_output($enc = null) { return p\Mbstring::mb_http_output($enc); } + function mb_strwidth($s, $enc = null) { return p\Mbstring::mb_strwidth($s, $enc); } + function mb_substr_count($haystack, $needle, $enc = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $enc); } + function mb_output_handler($contents, $status) { return p\Mbstring::mb_output_handler($contents, $status); } + function mb_http_input($type = '') { return p\Mbstring::mb_http_input($type); } + function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) { return p\Mbstring::mb_convert_variables($toEncoding, $fromEncoding, $a, $b, $c, $d, $e, $f); } +} +if (!function_exists('mb_chr')) { + function mb_ord($s, $enc = null) { return p\Mbstring::mb_ord($s, $enc); } + function mb_chr($code, $enc = null) { return p\Mbstring::mb_chr($code, $enc); } + function mb_scrub($s, $enc = null) { $enc = null === $enc ? mb_internal_encoding() : $enc; return mb_convert_encoding($s, $enc, $enc); } +} + +if (!function_exists('mb_str_split')) { + function mb_str_split($string, $split_length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $split_length, $encoding); } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/composer.json b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/composer.json new file mode 100644 index 0000000..02b8896 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-mbstring/composer.json @@ -0,0 +1,34 @@ +{ + "name": "symfony/polyfill-mbstring", + "type": "library", + "description": "Symfony polyfill for the Mbstring extension", + "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-php72/LICENSE b/plugins/panakour/backup/vendor/symfony/polyfill-php72/LICENSE new file mode 100644 index 0000000..4cd8bdd --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-php72/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2019 Fabien Potencier + +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/plugins/panakour/backup/vendor/symfony/polyfill-php72/Php72.php b/plugins/panakour/backup/vendor/symfony/polyfill-php72/Php72.php new file mode 100644 index 0000000..d531e84 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-php72/Php72.php @@ -0,0 +1,216 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php72; + +/** + * @author Nicolas Grekas + * @author Dariusz Rumiński + * + * @internal + */ +final class Php72 +{ + private static $hashMask; + + public static function utf8_encode($s) + { + $s .= $s; + $len = \strlen($s); + + for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) { + switch (true) { + case $s[$i] < "\x80": $s[$j] = $s[$i]; break; + case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break; + default: $s[$j] = "\xC3"; $s[++$j] = \chr(\ord($s[$i]) - 64); break; + } + } + + return substr($s, 0, $j); + } + + public static function utf8_decode($s) + { + $s = (string) $s; + $len = \strlen($s); + + for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) { + switch ($s[$i] & "\xF0") { + case "\xC0": + case "\xD0": + $c = (\ord($s[$i] & "\x1F") << 6) | \ord($s[++$i] & "\x3F"); + $s[$j] = $c < 256 ? \chr($c) : '?'; + break; + + case "\xF0": + ++$i; + // no break + + case "\xE0": + $s[$j] = '?'; + $i += 2; + break; + + default: + $s[$j] = $s[$i]; + } + } + + return substr($s, 0, $j); + } + + public static function php_os_family() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + return 'Windows'; + } + + $map = array( + 'Darwin' => 'Darwin', + 'DragonFly' => 'BSD', + 'FreeBSD' => 'BSD', + 'NetBSD' => 'BSD', + 'OpenBSD' => 'BSD', + 'Linux' => 'Linux', + 'SunOS' => 'Solaris', + ); + + return isset($map[PHP_OS]) ? $map[PHP_OS] : 'Unknown'; + } + + public static function spl_object_id($object) + { + if (null === self::$hashMask) { + self::initHashMask(); + } + if (null === $hash = spl_object_hash($object)) { + return; + } + + return self::$hashMask ^ hexdec(substr($hash, 16 - \PHP_INT_SIZE, \PHP_INT_SIZE)); + } + + public static function sapi_windows_vt100_support($stream, $enable = null) + { + if (!\is_resource($stream)) { + trigger_error('sapi_windows_vt100_support() expects parameter 1 to be resource, '.\gettype($stream).' given', E_USER_WARNING); + + return false; + } + + $meta = stream_get_meta_data($stream); + + if ('STDIO' !== $meta['stream_type']) { + trigger_error('sapi_windows_vt100_support() was not able to analyze the specified stream', E_USER_WARNING); + + return false; + } + + // We cannot actually disable vt100 support if it is set + if (false === $enable || !self::stream_isatty($stream)) { + return false; + } + + // The native function does not apply to stdin + $meta = array_map('strtolower', $meta); + $stdin = 'php://stdin' === $meta['uri'] || 'php://fd/0' === $meta['uri']; + + return !$stdin + && (false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM') + || 'Hyper' === getenv('TERM_PROGRAM')); + } + + public static function stream_isatty($stream) + { + if (!\is_resource($stream)) { + trigger_error('stream_isatty() expects parameter 1 to be resource, '.\gettype($stream).' given', E_USER_WARNING); + + return false; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + $stat = @fstat($stream); + // Check if formatted mode is S_IFCHR + return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; + } + + return \function_exists('posix_isatty') && @posix_isatty($stream); + } + + private static function initHashMask() + { + $obj = (object) array(); + self::$hashMask = -1; + + // check if we are nested in an output buffering handler to prevent a fatal error with ob_start() below + $obFuncs = array('ob_clean', 'ob_end_clean', 'ob_flush', 'ob_end_flush', 'ob_get_contents', 'ob_get_flush'); + foreach (debug_backtrace(\PHP_VERSION_ID >= 50400 ? DEBUG_BACKTRACE_IGNORE_ARGS : false) as $frame) { + if (isset($frame['function'][0]) && !isset($frame['class']) && 'o' === $frame['function'][0] && \in_array($frame['function'], $obFuncs)) { + $frame['line'] = 0; + break; + } + } + if (!empty($frame['line'])) { + ob_start(); + debug_zval_dump($obj); + self::$hashMask = (int) substr(ob_get_clean(), 17); + } + + self::$hashMask ^= hexdec(substr(spl_object_hash($obj), 16 - \PHP_INT_SIZE, \PHP_INT_SIZE)); + } + + public static function mb_chr($code, $encoding = null) + { + if (0x80 > $code %= 0x200000) { + $s = \chr($code); + } elseif (0x800 > $code) { + $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } else { + $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } + + if ('UTF-8' !== $encoding) { + $s = mb_convert_encoding($s, $encoding, 'UTF-8'); + } + + return $s; + } + + public static function mb_ord($s, $encoding = null) + { + if (null == $encoding) { + $s = mb_convert_encoding($s, 'UTF-8'); + } elseif ('UTF-8' !== $encoding) { + $s = mb_convert_encoding($s, 'UTF-8', $encoding); + } + + if (1 === \strlen($s)) { + return \ord($s); + } + + $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; + if (0xF0 <= $code) { + return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; + } + if (0xE0 <= $code) { + return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; + } + if (0xC0 <= $code) { + return (($code - 0xC0) << 6) + $s[2] - 0x80; + } + + return $code; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-php72/README.md b/plugins/panakour/backup/vendor/symfony/polyfill-php72/README.md new file mode 100644 index 0000000..8ac181e --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-php72/README.md @@ -0,0 +1,27 @@ +Symfony Polyfill / Php72 +======================== + +This component provides functions added to PHP 7.2 core: + +- [`spl_object_id`](https://php.net/spl_object_id) +- [`stream_isatty`](https://php.net/stream_isatty) + +On Windows only: + +- [`sapi_windows_vt100_support`](https://php.net/sapi_windows_vt100_support) + +Moved to core since 7.2 (was in the optional XML extension earlier): + +- [`utf8_encode`](https://php.net/utf8_encode) +- [`utf8_decode`](https://php.net/utf8_decode) + +Also, it provides a constant added to PHP 7.2: +- [`PHP_OS_FAMILY`](https://php.net/reserved.constants#constant.php-os-family) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-php72/bootstrap.php b/plugins/panakour/backup/vendor/symfony/polyfill-php72/bootstrap.php new file mode 100644 index 0000000..519056d --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-php72/bootstrap.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php72 as p; + +if (PHP_VERSION_ID < 70200) { + if ('\\' === DIRECTORY_SEPARATOR && !function_exists('sapi_windows_vt100_support')) { + function sapi_windows_vt100_support($stream, $enable = null) { return p\Php72::sapi_windows_vt100_support($stream, $enable); } + } + if (!function_exists('stream_isatty')) { + function stream_isatty($stream) { return p\Php72::stream_isatty($stream); } + } + if (!function_exists('utf8_encode')) { + function utf8_encode($s) { return p\Php72::utf8_encode($s); } + function utf8_decode($s) { return p\Php72::utf8_decode($s); } + } + if (!function_exists('spl_object_id')) { + function spl_object_id($s) { return p\Php72::spl_object_id($s); } + } + if (!defined('PHP_OS_FAMILY')) { + define('PHP_OS_FAMILY', p\Php72::php_os_family()); + } + if (!function_exists('mb_chr')) { + function mb_ord($s, $enc = null) { return p\Php72::mb_ord($s, $enc); } + function mb_chr($code, $enc = null) { return p\Php72::mb_chr($code, $enc); } + function mb_scrub($s, $enc = null) { $enc = null === $enc ? mb_internal_encoding() : $enc; return mb_convert_encoding($s, $enc, $enc); } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/polyfill-php72/composer.json b/plugins/panakour/backup/vendor/symfony/polyfill-php72/composer.json new file mode 100644 index 0000000..27901f2 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/polyfill-php72/composer.json @@ -0,0 +1,31 @@ +{ + "name": "symfony/polyfill-php72", + "type": "library", + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php72\\": "" }, + "files": [ "bootstrap.php" ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.15-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/CHANGELOG.md b/plugins/panakour/backup/vendor/symfony/process/CHANGELOG.md new file mode 100644 index 0000000..c5cdb99 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/CHANGELOG.md @@ -0,0 +1,57 @@ +CHANGELOG +========= + +3.4.0 +----- + + * deprecated the ProcessBuilder class + * deprecated calling `Process::start()` without setting a valid working directory beforehand (via `setWorkingDirectory()` or constructor) + +3.3.0 +----- + + * added command line arrays in the `Process` class + * added `$env` argument to `Process::start()`, `run()`, `mustRun()` and `restart()` methods + * deprecated the `ProcessUtils::escapeArgument()` method + * deprecated not inheriting environment variables + * deprecated configuring `proc_open()` options + * deprecated configuring enhanced Windows compatibility + * deprecated configuring enhanced sigchild compatibility + +2.5.0 +----- + + * added support for PTY mode + * added the convenience method "mustRun" + * deprecation: Process::setStdin() is deprecated in favor of Process::setInput() + * deprecation: Process::getStdin() is deprecated in favor of Process::getInput() + * deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types + +2.4.0 +----- + + * added the ability to define an idle timeout + +2.3.0 +----- + + * added ProcessUtils::escapeArgument() to fix the bug in escapeshellarg() function on Windows + * added Process::signal() + * added Process::getPid() + * added support for a TTY mode + +2.2.0 +----- + + * added ProcessBuilder::setArguments() to reset the arguments on a builder + * added a way to retrieve the standard and error output incrementally + * added Process:restart() + +2.1.0 +----- + + * added support for non-blocking processes (start(), wait(), isRunning(), stop()) + * enhanced Windows compatibility + * added Process::getExitCodeText() that returns a string representation for + the exit code returned by the process + * added ProcessBuilder diff --git a/plugins/panakour/backup/vendor/symfony/process/Exception/ExceptionInterface.php b/plugins/panakour/backup/vendor/symfony/process/Exception/ExceptionInterface.php new file mode 100644 index 0000000..75c1c9e --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * Marker Interface for the Process Component. + * + * @author Johannes M. Schmitt + */ +interface ExceptionInterface +{ +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Exception/InvalidArgumentException.php b/plugins/panakour/backup/vendor/symfony/process/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..926ee21 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * InvalidArgumentException for the Process Component. + * + * @author Romain Neutron + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Exception/LogicException.php b/plugins/panakour/backup/vendor/symfony/process/Exception/LogicException.php new file mode 100644 index 0000000..be3d490 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Exception/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * LogicException for the Process Component. + * + * @author Romain Neutron + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Exception/ProcessFailedException.php b/plugins/panakour/backup/vendor/symfony/process/Exception/ProcessFailedException.php new file mode 100644 index 0000000..328acfd --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Exception/ProcessFailedException.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for failed processes. + * + * @author Johannes M. Schmitt + */ +class ProcessFailedException extends RuntimeException +{ + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", + $process->getCommandLine(), + $process->getExitCode(), + $process->getExitCodeText(), + $process->getWorkingDirectory() + ); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", + $process->getOutput(), + $process->getErrorOutput() + ); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Exception/ProcessTimedOutException.php b/plugins/panakour/backup/vendor/symfony/process/Exception/ProcessTimedOutException.php new file mode 100644 index 0000000..fef4a8a --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Exception/ProcessTimedOutException.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception that is thrown when a process times out. + * + * @author Johannes M. Schmitt + */ +class ProcessTimedOutException extends RuntimeException +{ + const TYPE_GENERAL = 1; + const TYPE_IDLE = 2; + + private $process; + private $timeoutType; + + public function __construct(Process $process, $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf( + 'The process "%s" exceeded the timeout of %s seconds.', + $process->getCommandLine(), + $this->getExceededTimeout() + )); + } + + public function getProcess() + { + return $this->process; + } + + public function isGeneralTimeout() + { + return self::TYPE_GENERAL === $this->timeoutType; + } + + public function isIdleTimeout() + { + return self::TYPE_IDLE === $this->timeoutType; + } + + public function getExceededTimeout() + { + switch ($this->timeoutType) { + case self::TYPE_GENERAL: + return $this->process->getTimeout(); + + case self::TYPE_IDLE: + return $this->process->getIdleTimeout(); + + default: + throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Exception/RuntimeException.php b/plugins/panakour/backup/vendor/symfony/process/Exception/RuntimeException.php new file mode 100644 index 0000000..adead25 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * RuntimeException for the Process Component. + * + * @author Johannes M. Schmitt + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/plugins/panakour/backup/vendor/symfony/process/ExecutableFinder.php b/plugins/panakour/backup/vendor/symfony/process/ExecutableFinder.php new file mode 100644 index 0000000..cb4345e --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/ExecutableFinder.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * Generic executable finder. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class ExecutableFinder +{ + private $suffixes = ['.exe', '.bat', '.cmd', '.com']; + + /** + * Replaces default suffixes of executable. + */ + public function setSuffixes(array $suffixes) + { + $this->suffixes = $suffixes; + } + + /** + * Adds new possible suffix to check for executable. + * + * @param string $suffix + */ + public function addSuffix($suffix) + { + $this->suffixes[] = $suffix; + } + + /** + * Finds an executable by name. + * + * @param string $name The executable name (without the extension) + * @param string|null $default The default to return if no executable is found + * @param array $extraDirs Additional dirs to check into + * + * @return string|null The executable path or default value + */ + public function find($name, $default = null, array $extraDirs = []) + { + if (ini_get('open_basedir')) { + $searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs); + $dirs = []; + foreach ($searchPath as $path) { + // Silencing against https://bugs.php.net/69240 + if (@is_dir($path)) { + $dirs[] = $path; + } else { + if (basename($path) == $name && @is_executable($path)) { + return $path; + } + } + } + } else { + $dirs = array_merge( + explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); + } + + $suffixes = ['']; + if ('\\' === \DIRECTORY_SEPARATOR) { + $pathExt = getenv('PATHEXT'); + $suffixes = array_merge($pathExt ? explode(PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); + } + foreach ($suffixes as $suffix) { + foreach ($dirs as $dir) { + if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { + return $file; + } + } + } + + return $default; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/InputStream.php b/plugins/panakour/backup/vendor/symfony/process/InputStream.php new file mode 100644 index 0000000..ef38dd7 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/InputStream.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * Provides a way to continuously write to the input of a Process until the InputStream is closed. + * + * @author Nicolas Grekas + */ +class InputStream implements \IteratorAggregate +{ + /** @var callable|null */ + private $onEmpty = null; + private $input = []; + private $open = true; + + /** + * Sets a callback that is called when the write buffer becomes empty. + */ + public function onEmpty(callable $onEmpty = null) + { + $this->onEmpty = $onEmpty; + } + + /** + * Appends an input to the write buffer. + * + * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, + * stream resource or \Traversable + */ + public function write($input) + { + if (null === $input) { + return; + } + if ($this->isClosed()) { + throw new RuntimeException(sprintf('"%s" is closed.', static::class)); + } + $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); + } + + /** + * Closes the write buffer. + */ + public function close() + { + $this->open = false; + } + + /** + * Tells whether the write buffer is closed or not. + */ + public function isClosed() + { + return !$this->open; + } + + public function getIterator() + { + $this->open = true; + + while ($this->open || $this->input) { + if (!$this->input) { + yield ''; + continue; + } + $current = array_shift($this->input); + + if ($current instanceof \Iterator) { + foreach ($current as $cur) { + yield $cur; + } + } else { + yield $current; + } + if (!$this->input && $this->open && null !== $onEmpty = $this->onEmpty) { + $this->write($onEmpty($this)); + } + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/LICENSE b/plugins/panakour/backup/vendor/symfony/process/LICENSE new file mode 100644 index 0000000..9e936ec --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2020 Fabien Potencier + +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/plugins/panakour/backup/vendor/symfony/process/PhpExecutableFinder.php b/plugins/panakour/backup/vendor/symfony/process/PhpExecutableFinder.php new file mode 100644 index 0000000..a97aa12 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/PhpExecutableFinder.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * An executable finder specifically designed for the PHP executable. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class PhpExecutableFinder +{ + private $executableFinder; + + public function __construct() + { + $this->executableFinder = new ExecutableFinder(); + } + + /** + * Finds The PHP executable. + * + * @param bool $includeArgs Whether or not include command arguments + * + * @return string|false The PHP executable path or false if it cannot be found + */ + public function find($includeArgs = true) + { + $args = $this->findArguments(); + $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; + + // HHVM support + if (\defined('HHVM_VERSION')) { + return (getenv('PHP_BINARY') ?: PHP_BINARY).$args; + } + + // PHP_BINARY return the current sapi executable + if (PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) { + return PHP_BINARY.$args; + } + + if ($php = getenv('PHP_PATH')) { + if (!@is_executable($php)) { + return false; + } + + return $php; + } + + if ($php = getenv('PHP_PEAR_PHP_BIN')) { + if (@is_executable($php)) { + return $php; + } + } + + if (@is_executable($php = PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php'))) { + return $php; + } + + $dirs = [PHP_BINDIR]; + if ('\\' === \DIRECTORY_SEPARATOR) { + $dirs[] = 'C:\xampp\php\\'; + } + + return $this->executableFinder->find('php', false, $dirs); + } + + /** + * Finds the PHP executable arguments. + * + * @return array The PHP executable arguments + */ + public function findArguments() + { + $arguments = []; + + if (\defined('HHVM_VERSION')) { + $arguments[] = '--php'; + } elseif ('phpdbg' === \PHP_SAPI) { + $arguments[] = '-qrr'; + } + + return $arguments; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/PhpProcess.php b/plugins/panakour/backup/vendor/symfony/process/PhpProcess.php new file mode 100644 index 0000000..f0c47b2 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/PhpProcess.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpProcess runs a PHP script in an independent process. + * + * $p = new PhpProcess(''); + * $p->run(); + * print $p->getOutput()."\n"; + * + * @author Fabien Potencier + */ +class PhpProcess extends Process +{ + /** + * @param string $script The PHP script to run (as a string) + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int $timeout The timeout in seconds + * @param array $options An array of options for proc_open + */ + public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = null) + { + $executableFinder = new PhpExecutableFinder(); + if (false === $php = $executableFinder->find(false)) { + $php = null; + } else { + $php = array_merge([$php], $executableFinder->findArguments()); + } + if ('phpdbg' === \PHP_SAPI) { + $file = tempnam(sys_get_temp_dir(), 'dbg'); + file_put_contents($file, $script); + register_shutdown_function('unlink', $file); + $php[] = $file; + $script = null; + } + if (null !== $options) { + @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since Symfony 3.3 and will be removed in 4.0.', __CLASS__), E_USER_DEPRECATED); + } + + parent::__construct($php, $cwd, $env, $script, $timeout, $options); + } + + /** + * Sets the path to the PHP binary to use. + */ + public function setPhpBinary($php) + { + $this->setCommandLine($php); + } + + /** + * {@inheritdoc} + */ + public function start(callable $callback = null/*, array $env = []*/) + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + $env = 1 < \func_num_args() ? func_get_arg(1) : null; + + parent::start($callback, $env); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Pipes/AbstractPipes.php b/plugins/panakour/backup/vendor/symfony/process/Pipes/AbstractPipes.php new file mode 100644 index 0000000..cdffaaf --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Pipes/AbstractPipes.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * @author Romain Neutron + * + * @internal + */ +abstract class AbstractPipes implements PipesInterface +{ + public $pipes = []; + + private $inputBuffer = ''; + private $input; + private $blocked = true; + private $lastError; + + /** + * @param resource|string|int|float|bool|\Iterator|null $input + */ + public function __construct($input) + { + if (\is_resource($input) || $input instanceof \Iterator) { + $this->input = $input; + } elseif (\is_string($input)) { + $this->inputBuffer = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = []; + } + + /** + * Returns true if a system call has been interrupted. + * + * @return bool + */ + protected function hasSystemCallBeenInterrupted() + { + $lastError = $this->lastError; + $this->lastError = null; + + // stream_select returns false when the `select` system call is interrupted by an incoming signal + return null !== $lastError && false !== stripos($lastError, 'interrupted system call'); + } + + /** + * Unblocks streams. + */ + protected function unblock() + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (\is_resource($this->input)) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } + + /** + * Writes input to stdin. + * + * @return array|null + * + * @throws InvalidArgumentException When an input iterator yields a non supported value + */ + protected function write() + { + if (!isset($this->pipes[0])) { + return null; + } + $input = $this->input; + + if ($input instanceof \Iterator) { + if (!$input->valid()) { + $input = null; + } elseif (\is_resource($input = $input->current())) { + stream_set_blocking($input, 0); + } elseif (!isset($this->inputBuffer[0])) { + if (!\is_string($input)) { + if (!is_scalar($input)) { + throw new InvalidArgumentException(sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', \get_class($this->input), \gettype($input))); + } + $input = (string) $input; + } + $this->inputBuffer = $input; + $this->input->next(); + $input = null; + } else { + $input = null; + } + } + + $r = $e = []; + $w = [$this->pipes[0]]; + + // let's have a look if something changed in streams + if (false === @stream_select($r, $w, $e, 0, 0)) { + return null; + } + + foreach ($w as $stdin) { + if (isset($this->inputBuffer[0])) { + $written = fwrite($stdin, $this->inputBuffer); + $this->inputBuffer = substr($this->inputBuffer, $written); + if (isset($this->inputBuffer[0])) { + return [$this->pipes[0]]; + } + } + + if ($input) { + for (;;) { + $data = fread($input, self::CHUNK_SIZE); + if (!isset($data[0])) { + break; + } + $written = fwrite($stdin, $data); + $data = substr($data, $written); + if (isset($data[0])) { + $this->inputBuffer = $data; + + return [$this->pipes[0]]; + } + } + if (feof($input)) { + if ($this->input instanceof \Iterator) { + $this->input->next(); + } else { + $this->input = null; + } + } + } + } + + // no input to read on resource, buffer is empty + if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) { + $this->input = null; + fclose($this->pipes[0]); + unset($this->pipes[0]); + } elseif (!$w) { + return [$this->pipes[0]]; + } + + return null; + } + + /** + * @internal + */ + public function handleError($type, $msg) + { + $this->lastError = $msg; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Pipes/PipesInterface.php b/plugins/panakour/backup/vendor/symfony/process/Pipes/PipesInterface.php new file mode 100644 index 0000000..52bbe76 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Pipes/PipesInterface.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * PipesInterface manages descriptors and pipes for the use of proc_open. + * + * @author Romain Neutron + * + * @internal + */ +interface PipesInterface +{ + const CHUNK_SIZE = 16384; + + /** + * Returns an array of descriptors for the use of proc_open. + * + * @return array + */ + public function getDescriptors(); + + /** + * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. + * + * @return string[] + */ + public function getFiles(); + + /** + * Reads data in file handles and pipes. + * + * @param bool $blocking Whether to use blocking calls or not + * @param bool $close Whether to close pipes if they've reached EOF + * + * @return string[] An array of read data indexed by their fd + */ + public function readAndWrite($blocking, $close = false); + + /** + * Returns if the current state has open file handles or pipes. + * + * @return bool + */ + public function areOpen(); + + /** + * Returns if pipes are able to read output. + * + * @return bool + */ + public function haveReadSupport(); + + /** + * Closes file handles and pipes. + */ + public function close(); +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Pipes/UnixPipes.php b/plugins/panakour/backup/vendor/symfony/process/Pipes/UnixPipes.php new file mode 100644 index 0000000..1ebf213 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Pipes/UnixPipes.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; + +/** + * UnixPipes implementation uses unix pipes as handles. + * + * @author Romain Neutron + * + * @internal + */ +class UnixPipes extends AbstractPipes +{ + private $ttyMode; + private $ptyMode; + private $haveReadSupport; + + public function __construct($ttyMode, $ptyMode, $input, $haveReadSupport) + { + $this->ttyMode = (bool) $ttyMode; + $this->ptyMode = (bool) $ptyMode; + $this->haveReadSupport = (bool) $haveReadSupport; + + parent::__construct($input); + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if (!$this->haveReadSupport) { + $nullstream = fopen('/dev/null', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + if ($this->ttyMode) { + return [ + ['file', '/dev/tty', 'r'], + ['file', '/dev/tty', 'w'], + ['file', '/dev/tty', 'w'], + ]; + } + + if ($this->ptyMode && Process::isPtySupported()) { + return [ + ['pty'], + ['pty'], + ['pty'], + ]; + } + + return [ + ['pipe', 'r'], + ['pipe', 'w'], // stdout + ['pipe', 'w'], // stderr + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->unblock(); + $w = $this->write(); + + $read = $e = []; + $r = $this->pipes; + unset($r[0]); + + // let's have a look if something changed in streams + set_error_handler([$this, 'handleError']); + if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + restore_error_handler(); + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return $read; + } + restore_error_handler(); + + foreach ($r as $pipe) { + // prior PHP 5.4 the array passed to stream_select is modified and + // lose key association, we have to find back the key + $read[$type = array_search($pipe, $this->pipes, true)] = ''; + + do { + $data = fread($pipe, self::CHUNK_SIZE); + $read[$type] .= $data; + } while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1]))); + + if (!isset($read[$type][0])) { + unset($read[$type]); + } + + if ($close && feof($pipe)) { + fclose($pipe); + unset($this->pipes[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function haveReadSupport() + { + return $this->haveReadSupport; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Pipes/WindowsPipes.php b/plugins/panakour/backup/vendor/symfony/process/Pipes/WindowsPipes.php new file mode 100644 index 0000000..44056d5 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Pipes/WindowsPipes.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Process; + +/** + * WindowsPipes implementation uses temporary files as handles. + * + * @see https://bugs.php.net/51800 + * @see https://bugs.php.net/65650 + * + * @author Romain Neutron + * + * @internal + */ +class WindowsPipes extends AbstractPipes +{ + private $files = []; + private $fileHandles = []; + private $lockHandles = []; + private $readBytes = [ + Process::STDOUT => 0, + Process::STDERR => 0, + ]; + private $haveReadSupport; + + public function __construct($input, $haveReadSupport) + { + $this->haveReadSupport = (bool) $haveReadSupport; + + if ($this->haveReadSupport) { + // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. + // Workaround for this problem is to use temporary files instead of pipes on Windows platform. + // + // @see https://bugs.php.net/51800 + $pipes = [ + Process::STDOUT => Process::OUT, + Process::STDERR => Process::ERR, + ]; + $tmpDir = sys_get_temp_dir(); + $lastError = 'unknown reason'; + set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); + for ($i = 0;; ++$i) { + foreach ($pipes as $pipe => $name) { + $file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); + + if (!$h = fopen($file.'.lock', 'w')) { + restore_error_handler(); + throw new RuntimeException(sprintf('A temporary file could not be opened to write the process output: %s.', $lastError)); + } + if (!flock($h, LOCK_EX | LOCK_NB)) { + continue 2; + } + if (isset($this->lockHandles[$pipe])) { + flock($this->lockHandles[$pipe], LOCK_UN); + fclose($this->lockHandles[$pipe]); + } + $this->lockHandles[$pipe] = $h; + + if (!fclose(fopen($file, 'w')) || !$h = fopen($file, 'r')) { + flock($this->lockHandles[$pipe], LOCK_UN); + fclose($this->lockHandles[$pipe]); + unset($this->lockHandles[$pipe]); + continue 2; + } + $this->fileHandles[$pipe] = $h; + $this->files[$pipe] = $file; + } + break; + } + restore_error_handler(); + } + + parent::__construct($input); + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if (!$this->haveReadSupport) { + $nullstream = fopen('NUL', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/51800) + // We're not using file handles as it can produce corrupted output https://bugs.php.net/65650 + // So we redirect output within the commandline and pass the nul device to the process + return [ + ['pipe', 'r'], + ['file', 'NUL', 'w'], + ['file', 'NUL', 'w'], + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->unblock(); + $w = $this->write(); + $read = $r = $e = []; + + if ($blocking) { + if ($w) { + @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); + } elseif ($this->fileHandles) { + usleep(Process::TIMEOUT_PRECISION * 1E6); + } + } + foreach ($this->fileHandles as $type => $fileHandle) { + $data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]); + + if (isset($data[0])) { + $this->readBytes[$type] += \strlen($data); + $read[$type] = $data; + } + if ($close) { + ftruncate($fileHandle, 0); + fclose($fileHandle); + flock($this->lockHandles[$type], LOCK_UN); + fclose($this->lockHandles[$type]); + unset($this->fileHandles[$type], $this->lockHandles[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function haveReadSupport() + { + return $this->haveReadSupport; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return $this->pipes && $this->fileHandles; + } + + /** + * {@inheritdoc} + */ + public function close() + { + parent::close(); + foreach ($this->fileHandles as $type => $handle) { + ftruncate($handle, 0); + fclose($handle); + flock($this->lockHandles[$type], LOCK_UN); + fclose($this->lockHandles[$type]); + } + $this->fileHandles = $this->lockHandles = []; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Process.php b/plugins/panakour/backup/vendor/symfony/process/Process.php new file mode 100644 index 0000000..68d5251 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Process.php @@ -0,0 +1,1746 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Pipes\PipesInterface; +use Symfony\Component\Process\Pipes\UnixPipes; +use Symfony\Component\Process\Pipes\WindowsPipes; + +/** + * Process is a thin wrapper around proc_* functions to easily + * start independent PHP processes. + * + * @author Fabien Potencier + * @author Romain Neutron + */ +class Process implements \IteratorAggregate +{ + const ERR = 'err'; + const OUT = 'out'; + + const STATUS_READY = 'ready'; + const STATUS_STARTED = 'started'; + const STATUS_TERMINATED = 'terminated'; + + const STDIN = 0; + const STDOUT = 1; + const STDERR = 2; + + // Timeout Precision in seconds. + const TIMEOUT_PRECISION = 0.2; + + const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking + const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory + const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating + const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating + + private $callback; + private $hasCallback = false; + private $commandline; + private $cwd; + private $env; + private $input; + private $starttime; + private $lastOutputTime; + private $timeout; + private $idleTimeout; + private $options = ['suppress_errors' => true]; + private $exitcode; + private $fallbackStatus = []; + private $processInformation; + private $outputDisabled = false; + private $stdout; + private $stderr; + private $enhanceWindowsCompatibility = true; + private $enhanceSigchildCompatibility; + private $process; + private $status = self::STATUS_READY; + private $incrementalOutputOffset = 0; + private $incrementalErrorOutputOffset = 0; + private $tty = false; + private $pty; + private $inheritEnv = false; + + private $useFileHandles = false; + /** @var PipesInterface */ + private $processPipes; + + private $latestSignal; + + private static $sigchild; + + /** + * Exit codes translation table. + * + * User-defined errors must use exit codes in the 64-113 range. + */ + public static $exitCodes = [ + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ]; + + /** + * @param string|array $commandline The command line to run + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input + * @param int|float|null $timeout The timeout in seconds or null to disable + * @param array $options An array of options for proc_open + * + * @throws RuntimeException When proc_open is not installed + */ + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null) + { + if (!\function_exists('proc_open')) { + throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $commandline; + $this->cwd = $cwd; + + // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started + // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected + // @see : https://bugs.php.net/51800 + // @see : https://bugs.php.net/50524 + if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->setInput($input); + $this->setTimeout($timeout); + $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR; + $this->pty = false; + $this->enhanceSigchildCompatibility = '\\' !== \DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); + if (null !== $options) { + @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since Symfony 3.3 and will be removed in 4.0.', __CLASS__), E_USER_DEPRECATED); + $this->options = array_replace($this->options, $options); + } + } + + public function __destruct() + { + $this->stop(0); + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * Runs the process. + * + * The callback receives the type of output (out or err) and + * some bytes from the output in real-time. It allows to have feedback + * from the independent process during execution. + * + * The STDOUT and STDERR are also available after the process is finished + * via the getOutput() and getErrorOutput() methods. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return int The exit status code + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled + * + * @final since version 3.3 + */ + public function run($callback = null/*, array $env = []*/) + { + $env = 1 < \func_num_args() ? func_get_arg(1) : null; + $this->start($callback, $env); + + return $this->wait(); + } + + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @return $this + * + * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled + * @throws ProcessFailedException if the process didn't terminate successfully + * + * @final since version 3.3 + */ + public function mustRun(callable $callback = null/*, array $env = []*/) + { + if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + $env = 1 < \func_num_args() ? func_get_arg(1) : null; + + if (0 !== $this->run($callback, $env)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * Starts the process and returns after writing the input to STDIN. + * + * This method blocks until all STDIN data is sent to the process then it + * returns while the process runs in the background. + * + * The termination of the process can be awaited with wait(). + * + * The callback receives the type of output (out or err) and some bytes from + * the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * @throws LogicException In case a callback is provided and output has been disabled + */ + public function start(callable $callback = null/*, array $env = [*/) + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running.'); + } + if (2 <= \func_num_args()) { + $env = func_get_arg(1); + } else { + if (__CLASS__ !== static::class) { + $r = new \ReflectionMethod($this, __FUNCTION__); + if (__CLASS__ !== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() || 'env' !== $r->getParameters()[1]->name)) { + @trigger_error(sprintf('The %s::start() method expects a second "$env" argument since Symfony 3.3. It will be made mandatory in 4.0.', static::class), E_USER_DEPRECATED); + } + } + $env = null; + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $this->hasCallback = null !== $callback; + $descriptors = $this->getDescriptors(); + $inheritEnv = $this->inheritEnv; + + if (\is_array($commandline = $this->commandline)) { + $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline)); + + if ('\\' !== \DIRECTORY_SEPARATOR) { + // exec is mandatory to deal with sending a signal to the process + $commandline = 'exec '.$commandline; + } + } + + if (null === $env) { + $env = $this->env; + } else { + if ($this->env) { + $env += $this->env; + } + $inheritEnv = true; + } + + if (null !== $env && $inheritEnv) { + $env += $this->getDefaultEnv(); + } elseif (null !== $env) { + @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', E_USER_DEPRECATED); + } else { + $env = $this->getDefaultEnv(); + } + if ('\\' === \DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { + $this->options['bypass_shell'] = true; + $commandline = $this->prepareWindowsCommandLine($commandline, $env); + } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + // last exit code is output on the fourth pipe and caught to work around --enable-sigchild + $descriptors[3] = ['pipe', 'w']; + + // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input + $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; + $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; + + // Workaround for the bug, when PTS functionality is enabled. + // @see : https://bugs.php.net/69442 + $ptsWorkaround = fopen(__FILE__, 'r'); + } + if (\defined('HHVM_VERSION')) { + $envPairs = $env; + } else { + $envPairs = []; + foreach ($env as $k => $v) { + if (false !== $v) { + $envPairs[] = $k.'='.$v; + } + } + } + + if (!is_dir($this->cwd)) { + @trigger_error('The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED); + } + + $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + + if (!\is_resource($this->process)) { + throw new RuntimeException('Unable to launch a new process.'); + } + $this->status = self::STATUS_STARTED; + + if (isset($descriptors[3])) { + $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); + } + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * Restarts the process. + * + * Be warned that the process is cloned before being started. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return static + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * + * @see start() + * + * @final since version 3.3 + */ + public function restart(callable $callback = null/*, array $env = []*/) + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running.'); + } + $env = 1 < \func_num_args() ? func_get_arg(1) : null; + + $process = clone $this; + $process->start($callback, $env); + + return $process; + } + + /** + * Waits for the process to terminate. + * + * The callback receives the type of output (out or err) and some bytes + * from the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param callable|null $callback A valid PHP callback + * + * @return int The exitcode of the process + * + * @throws RuntimeException When process timed out + * @throws RuntimeException When process stopped after receiving signal + * @throws LogicException When process is not yet started + */ + public function wait(callable $callback = null) + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + + if (null !== $callback) { + if (!$this->processPipes->haveReadSupport()) { + $this->stop(0); + throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait.'); + } + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); + } while ($running); + + while ($this->isRunning()) { + $this->checkTimeout(); + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); + } + + return $this->exitcode; + } + + /** + * Returns the Pid (process identifier), if applicable. + * + * @return int|null The process id if running, null otherwise + */ + public function getPid() + { + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) + * + * @return $this + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed + * @throws RuntimeException In case of failure + */ + public function signal($signal) + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * Disables fetching output and error output from the underlying process. + * + * @return $this + * + * @throws RuntimeException In case the process is already running + * @throws LogicException if an idle timeout is set + */ + public function disableOutput() + { + if ($this->isRunning()) { + throw new RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new LogicException('Output can not be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * Enables fetching output and error output from the underlying process. + * + * @return $this + * + * @throws RuntimeException In case the process is already running + */ + public function enableOutput() + { + if ($this->isRunning()) { + throw new RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * Returns true in case the output is disabled, false otherwise. + * + * @return bool + */ + public function isOutputDisabled() + { + return $this->outputDisabled; + } + + /** + * Returns the current output of the process (STDOUT). + * + * @return string The process output + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getOutput() + { + $this->readPipesForOutput(__FUNCTION__); + + if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { + return ''; + } + + return $ret; + } + + /** + * Returns the output incrementally. + * + * In comparison with the getOutput method which always return the whole + * output, this one returns the new output since the last call. + * + * @return string The process output since the last call + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIncrementalOutput() + { + $this->readPipesForOutput(__FUNCTION__); + + $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); + $this->incrementalOutputOffset = ftell($this->stdout); + + if (false === $latest) { + return ''; + } + + return $latest; + } + + /** + * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). + * + * @param int $flags A bit field of Process::ITER_* flags + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + * + * @return \Generator + */ + public function getIterator($flags = 0) + { + $this->readPipesForOutput(__FUNCTION__, false); + + $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); + $blocking = !(self::ITER_NON_BLOCKING & $flags); + $yieldOut = !(self::ITER_SKIP_OUT & $flags); + $yieldErr = !(self::ITER_SKIP_ERR & $flags); + + while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { + if ($yieldOut) { + $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); + + if (isset($out[0])) { + if ($clearOutput) { + $this->clearOutput(); + } else { + $this->incrementalOutputOffset = ftell($this->stdout); + } + + yield self::OUT => $out; + } + } + + if ($yieldErr) { + $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); + + if (isset($err[0])) { + if ($clearOutput) { + $this->clearErrorOutput(); + } else { + $this->incrementalErrorOutputOffset = ftell($this->stderr); + } + + yield self::ERR => $err; + } + } + + if (!$blocking && !isset($out[0]) && !isset($err[0])) { + yield self::OUT => ''; + } + + $this->checkTimeout(); + $this->readPipesForOutput(__FUNCTION__, $blocking); + } + } + + /** + * Clears the process output. + * + * @return $this + */ + public function clearOutput() + { + ftruncate($this->stdout, 0); + fseek($this->stdout, 0); + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * Returns the current error output of the process (STDERR). + * + * @return string The process error output + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getErrorOutput() + { + $this->readPipesForOutput(__FUNCTION__); + + if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { + return ''; + } + + return $ret; + } + + /** + * Returns the errorOutput incrementally. + * + * In comparison with the getErrorOutput method which always return the + * whole error output, this one returns the new error output since the last + * call. + * + * @return string The process error output since the last call + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIncrementalErrorOutput() + { + $this->readPipesForOutput(__FUNCTION__); + + $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); + $this->incrementalErrorOutputOffset = ftell($this->stderr); + + if (false === $latest) { + return ''; + } + + return $latest; + } + + /** + * Clears the process output. + * + * @return $this + */ + public function clearErrorOutput() + { + ftruncate($this->stderr, 0); + fseek($this->stderr, 0); + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * Returns the exit code returned by the process. + * + * @return int|null The exit status code, null if the Process is not terminated + * + * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled + */ + public function getExitCode() + { + if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * Returns a string representation for the exit code returned by the process. + * + * This method relies on the Unix exit code status standardization + * and might not be relevant for other operating systems. + * + * @return string|null A string representation for the exit status code, null if the Process is not terminated + * + * @see http://tldp.org/LDP/abs/html/exitcodes.html + * @see http://en.wikipedia.org/wiki/Unix_signal + */ + public function getExitCodeText() + { + if (null === $exitcode = $this->getExitCode()) { + return null; + } + + return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; + } + + /** + * Checks if the process ended successfully. + * + * @return bool true if the process ended successfully, false otherwise + */ + public function isSuccessful() + { + return 0 === $this->getExitCode(); + } + + /** + * Returns true if the child process has been terminated by an uncaught signal. + * + * It always returns false on Windows. + * + * @return bool + * + * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated + */ + public function hasBeenSignaled() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + return $this->processInformation['signaled']; + } + + /** + * Returns the number of the signal that caused the child process to terminate its execution. + * + * It is only meaningful if hasBeenSignaled() returns true. + * + * @return int + * + * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated + */ + public function getTermSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + return $this->processInformation['termsig']; + } + + /** + * Returns true if the child process has been stopped by a signal. + * + * It always returns false on Windows. + * + * @return bool + * + * @throws LogicException In case the process is not terminated + */ + public function hasBeenStopped() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + return $this->processInformation['stopped']; + } + + /** + * Returns the number of the signal that caused the child process to stop its execution. + * + * It is only meaningful if hasBeenStopped() returns true. + * + * @return int + * + * @throws LogicException In case the process is not terminated + */ + public function getStopSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + return $this->processInformation['stopsig']; + } + + /** + * Checks if the process is currently running. + * + * @return bool true if the process is currently running, false otherwise + */ + public function isRunning() + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * Checks if the process has been started with no regard to the current state. + * + * @return bool true if status is ready, false otherwise + */ + public function isStarted() + { + return self::STATUS_READY != $this->status; + } + + /** + * Checks if the process is terminated. + * + * @return bool true if process is terminated, false otherwise + */ + public function isTerminated() + { + $this->updateStatus(false); + + return self::STATUS_TERMINATED == $this->status; + } + + /** + * Gets the process status. + * + * The status is one of: ready, started, terminated. + * + * @return string The current process status + */ + public function getStatus() + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * Stops the process. + * + * @param int|float $timeout The timeout in seconds + * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) + * + * @return int|null The exit-code of the process or null if it's not running + */ + public function stop($timeout = 10, $signal = null) + { + $timeoutMicro = microtime(true) + $timeout; + if ($this->isRunning()) { + // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here + $this->doSignal(15, false); + do { + usleep(1000); + } while ($this->isRunning() && microtime(true) < $timeoutMicro); + + if ($this->isRunning()) { + // Avoid exception here: process is supposed to be running, but it might have stopped just + // after this line. In any case, let's silently discard the error, we cannot do anything. + $this->doSignal($signal ?: 9, false); + } + } + + if ($this->isRunning()) { + if (isset($this->fallbackStatus['pid'])) { + unset($this->fallbackStatus['pid']); + + return $this->stop(0, $signal); + } + $this->close(); + } + + return $this->exitcode; + } + + /** + * Adds a line to the STDOUT stream. + * + * @internal + * + * @param string $line The line to append + */ + public function addOutput($line) + { + $this->lastOutputTime = microtime(true); + + fseek($this->stdout, 0, SEEK_END); + fwrite($this->stdout, $line); + fseek($this->stdout, $this->incrementalOutputOffset); + } + + /** + * Adds a line to the STDERR stream. + * + * @internal + * + * @param string $line The line to append + */ + public function addErrorOutput($line) + { + $this->lastOutputTime = microtime(true); + + fseek($this->stderr, 0, SEEK_END); + fwrite($this->stderr, $line); + fseek($this->stderr, $this->incrementalErrorOutputOffset); + } + + /** + * Gets the command line to be executed. + * + * @return string The command to execute + */ + public function getCommandLine() + { + return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline; + } + + /** + * Sets the command line to be executed. + * + * @param string|array $commandline The command to execute + * + * @return $this + */ + public function setCommandLine($commandline) + { + $this->commandline = $commandline; + + return $this; + } + + /** + * Gets the process timeout (max. runtime). + * + * @return float|null The timeout in seconds or null if it's disabled + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Gets the process idle timeout (max. time since last output). + * + * @return float|null The timeout in seconds or null if it's disabled + */ + public function getIdleTimeout() + { + return $this->idleTimeout; + } + + /** + * Sets the process timeout (max. runtime) in seconds. + * + * To disable the timeout, set this value to null. + * + * @param int|float|null $timeout The timeout in seconds + * + * @return $this + * + * @throws InvalidArgumentException if the timeout is negative + */ + public function setTimeout($timeout) + { + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Sets the process idle timeout (max. time since last output). + * + * To disable the timeout, set this value to null. + * + * @param int|float|null $timeout The timeout in seconds + * + * @return $this + * + * @throws LogicException if the output is disabled + * @throws InvalidArgumentException if the timeout is negative + */ + public function setIdleTimeout($timeout) + { + if (null !== $timeout && $this->outputDisabled) { + throw new LogicException('Idle timeout can not be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Enables or disables the TTY mode. + * + * @param bool $tty True to enabled and false to disable + * + * @return $this + * + * @throws RuntimeException In case the TTY mode is not supported + */ + public function setTty($tty) + { + if ('\\' === \DIRECTORY_SEPARATOR && $tty) { + throw new RuntimeException('TTY mode is not supported on Windows platform.'); + } + if ($tty) { + static $isTtySupported; + + if (null === $isTtySupported) { + $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); + } + + if (!$isTtySupported) { + throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); + } + } + + $this->tty = (bool) $tty; + + return $this; + } + + /** + * Checks if the TTY mode is enabled. + * + * @return bool true if the TTY mode is enabled, false otherwise + */ + public function isTty() + { + return $this->tty; + } + + /** + * Sets PTY mode. + * + * @param bool $bool + * + * @return $this + */ + public function setPty($bool) + { + $this->pty = (bool) $bool; + + return $this; + } + + /** + * Returns PTY state. + * + * @return bool + */ + public function isPty() + { + return $this->pty; + } + + /** + * Gets the working directory. + * + * @return string|null The current working directory or null on failure + */ + public function getWorkingDirectory() + { + if (null === $this->cwd) { + // getcwd() will return false if any one of the parent directories does not have + // the readable or search mode set, even if the current directory does + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * Sets the current working directory. + * + * @param string $cwd The new working directory + * + * @return $this + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * Gets the environment variables. + * + * @return array The current environment variables + */ + public function getEnv() + { + return $this->env; + } + + /** + * Sets the environment variables. + * + * Each environment variable value should be a string. + * If it is an array, the variable is ignored. + * If it is false or null, it will be removed when + * env vars are otherwise inherited. + * + * That happens in PHP when 'argv' is registered into + * the $_ENV array for instance. + * + * @param array $env The new environment variables + * + * @return $this + */ + public function setEnv(array $env) + { + // Process can not handle env values that are arrays + $env = array_filter($env, function ($value) { + return !\is_array($value); + }); + + $this->env = $env; + + return $this; + } + + /** + * Gets the Process input. + * + * @return resource|string|\Iterator|null The Process input + */ + public function getInput() + { + return $this->input; + } + + /** + * Sets the input. + * + * This content will be passed to the underlying process standard input. + * + * @param string|int|float|bool|resource|\Traversable|null $input The content + * + * @return $this + * + * @throws LogicException In case the process is running + */ + public function setInput($input) + { + if ($this->isRunning()) { + throw new LogicException('Input can not be set while the process is running.'); + } + + $this->input = ProcessUtils::validateInput(__METHOD__, $input); + + return $this; + } + + /** + * Gets the options for proc_open. + * + * @return array The current options + * + * @deprecated since version 3.3, to be removed in 4.0. + */ + public function getOptions() + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); + + return $this->options; + } + + /** + * Sets the options for proc_open. + * + * @param array $options The new options + * + * @return $this + * + * @deprecated since version 3.3, to be removed in 4.0. + */ + public function setOptions(array $options) + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); + + $this->options = $options; + + return $this; + } + + /** + * Gets whether or not Windows compatibility is enabled. + * + * This is true by default. + * + * @return bool + * + * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. + */ + public function getEnhanceWindowsCompatibility() + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); + + return $this->enhanceWindowsCompatibility; + } + + /** + * Sets whether or not Windows compatibility is enabled. + * + * @param bool $enhance + * + * @return $this + * + * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. + */ + public function setEnhanceWindowsCompatibility($enhance) + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); + + $this->enhanceWindowsCompatibility = (bool) $enhance; + + return $this; + } + + /** + * Returns whether sigchild compatibility mode is activated or not. + * + * @return bool + * + * @deprecated since version 3.3, to be removed in 4.0. Sigchild compatibility will always be enabled. + */ + public function getEnhanceSigchildCompatibility() + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); + + return $this->enhanceSigchildCompatibility; + } + + /** + * Activates sigchild compatibility mode. + * + * Sigchild compatibility mode is required to get the exit code and + * determine the success of a process when PHP has been compiled with + * the --enable-sigchild option + * + * @param bool $enhance + * + * @return $this + * + * @deprecated since version 3.3, to be removed in 4.0. + */ + public function setEnhanceSigchildCompatibility($enhance) + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); + + $this->enhanceSigchildCompatibility = (bool) $enhance; + + return $this; + } + + /** + * Sets whether environment variables will be inherited or not. + * + * @param bool $inheritEnv + * + * @return $this + */ + public function inheritEnvironmentVariables($inheritEnv = true) + { + if (!$inheritEnv) { + @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', E_USER_DEPRECATED); + } + + $this->inheritEnv = (bool) $inheritEnv; + + return $this; + } + + /** + * Returns whether environment variables will be inherited or not. + * + * @return bool + * + * @deprecated since version 3.3, to be removed in 4.0. Environment variables will always be inherited. + */ + public function areEnvironmentVariablesInherited() + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Environment variables will always be inherited.', __METHOD__), E_USER_DEPRECATED); + + return $this->inheritEnv; + } + + /** + * Performs a check between the timeout definition and the time the process started. + * + * In case you run a background process (with the start method), you should + * trigger this method regularly to ensure the process timeout + * + * @throws ProcessTimedOutException In case the timeout was reached + */ + public function checkTimeout() + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); + } + } + + /** + * Returns whether PTY is supported on the current operating system. + * + * @return bool + */ + public static function isPtySupported() + { + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + return $result = false; + } + + return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes); + } + + /** + * Creates the descriptors needed by the proc_open. + * + * @return array + */ + private function getDescriptors() + { + if ($this->input instanceof \Iterator) { + $this->input->rewind(); + } + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback); + } else { + $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback); + } + + return $this->processPipes->getDescriptors(); + } + + /** + * Builds up the callback used by wait(). + * + * The callbacks adds all occurred output to the specific buffer and calls + * the user callback (if present) with the received output. + * + * @param callable|null $callback The user defined PHP callback + * + * @return \Closure A PHP closure + */ + protected function buildCallback(callable $callback = null) + { + if ($this->outputDisabled) { + return function ($type, $data) use ($callback) { + if (null !== $callback) { + \call_user_func($callback, $type, $data); + } + }; + } + + $out = self::OUT; + + return function ($type, $data) use ($callback, $out) { + if ($out == $type) { + $this->addOutput($data); + } else { + $this->addErrorOutput($data); + } + + if (null !== $callback) { + \call_user_func($callback, $type, $data); + } + }; + } + + /** + * Updates the status of the process, reads pipes. + * + * @param bool $blocking Whether to use a blocking read call + */ + protected function updateStatus($blocking) + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + $this->processInformation = proc_get_status($this->process); + $running = $this->processInformation['running']; + + $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); + + if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + $this->processInformation = $this->fallbackStatus + $this->processInformation; + } + + if (!$running) { + $this->close(); + } + } + + /** + * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. + * + * @return bool + */ + protected function isSigchildEnabled() + { + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!\function_exists('phpinfo') || \defined('HHVM_VERSION')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(INFO_GENERAL); + + return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + /** + * Reads pipes for the freshest output. + * + * @param string $caller The name of the method that needs fresh outputs + * @param bool $blocking Whether to use blocking calls or not + * + * @throws LogicException in case output has been disabled or process is not started + */ + private function readPipesForOutput($caller, $blocking = false) + { + if ($this->outputDisabled) { + throw new LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted($caller); + + $this->updateStatus($blocking); + } + + /** + * Validates and returns the filtered timeout. + * + * @param int|float|null $timeout + * + * @return float|null + * + * @throws InvalidArgumentException if the given timeout is a negative number + */ + private function validateTimeout($timeout) + { + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * Reads pipes, executes callback. + * + * @param bool $blocking Whether to use blocking calls or not + * @param bool $close Whether to close file handles or not + */ + private function readPipes($blocking, $close) + { + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 !== $type) { + $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); + } elseif (!isset($this->fallbackStatus['signaled'])) { + $this->fallbackStatus['exitcode'] = (int) $data; + } + } + } + + /** + * Closes process resource, closes file handles, sets the exitcode. + * + * @return int The exitcode + */ + private function close() + { + $this->processPipes->close(); + if (\is_resource($this->process)) { + proc_close($this->process); + } + $this->exitcode = $this->processInformation['exitcode']; + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode) { + if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { + // if process has been signaled, no exitcode but a valid termsig, apply Unix convention + $this->exitcode = 128 + $this->processInformation['termsig']; + } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + $this->processInformation['signaled'] = true; + $this->processInformation['termsig'] = -1; + } + } + + // Free memory from self-reference callback created by buildCallback + // Doing so in other contexts like __destruct or by garbage collector is ineffective + // Now pipes are closed, so the callback is no longer necessary + $this->callback = null; + + return $this->exitcode; + } + + /** + * Resets data related to the latest run of the process. + */ + private function resetProcessData() + { + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackStatus = []; + $this->processInformation = null; + $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b'); + $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b'); + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) + * @param bool $throwException Whether to throw exception in case signal failed + * + * @return bool True if the signal was sent successfully, false otherwise + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed + * @throws RuntimeException In case of failure + */ + private function doSignal($signal, $throwException) + { + if (null === $pid = $this->getPid()) { + if ($throwException) { + throw new LogicException('Can not send signal on a non running process.'); + } + + return false; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); + if ($exitCode && $this->isRunning()) { + if ($throwException) { + throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); + } + + return false; + } + } else { + if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) { + $ok = @proc_terminate($this->process, $signal); + } elseif (\function_exists('posix_kill')) { + $ok = @posix_kill($pid, $signal); + } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { + $ok = false === fgets($pipes[2]); + } + if (!$ok) { + if ($throwException) { + throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); + } + + return false; + } + } + + $this->latestSignal = (int) $signal; + $this->fallbackStatus['signaled'] = true; + $this->fallbackStatus['exitcode'] = -1; + $this->fallbackStatus['termsig'] = $this->latestSignal; + + return true; + } + + private function prepareWindowsCommandLine($cmd, array &$env) + { + $uid = uniqid('', true); + $varCount = 0; + $varCache = []; + $cmd = preg_replace_callback( + '/"(?:( + [^"%!^]*+ + (?: + (?: !LF! | "(?:\^[%!^])?+" ) + [^"%!^]*+ + )++ + ) | [^"]*+ )"/x', + function ($m) use (&$env, &$varCache, &$varCount, $uid) { + if (!isset($m[1])) { + return $m[0]; + } + if (isset($varCache[$m[0]])) { + return $varCache[$m[0]]; + } + if (false !== strpos($value = $m[1], "\0")) { + $value = str_replace("\0", '?', $value); + } + if (false === strpbrk($value, "\"%!\n")) { + return '"'.$value.'"'; + } + + $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value); + $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; + $var = $uid.++$varCount; + + $env[$var] = $value; + + return $varCache[$m[0]] = '!'.$var.'!'; + }, + $cmd + ); + + $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $cmd .= ' '.$offset.'>"'.$filename.'"'; + } + + return $cmd; + } + + /** + * Ensures the process is running or terminated, throws a LogicException if the process has a not started. + * + * @param string $functionName The function name that was called + * + * @throws LogicException if the process has not run + */ + private function requireProcessIsStarted($functionName) + { + if (!$this->isStarted()) { + throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); + } + } + + /** + * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`. + * + * @param string $functionName The function name that was called + * + * @throws LogicException if the process is not yet terminated + */ + private function requireProcessIsTerminated($functionName) + { + if (!$this->isTerminated()) { + throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); + } + } + + /** + * Escapes a string to be used as a shell argument. + * + * @param string $argument The argument that will be escaped + * + * @return string The escaped argument + */ + private function escapeArgument($argument) + { + if ('\\' !== \DIRECTORY_SEPARATOR) { + return "'".str_replace("'", "'\\''", $argument)."'"; + } + if ('' === $argument = (string) $argument) { + return '""'; + } + if (false !== strpos($argument, "\0")) { + $argument = str_replace("\0", '?', $argument); + } + if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { + return $argument; + } + $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); + + return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; + } + + private function getDefaultEnv() + { + $env = []; + + foreach ($_SERVER as $k => $v) { + if (\is_string($v) && false !== $v = getenv($k)) { + $env[$k] = $v; + } + } + + foreach ($_ENV as $k => $v) { + if (\is_string($v)) { + $env[$k] = $v; + } + } + + return $env; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/ProcessBuilder.php b/plugins/panakour/backup/vendor/symfony/process/ProcessBuilder.php new file mode 100644 index 0000000..69d13c3 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/ProcessBuilder.php @@ -0,0 +1,280 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +@trigger_error(sprintf('The %s class is deprecated since Symfony 3.4 and will be removed in 4.0. Use the Process class instead.', ProcessBuilder::class), E_USER_DEPRECATED); + +use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; + +/** + * @author Kris Wallsmith + * + * @deprecated since version 3.4, to be removed in 4.0. Use the Process class instead. + */ +class ProcessBuilder +{ + private $arguments; + private $cwd; + private $env = []; + private $input; + private $timeout = 60; + private $options; + private $inheritEnv = true; + private $prefix = []; + private $outputDisabled = false; + + /** + * @param string[] $arguments An array of arguments + */ + public function __construct(array $arguments = []) + { + $this->arguments = $arguments; + } + + /** + * Creates a process builder instance. + * + * @param string[] $arguments An array of arguments + * + * @return static + */ + public static function create(array $arguments = []) + { + return new static($arguments); + } + + /** + * Adds an unescaped argument to the command string. + * + * @param string $argument A command argument + * + * @return $this + */ + public function add($argument) + { + $this->arguments[] = $argument; + + return $this; + } + + /** + * Adds a prefix to the command string. + * + * The prefix is preserved when resetting arguments. + * + * @param string|array $prefix A command prefix or an array of command prefixes + * + * @return $this + */ + public function setPrefix($prefix) + { + $this->prefix = \is_array($prefix) ? $prefix : [$prefix]; + + return $this; + } + + /** + * Sets the arguments of the process. + * + * Arguments must not be escaped. + * Previous arguments are removed. + * + * @param string[] $arguments + * + * @return $this + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + + return $this; + } + + /** + * Sets the working directory. + * + * @param string|null $cwd The working directory + * + * @return $this + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * Sets whether environment variables will be inherited or not. + * + * @param bool $inheritEnv + * + * @return $this + */ + public function inheritEnvironmentVariables($inheritEnv = true) + { + $this->inheritEnv = $inheritEnv; + + return $this; + } + + /** + * Sets an environment variable. + * + * Setting a variable overrides its previous value. Use `null` to unset a + * defined environment variable. + * + * @param string $name The variable name + * @param string|null $value The variable value + * + * @return $this + */ + public function setEnv($name, $value) + { + $this->env[$name] = $value; + + return $this; + } + + /** + * Adds a set of environment variables. + * + * Already existing environment variables with the same name will be + * overridden by the new values passed to this method. Pass `null` to unset + * a variable. + * + * @param array $variables The variables + * + * @return $this + */ + public function addEnvironmentVariables(array $variables) + { + $this->env = array_replace($this->env, $variables); + + return $this; + } + + /** + * Sets the input of the process. + * + * @param resource|string|int|float|bool|\Traversable|null $input The input content + * + * @return $this + * + * @throws InvalidArgumentException In case the argument is invalid + */ + public function setInput($input) + { + $this->input = ProcessUtils::validateInput(__METHOD__, $input); + + return $this; + } + + /** + * Sets the process timeout. + * + * To disable the timeout, set this value to null. + * + * @param float|null $timeout + * + * @return $this + * + * @throws InvalidArgumentException + */ + public function setTimeout($timeout) + { + if (null === $timeout) { + $this->timeout = null; + + return $this; + } + + $timeout = (float) $timeout; + + if ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + $this->timeout = $timeout; + + return $this; + } + + /** + * Adds a proc_open option. + * + * @param string $name The option name + * @param string $value The option value + * + * @return $this + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * Disables fetching output and error output from the underlying process. + * + * @return $this + */ + public function disableOutput() + { + $this->outputDisabled = true; + + return $this; + } + + /** + * Enables fetching output and error output from the underlying process. + * + * @return $this + */ + public function enableOutput() + { + $this->outputDisabled = false; + + return $this; + } + + /** + * Creates a Process instance and returns it. + * + * @return Process + * + * @throws LogicException In case no arguments have been provided + */ + public function getProcess() + { + if (0 === \count($this->prefix) && 0 === \count($this->arguments)) { + throw new LogicException('You must add() command arguments before calling getProcess().'); + } + + $arguments = array_merge($this->prefix, $this->arguments); + $process = new Process($arguments, $this->cwd, $this->env, $this->input, $this->timeout, $this->options); + // to preserve the BC with symfony <3.3, we convert the array structure + // to a string structure to avoid the prefixing with the exec command + $process->setCommandLine($process->getCommandLine()); + + if ($this->inheritEnv) { + $process->inheritEnvironmentVariables(); + } + if ($this->outputDisabled) { + $process->disableOutput(); + } + + return $process; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/ProcessUtils.php b/plugins/panakour/backup/vendor/symfony/process/ProcessUtils.php new file mode 100644 index 0000000..74f2d86 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/ProcessUtils.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * ProcessUtils is a bunch of utility methods. + * + * This class contains static methods only and is not meant to be instantiated. + * + * @author Martin Hasoň + */ +class ProcessUtils +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Escapes a string to be used as a shell argument. + * + * @param string $argument The argument that will be escaped + * + * @return string The escaped argument + * + * @deprecated since version 3.3, to be removed in 4.0. Use a command line array or give env vars to the `Process::start/run()` method instead. + */ + public static function escapeArgument($argument) + { + @trigger_error('The '.__METHOD__.'() method is deprecated since Symfony 3.3 and will be removed in 4.0. Use a command line array or give env vars to the Process::start/run() method instead.', E_USER_DEPRECATED); + + //Fix for PHP bug #43784 escapeshellarg removes % from given string + //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows + //@see https://bugs.php.net/43784 + //@see https://bugs.php.net/49446 + if ('\\' === \DIRECTORY_SEPARATOR) { + if ('' === $argument) { + return escapeshellarg($argument); + } + + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"'.$escapedArgument.'"'; + } + + return $escapedArgument; + } + + return "'".str_replace("'", "'\\''", $argument)."'"; + } + + /** + * Validates and normalizes a Process input. + * + * @param string $caller The name of method call that validates the input + * @param mixed $input The input to validate + * + * @return mixed The validated input + * + * @throws InvalidArgumentException In case the input is not valid + */ + public static function validateInput($caller, $input) + { + if (null !== $input) { + if (\is_resource($input)) { + return $input; + } + if (\is_string($input)) { + return $input; + } + if (is_scalar($input)) { + return (string) $input; + } + if ($input instanceof Process) { + return $input->getIterator($input::ITER_SKIP_ERR); + } + if ($input instanceof \Iterator) { + return $input; + } + if ($input instanceof \Traversable) { + return new \IteratorIterator($input); + } + + throw new InvalidArgumentException(sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); + } + + return $input; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < \strlen($arg) && $char === $arg[0] && $char === $arg[\strlen($arg) - 1]; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/README.md b/plugins/panakour/backup/vendor/symfony/process/README.md new file mode 100644 index 0000000..b7ca5b4 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/README.md @@ -0,0 +1,13 @@ +Process Component +================= + +The Process component executes commands in sub-processes. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/process.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/ErrorProcessInitiator.php b/plugins/panakour/backup/vendor/symfony/process/Tests/ErrorProcessInitiator.php new file mode 100644 index 0000000..c37aeb5 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/ErrorProcessInitiator.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Process; + +require \dirname(__DIR__).'/vendor/autoload.php'; + +list('e' => $php) = getopt('e:') + ['e' => 'php']; + +try { + $process = new Process("exec $php -r \"echo 'ready'; trigger_error('error', E_USER_ERROR);\""); + $process->start(); + $process->setTimeout(0.5); + while (false === strpos($process->getOutput(), 'ready')) { + usleep(1000); + } + $process->signal(SIGSTOP); + $process->wait(); + + return $process->getExitCode(); +} catch (ProcessTimedOutException $t) { + echo $t->getMessage().PHP_EOL; + + return 1; +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/ExecutableFinderTest.php b/plugins/panakour/backup/vendor/symfony/process/Tests/ExecutableFinderTest.php new file mode 100644 index 0000000..2942695 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/ExecutableFinderTest.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\ExecutableFinder; + +/** + * @author Chris Smith + */ +class ExecutableFinderTest extends TestCase +{ + private $path; + + protected function tearDown() + { + if ($this->path) { + // Restore path if it was changed. + putenv('PATH='.$this->path); + } + } + + private function setPath($path) + { + $this->path = getenv('PATH'); + putenv('PATH='.$path); + } + + public function testFind() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->setPath(\dirname(PHP_BINARY)); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); + + $this->assertSamePath(PHP_BINARY, $result); + } + + public function testFindWithDefault() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $expected = 'defaultValue'; + + $this->setPath(''); + + $finder = new ExecutableFinder(); + $result = $finder->find('foo', $expected); + + $this->assertEquals($expected, $result); + } + + public function testFindWithNullAsDefault() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->setPath(''); + + $finder = new ExecutableFinder(); + + $result = $finder->find('foo'); + + $this->assertNull($result); + } + + public function testFindWithExtraDirs() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->setPath(''); + + $extraDirs = [\dirname(PHP_BINARY)]; + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), null, $extraDirs); + + $this->assertSamePath(PHP_BINARY, $result); + } + + public function testFindWithOpenBaseDir() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Cannot run test on windows'); + } + + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->iniSet('open_basedir', \dirname(PHP_BINARY).(!\defined('HHVM_VERSION') || HHVM_VERSION_ID >= 30800 ? PATH_SEPARATOR.'/' : '')); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); + + $this->assertSamePath(PHP_BINARY, $result); + } + + public function testFindProcessInOpenBasedir() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Cannot run test on windows'); + } + + $this->setPath(''); + $this->iniSet('open_basedir', PHP_BINARY.(!\defined('HHVM_VERSION') || HHVM_VERSION_ID >= 30800 ? PATH_SEPARATOR.'/' : '')); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), false); + + $this->assertSamePath(PHP_BINARY, $result); + } + + public function testFindBatchExecutableOnWindows() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Can be only tested on windows'); + } + + $target = tempnam(sys_get_temp_dir(), 'example-windows-executable'); + + touch($target); + touch($target.'.BAT'); + + $this->assertFalse(is_executable($target)); + + $this->setPath(sys_get_temp_dir()); + + $finder = new ExecutableFinder(); + $result = $finder->find(basename($target), false); + + unlink($target); + unlink($target.'.BAT'); + + $this->assertSamePath($target.'.BAT', $result); + } + + private function assertSamePath($expected, $tested) + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertEquals(strtolower($expected), strtolower($tested)); + } else { + $this->assertEquals($expected, $tested); + } + } + + private function getPhpBinaryName() + { + return basename(PHP_BINARY, '\\' === \DIRECTORY_SEPARATOR ? '.exe' : ''); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/NonStopableProcess.php b/plugins/panakour/backup/vendor/symfony/process/Tests/NonStopableProcess.php new file mode 100644 index 0000000..5643259 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/NonStopableProcess.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Runs a PHP script that can be stopped only with a SIGKILL (9) signal for 3 seconds. + * + * @args duration Run this script with a custom duration + * + * @example `php NonStopableProcess.php 42` will run the script for 42 seconds + */ +function handleSignal($signal) +{ + switch ($signal) { + case SIGTERM: + $name = 'SIGTERM'; + break; + case SIGINT: + $name = 'SIGINT'; + break; + default: + $name = $signal.' (unknown)'; + break; + } + + echo "signal $name\n"; +} + +pcntl_signal(SIGTERM, 'handleSignal'); +pcntl_signal(SIGINT, 'handleSignal'); + +echo 'received '; + +$duration = isset($argv[1]) ? (int) $argv[1] : 3; +$start = microtime(true); + +while ($duration > (microtime(true) - $start)) { + usleep(10000); + pcntl_signal_dispatch(); +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/PhpExecutableFinderTest.php b/plugins/panakour/backup/vendor/symfony/process/Tests/PhpExecutableFinderTest.php new file mode 100644 index 0000000..795be8f --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/PhpExecutableFinderTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\PhpExecutableFinder; + +/** + * @author Robert Schönthal + */ +class PhpExecutableFinderTest extends TestCase +{ + /** + * tests find() with the constant PHP_BINARY. + */ + public function testFind() + { + if (\defined('HHVM_VERSION')) { + $this->markTestSkipped('Should not be executed in HHVM context.'); + } + + $f = new PhpExecutableFinder(); + + $current = PHP_BINARY; + $args = 'phpdbg' === \PHP_SAPI ? ' -qrr' : ''; + + $this->assertEquals($current.$args, $f->find(), '::find() returns the executable PHP'); + $this->assertEquals($current, $f->find(false), '::find() returns the executable PHP'); + } + + /** + * tests find() with the env var / constant PHP_BINARY with HHVM. + */ + public function testFindWithHHVM() + { + if (!\defined('HHVM_VERSION')) { + $this->markTestSkipped('Should be executed in HHVM context.'); + } + + $f = new PhpExecutableFinder(); + + $current = getenv('PHP_BINARY') ?: PHP_BINARY; + + $this->assertEquals($current.' --php', $f->find(), '::find() returns the executable PHP'); + $this->assertEquals($current, $f->find(false), '::find() returns the executable PHP'); + } + + /** + * tests find() with the env var PHP_PATH. + */ + public function testFindArguments() + { + $f = new PhpExecutableFinder(); + + if (\defined('HHVM_VERSION')) { + $this->assertEquals($f->findArguments(), ['--php'], '::findArguments() returns HHVM arguments'); + } elseif ('phpdbg' === \PHP_SAPI) { + $this->assertEquals($f->findArguments(), ['-qrr'], '::findArguments() returns phpdbg arguments'); + } else { + $this->assertEquals($f->findArguments(), [], '::findArguments() returns no arguments'); + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/PhpProcessTest.php b/plugins/panakour/backup/vendor/symfony/process/Tests/PhpProcessTest.php new file mode 100644 index 0000000..d34e842 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/PhpProcessTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\PhpProcess; + +class PhpProcessTest extends TestCase +{ + public function testNonBlockingWorks() + { + $expected = 'hello world!'; + $process = new PhpProcess(<<start(); + $process->wait(); + $this->assertEquals($expected, $process->getOutput()); + } + + public function testCommandLine() + { + $process = new PhpProcess(<<<'PHP' +getCommandLine(); + + $process->start(); + $this->assertStringContainsString($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after start'); + + $process->wait(); + $this->assertStringContainsString($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after wait'); + + $this->assertSame(PHP_VERSION.\PHP_SAPI, $process->getOutput()); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php b/plugins/panakour/backup/vendor/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php new file mode 100644 index 0000000..2e7716d --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +define('ERR_SELECT_FAILED', 1); +define('ERR_TIMEOUT', 2); +define('ERR_READ_FAILED', 3); +define('ERR_WRITE_FAILED', 4); + +$read = [STDIN]; +$write = [STDOUT, STDERR]; + +stream_set_blocking(STDIN, 0); +stream_set_blocking(STDOUT, 0); +stream_set_blocking(STDERR, 0); + +$out = $err = ''; +while ($read || $write) { + $r = $read; + $w = $write; + $e = null; + $n = stream_select($r, $w, $e, 5); + + if (false === $n) { + exit(ERR_SELECT_FAILED); + } elseif ($n < 1) { + exit(ERR_TIMEOUT); + } + + if (in_array(STDOUT, $w) && strlen($out) > 0) { + $written = fwrite(STDOUT, (string) $out, 32768); + if (false === $written) { + exit(ERR_WRITE_FAILED); + } + $out = (string) substr($out, $written); + } + if (null === $read && '' === $out) { + $write = array_diff($write, [STDOUT]); + } + + if (in_array(STDERR, $w) && strlen($err) > 0) { + $written = fwrite(STDERR, (string) $err, 32768); + if (false === $written) { + exit(ERR_WRITE_FAILED); + } + $err = (string) substr($err, $written); + } + if (null === $read && '' === $err) { + $write = array_diff($write, [STDERR]); + } + + if ($r) { + $str = fread(STDIN, 32768); + if (false !== $str) { + $out .= $str; + $err .= $str; + } + if (false === $str || feof(STDIN)) { + $read = null; + if (!feof(STDIN)) { + exit(ERR_READ_FAILED); + } + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessBuilderTest.php b/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessBuilderTest.php new file mode 100644 index 0000000..c285135 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessBuilderTest.php @@ -0,0 +1,220 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\ProcessBuilder; + +/** + * @group legacy + */ +class ProcessBuilderTest extends TestCase +{ + public function testInheritEnvironmentVars() + { + $proc = ProcessBuilder::create() + ->add('foo') + ->getProcess(); + + $this->assertTrue($proc->areEnvironmentVariablesInherited()); + + $proc = ProcessBuilder::create() + ->add('foo') + ->inheritEnvironmentVariables(false) + ->getProcess(); + + $this->assertFalse($proc->areEnvironmentVariablesInherited()); + } + + public function testAddEnvironmentVariables() + { + $pb = new ProcessBuilder(); + $env = [ + 'foo' => 'bar', + 'foo2' => 'bar2', + ]; + $proc = $pb + ->add('command') + ->setEnv('foo', 'bar2') + ->addEnvironmentVariables($env) + ->getProcess() + ; + + $this->assertSame($env, $proc->getEnv()); + } + + public function testNegativeTimeoutFromSetter() + { + $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException'); + $pb = new ProcessBuilder(); + $pb->setTimeout(-1); + } + + public function testNullTimeout() + { + $pb = new ProcessBuilder(); + $pb->setTimeout(10); + $pb->setTimeout(null); + + $r = new \ReflectionObject($pb); + $p = $r->getProperty('timeout'); + $p->setAccessible(true); + + $this->assertNull($p->getValue($pb)); + } + + public function testShouldSetArguments() + { + $pb = new ProcessBuilder(['initial']); + $pb->setArguments(['second']); + + $proc = $pb->getProcess(); + + $this->assertStringContainsString('second', $proc->getCommandLine()); + } + + public function testPrefixIsPrependedToAllGeneratedProcess() + { + $pb = new ProcessBuilder(); + $pb->setPrefix('/usr/bin/php'); + + $proc = $pb->setArguments(['-v'])->getProcess(); + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" -v', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' '-v'", $proc->getCommandLine()); + } + + $proc = $pb->setArguments(['-i'])->getProcess(); + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" -i', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' '-i'", $proc->getCommandLine()); + } + } + + public function testArrayPrefixesArePrependedToAllGeneratedProcess() + { + $pb = new ProcessBuilder(); + $pb->setPrefix(['/usr/bin/php', 'composer.phar']); + + $proc = $pb->setArguments(['-v'])->getProcess(); + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" composer.phar -v', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' 'composer.phar' '-v'", $proc->getCommandLine()); + } + + $proc = $pb->setArguments(['-i'])->getProcess(); + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" composer.phar -i', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' 'composer.phar' '-i'", $proc->getCommandLine()); + } + } + + public function testShouldEscapeArguments() + { + $pb = new ProcessBuilder(['%path%', 'foo " bar', '%baz%baz']); + $proc = $pb->getProcess(); + + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertSame('""^%"path"^%"" "foo "" bar" ""^%"baz"^%"baz"', $proc->getCommandLine()); + } else { + $this->assertSame("'%path%' 'foo \" bar' '%baz%baz'", $proc->getCommandLine()); + } + } + + public function testShouldEscapeArgumentsAndPrefix() + { + $pb = new ProcessBuilder(['arg']); + $pb->setPrefix('%prefix%'); + $proc = $pb->getProcess(); + + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertSame('""^%"prefix"^%"" arg', $proc->getCommandLine()); + } else { + $this->assertSame("'%prefix%' 'arg'", $proc->getCommandLine()); + } + } + + public function testShouldThrowALogicExceptionIfNoPrefixAndNoArgument() + { + $this->expectException('Symfony\Component\Process\Exception\LogicException'); + ProcessBuilder::create()->getProcess(); + } + + public function testShouldNotThrowALogicExceptionIfNoArgument() + { + $process = ProcessBuilder::create() + ->setPrefix('/usr/bin/php') + ->getProcess(); + + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); + } + } + + public function testShouldNotThrowALogicExceptionIfNoPrefix() + { + $process = ProcessBuilder::create(['/usr/bin/php']) + ->getProcess(); + + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); + } + } + + public function testShouldReturnProcessWithDisabledOutput() + { + $process = ProcessBuilder::create(['/usr/bin/php']) + ->disableOutput() + ->getProcess(); + + $this->assertTrue($process->isOutputDisabled()); + } + + public function testShouldReturnProcessWithEnabledOutput() + { + $process = ProcessBuilder::create(['/usr/bin/php']) + ->disableOutput() + ->enableOutput() + ->getProcess(); + + $this->assertFalse($process->isOutputDisabled()); + } + + public function testInvalidInput() + { + $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException'); + $this->expectExceptionMessage('"Symfony\Component\Process\ProcessBuilder::setInput" only accepts strings, Traversable objects or stream resources.'); + $builder = ProcessBuilder::create(); + $builder->setInput([]); + } + + public function testDoesNotPrefixExec() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test cannot run on Windows.'); + } + + $builder = ProcessBuilder::create(['command', '-v', 'ls']); + $process = $builder->getProcess(); + $process->run(); + + $this->assertTrue($process->isSuccessful()); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessFailedExceptionTest.php b/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessFailedExceptionTest.php new file mode 100644 index 0000000..18e3551 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessFailedExceptionTest.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Exception\ProcessFailedException; + +/** + * @author Sebastian Marek + */ +class ProcessFailedExceptionTest extends TestCase +{ + /** + * tests ProcessFailedException throws exception if the process was successful. + */ + public function testProcessFailedExceptionThrowsException() + { + $process = $this->getMockBuilder('Symfony\Component\Process\Process')->setMethods(['isSuccessful'])->setConstructorArgs(['php'])->getMock(); + $process->expects($this->once()) + ->method('isSuccessful') + ->willReturn(true); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected a failed process, but the given process was successful.'); + + new ProcessFailedException($process); + } + + /** + * tests ProcessFailedException uses information from process output + * to generate exception message. + */ + public function testProcessFailedExceptionPopulatesInformationFromProcessOutput() + { + $cmd = 'php'; + $exitCode = 1; + $exitText = 'General error'; + $output = 'Command output'; + $errorOutput = 'FATAL: Unexpected error'; + $workingDirectory = getcwd(); + + $process = $this->getMockBuilder('Symfony\Component\Process\Process')->setMethods(['isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'])->setConstructorArgs([$cmd])->getMock(); + $process->expects($this->once()) + ->method('isSuccessful') + ->willReturn(false); + + $process->expects($this->once()) + ->method('getOutput') + ->willReturn($output); + + $process->expects($this->once()) + ->method('getErrorOutput') + ->willReturn($errorOutput); + + $process->expects($this->once()) + ->method('getExitCode') + ->willReturn($exitCode); + + $process->expects($this->once()) + ->method('getExitCodeText') + ->willReturn($exitText); + + $process->expects($this->once()) + ->method('isOutputDisabled') + ->willReturn(false); + + $process->expects($this->once()) + ->method('getWorkingDirectory') + ->willReturn($workingDirectory); + + $exception = new ProcessFailedException($process); + + $this->assertEquals( + "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}", + $exception->getMessage() + ); + } + + /** + * Tests that ProcessFailedException does not extract information from + * process output if it was previously disabled. + */ + public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput() + { + $cmd = 'php'; + $exitCode = 1; + $exitText = 'General error'; + $workingDirectory = getcwd(); + + $process = $this->getMockBuilder('Symfony\Component\Process\Process')->setMethods(['isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'])->setConstructorArgs([$cmd])->getMock(); + $process->expects($this->once()) + ->method('isSuccessful') + ->willReturn(false); + + $process->expects($this->never()) + ->method('getOutput'); + + $process->expects($this->never()) + ->method('getErrorOutput'); + + $process->expects($this->once()) + ->method('getExitCode') + ->willReturn($exitCode); + + $process->expects($this->once()) + ->method('getExitCodeText') + ->willReturn($exitText); + + $process->expects($this->once()) + ->method('isOutputDisabled') + ->willReturn(true); + + $process->expects($this->once()) + ->method('getWorkingDirectory') + ->willReturn($workingDirectory); + + $exception = new ProcessFailedException($process); + + $this->assertEquals( + "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}", + $exception->getMessage() + ); + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessTest.php b/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessTest.php new file mode 100644 index 0000000..e533528 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessTest.php @@ -0,0 +1,1589 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\InputStream; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Pipes\PipesInterface; +use Symfony\Component\Process\Process; + +/** + * @author Robert Schönthal + */ +class ProcessTest extends TestCase +{ + private static $phpBin; + private static $process; + private static $sigchild; + private static $notEnhancedSigchild = false; + + public static function setUpBeforeClass() + { + $phpBin = new PhpExecutableFinder(); + self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === \PHP_SAPI ? 'php' : $phpBin->find()); + + ob_start(); + phpinfo(INFO_GENERAL); + self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + protected function tearDown() + { + if (self::$process) { + self::$process->stop(0); + self::$process = null; + } + } + + /** + * @group legacy + * @expectedDeprecation The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0. + */ + public function testInvalidCwd() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('False-positive on Windows/appveyor.'); + } + + // Check that it works fine if the CWD exists + $cmd = new Process('echo test', __DIR__); + $cmd->run(); + + $cmd = new Process('echo test', __DIR__.'/notfound/'); + $cmd->run(); + } + + public function testThatProcessDoesNotThrowWarningDuringRun() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test is transient on Windows'); + } + @trigger_error('Test Error', E_USER_NOTICE); + $process = $this->getProcessForCode('sleep(3)'); + $process->run(); + $actualError = error_get_last(); + $this->assertEquals('Test Error', $actualError['message']); + $this->assertEquals(E_USER_NOTICE, $actualError['type']); + } + + public function testNegativeTimeoutFromConstructor() + { + $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException'); + $this->getProcess('', null, null, null, -1); + } + + public function testNegativeTimeoutFromSetter() + { + $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException'); + $p = $this->getProcess(''); + $p->setTimeout(-1); + } + + public function testFloatAndNullTimeout() + { + $p = $this->getProcess(''); + + $p->setTimeout(10); + $this->assertSame(10.0, $p->getTimeout()); + + $p->setTimeout(null); + $this->assertNull($p->getTimeout()); + + $p->setTimeout(0.0); + $this->assertNull($p->getTimeout()); + } + + /** + * @requires extension pcntl + */ + public function testStopWithTimeoutIsActuallyWorking() + { + $p = $this->getProcess([self::$phpBin, __DIR__.'/NonStopableProcess.php', 30]); + $p->start(); + + while (false === strpos($p->getOutput(), 'received')) { + usleep(1000); + } + $start = microtime(true); + $p->stop(0.1); + + $p->wait(); + + $this->assertLessThan(15, microtime(true) - $start); + } + + public function testAllOutputIsActuallyReadOnTermination() + { + // this code will result in a maximum of 2 reads of 8192 bytes by calling + // start() and isRunning(). by the time getOutput() is called the process + // has terminated so the internal pipes array is already empty. normally + // the call to start() will not read any data as the process will not have + // generated output, but this is non-deterministic so we must count it as + // a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus + // another byte which will never be read. + $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2; + + $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); + $p = $this->getProcessForCode($code); + + $p->start(); + + // Don't call Process::run nor Process::wait to avoid any read of pipes + $h = new \ReflectionProperty($p, 'process'); + $h->setAccessible(true); + $h = $h->getValue($p); + $s = @proc_get_status($h); + + while (!empty($s['running'])) { + usleep(1000); + $s = proc_get_status($h); + } + + $o = $p->getOutput(); + + $this->assertEquals($expectedOutputSize, \strlen($o)); + } + + public function testCallbacksAreExecutedWithStart() + { + $process = $this->getProcess('echo foo'); + $process->start(function ($type, $buffer) use (&$data) { + $data .= $buffer; + }); + + $process->wait(); + + $this->assertSame('foo'.PHP_EOL, $data); + } + + /** + * tests results from sub processes. + * + * @dataProvider responsesCodeProvider + */ + public function testProcessResponses($expected, $getter, $code) + { + $p = $this->getProcessForCode($code); + $p->run(); + + $this->assertSame($expected, $p->$getter()); + } + + /** + * tests results from sub processes. + * + * @dataProvider pipesCodeProvider + */ + public function testProcessPipes($code, $size) + { + $expected = str_repeat(str_repeat('*', 1024), $size).'!'; + $expectedLength = (1024 * $size) + 1; + + $p = $this->getProcessForCode($code); + $p->setInput($expected); + $p->run(); + + $this->assertEquals($expectedLength, \strlen($p->getOutput())); + $this->assertEquals($expectedLength, \strlen($p->getErrorOutput())); + } + + /** + * @dataProvider pipesCodeProvider + */ + public function testSetStreamAsInput($code, $size) + { + $expected = str_repeat(str_repeat('*', 1024), $size).'!'; + $expectedLength = (1024 * $size) + 1; + + $stream = fopen('php://temporary', 'w+'); + fwrite($stream, $expected); + rewind($stream); + + $p = $this->getProcessForCode($code); + $p->setInput($stream); + $p->run(); + + fclose($stream); + + $this->assertEquals($expectedLength, \strlen($p->getOutput())); + $this->assertEquals($expectedLength, \strlen($p->getErrorOutput())); + } + + public function testLiveStreamAsInput() + { + $stream = fopen('php://memory', 'r+'); + fwrite($stream, 'hello'); + rewind($stream); + + $p = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); + $p->setInput($stream); + $p->start(function ($type, $data) use ($stream) { + if ('hello' === $data) { + fclose($stream); + } + }); + $p->wait(); + + $this->assertSame('hello', $p->getOutput()); + } + + public function testSetInputWhileRunningThrowsAnException() + { + $this->expectException('Symfony\Component\Process\Exception\LogicException'); + $this->expectExceptionMessage('Input can not be set while the process is running.'); + $process = $this->getProcessForCode('sleep(30);'); + $process->start(); + try { + $process->setInput('foobar'); + $process->stop(); + $this->fail('A LogicException should have been raised.'); + } catch (LogicException $e) { + } + $process->stop(); + + throw $e; + } + + /** + * @dataProvider provideInvalidInputValues + */ + public function testInvalidInput($value) + { + $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException'); + $this->expectExceptionMessage('"Symfony\Component\Process\Process::setInput" only accepts strings, Traversable objects or stream resources.'); + $process = $this->getProcess('foo'); + $process->setInput($value); + } + + public function provideInvalidInputValues() + { + return [ + [[]], + [new NonStringifiable()], + ]; + } + + /** + * @dataProvider provideInputValues + */ + public function testValidInput($expected, $value) + { + $process = $this->getProcess('foo'); + $process->setInput($value); + $this->assertSame($expected, $process->getInput()); + } + + public function provideInputValues() + { + return [ + [null, null], + ['24.5', 24.5], + ['input data', 'input data'], + ]; + } + + public function chainedCommandsOutputProvider() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + return [ + ["2 \r\n2\r\n", '&&', '2'], + ]; + } + + return [ + ["1\n1\n", ';', '1'], + ["2\n2\n", '&&', '2'], + ]; + } + + /** + * @dataProvider chainedCommandsOutputProvider + */ + public function testChainedCommandsOutput($expected, $operator, $input) + { + $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input)); + $process->run(); + $this->assertEquals($expected, $process->getOutput()); + } + + public function testCallbackIsExecutedForOutput() + { + $p = $this->getProcessForCode('echo \'foo\';'); + + $called = false; + $p->run(function ($type, $buffer) use (&$called) { + $called = 'foo' === $buffer; + }); + + $this->assertTrue($called, 'The callback should be executed with the output'); + } + + public function testCallbackIsExecutedForOutputWheneverOutputIsDisabled() + { + $p = $this->getProcessForCode('echo \'foo\';'); + $p->disableOutput(); + + $called = false; + $p->run(function ($type, $buffer) use (&$called) { + $called = 'foo' === $buffer; + }); + + $this->assertTrue($called, 'The callback should be executed with the output'); + } + + public function testGetErrorOutput() + { + $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'); + + $p->run(); + $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches)); + } + + public function testFlushErrorOutput() + { + $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'); + + $p->run(); + $p->clearErrorOutput(); + $this->assertEmpty($p->getErrorOutput()); + } + + /** + * @dataProvider provideIncrementalOutput + */ + public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri) + { + $lock = tempnam(sys_get_temp_dir(), __FUNCTION__); + + $p = $this->getProcessForCode('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');'); + + $h = fopen($lock, 'w'); + flock($h, LOCK_EX); + + $p->start(); + + foreach (['foo', 'bar'] as $s) { + while (false === strpos($p->$getOutput(), $s)) { + usleep(1000); + } + + $this->assertSame($s, $p->$getIncrementalOutput()); + $this->assertSame('', $p->$getIncrementalOutput()); + + flock($h, LOCK_UN); + } + + fclose($h); + } + + public function provideIncrementalOutput() + { + return [ + ['getOutput', 'getIncrementalOutput', 'php://stdout'], + ['getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'], + ]; + } + + public function testGetOutput() + { + $p = $this->getProcessForCode('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }'); + + $p->run(); + $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches)); + } + + public function testFlushOutput() + { + $p = $this->getProcessForCode('$n=0;while ($n<3) {echo \' foo \';$n++;}'); + + $p->run(); + $p->clearOutput(); + $this->assertEmpty($p->getOutput()); + } + + public function testZeroAsOutput() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line + $p = $this->getProcess('echo | set /p dummyName=0'); + } else { + $p = $this->getProcess('printf 0'); + } + + $p->run(); + $this->assertSame('0', $p->getOutput()); + } + + public function testExitCodeCommandFailed() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX exit code'); + } + $this->skipIfNotEnhancedSigchild(); + + // such command run in bash return an exitcode 127 + $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis'); + $process->run(); + + $this->assertGreaterThan(0, $process->getExitCode()); + } + + public function testTTYCommand() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not have /dev/tty support'); + } + + $process = $this->getProcess('echo "foo" >> /dev/null && '.$this->getProcessForCode('usleep(100000);')->getCommandLine()); + $process->setTty(true); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->wait(); + + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + } + + public function testTTYCommandExitCode() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does have /dev/tty support'); + } + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('echo "foo" >> /dev/null'); + $process->setTty(true); + $process->run(); + + $this->assertTrue($process->isSuccessful()); + } + + public function testTTYInWindowsEnvironment() + { + $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); + $this->expectExceptionMessage('TTY mode is not supported on Windows platform.'); + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test is for Windows platform only'); + } + + $process = $this->getProcess('echo "foo" >> /dev/null'); + $process->setTty(false); + $process->setTty(true); + } + + public function testExitCodeTextIsNullWhenExitCodeIsNull() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess(''); + $this->assertNull($process->getExitCodeText()); + } + + public function testPTYCommand() + { + if (!Process::isPtySupported()) { + $this->markTestSkipped('PTY is not supported on this operating system.'); + } + + $process = $this->getProcess('echo "foo"'); + $process->setPty(true); + $process->run(); + + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + $this->assertEquals("foo\r\n", $process->getOutput()); + } + + public function testMustRun() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('echo foo'); + + $this->assertSame($process, $process->mustRun()); + $this->assertEquals('foo'.PHP_EOL, $process->getOutput()); + } + + public function testSuccessfulMustRunHasCorrectExitCode() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('echo foo')->mustRun(); + $this->assertEquals(0, $process->getExitCode()); + } + + public function testMustRunThrowsException() + { + $this->expectException('Symfony\Component\Process\Exception\ProcessFailedException'); + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('exit 1'); + $process->mustRun(); + } + + public function testExitCodeText() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess(''); + $r = new \ReflectionObject($process); + $p = $r->getProperty('exitcode'); + $p->setAccessible(true); + + $p->setValue($process, 2); + $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText()); + } + + public function testStartIsNonBlocking() + { + $process = $this->getProcessForCode('usleep(500000);'); + $start = microtime(true); + $process->start(); + $end = microtime(true); + $this->assertLessThan(0.4, $end - $start); + $process->stop(); + } + + public function testUpdateStatus() + { + $process = $this->getProcess('echo foo'); + $process->run(); + $this->assertGreaterThan(0, \strlen($process->getOutput())); + } + + public function testGetExitCodeIsNullOnStart() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcessForCode('usleep(100000);'); + $this->assertNull($process->getExitCode()); + $process->start(); + $this->assertNull($process->getExitCode()); + $process->wait(); + $this->assertEquals(0, $process->getExitCode()); + } + + public function testGetExitCodeIsNullOnWhenStartingAgain() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcessForCode('usleep(100000);'); + $process->run(); + $this->assertEquals(0, $process->getExitCode()); + $process->start(); + $this->assertNull($process->getExitCode()); + $process->wait(); + $this->assertEquals(0, $process->getExitCode()); + } + + public function testGetExitCode() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('echo foo'); + $process->run(); + $this->assertSame(0, $process->getExitCode()); + } + + public function testStatus() + { + $process = $this->getProcessForCode('usleep(100000);'); + $this->assertFalse($process->isRunning()); + $this->assertFalse($process->isStarted()); + $this->assertFalse($process->isTerminated()); + $this->assertSame(Process::STATUS_READY, $process->getStatus()); + $process->start(); + $this->assertTrue($process->isRunning()); + $this->assertTrue($process->isStarted()); + $this->assertFalse($process->isTerminated()); + $this->assertSame(Process::STATUS_STARTED, $process->getStatus()); + $process->wait(); + $this->assertFalse($process->isRunning()); + $this->assertTrue($process->isStarted()); + $this->assertTrue($process->isTerminated()); + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + } + + public function testStop() + { + $process = $this->getProcessForCode('sleep(31);'); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->stop(); + $this->assertFalse($process->isRunning()); + } + + public function testIsSuccessful() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('echo foo'); + $process->run(); + $this->assertTrue($process->isSuccessful()); + } + + public function testIsSuccessfulOnlyAfterTerminated() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcessForCode('usleep(100000);'); + $process->start(); + + $this->assertFalse($process->isSuccessful()); + + $process->wait(); + + $this->assertTrue($process->isSuccessful()); + } + + public function testIsNotSuccessful() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcessForCode('throw new \Exception(\'BOUM\');'); + $process->run(); + $this->assertFalse($process->isSuccessful()); + } + + public function testProcessIsNotSignaled() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('echo foo'); + $process->run(); + $this->assertFalse($process->hasBeenSignaled()); + } + + public function testProcessWithoutTermSignal() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('echo foo'); + $process->run(); + $this->assertEquals(0, $process->getTermSignal()); + } + + public function testProcessIsSignaledIfStopped() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcessForCode('sleep(32);'); + $process->start(); + $process->stop(); + $this->assertTrue($process->hasBeenSignaled()); + $this->assertEquals(15, $process->getTermSignal()); // SIGTERM + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); + $this->expectExceptionMessage('The process has been signaled'); + if (!\function_exists('posix_kill')) { + $this->markTestSkipped('Function posix_kill is required.'); + } + $this->skipIfNotEnhancedSigchild(false); + + $process = $this->getProcessForCode('sleep(32.1);'); + $process->start(); + posix_kill($process->getPid(), 9); // SIGKILL + + $process->wait(); + } + + public function testRestart() + { + $process1 = $this->getProcessForCode('echo getmypid();'); + $process1->run(); + $process2 = $process1->restart(); + + $process2->wait(); // wait for output + + // Ensure that both processed finished and the output is numeric + $this->assertFalse($process1->isRunning()); + $this->assertFalse($process2->isRunning()); + $this->assertIsNumeric($process1->getOutput()); + $this->assertIsNumeric($process2->getOutput()); + + // Ensure that restart returned a new process by check that the output is different + $this->assertNotEquals($process1->getOutput(), $process2->getOutput()); + } + + public function testRunProcessWithTimeout() + { + $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException'); + $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.'); + $process = $this->getProcessForCode('sleep(30);'); + $process->setTimeout(0.1); + $start = microtime(true); + try { + $process->run(); + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + } + + $this->assertLessThan(15, microtime(true) - $start); + + throw $e; + } + + public function testIterateOverProcessWithTimeout() + { + $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException'); + $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.'); + $process = $this->getProcessForCode('sleep(30);'); + $process->setTimeout(0.1); + $start = microtime(true); + try { + $process->start(); + foreach ($process as $buffer); + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + } + + $this->assertLessThan(15, microtime(true) - $start); + + throw $e; + } + + public function testCheckTimeoutOnNonStartedProcess() + { + $process = $this->getProcess('echo foo'); + $this->assertNull($process->checkTimeout()); + } + + public function testCheckTimeoutOnTerminatedProcess() + { + $process = $this->getProcess('echo foo'); + $process->run(); + $this->assertNull($process->checkTimeout()); + } + + public function testCheckTimeoutOnStartedProcess() + { + $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException'); + $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.'); + $process = $this->getProcessForCode('sleep(33);'); + $process->setTimeout(0.1); + + $process->start(); + $start = microtime(true); + + try { + while ($process->isRunning()) { + $process->checkTimeout(); + usleep(100000); + } + $this->fail('A ProcessTimedOutException should have been raised'); + } catch (ProcessTimedOutException $e) { + } + + $this->assertLessThan(15, microtime(true) - $start); + + throw $e; + } + + public function testIdleTimeout() + { + $process = $this->getProcessForCode('sleep(34);'); + $process->setTimeout(60); + $process->setIdleTimeout(0.1); + + try { + $process->run(); + + $this->fail('A timeout exception was expected.'); + } catch (ProcessTimedOutException $e) { + $this->assertTrue($e->isIdleTimeout()); + $this->assertFalse($e->isGeneralTimeout()); + $this->assertEquals(0.1, $e->getExceededTimeout()); + } + } + + public function testIdleTimeoutNotExceededWhenOutputIsSent() + { + $process = $this->getProcessForCode('while (true) {echo \'foo \'; usleep(1000);}'); + $process->setTimeout(1); + $process->start(); + + while (false === strpos($process->getOutput(), 'foo')) { + usleep(1000); + } + + $process->setIdleTimeout(0.5); + + try { + $process->wait(); + $this->fail('A timeout exception was expected.'); + } catch (ProcessTimedOutException $e) { + $this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.'); + $this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.'); + $this->assertEquals(1, $e->getExceededTimeout()); + } + } + + public function testStartAfterATimeout() + { + $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException'); + $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.'); + $process = $this->getProcessForCode('sleep(35);'); + $process->setTimeout(0.1); + + try { + $process->run(); + $this->fail('A ProcessTimedOutException should have been raised.'); + } catch (ProcessTimedOutException $e) { + } + $this->assertFalse($process->isRunning()); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->stop(0); + + throw $e; + } + + public function testGetPid() + { + $process = $this->getProcessForCode('sleep(36);'); + $process->start(); + $this->assertGreaterThan(0, $process->getPid()); + $process->stop(0); + } + + public function testGetPidIsNullBeforeStart() + { + $process = $this->getProcess('foo'); + $this->assertNull($process->getPid()); + } + + public function testGetPidIsNullAfterRun() + { + $process = $this->getProcess('echo foo'); + $process->run(); + $this->assertNull($process->getPid()); + } + + /** + * @requires extension pcntl + */ + public function testSignal() + { + $process = $this->getProcess([self::$phpBin, __DIR__.'/SignalListener.php']); + $process->start(); + + while (false === strpos($process->getOutput(), 'Caught')) { + usleep(1000); + } + $process->signal(SIGUSR1); + $process->wait(); + + $this->assertEquals('Caught SIGUSR1', $process->getOutput()); + } + + /** + * @requires extension pcntl + */ + public function testExitCodeIsAvailableAfterSignal() + { + $this->skipIfNotEnhancedSigchild(); + + $process = $this->getProcess('sleep 4'); + $process->start(); + $process->signal(SIGKILL); + + while ($process->isRunning()) { + usleep(10000); + } + + $this->assertFalse($process->isRunning()); + $this->assertTrue($process->hasBeenSignaled()); + $this->assertFalse($process->isSuccessful()); + $this->assertEquals(137, $process->getExitCode()); + } + + public function testSignalProcessNotRunning() + { + $this->expectException('Symfony\Component\Process\Exception\LogicException'); + $this->expectExceptionMessage('Can not send signal on a non running process.'); + $process = $this->getProcess('foo'); + $process->signal(1); // SIGHUP + } + + /** + * @dataProvider provideMethodsThatNeedARunningProcess + */ + public function testMethodsThatNeedARunningProcess($method) + { + $process = $this->getProcess('foo'); + + $this->expectException('Symfony\Component\Process\Exception\LogicException'); + $this->expectExceptionMessage(sprintf('Process must be started before calling "%s()".', $method)); + + $process->{$method}(); + } + + public function provideMethodsThatNeedARunningProcess() + { + return [ + ['getOutput'], + ['getIncrementalOutput'], + ['getErrorOutput'], + ['getIncrementalErrorOutput'], + ['wait'], + ]; + } + + /** + * @dataProvider provideMethodsThatNeedATerminatedProcess + */ + public function testMethodsThatNeedATerminatedProcess($method) + { + $this->expectException('Symfony\Component\Process\Exception\LogicException'); + $this->expectExceptionMessage('Process must be terminated before calling'); + $process = $this->getProcessForCode('sleep(37);'); + $process->start(); + try { + $process->{$method}(); + $process->stop(0); + $this->fail('A LogicException must have been thrown'); + } catch (\Exception $e) { + } + $process->stop(0); + + throw $e; + } + + public function provideMethodsThatNeedATerminatedProcess() + { + return [ + ['hasBeenSignaled'], + ['getTermSignal'], + ['hasBeenStopped'], + ['getStopSignal'], + ]; + } + + /** + * @dataProvider provideWrongSignal + */ + public function testWrongSignal($signal) + { + $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('POSIX signals do not work on Windows'); + } + + $process = $this->getProcessForCode('sleep(38);'); + $process->start(); + try { + $process->signal($signal); + $this->fail('A RuntimeException must have been thrown'); + } catch (RuntimeException $e) { + $process->stop(0); + } + + throw $e; + } + + public function provideWrongSignal() + { + return [ + [-4], + ['Céphalopodes'], + ]; + } + + public function testDisableOutputDisablesTheOutput() + { + $p = $this->getProcess('foo'); + $this->assertFalse($p->isOutputDisabled()); + $p->disableOutput(); + $this->assertTrue($p->isOutputDisabled()); + $p->enableOutput(); + $this->assertFalse($p->isOutputDisabled()); + } + + public function testDisableOutputWhileRunningThrowsException() + { + $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); + $this->expectExceptionMessage('Disabling output while the process is running is not possible.'); + $p = $this->getProcessForCode('sleep(39);'); + $p->start(); + $p->disableOutput(); + } + + public function testEnableOutputWhileRunningThrowsException() + { + $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); + $this->expectExceptionMessage('Enabling output while the process is running is not possible.'); + $p = $this->getProcessForCode('sleep(40);'); + $p->disableOutput(); + $p->start(); + $p->enableOutput(); + } + + public function testEnableOrDisableOutputAfterRunDoesNotThrowException() + { + $p = $this->getProcess('echo foo'); + $p->disableOutput(); + $p->run(); + $p->enableOutput(); + $p->disableOutput(); + $this->assertTrue($p->isOutputDisabled()); + } + + public function testDisableOutputWhileIdleTimeoutIsSet() + { + $this->expectException('Symfony\Component\Process\Exception\LogicException'); + $this->expectExceptionMessage('Output can not be disabled while an idle timeout is set.'); + $process = $this->getProcess('foo'); + $process->setIdleTimeout(1); + $process->disableOutput(); + } + + public function testSetIdleTimeoutWhileOutputIsDisabled() + { + $this->expectException('Symfony\Component\Process\Exception\LogicException'); + $this->expectExceptionMessage('timeout can not be set while the output is disabled.'); + $process = $this->getProcess('foo'); + $process->disableOutput(); + $process->setIdleTimeout(1); + } + + public function testSetNullIdleTimeoutWhileOutputIsDisabled() + { + $process = $this->getProcess('foo'); + $process->disableOutput(); + $this->assertSame($process, $process->setIdleTimeout(null)); + } + + /** + * @dataProvider provideOutputFetchingMethods + */ + public function testGetOutputWhileDisabled($fetchMethod) + { + $this->expectException('Symfony\Component\Process\Exception\LogicException'); + $this->expectExceptionMessage('Output has been disabled.'); + $p = $this->getProcessForCode('sleep(41);'); + $p->disableOutput(); + $p->start(); + $p->{$fetchMethod}(); + } + + public function provideOutputFetchingMethods() + { + return [ + ['getOutput'], + ['getIncrementalOutput'], + ['getErrorOutput'], + ['getIncrementalErrorOutput'], + ]; + } + + public function testStopTerminatesProcessCleanly() + { + $process = $this->getProcessForCode('echo 123; sleep(42);'); + $process->run(function () use ($process) { + $process->stop(); + }); + $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException'); + } + + public function testKillSignalTerminatesProcessCleanly() + { + $process = $this->getProcessForCode('echo 123; sleep(43);'); + $process->run(function () use ($process) { + $process->signal(9); // SIGKILL + }); + $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); + } + + public function testTermSignalTerminatesProcessCleanly() + { + $process = $this->getProcessForCode('echo 123; sleep(44);'); + $process->run(function () use ($process) { + $process->signal(15); // SIGTERM + }); + $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); + } + + public function responsesCodeProvider() + { + return [ + //expected output / getter / code to execute + //[1,'getExitCode','exit(1);'], + //[true,'isSuccessful','exit();'], + ['output', 'getOutput', 'echo \'output\';'], + ]; + } + + public function pipesCodeProvider() + { + $variations = [ + 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);', + 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';', + ]; + + if ('\\' === \DIRECTORY_SEPARATOR) { + // Avoid XL buffers on Windows because of https://bugs.php.net/65650 + $sizes = [1, 2, 4, 8]; + } else { + $sizes = [1, 16, 64, 1024, 4096]; + } + + $codes = []; + foreach ($sizes as $size) { + foreach ($variations as $code) { + $codes[] = [$code, $size]; + } + } + + return $codes; + } + + /** + * @dataProvider provideVariousIncrementals + */ + public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method) + { + $process = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }', null, null, null, null); + $process->start(); + $result = ''; + $limit = microtime(true) + 3; + $expected = '012'; + + while ($result !== $expected && microtime(true) < $limit) { + $result .= $process->$method(); + } + + $this->assertSame($expected, $result); + $process->stop(); + } + + public function provideVariousIncrementals() + { + return [ + ['php://stdout', 'getIncrementalOutput'], + ['php://stderr', 'getIncrementalErrorOutput'], + ]; + } + + public function testIteratorInput() + { + $input = function () { + yield 'ping'; + yield 'pong'; + }; + + $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);', null, null, $input()); + $process->run(); + $this->assertSame('pingpong', $process->getOutput()); + } + + public function testSimpleInputStream() + { + $input = new InputStream(); + + $process = $this->getProcessForCode('echo \'ping\'; echo fread(STDIN, 4); echo fread(STDIN, 4);'); + $process->setInput($input); + + $process->start(function ($type, $data) use ($input) { + if ('ping' === $data) { + $input->write('pang'); + } elseif (!$input->isClosed()) { + $input->write('pong'); + $input->close(); + } + }); + + $process->wait(); + $this->assertSame('pingpangpong', $process->getOutput()); + } + + public function testInputStreamWithCallable() + { + $i = 0; + $stream = fopen('php://memory', 'w+'); + $stream = function () use ($stream, &$i) { + if ($i < 3) { + rewind($stream); + fwrite($stream, ++$i); + rewind($stream); + + return $stream; + } + + return null; + }; + + $input = new InputStream(); + $input->onEmpty($stream); + $input->write($stream()); + + $process = $this->getProcessForCode('echo fread(STDIN, 3);'); + $process->setInput($input); + $process->start(function ($type, $data) use ($input) { + $input->close(); + }); + + $process->wait(); + $this->assertSame('123', $process->getOutput()); + } + + public function testInputStreamWithGenerator() + { + $input = new InputStream(); + $input->onEmpty(function ($input) { + yield 'pong'; + $input->close(); + }); + + $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); + $process->setInput($input); + $process->start(); + $input->write('ping'); + $process->wait(); + $this->assertSame('pingpong', $process->getOutput()); + } + + public function testInputStreamOnEmpty() + { + $i = 0; + $input = new InputStream(); + $input->onEmpty(function () use (&$i) { ++$i; }); + + $process = $this->getProcessForCode('echo 123; echo fread(STDIN, 1); echo 456;'); + $process->setInput($input); + $process->start(function ($type, $data) use ($input) { + if ('123' === $data) { + $input->close(); + } + }); + $process->wait(); + + $this->assertSame(0, $i, 'InputStream->onEmpty callback should be called only when the input *becomes* empty'); + $this->assertSame('123456', $process->getOutput()); + } + + public function testIteratorOutput() + { + $input = new InputStream(); + + $process = $this->getProcessForCode('fwrite(STDOUT, 123); fwrite(STDERR, 234); flush(); usleep(10000); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);'); + $process->setInput($input); + $process->start(); + $output = []; + + foreach ($process as $type => $data) { + $output[] = [$type, $data]; + break; + } + $expectedOutput = [ + [$process::OUT, '123'], + ]; + $this->assertSame($expectedOutput, $output); + + $input->write(345); + + foreach ($process as $type => $data) { + $output[] = [$type, $data]; + } + + $this->assertSame('', $process->getOutput()); + $this->assertFalse($process->isRunning()); + + $expectedOutput = [ + [$process::OUT, '123'], + [$process::ERR, '234'], + [$process::OUT, '345'], + [$process::ERR, '456'], + ]; + $this->assertSame($expectedOutput, $output); + } + + public function testNonBlockingNorClearingIteratorOutput() + { + $input = new InputStream(); + + $process = $this->getProcessForCode('fwrite(STDOUT, fread(STDIN, 3));'); + $process->setInput($input); + $process->start(); + $output = []; + + foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) { + $output[] = [$type, $data]; + break; + } + $expectedOutput = [ + [$process::OUT, ''], + ]; + $this->assertSame($expectedOutput, $output); + + $input->write(123); + + foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) { + if ('' !== $data) { + $output[] = [$type, $data]; + } + } + + $this->assertSame('123', $process->getOutput()); + $this->assertFalse($process->isRunning()); + + $expectedOutput = [ + [$process::OUT, ''], + [$process::OUT, '123'], + ]; + $this->assertSame($expectedOutput, $output); + } + + public function testChainedProcesses() + { + $p1 = $this->getProcessForCode('fwrite(STDERR, 123); fwrite(STDOUT, 456);'); + $p2 = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); + $p2->setInput($p1); + + $p1->start(); + $p2->run(); + + $this->assertSame('123', $p1->getErrorOutput()); + $this->assertSame('', $p1->getOutput()); + $this->assertSame('', $p2->getErrorOutput()); + $this->assertSame('456', $p2->getOutput()); + } + + public function testSetBadEnv() + { + $process = $this->getProcess('echo hello'); + $process->setEnv(['bad%%' => '123']); + $process->inheritEnvironmentVariables(true); + + $process->run(); + + $this->assertSame('hello'.PHP_EOL, $process->getOutput()); + $this->assertSame('', $process->getErrorOutput()); + } + + public function testEnvBackupDoesNotDeleteExistingVars() + { + putenv('existing_var=foo'); + $_ENV['existing_var'] = 'foo'; + $process = $this->getProcess('php -r "echo getenv(\'new_test_var\');"'); + $process->setEnv(['existing_var' => 'bar', 'new_test_var' => 'foo']); + $process->inheritEnvironmentVariables(); + + $process->run(); + + $this->assertSame('foo', $process->getOutput()); + $this->assertSame('foo', getenv('existing_var')); + $this->assertFalse(getenv('new_test_var')); + + putenv('existing_var'); + unset($_ENV['existing_var']); + } + + public function testEnvIsInherited() + { + $process = $this->getProcessForCode('echo serialize($_SERVER);', null, ['BAR' => 'BAZ', 'EMPTY' => '']); + + putenv('FOO=BAR'); + $_ENV['FOO'] = 'BAR'; + + $process->run(); + + $expected = ['BAR' => 'BAZ', 'EMPTY' => '', 'FOO' => 'BAR']; + $env = array_intersect_key(unserialize($process->getOutput()), $expected); + + $this->assertEquals($expected, $env); + + putenv('FOO'); + unset($_ENV['FOO']); + } + + /** + * @group legacy + */ + public function testInheritEnvDisabled() + { + $process = $this->getProcessForCode('echo serialize($_SERVER);', null, ['BAR' => 'BAZ']); + + putenv('FOO=BAR'); + $_ENV['FOO'] = 'BAR'; + + $this->assertSame($process, $process->inheritEnvironmentVariables(false)); + $this->assertFalse($process->areEnvironmentVariablesInherited()); + + $process->run(); + + $expected = ['BAR' => 'BAZ', 'FOO' => 'BAR']; + $env = array_intersect_key(unserialize($process->getOutput()), $expected); + unset($expected['FOO']); + + $this->assertSame($expected, $env); + + putenv('FOO'); + unset($_ENV['FOO']); + } + + public function testGetCommandLine() + { + $p = new Process(['/usr/bin/php']); + + $expected = '\\' === \DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'"; + $this->assertSame($expected, $p->getCommandLine()); + } + + /** + * @dataProvider provideEscapeArgument + */ + public function testEscapeArgument($arg) + { + $p = new Process([self::$phpBin, '-r', 'echo $argv[1];', $arg]); + $p->run(); + + $this->assertSame((string) $arg, $p->getOutput()); + } + + /** + * @dataProvider provideEscapeArgument + * @group legacy + */ + public function testEscapeArgumentWhenInheritEnvDisabled($arg) + { + $p = new Process([self::$phpBin, '-r', 'echo $argv[1];', $arg], null, ['BAR' => 'BAZ']); + $p->inheritEnvironmentVariables(false); + $p->run(); + + $this->assertSame((string) $arg, $p->getOutput()); + } + + public function testRawCommandLine() + { + $p = new Process(sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);'))); + $p->run(); + + $expected = << - + [1] => a + [2] => + [3] => b +) + +EOTXT; + $this->assertSame($expected, str_replace('Standard input code', '-', $p->getOutput())); + } + + public function provideEscapeArgument() + { + yield ['a"b%c%']; + yield ['a"b^c^']; + yield ["a\nb'c"]; + yield ['a^b c!']; + yield ["a!b\tc"]; + yield ['a\\\\"\\"']; + yield ['éÉèÈàÀöä']; + yield [null]; + yield [1]; + yield [1.1]; + } + + public function testEnvArgument() + { + $env = ['FOO' => 'Foo', 'BAR' => 'Bar']; + $cmd = '\\' === \DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ'; + $p = new Process($cmd, null, $env); + $p->run(null, ['BAR' => 'baR', 'BAZ' => 'baZ']); + + $this->assertSame('Foo baR baZ', rtrim($p->getOutput())); + $this->assertSame($env, $p->getEnv()); + } + + public function testWaitStoppedDeadProcess() + { + $process = $this->getProcess(self::$phpBin.' '.__DIR__.'/ErrorProcessInitiator.php -e '.self::$phpBin); + $process->start(); + $process->setTimeout(2); + $process->wait(); + $this->assertFalse($process->isRunning()); + } + + /** + * @param string $commandline + * @param string|null $cwd + * @param string|null $input + * @param int $timeout + * + * @return Process + */ + private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60) + { + $process = new Process($commandline, $cwd, $env, $input, $timeout); + $process->inheritEnvironmentVariables(); + + if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) { + try { + $process->setEnhanceSigchildCompatibility(false); + $process->getExitCode(); + $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.'); + } catch (RuntimeException $e) { + $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage()); + if ($enhance) { + $process->setEnhanceSigchildCompatibility(true); + } else { + self::$notEnhancedSigchild = true; + } + } + } + + if (self::$process) { + self::$process->stop(0); + } + + return self::$process = $process; + } + + /** + * @return Process + */ + private function getProcessForCode($code, $cwd = null, array $env = null, $input = null, $timeout = 60) + { + return $this->getProcess([self::$phpBin, '-r', $code], $cwd, $env, $input, $timeout); + } + + private function skipIfNotEnhancedSigchild($expectException = true) + { + if (self::$sigchild) { + if (!$expectException) { + $this->markTestSkipped('PHP is compiled with --enable-sigchild.'); + } elseif (self::$notEnhancedSigchild) { + $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); + $this->expectExceptionMessage('This PHP has been compiled with --enable-sigchild.'); + } + } + } +} + +class NonStringifiable +{ +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessUtilsTest.php b/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessUtilsTest.php new file mode 100644 index 0000000..0c2fe94 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/ProcessUtilsTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\ProcessUtils; + +/** + * @group legacy + */ +class ProcessUtilsTest extends TestCase +{ + /** + * @dataProvider dataArguments + */ + public function testEscapeArgument($result, $argument) + { + $this->assertSame($result, ProcessUtils::escapeArgument($argument)); + } + + public function dataArguments() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + return [ + ['"\"php\" \"-v\""', '"php" "-v"'], + ['"foo bar"', 'foo bar'], + ['^%"path"^%', '%path%'], + ['"<|>\\" \\"\'f"', '<|>" "\'f'], + ['""', ''], + ['"with\trailingbs\\\\"', 'with\trailingbs\\'], + ]; + } + + return [ + ["'\"php\" \"-v\"'", '"php" "-v"'], + ["'foo bar'", 'foo bar'], + ["'%path%'", '%path%'], + ["'<|>\" \"'\\''f'", '<|>" "\'f'], + ["''", ''], + ["'with\\trailingbs\\'", 'with\trailingbs\\'], + ["'withNonAsciiAccentLikeéÉèÈàÀöä'", 'withNonAsciiAccentLikeéÉèÈàÀöä'], + ]; + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/Tests/SignalListener.php b/plugins/panakour/backup/vendor/symfony/process/Tests/SignalListener.php new file mode 100644 index 0000000..9e30ce3 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/Tests/SignalListener.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +pcntl_signal(SIGUSR1, function () { echo 'SIGUSR1'; exit; }); + +echo 'Caught '; + +$n = 0; + +while ($n++ < 400) { + usleep(10000); + pcntl_signal_dispatch(); +} diff --git a/plugins/panakour/backup/vendor/symfony/process/composer.json b/plugins/panakour/backup/vendor/symfony/process/composer.json new file mode 100644 index 0000000..b8867db --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/process", + "type": "library", + "description": "Symfony Process Component", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Process\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + } +} diff --git a/plugins/panakour/backup/vendor/symfony/process/phpunit.xml.dist b/plugins/panakour/backup/vendor/symfony/process/phpunit.xml.dist new file mode 100644 index 0000000..c32f251 --- /dev/null +++ b/plugins/panakour/backup/vendor/symfony/process/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/plugins/romanah/gokbakja/components/MachineProduction.php b/plugins/romanah/gokbakja/components/MachineProduction.php index e0a3bd4..86eaf4d 100644 --- a/plugins/romanah/gokbakja/components/MachineProduction.php +++ b/plugins/romanah/gokbakja/components/MachineProduction.php @@ -114,6 +114,8 @@ class MachineProduction extends ComponentBase $data = post(); $machinProdId = $data["machineProdId"]; + $user = \Auth::user(); + $production = ProductionMachineModel::where("id", $machinProdId)->first(); $colors = BagColor::get(); @@ -132,7 +134,7 @@ class MachineProduction extends ComponentBase
    - user_id != $user->id ? 'disabled' : '') .'> '; for ($x = 0; $x < count($machines); $x++) { $html_data .=''; @@ -142,12 +144,12 @@ class MachineProduction extends ComponentBase
    - + user_id != $user->id ? 'disabled' : '') .'>
    - user_id != $user->id ? 'disabled' : '') .'> '; for ($x = 0; $x < count($sizes); $x++) { @@ -158,7 +160,7 @@ class MachineProduction extends ComponentBase
    - user_id != $user->id ? 'disabled' : '') .'> '; for ($x = 0; $x < count($types); $x++) { $html_data .=''; @@ -167,7 +169,7 @@ class MachineProduction extends ComponentBase
    - user_id != $user->id ? 'disabled' : '') .'> '; for ($x = 0; $x < count($colors); $x++) { $html_data .=''; @@ -177,15 +179,15 @@ class MachineProduction extends ComponentBase
    + placeholder="Mukdar (kg)" value="'.$production->produced_weight.'" '.($production->user_id != $user->id ? 'disabled' : '') .'>
    - + user_id != $user->id ? 'disabled' : '') .'>
    -
    @@ -217,6 +219,7 @@ class MachineProduction extends ComponentBase public function onRender() { + $user = \Auth::user(); $html_data = ''; $currentDateFormat = Carbon::now()->format('Y-m-d'); @@ -225,6 +228,14 @@ class MachineProduction extends ComponentBase $html_data = ''; for ($x = 0; $x < count($machineProductions); $x++) { + + $editBtn = ''; + + if($machineProductions[$x]->user_id != $user->id){ + $editBtn = ' Size degişli däl '; + }else{ + $editBtn = ' ÜÝTGET '; + } // dd($machineProductions[0]->bag_size); $html_data .= ' ' . ($x + 1) . ' @@ -243,7 +254,7 @@ class MachineProduction extends ComponentBase ' . $machineProductions[$x]->employee_name . ' ' . $machineProductions[$x]->mechanic_name . ' ' . $machineProductions[$x]->note . ' - ÜÝTGET + '.$editBtn.' '; } diff --git a/plugins/romanah/gokbakja/components/Order.php b/plugins/romanah/gokbakja/components/Order.php index c5c7cf5..83278d9 100644 --- a/plugins/romanah/gokbakja/components/Order.php +++ b/plugins/romanah/gokbakja/components/Order.php @@ -231,6 +231,7 @@ class Order extends ComponentBase public function onRender() { + $user = \Auth::user(); $html_data = ''; @@ -249,6 +250,14 @@ class Order extends ComponentBase for ($x = 0; $x < count($orderDatas); $x++) { + $editBtn = ''; + + if($orderDatas[$x]->user_id != $user->id){ + $editBtn = 'Size degişli däl '; + }else{ + $editBtn = 'POZ'; + } + $html_data .= ' ' . ($x + 1) . ' Sargyt #' . $orderDatas[$x]->id . ' @@ -265,7 +274,7 @@ class Order extends ComponentBase ' . $orderDatas[$x]->created_at->format('d.m.Y') . ' ' . $orderDatas[$x]->note . ' - POZ + '.$editBtn.' '; } @@ -285,50 +294,44 @@ class Order extends ComponentBase $createPayment->order_id = $orderId; $createPayment->save(); - $html_data = ''; + // $html_data = ''; - $paymentDatas = PaymentModel::where("order_id", $orderId)->orderBy('id', 'DESC')->get(); + // $paymentDatas = PaymentModel::where("order_id", $orderId)->orderBy('id', 'DESC')->get(); - for ($x = 0; $x < count($paymentDatas); $x++) { - // dd($orderDatas[$x]->shipping->status); - $html_data .= ' - ' . ($x + 1) . ' - ' . $paymentDatas[$x]->amount . ' $ - ' . ($paymentDatas[$x]->date == null ? "" : Carbon::parse($paymentDatas[$x]->date)->format('d.m.Y')) . ' - ' . $paymentDatas[$x]->note . ' - Täzele - '; - } + // for ($x = 0; $x < count($paymentDatas); $x++) { + // // dd($orderDatas[$x]->shipping->status); + // $html_data .= ' + // ' . ($x + 1) . ' + // ' . $paymentDatas[$x]->amount . ' $ + // ' . ($paymentDatas[$x]->date == null ? "" : Carbon::parse($paymentDatas[$x]->date)->format('d.m.Y')) . ' + // ' . $paymentDatas[$x]->note . ' + // Täzele + // '; + // } - $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->first(); + // $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) + // ->withCount(['order_items as order_all_amount' => function ($query) { + // $query->select(DB::raw('sum(amount)')); + // }]) + // ->withCount(['order_items as order_all_price' => function ($query) { + // $query->select(DB::raw('sum(price)')); + // }]) + // ->first(); - $all = ($order->order_all_amount * $order->order_all_price); + // $all = ($order->order_all_amount * $order->order_all_price); - $allPaid = PaymentModel::where("order_id", $orderId)->sum("amount"); - $allNot = ($all - $allPaid); + // $allPaid = PaymentModel::where("order_id", $orderId)->sum("amount"); + // $allNot = ($all - $allPaid); if ($createPayment) { Flash::success("Töleg Ustunlikli Goşuldy"); - return [ - '#payment_item_datas' => $html_data, - '#allAmount' => $order->order_all_amount . " kg", - '#allPrice' => $order->order_all_price . " $", - '#all' => "Jemi Bahasy: " . number_format($all) . " $", - '#all_payment' => "Jemi Tölenen Töleg: " . number_format($allPaid) . " $", - '#all_not' => "Bergisi: " . number_format($allNot) . "$", - ]; + return Redirect::refresh(); } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } // } } @@ -393,50 +396,52 @@ class Order extends ComponentBase $createPayment->order_id = $orderId; $createPayment->save(); - $html_data = ''; + // $html_data = ''; - $paymentDatas = PaymentModel::where("order_id", $orderId)->orderBy('id', 'DESC')->get(); + // $paymentDatas = PaymentModel::where("order_id", $orderId)->orderBy('id', 'DESC')->get(); - for ($x = 0; $x < count($paymentDatas); $x++) { - // dd($orderDatas[$x]->shipping->status); - $html_data .= ' - ' . ($x + 1) . ' - ' . $paymentDatas[$x]->amount . ' $ - ' . ($paymentDatas[$x]->date == null ? "" : Carbon::parse($paymentDatas[$x]->date)->format('d.m.Y')) . ' - ' . $paymentDatas[$x]->note . ' - Täzele - '; - } + // for ($x = 0; $x < count($paymentDatas); $x++) { + // // dd($orderDatas[$x]->shipping->status); + // $html_data .= ' + // ' . ($x + 1) . ' + // ' . $paymentDatas[$x]->amount . ' $ + // ' . ($paymentDatas[$x]->date == null ? "" : Carbon::parse($paymentDatas[$x]->date)->format('d.m.Y')) . ' + // ' . $paymentDatas[$x]->note . ' + // Täzele + // '; + // } - $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->first(); + // $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) + // ->withCount(['order_items as order_all_amount' => function ($query) { + // $query->select(DB::raw('sum(amount)')); + // }]) + // ->withCount(['order_items as order_all_price' => function ($query) { + // $query->select(DB::raw('sum(price)')); + // }]) + // ->first(); - $all = ($order->order_all_amount * $order->order_all_price); + // $all = ($order->order_all_amount * $order->order_all_price); - $allPaid = PaymentModel::where("order_id", $orderId)->sum("amount"); - $allNot = ($all - $allPaid); + // $allPaid = PaymentModel::where("order_id", $orderId)->sum("amount"); + // $allNot = ($all - $allPaid); if ($createPayment) { Flash::success("Töleg Ustunlikli Goşuldy"); - return [ - '#payment_item_datas' => $html_data, - '#allAmount' => $order->order_all_amount . " kg", - '#allPrice' => $order->order_all_price . " $", - '#all' => "Jemi Bahasy: " . number_format($all) . " $", - '#all_payment' => "Jemi Tölenen Töleg: " . number_format($allPaid) . " $", - '#all_not' => "Bergisi: " . number_format($allNot) . "$", - ]; + return Redirect::refresh(); + // return [ + // '#payment_item_datas' => $html_data, + // '#allAmount' => $order->order_all_amount . " kg", + // '#allPrice' => $order->order_all_price . " $", + // '#all' => "Jemi Bahasy: " . number_format($all) . " $", + // '#all_payment' => "Jemi Tölenen Töleg: " . number_format($allPaid) . " $", + // '#all_not' => "Bergisi: " . number_format($allNot) . "$", + // ]; } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } // } } @@ -553,64 +558,67 @@ class Order extends ComponentBase - $html_data = ''; + // $html_data = ''; - $transportDatas = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->orderBy('id', 'DESC')->get(); + // $transportDatas = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->orderBy('id', 'DESC')->get(); - for ($x = 0; $x < count($transportDatas); $x++) { - // dd($orderDatas[$x]->shipping->status); - $html_data .= ' - ' . ($x + 1) . ' - ' . $transportDatas[$x]->transport_type . ' - ' . $transportDatas[$x]->place_now . ' - ' . $transportDatas[$x]->loaded_amount . ' kg - ' . $transportDatas[$x]->transport_no . ' - ' . Carbon::parse($transportDatas[$x]->date)->format('d.m.Y') ?? " " . ''; + // for ($x = 0; $x < count($transportDatas); $x++) { + // // dd($orderDatas[$x]->shipping->status); + // $html_data .= ' + // ' . ($x + 1) . ' + // ' . $transportDatas[$x]->transport_type . ' + // ' . $transportDatas[$x]->place_now . ' + // ' . $transportDatas[$x]->loaded_amount . ' kg + // ' . $transportDatas[$x]->transport_no . ' + // ' . Carbon::parse($transportDatas[$x]->date)->format('d.m.Y') ?? " " . ''; - $html_data .= '' . $transportDatas[$x]->note . ' - ' . $transportDatas[$x]->status . ''; + // $html_data .= '' . $transportDatas[$x]->note . ' + // ' . $transportDatas[$x]->status . ''; - if ($transportDatas[$x]->status == "complated") { - $html_data .= ' ' . $transportDatas[$x]->status . ' '; - } else { - $html_data .= ' Çalyşmak '; - } - $html_data .= ' Täzele - '; - } + // if ($transportDatas[$x]->status == "complated") { + // $html_data .= ' ' . $transportDatas[$x]->status . ' '; + // } else { + // $html_data .= ' Çalyşmak '; + // } + // $html_data .= ' Täzele + // '; + // } - $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->first(); + // $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) + // ->withCount(['order_items as order_all_amount' => function ($query) { + // $query->select(DB::raw('sum(amount)')); + // }]) + // ->withCount(['order_items as order_all_price' => function ($query) { + // $query->select(DB::raw('sum(price)')); + // }]) + // ->first(); - $all = ($order->order_all_amount * $order->order_all_price); + // $all = ($order->order_all_amount * $order->order_all_price); - $allLoaded = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->sum("loaded_amount"); + // $allLoaded = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->sum("loaded_amount"); if ($createShippingTransport) { Flash::success("Transport Ustunlikli Goşuldy"); - return [ - '#transport_item_datas' => $html_data, - '#allAmount' => $order->order_all_amount . " kg", - '#allPrice' => $order->order_all_price . " $", - '#all' => "Jemi Bahasy: " . number_format($all, 2) . " $", - '#all_loaded' => "Jemi Ýüklenen Ýük: " . number_format($allLoaded, 2) . " kg", - ]; + return Redirect::refresh(); + // return [ + // '#transport_item_datas' => $html_data, + // '#allAmount' => $order->order_all_amount . " kg", + // '#allPrice' => $order->order_all_price . " $", + // '#all' => "Jemi Bahasy: " . number_format($all, 2) . " $", + // '#all_loaded' => "Jemi Ýüklenen Ýük: " . number_format($allLoaded, 2) . " kg", + // ]; } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } } else { Flash::error("Ýüklenmeli Transport Saýlaň!"); + return Redirect::refresh(); } // } } @@ -742,61 +750,63 @@ class Order extends ComponentBase $createShippingTransport->shipping_id = $orderq->shipping_id; $createShippingTransport->save(); - $html_data = ''; + // $html_data = ''; - $transportDatas = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->orderBy('id', 'DESC')->get(); + // $transportDatas = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->orderBy('id', 'DESC')->get(); - for ($x = 0; $x < count($transportDatas); $x++) { - // dd($orderDatas[$x]->shipping->status); - $html_data .= ' - ' . ($x + 1) . ' - ' . $transportDatas[$x]->transport_type . ' - ' . $transportDatas[$x]->place_now . ' - ' . $transportDatas[$x]->loaded_amount . ' kg - ' . $transportDatas[$x]->transport_no . ' - ' . Carbon::parse($transportDatas[$x]->date)->format('d.m.Y') ?? " " . ''; + // for ($x = 0; $x < count($transportDatas); $x++) { + // // dd($orderDatas[$x]->shipping->status); + // $html_data .= ' + // ' . ($x + 1) . ' + // ' . $transportDatas[$x]->transport_type . ' + // ' . $transportDatas[$x]->place_now . ' + // ' . $transportDatas[$x]->loaded_amount . ' kg + // ' . $transportDatas[$x]->transport_no . ' + // ' . Carbon::parse($transportDatas[$x]->date)->format('d.m.Y') ?? " " . ''; - $html_data .= '' . $transportDatas[$x]->note . ' - ' . $transportDatas[$x]->status . ''; + // $html_data .= '' . $transportDatas[$x]->note . ' + // ' . $transportDatas[$x]->status . ''; - if ($transportDatas[$x]->status == "complated") { - $html_data .= ' ' . $transportDatas[$x]->status . ' '; - } else { - $html_data .= ' Çalyşmak '; - } - $html_data .= ' Täzele - '; - } + // if ($transportDatas[$x]->status == "complated") { + // $html_data .= ' ' . $transportDatas[$x]->status . ' '; + // } else { + // $html_data .= ' Çalyşmak '; + // } + // $html_data .= ' Täzele + // '; + // } - $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->first(); + // $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) + // ->withCount(['order_items as order_all_amount' => function ($query) { + // $query->select(DB::raw('sum(amount)')); + // }]) + // ->withCount(['order_items as order_all_price' => function ($query) { + // $query->select(DB::raw('sum(price)')); + // }]) + // ->first(); - $all = ($order->order_all_amount * $order->order_all_price); + // $all = ($order->order_all_amount * $order->order_all_price); - $allLoaded = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->sum("loaded_amount"); + // $allLoaded = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->sum("loaded_amount"); if ($createShippingTransport) { Flash::success("Transport Ustunlikli Goşuldy"); - return [ - '#transport_item_datas' => $html_data, - '#allAmount' => $order->order_all_amount . " kg", - '#allPrice' => $order->order_all_price . " $", - '#all' => "Jemi Bahasy: " . number_format($all, 2) . " $", - '#all_loaded' => "Jemi Ýüklenen Ýük: " . number_format($allLoaded, 2) . " kg", - ]; + return Redirect::refresh(); + // return [ + // '#transport_item_datas' => $html_data, + // '#allAmount' => $order->order_all_amount . " kg", + // '#allPrice' => $order->order_all_price . " $", + // '#all' => "Jemi Bahasy: " . number_format($all, 2) . " $", + // '#all_loaded' => "Jemi Ýüklenen Ýük: " . number_format($allLoaded, 2) . " kg", + // ]; } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } // } } @@ -829,61 +839,63 @@ class Order extends ComponentBase $createShippingTransport->shipping_id = $orderq->shipping_id; $createShippingTransport->save(); - $html_data = ''; + // $html_data = ''; - $transportDatas = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->orderBy('id', 'DESC')->get(); + // $transportDatas = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->orderBy('id', 'DESC')->get(); - for ($x = 0; $x < count($transportDatas); $x++) { - // dd($orderDatas[$x]->shipping->status); - $html_data .= ' - ' . ($x + 1) . ' - ' . $transportDatas[$x]->transport_type . ' - ' . $transportDatas[$x]->place_now . ' - ' . $transportDatas[$x]->loaded_amount . ' kg - ' . $transportDatas[$x]->transport_no . ' - ' . Carbon::parse($transportDatas[$x]->date)->format('d.m.Y') ?? " " . ''; + // for ($x = 0; $x < count($transportDatas); $x++) { + // // dd($orderDatas[$x]->shipping->status); + // $html_data .= ' + // ' . ($x + 1) . ' + // ' . $transportDatas[$x]->transport_type . ' + // ' . $transportDatas[$x]->place_now . ' + // ' . $transportDatas[$x]->loaded_amount . ' kg + // ' . $transportDatas[$x]->transport_no . ' + // ' . Carbon::parse($transportDatas[$x]->date)->format('d.m.Y') ?? " " . ''; - $html_data .= '' . $transportDatas[$x]->note . ' - ' . $transportDatas[$x]->status . ''; + // $html_data .= '' . $transportDatas[$x]->note . ' + // ' . $transportDatas[$x]->status . ''; - if ($transportDatas[$x]->status == "complated") { - $html_data .= ' ' . $transportDatas[$x]->status . ' '; - } else { - $html_data .= ' Çalyşmak '; - } - $html_data .= ' Täzele - '; - } + // if ($transportDatas[$x]->status == "complated") { + // $html_data .= ' ' . $transportDatas[$x]->status . ' '; + // } else { + // $html_data .= ' Çalyşmak '; + // } + // $html_data .= ' Täzele + // '; + // } - $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->first(); + // $order = OrderModel::where("id", $orderId)->with(["client", "shipping"]) + // ->withCount(['order_items as order_all_amount' => function ($query) { + // $query->select(DB::raw('sum(amount)')); + // }]) + // ->withCount(['order_items as order_all_price' => function ($query) { + // $query->select(DB::raw('sum(price)')); + // }]) + // ->first(); - $all = ($order->order_all_amount * $order->order_all_price); + // $all = ($order->order_all_amount * $order->order_all_price); - $allLoaded = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->sum("loaded_amount"); + // $allLoaded = ShippingTransportModel::where("shipping_id", $orderq->shipping_id)->sum("loaded_amount"); if ($createShippingTransport) { Flash::success("Transport Ustunlikli Goşuldy"); - return [ - '#transport_item_datas' => $html_data, - '#allAmount' => $order->order_all_amount . " kg", - '#allPrice' => $order->order_all_price . " $", - '#all' => "Jemi Bahasy: " . number_format($all, 2) . " $", - '#all_loaded' => "Jemi Ýüklenen Ýük: " . number_format($allLoaded, 2) . " kg", - ]; + return Redirect::refresh(); + // return [ + // '#transport_item_datas' => $html_data, + // '#allAmount' => $order->order_all_amount . " kg", + // '#allPrice' => $order->order_all_price . " $", + // '#all' => "Jemi Bahasy: " . number_format($all, 2) . " $", + // '#all_loaded' => "Jemi Ýüklenen Ýük: " . number_format($allLoaded, 2) . " kg", + // ]; } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } // } } @@ -896,48 +908,50 @@ class Order extends ComponentBase $order = OrderModel::where("id", $data["orderId"])->delete(); - $html_data = ''; + // $html_data = ''; - $orderDatas = OrderModel::with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->orderBy('id', 'DESC') - ->get(); + // $orderDatas = OrderModel::with(["client", "shipping"]) + // ->withCount(['order_items as order_all_amount' => function ($query) { + // $query->select(DB::raw('sum(amount)')); + // }]) + // ->withCount(['order_items as order_all_price' => function ($query) { + // $query->select(DB::raw('sum(price)')); + // }]) + // ->orderBy('id', 'DESC') + // ->get(); - for ($x = 0; $x < count($orderDatas); $x++) { - $html_data .= ' - ' . ($x + 1) . ' - Sargyt #' . $orderDatas[$x]->id . ' - ' . $orderDatas[$x]->client->name . ' - ' . $orderDatas[$x]->client->country . ' - ' . number_format($orderDatas[$x]->order_all_amount) . ' kg - ' . number_format($orderDatas[$x]->order_all_price) . ' $ - - ' . number_format($orderDatas[$x]->all_payments) . ' $ - - ' . number_format($orderDatas[$x]->order_all_price - $orderDatas[$x]->all_payments) . ' $ - - ' . $orderDatas[$x]->created_at->format('d.m.Y') . ' - ' . $orderDatas[$x]->note . ' - POZ - '; - } + // for ($x = 0; $x < count($orderDatas); $x++) { + // $html_data .= ' + // ' . ($x + 1) . ' + // Sargyt #' . $orderDatas[$x]->id . ' + // ' . $orderDatas[$x]->client->name . ' + // ' . $orderDatas[$x]->client->country . ' + // ' . number_format($orderDatas[$x]->order_all_amount) . ' kg + // ' . number_format($orderDatas[$x]->order_all_price) . ' $ + // + // ' . number_format($orderDatas[$x]->all_payments) . ' $ + // + // ' . number_format($orderDatas[$x]->order_all_price - $orderDatas[$x]->all_payments) . ' $ + // + // ' . $orderDatas[$x]->created_at->format('d.m.Y') . ' + // ' . $orderDatas[$x]->note . ' + // POZ + // '; + // } if ($order) { Flash::success("Sargyt Ustunlikli Pozuldy"); - return [ - '#order_datas' => $html_data, - ]; + return Redirect::refresh(); + // return [ + // '#order_datas' => $html_data, + // ]; } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } } @@ -1017,51 +1031,53 @@ class Order extends ComponentBase - $html_data = ''; + // $html_data = ''; - $orderDatas = OrderItemModel::with(["type", "size"])->where("order_id", $orderItem->order_id)->orderBy('id', 'DESC')->get(); + // $orderDatas = OrderItemModel::with(["type", "size"])->where("order_id", $orderItem->order_id)->orderBy('id', 'DESC')->get(); - for ($x = 0; $x < count($orderDatas); $x++) { - // dd($orderDatas[$x]->shipping->status); - $html_data .= ' - ' . ($x + 1) . ' - ' . $orderDatas[$x]->type->name . ' - ' . $orderDatas[$x]->size->name . ' - ' . number_format($orderDatas[$x]->price) . ' $ - - ' . $orderDatas[$x]->amount . ' - ' . $orderDatas[$x]->note . ' - '; - } + // for ($x = 0; $x < count($orderDatas); $x++) { + // // dd($orderDatas[$x]->shipping->status); + // $html_data .= ' + // ' . ($x + 1) . ' + // ' . $orderDatas[$x]->type->name . ' + // ' . $orderDatas[$x]->size->name . ' + // ' . number_format($orderDatas[$x]->price) . ' $ + // + // ' . $orderDatas[$x]->amount . ' + // ' . $orderDatas[$x]->note . ' + // '; + // } - $order = OrderModel::where("id", $orderItem->order_id)->with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->first(); + // $order = OrderModel::where("id", $orderItem->order_id)->with(["client", "shipping"]) + // ->withCount(['order_items as order_all_amount' => function ($query) { + // $query->select(DB::raw('sum(amount)')); + // }]) + // ->withCount(['order_items as order_all_price' => function ($query) { + // $query->select(DB::raw('sum(price)')); + // }]) + // ->first(); - $all = ($order->order_all_amount * $order->order_all_price); + // $all = ($order->order_all_amount * $order->order_all_price); if ($orderItem) { Flash::success("Sargyt Harydy Ustunlikli Goşuldy"); - return [ - '#order_item_datas' => $html_data, - '#allAmount' => $order->order_all_amount . " kg", - '#allPrice' => $order->order_all_price . " $", - '#all' => "Jemi Bahasy: " . number_format($all) . " $", - ]; + return Redirect::refresh(); + // return [ + // '#order_item_datas' => $html_data, + // '#allAmount' => $order->order_all_amount . " kg", + // '#allPrice' => $order->order_all_price . " $", + // '#all' => "Jemi Bahasy: " . number_format($all) . " $", + // ]; } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } } @@ -1077,51 +1093,53 @@ class Order extends ComponentBase - $html_data = ''; + // $html_data = ''; - $orderDatas = OrderItemModel::with(["type", "size"])->where("order_id", $orderItem->order_id)->orderBy('id', 'DESC')->get(); + // $orderDatas = OrderItemModel::with(["type", "size"])->where("order_id", $orderItem->order_id)->orderBy('id', 'DESC')->get(); - for ($x = 0; $x < count($orderDatas); $x++) { - // dd($orderDatas[$x]->shipping->status); - $html_data .= ' - ' . ($x + 1) . ' - ' . $orderDatas[$x]->type->name . ' - ' . $orderDatas[$x]->size->name . ' - ' . number_format($orderDatas[$x]->price) . ' $ - - ' . $orderDatas[$x]->amount . ' - ' . $orderDatas[$x]->note . ' - '; - } + // for ($x = 0; $x < count($orderDatas); $x++) { + // // dd($orderDatas[$x]->shipping->status); + // $html_data .= ' + // ' . ($x + 1) . ' + // ' . $orderDatas[$x]->type->name . ' + // ' . $orderDatas[$x]->size->name . ' + // ' . number_format($orderDatas[$x]->price) . ' $ + // + // ' . $orderDatas[$x]->amount . ' + // ' . $orderDatas[$x]->note . ' + // '; + // } - $order = OrderModel::where("id", $orderItem->order_id)->with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->first(); + // $order = OrderModel::where("id", $orderItem->order_id)->with(["client", "shipping"]) + // ->withCount(['order_items as order_all_amount' => function ($query) { + // $query->select(DB::raw('sum(amount)')); + // }]) + // ->withCount(['order_items as order_all_price' => function ($query) { + // $query->select(DB::raw('sum(price)')); + // }]) + // ->first(); - $all = ($order->order_all_amount * $order->order_all_price); + // $all = ($order->order_all_amount * $order->order_all_price); if ($orderItem) { Flash::success("Sargyt Harydy Ustunlikli Goşuldy"); - return [ - '#order_item_datas' => $html_data, - '#allAmount' => $order->order_all_amount . " kg", - '#allPrice' => $order->order_all_price . " $", - '#all' => "Jemi Bahasy: " . number_format($all) . " $", - ]; + return Redirect::refresh(); + // return [ + // '#order_item_datas' => $html_data, + // '#allAmount' => $order->order_all_amount . " kg", + // '#allPrice' => $order->order_all_price . " $", + // '#all' => "Jemi Bahasy: " . number_format($all) . " $", + // ]; } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } } @@ -1153,6 +1171,7 @@ class Order extends ComponentBase public function onCreateOrder() { + $user = \Auth::user(); $currentDate = Carbon::now()->timezone('UTC +05:00'); $currentDateFormat = $currentDate->format('Y-m-d'); @@ -1166,6 +1185,7 @@ class Order extends ComponentBase $createOrder->client_id = $data["client_id"]; $createOrder->note = $data["note"]; $createOrder->shipping_id = 0; + $createOrder->user_id = $user->id; $createOrder->save(); $createShipping = new ShippingModel(); @@ -1181,48 +1201,13 @@ class Order extends ComponentBase $createOrder->shipping_id = $createShipping->id; $createOrder->save(); - $html_data = ''; - - $orderDatas = OrderModel::with(["client", "shipping"]) - ->withCount(['order_items as order_all_amount' => function ($query) { - $query->select(DB::raw('sum(amount)')); - }]) - ->withCount(['order_items as order_all_price' => function ($query) { - $query->select(DB::raw('sum(price)')); - }]) - ->orderBy('id', 'DESC') - ->get(); - - - for ($x = 0; $x < count($orderDatas); $x++) { - $html_data .= ' - ' . ($x + 1) . ' - Sargyt #' . $orderDatas[$x]->id . ' - ' . $orderDatas[$x]->client->name . ' - ' . $orderDatas[$x]->client->country . ' - ' . number_format($orderDatas[$x]->order_all_amount) . ' kg - ' . number_format($orderDatas[$x]->order_all_price) . ' $ - - ' . number_format($orderDatas[$x]->all_payments) . ' $ - - ' . number_format($orderDatas[$x]->order_all_price - $orderDatas[$x]->all_payments) . ' $ - - ' . $orderDatas[$x]->created_at->format('d.m.Y') . ' - ' . $orderDatas[$x]->note . ' - POZ - '; - } - if ($createOrder) { Flash::success("Sargyt Ustunlikli Goşuldy"); - return [ - '#order_datas' => $html_data, - ]; + return Redirect::refresh(); } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } } } diff --git a/plugins/romanah/gokbakja/components/OrderItem.php b/plugins/romanah/gokbakja/components/OrderItem.php index 233eac2..82db5d7 100644 --- a/plugins/romanah/gokbakja/components/OrderItem.php +++ b/plugins/romanah/gokbakja/components/OrderItem.php @@ -28,6 +28,8 @@ class OrderItem extends ComponentBase public function onRender() { + $user = \Auth::user(); + $orderId = $this->param("orderId"); $html_data = ''; @@ -98,7 +100,31 @@ class OrderItem extends ComponentBase } } + $editBtnPrice = ''; + $editAmount = ''; + $editDelete = ''; + if($orderItems[$x]->user_id != $user->id){ + $editBtnPrice = ''.number_format($orderItems[$x]->price).' $'; + + $editAmount = ''; + + $editDelete = 'Size degişli däl'; + }else{ + $editBtnPrice = ''.number_format($orderItems[$x]->price).' $'; + + $editAmount = ''; + + $editDelete = 'POZ'; + } $html_data .= ' '.($x+1).', #'.$orderItems[$x]->id.' @@ -118,16 +144,11 @@ class OrderItem extends ComponentBase $html_data .= 'Ini: '.$orderItems[$x]->bag_width.' Boýy: '.$orderItems[$x]->bag_height; } $html_data .=' - '.number_format($orderItems[$x]->price).' $ + + '.$editBtnPrice.' - '.$orderItems[$x]->amount; + '.$editAmount.' '.$orderItems[$x]->amount; if ($orderItems[$x]->order_item_type == 'rulon'){ $html_data .=' kg'; @@ -151,7 +172,7 @@ class OrderItem extends ComponentBase $html_data .= ' '.$notCompleteCalc.' '.$orderItems[$x]->note.' - POZ + '.$editDelete.' '; } @@ -237,7 +258,8 @@ class OrderItem extends ComponentBase Flash::success("Sargyt Harydy Ustunlikli Uytgedildi"); return Redirect::refresh(); } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } } @@ -258,12 +280,14 @@ class OrderItem extends ComponentBase Flash::success("Sargyt Harydy Ustunlikli Uytgedildi"); return Redirect::refresh(); } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } } public function onCreateOrderItem() { + $user = \Auth::user(); $data = post(); $orderId = $this->param("orderId"); @@ -280,6 +304,7 @@ class OrderItem extends ComponentBase $createOrderItem->bag_height = $data["bag_height"]; $createOrderItem->price = $data["price"]; $createOrderItem->note = $data["note"]; + $createOrderItem->user_id = $user->id; $createOrderItem->save(); if ($createOrderItem) { @@ -300,7 +325,8 @@ class OrderItem extends ComponentBase Flash::success("Sargyt Ustunlikli Pozuldy"); return Redirect::refresh(); } else { - return Flash::error("Yalnyshlyk bar!!"); + Flash::error("Yalnyshlyk bar!!"); + return Redirect::refresh(); } } diff --git a/plugins/romanah/gokbakja/components/Sewer.php b/plugins/romanah/gokbakja/components/Sewer.php index 56a7557..a2adfb2 100644 --- a/plugins/romanah/gokbakja/components/Sewer.php +++ b/plugins/romanah/gokbakja/components/Sewer.php @@ -187,12 +187,46 @@ class Sewer extends ComponentBase public function onRender() { - + $user = \Auth::user(); $html_data = ''; $sewerDatas = SewerModel::with(["employee", "machin_production.bag_size", "machin_production.bag_type", "machin_production.color", "pivot_sewer"])->orderBy('id', 'DESC')->get(); // dd($sewerDatas); for ($x = 0; $x < count($sewerDatas); $x++) { + + $editBtn = ''; + + if($sewerDatas[$x]->user_id != $user->id){ + $editBtn = ''; + }else{ + $editBtn = ''; + } + $statusTitle = ''; if ($sewerDatas[$x]->pivot_sewer[0]->status == "working") { $statusTitle = "Işlenilýär"; @@ -224,24 +258,7 @@ class Sewer extends ComponentBase style="font-size: 14px;">' . $statusTitle . ' ' . $sewerDatas[$x]->note . ' - + '. $editBtn .' '; } @@ -393,6 +410,7 @@ class Sewer extends ComponentBase public function onCreateSewerProduction() { + $user = \Auth::user(); $currentDate = Carbon::now()->timezone('UTC +05:00'); $currentDateFormat = $currentDate->format('Y-m-d'); @@ -420,6 +438,7 @@ class Sewer extends ComponentBase $createSewer = new SewerModel(); $createSewer->employee_id = $data["employee_id"]; $createSewer->produced_bag_qty = 0; + $createSewer->user_id = $user->id; $createSewer->note = $data["note"] ?? ''; $createSewer->machine_production_id = $data["machine_production_id"]; $createSewer->width = (float) $data["width"]; diff --git a/plugins/romanah/gokbakja/models/Order.php b/plugins/romanah/gokbakja/models/Order.php index 4c7d844..e67aa03 100644 --- a/plugins/romanah/gokbakja/models/Order.php +++ b/plugins/romanah/gokbakja/models/Order.php @@ -1,5 +1,6 @@ [ 'Romanah\Gokbakja\Models\OrderItem', @@ -35,6 +38,7 @@ class Order extends Model 'key' => 'order_id', 'softDelete' => true ], + ]; /** diff --git a/plugins/romanah/gokbakja/models/Shipping.php b/plugins/romanah/gokbakja/models/Shipping.php index 8140fd9..da756f6 100644 --- a/plugins/romanah/gokbakja/models/Shipping.php +++ b/plugins/romanah/gokbakja/models/Shipping.php @@ -29,7 +29,8 @@ class Shipping extends Model public $hasMany = [ 'shipping_transport' => [ 'Romanah\Gokbakja\Models\ShippingTransport', - 'key' => 'shipping_id' + 'key' => 'shipping_id', + 'softDelete' => true ] ]; /** diff --git a/themes/gokbakja/pages/home.htm b/themes/gokbakja/pages/home.htm index 33f9886..322ca81 100644 --- a/themes/gokbakja/pages/home.htm +++ b/themes/gokbakja/pages/home.htm @@ -19,19 +19,63 @@ function onStart(){ - $this["production1"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 1)->whereYear('created_at', $year)->avg('all_amount') ?? 0; + $this["production1"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 1)->whereYear('date', $year)->count() ?? 0; - $this["production2"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 2)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production3"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 3)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production4"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 4)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production5"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 5)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production6"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 6)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production7"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 7)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production8"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 8)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production9"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 9)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production10"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 10)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production11"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 11)->whereYear('created_at', $year)->avg('all_amount') ?? 0; - $this["production12"] = Romanah\Gokbakja\Models\Production::whereMonth('created_at', 12)->whereYear('created_at', $year)->avg('all_amount') ?? 0; + $this["production2"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 2)->whereYear('date', $year)->count() ?? 0; + $this["production3"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 3)->whereYear('date', $year)->count() ?? 0; + $this["production4"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 4)->whereYear('date', $year)->count() ?? 0; + $this["production5"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 5)->whereYear('date', $year)->count() ?? 0; + $this["production6"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 6)->whereYear('date', $year)->count() ?? 0; + $this["production7"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 7)->whereYear('date', $year)->count() ?? 0; + $this["production8"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 8)->whereYear('date', $year)->count() ?? 0; + $this["production9"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 9)->whereYear('date', $year)->count() ?? 0; + $this["production10"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 10)->whereYear('date', $year)->count() ?? 0; + $this["production11"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 11)->whereYear('date', $year)->count() ?? 0; + $this["production12"] = Romanah\Gokbakja\Models\SewerProduction::whereMonth('date', 12)->whereYear('date', $year)->count() ?? 0; + + + $this["order1"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 1)->whereYear('created_at', $year)->count() ?? 0; + $this["order2"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 2)->whereYear('created_at', $year)->count() ?? 0; + $this["order3"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 3)->whereYear('created_at', $year)->count() ?? 0; + $this["order4"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 4)->whereYear('created_at', $year)->count() ?? 0; + $this["order5"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 5)->whereYear('created_at', $year)->count() ?? 0; + $this["order6"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 6)->whereYear('created_at', $year)->count() ?? 0; + $this["order7"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 7)->whereYear('created_at', $year)->count() ?? 0; + $this["order8"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 8)->whereYear('created_at', $year)->count() ?? 0; + $this["order9"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 9)->whereYear('created_at', $year)->count() ?? 0; + $this["order10"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 10)->whereYear('created_at', $year)->count() ?? 0; + $this["order11"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 11)->whereYear('created_at', $year)->count() ?? 0; + $this["order12"] = Romanah\Gokbakja\Models\Order::whereMonth('created_at', 12)->whereYear('created_at', $year)->count() ?? 0; + + + $this["rulonProduction1"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 1)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction2"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 2)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction3"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 3)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction4"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 4)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction5"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 5)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction6"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 6)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction7"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 7)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction8"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 8)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction9"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 9)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction10"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 10)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction11"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 11)->whereYear('date', $year)->count() ?? 0; + $this["rulonProduction12"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 12)->whereYear('date', $year)->count() ?? 0; + + + + + $this["mechanicRulonProduction1"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 1)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction2"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 2)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction3"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 3)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction4"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 4)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction5"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 5)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction6"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 6)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction7"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 7)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction8"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 8)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction9"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 9)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction10"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 10)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction11"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 11)->whereYear('date', $year)->sum('produced_weight') ?? 0; + $this["mechanicRulonProduction12"] = Romanah\Gokbakja\Models\ProductionMachine::whereMonth('date', 12)->whereYear('date', $year)->sum('produced_weight') ?? 0; @@ -59,12 +103,45 @@ function onStart(){
    +
    +
    +
    +

    Tikilen Halta hasabaty {{year}} ýyl

    + +
    +
    +
    + +
    +
    +
    +
    +

    Sargytlar hasabaty {{year}} ýyl

    + +
    +
    +
    + +
    +
    -

    Statistika hasabat {{year}} ýyl

    +

    Rulon hasabaty {{year}} ýyl

    -
    +
    +
    +
    + +
    + + +
    +
    +
    +

    Rulon hasabaty (kg) {{year}} ýyl

    + +
    @@ -90,7 +167,7 @@ function onStart(){ show: !1 } }, - colors: ["#6fbe36", "#ff5d5d"], + colors: ["#00aba7"], dataLabels: { enabled: !1 }, @@ -99,14 +176,162 @@ function onStart(){ curve: "straight" }, series: [{ - name: "Ortaça Öndürilen Önüm ", - data: ['{{production1}}', '{{production2}}', '{{production3}}', '{{production4}}', '{{production5}}', '{{production6}}', '{{production7}}', '{{production8}}', '{{production9}}', '{{production10}}', '{{production11}}', '{{production12}}'] - }, { - name: "Brak Önüm ", - data: ['{{production1}}', '{{production2}}', '{{production3}}', '{{production4}}', '{{production5}}', '{{production6}}', '{{production7}}', '{{production8}}', '12312', '{{production10}}', '{{production11}}', '{{production12}}'] + name: "Rulon Kg ", + data: ['{{mechanicRulonProduction1}}', '{{mechanicRulonProduction2}}', '{{mechanicRulonProduction3}}', '{{mechanicRulonProduction4}}', '{{mechanicRulonProduction5}}', '{{mechanicRulonProduction6}}', '{{mechanicRulonProduction7}}', '{{mechanicRulonProduction8}}', '{{mechanicRulonProduction9}}', '{{mechanicRulonProduction10}}', '{{mechanicRulonProduction11}}', '{{mechanicRulonProduction12}}'] }], title: { - text: "Hasabat", + text: "Rulon kg Hasabat", + align: "left" + }, + grid: { + row: { + colors: ["transparent", "transparent"], + opacity: .2 + }, + borderColor: "#f1f1f1" + }, + markers: { + style: "inverted", + size: 6 + }, + xaxis: { + categories: ['Ýanwar', 'Fewral', 'Mart', 'Aprel', 'Maý', 'Iýun', 'Iýul', 'Awgust', 'Sentýabr', 'Oktýabr', 'Noýabr', 'Dekabr'], + title: { + text: "Aýlar" + } + }, + yaxis: { + title: { + text: "" + } + }, + legend: { + position: "top", + horizontalAlign: "right", + floating: !0, + offsetY: -25, + offsetX: -5 + }, + responsive: [{ + breakpoint: 600, + options: { + chart: { + toolbar: { + show: !1 + } + }, + legend: { + show: !1 + } + } + }] + }, + chart = new ApexCharts(document.querySelector("#line_chart_datalabel4"), options); + chart.render(); + + + + + + -{% endput %} \ No newline at end of file + + + +{% endput %} diff --git a/themes/gokbakja/pages/orders/order-detail.htm b/themes/gokbakja/pages/orders/order-detail.htm index dfbc872..9a792f8 100644 --- a/themes/gokbakja/pages/orders/order-detail.htm +++ b/themes/gokbakja/pages/orders/order-detail.htm @@ -23,9 +23,9 @@ function onStart(){ }]) ->first(); - + if($count > 0){ $this["all"] = ($this["order"]->order_all_amount * $this["order"]->order_all_price) / $count; - + } $this["orderItems"] = Romanah\Gokbakja\Models\OrderItem::where("order_id", $orderId)->orderBy("id", "DESC")->with(["type", "size", "color"])->get(); $this["sizes"] = Romanah\Gokbakja\Models\BagSize::get(); @@ -185,9 +185,11 @@ function onStart(){

    Hasabat

    + {% if order.user_id == user.id %} + {% endif %}
    diff --git a/themes/gokbakja/pages/orders/order-detaillogistics.htm b/themes/gokbakja/pages/orders/order-detaillogistics.htm index 217429b..117dcad 100644 --- a/themes/gokbakja/pages/orders/order-detaillogistics.htm +++ b/themes/gokbakja/pages/orders/order-detaillogistics.htm @@ -25,7 +25,7 @@ function onStart(){ $this["all"] = ($this["order"]->order_all_amount * $this["order"]->order_all_price); - $this["transports"] = Romanah\Gokbakja\Models\ShippingTransport::where("shipping_id", $this["order"]->shipping_id)->orderBy("id", "DESC")->get(); + $this["transports"] = Romanah\Gokbakja\Models\ShippingTransport::where("shipping_id", $this["order"]->shipping_id)->with('shipping.order')->orderBy("id", "DESC")->get(); $this["allLoaded"] = Romanah\Gokbakja\Models\ShippingTransport::where("shipping_id", $this["order"]->shipping_id)->sum("loaded_amount"); @@ -92,11 +92,13 @@ function onStart(){ {% for item in orderItems %} {% endfor %} @@ -154,9 +156,11 @@ function onStart(){

    Jemi Ýüklenen Ýük: {{allLoaded|number_format(2)}} kg

    + {% if order.user_id == user.id %} + {% endif %}
    @@ -192,14 +196,26 @@ function onStart(){ {% if transport.status == "complated" %} {{transport.status}} {% else %} + + {% if transport.shipping.order.user_id == user.id %} Çalyşmak + {% else %} + Size degişli däl + {% endif %} + {% endif %} - + {% if transport.shipping.order.user_id == user.id %} + Täzele + data-bs-toggle="modal" data-bs-target=".bs-example-modal-sm-1" style="color: orange;"> Täzele + {% else %} + Size degişli däl + {% endif %} + {% endfor %} diff --git a/themes/gokbakja/pages/orders/order-detailpayment.htm b/themes/gokbakja/pages/orders/order-detailpayment.htm index 60fae3e..e7ea989 100644 --- a/themes/gokbakja/pages/orders/order-detailpayment.htm +++ b/themes/gokbakja/pages/orders/order-detailpayment.htm @@ -23,7 +23,7 @@ function onStart(){ $this["all"] = ($this["order"]->order_all_amount * $this["order"]->order_all_price); - $this["payments"] = Romanah\Gokbakja\Models\Payment::where("order_id", $orderId)->orderBy("id", "DESC")->get(); + $this["payments"] = Romanah\Gokbakja\Models\Payment::where("order_id", $orderId)->with('order')->orderBy("id", "DESC")->get(); $this["allPaid"] = Romanah\Gokbakja\Models\Payment::where("order_id", $orderId)->sum("amount"); $this["allNot"] = ($this["all"] - $this["allPaid"]); @@ -104,8 +104,10 @@ function onStart(){
    + +
    @@ -131,9 +133,11 @@ function onStart(){

    Bergisi: {{allNot|number_format}} $

    + {% if order.user_id == user.id %} + {% endif %}
    @@ -156,9 +160,14 @@ function onStart(){ {{payment.amount}} $ {{payment.date != null ? payment.date|date('d.m.Y') : ""}} {{payment.note}} - + {% if payment.order.user_id == user.id %} + Täzele + {% else %} + Size degişli däl + {% endif %} {% endfor %} diff --git a/themes/gokbakja/pages/product/actions.htm b/themes/gokbakja/pages/product/actions.htm index 77983f4..ab08a49 100644 --- a/themes/gokbakja/pages/product/actions.htm +++ b/themes/gokbakja/pages/product/actions.htm @@ -126,8 +126,19 @@ function onStart(){ {{record.note}}
    + {% if record.user_id != user.id %} + + + {% else %} + + + + {% endif %}
    diff --git a/themes/gokbakja/pages/production/production.htm b/themes/gokbakja/pages/production/production.htm index 3033bd9..a8daeb6 100644 --- a/themes/gokbakja/pages/production/production.htm +++ b/themes/gokbakja/pages/production/production.htm @@ -206,11 +206,20 @@ function onStart(){ {% for product in report.pivot_production %} - {{product.amount_percentage|number_format(2)}} - % + + {% if report.user_id == user.id %} + {{product.amount_percentage|number_format(2)}} % + {% else %} + + {{product.amount_percentage|number_format(2)}} % + + {% endif %} + + + {% if report.user_id == user.id %} + {% endif %} + {% endfor %} {{report.note}} - POZ + + {% if report.user_id == user.id %} + POZ + {% else %} + + Size degişli däl + + {% endif %} + +