From b2b84f3a6f3c98005f80c6c7c558a33b4ea36193 Mon Sep 17 00:00:00 2001 From: Susinthiran Sithamparanathan Date: Wed, 17 Oct 2012 12:25:34 +0200 Subject: [PATCH 01/15] Update Sabre to version 1.7.1 --- 3rdparty/Sabre.includes.php | 26 - 3rdparty/Sabre/CalDAV/Backend/Abstract.php | 182 +++--- .../Sabre/CalDAV/Backend/BackendInterface.php | 231 +++++++ .../CalDAV/Backend/NotificationSupport.php | 47 ++ 3rdparty/Sabre/CalDAV/Backend/PDO.php | 337 +++++++++- .../Sabre/CalDAV/Backend/SharingSupport.php | 238 +++++++ 3rdparty/Sabre/CalDAV/Calendar.php | 63 +- 3rdparty/Sabre/CalDAV/CalendarObject.php | 22 +- 3rdparty/Sabre/CalDAV/CalendarQueryParser.php | 12 +- .../Sabre/CalDAV/CalendarQueryValidator.php | 28 +- 3rdparty/Sabre/CalDAV/CalendarRootNode.php | 13 +- .../CalDAV/Exception/InvalidComponentType.php | 32 + 3rdparty/Sabre/CalDAV/ICSExportPlugin.php | 6 +- 3rdparty/Sabre/CalDAV/ICalendar.php | 21 +- 3rdparty/Sabre/CalDAV/ICalendarObject.php | 0 3rdparty/Sabre/CalDAV/IShareableCalendar.php | 48 ++ 3rdparty/Sabre/CalDAV/ISharedCalendar.php | 22 + .../Sabre/CalDAV/Notifications/Collection.php | 169 +++++ .../CalDAV/Notifications/ICollection.php | 22 + 3rdparty/Sabre/CalDAV/Notifications/INode.php | 38 ++ .../Notifications/INotificationType.php | 43 ++ 3rdparty/Sabre/CalDAV/Notifications/Node.php | 188 ++++++ .../Notifications/Notification/Invite.php | 276 ++++++++ .../Notification/InviteReply.php | 216 +++++++ .../Notification/SystemStatus.php | 179 ++++++ 3rdparty/Sabre/CalDAV/Plugin.php | 587 +++++++++++++++--- .../Sabre/CalDAV/Principal/Collection.php | 0 3rdparty/Sabre/CalDAV/Principal/ProxyRead.php | 0 .../Sabre/CalDAV/Principal/ProxyWrite.php | 0 3rdparty/Sabre/CalDAV/Principal/User.php | 0 .../CalDAV/Property/AllowedSharingModes.php | 72 +++ 3rdparty/Sabre/CalDAV/Property/Invite.php | 173 ++++++ .../Property/ScheduleCalendarTransp.php | 99 +++ .../SupportedCalendarComponentSet.php | 0 .../CalDAV/Property/SupportedCalendarData.php | 0 .../CalDAV/Property/SupportedCollationSet.php | 0 3rdparty/Sabre/CalDAV/Schedule/IMip.php | 18 +- 3rdparty/Sabre/CalDAV/Schedule/IOutbox.php | 0 3rdparty/Sabre/CalDAV/Schedule/Outbox.php | 10 +- 3rdparty/Sabre/CalDAV/Server.php | 68 -- 3rdparty/Sabre/CalDAV/ShareableCalendar.php | 72 +++ 3rdparty/Sabre/CalDAV/SharedCalendar.php | 98 +++ 3rdparty/Sabre/CalDAV/SharingPlugin.php | 475 ++++++++++++++ 3rdparty/Sabre/CalDAV/UserCalendars.php | 64 +- 3rdparty/Sabre/CalDAV/Version.php | 2 +- 3rdparty/Sabre/CalDAV/includes.php | 25 +- 3rdparty/Sabre/CardDAV/AddressBook.php | 4 +- .../Sabre/CardDAV/AddressBookQueryParser.php | 0 3rdparty/Sabre/CardDAV/AddressBookRoot.php | 0 3rdparty/Sabre/CardDAV/Backend/Abstract.php | 0 3rdparty/Sabre/CardDAV/Backend/PDO.php | 0 3rdparty/Sabre/CardDAV/Card.php | 16 +- 3rdparty/Sabre/CardDAV/IAddressBook.php | 0 3rdparty/Sabre/CardDAV/ICard.php | 0 3rdparty/Sabre/CardDAV/IDirectory.php | 0 3rdparty/Sabre/CardDAV/Plugin.php | 55 +- .../CardDAV/Property/SupportedAddressData.php | 0 3rdparty/Sabre/CardDAV/UserAddressBooks.php | 0 3rdparty/Sabre/CardDAV/VCFExportPlugin.php | 107 ++++ 3rdparty/Sabre/CardDAV/Version.php | 2 +- 3rdparty/Sabre/CardDAV/includes.php | 1 + .../Sabre/DAV/Auth/Backend/AbstractBasic.php | 0 .../Sabre/DAV/Auth/Backend/AbstractDigest.php | 0 3rdparty/Sabre/DAV/Auth/Backend/Apache.php | 0 3rdparty/Sabre/DAV/Auth/Backend/File.php | 0 3rdparty/Sabre/DAV/Auth/Backend/PDO.php | 0 3rdparty/Sabre/DAV/Auth/IBackend.php | 0 3rdparty/Sabre/DAV/Auth/Plugin.php | 0 .../Sabre/DAV/Browser/GuessContentType.php | 0 .../Sabre/DAV/Browser/MapGetToPropFind.php | 0 3rdparty/Sabre/DAV/Browser/Plugin.php | 2 +- 3rdparty/Sabre/DAV/Browser/assets/favicon.ico | Bin .../DAV/Browser/assets/icons/addressbook.png | Bin .../DAV/Browser/assets/icons/calendar.png | Bin .../Sabre/DAV/Browser/assets/icons/card.png | Bin .../DAV/Browser/assets/icons/collection.png | Bin .../Sabre/DAV/Browser/assets/icons/file.png | Bin .../Sabre/DAV/Browser/assets/icons/parent.png | Bin .../DAV/Browser/assets/icons/principal.png | Bin 3rdparty/Sabre/DAV/Client.php | 69 +- 3rdparty/Sabre/DAV/Collection.php | 6 +- 3rdparty/Sabre/DAV/Directory.php | 17 - 3rdparty/Sabre/DAV/Exception.php | 0 3rdparty/Sabre/DAV/Exception/BadRequest.php | 0 3rdparty/Sabre/DAV/Exception/Conflict.php | 0 .../Sabre/DAV/Exception/ConflictingLock.php | 0 3rdparty/Sabre/DAV/Exception/FileNotFound.php | 0 3rdparty/Sabre/DAV/Exception/Forbidden.php | 0 .../DAV/Exception/InsufficientStorage.php | 0 .../DAV/Exception/InvalidResourceType.php | 0 .../Exception/LockTokenMatchesRequestUri.php | 0 3rdparty/Sabre/DAV/Exception/Locked.php | 0 .../Sabre/DAV/Exception/MethodNotAllowed.php | 0 .../Sabre/DAV/Exception/NotAuthenticated.php | 0 3rdparty/Sabre/DAV/Exception/NotFound.php | 0 .../Sabre/DAV/Exception/NotImplemented.php | 0 .../Sabre/DAV/Exception/PaymentRequired.php | 0 .../DAV/Exception/PreconditionFailed.php | 0 ...Implemented.php => ReportNotSupported.php} | 4 +- .../RequestedRangeNotSatisfiable.php | 0 .../DAV/Exception/UnsupportedMediaType.php | 0 3rdparty/Sabre/DAV/FS/Directory.php | 5 +- 3rdparty/Sabre/DAV/FS/File.php | 0 3rdparty/Sabre/DAV/FS/Node.php | 0 3rdparty/Sabre/DAV/FSExt/Directory.php | 5 +- 3rdparty/Sabre/DAV/FSExt/File.php | 32 +- 3rdparty/Sabre/DAV/FSExt/Node.php | 0 3rdparty/Sabre/DAV/File.php | 0 3rdparty/Sabre/DAV/ICollection.php | 5 +- 3rdparty/Sabre/DAV/IExtendedCollection.php | 0 3rdparty/Sabre/DAV/IFile.php | 2 +- 3rdparty/Sabre/DAV/INode.php | 0 3rdparty/Sabre/DAV/IProperties.php | 0 3rdparty/Sabre/DAV/IQuota.php | 0 3rdparty/Sabre/DAV/Locks/Backend/Abstract.php | 0 3rdparty/Sabre/DAV/Locks/Backend/FS.php | 0 3rdparty/Sabre/DAV/Locks/Backend/File.php | 0 3rdparty/Sabre/DAV/Locks/Backend/PDO.php | 0 3rdparty/Sabre/DAV/Locks/LockInfo.php | 0 3rdparty/Sabre/DAV/Locks/Plugin.php | 1 + 3rdparty/Sabre/DAV/Mount/Plugin.php | 0 3rdparty/Sabre/DAV/Node.php | 2 +- 3rdparty/Sabre/DAV/ObjectTree.php | 26 +- 3rdparty/Sabre/DAV/PartialUpdate/IFile.php | 38 ++ 3rdparty/Sabre/DAV/PartialUpdate/Plugin.php | 209 +++++++ 3rdparty/Sabre/DAV/Property.php | 12 +- .../Sabre/DAV/Property/GetLastModified.php | 0 3rdparty/Sabre/DAV/Property/Href.php | 2 +- 3rdparty/Sabre/DAV/Property/HrefList.php | 2 +- 3rdparty/Sabre/DAV/Property/IHref.php | 0 3rdparty/Sabre/DAV/Property/LockDiscovery.php | 0 3rdparty/Sabre/DAV/Property/ResourceType.php | 0 3rdparty/Sabre/DAV/Property/Response.php | 2 +- 3rdparty/Sabre/DAV/Property/ResponseList.php | 0 3rdparty/Sabre/DAV/Property/SupportedLock.php | 0 .../Sabre/DAV/Property/SupportedReportSet.php | 0 3rdparty/Sabre/DAV/PropertyInterface.php | 21 + 3rdparty/Sabre/DAV/Server.php | 175 +++++- 3rdparty/Sabre/DAV/ServerPlugin.php | 2 +- 3rdparty/Sabre/DAV/SimpleCollection.php | 5 +- 3rdparty/Sabre/DAV/SimpleDirectory.php | 21 - 3rdparty/Sabre/DAV/SimpleFile.php | 0 3rdparty/Sabre/DAV/StringUtil.php | 0 .../Sabre/DAV/TemporaryFileFilterPlugin.php | 0 3rdparty/Sabre/DAV/Tree.php | 0 3rdparty/Sabre/DAV/Tree/Filesystem.php | 0 3rdparty/Sabre/DAV/URLUtil.php | 0 3rdparty/Sabre/DAV/UUIDUtil.php | 0 3rdparty/Sabre/DAV/Version.php | 2 +- 3rdparty/Sabre/DAV/XMLUtil.php | 29 +- 3rdparty/Sabre/DAV/includes.php | 27 +- .../DAVACL/AbstractPrincipalCollection.php | 2 +- .../Sabre/DAVACL/Exception/AceConflict.php | 0 .../Sabre/DAVACL/Exception/NeedPrivileges.php | 0 .../Sabre/DAVACL/Exception/NoAbstract.php | 0 .../Exception/NotRecognizedPrincipal.php | 0 .../Exception/NotSupportedPrivilege.php | 0 3rdparty/Sabre/DAVACL/IACL.php | 2 +- 3rdparty/Sabre/DAVACL/IPrincipal.php | 0 3rdparty/Sabre/DAVACL/IPrincipalBackend.php | 0 3rdparty/Sabre/DAVACL/Plugin.php | 54 +- 3rdparty/Sabre/DAVACL/Principal.php | 0 .../Sabre/DAVACL/PrincipalBackend/PDO.php | 0 3rdparty/Sabre/DAVACL/PrincipalCollection.php | 0 3rdparty/Sabre/DAVACL/Property/Acl.php | 10 +- .../Sabre/DAVACL/Property/AclRestrictions.php | 0 .../Property/CurrentUserPrivilegeSet.php | 0 3rdparty/Sabre/DAVACL/Property/Principal.php | 4 +- .../DAVACL/Property/SupportedPrivilegeSet.php | 0 3rdparty/Sabre/DAVACL/Version.php | 2 +- 3rdparty/Sabre/DAVACL/includes.php | 0 3rdparty/Sabre/HTTP/AWSAuth.php | 0 3rdparty/Sabre/HTTP/AbstractAuth.php | 0 3rdparty/Sabre/HTTP/BasicAuth.php | 0 3rdparty/Sabre/HTTP/DigestAuth.php | 0 3rdparty/Sabre/HTTP/Request.php | 2 +- 3rdparty/Sabre/HTTP/Response.php | 7 +- 3rdparty/Sabre/HTTP/Util.php | 0 3rdparty/Sabre/HTTP/Version.php | 2 +- 3rdparty/Sabre/HTTP/includes.php | 0 3rdparty/Sabre/VObject/Component.php | 136 ++-- 3rdparty/Sabre/VObject/Component/VAlarm.php | 34 +- .../Sabre/VObject/Component/VCalendar.php | 129 +++- 3rdparty/Sabre/VObject/Component/VCard.php | 105 ++++ 3rdparty/Sabre/VObject/Component/VEvent.php | 21 +- .../Sabre/VObject/Component/VFreeBusy.php | 68 ++ 3rdparty/Sabre/VObject/Component/VJournal.php | 14 +- 3rdparty/Sabre/VObject/Component/VTodo.php | 14 +- 3rdparty/Sabre/VObject/DateTimeParser.php | 28 +- 3rdparty/Sabre/VObject/Element.php | 16 - 3rdparty/Sabre/VObject/Element/DateTime.php | 37 -- .../Sabre/VObject/Element/MultiDateTime.php | 17 - 3rdparty/Sabre/VObject/ElementList.php | 12 +- 3rdparty/Sabre/VObject/FreeBusyGenerator.php | 97 +-- 3rdparty/Sabre/VObject/Node.php | 60 +- 3rdparty/Sabre/VObject/Parameter.php | 10 +- 3rdparty/Sabre/VObject/ParseException.php | 8 +- 3rdparty/Sabre/VObject/Property.php | 172 +++-- 3rdparty/Sabre/VObject/Property/Compound.php | 129 ++++ 3rdparty/Sabre/VObject/Property/DateTime.php | 91 +-- .../Sabre/VObject/Property/MultiDateTime.php | 32 +- 3rdparty/Sabre/VObject/Reader.php | 78 ++- 3rdparty/Sabre/VObject/RecurrenceIterator.php | 58 +- 3rdparty/Sabre/VObject/Splitter/ICalendar.php | 111 ++++ .../VObject/Splitter/SplitterInterface.php | 39 ++ 3rdparty/Sabre/VObject/Splitter/VCard.php | 76 +++ 3rdparty/Sabre/VObject/StringUtil.php | 61 ++ 3rdparty/Sabre/VObject/TimeZoneUtil.php | 351 +++++++++++ 3rdparty/Sabre/VObject/Version.php | 8 +- 3rdparty/Sabre/VObject/WindowsTimezoneMap.php | 128 ---- 3rdparty/Sabre/VObject/includes.php | 23 +- 3rdparty/Sabre/autoload.php | 28 +- apps/files/templates/index.php_orig | 90 +++ apps/files/templates/index.php_test | 90 +++ lib/base.php | 5 + lib/vobject.php | 24 +- 216 files changed, 6472 insertions(+), 1143 deletions(-) delete mode 100755 3rdparty/Sabre.includes.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Backend/Abstract.php create mode 100644 3rdparty/Sabre/CalDAV/Backend/BackendInterface.php create mode 100644 3rdparty/Sabre/CalDAV/Backend/NotificationSupport.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Backend/PDO.php create mode 100644 3rdparty/Sabre/CalDAV/Backend/SharingSupport.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Calendar.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/CalendarObject.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/CalendarQueryParser.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/CalendarQueryValidator.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/CalendarRootNode.php create mode 100644 3rdparty/Sabre/CalDAV/Exception/InvalidComponentType.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/ICSExportPlugin.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/ICalendar.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/ICalendarObject.php create mode 100644 3rdparty/Sabre/CalDAV/IShareableCalendar.php create mode 100644 3rdparty/Sabre/CalDAV/ISharedCalendar.php create mode 100644 3rdparty/Sabre/CalDAV/Notifications/Collection.php create mode 100644 3rdparty/Sabre/CalDAV/Notifications/ICollection.php create mode 100644 3rdparty/Sabre/CalDAV/Notifications/INode.php create mode 100644 3rdparty/Sabre/CalDAV/Notifications/INotificationType.php create mode 100644 3rdparty/Sabre/CalDAV/Notifications/Node.php create mode 100644 3rdparty/Sabre/CalDAV/Notifications/Notification/Invite.php create mode 100644 3rdparty/Sabre/CalDAV/Notifications/Notification/InviteReply.php create mode 100644 3rdparty/Sabre/CalDAV/Notifications/Notification/SystemStatus.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Plugin.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Principal/Collection.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Principal/ProxyRead.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Principal/ProxyWrite.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Principal/User.php create mode 100644 3rdparty/Sabre/CalDAV/Property/AllowedSharingModes.php create mode 100644 3rdparty/Sabre/CalDAV/Property/Invite.php create mode 100644 3rdparty/Sabre/CalDAV/Property/ScheduleCalendarTransp.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Property/SupportedCalendarComponentSet.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Property/SupportedCalendarData.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Property/SupportedCollationSet.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Schedule/IMip.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Schedule/IOutbox.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Schedule/Outbox.php delete mode 100755 3rdparty/Sabre/CalDAV/Server.php create mode 100644 3rdparty/Sabre/CalDAV/ShareableCalendar.php create mode 100644 3rdparty/Sabre/CalDAV/SharedCalendar.php create mode 100644 3rdparty/Sabre/CalDAV/SharingPlugin.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/UserCalendars.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/Version.php mode change 100755 => 100644 3rdparty/Sabre/CalDAV/includes.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/AddressBook.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/AddressBookQueryParser.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/AddressBookRoot.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/Backend/Abstract.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/Backend/PDO.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/Card.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/IAddressBook.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/ICard.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/IDirectory.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/Plugin.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/Property/SupportedAddressData.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/UserAddressBooks.php create mode 100644 3rdparty/Sabre/CardDAV/VCFExportPlugin.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/Version.php mode change 100755 => 100644 3rdparty/Sabre/CardDAV/includes.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Auth/Backend/AbstractBasic.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Auth/Backend/AbstractDigest.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Auth/Backend/Apache.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Auth/Backend/File.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Auth/Backend/PDO.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Auth/IBackend.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Auth/Plugin.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/GuessContentType.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/MapGetToPropFind.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/Plugin.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/assets/favicon.ico mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/assets/icons/addressbook.png mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/assets/icons/calendar.png mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/assets/icons/card.png mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/assets/icons/collection.png mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/assets/icons/file.png mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/assets/icons/parent.png mode change 100755 => 100644 3rdparty/Sabre/DAV/Browser/assets/icons/principal.png mode change 100755 => 100644 3rdparty/Sabre/DAV/Client.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Collection.php delete mode 100755 3rdparty/Sabre/DAV/Directory.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/BadRequest.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/Conflict.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/ConflictingLock.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/FileNotFound.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/Forbidden.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/InsufficientStorage.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/InvalidResourceType.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/LockTokenMatchesRequestUri.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/Locked.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/MethodNotAllowed.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/NotAuthenticated.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/NotFound.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/NotImplemented.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/PaymentRequired.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/PreconditionFailed.php rename 3rdparty/Sabre/DAV/Exception/{ReportNotImplemented.php => ReportNotSupported.php} (87%) mode change 100755 => 100644 mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/RequestedRangeNotSatisfiable.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Exception/UnsupportedMediaType.php mode change 100755 => 100644 3rdparty/Sabre/DAV/FS/Directory.php mode change 100755 => 100644 3rdparty/Sabre/DAV/FS/File.php mode change 100755 => 100644 3rdparty/Sabre/DAV/FS/Node.php mode change 100755 => 100644 3rdparty/Sabre/DAV/FSExt/Directory.php mode change 100755 => 100644 3rdparty/Sabre/DAV/FSExt/File.php mode change 100755 => 100644 3rdparty/Sabre/DAV/FSExt/Node.php mode change 100755 => 100644 3rdparty/Sabre/DAV/File.php mode change 100755 => 100644 3rdparty/Sabre/DAV/ICollection.php mode change 100755 => 100644 3rdparty/Sabre/DAV/IExtendedCollection.php mode change 100755 => 100644 3rdparty/Sabre/DAV/IFile.php mode change 100755 => 100644 3rdparty/Sabre/DAV/INode.php mode change 100755 => 100644 3rdparty/Sabre/DAV/IProperties.php mode change 100755 => 100644 3rdparty/Sabre/DAV/IQuota.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Locks/Backend/Abstract.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Locks/Backend/FS.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Locks/Backend/File.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Locks/Backend/PDO.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Locks/LockInfo.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Locks/Plugin.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Mount/Plugin.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Node.php mode change 100755 => 100644 3rdparty/Sabre/DAV/ObjectTree.php create mode 100644 3rdparty/Sabre/DAV/PartialUpdate/IFile.php create mode 100644 3rdparty/Sabre/DAV/PartialUpdate/Plugin.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/GetLastModified.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/Href.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/HrefList.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/IHref.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/LockDiscovery.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/ResourceType.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/Response.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/ResponseList.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/SupportedLock.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Property/SupportedReportSet.php create mode 100644 3rdparty/Sabre/DAV/PropertyInterface.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Server.php mode change 100755 => 100644 3rdparty/Sabre/DAV/ServerPlugin.php mode change 100755 => 100644 3rdparty/Sabre/DAV/SimpleCollection.php delete mode 100755 3rdparty/Sabre/DAV/SimpleDirectory.php mode change 100755 => 100644 3rdparty/Sabre/DAV/SimpleFile.php mode change 100755 => 100644 3rdparty/Sabre/DAV/StringUtil.php mode change 100755 => 100644 3rdparty/Sabre/DAV/TemporaryFileFilterPlugin.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Tree.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Tree/Filesystem.php mode change 100755 => 100644 3rdparty/Sabre/DAV/URLUtil.php mode change 100755 => 100644 3rdparty/Sabre/DAV/UUIDUtil.php mode change 100755 => 100644 3rdparty/Sabre/DAV/Version.php mode change 100755 => 100644 3rdparty/Sabre/DAV/XMLUtil.php mode change 100755 => 100644 3rdparty/Sabre/DAV/includes.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/AbstractPrincipalCollection.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Exception/AceConflict.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Exception/NeedPrivileges.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Exception/NoAbstract.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Exception/NotRecognizedPrincipal.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Exception/NotSupportedPrivilege.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/IACL.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/IPrincipal.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/IPrincipalBackend.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Plugin.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Principal.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/PrincipalBackend/PDO.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/PrincipalCollection.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Property/Acl.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Property/AclRestrictions.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Property/CurrentUserPrivilegeSet.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Property/Principal.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Property/SupportedPrivilegeSet.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/Version.php mode change 100755 => 100644 3rdparty/Sabre/DAVACL/includes.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/AWSAuth.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/AbstractAuth.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/BasicAuth.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/DigestAuth.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/Request.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/Response.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/Util.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/Version.php mode change 100755 => 100644 3rdparty/Sabre/HTTP/includes.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Component.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Component/VAlarm.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Component/VCalendar.php create mode 100644 3rdparty/Sabre/VObject/Component/VCard.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Component/VEvent.php create mode 100644 3rdparty/Sabre/VObject/Component/VFreeBusy.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Component/VJournal.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Component/VTodo.php mode change 100755 => 100644 3rdparty/Sabre/VObject/DateTimeParser.php delete mode 100755 3rdparty/Sabre/VObject/Element.php delete mode 100755 3rdparty/Sabre/VObject/Element/DateTime.php delete mode 100755 3rdparty/Sabre/VObject/Element/MultiDateTime.php mode change 100755 => 100644 3rdparty/Sabre/VObject/ElementList.php mode change 100755 => 100644 3rdparty/Sabre/VObject/FreeBusyGenerator.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Node.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Parameter.php mode change 100755 => 100644 3rdparty/Sabre/VObject/ParseException.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Property.php create mode 100644 3rdparty/Sabre/VObject/Property/Compound.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Property/DateTime.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Property/MultiDateTime.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Reader.php mode change 100755 => 100644 3rdparty/Sabre/VObject/RecurrenceIterator.php create mode 100644 3rdparty/Sabre/VObject/Splitter/ICalendar.php create mode 100644 3rdparty/Sabre/VObject/Splitter/SplitterInterface.php create mode 100644 3rdparty/Sabre/VObject/Splitter/VCard.php create mode 100644 3rdparty/Sabre/VObject/StringUtil.php create mode 100644 3rdparty/Sabre/VObject/TimeZoneUtil.php mode change 100755 => 100644 3rdparty/Sabre/VObject/Version.php delete mode 100755 3rdparty/Sabre/VObject/WindowsTimezoneMap.php mode change 100755 => 100644 3rdparty/Sabre/VObject/includes.php mode change 100755 => 100644 3rdparty/Sabre/autoload.php create mode 100644 apps/files/templates/index.php_orig create mode 100644 apps/files/templates/index.php_test diff --git a/3rdparty/Sabre.includes.php b/3rdparty/Sabre.includes.php deleted file mode 100755 index c133437366..0000000000 --- a/3rdparty/Sabre.includes.php +++ /dev/null @@ -1,26 +0,0 @@ -getCalendarObjects($calendarId); + + $validator = new Sabre_CalDAV_CalendarQueryValidator(); + + foreach($objects as $object) { + + if ($this->validateFilterForObject($object, $filters)) { + $result[] = $object['uri']; + } + + } + + return $result; + + } /** - * Returns information from a single calendar object, based on it's object - * uri. + * This method validates if a filters (as passed to calendarQuery) matches + * the given object. * - * The returned array must have the same keys as getCalendarObjects. The - * 'calendardata' object is required here though, while it's not required - * for getCalendarObjects. - * - * @param string $calendarId - * @param string $objectUri - * @return array + * @param array $object + * @param array $filter + * @return bool */ - abstract function getCalendarObject($calendarId,$objectUri); + protected function validateFilterForObject(array $object, array $filters) { - /** - * Creates a new calendar object. - * - * @param string $calendarId - * @param string $objectUri - * @param string $calendarData - * @return void - */ - abstract function createCalendarObject($calendarId,$objectUri,$calendarData); + // 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']); + } - /** - * Updates an existing calendarobject, based on it's uri. - * - * @param string $calendarId - * @param string $objectUri - * @param string $calendarData - * @return void - */ - abstract function updateCalendarObject($calendarId,$objectUri,$calendarData); + $data = is_resource($object['calendardata'])?stream_get_contents($object['calendardata']):$object['calendardata']; + $vObject = VObject\Reader::read($data); + + $validator = new Sabre_CalDAV_CalendarQueryValidator(); + return $validator->validate($vObject, $filters); + + } - /** - * Deletes an existing calendar object. - * - * @param string $calendarId - * @param string $objectUri - * @return void - */ - abstract function deleteCalendarObject($calendarId,$objectUri); } diff --git a/3rdparty/Sabre/CalDAV/Backend/BackendInterface.php b/3rdparty/Sabre/CalDAV/Backend/BackendInterface.php new file mode 100644 index 0000000000..881538ab60 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Backend/BackendInterface.php @@ -0,0 +1,231 @@ + array( + * '{DAV:}displayname' => null, + * ), + * 424 => array( + * '{DAV:}owner' => null, + * ) + * ) + * + * In this example it was forbidden to update {DAV:}displayname. + * (403 Forbidden), which in turn also caused {DAV:}owner to fail + * (424 Failed Dependency) because the request needs to be atomic. + * + * @param mixed $calendarId + * @param array $mutations + * @return bool|array + */ + public function updateCalendar($calendarId, array $mutations); + + /** + * Delete a calendar and all it's objects + * + * @param mixed $calendarId + * @return void + */ + public function deleteCalendar($calendarId); + + /** + * Returns all calendar objects within a calendar. + * + * 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. + * * size - The size of the calendar objects, in bytes. + * + * 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); + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * @param mixed $calendarId + * @param string $objectUri + * @return array + */ + public function getCalendarObject($calendarId,$objectUri); + + /** + * Creates a new calendar object. + * + * 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); + + /** + * Updates an existing calendarobject, based on it's uri. + * + * 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); + + /** + * Deletes an existing calendar object. + * + * @param mixed $calendarId + * @param string $objectUri + * @return void + */ + public function deleteCalendarObject($calendarId,$objectUri); + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre_CalDAV_CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly adviced to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on either VEVENT or VTODO. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interprete all these filters can also simply + * be found in Sabre_CalDAV_CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * @param mixed $calendarId + * @param array $filters + * @return array + */ + public function calendarQuery($calendarId, array $filters); + +} diff --git a/3rdparty/Sabre/CalDAV/Backend/NotificationSupport.php b/3rdparty/Sabre/CalDAV/Backend/NotificationSupport.php new file mode 100644 index 0000000000..d5a1409d9b --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Backend/NotificationSupport.php @@ -0,0 +1,47 @@ + $row['principaluri'], '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}getctag' => $row['ctag']?$row['ctag']:'0', '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre_CalDAV_Property_SupportedCalendarComponentSet($components), + '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}schedule-calendar-transp' => new Sabre_CalDAV_Property_ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), ); @@ -142,11 +157,13 @@ class Sabre_CalDAV_Backend_PDO extends Sabre_CalDAV_Backend_Abstract { 'principaluri', 'uri', 'ctag', + 'transparent', ); $values = array( ':principaluri' => $principalUri, ':uri' => $calendarUri, ':ctag' => 1, + ':transparent' => 0, ); // Default value @@ -160,6 +177,10 @@ class Sabre_CalDAV_Backend_PDO extends Sabre_CalDAV_Backend_Abstract { } $values[':components'] = implode(',',$properties[$sccs]->getValue()); } + $transp = '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}schedule-calendar-transp'; + if (isset($properties[$transp])) { + $values[':transparent'] = $properties[$transp]->getValue()==='transparent'; + } foreach($this->propertyMap as $xmlName=>$dbName) { if (isset($properties[$xmlName])) { @@ -225,16 +246,24 @@ class Sabre_CalDAV_Backend_PDO extends Sabre_CalDAV_Backend_Abstract { foreach($mutations as $propertyName=>$propertyValue) { - // We don't know about this property. - if (!isset($this->propertyMap[$propertyName])) { - $hasError = true; - $result[403][$propertyName] = null; - unset($mutations[$propertyName]); - continue; - } + switch($propertyName) { + case '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}schedule-calendar-transp' : + $fieldName = 'transparent'; + $newValues[$fieldName] = $propertyValue->getValue()==='transparent'; + break; + default : + // Checking the property map + if (!isset($this->propertyMap[$propertyName])) { + // We don't know about this property. + $hasError = true; + $result[403][$propertyName] = null; + unset($mutations[$propertyName]); + continue; + } - $fieldName = $this->propertyMap[$propertyName]; - $newValues[$fieldName] = $propertyValue; + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + } } @@ -316,9 +345,22 @@ class Sabre_CalDAV_Backend_PDO extends Sabre_CalDAV_Backend_Abstract { */ public function getCalendarObjects($calendarId) { - $stmt = $this->pdo->prepare('SELECT * FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'); + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'); $stmt->execute(array($calendarId)); - return $stmt->fetchAll(); + + $result = array(); + foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = array( + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + ); + } + + return $result; } @@ -336,44 +378,166 @@ class Sabre_CalDAV_Backend_PDO extends Sabre_CalDAV_Backend_Abstract { */ public function getCalendarObject($calendarId,$objectUri) { - $stmt = $this->pdo->prepare('SELECT * FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?'); + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?'); $stmt->execute(array($calendarId, $objectUri)); - return $stmt->fetch(); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if(!$row) return null; + + return array( + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'calendardata' => $row['calendardata'], + ); } + /** * Creates a new calendar object. * - * @param string $calendarId + * 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 void + * @return string|null */ public function createCalendarObject($calendarId,$objectUri,$calendarData) { - $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified) VALUES (?,?,?,?)'); - $stmt->execute(array($calendarId,$objectUri,$calendarData,time())); + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence) VALUES (?,?,?,?,?,?,?,?,?)'); + $stmt->execute(array( + $calendarId, + $objectUri, + $calendarData, + time(), + $extraData['etag'], + $extraData['size'], + $extraData['componentType'], + $extraData['firstOccurence'], + $extraData['lastOccurence'], + )); $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET ctag = ctag + 1 WHERE id = ?'); $stmt->execute(array($calendarId)); + return '"' . $extraData['etag'] . '"'; + } /** * Updates an existing calendarobject, based on it's uri. * - * @param string $calendarId + * 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 void + * @return string|null */ public function updateCalendarObject($calendarId,$objectUri,$calendarData) { - $stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ? WHERE calendarid = ? AND uri = ?'); - $stmt->execute(array($calendarData,time(),$calendarId,$objectUri)); + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute(array($calendarData,time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'] ,$calendarId,$objectUri)); $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET ctag = ctag + 1 WHERE id = ?'); $stmt->execute(array($calendarId)); + return '"' . $extraData['etag'] . '"'; + + } + + /** + * Parses some information from calendar objects, used for optimized + * calendar-queries. + * + * Returns an array with the following keys: + * * etag + * * size + * * componentType + * * firstOccurence + * * lastOccurence + * + * @param string $calendarData + * @return array + */ + protected function getDenormalizedData($calendarData) { + + $vObject = VObject\Reader::read($calendarData); + $componentType = null; + $component = null; + $firstOccurence = null; + $lastOccurence = null; + foreach($vObject->getComponents() as $component) { + if ($component->name!=='VTIMEZONE') { + $componentType = $component->name; + break; + } + } + if (!$componentType) { + throw new Sabre_DAV_Exception_BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); + } + if ($componentType === 'VEVENT') { + $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); + // Finding the last occurence is a bit harder + if (!isset($component->RRULE)) { + if (isset($component->DTEND)) { + $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); + } elseif (isset($component->DURATION)) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate->add(VObject\DateTimeParser::parse($component->DURATION->value)); + $lastOccurence = $endDate->getTimeStamp(); + } elseif ($component->DTSTART->getDateType()===VObject\Property\DateTime::DATE) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate->modify('+1 day'); + $lastOccurence = $endDate->getTimeStamp(); + } else { + $lastOccurence = $firstOccurence; + } + } else { + $it = new VObject\RecurrenceIterator($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(); + } + + } + } + + return array( + 'etag' => md5($calendarData), + 'size' => strlen($calendarData), + 'componentType' => $componentType, + 'firstOccurence' => $firstOccurence, + 'lastOccurence' => $lastOccurence, + ); + } /** @@ -392,5 +556,132 @@ class Sabre_CalDAV_Backend_PDO extends Sabre_CalDAV_Backend_Abstract { } + /** + * 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 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. + * + * This specific implementation (for the PDO) backend optimizes filters on + * specific components, and VEVENT time-ranges. + * + * @param string $calendarId + * @param array $filters + * @return array + */ + public function calendarQuery($calendarId, array $filters) { + $result = array(); + $validator = new Sabre_CalDAV_CalendarQueryValidator(); + + $componentType = null; + $requirePostFilter = true; + $timeRange = null; + + // if no filters were specified, we don't need to filter after a query + if (!$filters['prop-filters'] && !$filters['comp-filters']) { + $requirePostFilter = false; + } + + // Figuring out if there's a component filter + if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { + $componentType = $filters['comp-filters'][0]['name']; + + // Checking if we need post-filters + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) { + $requirePostFilter = false; + } + // There was a time-range filter + if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) { + $timeRange = $filters['comp-filters'][0]['time-range']; + + // If start time OR the end time is not specified, we can do a + // 100% accurate mysql query. + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) { + $requirePostFilter = false; + } + } + + } + + if ($requirePostFilter) { + $query = "SELECT uri, calendardata FROM ".$this->calendarObjectTableName." WHERE calendarid = :calendarid"; + } else { + $query = "SELECT uri FROM ".$this->calendarObjectTableName." WHERE calendarid = :calendarid"; + } + + $values = array( + '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 = array(); + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($requirePostFilter) { + if (!$this->validateFilterForObject($row, $filters)) { + continue; + } + } + $result[] = $row['uri']; + + } + + return $result; + + } } diff --git a/3rdparty/Sabre/CalDAV/Backend/SharingSupport.php b/3rdparty/Sabre/CalDAV/Backend/SharingSupport.php new file mode 100644 index 0000000000..f73f0ff3d8 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Backend/SharingSupport.php @@ -0,0 +1,238 @@ +caldavBackend = $caldavBackend; $this->principalBackend = $principalBackend; $this->calendarInfo = $calendarInfo; - } /** @@ -92,9 +91,6 @@ class Sabre_CalDAV_Calendar implements Sabre_CalDAV_ICalendar, Sabre_DAV_IProper case '{urn:ietf:params:xml:ns:caldav}supported-collation-set' : $response[$prop] = new Sabre_CalDAV_Property_SupportedCollationSet(); break; - case '{DAV:}owner' : - $response[$prop] = new Sabre_DAVACL_Property_Principal(Sabre_DAVACL_Property_Principal::HREF,$this->calendarInfo['principaluri']); - break; default : if (isset($this->calendarInfo[$prop])) $response[$prop] = $this->calendarInfo[$prop]; break; @@ -110,12 +106,21 @@ class Sabre_CalDAV_Calendar implements Sabre_CalDAV_ICalendar, Sabre_DAV_IProper * The contained calendar objects are for example Events or Todo's. * * @param string $name - * @return Sabre_DAV_ICalendarObject + * @return Sabre_CalDAV_ICalendarObject */ public function getChild($name) { $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name); if (!$obj) throw new Sabre_DAV_Exception_NotFound('Calendar object not found'); + + $obj['acl'] = $this->getACL(); + // Removing the irrelivant + foreach($obj['acl'] as $key=>$acl) { + if ($acl['privilege'] === '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}read-free-busy') { + unset($obj['acl'][$key]); + } + } + return new Sabre_CalDAV_CalendarObject($this->caldavBackend,$this->calendarInfo,$obj); } @@ -130,6 +135,13 @@ class Sabre_CalDAV_Calendar implements Sabre_CalDAV_ICalendar, Sabre_DAV_IProper $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']); $children = array(); foreach($objs as $obj) { + $obj['acl'] = $this->getACL(); + // Removing the irrelivant + foreach($obj['acl'] as $key=>$acl) { + if ($acl['privilege'] === '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}read-free-busy') { + unset($obj['acl'][$key]); + } + } $children[] = new Sabre_CalDAV_CalendarObject($this->caldavBackend,$this->calendarInfo,$obj); } return $children; @@ -262,27 +274,27 @@ class Sabre_CalDAV_Calendar implements Sabre_CalDAV_ICalendar, Sabre_DAV_IProper return array( array( 'privilege' => '{DAV:}read', - 'principal' => $this->calendarInfo['principaluri'], + 'principal' => $this->getOwner(), 'protected' => true, ), array( 'privilege' => '{DAV:}write', - 'principal' => $this->calendarInfo['principaluri'], + 'principal' => $this->getOwner(), 'protected' => true, ), array( 'privilege' => '{DAV:}read', - 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'principal' => $this->getOwner() . '/calendar-proxy-write', 'protected' => true, ), array( 'privilege' => '{DAV:}write', - 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'principal' => $this->getOwner() . '/calendar-proxy-write', 'protected' => true, ), array( 'privilege' => '{DAV:}read', - 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read', + 'principal' => $this->getOwner() . '/calendar-proxy-read', 'protected' => true, ), array( @@ -340,4 +352,27 @@ class Sabre_CalDAV_Calendar implements Sabre_CalDAV_ICalendar, Sabre_DAV_IProper } + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre_CalDAV_CalendarQueryParser. + * + * @param array $filters + * @return array + */ + public function calendarQuery(array $filters) { + + return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters); + + } + } diff --git a/3rdparty/Sabre/CalDAV/CalendarObject.php b/3rdparty/Sabre/CalDAV/CalendarObject.php old mode 100755 new mode 100644 index 72f0a578d1..40bd8588c7 --- a/3rdparty/Sabre/CalDAV/CalendarObject.php +++ b/3rdparty/Sabre/CalDAV/CalendarObject.php @@ -6,13 +6,13 @@ * @package Sabre * @subpackage CalDAV * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved. - * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ class Sabre_CalDAV_CalendarObject extends Sabre_DAV_File implements Sabre_CalDAV_ICalendarObject, Sabre_DAVACL_IACL { /** - * Sabre_CalDAV_Backend_Abstract + * Sabre_CalDAV_Backend_BackendInterface * * @var array */ @@ -35,11 +35,11 @@ class Sabre_CalDAV_CalendarObject extends Sabre_DAV_File implements Sabre_CalDAV /** * Constructor * - * @param Sabre_CalDAV_Backend_Abstract $caldavBackend + * @param Sabre_CalDAV_Backend_BackendInterface $caldavBackend * @param array $calendarInfo * @param array $objectData */ - public function __construct(Sabre_CalDAV_Backend_Abstract $caldavBackend,array $calendarInfo,array $objectData) { + public function __construct(Sabre_CalDAV_Backend_BackendInterface $caldavBackend,array $calendarInfo,array $objectData) { $this->caldavBackend = $caldavBackend; @@ -85,8 +85,8 @@ class Sabre_CalDAV_CalendarObject extends Sabre_DAV_File implements Sabre_CalDAV /** * Updates the ICalendar-formatted object * - * @param string $calendarData - * @return void + * @param string|resource $calendarData + * @return string */ public function put($calendarData) { @@ -119,7 +119,7 @@ class Sabre_CalDAV_CalendarObject extends Sabre_DAV_File implements Sabre_CalDAV */ public function getContentType() { - return 'text/calendar'; + return 'text/calendar; charset=utf-8'; } @@ -143,7 +143,7 @@ class Sabre_CalDAV_CalendarObject extends Sabre_DAV_File implements Sabre_CalDAV /** * Returns the last modification date as a unix timestamp * - * @return time + * @return int */ public function getLastModified() { @@ -206,6 +206,12 @@ class Sabre_CalDAV_CalendarObject extends Sabre_DAV_File implements Sabre_CalDAV */ 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 array( array( 'privilege' => '{DAV:}read', diff --git a/3rdparty/Sabre/CalDAV/CalendarQueryParser.php b/3rdparty/Sabre/CalDAV/CalendarQueryParser.php old mode 100755 new mode 100644 index bd0d343382..b95095f96f --- a/3rdparty/Sabre/CalDAV/CalendarQueryParser.php +++ b/3rdparty/Sabre/CalDAV/CalendarQueryParser.php @@ -1,5 +1,7 @@ xpath = new DOMXPath($dom); $this->xpath->registerNameSpace('cal',Sabre_CalDAV_Plugin::NS_CALDAV); - $this->xpath->registerNameSpace('dav','urn:DAV'); + $this->xpath->registerNameSpace('dav','DAV:'); } @@ -241,12 +243,12 @@ class Sabre_CalDAV_CalendarQueryParser { $timeRangeNode = $timeRangeNodes->item(0); if ($start = $timeRangeNode->getAttribute('start')) { - $start = Sabre_VObject_DateTimeParser::parseDateTime($start); + $start = VObject\DateTimeParser::parseDateTime($start); } else { $start = null; } if ($end = $timeRangeNode->getAttribute('end')) { - $end = Sabre_VObject_DateTimeParser::parseDateTime($end); + $end = VObject\DateTimeParser::parseDateTime($end); } else { $end = null; } @@ -274,13 +276,13 @@ class Sabre_CalDAV_CalendarQueryParser { if(!$start) { throw new Sabre_DAV_Exception_BadRequest('The "start" attribute is required for the CALDAV:expand element'); } - $start = Sabre_VObject_DateTimeParser::parseDateTime($start); + $start = VObject\DateTimeParser::parseDateTime($start); $end = $parentNode->getAttribute('end'); if(!$end) { throw new Sabre_DAV_Exception_BadRequest('The "end" attribute is required for the CALDAV:expand element'); } - $end = Sabre_VObject_DateTimeParser::parseDateTime($end); + $end = VObject\DateTimeParser::parseDateTime($end); if ($end <= $start) { throw new Sabre_DAV_Exception_BadRequest('The end-date must be larger than the start-date in the expand element.'); diff --git a/3rdparty/Sabre/CalDAV/CalendarQueryValidator.php b/3rdparty/Sabre/CalDAV/CalendarQueryValidator.php old mode 100755 new mode 100644 index 8f674840e8..53e86fc509 --- a/3rdparty/Sabre/CalDAV/CalendarQueryValidator.php +++ b/3rdparty/Sabre/CalDAV/CalendarQueryValidator.php @@ -1,5 +1,7 @@ parent->name === 'VEVENT' && $component->parent->RRULE) { // Fire up the iterator! - $it = new Sabre_VObject_RecurrenceIterator($component->parent->parent, (string)$component->parent->UID); + $it = new VObject\RecurrenceIterator($component->parent->parent, (string)$component->parent->UID); while($it->valid()) { $expandedEvent = $it->getEventObject(); diff --git a/3rdparty/Sabre/CalDAV/CalendarRootNode.php b/3rdparty/Sabre/CalDAV/CalendarRootNode.php old mode 100755 new mode 100644 index 3907913cc7..eb62eea75a --- a/3rdparty/Sabre/CalDAV/CalendarRootNode.php +++ b/3rdparty/Sabre/CalDAV/CalendarRootNode.php @@ -1,14 +1,15 @@ caldavBackend = $caldavBackend; diff --git a/3rdparty/Sabre/CalDAV/Exception/InvalidComponentType.php b/3rdparty/Sabre/CalDAV/Exception/InvalidComponentType.php new file mode 100644 index 0000000000..4ac617d22f --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Exception/InvalidComponentType.php @@ -0,0 +1,32 @@ +ownerDocument; + + $np = $doc->createElementNS(Sabre_CalDAV_Plugin::NS_CALDAV,'cal:supported-calendar-component'); + $errorNode->appendChild($np); + + } + +} \ No newline at end of file diff --git a/3rdparty/Sabre/CalDAV/ICSExportPlugin.php b/3rdparty/Sabre/CalDAV/ICSExportPlugin.php old mode 100755 new mode 100644 index ec42b406b2..d3e4e7b720 --- a/3rdparty/Sabre/CalDAV/ICSExportPlugin.php +++ b/3rdparty/Sabre/CalDAV/ICSExportPlugin.php @@ -1,5 +1,7 @@ version = '2.0'; if (Sabre_DAV_Server::$exposeVersion) { $calendar->prodid = '-//SabreDAV//SabreDAV ' . Sabre_DAV_Version::VERSION . '//EN'; @@ -103,7 +105,7 @@ class Sabre_CalDAV_ICSExportPlugin extends Sabre_DAV_ServerPlugin { } $nodeData = $node[200]['{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}calendar-data']; - $nodeComp = Sabre_VObject_Reader::read($nodeData); + $nodeComp = VObject\Reader::read($nodeData); foreach($nodeComp->children() as $child) { diff --git a/3rdparty/Sabre/CalDAV/ICalendar.php b/3rdparty/Sabre/CalDAV/ICalendar.php old mode 100755 new mode 100644 index 15d51ebcf7..40aa9f9579 --- a/3rdparty/Sabre/CalDAV/ICalendar.php +++ b/3rdparty/Sabre/CalDAV/ICalendar.php @@ -8,11 +8,28 @@ * @package Sabre * @subpackage CalDAV * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved. - * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ interface Sabre_CalDAV_ICalendar extends Sabre_DAV_ICollection { - + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre_CalDAV_CalendarQueryParser. + * + * @param array $filters + * @return array + */ + public function calendarQuery(array $filters); } diff --git a/3rdparty/Sabre/CalDAV/ICalendarObject.php b/3rdparty/Sabre/CalDAV/ICalendarObject.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/IShareableCalendar.php b/3rdparty/Sabre/CalDAV/IShareableCalendar.php new file mode 100644 index 0000000000..5b55788c01 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/IShareableCalendar.php @@ -0,0 +1,48 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + + } + + /** + * Returns all notifications for a principal + * + * @return array + */ + public function getChildren() { + + $children = array(); + $notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri); + + foreach($notifications as $notification) { + + $children[] = new Sabre_CalDAV_Notifications_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; + + } + + /** + * Returns a group principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getGroup() { + + return null; + + } + + /** + * 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 array( + array( + 'principal' => $this->getOwner(), + 'privilege' => '{DAV:}read', + 'protected' => true, + ), + array( + 'principal' => $this->getOwner(), + 'privilege' => '{DAV:}write', + 'protected' => true, + ) + ); + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's as an array argument. + * + * @param array $acl + * @return void + */ + public function setACL(array $acl) { + + throw new Sabre_DAV_Exception_NotImplemented('Updating ACLs is not implemented here'); + + } + + /** + * 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/3rdparty/Sabre/CalDAV/Notifications/ICollection.php b/3rdparty/Sabre/CalDAV/Notifications/ICollection.php new file mode 100644 index 0000000000..eb873af3f9 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Notifications/ICollection.php @@ -0,0 +1,22 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + $this->notification = $notification; + + } + + /** + * Returns the path name for this notification + * + * @return id + */ + 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_Notifications_INotificationType classes. + * + * @return Sabre_DAVNotification_INotificationType + */ + public function getNotificationType() { + + return $this->notification; + + } + + /** + * Deletes this notification + * + * @return void + */ + 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; + + } + + /** + * Returns a group principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getGroup() { + + return null; + + } + + /** + * 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 array( + array( + 'principal' => $this->getOwner(), + 'privilege' => '{DAV:}read', + 'protected' => true, + ), + array( + 'principal' => $this->getOwner(), + 'privilege' => '{DAV:}write', + 'protected' => true, + ) + ); + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's as an array argument. + * + * @param array $acl + * @return void + */ + public function setACL(array $acl) { + + throw new Sabre_DAV_Exception_NotImplemented('Updating ACLs is not implemented here'); + + } + + /** + * 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/3rdparty/Sabre/CalDAV/Notifications/Notification/Invite.php b/3rdparty/Sabre/CalDAV/Notifications/Notification/Invite.php new file mode 100644 index 0000000000..a6b36203f3 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Notifications/Notification/Invite.php @@ -0,0 +1,276 @@ +$value) { + if (!property_exists($this, $key)) { + throw new InvalidArgumentException('Unknown option: ' . $key); + } + $this->$key = $value; + } + + } + + /** + * Serializes the notification as a single property. + * + * You should usually just encode the single top-level element of the + * notification. + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serialize(Sabre_DAV_Server $server, \DOMElement $node) { + + $prop = $node->ownerDocument->createElement('cs:invite-notification'); + $node->appendChild($prop); + + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serializeBody(Sabre_DAV_Server $server, \DOMElement $node) { + + $doc = $node->ownerDocument; + + $dt = $doc->createElement('cs:dtstamp'); + $this->dtStamp->setTimezone(new \DateTimezone('GMT')); + $dt->appendChild($doc->createTextNode($this->dtStamp->format('Ymd\\THis\\Z'))); + $node->appendChild($dt); + + $prop = $doc->createElement('cs:invite-notification'); + $node->appendChild($prop); + + $uid = $doc->createElement('cs:uid'); + $uid->appendChild( $doc->createTextNode($this->id) ); + $prop->appendChild($uid); + + $href = $doc->createElement('d:href'); + $href->appendChild( $doc->createTextNode( $this->href ) ); + $prop->appendChild($href); + + $nodeName = null; + switch($this->type) { + + case SharingPlugin::STATUS_ACCEPTED : + $nodeName = 'cs:invite-accepted'; + break; + case SharingPlugin::STATUS_DECLINED : + $nodeName = 'cs:invite-declined'; + break; + case SharingPlugin::STATUS_DELETED : + $nodeName = 'cs:invite-deleted'; + break; + case SharingPlugin::STATUS_NORESPONSE : + $nodeName = 'cs:invite-noresponse'; + break; + + } + $prop->appendChild( + $doc->createElement($nodeName) + ); + $hostHref = $doc->createElement('d:href', $server->getBaseUri() . $this->hostUrl); + $hostUrl = $doc->createElement('cs:hosturl'); + $hostUrl->appendChild($hostHref); + $prop->appendChild($hostUrl); + + $access = $doc->createElement('cs:access'); + if ($this->readOnly) { + $access->appendChild($doc->createElement('cs:read')); + } else { + $access->appendChild($doc->createElement('cs:read-write')); + } + $prop->appendChild($access); + + $organizerHref = $doc->createElement('d:href', $server->getBaseUri() . $this->organizer); + $organizerUrl = $doc->createElement('cs:organizer'); + if ($this->commonName) { + $commonName = $doc->createElement('cs:common-name'); + $commonName->appendChild($doc->createTextNode($this->commonName)); + $organizerUrl->appendChild($commonName); + } + $organizerUrl->appendChild($organizerHref); + $prop->appendChild($organizerUrl); + + if ($this->summary) { + $summary = $doc->createElement('cs:summary'); + $summary->appendChild($doc->createTextNode($this->summary)); + $prop->appendChild($summary); + } + if ($this->supportedComponents) { + + $xcomp = $doc->createElement('cal:supported-calendar-component-set'); + $this->supportedComponents->serialize($server, $xcomp); + $prop->appendChild($xcomp); + + } + + } + + /** + * 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/3rdparty/Sabre/CalDAV/Notifications/Notification/InviteReply.php b/3rdparty/Sabre/CalDAV/Notifications/Notification/InviteReply.php new file mode 100644 index 0000000000..e935aa5aa1 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Notifications/Notification/InviteReply.php @@ -0,0 +1,216 @@ +$value) { + if (!property_exists($this, $key)) { + throw new InvalidArgumentException('Unknown option: ' . $key); + } + $this->$key = $value; + } + + } + + /** + * Serializes the notification as a single property. + * + * You should usually just encode the single top-level element of the + * notification. + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serialize(Sabre_DAV_Server $server, \DOMElement $node) { + + $prop = $node->ownerDocument->createElement('cs:invite-reply'); + $node->appendChild($prop); + + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serializeBody(Sabre_DAV_Server $server, \DOMElement $node) { + + $doc = $node->ownerDocument; + + $dt = $doc->createElement('cs:dtstamp'); + $this->dtStamp->setTimezone(new \DateTimezone('GMT')); + $dt->appendChild($doc->createTextNode($this->dtStamp->format('Ymd\\THis\\Z'))); + $node->appendChild($dt); + + $prop = $doc->createElement('cs:invite-reply'); + $node->appendChild($prop); + + $uid = $doc->createElement('cs:uid'); + $uid->appendChild($doc->createTextNode($this->id)); + $prop->appendChild($uid); + + $inReplyTo = $doc->createElement('cs:in-reply-to'); + $inReplyTo->appendChild( $doc->createTextNode($this->inReplyTo) ); + $prop->appendChild($inReplyTo); + + $href = $doc->createElement('d:href'); + $href->appendChild( $doc->createTextNode($this->href) ); + $prop->appendChild($href); + + $nodeName = null; + switch($this->type) { + + case SharingPlugin::STATUS_ACCEPTED : + $nodeName = 'cs:invite-accepted'; + break; + case SharingPlugin::STATUS_DECLINED : + $nodeName = 'cs:invite-declined'; + break; + + } + $prop->appendChild( + $doc->createElement($nodeName) + ); + $hostHref = $doc->createElement('d:href', $server->getBaseUri() . $this->hostUrl); + $hostUrl = $doc->createElement('cs:hosturl'); + $hostUrl->appendChild($hostHref); + $prop->appendChild($hostUrl); + + if ($this->summary) { + $summary = $doc->createElement('cs:summary'); + $summary->appendChild($doc->createTextNode($this->summary)); + $prop->appendChild($summary); + } + + } + + /** + * 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/3rdparty/Sabre/CalDAV/Notifications/Notification/SystemStatus.php b/3rdparty/Sabre/CalDAV/Notifications/Notification/SystemStatus.php new file mode 100644 index 0000000000..f09ed3525f --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Notifications/Notification/SystemStatus.php @@ -0,0 +1,179 @@ +id = $id; + $this->type = $type; + $this->description = $description; + $this->href = $href; + $this->etag = $etag; + + } + + /** + * Serializes the notification as a single property. + * + * You should usually just encode the single top-level element of the + * notification. + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serialize(Sabre_DAV_Server $server, \DOMElement $node) { + + 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; + } + + $prop = $node->ownerDocument->createElement('cs:systemstatus'); + $prop->setAttribute('type', $type); + + $node->appendChild($prop); + + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serializeBody(Sabre_DAV_Server $server, \DOMElement $node) { + + 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; + } + + $prop = $node->ownerDocument->createElement('cs:systemstatus'); + $prop->setAttribute('type', $type); + + if ($this->description) { + $text = $node->ownerDocument->createTextNode($this->description); + $desc = $node->ownerDocument->createElement('cs:description'); + $desc->appendChild($text); + $prop->appendChild($desc); + } + if ($this->href) { + $text = $node->ownerDocument->createTextNode($this->href); + $href = $node->ownerDocument->createElement('d:href'); + $href->appendChild($text); + $prop->appendChild($href); + } + + $node->appendChild($prop); + + } + + /** + * 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/3rdparty/Sabre/CalDAV/Plugin.php b/3rdparty/Sabre/CalDAV/Plugin.php old mode 100755 new mode 100644 index c56ab38484..f3d11969c8 --- a/3rdparty/Sabre/CalDAV/Plugin.php +++ b/3rdparty/Sabre/CalDAV/Plugin.php @@ -1,5 +1,7 @@ subscribeEvent('onBrowserPostAction', array($this,'browserPostAction')); $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent')); $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile')); + $server->subscribeEvent('beforeMethod', array($this,'beforeMethod')); $server->xmlNamespaces[self::NS_CALDAV] = 'cal'; $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs'; $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre_CalDAV_Property_SupportedCalendarComponentSet'; + $server->propertyMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre_CalDAV_Property_ScheduleCalendarTransp'; $server->resourceTypeMapping['Sabre_CalDAV_ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar'; $server->resourceTypeMapping['Sabre_CalDAV_Schedule_IOutbox'] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox'; $server->resourceTypeMapping['Sabre_CalDAV_Principal_ProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read'; $server->resourceTypeMapping['Sabre_CalDAV_Principal_ProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write'; + $server->resourceTypeMapping['Sabre_CalDAV_Notifications_ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification'; array_push($server->protectedProperties, @@ -205,7 +200,9 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { // CalendarServer extensions '{' . self::NS_CALENDARSERVER . '}getctag', '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for', - '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for' + '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for', + '{' . self::NS_CALENDARSERVER . '}notification-URL', + '{' . self::NS_CALENDARSERVER . '}notificationtype' ); } @@ -226,6 +223,13 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { // unknownMethod event. return false; case 'POST' : + + // Checking if this is a text/calendar content type + $contentType = $this->server->httpRequest->getHeader('Content-Type'); + if (strpos($contentType, 'text/calendar')!==0) { + return; + } + // Checking if we're talking to an outbox try { $node = $this->server->tree->getNodeForPath($uri); @@ -235,7 +239,7 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { if (!$node instanceof Sabre_CalDAV_Schedule_IOutbox) return; - $this->outboxRequest($node); + $this->outboxRequest($node, $uri); return false; } @@ -348,7 +352,7 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { if (in_array($calProp,$requestedProperties)) { $addresses = $node->getAlternateUriSet(); - $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl(); + $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/'; unset($requestedProperties[$calProp]); $returnedProperties[200][$calProp] = new Sabre_DAV_Property_HrefList($addresses, false); @@ -390,8 +394,31 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { } + // notification-URL property + $notificationUrl = '{' . self::NS_CALENDARSERVER . '}notification-URL'; + if (($index = array_search($notificationUrl, $requestedProperties)) !== false) { + $principalId = $node->getName(); + $calendarHomePath = 'calendars/' . $principalId . '/notifications/'; + unset($requestedProperties[$index]); + $returnedProperties[200][$notificationUrl] = new Sabre_DAV_Property_Href($calendarHomePath); + } + } // instanceof IPrincipal + if ($node instanceof Sabre_CalDAV_Notifications_INode) { + + $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype'; + if (($index = array_search($propertyName, $requestedProperties)) !== false) { + + $returnedProperties[200][$propertyName] = + $node->getNotificationType(); + + unset($requestedProperties[$index]); + + } + + } // instanceof Notifications_INode + if ($node instanceof Sabre_CalDAV_ICalendarObject) { // The calendar-data property is not supposed to be a 'real' @@ -424,11 +451,11 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { public function calendarMultiGetReport($dom) { $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)); - $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href'); + $hrefElems = $dom->getElementsByTagNameNS('DAV:','href'); $xpath = new DOMXPath($dom); $xpath->registerNameSpace('cal',Sabre_CalDAV_Plugin::NS_CALDAV); - $xpath->registerNameSpace('dav','urn:DAV'); + $xpath->registerNameSpace('dav','DAV:'); $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand'); if ($expand->length>0) { @@ -438,8 +465,8 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { if(!$start || !$end) { throw new Sabre_DAV_Exception_BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element'); } - $start = Sabre_VObject_DateTimeParser::parseDateTime($start); - $end = Sabre_VObject_DateTimeParser::parseDateTime($end); + $start = VObject\DateTimeParser::parseDateTime($start); + $end = VObject\DateTimeParser::parseDateTime($end); if ($end <= $start) { throw new Sabre_DAV_Exception_BadRequest('The end-date must be larger than the start-date in the expand element.'); @@ -458,7 +485,7 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { list($objProps) = $this->server->getPropertiesForPath($uri,$properties); if ($expand && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) { - $vObject = Sabre_VObject_Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']); + $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']); $vObject->expand($start, $end); $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); } @@ -467,9 +494,12 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { } + $prefer = $this->server->getHTTPPRefer(); + $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); - $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList)); + $this->server->httpResponse->setHeader('Vary','Brief,Prefer'); + $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList, $prefer['return-minimal'])); } @@ -487,54 +517,95 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { $parser = new Sabre_CalDAV_CalendarQueryParser($dom); $parser->parse(); - $requestedCalendarData = true; - $requestedProperties = $parser->requestedProperties; + $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); + $depth = $this->server->getHTTPDepth(0); - if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { + // The default result is an empty array + $result = array(); - // We always retrieve calendar-data, as we need it for filtering. - $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; + // The calendarobject was requested directly. In this case we handle + // this locally. + if ($depth == 0 && $node instanceof Sabre_CalDAV_ICalendarObject) { + + $requestedCalendarData = true; + $requestedProperties = $parser->requestedProperties; + + 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( + $this->server->getRequestUri(), + $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 Sabre_CalDAV_CalendarQueryValidator(); + $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + if ($validator->validate($vObject,$parser->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 ($parser->expand) { + $vObject->expand($parser->expand['start'], $parser->expand['end']); + $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); + } + } + + $result = array($properties); + + } + + } - // If calendar-data wasn't explicitly requested, we need to remove - // it after processing. - $requestedCalendarData = false; } + // If we're dealing with a calendar, the calendar itself is responsible + // for the calendar-query. + if ($node instanceof Sabre_CalDAV_ICalendar && $depth = 1) { - // These are the list of nodes that potentially match the requirement - $candidateNodes = $this->server->getPropertiesForPath( - $this->server->getRequestUri(), - $requestedProperties, - $this->server->getHTTPDepth(0) - ); + $nodePaths = $node->calendarQuery($parser->filters); - $verifiedNodes = array(); + foreach($nodePaths as $path) { - $validator = new Sabre_CalDAV_CalendarQueryValidator(); + list($properties) = + $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties); - foreach($candidateNodes as $node) { - - // If the node didn't have a calendar-data property, it must not be a calendar object - if (!isset($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) - continue; - - $vObject = Sabre_VObject_Reader::read($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); - if ($validator->validate($vObject,$parser->filters)) { - - if (!$requestedCalendarData) { - unset($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); - } if ($parser->expand) { + // We need to do some post-processing + $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); $vObject->expand($parser->expand['start'], $parser->expand['end']); - $node[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); + $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); } - $verifiedNodes[] = $node; + + $result[] = $properties; + } } + $prefer = $this->server->getHTTPPRefer(); + $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); - $this->server->httpResponse->sendBody($this->server->generateMultiStatus($verifiedNodes)); + $this->server->httpResponse->setHeader('Vary','Brief,Prefer'); + $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal'])); } @@ -561,10 +632,10 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { } if ($start) { - $start = Sabre_VObject_DateTimeParser::parseDateTime($start); + $start = VObject\DateTimeParser::parseDateTime($start); } if ($end) { - $end = Sabre_VObject_DateTimeParser::parseDateTime($end); + $end = VObject\DateTimeParser::parseDateTime($end); } if (!$start && !$end) { @@ -583,15 +654,33 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { throw new Sabre_DAV_Exception_NotImplemented('The free-busy-query REPORT is only implemented on calendars'); } - $objects = array_map(function($child) { - $obj = $child->get(); - if (is_resource($obj)) { - $obj = stream_get_contents($obj); - } - return $obj; - }, $calendar->getChildren()); + // Doing a calendar-query first, to make sure we get the most + // performance. + $urls = $calendar->calendarQuery(array( + 'name' => 'VCALENDAR', + 'comp-filters' => array( + array( + 'name' => 'VEVENT', + 'comp-filters' => array(), + 'prop-filters' => array(), + 'is-not-defined' => false, + 'time-range' => array( + 'start' => $start, + 'end' => $end, + ), + ), + ), + 'prop-filters' => array(), + 'is-not-defined' => false, + 'time-range' => null, + )); - $generator = new Sabre_VObject_FreeBusyGenerator(); + $objects = array_map(function($url) use ($calendar) { + $obj = $calendar->getChild($url)->get(); + return $obj; + }, $urls); + + $generator = new VObject\FreeBusyGenerator(); $generator->setObjects($objects); $generator->setTimeRange($start, $end); $result = $generator->getResult(); @@ -620,7 +709,7 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { if (!$node instanceof Sabre_CalDAV_ICalendarObject) return; - $this->validateICalendar($data); + $this->validateICalendar($data, $path); } @@ -640,7 +729,52 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { if (!$parentNode instanceof Sabre_CalDAV_Calendar) return; - $this->validateICalendar($data); + $this->validateICalendar($data, $path); + + } + + /** + * This event is triggered before any HTTP request is handled. + * + * We use this to intercept GET calls to notification nodes, and return the + * proper response. + * + * @param string $method + * @param string $path + * @return void + */ + public function beforeMethod($method, $path) { + + if ($method!=='GET') return; + + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (Sabre_DAV_Exception_NotFound $e) { + return; + } + + if (!$node instanceof Sabre_CalDAV_Notifications_INode) + return; + + if (!$this->server->checkPreconditions(true)) return false; + + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + + $root = $dom->createElement('cs:notification'); + foreach($this->server->xmlNamespaces as $namespace => $prefix) { + $root->setAttribute('xmlns:' . $prefix, $namespace); + } + + $dom->appendChild($root); + $node->getNotificationType()->serializeBody($this->server, $root); + + $this->server->httpResponse->setHeader('Content-Type','application/xml'); + $this->server->httpResponse->setHeader('ETag',$node->getETag()); + $this->server->httpResponse->sendStatus(200); + $this->server->httpResponse->sendBody($dom->saveXML()); + + return false; } @@ -650,9 +784,10 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { * An exception is thrown if it's not. * * @param resource|string $data + * @param string $path * @return void */ - protected function validateICalendar(&$data) { + protected function validateICalendar(&$data, $path) { // If it's a stream, we convert it to a string first. if (is_resource($data)) { @@ -664,9 +799,9 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { try { - $vobj = Sabre_VObject_Reader::read($data); + $vobj = VObject\Reader::read($data); - } catch (Sabre_VObject_ParseException $e) { + } catch (VObject\ParseException $e) { throw new Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage()); @@ -676,6 +811,11 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { throw new Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support iCalendar objects.'); } + // Get the Supported Components for the target calendar + list($parentPath,$object) = Sabre_Dav_URLUtil::splitPath($path); + $calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set')); + $supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue(); + $foundType = null; $foundUID = null; foreach($vobj->getComponents() as $component) { @@ -687,6 +827,9 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { case 'VJOURNAL' : if (is_null($foundType)) { $foundType = $component->name; + if (!in_array($foundType, $supportedComponents)) { + throw new Sabre_CalDAV_Exception_InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType); + } if (!isset($component->UID)) { throw new Sabre_DAV_Exception_BadRequest('Every ' . $component->name . ' component must have an UID'); } @@ -711,12 +854,81 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { } /** - * This method handles POST requests to the schedule-outbox + * This method handles POST requests to the schedule-outbox. + * + * Currently, two types of requests are support: + * * FREEBUSY requests from RFC 6638 + * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04 + * + * The latter is from an expired early draft of the CalDAV scheduling + * extensions, but iCal depends on a feature from that spec, so we + * implement it. * * @param Sabre_CalDAV_Schedule_IOutbox $outboxNode + * @param string $outboxUri * @return void */ - public function outboxRequest(Sabre_CalDAV_Schedule_IOutbox $outboxNode) { + public function outboxRequest(Sabre_CalDAV_Schedule_IOutbox $outboxNode, $outboxUri) { + + // Parsing the request body + try { + $vObject = VObject\Reader::read($this->server->httpRequest->getBody(true)); + } catch (VObject\ParseException $e) { + throw new Sabre_DAV_Exception_BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage()); + } + + // The incoming iCalendar object must have a METHOD property, and a + // component. The combination of both determines what type of request + // this is. + $componentType = null; + foreach($vObject->getComponents() as $component) { + if ($component->name !== 'VTIMEZONE') { + $componentType = $component->name; + break; + } + } + if (is_null($componentType)) { + throw new Sabre_DAV_Exception_BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component'); + } + + // Validating the METHOD + $method = strtoupper((string)$vObject->METHOD); + if (!$method) { + throw new Sabre_DAV_Exception_BadRequest('A METHOD property must be specified in iTIP messages'); + } + + // So we support two types of requests: + // + // REQUEST with a VFREEBUSY component + // REQUEST, REPLY, ADD, CANCEL on VEVENT components + + $acl = $this->server->getPlugin('acl'); + + if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') { + + $acl && $acl->checkPrivileges($outboxUri,'{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}schedule-query-freebusy'); + $this->handleFreeBusyRequest($outboxNode, $vObject); + + } elseif ($componentType === 'VEVENT' && in_array($method, array('REQUEST','REPLY','ADD','CANCEL'))) { + + $acl && $acl->checkPrivileges($outboxUri,'{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}schedule-post-vevent'); + $this->handleEventNotification($outboxNode, $vObject); + + } else { + + throw new Sabre_DAV_Exception_NotImplemented('SabreDAV supports only VFREEBUSY (REQUEST) and VEVENT (REQUEST, REPLY, ADD, CANCEL)'); + + } + + } + + /** + * This method handles the REQUEST, REPLY, ADD and CANCEL methods for + * VEVENT iTip messages. + * + * @return void + */ + protected function handleEventNotification(Sabre_CalDAV_Schedule_IOutbox $outboxNode, VObject\Component $vObject) { $originator = $this->server->httpRequest->getHeader('Originator'); $recipients = $this->server->httpRequest->getHeader('Recipient'); @@ -760,38 +972,10 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { throw new Sabre_DAV_Exception_Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header'); } - try { - $vObject = Sabre_VObject_Reader::read($this->server->httpRequest->getBody(true)); - } catch (Sabre_VObject_ParseException $e) { - throw new Sabre_DAV_Exception_BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage()); - } - - // Checking for the object type - $componentType = null; - foreach($vObject->getComponents() as $component) { - if ($component->name !== 'VTIMEZONE') { - $componentType = $component->name; - break; - } - } - if (is_null($componentType)) { - throw new Sabre_DAV_Exception_BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component'); - } - - // Validating the METHOD - $method = strtoupper((string)$vObject->METHOD); - if (!$method) { - throw new Sabre_DAV_Exception_BadRequest('A METHOD property must be specified in iTIP messages'); - } - - if (in_array($method, array('REQUEST','REPLY','ADD','CANCEL')) && $componentType==='VEVENT') { - $result = $this->iMIPMessage($originator, $recipients, $vObject); - $this->server->httpResponse->sendStatus(200); - $this->server->httpResponse->setHeader('Content-Type','application/xml'); - $this->server->httpResponse->sendBody($this->generateScheduleResponse($result)); - } else { - throw new Sabre_DAV_Exception_NotImplemented('This iTIP method is currently not implemented'); - } + $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal); + $this->server->httpResponse->sendStatus(200); + $this->server->httpResponse->setHeader('Content-Type','application/xml'); + $this->server->httpResponse->sendBody($this->generateScheduleResponse($result)); } @@ -813,15 +997,15 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { * * @param string $originator * @param array $recipients - * @param Sabre_VObject_Component $vObject + * @param Sabre\VObject\Component $vObject * @return array */ - protected function iMIPMessage($originator, array $recipients, Sabre_VObject_Component $vObject) { + protected function iMIPMessage($originator, array $recipients, VObject\Component $vObject, $principal) { if (!$this->imipHandler) { $resultStatus = '5.2;This server does not support this operation'; } else { - $this->imipHandler->sendMessage($originator, $recipients, $vObject); + $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal); $resultStatus = '2.0;Success'; } @@ -832,7 +1016,6 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { return $result; - } /** @@ -877,6 +1060,204 @@ class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin { } + /** + * This method is responsible for parsing a free-busy query request and + * returning it's result. + * + * @param Sabre_CalDAV_Schedule_IOutbox $outbox + * @param string $request + * @return string + */ + protected function handleFreeBusyRequest(Sabre_CalDAV_Schedule_IOutbox $outbox, VObject\Component $vObject) { + + $vFreeBusy = $vObject->VFREEBUSY; + $organizer = $vFreeBusy->organizer; + + $organizer = (string)$organizer; + + // Validating if the organizer matches the owner of the inbox. + $owner = $outbox->getOwner(); + + $caldavNS = '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}'; + + $uas = $caldavNS . 'calendar-user-address-set'; + $props = $this->server->getProperties($owner,array($uas)); + + if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) { + throw new Sabre_DAV_Exception_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 Sabre_DAV_Exception_BadRequest('You must at least specify 1 attendee'); + } + + $attendees = array(); + foreach($vFreeBusy->ATTENDEE as $attendee) { + $attendees[]= (string)$attendee; + } + + + if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) { + throw new Sabre_DAV_Exception_BadRequest('DTSTART and DTEND must both be specified'); + } + + $startRange = $vFreeBusy->DTSTART->getDateTime(); + $endRange = $vFreeBusy->DTEND->getDateTime(); + + $results = array(); + 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->xmlNamespaces as $namespace=>$prefix) { + + $scheduleResponse->setAttribute('xmlns:' . $prefix,$namespace); + + } + $dom->appendChild($scheduleResponse); + + foreach($results as $result) { + $response = $dom->createElement('cal:response'); + + $recipient = $dom->createElement('cal:recipient'); + $recipientHref = $dom->createElement('d:href'); + + $recipientHref->appendChild($dom->createTextNode($result['href'])); + $recipient->appendChild($recipientHref); + $response->appendChild($recipient); + + $reqStatus = $dom->createElement('cal:request-status'); + $reqStatus->appendChild($dom->createTextNode($result['request-status'])); + $response->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()))); + $response->appendChild($calendardata); + + } + $scheduleResponse->appendChild($response); + } + + $this->server->httpResponse->sendStatus(200); + $this->server->httpResponse->setHeader('Content-Type','application/xml'); + $this->server->httpResponse->sendBody($dom->saveXML()); + + } + + /** + * Returns free-busy information for a specific address. The returned + * data is an array containing the following properties: + * + * calendar-data : A VFREEBUSY VObject + * request-status : an iTip status code. + * href: The principal's email address, as requested + * + * The following request status codes may be returned: + * * 2.0;description + * * 3.7;description + * + * @param string $email address + * @param DateTime $start + * @param DateTime $end + * @param Sabre_VObject_Component $request + * @return Sabre_VObject_Component + */ + protected function getFreeBusyForEmail($email, DateTime $start, DateTime $end, VObject\Component $request) { + + $caldavNS = '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}'; + + $aclPlugin = $this->server->getPlugin('acl'); + if (substr($email,0,7)==='mailto:') $email = substr($email,7); + + $result = $aclPlugin->principalSearch( + array('{http://sabredav.org/ns}email-address' => $email), + array( + '{DAV:}principal-URL', $caldavNS . 'calendar-home-set', + '{http://sabredav.org/ns}email-address', + ) + ); + + if (!count($result)) { + return array( + 'request-status' => '3.7;Could not find principal', + 'href' => 'mailto:' . $email, + ); + } + + if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) { + return array( + 'request-status' => '3.7;No calendar-home-set property found', + 'href' => 'mailto:' . $email, + ); + } + $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref(); + + // Grabbing the calendar list + $objects = array(); + foreach($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) { + if (!$node instanceof Sabre_CalDAV_ICalendar) { + continue; + } + $aclPlugin->checkPrivileges($homeSet . $node->getName() ,$caldavNS . 'read-free-busy'); + + // Getting the list of object uris within the time-range + $urls = $node->calendarQuery(array( + 'name' => 'VCALENDAR', + 'comp-filters' => array( + array( + 'name' => 'VEVENT', + 'comp-filters' => array(), + 'prop-filters' => array(), + 'is-not-defined' => false, + 'time-range' => array( + 'start' => $start, + 'end' => $end, + ), + ), + ), + 'prop-filters' => array(), + '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); + + } + + $vcalendar = VObject\Component::create('VCALENDAR'); + $vcalendar->VERSION = '2.0'; + $vcalendar->METHOD = 'REPLY'; + $vcalendar->CALSCALE = 'GREGORIAN'; + $vcalendar->PRODID = '-//SabreDAV//SabreDAV ' . Sabre_DAV_Version::VERSION . '//EN'; + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($start, $end); + $generator->setBaseObject($vcalendar); + + $result = $generator->getResult(); + + $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email; + $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID; + $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER; + + return array( + 'calendar-data' => $result, + 'request-status' => '2.0;Success', + 'href' => 'mailto:' . $email, + ); + } + /** * This method is used to generate HTML output for the * Sabre_DAV_Browser_Plugin. This allows us to generate an interface users diff --git a/3rdparty/Sabre/CalDAV/Principal/Collection.php b/3rdparty/Sabre/CalDAV/Principal/Collection.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/Principal/ProxyRead.php b/3rdparty/Sabre/CalDAV/Principal/ProxyRead.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/Principal/ProxyWrite.php b/3rdparty/Sabre/CalDAV/Principal/ProxyWrite.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/Principal/User.php b/3rdparty/Sabre/CalDAV/Principal/User.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/Property/AllowedSharingModes.php b/3rdparty/Sabre/CalDAV/Property/AllowedSharingModes.php new file mode 100644 index 0000000000..efe751732c --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Property/AllowedSharingModes.php @@ -0,0 +1,72 @@ +canBeShared = $canBeShared; + $this->canBePublished = $canBePublished; + + } + + /** + * Serializes the property in a DOMDocument + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $node) { + + $doc = $node->ownerDocument; + if ($this->canBeShared) { + $xcomp = $doc->createElement('cs:can-be-shared'); + $node->appendChild($xcomp); + } + if ($this->canBePublished) { + $xcomp = $doc->createElement('cs:can-be-published'); + $node->appendChild($xcomp); + } + + } + +} diff --git a/3rdparty/Sabre/CalDAV/Property/Invite.php b/3rdparty/Sabre/CalDAV/Property/Invite.php new file mode 100644 index 0000000000..4ed94877df --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Property/Invite.php @@ -0,0 +1,173 @@ +users = $users; + + } + + /** + * Returns the list of users, as it was passed to the constructor. + * + * @return array + */ + public function getValue() { + + return $this->users; + + } + + /** + * Serializes the property in a DOMDocument + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $node) { + + $doc = $node->ownerDocument; + foreach($this->users as $user) { + + $xuser = $doc->createElement('cs:user'); + + $href = $doc->createElement('d:href'); + $href->appendChild($doc->createTextNode($user['href'])); + $xuser->appendChild($href); + + if (isset($user['commonName']) && $user['commonName']) { + $commonName = $doc->createElement('cs:common-name'); + $commonName->appendChild($doc->createTextNode($user['commonName'])); + $xuser->appendChild($commonName); + } + + switch($user['status']) { + + case SharingPlugin::STATUS_ACCEPTED : + $status = $doc->createElement('cs:invite-accepted'); + $xuser->appendChild($status); + break; + case SharingPlugin::STATUS_DECLINED : + $status = $doc->createElement('cs:invite-declined'); + $xuser->appendChild($status); + break; + case SharingPlugin::STATUS_NORESPONSE : + $status = $doc->createElement('cs:invite-noresponse'); + $xuser->appendChild($status); + break; + case SharingPlugin::STATUS_INVALID : + $status = $doc->createElement('cs:invite-invalid'); + $xuser->appendChild($status); + break; + + } + + $xaccess = $doc->createElement('cs:access'); + + if ($user['readOnly']) { + $xaccess->appendChild( + $doc->createElement('cs:read') + ); + } else { + $xaccess->appendChild( + $doc->createElement('cs:read-write') + ); + } + $xuser->appendChild($xaccess); + + if (isset($user['summary']) && $user['summary']) { + $summary = $doc->createElement('cs:summary'); + $summary->appendChild($doc->createTextNode($user['summary'])); + $xuser->appendChild($summary); + } + + $node->appendChild($xuser); + + } + + } + + /** + * Unserializes the property. + * + * This static method should return a an instance of this object. + * + * @param DOMElement $prop + * @return Sabre_DAV_IProperty + */ + static function unserialize(DOMElement $prop) { + + $xpath = new \DOMXPath($prop->ownerDocument); + $xpath->registerNamespace('cs', Sabre_CalDAV_Plugin::NS_CALENDARSERVER); + $xpath->registerNamespace('d', 'DAV:'); + + $users = array(); + + foreach($xpath->query('cs:user', $prop) as $user) { + + $status = null; + if ($xpath->evaluate('boolean(cs:invite-accepted)', $user)) { + $status = SharingPlugin::STATUS_ACCEPTED; + } elseif ($xpath->evaluate('boolean(cs:invite-declined)', $user)) { + $status = SharingPlugin::STATUS_DECLINED; + } elseif ($xpath->evaluate('boolean(cs:invite-noresponse)', $user)) { + $status = SharingPlugin::STATUS_NORESPONSE; + } elseif ($xpath->evaluate('boolean(cs:invite-invalid)', $user)) { + $status = SharingPlugin::STATUS_INVALID; + } else { + throw new Sabre_DAV_Exception('Every cs:user property must have one of cs:invite-accepted, cs:invite-declined, cs:invite-noresponse or cs:invite-invalid'); + } + $users[] = array( + 'href' => $xpath->evaluate('string(d:href)', $user), + 'commonName' => $xpath->evaluate('string(cs:common-name)', $user), + 'readOnly' => $xpath->evaluate('boolean(cs:access/cs:read)', $user), + 'summary' => $xpath->evaluate('string(cs:summary)', $user), + 'status' => $status, + ); + + } + + return new self($users); + + } + +} diff --git a/3rdparty/Sabre/CalDAV/Property/ScheduleCalendarTransp.php b/3rdparty/Sabre/CalDAV/Property/ScheduleCalendarTransp.php new file mode 100644 index 0000000000..76c1dbaec2 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/Property/ScheduleCalendarTransp.php @@ -0,0 +1,99 @@ +value = $value; + + } + + /** + * Returns the current value + * + * @return string + */ + public function getValue() { + + return $this->value; + + } + + /** + * Serializes the property in a DOMDocument + * + * @param Sabre_DAV_Server $server + * @param DOMElement $node + * @return void + */ + public function serialize(Sabre_DAV_Server $server,DOMElement $node) { + + $doc = $node->ownerDocument; + switch($this->value) { + case self::TRANSPARENT : + $xval = $doc->createElement('cal:transparent'); + break; + case self::OPAQUE : + $xval = $doc->createElement('cal:opaque'); + break; + } + + $node->appendChild($xval); + + } + + /** + * Unserializes the DOMElement back into a Property class. + * + * @param DOMElement $node + * @return Sabre_CalDAV_Property_ScheduleCalendarTransp + */ + static function unserialize(DOMElement $node) { + + $value = null; + foreach($node->childNodes as $childNode) { + switch(Sabre_DAV_XMLUtil::toClarkNotation($childNode)) { + case '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}opaque' : + $value = self::OPAQUE; + break; + case '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}transparent' : + $value = self::TRANSPARENT; + break; + } + } + if (is_null($value)) + return null; + + return new self($value); + + } +} diff --git a/3rdparty/Sabre/CalDAV/Property/SupportedCalendarComponentSet.php b/3rdparty/Sabre/CalDAV/Property/SupportedCalendarComponentSet.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/Property/SupportedCalendarData.php b/3rdparty/Sabre/CalDAV/Property/SupportedCalendarData.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/Property/SupportedCollationSet.php b/3rdparty/Sabre/CalDAV/Property/SupportedCollationSet.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/Schedule/IMip.php b/3rdparty/Sabre/CalDAV/Schedule/IMip.php old mode 100755 new mode 100644 index 37e75fcc4a..92c3c097c1 --- a/3rdparty/Sabre/CalDAV/Schedule/IMip.php +++ b/3rdparty/Sabre/CalDAV/Schedule/IMip.php @@ -1,5 +1,7 @@ diff --git a/3rdparty/Sabre/CalDAV/Schedule/IOutbox.php b/3rdparty/Sabre/CalDAV/Schedule/IOutbox.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CalDAV/Schedule/Outbox.php b/3rdparty/Sabre/CalDAV/Schedule/Outbox.php old mode 100755 new mode 100644 index 014c37230d..462aa527e2 --- a/3rdparty/Sabre/CalDAV/Schedule/Outbox.php +++ b/3rdparty/Sabre/CalDAV/Schedule/Outbox.php @@ -13,7 +13,7 @@ * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ -class Sabre_CalDAV_Schedule_Outbox extends Sabre_DAV_Directory implements Sabre_CalDAV_Schedule_IOutbox { +class Sabre_CalDAV_Schedule_Outbox extends Sabre_DAV_Collection implements Sabre_CalDAV_Schedule_IOutbox { /** * The principal Uri @@ -103,6 +103,11 @@ class Sabre_CalDAV_Schedule_Outbox extends Sabre_DAV_Directory implements Sabre_ 'principal' => $this->getOwner(), 'protected' => true, ), + array( + 'privilege' => '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}schedule-post-vevent', + 'principal' => $this->getOwner(), + 'protected' => true, + ), array( 'privilege' => '{DAV:}read', 'principal' => $this->getOwner(), @@ -144,6 +149,9 @@ class Sabre_CalDAV_Schedule_Outbox extends Sabre_DAV_Directory implements Sabre_ $default['aggregates'][] = array( 'privilege' => '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}schedule-query-freebusy', ); + $default['aggregates'][] = array( + 'privilege' => '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}schedule-post-vevent', + ); return $default; diff --git a/3rdparty/Sabre/CalDAV/Server.php b/3rdparty/Sabre/CalDAV/Server.php deleted file mode 100755 index 325e3d80a7..0000000000 --- a/3rdparty/Sabre/CalDAV/Server.php +++ /dev/null @@ -1,68 +0,0 @@ -authRealm); - $this->addPlugin($authPlugin); - - $aclPlugin = new Sabre_DAVACL_Plugin(); - $this->addPlugin($aclPlugin); - - $caldavPlugin = new Sabre_CalDAV_Plugin(); - $this->addPlugin($caldavPlugin); - - } - -} diff --git a/3rdparty/Sabre/CalDAV/ShareableCalendar.php b/3rdparty/Sabre/CalDAV/ShareableCalendar.php new file mode 100644 index 0000000000..0e44885c62 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/ShareableCalendar.php @@ -0,0 +1,72 @@ +caldavBackend->updateShares($this->calendarInfo['id'], $add, $remove); + + } + + /** + * Returns the list of people whom this calendar is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre_CalDAV_SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + public function getShares() { + + return $this->caldavBackend->getShares($this->calendarInfo['id']); + + } + + /** + * Marks this calendar as published. + * + * Publishing a calendar should automatically create a read-only, public, + * subscribable calendar. + * + * @param bool $value + * @return void + */ + public function setPublishStatus($value) { + + $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value); + + } + +} diff --git a/3rdparty/Sabre/CalDAV/SharedCalendar.php b/3rdparty/Sabre/CalDAV/SharedCalendar.php new file mode 100644 index 0000000000..9000697d3e --- /dev/null +++ b/3rdparty/Sabre/CalDAV/SharedCalendar.php @@ -0,0 +1,98 @@ +calendarInfo['{http://calendarserver.org/ns/}shared-url']; + + } + + /** + * 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['{http://sabredav.org/ns}owner-principal']; + + } + + /** + * 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() { + + // The top-level ACL only contains access information for the true + // owner of the calendar, so we need to add the information for the + // sharee. + $acl = parent::getACL(); + $acl[] = array( + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ); + if (!$this->calendarInfo['{http://sabredav.org/ns}read-only']) { + $acl[] = array( + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ); + } + return $acl; + + } + + +} diff --git a/3rdparty/Sabre/CalDAV/SharingPlugin.php b/3rdparty/Sabre/CalDAV/SharingPlugin.php new file mode 100644 index 0000000000..31df8057b2 --- /dev/null +++ b/3rdparty/Sabre/CalDAV/SharingPlugin.php @@ -0,0 +1,475 @@ +server = $server; + //$server->resourceTypeMapping['Sabre_CalDAV_IShareableCalendar'] = '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}shared-owner'; + $server->resourceTypeMapping['Sabre_CalDAV_ISharedCalendar'] = '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}shared'; + + array_push( + $this->server->protectedProperties, + '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}invite', + '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', + '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}shared-url' + ); + + $this->server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties')); + $this->server->subscribeEvent('afterGetProperties', array($this, 'afterGetProperties')); + $this->server->subscribeEvent('updateProperties', array($this, 'updateProperties')); + $this->server->subscribeEvent('unknownMethod', array($this,'unknownMethod')); + + } + + /** + * This event is triggered when properties are requested for a certain + * node. + * + * This allows us to inject any properties early. + * + * @param string $path + * @param Sabre_DAV_INode $node + * @param array $requestedProperties + * @param array $returnedProperties + * @return void + */ + public function beforeGetProperties($path, Sabre_DAV_INode $node, &$requestedProperties, &$returnedProperties) { + + if ($node instanceof Sabre_CalDAV_IShareableCalendar) { + if (($index = array_search('{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) { + + unset($requestedProperties[$index]); + $returnedProperties[200]['{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}invite'] = + new Sabre_CalDAV_Property_Invite( + $node->getShares() + ); + + } + + } + if ($node instanceof Sabre_CalDAV_ISharedCalendar) { + if (($index = array_search('{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}shared-url', $requestedProperties))!==false) { + + unset($requestedProperties[$index]); + $returnedProperties[200]['{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}shared-url'] = + new Sabre_DAV_Property_Href( + $node->getSharedUrl() + ); + + } + + } + + } + + /** + * This method is triggered *after* all properties have been retrieved. + * This allows us to inject the correct resourcetype for calendars that + * have been shared. + * + * @param string $path + * @param array $properties + * @param Sabre_DAV_INode $node + * @return void + */ + public function afterGetProperties($path, &$properties, Sabre_DAV_INode $node) { + + if ($node instanceof Sabre_CalDAV_IShareableCalendar) { + if (isset($properties[200]['{DAV:}resourcetype'])) { + if (count($node->getShares())>0) { + $properties[200]['{DAV:}resourcetype']->add( + '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}shared-owner' + ); + } + } + $propName = '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes'; + if (array_key_exists($propName, $properties[404])) { + unset($properties[404][$propName]); + $properties[200][$propName] = new Sabre_CalDAV_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 array $mutations + * @param array $result + * @param Sabre_DAV_INode $node + * @return void + */ + public function updateProperties(array &$mutations, array &$result, Sabre_DAV_INode $node) { + + if (!$node instanceof Sabre_CalDAV_IShareableCalendar) + return; + + if (!isset($mutations['{DAV:}resourcetype'])) { + return; + } + + // Only doing something if shared-owner is indeed not in the list. + if($mutations['{DAV:}resourcetype']->is('{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}shared-owner')) return; + + $shares = $node->getShares(); + $remove = array(); + foreach($shares as $share) { + $remove[] = $share['href']; + } + $node->updateShares(array(), $remove); + + // We're marking this update as 200 OK + $result[200]['{DAV:}resourcetype'] = null; + + // Removing it from the mutations list + unset($mutations['{DAV:}resourcetype']); + + } + + /** + * This event is triggered when the server didn't know how to handle a + * certain request. + * + * We intercept this to handle POST requests on calendars. + * + * @param string $method + * @param string $uri + * @return null|bool + */ + public function unknownMethod($method, $uri) { + + if ($method!=='POST') { + return; + } + + // Only handling xml + $contentType = $this->server->httpRequest->getHeader('Content-Type'); + if (strpos($contentType,'application/xml')===false && strpos($contentType,'text/xml')===false) + return; + + // Making sure the node exists + try { + $node = $this->server->tree->getNodeForPath($uri); + } catch (Sabre_DAV_Exception_NotFound $e) { + return; + } + + + $dom = Sabre_DAV_XMLUtil::loadDOMDocument($this->server->httpRequest->getBody(true)); + + $documentType = Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild); + + switch($documentType) { + + // Dealing with the 'share' document, which modified invitees on a + // calendar. + case '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}share' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof Sabre_CalDAV_IShareableCalendar) { + return; + } + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($uri, '{DAV:}write'); + } + + $mutations = $this->parseShareRequest($dom); + + $node->updateShares($mutations[0], $mutations[1]); + + $this->server->httpResponse->sendStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $this->server->httpResponse->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 '{'. Sabre_CalDAV_Plugin::NS_CALENDARSERVER.'}invite-reply' : + + // This only works on the calendar-home-root node. + if (!$node instanceof Sabre_CalDAV_UserCalendars) { + return; + } + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($uri, '{DAV:}write'); + } + + $message = $this->parseInviteReplyRequest($dom); + + $url = $node->shareReply( + $message['href'], + $message['status'], + $message['calendarUri'], + $message['inReplyTo'], + $message['summary'] + ); + + $this->server->httpResponse->sendStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well'); + + if ($url) { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + + $root = $dom->createElement('cs:shared-as'); + foreach($this->server->xmlNamespaces as $namespace => $prefix) { + $root->setAttribute('xmlns:' . $prefix, $namespace); + } + + $dom->appendChild($root); + $href = new Sabre_DAV_Property_Href($url); + + $href->serialize($this->server, $root); + $this->server->httpResponse->setHeader('Content-Type','application/xml'); + $this->server->httpResponse->sendBody($dom->saveXML()); + + } + + // Breaking the event chain + return false; + + case '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}publish-calendar' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof Sabre_CalDAV_IShareableCalendar) { + return; + } + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($uri, '{DAV:}write'); + } + + $node->setPublishStatus(true); + + // iCloud sends back the 202, so we will too. + $this->server->httpResponse->sendStatus(202); + + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + case '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}unpublish-calendar' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof Sabre_CalDAV_IShareableCalendar) { + return; + } + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($uri, '{DAV:}write'); + } + + $node->setPublishStatus(false); + + $this->server->httpResponse->sendStatus(200); + + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + } + + + } + + /** + * Parses the 'share' POST request. + * + * This method returns an array, containing two arrays. + * The first array is a list of new sharees. Every element is a struct + * containing a: + * * href element. (usually a mailto: address) + * * commonName element (often a first and lastname, but can also be + * false) + * * readOnly (true or false) + * * summary (A description of the share, can also be false) + * + * The second array is a list of sharees that are to be removed. This is + * just a simple array with 'hrefs'. + * + * @param DOMDocument $dom + * @return array + */ + protected function parseShareRequest(DOMDocument $dom) { + + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('cs', Sabre_CalDAV_Plugin::NS_CALENDARSERVER); + $xpath->registerNamespace('d', 'DAV:'); + + + $set = array(); + $elems = $xpath->query('cs:set'); + + for($i=0; $i < $elems->length; $i++) { + + $xset = $elems->item($i); + $set[] = array( + 'href' => $xpath->evaluate('string(d:href)', $xset), + 'commonName' => $xpath->evaluate('string(cs:common-name)', $xset), + 'summary' => $xpath->evaluate('string(cs:summary)', $xset), + 'readOnly' => $xpath->evaluate('boolean(cs:read)', $xset)!==false + ); + + } + + $remove = array(); + $elems = $xpath->query('cs:remove'); + + for($i=0; $i < $elems->length; $i++) { + + $xremove = $elems->item($i); + $remove[] = $xpath->evaluate('string(d:href)', $xremove); + + } + + return array($set, $remove); + + } + + /** + * Parses the 'invite-reply' POST request. + * + * This method returns an array, containing the following properties: + * * href - The sharee who is replying + * * status - One of the self::STATUS_* constants + * * calendarUri - The url of the shared calendar + * * inReplyTo - The unique id of the share invitation. + * * summary - Optional description of the reply. + * + * @param DOMDocument $dom + * @return array + */ + protected function parseInviteReplyRequest(DOMDocument $dom) { + + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('cs', Sabre_CalDAV_Plugin::NS_CALENDARSERVER); + $xpath->registerNamespace('d', 'DAV:'); + + $hostHref = $xpath->evaluate('string(cs:hosturl/d:href)'); + if (!$hostHref) { + throw new Sabre_DAV_Exception_BadRequest('The {' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}hosturl/{DAV:}href element is required'); + } + + return array( + 'href' => $xpath->evaluate('string(d:href)'), + 'calendarUri' => $this->server->calculateUri($hostHref), + 'inReplyTo' => $xpath->evaluate('string(cs:in-reply-to)'), + 'summary' => $xpath->evaluate('string(cs:summary)'), + 'status' => $xpath->evaluate('boolean(cs:invite-accepted)')?self::STATUS_ACCEPTED:self::STATUS_DECLINED + ); + + } + +} diff --git a/3rdparty/Sabre/CalDAV/UserCalendars.php b/3rdparty/Sabre/CalDAV/UserCalendars.php old mode 100755 new mode 100644 index b8d3f0573f..3194e6677a --- a/3rdparty/Sabre/CalDAV/UserCalendars.php +++ b/3rdparty/Sabre/CalDAV/UserCalendars.php @@ -6,7 +6,7 @@ * @package Sabre * @subpackage CalDAV * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved. - * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ class Sabre_CalDAV_UserCalendars implements Sabre_DAV_IExtendedCollection, Sabre_DAVACL_IACL { @@ -21,7 +21,7 @@ class Sabre_CalDAV_UserCalendars implements Sabre_DAV_IExtendedCollection, Sabre /** * CalDAV backend * - * @var Sabre_CalDAV_Backend_Abstract + * @var Sabre_CalDAV_Backend_BackendInterface */ protected $caldavBackend; @@ -36,10 +36,10 @@ class Sabre_CalDAV_UserCalendars implements Sabre_DAV_IExtendedCollection, Sabre * Constructor * * @param Sabre_DAVACL_IPrincipalBackend $principalBackend - * @param Sabre_CalDAV_Backend_Abstract $caldavBackend + * @param Sabre_CalDAV_Backend_BackendInterface $caldavBackend * @param mixed $userUri */ - public function __construct(Sabre_DAVACL_IPrincipalBackend $principalBackend, Sabre_CalDAV_Backend_Abstract $caldavBackend, $userUri) { + public function __construct(Sabre_DAVACL_IPrincipalBackend $principalBackend, Sabre_CalDAV_Backend_BackendInterface $caldavBackend, $userUri) { $this->principalBackend = $principalBackend; $this->caldavBackend = $caldavBackend; @@ -168,9 +168,22 @@ class Sabre_CalDAV_UserCalendars implements Sabre_DAV_IExtendedCollection, Sabre $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); $objs = array(); foreach($calendars as $calendar) { - $objs[] = new Sabre_CalDAV_Calendar($this->principalBackend, $this->caldavBackend, $calendar); + if ($this->caldavBackend instanceof Sabre_CalDAV_Backend_SharingSupport) { + if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) { + $objs[] = new Sabre_CalDAV_SharedCalendar($this->principalBackend, $this->caldavBackend, $calendar); + } else { + $objs[] = new Sabre_CalDAV_ShareableCalendar($this->principalBackend, $this->caldavBackend, $calendar); + } + } else { + $objs[] = new Sabre_CalDAV_Calendar($this->principalBackend, $this->caldavBackend, $calendar); + } } $objs[] = new Sabre_CalDAV_Schedule_Outbox($this->principalInfo['uri']); + + // We're adding a notifications node, if it's supported by the backend. + if ($this->caldavBackend instanceof Sabre_CalDAV_Backend_NotificationSupport) { + $objs[] = new Sabre_CalDAV_Notifications_Collection($this->caldavBackend, $this->principalInfo['uri']); + } return $objs; } @@ -185,8 +198,22 @@ class Sabre_CalDAV_UserCalendars implements Sabre_DAV_IExtendedCollection, Sabre */ public function createExtendedCollection($name, array $resourceType, array $properties) { - if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar',$resourceType) || count($resourceType)!==2) { - throw new Sabre_DAV_Exception_InvalidResourceType('Unknown resourceType for this collection'); + $isCalendar = false; + foreach($resourceType 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; + default : + throw new Sabre_DAV_Exception_InvalidResourceType('Unknown resourceType: ' . $rt); + } + } + if (!$isCalendar) { + throw new Sabre_DAV_Exception_InvalidResourceType('You can only create calendars in this collection'); } $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties); @@ -295,4 +322,27 @@ class Sabre_CalDAV_UserCalendars implements Sabre_DAV_IExtendedCollection, Sabre } + /** + * This method is called when a user replied to a request to share. + * + * This method should return the url of the newly created calendar if the + * share was accepted. + * + * @param string href The sharee who is replying (often a mailto: address) + * @param int status One of the SharingPlugin::STATUS_* constants + * @param string $calendarUri The url to the calendar thats being shared + * @param string $inReplyTo The unique id this message is a response to + * @param string $summary A description of the reply + * @return null|string + */ + public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) { + + if (!$this->caldavBackend instanceof Sabre_CalDAV_Backend_SharingSupport) { + throw new Sabre_DAV_Exception_NotImplemented('Sharing support is not implemented by this backend.'); + } + + return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary); + + } + } diff --git a/3rdparty/Sabre/CalDAV/Version.php b/3rdparty/Sabre/CalDAV/Version.php old mode 100755 new mode 100644 index ace9901c08..0ad14fa086 --- a/3rdparty/Sabre/CalDAV/Version.php +++ b/3rdparty/Sabre/CalDAV/Version.php @@ -14,7 +14,7 @@ class Sabre_CalDAV_Version { /** * Full version number */ - const VERSION = '1.6.4'; + const VERSION = '1.7.0'; /** * Stability : alpha, beta, stable diff --git a/3rdparty/Sabre/CalDAV/includes.php b/3rdparty/Sabre/CalDAV/includes.php old mode 100755 new mode 100644 index 1ecb870a0e..8b38e398f6 --- a/3rdparty/Sabre/CalDAV/includes.php +++ b/3rdparty/Sabre/CalDAV/includes.php @@ -16,28 +16,47 @@ */ // Begin includes -include __DIR__ . '/Backend/Abstract.php'; -include __DIR__ . '/Backend/PDO.php'; +include __DIR__ . '/Backend/BackendInterface.php'; +include __DIR__ . '/Backend/NotificationSupport.php'; +include __DIR__ . '/Backend/SharingSupport.php'; include __DIR__ . '/CalendarQueryParser.php'; include __DIR__ . '/CalendarQueryValidator.php'; include __DIR__ . '/CalendarRootNode.php'; +include __DIR__ . '/Exception/InvalidComponentType.php'; include __DIR__ . '/ICalendar.php'; include __DIR__ . '/ICalendarObject.php'; include __DIR__ . '/ICSExportPlugin.php'; +include __DIR__ . '/IShareableCalendar.php'; +include __DIR__ . '/ISharedCalendar.php'; +include __DIR__ . '/Notifications/ICollection.php'; +include __DIR__ . '/Notifications/INode.php'; +include __DIR__ . '/Notifications/INotificationType.php'; +include __DIR__ . '/Notifications/Node.php'; +include __DIR__ . '/Notifications/Notification/Invite.php'; +include __DIR__ . '/Notifications/Notification/InviteReply.php'; +include __DIR__ . '/Notifications/Notification/SystemStatus.php'; include __DIR__ . '/Plugin.php'; include __DIR__ . '/Principal/Collection.php'; include __DIR__ . '/Principal/ProxyRead.php'; include __DIR__ . '/Principal/ProxyWrite.php'; include __DIR__ . '/Principal/User.php'; +include __DIR__ . '/Property/AllowedSharingModes.php'; +include __DIR__ . '/Property/Invite.php'; +include __DIR__ . '/Property/ScheduleCalendarTransp.php'; include __DIR__ . '/Property/SupportedCalendarComponentSet.php'; include __DIR__ . '/Property/SupportedCalendarData.php'; include __DIR__ . '/Property/SupportedCollationSet.php'; include __DIR__ . '/Schedule/IMip.php'; include __DIR__ . '/Schedule/IOutbox.php'; include __DIR__ . '/Schedule/Outbox.php'; -include __DIR__ . '/Server.php'; +include __DIR__ . '/SharingPlugin.php'; include __DIR__ . '/UserCalendars.php'; include __DIR__ . '/Version.php'; +include __DIR__ . '/Backend/Abstract.php'; +include __DIR__ . '/Backend/PDO.php'; include __DIR__ . '/Calendar.php'; include __DIR__ . '/CalendarObject.php'; +include __DIR__ . '/Notifications/Collection.php'; +include __DIR__ . '/ShareableCalendar.php'; +include __DIR__ . '/SharedCalendar.php'; // End includes diff --git a/3rdparty/Sabre/CardDAV/AddressBook.php b/3rdparty/Sabre/CardDAV/AddressBook.php old mode 100755 new mode 100644 index 12297175a8..8d545114d9 --- a/3rdparty/Sabre/CardDAV/AddressBook.php +++ b/3rdparty/Sabre/CardDAV/AddressBook.php @@ -55,7 +55,7 @@ class Sabre_CardDAV_AddressBook extends Sabre_DAV_Collection implements Sabre_Ca * Returns a card * * @param string $name - * @return Sabre_DAV_Card + * @return Sabre_CardDAV_ICard */ public function getChild($name) { @@ -104,7 +104,7 @@ class Sabre_CardDAV_AddressBook extends Sabre_DAV_Collection implements Sabre_Ca * * @param string $name * @param resource $vcardData - * @return void|null + * @return string|null */ public function createFile($name,$vcardData = null) { diff --git a/3rdparty/Sabre/CardDAV/AddressBookQueryParser.php b/3rdparty/Sabre/CardDAV/AddressBookQueryParser.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/AddressBookRoot.php b/3rdparty/Sabre/CardDAV/AddressBookRoot.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/Backend/Abstract.php b/3rdparty/Sabre/CardDAV/Backend/Abstract.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/Backend/PDO.php b/3rdparty/Sabre/CardDAV/Backend/PDO.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/Card.php b/3rdparty/Sabre/CardDAV/Card.php old mode 100755 new mode 100644 index d7c6633383..0e35d321eb --- a/3rdparty/Sabre/CardDAV/Card.php +++ b/3rdparty/Sabre/CardDAV/Card.php @@ -6,7 +6,7 @@ * @package Sabre * @subpackage CardDAV * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved. - * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ class Sabre_CardDAV_Card extends Sabre_DAV_File implements Sabre_CardDAV_ICard, Sabre_DAVACL_IACL { @@ -78,7 +78,7 @@ class Sabre_CardDAV_Card extends Sabre_DAV_File implements Sabre_CardDAV_ICard, * Updates the VCard-formatted object * * @param string $cardData - * @return void + * @return string|null */ public function put($cardData) { @@ -114,7 +114,7 @@ class Sabre_CardDAV_Card extends Sabre_DAV_File implements Sabre_CardDAV_ICard, */ public function getContentType() { - return 'text/x-vcard'; + return 'text/x-vcard; charset=utf-8'; } @@ -128,7 +128,13 @@ class Sabre_CardDAV_Card extends Sabre_DAV_File implements Sabre_CardDAV_ICard, if (isset($this->cardData['etag'])) { return $this->cardData['etag']; } else { - return '"' . md5($this->get()) . '"'; + $data = $this->get(); + if (is_string($data)) { + return '"' . md5($data) . '"'; + } else { + // We refuse to calculate the md5 if it's a stream. + return null; + } } } @@ -136,7 +142,7 @@ class Sabre_CardDAV_Card extends Sabre_DAV_File implements Sabre_CardDAV_ICard, /** * Returns the last modification date as a unix timestamp * - * @return time + * @return int */ public function getLastModified() { diff --git a/3rdparty/Sabre/CardDAV/IAddressBook.php b/3rdparty/Sabre/CardDAV/IAddressBook.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/ICard.php b/3rdparty/Sabre/CardDAV/ICard.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/IDirectory.php b/3rdparty/Sabre/CardDAV/IDirectory.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/Plugin.php b/3rdparty/Sabre/CardDAV/Plugin.php old mode 100755 new mode 100644 index 96def6dd96..12bccaec4f --- a/3rdparty/Sabre/CardDAV/Plugin.php +++ b/3rdparty/Sabre/CardDAV/Plugin.php @@ -1,5 +1,7 @@ subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties')); + $server->subscribeEvent('afterGetProperties', array($this, 'afterGetProperties')); $server->subscribeEvent('updateProperties', array($this, 'updateProperties')); $server->subscribeEvent('report', array($this,'report')); $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel')); @@ -153,10 +156,6 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { if (is_resource($val)) $val = stream_get_contents($val); - // Taking out \r to not screw up the xml output - //$returnedProperties[200][$addressDataProp] = str_replace("\r","", $val); - // The stripping of \r breaks the Mail App in OSX Mountain Lion - // this is fixed in master, but not backported. /Tanghus $returnedProperties[200][$addressDataProp] = $val; } @@ -190,7 +189,7 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { * @param array $mutations * @param array $result * @param Sabre_DAV_INode $node - * @return void + * @return bool */ public function updateProperties(&$mutations, &$result, $node) { @@ -272,7 +271,7 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)); - $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href'); + $hrefElems = $dom->getElementsByTagNameNS('DAV:','href'); $propertyList = array(); foreach($hrefElems as $elem) { @@ -282,9 +281,12 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { } + $prefer = $this->server->getHTTPPRefer(); + $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); - $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList)); + $this->server->httpResponse->setHeader('Vary','Brief,Prefer'); + $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList, $prefer['return-minimal'])); } @@ -348,9 +350,9 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { try { - $vobj = Sabre_VObject_Reader::read($data); + $vobj = VObject\Reader::read($data); - } catch (Sabre_VObject_ParseException $e) { + } catch (VObject\ParseException $e) { throw new Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage()); @@ -360,6 +362,10 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { throw new Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support vcard objects.'); } + if (!isset($vobj->UID)) { + throw new Sabre_DAV_Exception_BadRequest('Every vcard must have an UID.'); + } + } @@ -424,9 +430,12 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { } + $prefer = $this->server->getHTTPPRefer(); + $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); - $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result)); + $this->server->httpResponse->setHeader('Vary','Brief,Prefer'); + $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal'])); } @@ -440,7 +449,7 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { */ public function validateFilters($vcardData, array $filters, $test) { - $vcard = Sabre_VObject_Reader::read($vcardData); + $vcard = VObject\Reader::read($vcardData); if (!$filters) return true; @@ -615,6 +624,30 @@ class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin { } + /** + * This event is triggered after webdav-properties have been retrieved. + * + * @return bool + */ + public function afterGetProperties($uri, &$properties) { + + // 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 (!isset($properties[200]['{DAV:}getcontenttype'])) + return; + + if (strpos($this->server->httpRequest->getHeader('User-Agent'),'Thunderbird')===false) { + return; + } + + if (strpos($properties[200]['{DAV:}getcontenttype'],'text/x-vcard')===0) { + $properties[200]['{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 diff --git a/3rdparty/Sabre/CardDAV/Property/SupportedAddressData.php b/3rdparty/Sabre/CardDAV/Property/SupportedAddressData.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/UserAddressBooks.php b/3rdparty/Sabre/CardDAV/UserAddressBooks.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/CardDAV/VCFExportPlugin.php b/3rdparty/Sabre/CardDAV/VCFExportPlugin.php new file mode 100644 index 0000000000..8850fef8af --- /dev/null +++ b/3rdparty/Sabre/CardDAV/VCFExportPlugin.php @@ -0,0 +1,107 @@ +server = $server; + $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'), 90); + + } + + /** + * 'beforeMethod' event handles. This event handles intercepts GET requests ending + * with ?export + * + * @param string $method + * @param string $uri + * @return bool + */ + public function beforeMethod($method, $uri) { + + if ($method!='GET') return; + if ($this->server->httpRequest->getQueryString()!='export') return; + + // splitting uri + list($uri) = explode('?',$uri,2); + + $node = $this->server->tree->getNodeForPath($uri); + + if (!($node instanceof Sabre_CardDAV_IAddressBook)) return; + + // Checking ACL, if available. + if ($aclPlugin = $this->server->getPlugin('acl')) { + $aclPlugin->checkPrivileges($uri, '{DAV:}read'); + } + + $this->server->httpResponse->setHeader('Content-Type','text/directory'); + $this->server->httpResponse->sendStatus(200); + + $nodes = $this->server->getPropertiesForPath($uri, array( + '{' . Sabre_CardDAV_Plugin::NS_CARDDAV . '}address-data', + ),1); + + $this->server->httpResponse->sendBody($this->generateVCF($nodes)); + + // Returning false to break the event chain + return false; + + } + + /** + * Merges all vcard objects, and builds one big vcf export + * + * @param array $nodes + * @return string + */ + public function generateVCF(array $nodes) { + + $output = ""; + + foreach($nodes as $node) { + + if (!isset($node[200]['{' . Sabre_CardDAV_Plugin::NS_CARDDAV . '}address-data'])) { + continue; + } + $nodeData = $node[200]['{' . Sabre_CardDAV_Plugin::NS_CARDDAV . '}address-data']; + + // Parsing this node so VObject can clean up the output. + $output .= + VObject\Reader::read($nodeData)->serialize(); + + } + + return $output; + + } + +} diff --git a/3rdparty/Sabre/CardDAV/Version.php b/3rdparty/Sabre/CardDAV/Version.php old mode 100755 new mode 100644 index d0623f0d3e..6b70a2df9b --- a/3rdparty/Sabre/CardDAV/Version.php +++ b/3rdparty/Sabre/CardDAV/Version.php @@ -16,7 +16,7 @@ class Sabre_CardDAV_Version { /** * Full version number */ - const VERSION = '1.6.3'; + const VERSION = '1.7.0'; /** * Stability : alpha, beta, stable diff --git a/3rdparty/Sabre/CardDAV/includes.php b/3rdparty/Sabre/CardDAV/includes.php old mode 100755 new mode 100644 index c3b8c04b07..08b4f76bd0 --- a/3rdparty/Sabre/CardDAV/includes.php +++ b/3rdparty/Sabre/CardDAV/includes.php @@ -26,6 +26,7 @@ include __DIR__ . '/IDirectory.php'; include __DIR__ . '/Plugin.php'; include __DIR__ . '/Property/SupportedAddressData.php'; include __DIR__ . '/UserAddressBooks.php'; +include __DIR__ . '/VCFExportPlugin.php'; include __DIR__ . '/Version.php'; include __DIR__ . '/AddressBook.php'; include __DIR__ . '/Card.php'; diff --git a/3rdparty/Sabre/DAV/Auth/Backend/AbstractBasic.php b/3rdparty/Sabre/DAV/Auth/Backend/AbstractBasic.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Auth/Backend/AbstractDigest.php b/3rdparty/Sabre/DAV/Auth/Backend/AbstractDigest.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Auth/Backend/Apache.php b/3rdparty/Sabre/DAV/Auth/Backend/Apache.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Auth/Backend/File.php b/3rdparty/Sabre/DAV/Auth/Backend/File.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Auth/Backend/PDO.php b/3rdparty/Sabre/DAV/Auth/Backend/PDO.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Auth/IBackend.php b/3rdparty/Sabre/DAV/Auth/IBackend.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Auth/Plugin.php b/3rdparty/Sabre/DAV/Auth/Plugin.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/GuessContentType.php b/3rdparty/Sabre/DAV/Browser/GuessContentType.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/MapGetToPropFind.php b/3rdparty/Sabre/DAV/Browser/MapGetToPropFind.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/Plugin.php b/3rdparty/Sabre/DAV/Browser/Plugin.php old mode 100755 new mode 100644 index 09bbdd2ae0..b6440ab634 --- a/3rdparty/Sabre/DAV/Browser/Plugin.php +++ b/3rdparty/Sabre/DAV/Browser/Plugin.php @@ -338,7 +338,7 @@ class Sabre_DAV_Browser_Plugin extends Sabre_DAV_ServerPlugin { $icon = ''; if ($this->enableAssets) { - $node = $parent->getChild($name); + $node = $this->server->tree->getNodeForPath(($path?$path.'/':'') . $name); foreach(array_reverse($this->iconMap) as $class=>$iconName) { if ($node instanceof $class) { diff --git a/3rdparty/Sabre/DAV/Browser/assets/favicon.ico b/3rdparty/Sabre/DAV/Browser/assets/favicon.ico old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/assets/icons/addressbook.png b/3rdparty/Sabre/DAV/Browser/assets/icons/addressbook.png old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/assets/icons/calendar.png b/3rdparty/Sabre/DAV/Browser/assets/icons/calendar.png old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/assets/icons/card.png b/3rdparty/Sabre/DAV/Browser/assets/icons/card.png old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/assets/icons/collection.png b/3rdparty/Sabre/DAV/Browser/assets/icons/collection.png old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/assets/icons/file.png b/3rdparty/Sabre/DAV/Browser/assets/icons/file.png old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/assets/icons/parent.png b/3rdparty/Sabre/DAV/Browser/assets/icons/parent.png old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Browser/assets/icons/principal.png b/3rdparty/Sabre/DAV/Browser/assets/icons/principal.png old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Client.php b/3rdparty/Sabre/DAV/Client.php old mode 100755 new mode 100644 index 9a428765e9..9d9f4c96b0 --- a/3rdparty/Sabre/DAV/Client.php +++ b/3rdparty/Sabre/DAV/Client.php @@ -16,12 +16,25 @@ */ class Sabre_DAV_Client { + /** + * The propertyMap is a key-value array. + * + * If you use the propertyMap, any {DAV:}multistatus responses with the + * proeprties listed in this array, will automatically be mapped to a + * respective class. + * + * The {DAV:}resourcetype property is automatically added. This maps to + * Sabre_DAV_Property_ResourceType + * + * @var array + */ public $propertyMap = array(); protected $baseUri; protected $userName; protected $password; protected $proxy; + protected $trustedCertificates; /** * Basic authentication @@ -87,6 +100,18 @@ class Sabre_DAV_Client { } + /** + * Add trusted root certificates to the webdav client. + * + * The parameter certificates should be a absulute path to a file + * which contains all trusted certificates + * + * @param string $certificates + */ + public function addTrustedCertificates($certificates) { + $this->trustedCertificates = $certificates; + } + /** * Does a PROPFIND request * @@ -143,13 +168,13 @@ class Sabre_DAV_Client { if ($depth===0) { reset($result); $result = current($result); - return $result[200]; + return isset($result[200])?$result[200]:array(); } $newResult = array(); foreach($result as $href => $statusList) { - $newResult[$href] = $statusList[200]; + $newResult[$href] = isset($statusList[200])?$statusList[200]:array(); } @@ -279,6 +304,10 @@ class Sabre_DAV_Client { CURLOPT_MAXREDIRS => 5, ); + if($this->trustedCertificates) { + $curlSettings[CURLOPT_CAINFO] = $this->trustedCertificates; + } + switch ($method) { case 'HEAD' : @@ -363,10 +392,30 @@ class Sabre_DAV_Client { if ($response['statusCode']>=400) { switch ($response['statusCode']) { + case 400 : + throw new Sabre_DAV_Exception_BadRequest('Bad request'); + case 401 : + throw new Sabre_DAV_Exception_NotAuthenticated('Not authenticated'); + case 402 : + throw new Sabre_DAV_Exception_PaymentRequired('Payment required'); + case 403 : + throw new Sabre_DAV_Exception_Forbidden('Forbidden'); case 404: - throw new Sabre_DAV_Exception_NotFound('Resource ' . $url . ' not found.'); - break; - + throw new Sabre_DAV_Exception_NotFound('Resource not found.'); + case 405 : + throw new Sabre_DAV_Exception_MethodNotAllowed('Method not allowed'); + case 409 : + throw new Sabre_DAV_Exception_Conflict('Conflict'); + case 412 : + throw new Sabre_DAV_Exception_PreconditionFailed('Precondition failed'); + case 416 : + throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('Requested Range Not Satisfiable'); + case 500 : + throw new Sabre_DAV_Exception('Internal server error'); + case 501 : + throw new Sabre_DAV_Exception_NotImplemented('Not Implemented'); + case 507 : + throw new Sabre_DAV_Exception_InsufficientStorage('Insufficient storage'); default: throw new Sabre_DAV_Exception('HTTP error response. (errorcode ' . $response['statusCode'] . ')'); } @@ -386,6 +435,7 @@ class Sabre_DAV_Client { * @param array $settings * @return array */ + // @codeCoverageIgnoreStart protected function curlRequest($url, $settings) { $curl = curl_init($url); @@ -399,6 +449,7 @@ class Sabre_DAV_Client { ); } + // @codeCoverageIgnoreEnd /** * Returns the full url based on the given url (which may be relative). All @@ -453,19 +504,17 @@ class Sabre_DAV_Client { */ public function parseMultiStatus($body) { - $body = Sabre_DAV_XMLUtil::convertDAVNamespace($body); - $responseXML = simplexml_load_string($body, null, LIBXML_NOBLANKS | LIBXML_NOCDATA); if ($responseXML===false) { throw new InvalidArgumentException('The passed data is not valid XML'); } - $responseXML->registerXPathNamespace('d', 'urn:DAV'); + $responseXML->registerXPathNamespace('d', 'DAV:'); $propResult = array(); foreach($responseXML->xpath('d:response') as $response) { - $response->registerXPathNamespace('d', 'urn:DAV'); + $response->registerXPathNamespace('d', 'DAV:'); $href = $response->xpath('d:href'); $href = (string)$href[0]; @@ -473,7 +522,7 @@ class Sabre_DAV_Client { foreach($response->xpath('d:propstat') as $propStat) { - $propStat->registerXPathNamespace('d', 'urn:DAV'); + $propStat->registerXPathNamespace('d', 'DAV:'); $status = $propStat->xpath('d:status'); list($httpVersion, $statusCode, $message) = explode(' ', (string)$status[0],3); diff --git a/3rdparty/Sabre/DAV/Collection.php b/3rdparty/Sabre/DAV/Collection.php old mode 100755 new mode 100644 index 776c22531b..c7648a8a52 --- a/3rdparty/Sabre/DAV/Collection.php +++ b/3rdparty/Sabre/DAV/Collection.php @@ -17,9 +17,13 @@ abstract class Sabre_DAV_Collection extends Sabre_DAV_Node implements Sabre_DAV_ /** * 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. + * 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 Sabre_DAV_Exception_NotFound * @return Sabre_DAV_INode diff --git a/3rdparty/Sabre/DAV/Directory.php b/3rdparty/Sabre/DAV/Directory.php deleted file mode 100755 index 6db8febc02..0000000000 --- a/3rdparty/Sabre/DAV/Directory.php +++ /dev/null @@ -1,17 +0,0 @@ -path, 'c'); + fseek($f,$offset-1); + if (is_string($data)) { + fwrite($f, $data); + } else { + stream_copy_to_stream($data,$f); + } + fclose($f); + return '"' . md5_file($this->path) . '"'; + + } + /** * Returns the data * - * @return string + * @return resource */ public function get() { diff --git a/3rdparty/Sabre/DAV/FSExt/Node.php b/3rdparty/Sabre/DAV/FSExt/Node.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/File.php b/3rdparty/Sabre/DAV/File.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/ICollection.php b/3rdparty/Sabre/DAV/ICollection.php old mode 100755 new mode 100644 index 4626038a66..94431a0c49 --- a/3rdparty/Sabre/DAV/ICollection.php +++ b/3rdparty/Sabre/DAV/ICollection.php @@ -19,7 +19,7 @@ interface Sabre_DAV_ICollection extends Sabre_DAV_INode { * 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 succesful creation of the file, you may choose to return the ETag + * 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 @@ -50,6 +50,9 @@ interface Sabre_DAV_ICollection extends Sabre_DAV_INode { /** * 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 * @return Sabre_DAV_INode */ diff --git a/3rdparty/Sabre/DAV/IExtendedCollection.php b/3rdparty/Sabre/DAV/IExtendedCollection.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/IFile.php b/3rdparty/Sabre/DAV/IFile.php old mode 100755 new mode 100644 index 478f822ae7..1eca8986a5 --- a/3rdparty/Sabre/DAV/IFile.php +++ b/3rdparty/Sabre/DAV/IFile.php @@ -51,7 +51,7 @@ interface Sabre_DAV_IFile extends Sabre_DAV_INode { * * If null is returned, we'll assume application/octet-stream * - * @return void + * @return string|null */ function getContentType(); diff --git a/3rdparty/Sabre/DAV/INode.php b/3rdparty/Sabre/DAV/INode.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/IProperties.php b/3rdparty/Sabre/DAV/IProperties.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/IQuota.php b/3rdparty/Sabre/DAV/IQuota.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Locks/Backend/Abstract.php b/3rdparty/Sabre/DAV/Locks/Backend/Abstract.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Locks/Backend/FS.php b/3rdparty/Sabre/DAV/Locks/Backend/FS.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Locks/Backend/File.php b/3rdparty/Sabre/DAV/Locks/Backend/File.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Locks/Backend/PDO.php b/3rdparty/Sabre/DAV/Locks/Backend/PDO.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Locks/LockInfo.php b/3rdparty/Sabre/DAV/Locks/LockInfo.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Locks/Plugin.php b/3rdparty/Sabre/DAV/Locks/Plugin.php old mode 100755 new mode 100644 index 035b3a6386..957ac506a9 --- a/3rdparty/Sabre/DAV/Locks/Plugin.php +++ b/3rdparty/Sabre/DAV/Locks/Plugin.php @@ -152,6 +152,7 @@ class Sabre_DAV_Locks_Plugin extends Sabre_DAV_ServerPlugin { case 'MKCOL' : case 'PROPPATCH' : case 'PUT' : + case 'PATCH' : $lastLock = null; if (!$this->validateLock($uri,$lastLock)) throw new Sabre_DAV_Exception_Locked($lastLock); diff --git a/3rdparty/Sabre/DAV/Mount/Plugin.php b/3rdparty/Sabre/DAV/Mount/Plugin.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Node.php b/3rdparty/Sabre/DAV/Node.php old mode 100755 new mode 100644 index 070b7176af..3b95dfec2f --- a/3rdparty/Sabre/DAV/Node.php +++ b/3rdparty/Sabre/DAV/Node.php @@ -27,7 +27,7 @@ abstract class Sabre_DAV_Node implements Sabre_DAV_INode { } /** - * Deleted the current node + * Deletes the current node * * @throws Sabre_DAV_Exception_Forbidden * @return void diff --git a/3rdparty/Sabre/DAV/ObjectTree.php b/3rdparty/Sabre/DAV/ObjectTree.php old mode 100755 new mode 100644 index bce5146390..3b7f222d64 --- a/3rdparty/Sabre/DAV/ObjectTree.php +++ b/3rdparty/Sabre/DAV/ObjectTree.php @@ -51,24 +51,30 @@ class Sabre_DAV_ObjectTree extends Sabre_DAV_Tree { $path = trim($path,'/'); if (isset($this->cache[$path])) return $this->cache[$path]; - //if (!$path || $path=='.') return $this->rootNode; - $currentNode = $this->rootNode; + // Is it the root node? + if (!strlen($path)) { + return $this->rootNode; + } - // We're splitting up the path variable into folder/subfolder components and traverse to the correct node.. - foreach(explode('/',$path) as $pathPart) { + // Attempting to fetch its parent + list($parentName, $baseName) = Sabre_DAV_URLUtil::splitPath($path); - // If this part of the path is just a dot, it actually means we can skip it - if ($pathPart=='.' || $pathPart=='') continue; + // 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 (!($currentNode instanceof Sabre_DAV_ICollection)) + if (!($parent instanceof Sabre_DAV_ICollection)) throw new Sabre_DAV_Exception_NotFound('Could not find node at path: ' . $path); - $currentNode = $currentNode->getChild($pathPart); + $node = $parent->getChild($baseName); } - $this->cache[$path] = $currentNode; - return $currentNode; + $this->cache[$path] = $node; + return $node; } diff --git a/3rdparty/Sabre/DAV/PartialUpdate/IFile.php b/3rdparty/Sabre/DAV/PartialUpdate/IFile.php new file mode 100644 index 0000000000..cf5ad55c6d --- /dev/null +++ b/3rdparty/Sabre/DAV/PartialUpdate/IFile.php @@ -0,0 +1,38 @@ +addPlugin($patchPlugin); + * + * @package Sabre + * @subpackage DAV + * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved. + * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class Sabre_DAV_PartialUpdate_Plugin extends Sabre_DAV_ServerPlugin { + + /** + * Reference to server + * + * @var Sabre_DAV_Server + */ + protected $server; + + /** + * Initializes the plugin + * + * This method is automatically called by the Server class after addPlugin. + * + * @param Sabre_DAV_Server $server + * @return void + */ + public function initialize(Sabre_DAV_Server $server) { + + $this->server = $server; + $server->subscribeEvent('unknownMethod',array($this,'unknownMethod')); + + } + + /** + * 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 'partialupdate'; + + } + + /** + * This method is called by the Server if the user used an HTTP method + * the server didn't recognize. + * + * This plugin intercepts the PATCH methods. + * + * @param string $method + * @param string $uri + * @return bool|null + */ + public function unknownMethod($method, $uri) { + + switch($method) { + + case 'PATCH': + return $this->httpPatch($uri); + + } + + } + + /** + * 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 (partial 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) && + $tree->getNodeForPath($uri) instanceof Sabre_DAV_PartialUpdate_IFile) { + return array('PATCH'); + } + + return array(); + + } + + /** + * Returns a list of features for the HTTP OPTIONS Dav: header. + * + * @return array + */ + public function getFeatures() { + + return array('sabredav-partialupdate'); + + } + + /** + * Patch an uri + * + * The WebDAV patch request can be used to modify only a part of an + * existing resource. If the resource does not exist yet and the first + * offset is not 0, the request fails + * + * @param string $uri + * @return void + */ + protected function httpPatch($uri) { + + // Get the node. Will throw a 404 if not found + $node = $this->server->tree->getNodeForPath($uri); + if (!($node instanceof Sabre_DAV_PartialUpdate_IFile)) { + throw new Sabre_DAV_Exception_MethodNotAllowed('The target resource does not support the PATCH method.'); + } + + $range = $this->getHTTPUpdateRange(); + + if (!$range) { + throw new Sabre_DAV_Exception_BadRequest('No valid "X-Update-Range" found in the headers'); + } + + $contentType = strtolower( + $this->server->httpRequest->getHeader('Content-Type') + ); + + if ($contentType != 'application/x-sabredav-partialupdate') { + throw new Sabre_DAV_Exception_UnsupportedMediaType('Unknown Content-Type header "' . $contentType . '"'); + } + + $len = $this->server->httpRequest->getHeader('Content-Length'); + + // Load the begin and end data + $start = ($range[0])?$range[0]:0; + $end = ($range[1])?$range[1]:$len-1; + + // Check consistency + if($end < $start) + throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')'); + if($end - $start + 1 != $len) + throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('Actual data length (' . $len . ') is not consistent with begin (' . $range[0] . ') and end (' . $range[1] . ') offsets'); + + // Checking If-None-Match and related headers. + if (!$this->server->checkPreconditions()) return; + + if (!$this->server->broadcastEvent('beforeWriteContent',array($uri, $node, null))) + return; + + $body = $this->server->httpRequest->getBody(); + $etag = $node->putRange($body, $start-1); + + $this->server->broadcastEvent('afterWriteContent',array($uri, $node)); + + $this->server->httpResponse->setHeader('Content-Length','0'); + if ($etag) $this->server->httpResponse->setHeader('ETag',$etag); + $this->server->httpResponse->sendStatus(204); + + return false; + + } + + /** + * Returns the HTTP custom range update header + * + * This method returns null if there is no well-formed HTTP range request + * header or array($start, $end). + * + * The first number is the offset of the first byte in the range. + * The second number is the offset of the last byte in the range. + * + * If the second offset is null, it should be treated as the offset of the last byte of the entity + * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity + * + * @return array|null + */ + public function getHTTPUpdateRange() { + + $range = $this->server->httpRequest->getHeader('X-Update-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 array( + $matches[1]!==''?$matches[1]:null, + $matches[2]!==''?$matches[2]:null, + ); + + } +} diff --git a/3rdparty/Sabre/DAV/Property.php b/3rdparty/Sabre/DAV/Property.php old mode 100755 new mode 100644 index 1cfada3236..6487bf44bc --- a/3rdparty/Sabre/DAV/Property.php +++ b/3rdparty/Sabre/DAV/Property.php @@ -11,10 +11,16 @@ * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ -abstract class Sabre_DAV_Property { - - abstract function serialize(Sabre_DAV_Server $server, DOMElement $prop); +abstract class Sabre_DAV_Property implements Sabre_DAV_PropertyInterface { + /** + * Unserializes the property. + * + * This static method should return a an instance of this object. + * + * @param DOMElement $prop + * @return Sabre_DAV_IProperty + */ static function unserialize(DOMElement $prop) { throw new Sabre_DAV_Exception('Unserialize has not been implemented for this class'); diff --git a/3rdparty/Sabre/DAV/Property/GetLastModified.php b/3rdparty/Sabre/DAV/Property/GetLastModified.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Property/Href.php b/3rdparty/Sabre/DAV/Property/Href.php old mode 100755 new mode 100644 index dac564f24d..cd1d867f71 --- a/3rdparty/Sabre/DAV/Property/Href.php +++ b/3rdparty/Sabre/DAV/Property/Href.php @@ -82,7 +82,7 @@ class Sabre_DAV_Property_Href extends Sabre_DAV_Property implements Sabre_DAV_Pr */ static function unserialize(DOMElement $dom) { - if (Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)==='{DAV:}href') { + if ($dom->firstChild && Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)==='{DAV:}href') { return new self($dom->firstChild->textContent,false); } diff --git a/3rdparty/Sabre/DAV/Property/HrefList.php b/3rdparty/Sabre/DAV/Property/HrefList.php old mode 100755 new mode 100644 index 7a52272e88..282c452ca0 --- a/3rdparty/Sabre/DAV/Property/HrefList.php +++ b/3rdparty/Sabre/DAV/Property/HrefList.php @@ -79,7 +79,7 @@ class Sabre_DAV_Property_HrefList extends Sabre_DAV_Property { * It will only decode {DAV:}href values. * * @param DOMElement $dom - * @return Sabre_DAV_Property_Href + * @return Sabre_DAV_Property_HrefList */ static function unserialize(DOMElement $dom) { diff --git a/3rdparty/Sabre/DAV/Property/IHref.php b/3rdparty/Sabre/DAV/Property/IHref.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Property/LockDiscovery.php b/3rdparty/Sabre/DAV/Property/LockDiscovery.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Property/ResourceType.php b/3rdparty/Sabre/DAV/Property/ResourceType.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Property/Response.php b/3rdparty/Sabre/DAV/Property/Response.php old mode 100755 new mode 100644 index 88afbcfb26..9f21163d12 --- a/3rdparty/Sabre/DAV/Property/Response.php +++ b/3rdparty/Sabre/DAV/Property/Response.php @@ -138,7 +138,7 @@ class Sabre_DAV_Property_Response extends Sabre_DAV_Property implements Sabre_DA if (is_scalar($propertyValue)) { $text = $document->createTextNode($propertyValue); $currentProperty->appendChild($text); - } elseif ($propertyValue instanceof Sabre_DAV_Property) { + } elseif ($propertyValue instanceof Sabre_DAV_PropertyInterface) { $propertyValue->serialize($server,$currentProperty); } elseif (!is_null($propertyValue)) { throw new Sabre_DAV_Exception('Unknown property value type: ' . gettype($propertyValue) . ' for property: ' . $propertyName); diff --git a/3rdparty/Sabre/DAV/Property/ResponseList.php b/3rdparty/Sabre/DAV/Property/ResponseList.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Property/SupportedLock.php b/3rdparty/Sabre/DAV/Property/SupportedLock.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/Property/SupportedReportSet.php b/3rdparty/Sabre/DAV/Property/SupportedReportSet.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAV/PropertyInterface.php b/3rdparty/Sabre/DAV/PropertyInterface.php new file mode 100644 index 0000000000..515072cbd3 --- /dev/null +++ b/3rdparty/Sabre/DAV/PropertyInterface.php @@ -0,0 +1,21 @@ +broadcastEvent('exception', array($e)); + } catch (Exception $ignore) { + } $DOM = new DOMDocument('1.0','utf-8'); $DOM->formatOutput = true; @@ -214,17 +217,23 @@ class Sabre_DAV_Server { $error->setAttribute('xmlns:s',self::NS_SABREDAV); $DOM->appendChild($error); - $error->appendChild($DOM->createElement('s:exception',get_class($e))); - $error->appendChild($DOM->createElement('s:message',$e->getMessage())); + $h = function($v) { + + return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8'); + + }; + + $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',$e->getFile())); - $error->appendChild($DOM->createElement('s:line',$e->getLine())); - $error->appendChild($DOM->createElement('s:code',$e->getCode())); - $error->appendChild($DOM->createElement('s:stacktrace',$e->getTraceAsString())); + $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 (self::$exposeVersion) { - $error->appendChild($DOM->createElement('s:sabredav-version',Sabre_DAV_Version::VERSION)); + $error->appendChild($DOM->createElement('s:sabredav-version',$h(Sabre_DAV_Version::VERSION))); } if($e instanceof Sabre_DAV_Exception) { @@ -508,7 +517,7 @@ class Sabre_DAV_Server { if (!$this->checkPreconditions(true)) return false; - if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_NotImplemented('GET is only implemented on File objects'); + if (!$node instanceof Sabre_DAV_IFile) throw new Sabre_DAV_Exception_NotImplemented('GET is only implemented on File objects'); $body = $node->get(); // Converting string into stream, if needed. @@ -696,6 +705,7 @@ class Sabre_DAV_Server { // This is a multi-status response $this->httpResponse->sendStatus(207); $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $this->httpResponse->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 @@ -704,7 +714,10 @@ class Sabre_DAV_Server { foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures()); $this->httpResponse->setHeader('DAV',implode(', ',$features)); - $data = $this->generateMultiStatus($newProperties); + $prefer = $this->getHTTPPrefer(); + $minimal = $prefer['return-minimal']; + + $data = $this->generateMultiStatus($newProperties, $minimal); $this->httpResponse->sendBody($data); } @@ -724,6 +737,30 @@ class Sabre_DAV_Server { $result = $this->updateProperties($uri, $newProperties); + $prefer = $this->getHTTPPrefer(); + $this->httpResponse->setHeader('Vary','Brief,Prefer'); + + if ($prefer['return-minimal']) { + + // If return-minimal is specified, we only have to check if the + // request was succesful, and don't need to return the + // multi-status. + $ok = true; + foreach($result as $code=>$prop) { + if ((int)$code > 299) { + $ok = false; + } + } + + if ($ok) { + + $this->httpResponse->sendStatus(204); + return; + + } + + } + $this->httpResponse->sendStatus(207); $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); @@ -927,7 +964,7 @@ class Sabre_DAV_Server { * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo * * @param string $uri - * @return void + * @return bool */ protected function httpMove($uri) { @@ -1009,7 +1046,7 @@ class Sabre_DAV_Server { if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) { // If broadcastEvent returned true, it means the report was not supported - throw new Sabre_DAV_Exception_ReportNotImplemented(); + throw new Sabre_DAV_Exception_ReportNotSupported(); } @@ -1158,6 +1195,85 @@ class Sabre_DAV_Server { } + /** + * 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: + * array( + * '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 = array( + 'return-asynch' => false, + 'return-minimal' => false, + 'return-representation' => false, + 'wait' => null, + 'strict' => false, + 'lenient' => false, + ); + + if ($prefer = $this->httpRequest->getHeader('Prefer')) { + + $parameters = array_map('trim', + explode(',', $prefer) + ); + + foreach($parameters as $parameter) { + + // Right now our regex only supports the tokens actually + // specified in the draft. We may need to expand this if new + // tokens get registered. + if(!preg_match('/^(?P[a-z0-9-]+)(?:=(?P[0-9]+))?$/', $parameter, $matches)) { + continue; + } + + switch($matches['token']) { + + case 'return-asynch' : + case 'return-minimal' : + case 'return-representation' : + case 'strict' : + case 'lenient' : + $result[$matches['token']] = true; + break; + case 'wait' : + $result[$matches['token']] = $matches['value']; + break; + + } + + } + + } + + if ($this->httpRequest->getHeader('Brief')=='t') { + $result['return-minimal'] = true; + } + + return $result; + + } + /** * Returns information about Copy and Move requests @@ -1433,15 +1549,18 @@ class Sabre_DAV_Server { } - $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties)); + $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties, $node)); $newProperties['href'] = trim($myPath,'/'); // Its is a WebDAV recommendation to add a trailing slash to collectionnames. - // Apple's iCal also requires a trailing slash for principals (rfc 3744). - // Therefore we add a trailing / for any non-file. This might need adjustments - // if we find there are other edge cases. - if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype']) && count($newProperties[200]['{DAV:}resourcetype']->getValue())>0) $newProperties['href'] .='/'; + // Apple's iCal also requires a trailing slash for principals (rfc 3744), though this is non-standard. + if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype'])) { + $rt = $newProperties[200]['{DAV:}resourcetype']; + if ($rt->is('{DAV:}collection') || $rt->is('{DAV:}principal')) { + $newProperties['href'] .='/'; + } + } // If the resourcetype property was manually added to the requested property list, // we will remove it again. @@ -1476,11 +1595,14 @@ class Sabre_DAV_Server { if (!$this->broadcastEvent('beforeBind',array($uri))) return false; $parent = $this->tree->getNodeForPath($dir); + if (!$parent instanceof Sabre_DAV_ICollection) { + throw new Sabre_DAV_Exception_Conflict('Files can only be created as children of collections'); + } if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false; $etag = $parent->createFile($name,$data); - $this->tree->markDirty($dir); + $this->tree->markDirty($dir . '/' . $name); $this->broadcastEvent('afterBind',array($uri)); $this->broadcastEvent('afterCreateFile',array($uri, $parent)); @@ -1901,12 +2023,15 @@ class Sabre_DAV_Server { /** - * Generates a WebDAV propfind response body based on a list of nodes + * Generates a WebDAV propfind response body based on a list of nodes. + * + * If 'strip404s' is set to true, all 404 responses will be removed. * * @param array $fileProperties The list with nodes + * @param bool strip404s * @return string */ - public function generateMultiStatus(array $fileProperties) { + public function generateMultiStatus(array $fileProperties, $strip404s = false) { $dom = new DOMDocument('1.0','utf-8'); //$dom->formatOutput = true; @@ -1925,6 +2050,10 @@ class Sabre_DAV_Server { $href = $entry['href']; unset($entry['href']); + if ($strip404s && isset($entry[404])) { + unset($entry[404]); + } + $response = new Sabre_DAV_Property_Response($href,$entry); $response->serialize($this,$multiStatus); @@ -1995,7 +2124,7 @@ class Sabre_DAV_Server { if (!$body) return array(); $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body); - $elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0); + $elem = $dom->getElementsByTagNameNS('DAV:','propfind')->item(0); return array_keys(Sabre_DAV_XMLUtil::parseProperties($elem)); } diff --git a/3rdparty/Sabre/DAV/ServerPlugin.php b/3rdparty/Sabre/DAV/ServerPlugin.php old mode 100755 new mode 100644 index 131863d13f..120569ffcc --- a/3rdparty/Sabre/DAV/ServerPlugin.php +++ b/3rdparty/Sabre/DAV/ServerPlugin.php @@ -19,7 +19,7 @@ abstract class Sabre_DAV_ServerPlugin { * This function is called by Sabre_DAV_Server, after * addPlugin is called. * - * This method should set up the requires event subscriptions. + * This method should set up the required event subscriptions. * * @param Sabre_DAV_Server $server * @return void diff --git a/3rdparty/Sabre/DAV/SimpleCollection.php b/3rdparty/Sabre/DAV/SimpleCollection.php old mode 100755 new mode 100644 index 4acf971caa..79e2eaaacd --- a/3rdparty/Sabre/DAV/SimpleCollection.php +++ b/3rdparty/Sabre/DAV/SimpleCollection.php @@ -31,7 +31,7 @@ class Sabre_DAV_SimpleCollection extends Sabre_DAV_Collection { /** * Creates this node * - * The name of the node must be passed, child nodes can also be bassed. + * The name of the node must be passed, child nodes can also be passed. * This nodes must be instances of Sabre_DAV_INode * * @param string $name @@ -78,6 +78,9 @@ class Sabre_DAV_SimpleCollection extends Sabre_DAV_Collection { * 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 Sabre_DAV_Exception_NotFound * @return Sabre_DAV_INode diff --git a/3rdparty/Sabre/DAV/SimpleDirectory.php b/3rdparty/Sabre/DAV/SimpleDirectory.php deleted file mode 100755 index 621222ebc5..0000000000 --- a/3rdparty/Sabre/DAV/SimpleDirectory.php +++ /dev/null @@ -1,21 +0,0 @@ -nodeType !== XML_ELEMENT_NODE) return null; - // Mapping back to the real namespace, in case it was dav - if ($dom->namespaceURI=='urn:DAV') $ns = 'DAV:'; else $ns = $dom->namespaceURI; + $ns = $dom->namespaceURI; // Mapping to clark notation return '{' . $ns . '}' . $dom->localName; @@ -64,29 +60,11 @@ class Sabre_DAV_XMLUtil { } - /** - * This method takes an XML document (as string) and converts all instances of the - * DAV: namespace to urn:DAV - * - * This is unfortunately needed, because the DAV: namespace violates the xml namespaces - * spec, and causes the DOM to throw errors - * - * @param string $xmlDocument - * @return array|string|null - */ - static function convertDAVNamespace($xmlDocument) { - - // This is used to map the DAV: namespace to urn:DAV. This is needed, because the DAV: - // namespace is actually a violation of the XML namespaces specification, and will cause errors - return preg_replace("/xmlns(:[A-Za-z0-9_]*)?=(\"|\')DAV:(\\2)/","xmlns\\1=\\2urn:DAV\\2",$xmlDocument); - - } - /** * This method provides a generic way to load a DOMDocument for WebDAV use. * * This method throws a Sabre_DAV_Exception_BadRequest exception for any xml errors. - * It does not preserve whitespace, and it converts the DAV: namespace to urn:DAV. + * It does not preserve whitespace. * * @param string $xml * @throws Sabre_DAV_Exception_BadRequest @@ -118,10 +96,11 @@ class Sabre_DAV_XMLUtil { libxml_clear_errors(); $dom = new DOMDocument(); - $dom->loadXML(self::convertDAVNamespace($xml),LIBXML_NOWARNING | LIBXML_NOERROR); // We don't generally care about any whitespace $dom->preserveWhiteSpace = false; + + $dom->loadXML($xml,LIBXML_NOWARNING | LIBXML_NOERROR); if ($error = libxml_get_last_error()) { libxml_clear_errors(); diff --git a/3rdparty/Sabre/DAV/includes.php b/3rdparty/Sabre/DAV/includes.php old mode 100755 new mode 100644 index 6a4890677e..6728f88ce7 --- a/3rdparty/Sabre/DAV/includes.php +++ b/3rdparty/Sabre/DAV/includes.php @@ -28,7 +28,7 @@ include __DIR__ . '/Locks/Backend/PDO.php'; include __DIR__ . '/Locks/LockInfo.php'; include __DIR__ . '/Node.php'; include __DIR__ . '/Property/IHref.php'; -include __DIR__ . '/Property.php'; +include __DIR__ . '/PropertyInterface.php'; include __DIR__ . '/Server.php'; include __DIR__ . '/ServerPlugin.php'; include __DIR__ . '/StringUtil.php'; @@ -60,7 +60,7 @@ include __DIR__ . '/Exception/NotFound.php'; include __DIR__ . '/Exception/NotImplemented.php'; include __DIR__ . '/Exception/PaymentRequired.php'; include __DIR__ . '/Exception/PreconditionFailed.php'; -include __DIR__ . '/Exception/ReportNotImplemented.php'; +include __DIR__ . '/Exception/ReportNotSupported.php'; include __DIR__ . '/Exception/RequestedRangeNotSatisfiable.php'; include __DIR__ . '/Exception/UnsupportedMediaType.php'; include __DIR__ . '/FS/Node.php'; @@ -72,6 +72,18 @@ include __DIR__ . '/IQuota.php'; include __DIR__ . '/Locks/Plugin.php'; include __DIR__ . '/Mount/Plugin.php'; include __DIR__ . '/ObjectTree.php'; +include __DIR__ . '/PartialUpdate/IFile.php'; +include __DIR__ . '/PartialUpdate/Plugin.php'; +include __DIR__ . '/Property.php'; +include __DIR__ . '/Tree/Filesystem.php'; +include __DIR__ . '/Collection.php'; +include __DIR__ . '/Exception/ConflictingLock.php'; +include __DIR__ . '/Exception/FileNotFound.php'; +include __DIR__ . '/File.php'; +include __DIR__ . '/FS/Directory.php'; +include __DIR__ . '/FS/File.php'; +include __DIR__ . '/FSExt/Directory.php'; +include __DIR__ . '/FSExt/File.php'; include __DIR__ . '/Property/GetLastModified.php'; include __DIR__ . '/Property/Href.php'; include __DIR__ . '/Property/HrefList.php'; @@ -81,17 +93,6 @@ include __DIR__ . '/Property/Response.php'; include __DIR__ . '/Property/ResponseList.php'; include __DIR__ . '/Property/SupportedLock.php'; include __DIR__ . '/Property/SupportedReportSet.php'; -include __DIR__ . '/Tree/Filesystem.php'; -include __DIR__ . '/Collection.php'; -include __DIR__ . '/Directory.php'; -include __DIR__ . '/Exception/ConflictingLock.php'; -include __DIR__ . '/Exception/FileNotFound.php'; -include __DIR__ . '/File.php'; -include __DIR__ . '/FS/Directory.php'; -include __DIR__ . '/FS/File.php'; -include __DIR__ . '/FSExt/Directory.php'; -include __DIR__ . '/FSExt/File.php'; include __DIR__ . '/SimpleCollection.php'; -include __DIR__ . '/SimpleDirectory.php'; include __DIR__ . '/SimpleFile.php'; // End includes diff --git a/3rdparty/Sabre/DAVACL/AbstractPrincipalCollection.php b/3rdparty/Sabre/DAVACL/AbstractPrincipalCollection.php old mode 100755 new mode 100644 index e05b774980..f67eadad6e --- a/3rdparty/Sabre/DAVACL/AbstractPrincipalCollection.php +++ b/3rdparty/Sabre/DAVACL/AbstractPrincipalCollection.php @@ -107,7 +107,7 @@ abstract class Sabre_DAVACL_AbstractPrincipalCollection extends Sabre_DAV_Collec * * @param string $name * @throws Sabre_DAV_Exception_NotFound - * @return Sabre_DAV_IPrincipal + * @return Sabre_DAVACL_IPrincipal */ public function getChild($name) { diff --git a/3rdparty/Sabre/DAVACL/Exception/AceConflict.php b/3rdparty/Sabre/DAVACL/Exception/AceConflict.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Exception/NeedPrivileges.php b/3rdparty/Sabre/DAVACL/Exception/NeedPrivileges.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Exception/NoAbstract.php b/3rdparty/Sabre/DAVACL/Exception/NoAbstract.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Exception/NotRecognizedPrincipal.php b/3rdparty/Sabre/DAVACL/Exception/NotRecognizedPrincipal.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Exception/NotSupportedPrivilege.php b/3rdparty/Sabre/DAVACL/Exception/NotSupportedPrivilege.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/IACL.php b/3rdparty/Sabre/DAVACL/IACL.php old mode 100755 new mode 100644 index 003e699348..356bb481d5 --- a/3rdparty/Sabre/DAVACL/IACL.php +++ b/3rdparty/Sabre/DAVACL/IACL.php @@ -48,7 +48,7 @@ interface Sabre_DAVACL_IACL extends Sabre_DAV_INode { /** * Updates the ACL * - * This method will receive a list of new ACE's. + * This method will receive a list of new ACE's as an array argument. * * @param array $acl * @return void diff --git a/3rdparty/Sabre/DAVACL/IPrincipal.php b/3rdparty/Sabre/DAVACL/IPrincipal.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/IPrincipalBackend.php b/3rdparty/Sabre/DAVACL/IPrincipalBackend.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Plugin.php b/3rdparty/Sabre/DAVACL/Plugin.php old mode 100755 new mode 100644 index 5c828c6d97..5b17c83847 --- a/3rdparty/Sabre/DAVACL/Plugin.php +++ b/3rdparty/Sabre/DAVACL/Plugin.php @@ -3,7 +3,7 @@ /** * SabreDAV ACL Plugin * - * This plugin provides funcitonality to enforce ACL permissions. + * This plugin provides functionality to enforce ACL permissions. * ACL is defined in RFC3744. * * In addition it also provides support for the {DAV:}current-user-principal @@ -102,11 +102,11 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { ); /** - * 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 + * 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 + * + * @var array */ public $adminPrincipals = array(); @@ -233,6 +233,7 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { $authPlugin = $this->server->getPlugin('auth'); if (is_null($authPlugin)) return null; + /** @var $authPlugin Sabre_DAV_Auth_Plugin */ $userName = $authPlugin->getCurrentUser(); if (!$userName) return null; @@ -241,6 +242,14 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { } + /** + * This array holds a cache for all the principals that are associated with + * a single principal. + * + * @var array + */ + protected $currentUserPrincipalsCache = array(); + /** * Returns a list of principals that's associated to the current * user, either directly or through group membership. @@ -253,6 +262,11 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { if (is_null($currentUser)) return array(); + // First check our cache + if (isset($this->currentUserPrincipalsCache[$currentUser])) { + return $this->currentUserPrincipalsCache[$currentUser]; + } + $check = array($currentUser); $principals = array($currentUser); @@ -277,6 +291,9 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { } + // Store the result in the cache + $this->currentUserPrincipalsCache[$currentUser] = $principals; + return $principals; } @@ -771,7 +788,7 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { * @param array $requestedProperties * @param array $returnedProperties * @TODO really should be broken into multiple methods, or even a class. - * @return void + * @return bool */ public function beforeGetProperties($uri, Sabre_DAV_INode $node, &$requestedProperties, &$returnedProperties) { @@ -895,6 +912,18 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { $returnedProperties[200]['{DAV:}acl-restrictions'] = new Sabre_DAVACL_Property_AclRestrictions(); } + /* Adding ACL properties */ + if ($node instanceof Sabre_DAVACL_IACL) { + + if (false !== ($index = array_search('{DAV:}owner', $requestedProperties))) { + + unset($requestedProperties[$index]); + $returnedProperties[200]['{DAV:}owner'] = new Sabre_DAV_Property_Href($node->getOwner() . '/'); + + } + + } + } /** @@ -928,6 +957,9 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { } $node->setGroupMemberSet($memberSet); + // We must also clear our cache, just in case + + $this->currentUserPrincipalsCache = array(); $result[200]['{DAV:}group-member-set'] = null; unset($propertyDelta['{DAV:}group-member-set']); @@ -935,7 +967,7 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { } /** - * This method handels HTTP REPORT requests + * This method handles HTTP REPORT requests * * @param string $reportName * @param DOMNode $dom @@ -1268,10 +1300,12 @@ class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin { } $result = $this->principalSearch($searchProperties, $requestedProperties, $uri); - $xml = $this->server->generateMultiStatus($result); - $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $prefer = $this->server->getHTTPPRefer(); + $this->server->httpResponse->sendStatus(207); - $this->server->httpResponse->sendBody($xml); + $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary','Brief,Prefer'); + $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal'])); } diff --git a/3rdparty/Sabre/DAVACL/Principal.php b/3rdparty/Sabre/DAVACL/Principal.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/PrincipalBackend/PDO.php b/3rdparty/Sabre/DAVACL/PrincipalBackend/PDO.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/PrincipalCollection.php b/3rdparty/Sabre/DAVACL/PrincipalCollection.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Property/Acl.php b/3rdparty/Sabre/DAVACL/Property/Acl.php old mode 100755 new mode 100644 index 05e1a690b3..3f79a8d532 --- a/3rdparty/Sabre/DAVACL/Property/Acl.php +++ b/3rdparty/Sabre/DAVACL/Property/Acl.php @@ -88,11 +88,11 @@ class Sabre_DAVACL_Property_Acl extends Sabre_DAV_Property { static public function unserialize(DOMElement $dom) { $privileges = array(); - $xaces = $dom->getElementsByTagNameNS('urn:DAV','ace'); + $xaces = $dom->getElementsByTagNameNS('DAV:','ace'); for($ii=0; $ii < $xaces->length; $ii++) { $xace = $xaces->item($ii); - $principal = $xace->getElementsByTagNameNS('urn:DAV','principal'); + $principal = $xace->getElementsByTagNameNS('DAV:','principal'); if ($principal->length !== 1) { throw new Sabre_DAV_Exception_BadRequest('Each {DAV:}ace element must have one {DAV:}principal element'); } @@ -116,17 +116,17 @@ class Sabre_DAVACL_Property_Acl extends Sabre_DAV_Property { $protected = false; - if ($xace->getElementsByTagNameNS('urn:DAV','protected')->length > 0) { + if ($xace->getElementsByTagNameNS('DAV:','protected')->length > 0) { $protected = true; } - $grants = $xace->getElementsByTagNameNS('urn:DAV','grant'); + $grants = $xace->getElementsByTagNameNS('DAV:','grant'); if ($grants->length < 1) { throw new Sabre_DAV_Exception_NotImplemented('Every {DAV:}ace element must have a {DAV:}grant element. {DAV:}deny is not yet supported'); } $grant = $grants->item(0); - $xprivs = $grant->getElementsByTagNameNS('urn:DAV','privilege'); + $xprivs = $grant->getElementsByTagNameNS('DAV:','privilege'); for($jj=0; $jj<$xprivs->length; $jj++) { $xpriv = $xprivs->item($jj); diff --git a/3rdparty/Sabre/DAVACL/Property/AclRestrictions.php b/3rdparty/Sabre/DAVACL/Property/AclRestrictions.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Property/CurrentUserPrivilegeSet.php b/3rdparty/Sabre/DAVACL/Property/CurrentUserPrivilegeSet.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Property/Principal.php b/3rdparty/Sabre/DAVACL/Property/Principal.php old mode 100755 new mode 100644 index c36328a58e..3f68174227 --- a/3rdparty/Sabre/DAVACL/Property/Principal.php +++ b/3rdparty/Sabre/DAVACL/Property/Principal.php @@ -15,7 +15,7 @@ class Sabre_DAVACL_Property_Principal extends Sabre_DAV_Property implements Sabre_DAV_Property_IHref { /** - * To specify a not-logged-in user, use the UNAUTHENTICTED principal + * To specify a not-logged-in user, use the UNAUTHENTICATED principal */ const UNAUTHENTICATED = 1; @@ -131,7 +131,7 @@ class Sabre_DAVACL_Property_Principal extends Sabre_DAV_Property implements Sabr * Deserializes a DOM element into a property object. * * @param DOMElement $dom - * @return Sabre_DAV_Property_Principal + * @return Sabre_DAVACL_Property_Principal */ static public function unserialize(DOMElement $dom) { diff --git a/3rdparty/Sabre/DAVACL/Property/SupportedPrivilegeSet.php b/3rdparty/Sabre/DAVACL/Property/SupportedPrivilegeSet.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/DAVACL/Version.php b/3rdparty/Sabre/DAVACL/Version.php old mode 100755 new mode 100644 index 9950f74874..084a9c13c8 --- a/3rdparty/Sabre/DAVACL/Version.php +++ b/3rdparty/Sabre/DAVACL/Version.php @@ -14,7 +14,7 @@ class Sabre_DAVACL_Version { /** * Full version number */ - const VERSION = '1.6.0'; + const VERSION = '1.7.0'; /** * Stability : alpha, beta, stable diff --git a/3rdparty/Sabre/DAVACL/includes.php b/3rdparty/Sabre/DAVACL/includes.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/HTTP/AWSAuth.php b/3rdparty/Sabre/HTTP/AWSAuth.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/HTTP/AbstractAuth.php b/3rdparty/Sabre/HTTP/AbstractAuth.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/HTTP/BasicAuth.php b/3rdparty/Sabre/HTTP/BasicAuth.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/HTTP/DigestAuth.php b/3rdparty/Sabre/HTTP/DigestAuth.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/HTTP/Request.php b/3rdparty/Sabre/HTTP/Request.php old mode 100755 new mode 100644 index 4746ef7770..74d5ed3d7e --- a/3rdparty/Sabre/HTTP/Request.php +++ b/3rdparty/Sabre/HTTP/Request.php @@ -184,7 +184,7 @@ class Sabre_HTTP_Request { * This method returns a readable stream resource. * If the asString parameter is set to true, a string is sent instead. * - * @param bool asString + * @param bool $asString * @return resource */ public function getBody($asString = false) { diff --git a/3rdparty/Sabre/HTTP/Response.php b/3rdparty/Sabre/HTTP/Response.php old mode 100755 new mode 100644 index ffe9bda208..9d436881bd --- a/3rdparty/Sabre/HTTP/Response.php +++ b/3rdparty/Sabre/HTTP/Response.php @@ -4,7 +4,7 @@ * Sabre_HTTP_Response * * @package Sabre - * @subpackage HTTP + * @subpackage HTTP * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved. * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License @@ -85,6 +85,9 @@ class Sabre_HTTP_Response { } + // @codeCoverageIgnoreStart + // We cannot reasonably test header() related methods. + /** * Sends an HTTP status header to the client * @@ -114,7 +117,9 @@ class Sabre_HTTP_Response { return header($name . ': ' . $value, $replace); else return false; + } + // @codeCoverageIgnoreEnd /** * Sets a bunch of HTTP Headers diff --git a/3rdparty/Sabre/HTTP/Util.php b/3rdparty/Sabre/HTTP/Util.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/HTTP/Version.php b/3rdparty/Sabre/HTTP/Version.php old mode 100755 new mode 100644 index e6b4f7e535..8ccd7c9edf --- a/3rdparty/Sabre/HTTP/Version.php +++ b/3rdparty/Sabre/HTTP/Version.php @@ -14,7 +14,7 @@ class Sabre_HTTP_Version { /** * Full version number */ - const VERSION = '1.6.4'; + const VERSION = '1.7.0'; /** * Stability : alpha, beta, stable diff --git a/3rdparty/Sabre/HTTP/includes.php b/3rdparty/Sabre/HTTP/includes.php old mode 100755 new mode 100644 diff --git a/3rdparty/Sabre/VObject/Component.php b/3rdparty/Sabre/VObject/Component.php old mode 100755 new mode 100644 index b78a26133f..7604e3a9bb --- a/3rdparty/Sabre/VObject/Component.php +++ b/3rdparty/Sabre/VObject/Component.php @@ -1,5 +1,7 @@ 'Sabre_VObject_Component_VCalendar', - 'VEVENT' => 'Sabre_VObject_Component_VEvent', - 'VTODO' => 'Sabre_VObject_Component_VTodo', - 'VJOURNAL' => 'Sabre_VObject_Component_VJournal', - 'VALARM' => 'Sabre_VObject_Component_VAlarm', + 'VALARM' => 'Sabre\\VObject\\Component\\VAlarm', + 'VCALENDAR' => 'Sabre\\VObject\\Component\\VCalendar', + 'VCARD' => 'Sabre\\VObject\\Component\\VCard', + 'VEVENT' => 'Sabre\\VObject\\Component\\VEvent', + 'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal', + 'VTODO' => 'Sabre\\VObject\\Component\\VTodo', + 'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy', ); /** @@ -50,7 +52,7 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { * * @param string $name * @param string $value - * @return Sabre_VObject_Component + * @return Component */ static public function create($name, $value = null) { @@ -71,9 +73,9 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { * be overridden with the iterator argument * * @param string $name - * @param Sabre_VObject_ElementList $iterator + * @param ElementList $iterator */ - public function __construct($name, Sabre_VObject_ElementList $iterator = null) { + public function __construct($name, ElementList $iterator = null) { $this->name = strtoupper($name); if (!is_null($iterator)) $this->iterator = $iterator; @@ -94,40 +96,54 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { * * This is solely used by the childrenSort method. * - * A higher score means the item will be higher in the list + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accomodate elements. The $key is added to the $score to + * preserve the original relative order of elements. * - * @param Sabre_VObject_Node $n + * @param int $key + * @param array $array * @return int */ - $sortScore = function($n) { + $sortScore = function($key, $array) { + + if ($array[$key] instanceof Component) { - if ($n instanceof Sabre_VObject_Component) { // We want to encode VTIMEZONE first, this is a personal // preference. - if ($n->name === 'VTIMEZONE') { - return 1; + if ($array[$key]->name === 'VTIMEZONE') { + $score=300000000; + return $score+$key; } else { - return 0; + $score=400000000; + return $score+$key; } } else { + // Properties get encoded first // VCARD version 4.0 wants the VERSION property to appear first - if ($n->name === 'VERSION') { - return 3; - } else { - return 2; + if ($array[$key] instanceof Property) { + if ($array[$key]->name === 'VERSION') { + $score=100000000; + return $score+$key; + } else { + // All other properties + $score=200000000; + return $score+$key; + } } } }; - usort($this->children, function($a, $b) use ($sortScore) { + $tmp = $this->children; + uksort($this->children, function($a, $b) use ($sortScore, $tmp) { - $sA = $sortScore($a); - $sB = $sortScore($b); + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); if ($sA === $sB) return 0; - return ($sA > $sB) ? -1 : 1; + return ($sA < $sB) ? -1 : 1; }); @@ -143,8 +159,8 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { * * You can call this method with the following syntaxes: * - * add(Sabre_VObject_Element $element) - * add(string $name, $value) + * add(Node $node) + * add(string $name, $value, array $parameters = array()) * * The first version adds an Element * The second adds a property as a string. @@ -153,26 +169,23 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { * @param mixed $itemValue * @return void */ - public function add($item, $itemValue = null) { + public function add($item, $itemValue = null, array $parameters = array()) { - if ($item instanceof Sabre_VObject_Element) { + if ($item instanceof Node) { if (!is_null($itemValue)) { - throw new InvalidArgumentException('The second argument must not be specified, when passing a VObject'); + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); } $item->parent = $this; $this->children[] = $item; } elseif(is_string($item)) { - if (!is_scalar($itemValue)) { - throw new InvalidArgumentException('The second argument must be scalar'); - } - $item = Sabre_VObject_Property::create($item,$itemValue); + $item = Property::create($item,$itemValue, $parameters); $item->parent = $this; $this->children[] = $item; } else { - throw new InvalidArgumentException('The first argument must either be a Sabre_VObject_Element or a string'); + throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); } @@ -181,11 +194,11 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { /** * Returns an iterable list of children * - * @return Sabre_VObject_ElementList + * @return ElementList */ public function children() { - return new Sabre_VObject_ElementList($this->children); + return new ElementList($this->children); } @@ -218,7 +231,7 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { if ( strtoupper($child->name) === $name && - (is_null($group) || ( $child instanceof Sabre_VObject_Property && strtoupper($child->group) === $group)) + (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group)) ) { $result[$key] = $child; @@ -241,7 +254,7 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { $result = array(); foreach($this->children as $child) { - if ($child instanceof Sabre_VObject_Component) { + if ($child instanceof Component) { $result[] = $child; } } @@ -250,6 +263,33 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { } + /** + * 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) { + + $result = array(); + foreach($this->children as $child) { + $result = array_merge($result, $child->validate($options)); + } + return $result; + + } + /* Magic property accessors {{{ */ /** @@ -259,7 +299,7 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { * null is returned. * * @param string $name - * @return Sabre_VObject_Property + * @return Property */ public function __get($name) { @@ -268,8 +308,8 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { return null; } else { $firstMatch = current($matches); - /** @var $firstMatch Sabre_VObject_Property */ - $firstMatch->setIterator(new Sabre_VObject_ElementList(array_values($matches))); + /** @var $firstMatch Property */ + $firstMatch->setIterator(new ElementList(array_values($matches))); return $firstMatch; } @@ -291,7 +331,7 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { /** * Using the setter method you can add properties or subcomponents * - * You can either pass a Sabre_VObject_Component, Sabre_VObject_Property + * 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 @@ -306,7 +346,7 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { $matches = $this->select($name); $overWrite = count($matches)?key($matches):null; - if ($value instanceof Sabre_VObject_Component || $value instanceof Sabre_VObject_Property) { + if ($value instanceof Component || $value instanceof Property) { $value->parent = $this; if (!is_null($overWrite)) { $this->children[$overWrite] = $value; @@ -314,7 +354,7 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { $this->children[] = $value; } } elseif (is_scalar($value)) { - $property = Sabre_VObject_Property::create($name,$value); + $property = Property::create($name,$value); $property->parent = $this; if (!is_null($overWrite)) { $this->children[$overWrite] = $property; @@ -322,7 +362,7 @@ class Sabre_VObject_Component extends Sabre_VObject_Element { $this->children[] = $property; } } else { - throw new InvalidArgumentException('You must pass a Sabre_VObject_Component, Sabre_VObject_Property or scalar type'); + throw new \InvalidArgumentException('You must pass a \\Sabre\\VObject\\Component, \\Sabre\\VObject\\Property or scalar type'); } } diff --git a/3rdparty/Sabre/VObject/Component/VAlarm.php b/3rdparty/Sabre/VObject/Component/VAlarm.php old mode 100755 new mode 100644 index ebb4a9b18f..383e16eef1 --- a/3rdparty/Sabre/VObject/Component/VAlarm.php +++ b/3rdparty/Sabre/VObject/Component/VAlarm.php @@ -1,17 +1,18 @@ TRIGGER; if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') { - $triggerDuration = Sabre_VObject_DateTimeParser::parseDuration($this->TRIGGER); + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START'; $parentComponent = $this->parent; if ($related === 'START') { - $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); + + if ($parentComponent->name === 'VTODO') { + $propName = 'DUE'; + } else { + $propName = 'DTSTART'; + } + + $effectiveTrigger = clone $parentComponent->$propName->getDateTime(); $effectiveTrigger->add($triggerDuration); } else { if ($parentComponent->name === 'VTODO') { @@ -37,7 +45,7 @@ class Sabre_VObject_Component_VAlarm extends Sabre_VObject_Component { } elseif ($parentComponent->name === 'VEVENT') { $endProp = 'DTEND'; } else { - throw new Sabre_DAV_Exception('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); + throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); } if (isset($parentComponent->$endProp)) { @@ -45,7 +53,7 @@ class Sabre_VObject_Component_VAlarm extends Sabre_VObject_Component { $effectiveTrigger->add($triggerDuration); } elseif (isset($parentComponent->DURATION)) { $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime(); - $duration = Sabre_VObject_DateTimeParser::parseDuration($parentComponent->DURATION); + $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); $effectiveTrigger->add($duration); $effectiveTrigger->add($triggerDuration); } else { @@ -67,22 +75,22 @@ class Sabre_VObject_Component_VAlarm extends Sabre_VObject_Component { * 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 + * @param \DateTime $start + * @param \DateTime $end * @return bool */ - public function isInTimeRange(DateTime $start, DateTime $end) { + public function isInTimeRange(\DateTime $start, \DateTime $end) { $effectiveTrigger = $this->getEffectiveTriggerTime(); if (isset($this->DURATION)) { - $duration = Sabre_VObject_DateTimeParser::parseDuration($this->DURATION); + $duration = VObject\DateTimeParser::parseDuration($this->DURATION); $repeat = (string)$this->repeat; if (!$repeat) { $repeat = 1; } - $period = new DatePeriod($effectiveTrigger, $duration, (int)$repeat); + $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat); foreach($period as $occurrence) { @@ -98,5 +106,3 @@ class Sabre_VObject_Component_VAlarm extends Sabre_VObject_Component { } } - -?> diff --git a/3rdparty/Sabre/VObject/Component/VCalendar.php b/3rdparty/Sabre/VObject/Component/VCalendar.php old mode 100755 new mode 100644 index f3be29afdb..73f2f6d34f --- a/3rdparty/Sabre/VObject/Component/VCalendar.php +++ b/3rdparty/Sabre/VObject/Component/VCalendar.php @@ -1,17 +1,19 @@ children as $component) { - if (!$component instanceof Sabre_VObject_Component) + if (!$component instanceof VObject\Component) continue; if (isset($component->{'RECURRENCE-ID'})) @@ -69,7 +71,7 @@ class Sabre_VObject_Component_VCalendar extends Sabre_VObject_Component { * @param DateTime $end * @return void */ - public function expand(DateTime $start, DateTime $end) { + public function expand(\DateTime $start, \DateTime $end) { $newEvents = array(); @@ -91,10 +93,10 @@ class Sabre_VObject_Component_VCalendar extends Sabre_VObject_Component { $uid = (string)$vevent->uid; if (!$uid) { - throw new LogicException('Event did not have a UID!'); + throw new \LogicException('Event did not have a UID!'); } - $it = new Sabre_VObject_RecurrenceIterator($this, $vevent->uid); + $it = new VObject\RecurrenceIterator($this, $vevent->uid); $it->fastForward($start); while($it->valid() && $it->getDTStart() < $end) { @@ -114,9 +116,9 @@ class Sabre_VObject_Component_VCalendar extends Sabre_VObject_Component { foreach($newEvents as $newEvent) { foreach($newEvent->children as $child) { - if ($child instanceof Sabre_VObject_Property_DateTime && - $child->getDateType() == Sabre_VObject_Property_DateTime::LOCALTZ) { - $child->setDateTime($child->getDateTime(),Sabre_VObject_Property_DateTime::UTC); + if ($child instanceof VObject\Property\DateTime && + $child->getDateType() == VObject\Property\DateTime::LOCALTZ) { + $child->setDateTime($child->getDateTime(),VObject\Property\DateTime::UTC); } } @@ -129,5 +131,112 @@ class Sabre_VObject_Component_VCalendar extends Sabre_VObject_Component { } + /** + * Validates the node for correctness. + * 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) + * + * @return array + */ + /* + public function validate() { + + $warnings = array(); + + $version = $this->select('VERSION'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The VERSION property must appear in the VCALENDAR component exactly 1 time', + 'node' => $this, + ); + } else { + if ((string)$this->VERSION !== '2.0') { + $warnings[] = array( + 'level' => 1, + 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', + 'node' => $this, + ); + } + } + $version = $this->select('PRODID'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The PRODID property must appear in the VCALENDAR component exactly 1 time', + 'node' => $this, + ); + } + if (count($this->CALSCALE) > 1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The CALSCALE property must not be specified more than once.', + 'node' => $this, + ); + } + if (count($this->METHOD) > 1) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The METHOD property must not be specified more than once.', + 'node' => $this, + ); + } + + $allowedComponents = array( + 'VEVENT', + 'VTODO', + 'VJOURNAL', + 'VFREEBUSY', + 'VTIMEZONE', + ); + $allowedProperties = array( + 'PRODID', + 'VERSION', + 'CALSCALE', + 'METHOD', + ); + $componentsFound = 0; + foreach($this->children as $child) { + if($child instanceof Component) { + $componentsFound++; + if (!in_array($child->name, $allowedComponents)) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The ' . $child->name . " component is not allowed in the VCALENDAR component", + 'node' => $this, + ); + } + } + if ($child instanceof Property) { + if (!in_array($child->name, $allowedProperties)) { + $warnings[] = array( + 'level' => 2, + 'message' => 'The ' . $child->name . " property is not allowed in the VCALENDAR component", + 'node' => $this, + ); + } + } + } + + if ($componentsFound===0) { + $warnings[] = array( + 'level' => 1, + 'message' => 'An iCalendar object must have at least 1 component.', + 'node' => $this, + ); + } + + return array_merge( + $warnings, + parent::validate() + ); + + } + */ + } diff --git a/3rdparty/Sabre/VObject/Component/VCard.php b/3rdparty/Sabre/VObject/Component/VCard.php new file mode 100644 index 0000000000..b292698555 --- /dev/null +++ b/3rdparty/Sabre/VObject/Component/VCard.php @@ -0,0 +1,105 @@ +select('VERSION'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The VERSION property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->VERSION = self::DEFAULT_VERSION; + } + } else { + $version = (string)$this->VERSION; + if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') { + $warnings[] = array( + 'level' => 1, + '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 = '4.0'; + } + } + + } + $version = $this->select('FN'); + if (count($version)!==1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The FN property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + if (($options & self::REPAIR) && count($version) === 0) { + // We're going to try to see if we can use the contents of the + // N property. + if (isset($this->N)) { + $value = explode(';', (string)$this->N); + if (isset($value[1]) && $value[1]) { + $this->FN = $value[1] . ' ' . $value[0]; + } else { + $this->FN = $value[0]; + } + + // Otherwise, the ORG property may work + } elseif (isset($this->ORG)) { + $this->FN = (string)$this->ORG; + } + + } + } + + return array_merge( + parent::validate($options), + $warnings + ); + + } + +} + diff --git a/3rdparty/Sabre/VObject/Component/VEvent.php b/3rdparty/Sabre/VObject/Component/VEvent.php old mode 100755 new mode 100644 index d6b910874d..9d10966e20 --- a/3rdparty/Sabre/VObject/Component/VEvent.php +++ b/3rdparty/Sabre/VObject/Component/VEvent.php @@ -1,17 +1,18 @@ RRULE) { - $it = new Sabre_VObject_RecurrenceIterator($this); + $it = new VObject\RecurrenceIterator($this); $it->fastForward($start); // We fast-forwarded to a spot where the end-time of the @@ -53,8 +54,8 @@ class Sabre_VObject_Component_VEvent extends Sabre_VObject_Component { } elseif (isset($this->DURATION)) { $effectiveEnd = clone $effectiveStart; - $effectiveEnd->add( Sabre_VObject_DateTimeParser::parseDuration($this->DURATION) ); - } elseif ($this->DTSTART->getDateType() == Sabre_VObject_Element_DateTime::DATE) { + $effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) ); + } elseif ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) { $effectiveEnd = clone $effectiveStart; $effectiveEnd->modify('+1 day'); } else { @@ -67,5 +68,3 @@ class Sabre_VObject_Component_VEvent extends Sabre_VObject_Component { } } - -?> diff --git a/3rdparty/Sabre/VObject/Component/VFreeBusy.php b/3rdparty/Sabre/VObject/Component/VFreeBusy.php new file mode 100644 index 0000000000..d6da52cbd7 --- /dev/null +++ b/3rdparty/Sabre/VObject/Component/VFreeBusy.php @@ -0,0 +1,68 @@ +select('FREEBUSY') as $freebusy) { + + // We are only interested in FBTYPE=BUSY (the default), + // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. + if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') { + continue; + } + + // The freebusy component can hold more than 1 value, separated by + // commas. + $periods = explode(',', (string)$freebusy); + + foreach($periods as $period) { + // Every period is formatted as [start]/[end]. The start is an + // absolute UTC time, the end may be an absolute UTC time, or + // duration (relative) value. + list($busyStart, $busyEnd) = explode('/', $period); + + $busyStart = VObject\DateTimeParser::parse($busyStart); + $busyEnd = VObject\DateTimeParser::parse($busyEnd); + if ($busyEnd instanceof \DateInterval) { + $tmp = clone $busyStart; + $tmp->add($busyEnd); + $busyEnd = $tmp; + } + + if($start < $busyEnd && $end > $busyStart) { + return false; + } + + } + + } + + return true; + + } + +} + diff --git a/3rdparty/Sabre/VObject/Component/VJournal.php b/3rdparty/Sabre/VObject/Component/VJournal.php old mode 100755 new mode 100644 index 22b3ec921e..f104a1f66e --- a/3rdparty/Sabre/VObject/Component/VJournal.php +++ b/3rdparty/Sabre/VObject/Component/VJournal.php @@ -1,17 +1,19 @@ DTSTART)?$this->DTSTART->getDateTime():null; if ($dtstart) { $effectiveEnd = clone $dtstart; - if ($this->DTSTART->getDateType() == Sabre_VObject_Element_DateTime::DATE) { + if ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) { $effectiveEnd->modify('+1 day'); } @@ -42,5 +44,3 @@ class Sabre_VObject_Component_VJournal extends Sabre_VObject_Component { } } - -?> diff --git a/3rdparty/Sabre/VObject/Component/VTodo.php b/3rdparty/Sabre/VObject/Component/VTodo.php old mode 100755 new mode 100644 index 79d06298d7..5f879aea43 --- a/3rdparty/Sabre/VObject/Component/VTodo.php +++ b/3rdparty/Sabre/VObject/Component/VTodo.php @@ -1,17 +1,19 @@ DTSTART)?$this->DTSTART->getDateTime():null; - $duration = isset($this->DURATION)?Sabre_VObject_DateTimeParser::parseDuration($this->DURATION):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; @@ -64,5 +66,3 @@ class Sabre_VObject_Component_VTodo extends Sabre_VObject_Component { } } - -?> diff --git a/3rdparty/Sabre/VObject/DateTimeParser.php b/3rdparty/Sabre/VObject/DateTimeParser.php old mode 100755 new mode 100644 index 23a4bb6991..d09ded9676 --- a/3rdparty/Sabre/VObject/DateTimeParser.php +++ b/3rdparty/Sabre/VObject/DateTimeParser.php @@ -1,18 +1,18 @@ setTimeZone(new DateTimeZone('UTC')); + $date->setTimeZone(new \DateTimeZone('UTC')); return $date; } @@ -54,13 +54,13 @@ class Sabre_VObject_DateTimeParser { static public function parseDate($date) { // Format is YYYYMMDD - $result = preg_match('/^([1-3][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches); + $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches); if (!$result) { - throw new Sabre_DAV_Exception_BadRequest('The supplied iCalendar date value is incorrect: ' . $date); + throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date); } - $date = new DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new DateTimeZone('UTC')); + $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new \DateTimeZone('UTC')); return $date; } @@ -79,7 +79,7 @@ class Sabre_VObject_DateTimeParser { $result = preg_match('/^(?P\+|-)?P((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$/', $duration, $matches); if (!$result) { - throw new Sabre_DAV_Exception_BadRequest('The supplied iCalendar duration value is incorrect: ' . $duration); + throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration); } if (!$asString) { @@ -128,7 +128,7 @@ class Sabre_VObject_DateTimeParser { if ($duration==='P') { $duration = 'PT0S'; } - $iv = new DateInterval($duration); + $iv = new \DateInterval($duration); if ($invert) $iv->invert = true; return $iv; diff --git a/3rdparty/Sabre/VObject/Element.php b/3rdparty/Sabre/VObject/Element.php deleted file mode 100755 index e20ff0b353..0000000000 --- a/3rdparty/Sabre/VObject/Element.php +++ /dev/null @@ -1,16 +0,0 @@ -vevent where there's multiple VEVENT objects. * - * @package Sabre - * @subpackage VObject * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved. * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ -class Sabre_VObject_ElementList implements Iterator, Countable, ArrayAccess { +class ElementList implements \Iterator, \Countable, \ArrayAccess { /** * Inner elements @@ -44,7 +44,7 @@ class Sabre_VObject_ElementList implements Iterator, Countable, ArrayAccess { /** * Returns current item in iteration * - * @return Sabre_VObject_Element + * @return Element */ public function current() { @@ -149,7 +149,7 @@ class Sabre_VObject_ElementList implements Iterator, Countable, ArrayAccess { */ public function offsetSet($offset,$value) { - throw new LogicException('You can not add new objects to an ElementList'); + throw new \LogicException('You can not add new objects to an ElementList'); } @@ -163,7 +163,7 @@ class Sabre_VObject_ElementList implements Iterator, Countable, ArrayAccess { */ public function offsetUnset($offset) { - throw new LogicException('You can not remove objects from an ElementList'); + throw new \LogicException('You can not remove objects from an ElementList'); } diff --git a/3rdparty/Sabre/VObject/FreeBusyGenerator.php b/3rdparty/Sabre/VObject/FreeBusyGenerator.php old mode 100755 new mode 100644 index 1c96a64a00..c607d119ce --- a/3rdparty/Sabre/VObject/FreeBusyGenerator.php +++ b/3rdparty/Sabre/VObject/FreeBusyGenerator.php @@ -1,5 +1,7 @@ setTimeRange($start, $end); + } + + if ($objects) { + $this->setObjects($objects); + } + + } + /** * Sets the VCALENDAR object. * @@ -54,10 +77,10 @@ class Sabre_VObject_FreeBusyGenerator { * * The VFREEBUSY object will be automatically added though. * - * @param Sabre_VObject_Component $vcalendar + * @param Component $vcalendar * @return void */ - public function setBaseObject(Sabre_VObject_Component $vcalendar) { + public function setBaseObject(Component $vcalendar) { $this->baseObject = $vcalendar; @@ -66,22 +89,28 @@ class Sabre_VObject_FreeBusyGenerator { /** * Sets the input objects * - * Every object must either be a string or a Sabre_VObject_Component. + * You must either specify a valendar object as a strong, or as the parse + * Component. + * It's also possible to specify multiple objects as an array. * - * @param array $objects + * @param mixed $objects * @return void */ - public function setObjects(array $objects) { + public function setObjects($objects) { + + if (!is_array($objects)) { + $objects = array($objects); + } $this->objects = array(); foreach($objects as $object) { if (is_string($object)) { - $this->objects[] = Sabre_VObject_Reader::read($object); - } elseif ($object instanceof Sabre_VObject_Component) { + $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'); + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects'); } } @@ -97,7 +126,7 @@ class Sabre_VObject_FreeBusyGenerator { * @param DateTime $end * @return void */ - public function setTimeRange(DateTime $start = null, DateTime $end = null) { + public function setTimeRange(\DateTime $start = null, \DateTime $end = null) { $this->start = $start; $this->end = $end; @@ -108,7 +137,7 @@ class Sabre_VObject_FreeBusyGenerator { * Parses the input data and returns a correct VFREEBUSY object, wrapped in * a VCALENDAR. * - * @return Sabre_VObject_Component + * @return Component */ public function getResult() { @@ -140,7 +169,7 @@ class Sabre_VObject_FreeBusyGenerator { if ($component->RRULE) { - $iterator = new Sabre_VObject_RecurrenceIterator($object, (string)$component->uid); + $iterator = new RecurrenceIterator($object, (string)$component->uid); if ($this->start) { $iterator->fastForward($this->start); } @@ -172,10 +201,10 @@ class Sabre_VObject_FreeBusyGenerator { if (isset($component->DTEND)) { $endTime = $component->DTEND->getDateTime(); } elseif (isset($component->DURATION)) { - $duration = Sabre_VObject_DateTimeParser::parseDuration((string)$component->DURATION); + $duration = DateTimeParser::parseDuration((string)$component->DURATION); $endTime = clone $startTime; $endTime->add($duration); - } elseif ($component->DTSTART->getDateType() === Sabre_VObject_Property_DateTime::DATE) { + } elseif ($component->DTSTART->getDateType() === Property\DateTime::DATE) { $endTime = clone $startTime; $endTime->modify('+1 day'); } else { @@ -212,14 +241,14 @@ class Sabre_VObject_FreeBusyGenerator { $values = explode(',', $freebusy); foreach($values as $value) { list($startTime, $endTime) = explode('/', $value); - $startTime = Sabre_VObject_DateTimeParser::parseDateTime($startTime); + $startTime = DateTimeParser::parseDateTime($startTime); if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') { - $duration = Sabre_VObject_DateTimeParser::parseDuration($endTime); + $duration = DateTimeParser::parseDuration($endTime); $endTime = clone $startTime; $endTime->add($duration); } else { - $endTime = Sabre_VObject_DateTimeParser::parseDateTime($endTime); + $endTime = DateTimeParser::parseDateTime($endTime); } if($this->start && $this->start > $endTime) continue; @@ -248,39 +277,35 @@ class Sabre_VObject_FreeBusyGenerator { if ($this->baseObject) { $calendar = $this->baseObject; } else { - $calendar = new Sabre_VObject_Component('VCALENDAR'); + $calendar = new Component('VCALENDAR'); $calendar->version = '2.0'; - if (Sabre_DAV_Server::$exposeVersion) { - $calendar->prodid = '-//SabreDAV//Sabre VObject ' . Sabre_VObject_Version::VERSION . '//EN'; - } else { - $calendar->prodid = '-//SabreDAV//Sabre VObject//EN'; - } + $calendar->prodid = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN'; $calendar->calscale = 'GREGORIAN'; } - $vfreebusy = new Sabre_VObject_Component('VFREEBUSY'); + $vfreebusy = new Component('VFREEBUSY'); $calendar->add($vfreebusy); if ($this->start) { - $dtstart = new Sabre_VObject_Property_DateTime('DTSTART'); - $dtstart->setDateTime($this->start,Sabre_VObject_Property_DateTime::UTC); + $dtstart = new Property\DateTime('DTSTART'); + $dtstart->setDateTime($this->start,Property\DateTime::UTC); $vfreebusy->add($dtstart); } if ($this->end) { - $dtend = new Sabre_VObject_Property_DateTime('DTEND'); - $dtend->setDateTime($this->start,Sabre_VObject_Property_DateTime::UTC); + $dtend = new Property\DateTime('DTEND'); + $dtend->setDateTime($this->end,Property\DateTime::UTC); $vfreebusy->add($dtend); } - $dtstamp = new Sabre_VObject_Property_DateTime('DTSTAMP'); - $dtstamp->setDateTime(new DateTime('now'), Sabre_VObject_Property_DateTime::UTC); + $dtstamp = new Property\DateTime('DTSTAMP'); + $dtstamp->setDateTime(new \DateTime('now'), Property\DateTime::UTC); $vfreebusy->add($dtstamp); foreach($busyTimes as $busyTime) { - $busyTime[0]->setTimeZone(new DateTimeZone('UTC')); - $busyTime[1]->setTimeZone(new DateTimeZone('UTC')); + $busyTime[0]->setTimeZone(new \DateTimeZone('UTC')); + $busyTime[1]->setTimeZone(new \DateTimeZone('UTC')); - $prop = new Sabre_VObject_Property( + $prop = new Property( 'FREEBUSY', $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z') ); diff --git a/3rdparty/Sabre/VObject/Node.php b/3rdparty/Sabre/VObject/Node.php old mode 100755 new mode 100644 index d89e01b56c..5d2e1ce300 --- a/3rdparty/Sabre/VObject/Node.php +++ b/3rdparty/Sabre/VObject/Node.php @@ -1,15 +1,20 @@ iterator)) return $this->iterator; - return new Sabre_VObject_ElementList(array($this)); + return new ElementList(array($this)); } @@ -53,10 +81,10 @@ abstract class Sabre_VObject_Node implements IteratorAggregate, ArrayAccess, Cou * * Note that this is not actually part of the iterator interface * - * @param Sabre_VObject_ElementList $iterator + * @param ElementList $iterator * @return void */ - public function setIterator(Sabre_VObject_ElementList $iterator) { + public function setIterator(ElementList $iterator) { $this->iterator = $iterator; @@ -125,9 +153,14 @@ abstract class Sabre_VObject_Node implements IteratorAggregate, ArrayAccess, Cou public function offsetSet($offset,$value) { $iterator = $this->getIterator(); - return $iterator->offsetSet($offset,$value); + $iterator->offsetSet($offset,$value); + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace } + // @codeCoverageIgnoreEnd /** * Sets an item through ArrayAccess. @@ -140,9 +173,14 @@ abstract class Sabre_VObject_Node implements IteratorAggregate, ArrayAccess, Cou public function offsetUnset($offset) { $iterator = $this->getIterator(); - return $iterator->offsetUnset($offset); + $iterator->offsetUnset($offset); + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace } + // @codeCoverageIgnoreEnd /* }}} */ diff --git a/3rdparty/Sabre/VObject/Parameter.php b/3rdparty/Sabre/VObject/Parameter.php old mode 100755 new mode 100644 index 2e39af5f78..d6d7c54c3b --- a/3rdparty/Sabre/VObject/Parameter.php +++ b/3rdparty/Sabre/VObject/Parameter.php @@ -1,5 +1,7 @@ name = strtoupper($name); $this->value = $value; diff --git a/3rdparty/Sabre/VObject/ParseException.php b/3rdparty/Sabre/VObject/ParseException.php old mode 100755 new mode 100644 index 1b5e95bf16..91386fec53 --- a/3rdparty/Sabre/VObject/ParseException.php +++ b/3rdparty/Sabre/VObject/ParseException.php @@ -1,12 +1,12 @@ 'Sabre_VObject_Property_DateTime', - 'CREATED' => 'Sabre_VObject_Property_DateTime', - 'DTEND' => 'Sabre_VObject_Property_DateTime', - 'DTSTAMP' => 'Sabre_VObject_Property_DateTime', - 'DTSTART' => 'Sabre_VObject_Property_DateTime', - 'DUE' => 'Sabre_VObject_Property_DateTime', - 'EXDATE' => 'Sabre_VObject_Property_MultiDateTime', - 'LAST-MODIFIED' => 'Sabre_VObject_Property_DateTime', - 'RECURRENCE-ID' => 'Sabre_VObject_Property_DateTime', - 'TRIGGER' => 'Sabre_VObject_Property_DateTime', + 'COMPLETED' => 'Sabre\\VObject\\Property\\DateTime', + 'CREATED' => 'Sabre\\VObject\\Property\\DateTime', + 'DTEND' => 'Sabre\\VObject\\Property\\DateTime', + 'DTSTAMP' => 'Sabre\\VObject\\Property\\DateTime', + 'DTSTART' => 'Sabre\\VObject\\Property\\DateTime', + 'DUE' => 'Sabre\\VObject\\Property\\DateTime', + 'EXDATE' => 'Sabre\\VObject\\Property\\MultiDateTime', + 'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\DateTime', + 'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\DateTime', + 'TRIGGER' => 'Sabre\\VObject\\Property\\DateTime', + 'N' => 'Sabre\\VObject\\Property\\Compound', + 'ORG' => 'Sabre\\VObject\\Property\\Compound', + 'ADR' => 'Sabre\\VObject\\Property\\Compound', + 'CATEGORIES' => 'Sabre\\VObject\\Property\\Compound', ); /** * Creates the new property by name, but in addition will also see if * there's a class mapped to the property name. * + * Parameters can be specified with the optional third argument. Parameters + * must be a key->value map of the parameter name, and value. If the value + * is specified as an array, it is assumed that multiple parameters with + * the same name should be added. + * * @param string $name * @param string $value - * @return void + * @param array $parameters + * @return Property */ - static public function create($name, $value = null) { + static public function create($name, $value = null, array $parameters = array()) { $name = strtoupper($name); $shortName = $name; @@ -87,9 +97,9 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { } if (isset(self::$classMap[$shortName])) { - return new self::$classMap[$shortName]($name, $value); + return new self::$classMap[$shortName]($name, $value, $parameters); } else { - return new self($name, $value); + return new self($name, $value, $parameters); } } @@ -97,14 +107,20 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { /** * Creates a new property object * - * By default this object will iterate over its own children, but this can - * be overridden with the iterator argument + * Parameters can be specified with the optional third argument. Parameters + * must be a key->value map of the parameter name, and value. If the value + * is specified as an array, it is assumed that multiple parameters with + * the same name should be added. * * @param string $name * @param string $value - * @param Sabre_VObject_ElementList $iterator + * @param array $parameters */ - public function __construct($name, $value = null, $iterator = null) { + public function __construct($name, $value = null, array $parameters = array()) { + + if (!is_scalar($value) && !is_null($value)) { + throw new \InvalidArgumentException('The value argument must be scalar or null'); + } $name = strtoupper($name); $group = null; @@ -113,13 +129,22 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { } $this->name = $name; $this->group = $group; - if (!is_null($iterator)) $this->iterator = $iterator; $this->setValue($value); + foreach($parameters as $paramName => $paramValues) { + + if (!is_array($paramValues)) { + $paramValues = array($paramValues); + } + + foreach($paramValues as $paramValue) { + $this->add($paramName, $paramValue); + } + + } + } - - /** * Updates the internal value * @@ -142,13 +167,12 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { $str = $this->name; if ($this->group) $str = $this->group . '.' . $this->name; - if (count($this->parameters)) { - foreach($this->parameters as $param) { + foreach($this->parameters as $param) { - $str.=';' . $param->serialize(); + $str.=';' . $param->serialize(); - } } + $src = array( '\\', "\n", @@ -180,7 +204,7 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { * * You can call this method with the following syntaxes: * - * add(Sabre_VObject_Parameter $element) + * add(Parameter $element) * add(string $name, $value) * * The first version adds an Parameter @@ -192,24 +216,21 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { */ public function add($item, $itemValue = null) { - if ($item instanceof Sabre_VObject_Parameter) { + if ($item instanceof Parameter) { if (!is_null($itemValue)) { - throw new InvalidArgumentException('The second argument must not be specified, when passing a VObject'); + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject'); } $item->parent = $this; $this->parameters[] = $item; } elseif(is_string($item)) { - if (!is_scalar($itemValue) && !is_null($itemValue)) { - throw new InvalidArgumentException('The second argument must be scalar'); - } - $parameter = new Sabre_VObject_Parameter($item,$itemValue); + $parameter = new Parameter($item,$itemValue); $parameter->parent = $this; $this->parameters[] = $parameter; } else { - throw new InvalidArgumentException('The first argument must either be a Sabre_VObject_Element or a string'); + throw new \InvalidArgumentException('The first argument must either be a Node a string'); } @@ -240,7 +261,7 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { * Returns a parameter, or parameter list. * * @param string $name - * @return Sabre_VObject_Element + * @return Node */ public function offsetGet($name) { @@ -258,7 +279,7 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { } elseif (count($result)===1) { return $result[0]; } else { - $result[0]->setIterator(new Sabre_VObject_ElementList($result)); + $result[0]->setIterator(new ElementList($result)); return $result[0]; } @@ -273,25 +294,25 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { */ public function offsetSet($name, $value) { - if (is_int($name)) return parent::offsetSet($name, $value); + if (is_int($name)) parent::offsetSet($name, $value); if (is_scalar($value)) { if (!is_string($name)) - throw new InvalidArgumentException('A parameter name must be specified. This means you cannot use the $array[]="string" to add parameters.'); + throw new \InvalidArgumentException('A parameter name must be specified. This means you cannot use the $array[]="string" to add parameters.'); $this->offsetUnset($name); - $parameter = new Sabre_VObject_Parameter($name, $value); + $parameter = new Parameter($name, $value); $parameter->parent = $this; $this->parameters[] = $parameter; - } elseif ($value instanceof Sabre_VObject_Parameter) { + } elseif ($value instanceof Parameter) { if (!is_null($name)) - throw new InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a Sabre_VObject_Parameter. Add using $array[]=$parameterObject.'); + throw new \InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a \\Sabre\\VObject\\Parameter. Add using $array[]=$parameterObject.'); $value->parent = $this; $this->parameters[] = $value; } else { - throw new InvalidArgumentException('You can only add parameters to the property object'); + throw new \InvalidArgumentException('You can only add parameters to the property object'); } } @@ -304,7 +325,7 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { */ public function offsetUnset($name) { - if (is_int($name)) return parent::offsetUnset($name); + if (is_int($name)) parent::offsetUnset($name); $name = strtoupper($name); foreach($this->parameters as $key=>$parameter) { @@ -345,4 +366,65 @@ class Sabre_VObject_Property extends Sabre_VObject_Element { } + /** + * 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 = array(); + + // Checking if our value is UTF-8 + if (!StringUtil::isUTF8($this->value)) { + $warnings[] = array( + 'level' => 1, + 'message' => 'Property is not valid UTF-8!', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->value = StringUtil::convertToUTF8($this->value); + } + } + + // Checking if the propertyname does not contain any invalid bytes. + if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { + $warnings[] = array( + 'level' => 1, + '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); + + } + + } + + // Validating inner parameters + foreach($this->parameters as $param) { + $warnings = array_merge($warnings, $param->validate($options)); + } + + return $warnings; + + } + } diff --git a/3rdparty/Sabre/VObject/Property/Compound.php b/3rdparty/Sabre/VObject/Property/Compound.php new file mode 100644 index 0000000000..e2c18e7726 --- /dev/null +++ b/3rdparty/Sabre/VObject/Property/Compound.php @@ -0,0 +1,129 @@ + ';', + 'ADR' => ';', + 'ORG' => ';', + 'CATEGORIES' => ',', + ); + + /** + * The currently used delimiter. + * + * @var string + */ + protected $delimiter = null; + + /** + * Get a compound value as an array. + * + * @param $name string + * @return array + */ + public function getParts() { + + if (is_null($this->value)) { + return array(); + } + + $delimiter = $this->getDelimiter(); + + // split by any $delimiter which is NOT prefixed by a slash. + // Note that this is not a a perfect solution. If a value is prefixed + // by two slashes, it should actually be split anyway. + // + // Hopefully we can fix this better in a future version, where we can + // break compatibility a bit. + $compoundValues = preg_split("/(?value); + + // remove slashes from any semicolon and comma left escaped in the single values + $compoundValues = array_map( + function($val) { + return strtr($val, array('\,' => ',', '\;' => ';')); + }, $compoundValues); + + return $compoundValues; + + } + + /** + * Returns the delimiter for this property. + * + * @return string + */ + public function getDelimiter() { + + if (!$this->delimiter) { + if (isset(self::$delimiterMap[$this->name])) { + $this->delimiter = self::$delimiterMap[$this->name]; + } else { + // To be a bit future proof, we are going to default the + // delimiter to ; + $this->delimiter = ';'; + } + } + return $this->delimiter; + + } + + /** + * Set a compound value as an array. + * + * + * @param $name string + * @return array + */ + public function setParts(array $values) { + + // add slashes to all semicolons and commas in the single values + $values = array_map( + function($val) { + return strtr($val, array(',' => '\,', ';' => '\;')); + }, $values); + + $this->setValue( + implode($this->getDelimiter(), $values) + ); + + } + +} diff --git a/3rdparty/Sabre/VObject/Property/DateTime.php b/3rdparty/Sabre/VObject/Property/DateTime.php old mode 100755 new mode 100644 index fe2372caa8..556cd441d8 --- a/3rdparty/Sabre/VObject/Property/DateTime.php +++ b/3rdparty/Sabre/VObject/Property/DateTime.php @@ -1,5 +1,9 @@ offsetSet('VALUE','DATE-TIME'); break; case self::UTC : - $dt->setTimeZone(new DateTimeZone('UTC')); + $dt->setTimeZone(new \DateTimeZone('UTC')); $this->setValue($dt->format('Ymd\\THis\\Z')); $this->offsetUnset('VALUE'); $this->offsetUnset('TZID'); @@ -93,7 +95,7 @@ class Sabre_VObject_Property_DateTime extends Sabre_VObject_Property { $this->offsetSet('VALUE','DATE'); break; default : - throw new InvalidArgumentException('You must pass a valid dateType constant'); + throw new \InvalidArgumentException('You must pass a valid dateType constant'); } $this->dateTime = $dt; @@ -106,7 +108,7 @@ class Sabre_VObject_Property_DateTime extends Sabre_VObject_Property { * * If no value was set, this method returns null. * - * @return DateTime|null + * @return \DateTime|null */ public function getDateTime() { @@ -152,11 +154,11 @@ class Sabre_VObject_Property_DateTime extends Sabre_VObject_Property { * * @param string|null $propertyValue The string to parse (yymmdd or * ymmddThhmmss, etc..) - * @param Sabre_VObject_Property|null $property The instance of the + * @param \Sabre\VObject\Property|null $property The instance of the * property we're parsing. * @return array */ - static public function parseData($propertyValue, Sabre_VObject_Property $property = null) { + static public function parseData($propertyValue, VObject\Property $property = null) { if (is_null($propertyValue)) { return array(null, null); @@ -167,14 +169,14 @@ class Sabre_VObject_Property_DateTime extends Sabre_VObject_Property { $regex = "/^$date(T$time(?PZ)?)?$/"; if (!preg_match($regex, $propertyValue, $matches)) { - throw new InvalidArgumentException($propertyValue . ' is not a valid DateTime or Date string'); + throw new \InvalidArgumentException($propertyValue . ' is not a valid \DateTime or Date string'); } if (!isset($matches['hour'])) { // Date-only return array( self::DATE, - new DateTime($matches['year'] . '-' . $matches['month'] . '-' . $matches['date'] . ' 00:00:00'), + new \DateTime($matches['year'] . '-' . $matches['month'] . '-' . $matches['date'] . ' 00:00:00', new \DateTimeZone('UTC')), ); } @@ -187,8 +189,8 @@ class Sabre_VObject_Property_DateTime extends Sabre_VObject_Property { $matches['second']; if (isset($matches['isutc'])) { - $dt = new DateTime($dateStr,new DateTimeZone('UTC')); - $dt->setTimeZone(new DateTimeZone('UTC')); + $dt = new \DateTime($dateStr,new \DateTimeZone('UTC')); + $dt->setTimeZone(new \DateTimeZone('UTC')); return array( self::UTC, $dt @@ -198,56 +200,27 @@ class Sabre_VObject_Property_DateTime extends Sabre_VObject_Property { // Finding the timezone. $tzid = $property['TZID']; if (!$tzid) { + // This was a floating time string. This implies we use the + // timezone from date_default_timezone_set / date.timezone ini + // setting. return array( self::LOCAL, - new DateTime($dateStr) + new \DateTime($dateStr) ); } - try { - // tzid an Olson identifier? - $tz = new DateTimeZone($tzid->value); - } catch (Exception $e) { - - // Not an Olson id, we're going to try to find the information - // through the time zone name map. - $newtzid = Sabre_VObject_WindowsTimezoneMap::lookup($tzid->value); - if (is_null($newtzid)) { - - // Not a well known time zone name either, we're going to try - // to find the information through the VTIMEZONE object. - - // First we find the root object - $root = $property; - while($root->parent) { - $root = $root->parent; - } - - if (isset($root->VTIMEZONE)) { - foreach($root->VTIMEZONE as $vtimezone) { - if (((string)$vtimezone->TZID) == $tzid) { - if (isset($vtimezone->{'X-LIC-LOCATION'})) { - $newtzid = (string)$vtimezone->{'X-LIC-LOCATION'}; - } else { - // No libical location specified. As a last resort we could - // try matching $vtimezone's DST rules against all known - // time zones returned by DateTimeZone::list* - - // TODO - } - } - } - } - } - - try { - $tz = new DateTimeZone($newtzid); - } catch (Exception $e) { - // If all else fails, we use the default PHP timezone - $tz = new DateTimeZone(date_default_timezone_get()); - } + // To look up the timezone, we must first find the VCALENDAR component. + $root = $property; + while($root->parent) { + $root = $root->parent; } - $dt = new DateTime($dateStr, $tz); + if ($root->name === 'VCALENDAR') { + $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid, $root); + } else { + $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid); + } + + $dt = new \DateTime($dateStr, $tz); $dt->setTimeZone($tz); return array( diff --git a/3rdparty/Sabre/VObject/Property/MultiDateTime.php b/3rdparty/Sabre/VObject/Property/MultiDateTime.php old mode 100755 new mode 100644 index ae53ab6a61..629ef4a134 --- a/3rdparty/Sabre/VObject/Property/MultiDateTime.php +++ b/3rdparty/Sabre/VObject/Property/MultiDateTime.php @@ -1,5 +1,9 @@ offsetUnset('VALUE'); $this->offsetUnset('TZID'); switch($dateType) { - case Sabre_VObject_Property_DateTime::LOCAL : + case DateTime::LOCAL : $val = array(); foreach($dt as $i) { $val[] = $i->format('Ymd\\THis'); @@ -62,16 +64,16 @@ class Sabre_VObject_Property_MultiDateTime extends Sabre_VObject_Property { $this->setValue(implode(',',$val)); $this->offsetSet('VALUE','DATE-TIME'); break; - case Sabre_VObject_Property_DateTime::UTC : + case DateTime::UTC : $val = array(); foreach($dt as $i) { - $i->setTimeZone(new DateTimeZone('UTC')); + $i->setTimeZone(new \DateTimeZone('UTC')); $val[] = $i->format('Ymd\\THis\\Z'); } $this->setValue(implode(',',$val)); $this->offsetSet('VALUE','DATE-TIME'); break; - case Sabre_VObject_Property_DateTime::LOCALTZ : + case DateTime::LOCALTZ : $val = array(); foreach($dt as $i) { $val[] = $i->format('Ymd\\THis'); @@ -80,7 +82,7 @@ class Sabre_VObject_Property_MultiDateTime extends Sabre_VObject_Property { $this->offsetSet('VALUE','DATE-TIME'); $this->offsetSet('TZID', $dt[0]->getTimeZone()->getName()); break; - case Sabre_VObject_Property_DateTime::DATE : + case DateTime::DATE : $val = array(); foreach($dt as $i) { $val[] = $i->format('Ymd'); @@ -89,7 +91,7 @@ class Sabre_VObject_Property_MultiDateTime extends Sabre_VObject_Property { $this->offsetSet('VALUE','DATE'); break; default : - throw new InvalidArgumentException('You must pass a valid dateType constant'); + throw new \InvalidArgumentException('You must pass a valid dateType constant'); } $this->dateTimes = $dt; @@ -121,7 +123,7 @@ class Sabre_VObject_Property_MultiDateTime extends Sabre_VObject_Property { list( $type, $dt - ) = Sabre_VObject_Property_DateTime::parseData($val, $this); + ) = DateTime::parseData($val, $this); $dts[] = $dt; $this->dateType = $type; } @@ -154,7 +156,7 @@ class Sabre_VObject_Property_MultiDateTime extends Sabre_VObject_Property { list( $type, $dt - ) = Sabre_VObject_Property_DateTime::parseData($val, $this); + ) = DateTime::parseData($val, $this); $dts[] = $dt; $this->dateType = $type; } diff --git a/3rdparty/Sabre/VObject/Reader.php b/3rdparty/Sabre/VObject/Reader.php old mode 100755 new mode 100644 index eea73fa3dc..a24590cb38 --- a/3rdparty/Sabre/VObject/Reader.php +++ b/3rdparty/Sabre/VObject/Reader.php @@ -1,5 +1,7 @@ add(self::readLine($lines)); + while(strtoupper(substr($nextLine,0,4))!=="END:") { + $parsedLine = self::readLine($lines, $options); $nextLine = current($lines); + if (is_null($parsedLine)) { + continue; + } + $obj->add($parsedLine); + if ($nextLine===false) - throw new Sabre_VObject_ParseException('Invalid VObject. Document ended prematurely.'); + throw new ParseException('Invalid VObject. Document ended prematurely.'); } // Checking component name of the 'END:' line. if (substr($nextLine,4)!==$obj->name) { - throw new Sabre_VObject_ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"'); + throw new ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"'); } next($lines); @@ -99,19 +126,26 @@ class Sabre_VObject_Reader { // Properties //$result = preg_match('/(?P[A-Z0-9-]+)(?:;(?P^(?([^:^\"]|\"([^\"]*)\")*))?"; $regex = "/^(?P$token)$parameters:(?P.*)$/i"; $result = preg_match($regex,$line,$matches); if (!$result) { - throw new Sabre_VObject_ParseException('Invalid VObject, line ' . ($lineNr+1) . ' did not follow the icalendar/vcard format'); + if ($options & self::OPTION_IGNORE_INVALID_LINES) { + return null; + } else { + throw new ParseException('Invalid VObject, line ' . ($lineNr+1) . ' did not follow the icalendar/vcard format'); + } } $propertyName = strtoupper($matches['name']); - $propertyValue = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) { + $propertyValue = preg_replace_callback('#(\\\\(\\\\|N|n))#',function($matches) { if ($matches[2]==='n' || $matches[2]==='N') { return "\n"; } else { @@ -119,7 +153,7 @@ class Sabre_VObject_Reader { } }, $matches['value']); - $obj = Sabre_VObject_Property::create($propertyName, $propertyValue); + $obj = Property::create($propertyName, $propertyValue); if ($matches['parameters']) { @@ -137,7 +171,7 @@ class Sabre_VObject_Reader { /** * Reads a parameter list from a property * - * This method returns an array of Sabre_VObject_Parameter + * This method returns an array of Parameter * * @param string $parameters * @return array @@ -171,7 +205,7 @@ class Sabre_VObject_Reader { } }, $value); - $params[] = new Sabre_VObject_Parameter($match['paramName'], $value); + $params[] = new Parameter($match['paramName'], $value); } diff --git a/3rdparty/Sabre/VObject/RecurrenceIterator.php b/3rdparty/Sabre/VObject/RecurrenceIterator.php old mode 100755 new mode 100644 index 740270dd8f..46c7f44780 --- a/3rdparty/Sabre/VObject/RecurrenceIterator.php +++ b/3rdparty/Sabre/VObject/RecurrenceIterator.php @@ -1,5 +1,7 @@ name === 'VCALENDAR') { - throw new InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well'); + throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well'); } $components = array($vcal); $uid = (string)$vcal->uid; @@ -325,7 +325,7 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { } } if (!$this->baseEvent) { - throw new InvalidArgumentException('Could not find a base event with uid: ' . $uid); + throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid); } $this->startDate = clone $this->baseEvent->DTSTART->getDateTime(); @@ -336,8 +336,8 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { } else { $this->endDate = clone $this->startDate; if (isset($this->baseEvent->DURATION)) { - $this->endDate->add(Sabre_VObject_DateTimeParser::parse($this->baseEvent->DURATION->value)); - } elseif ($this->baseEvent->DTSTART->getDateType()===Sabre_VObject_Property_DateTime::DATE) { + $this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value)); + } elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) { $this->endDate->modify('+1 day'); } } @@ -347,7 +347,11 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { $parts = explode(';', $rrule); - foreach($parts as $part) { + // If no rrule was specified, we create a default setting + if (!$rrule) { + $this->frequency = 'daily'; + $this->count = 1; + } else foreach($parts as $part) { list($key, $value) = explode('=', $part, 2); @@ -358,14 +362,14 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { strtolower($value), array('secondly','minutely','hourly','daily','weekly','monthly','yearly') )) { - throw new InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value)); + throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value)); } $this->frequency = strtolower($value); break; case 'UNTIL' : - $this->until = Sabre_VObject_DateTimeParser::parse($value); + $this->until = DateTimeParser::parse($value); break; case 'COUNT' : @@ -374,6 +378,9 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { case 'INTERVAL' : $this->interval = (int)$value; + if ($this->interval < 1) { + throw new \InvalidArgumentException('INTERVAL in RRULE must be a positive integer!'); + } break; case 'BYSECOND' : @@ -427,7 +434,7 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { foreach(explode(',', (string)$exDate) as $exceptionDate) { $this->exceptionDates[] = - Sabre_VObject_DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone()); + DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone()); } @@ -485,7 +492,7 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { * * This method always returns a cloned instance. * - * @return void + * @return Component\VEvent */ public function getEventObject() { @@ -561,7 +568,7 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { * @param DateTime $dt * @return void */ - public function fastForward(DateTime $dt) { + public function fastForward(\DateTime $dt) { while($this->valid() && $this->getDTEnd() <= $dt) { $this->next(); @@ -569,6 +576,17 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { } + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() { + + return !$this->count && !$this->until; + + } + /** * Goes on to the next iteration * @@ -632,7 +650,7 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { } - // Checking overriden events + // Checking overridden events foreach($this->overriddenEvents as $index=>$event) { if ($index > $previousStamp && $index <= $currentStamp) { @@ -880,7 +898,7 @@ class Sabre_VObject_RecurrenceIterator implements Iterator { // 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 - // occurence is always correct. + // occurrence is always correct. if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { break 2; } diff --git a/3rdparty/Sabre/VObject/Splitter/ICalendar.php b/3rdparty/Sabre/VObject/Splitter/ICalendar.php new file mode 100644 index 0000000000..7a9a63e1b2 --- /dev/null +++ b/3rdparty/Sabre/VObject/Splitter/ICalendar.php @@ -0,0 +1,111 @@ +children as $component) { + if (!$component instanceof VObject\Component) { + continue; + } + + // Get all timezones + if ($component->name === 'VTIMEZONE') { + $this->vtimezones[(string)$component->TZID] = $component; + continue; + } + + // Get component UID for recurring Events search + if($component->UID) { + $uid = (string)$component->UID; + } else { + // Generating a random UID + $uid = sha1(microtime()) . '-vobjectimport'; + } + + // Take care of recurring events + if (!array_key_exists($uid, $this->objects)) { + $this->objects[$uid] = VObject\Component::create('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 null; + + } + + } + +} diff --git a/3rdparty/Sabre/VObject/Splitter/SplitterInterface.php b/3rdparty/Sabre/VObject/Splitter/SplitterInterface.php new file mode 100644 index 0000000000..9f7a82450e --- /dev/null +++ b/3rdparty/Sabre/VObject/Splitter/SplitterInterface.php @@ -0,0 +1,39 @@ +input = $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() { + + $vcard = ''; + + do { + + if (feof($this->input)) { + return false; + } + + $line = fgets($this->input); + $vcard .= $line; + + } while(strtoupper(substr($line,0,4))!=="END:"); + + $object = VObject\Reader::read($vcard); + + if($object->name !== 'VCARD') { + throw new \InvalidArgumentException("Thats no vCard!", 1); + } + + return $object; + + } + +} diff --git a/3rdparty/Sabre/VObject/StringUtil.php b/3rdparty/Sabre/VObject/StringUtil.php new file mode 100644 index 0000000000..886a7135d6 --- /dev/null +++ b/3rdparty/Sabre/VObject/StringUtil.php @@ -0,0 +1,61 @@ +'Australia/Darwin', + 'AUS Eastern Standard Time'=>'Australia/Sydney', + 'Afghanistan Standard Time'=>'Asia/Kabul', + 'Alaskan Standard Time'=>'America/Anchorage', + 'Arab Standard Time'=>'Asia/Riyadh', + 'Arabian Standard Time'=>'Asia/Dubai', + 'Arabic Standard Time'=>'Asia/Baghdad', + 'Argentina Standard Time'=>'America/Buenos_Aires', + 'Armenian Standard Time'=>'Asia/Yerevan', + 'Atlantic Standard Time'=>'America/Halifax', + 'Azerbaijan Standard Time'=>'Asia/Baku', + 'Azores Standard Time'=>'Atlantic/Azores', + 'Bangladesh Standard Time'=>'Asia/Dhaka', + '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', + 'China Standard Time'=>'Asia/Shanghai', + 'Dateline Standard Time'=>'Etc/GMT+12', + 'E. Africa Standard Time'=>'Africa/Nairobi', + 'E. Australia Standard Time'=>'Australia/Brisbane', + 'E. Europe Standard Time'=>'Europe/Minsk', + 'E. South America Standard Time'=>'America/Sao_Paulo', + 'Eastern Standard Time'=>'America/New_York', + 'Egypt Standard Time'=>'Africa/Cairo', + 'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg', + 'FLE Standard Time'=>'Europe/Kiev', + 'Fiji Standard Time'=>'Pacific/Fiji', + 'GMT Standard Time'=>'Europe/London', + 'GTB Standard Time'=>'Europe/Istanbul', + 'Georgian Standard Time'=>'Asia/Tbilisi', + 'Greenland Standard Time'=>'America/Godthab', + 'Greenwich Standard Time'=>'Atlantic/Reykjavik', + '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', + 'Kamchatka Standard Time'=>'Asia/Kamchatka', + 'Korea Standard Time'=>'Asia/Seoul', + 'Magadan Standard Time'=>'Asia/Magadan', + 'Mauritius Standard Time'=>'Indian/Mauritius', + 'Mexico Standard Time'=>'America/Mexico_City', + 'Mexico Standard Time 2'=>'America/Chihuahua', + 'Mid-Atlantic Standard Time'=>'Etc/GMT+2', + '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', + 'North Asia East Standard Time'=>'Asia/Irkutsk', + 'North Asia Standard Time'=>'Asia/Krasnoyarsk', + 'Pacific SA Standard Time'=>'America/Santiago', + 'Pacific Standard Time'=>'America/Los_Angeles', + 'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel', + 'Pakistan Standard Time'=>'Asia/Karachi', + 'Paraguay Standard Time'=>'America/Asuncion', + 'Romance Standard Time'=>'Europe/Paris', + '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', + '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', + 'Tokyo Standard Time'=>'Asia/Tokyo', + 'Tonga Standard Time'=>'Pacific/Tongatapu', + '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-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', + 'West Asia Standard Time'=>'Asia/Tashkent', + 'West Pacific Standard Time'=>'Pacific/Port_Moresby', + 'Yakutsk Standard Time'=>'Asia/Yakutsk', + + // 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 + 'Universal Coordinated Time' => 'UTC', + 'Casablanca, Monrovia' => 'Africa/Casablanca', + 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', + 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', + 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', + 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', + 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', + 'Prague, Central Europe' => 'Europe/Prague', + 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo', + 'West Central Africa' => 'Africa/Luanda', // This was a best guess + 'Athens, Istanbul, Minsk' => 'Europe/Athens', + 'Bucharest' => 'Europe/Bucharest', + 'Cairo' => 'Africa/Cairo', + 'Harare, Pretoria' => 'Africa/Harare', + 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki', + 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem', + 'Baghdad' => 'Asia/Baghdad', + 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait', + 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', + 'East Africa, Nairobi' => 'Africa/Nairobi', + 'Tehran' => 'Asia/Tehran', + 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess + 'Baku, Tbilisi, Yerevan' => 'Asia/Baku', + 'Kabul' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', + 'Kathmandu, Nepal' => 'Asia/Kathmandu', + 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', + 'Astana, Dhaka' => 'Asia/Dhaka', + 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', + 'Rangoon' => 'Asia/Rangoon', + 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', + 'Krasnoyarsk' => 'Asia/Krasnoyarsk', + 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', + 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk', + 'Kuala Lumpur, Singapore' => 'Asia/Singapore', + 'Perth, Western Australia' => 'Australia/Perth', + 'Taipei' => 'Asia/Taipei', + 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', + 'Seoul, Korea Standard time' => 'Asia/Seoul', + 'Yakutsk' => 'Asia/Yakutsk', + 'Adelaide, Central Australia' => 'Australia/Adelaide', + 'Darwin' => 'Australia/Darwin', + 'Brisbane, East Australia' => 'Australia/Brisbane', + 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney', + 'Guam, Port Moresby' => 'Pacific/Guam', + 'Hobart, Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan', + 'Auckland, Wellington' => 'Pacific/Auckland', + 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji', + 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu', + 'Azores' => 'Atlantic/Azores', + 'Cape Verde Is.' => 'Atlantic/Cape_Verde', + 'Mid-Atlantic' => 'America/Noronha', + 'Brasilia' => 'America/Sao_Paulo', // Best guess + 'Buenos Aires' => 'America/Argentina/Buenos_Aires', + 'Greenland' => 'America/Godthab', + 'Newfoundland' => 'America/St_Johns', + 'Atlantic Time (Canada)' => 'America/Halifax', + 'Caracas, La Paz' => 'America/Caracas', + 'Santiago' => 'America/Santiago', + 'Bogota, Lima, Quito' => 'America/Bogota', + 'Eastern Time (US & Canada)' => 'America/New_York', + 'Indiana (East)' => 'America/Indiana/Indianapolis', + 'Central America' => 'America/Guatemala', + 'Central Time (US & Canada)' => 'America/Chicago', + 'Mexico City, Tegucigalpa' => 'America/Mexico_City', + 'Saskatchewan' => 'America/Edmonton', + 'Arizona' => 'America/Phoenix', + 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess + 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess + 'Alaska' => 'America/Anchorage', + 'Hawaii' => 'Pacific/Honolulu', + 'Midway Island, Samoa' => 'Pacific/Midway', + 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', + + ); + + public static $microsoftExchangeMap = array( + 0 => 'UTC', + 31 => 'Africa/Casablanca', + 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 UTC. + * + * @param string $tzid + * @param Sabre\VObject\Component $vcalendar + * @return DateTimeZone + */ + static public function getTimeZone($tzid, Component $vcalendar = null) { + + // First we will just see if the tzid is a support timezone identifier. + try { + return new \DateTimeZone($tzid); + } catch (\Exception $e) { + } + + // Next, we check if the tzid is somewhere in our tzid map. + if (isset(self::$map[$tzid])) { + return new \DateTimeZone(self::$map[$tzid]); + } + + 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'})) { + try { + return new \DateTimeZone($vtimezone->{'X-LIC-LOCATION'}); + } catch (\Exception $e) { + } + + } + // Microsoft may add a magic number, which we also have an + // answer for. + if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { + if (isset(self::$microsoftExchangeMap[(int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->value])) { + return new \DateTimeZone(self::$microsoftExchangeMap[(int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->value]); + } + } + } + + } + + } + + // If we got all the way here, we default to UTC. + return new \DateTimeZone(date_default_timezone_get()); + + + } + + +} diff --git a/3rdparty/Sabre/VObject/Version.php b/3rdparty/Sabre/VObject/Version.php old mode 100755 new mode 100644 index 9ee03d8711..0065b7abc9 --- a/3rdparty/Sabre/VObject/Version.php +++ b/3rdparty/Sabre/VObject/Version.php @@ -1,20 +1,20 @@ 'Australia/Darwin', - 'AUS Eastern Standard Time'=>'Australia/Sydney', - 'Afghanistan Standard Time'=>'Asia/Kabul', - 'Alaskan Standard Time'=>'America/Anchorage', - 'Arab Standard Time'=>'Asia/Riyadh', - 'Arabian Standard Time'=>'Asia/Dubai', - 'Arabic Standard Time'=>'Asia/Baghdad', - 'Argentina Standard Time'=>'America/Buenos_Aires', - 'Armenian Standard Time'=>'Asia/Yerevan', - 'Atlantic Standard Time'=>'America/Halifax', - 'Azerbaijan Standard Time'=>'Asia/Baku', - 'Azores Standard Time'=>'Atlantic/Azores', - 'Bangladesh Standard Time'=>'Asia/Dhaka', - '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', - 'China Standard Time'=>'Asia/Shanghai', - 'Dateline Standard Time'=>'Etc/GMT+12', - 'E. Africa Standard Time'=>'Africa/Nairobi', - 'E. Australia Standard Time'=>'Australia/Brisbane', - 'E. Europe Standard Time'=>'Europe/Minsk', - 'E. South America Standard Time'=>'America/Sao_Paulo', - 'Eastern Standard Time'=>'America/New_York', - 'Egypt Standard Time'=>'Africa/Cairo', - 'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg', - 'FLE Standard Time'=>'Europe/Kiev', - 'Fiji Standard Time'=>'Pacific/Fiji', - 'GMT Standard Time'=>'Europe/London', - 'GTB Standard Time'=>'Europe/Istanbul', - 'Georgian Standard Time'=>'Asia/Tbilisi', - 'Greenland Standard Time'=>'America/Godthab', - 'Greenwich Standard Time'=>'Atlantic/Reykjavik', - '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', - 'Kamchatka Standard Time'=>'Asia/Kamchatka', - 'Korea Standard Time'=>'Asia/Seoul', - 'Magadan Standard Time'=>'Asia/Magadan', - 'Mauritius Standard Time'=>'Indian/Mauritius', - 'Mexico Standard Time'=>'America/Mexico_City', - 'Mexico Standard Time 2'=>'America/Chihuahua', - 'Mid-Atlantic Standard Time'=>'Etc/GMT+2', - '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', - 'North Asia East Standard Time'=>'Asia/Irkutsk', - 'North Asia Standard Time'=>'Asia/Krasnoyarsk', - 'Pacific SA Standard Time'=>'America/Santiago', - 'Pacific Standard Time'=>'America/Los_Angeles', - 'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel', - 'Pakistan Standard Time'=>'Asia/Karachi', - 'Paraguay Standard Time'=>'America/Asuncion', - 'Romance Standard Time'=>'Europe/Paris', - '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', - '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', - 'Tokyo Standard Time'=>'Asia/Tokyo', - 'Tonga Standard Time'=>'Pacific/Tongatapu', - '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-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', - 'West Asia Standard Time'=>'Asia/Tashkent', - 'West Pacific Standard Time'=>'Pacific/Port_Moresby', - 'Yakutsk Standard Time'=>'Asia/Yakutsk', - ); - - static public function lookup($tzid) { - return isset(self::$map[$tzid]) ? self::$map[$tzid] : null; - } -} diff --git a/3rdparty/Sabre/VObject/includes.php b/3rdparty/Sabre/VObject/includes.php old mode 100755 new mode 100644 index 0177a8f1ba..b74bd3b6cb --- a/3rdparty/Sabre/VObject/includes.php +++ b/3rdparty/Sabre/VObject/includes.php @@ -1,15 +1,11 @@ +
+ + +
+
+ t('New');?> + +
+
+
+ + + + + + + +
+
+
+
+ +
+ +
+
+ + + + +
+
+ + +
t('Nothing in here. Upload something!')?>
+ + + + + + + + + + + + + +
+ + t( 'Name' ); ?> + + + + Download" /> t('Download')?> + + + t( 'Size' ); ?> + t( 'Modified' ); ?> + + + + t('Unshare')?> <?php echo $l->t('Unshare')?>" /> + + t('Delete')?> <?php echo $l->t('Delete')?>" /> + + +
+
+
+

+ t('The files you are trying to upload exceed the maximum size for file uploads on this server.');?> +

+
+
+

+ t('Files are being scanned, please wait.');?> +

+

+ t('Current scanning');?> +

+
+ + + diff --git a/apps/files/templates/index.php_test b/apps/files/templates/index.php_test new file mode 100644 index 0000000000..a509333aa4 --- /dev/null +++ b/apps/files/templates/index.php_test @@ -0,0 +1,90 @@ + +
+ + +
+
+ t('New');?> + +
+
+
+ + + + + + + +
+
+
+
+ +
+ +
+
+ + + + +
+
+ + +
t('Nothing in here. Upload something!')?>
+ + + + + + + + + + + + + +
+ + t( 'Name' ); ?> + + + + Download" /> t('Download')?> + + + t( 'Size' ); ?> + t( 'Modified' ); ?> + + + + t('Unshare')?> <?php echo $l->t('Unshare')?>" /> + + t('Delete')?> <?php echo $l->t('Delete')?>" /> + + +
+
+
+

+ t('The files you are trying to upload exceed the maximum size for file uploads on this server.');?> +

+
+
+

+ t('Files are being scanned, please wait.');?> +

+

+ t('Current scanning');?> +

+
+ + + diff --git a/lib/base.php b/lib/base.php index a1ad4f6dc0..bd8829e1ab 100644 --- a/lib/base.php +++ b/lib/base.php @@ -93,6 +93,11 @@ class OC{ elseif(strpos($className, 'Sabre_')===0) { $path = str_replace('_', '/', $className) . '.php'; } + + elseif(strpos($className, 'Sabre\\VObject')===0) { + $path = '3rdparty/'.str_replace('\\', '/', $className) . '.php'; + } + elseif(strpos($className, 'Test_')===0) { $path = 'tests/lib/'.strtolower(str_replace('_', '/', substr($className, 5)) . '.php'); }else{ diff --git a/lib/vobject.php b/lib/vobject.php index b5a04b4bf6..5070c3f1ee 100644 --- a/lib/vobject.php +++ b/lib/vobject.php @@ -24,11 +24,11 @@ * This class provides a streamlined interface to the Sabre VObject classes */ class OC_VObject{ - /** @var Sabre_VObject_Component */ + /** @var Sabre\VObject\Component */ protected $vobject; /** - * @returns Sabre_VObject_Component + * @returns Sabre\VObject\Component */ public function getVObject() { return $this->vobject; @@ -41,9 +41,9 @@ class OC_VObject{ */ public static function parse($data) { try { - Sabre_VObject_Property::$classMap['LAST-MODIFIED'] = 'Sabre_VObject_Property_DateTime'; - $vobject = Sabre_VObject_Reader::read($data); - if ($vobject instanceof Sabre_VObject_Component) { + Sabre\VObject\Property::$classMap['LAST-MODIFIED'] = 'Sabre\VObject\Property\DateTime'; + $vobject = Sabre\VObject\Reader::read($data); + if ($vobject instanceof Sabre\VObject\Component) { $vobject = new OC_VObject($vobject); } return $vobject; @@ -89,13 +89,13 @@ class OC_VObject{ /** * Constuctor - * @param Sabre_VObject_Component or string + * @param Sabre\VObject\Component or string */ public function __construct($vobject_or_name) { if (is_object($vobject_or_name)) { $this->vobject = $vobject_or_name; } else { - $this->vobject = new Sabre_VObject_Component($vobject_or_name); + $this->vobject = new Sabre\VObject\Component($vobject_or_name); } } @@ -117,9 +117,9 @@ class OC_VObject{ if(is_array($value)) { $value = OC_VObject::escapeSemicolons($value); } - $property = new Sabre_VObject_Property( $name, $value ); + $property = new Sabre\VObject\Property( $name, $value ); foreach($parameters as $name => $value) { - $property->parameters[] = new Sabre_VObject_Parameter($name, $value); + $property->parameters[] = new Sabre\VObject\Parameter($name, $value); } $this->vobject->add($property); @@ -150,12 +150,12 @@ class OC_VObject{ * @param int $dateType * @return void */ - public function setDateTime($name, $datetime, $dateType=Sabre_VObject_Property_DateTime::LOCALTZ) { + public function setDateTime($name, $datetime, $dateType=Sabre\VObject\Property\DateTime::LOCALTZ) { if ($datetime == 'now') { $datetime = new DateTime(); } if ($datetime instanceof DateTime) { - $datetime_element = new Sabre_VObject_Property_DateTime($name); + $datetime_element = new Sabre\VObject\Property\DateTime($name); $datetime_element->setDateTime($datetime, $dateType); $this->vobject->__set($name, $datetime_element); }else{ @@ -183,7 +183,7 @@ class OC_VObject{ return $this->vobject->children; } $return = $this->vobject->__get($name); - if ($return instanceof Sabre_VObject_Component) { + if ($return instanceof Sabre\VObject\Component) { $return = new OC_VObject($return); } return $return; From c1a0e809bf865190f669399d9b4a1b51e35c08d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schie=C3=9Fle?= Date: Sat, 27 Oct 2012 15:26:28 +0200 Subject: [PATCH 02/15] don't call $('#found_versions').chosen(); after opening the history drop-down since no version is selected at the beginning --- apps/files_versions/js/versions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/files_versions/js/versions.js b/apps/files_versions/js/versions.js index 426d521df8..b9c5468981 100644 --- a/apps/files_versions/js/versions.js +++ b/apps/files_versions/js/versions.js @@ -73,7 +73,6 @@ function createVersionsDropdown(filename, files) { $.each( versions, function(index, row ) { addVersion( row ); }); - $('#found_versions').chosen(); } else { $('#found_versions').hide(); $('#makelink').hide(); From a93660d37a49128151c359b435c5d108160e8d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schie=C3=9Fle?= Date: Sat, 27 Oct 2012 15:24:01 +0200 Subject: [PATCH 03/15] fix broken paths in versions app --- apps/files_versions/lib/versions.php | 559 +++++++++++++-------------- 1 file changed, 279 insertions(+), 280 deletions(-) diff --git a/apps/files_versions/lib/versions.php b/apps/files_versions/lib/versions.php index 56878b470d..56664427fb 100644 --- a/apps/files_versions/lib/versions.php +++ b/apps/files_versions/lib/versions.php @@ -1,280 +1,279 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - -/** - * Versions - * - * A class to handle the versioning of files. - */ - -namespace OCA_Versions; - -class Storage { - - - // config.php configuration: - // - files_versions - // - files_versionsfolder - // - files_versionsblacklist - // - files_versionsmaxfilesize - // - files_versionsinterval - // - files_versionmaxversions - // - // todo: - // - finish porting to OC_FilesystemView to enable network transparency - // - add transparent compression. first test if it´s worth it. - - const DEFAULTENABLED=true; - const DEFAULTBLACKLIST='avi mp3 mpg mp4 ctmp'; - const DEFAULTMAXFILESIZE=1048576; // 10MB - const DEFAULTMININTERVAL=60; // 1 min - const DEFAULTMAXVERSIONS=50; - - private static function getUidAndFilename($filename) - { - if (\OCP\App::isEnabled('files_sharing') - && substr($filename, 0, 7) == '/Shared' - && $source = \OCP\Share::getItemSharedWith('file', - substr($filename, 7), - \OC_Share_Backend_File::FORMAT_SHARED_STORAGE)) { - $filename = $source['path']; - $pos = strpos($filename, '/files', 1); - $uid = substr($filename, 1, $pos - 1); - $filename = substr($filename, $pos + 6); - } else { - $uid = \OCP\User::getUser(); - } - return array($uid, $filename); - } - - /** - * store a new version of a file. - */ - public function store($filename) { - if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { - list($uid, $filename) = self::getUidAndFilename($filename); - $userHome = \OC_User::getHome($uid); - $files_view = new \OC_FilesystemView($userHome.'/files'); - $users_view = new \OC_FilesystemView($userHome); - - //check if source file already exist as version to avoid recursions. - // todo does this check work? - if ($users_view->file_exists($filename)) { - return false; - } - - // check if filename is a directory - if($files_view->is_dir($filename)) { - return false; - } - - // check filetype blacklist - $blacklist=explode(' ',\OCP\Config::getSystemValue('files_versionsblacklist', Storage::DEFAULTBLACKLIST)); - foreach($blacklist as $bl) { - $parts=explode('.', $filename); - $ext=end($parts); - if(strtolower($ext)==$bl) { - return false; - } - } - // we should have a source file to work with - if (!$files_view->file_exists($filename)) { - return false; - } - - // check filesize - if($files_view->filesize($filename)>\OCP\Config::getSystemValue('files_versionsmaxfilesize', Storage::DEFAULTMAXFILESIZE)) { - return false; - } - - - // check mininterval if the file is being modified by the owner (all shared files should be versioned despite mininterval) - if ($uid == \OCP\User::getUser()) { - $versions_fileview = new \OC_FilesystemView($userHome.'/files_versions'); - $versionsFolderName=\OCP\Config::getSystemValue('datadirectory'). $versions_fileview->getAbsolutePath(''); - $matches=glob($versionsFolderName.'/'.$filename.'.v*'); - sort($matches); - $parts=explode('.v', end($matches)); - if((end($parts)+Storage::DEFAULTMININTERVAL)>time()) { - return false; - } - } - - - // create all parent folders - $info=pathinfo($filename); - if(!file_exists($versionsFolderName.'/'.$info['dirname'])) { - mkdir($versionsFolderName.'/'.$info['dirname'],0750, true); - } - - // store a new version of a file - $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.time()); - - // expire old revisions if necessary - Storage::expire($filename); - } - } - - - /** - * rollback to an old version of a file. - */ - public static function rollback($filename,$revision) { - - if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { - list($uid, $filename) = self::getUidAndFilename($filename); - $users_view = new \OC_FilesystemView(\OC_User::getHome($uid)); - - // rollback - if( @$users_view->copy('files_versions'.$filename.'.v'.$revision, 'files'.$filename) ) { - - return true; - - }else{ - - return false; - - } - - } - - } - - /** - * check if old versions of a file exist. - */ - public static function isversioned($filename) { - if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { - list($uid, $filename) = self::getUidAndFilename($filename); - $versions_fileview = new \OC_FilesystemView(\OC_User::getHome($uid).'/files_versions'); - - $versionsFolderName=\OCP\Config::getSystemValue('datadirectory'). $versions_fileview->getAbsolutePath(''); - - // check for old versions - $matches=glob($versionsFolderName.$filename.'.v*'); - if(count($matches)>0) { - return true; - }else{ - return false; - } - }else{ - return(false); - } - } - - - - /** - * @brief get a list of all available versions of a file in descending chronological order - * @param $filename file to find versions of, relative to the user files dir - * @param $count number of versions to return - * @returns array - */ - public static function getVersions( $filename, $count = 0 ) { - - if( \OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' ) { - list($uid, $filename) = self::getUidAndFilename($filename); - $versions_fileview = new \OC_FilesystemView(\OC_User::getHome($uid).'/files_versions'); - - $versionsFolderName = \OCP\Config::getSystemValue('datadirectory'). $versions_fileview->getAbsolutePath(''); - $versions = array(); - - // fetch for old versions - $matches = glob( $versionsFolderName.'/'.$filename.'.v*' ); - - sort( $matches ); - - $i = 0; - - $files_view = new \OC_FilesystemView(\OC_User::getHome($uid).'/files'); - $local_file = $files_view->getLocalFile($filename); - foreach( $matches as $ma ) { - - $i++; - $versions[$i]['cur'] = 0; - $parts = explode( '.v', $ma ); - $versions[$i]['version'] = ( end( $parts ) ); - - // if file with modified date exists, flag it in array as currently enabled version - ( \md5_file( $ma ) == \md5_file( $local_file ) ? $versions[$i]['fileMatch'] = 1 : $versions[$i]['fileMatch'] = 0 ); - - } - - $versions = array_reverse( $versions ); - - foreach( $versions as $key => $value ) { - - // flag the first matched file in array (which will have latest modification date) as current version - if ( $value['fileMatch'] ) { - - $value['cur'] = 1; - break; - - } - - } - - $versions = array_reverse( $versions ); - - // only show the newest commits - if( $count != 0 and ( count( $versions )>$count ) ) { - - $versions = array_slice( $versions, count( $versions ) - $count ); - - } - - return( $versions ); - - - } else { - - // if versioning isn't enabled then return an empty array - return( array() ); - - } - - } - - /** - * @brief Erase a file's versions which exceed the set quota - */ - public static function expire($filename) { - if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { - list($uid, $filename) = self::getUidAndFilename($filename); - $versions_fileview = new \OC_FilesystemView('/'.$uid.'/files_versions'); - - $versionsFolderName=\OCP\Config::getSystemValue('datadirectory'). $versions_fileview->getAbsolutePath(''); - - // check for old versions - $matches = glob( $versionsFolderName.'/'.$filename.'.v*' ); - - if( count( $matches ) > \OCP\Config::getSystemValue( 'files_versionmaxversions', Storage::DEFAULTMAXVERSIONS ) ) { - - $numberToDelete = count($matches) - \OCP\Config::getSystemValue( 'files_versionmaxversions', Storage::DEFAULTMAXVERSIONS ); - - // delete old versions of a file - $deleteItems = array_slice( $matches, 0, $numberToDelete ); - - foreach( $deleteItems as $de ) { - - unlink( $versionsFolderName.'/'.$filename.'.v'.$de ); - - } - } - } - } - - /** - * @brief Erase all old versions of all user files - * @return true/false - */ - public function expireAll() { - $view = \OCP\Files::getStorage('files_versions'); - return $view->deleteAll('', true); - } -} + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +/** + * Versions + * + * A class to handle the versioning of files. + */ + +namespace OCA_Versions; + +class Storage { + + + // config.php configuration: + // - files_versions + // - files_versionsfolder + // - files_versionsblacklist + // - files_versionsmaxfilesize + // - files_versionsinterval + // - files_versionmaxversions + // + // todo: + // - finish porting to OC_FilesystemView to enable network transparency + // - add transparent compression. first test if it´s worth it. + + const DEFAULTENABLED=true; + const DEFAULTBLACKLIST='avi mp3 mpg mp4 ctmp'; + const DEFAULTMAXFILESIZE=1048576; // 10MB + const DEFAULTMININTERVAL=60; // 1 min + const DEFAULTMAXVERSIONS=50; + + private static function getUidAndFilename($filename) + { + if (\OCP\App::isEnabled('files_sharing') + && substr($filename, 0, 7) == '/Shared' + && $source = \OCP\Share::getItemSharedWith('file', + substr($filename, 7), + \OC_Share_Backend_File::FORMAT_SHARED_STORAGE)) { + $filename = $source['path']; + $pos = strpos($filename, '/files', 1); + $uid = substr($filename, 1, $pos - 1); + $filename = substr($filename, $pos + 6); + } else { + $uid = \OCP\User::getUser(); + } + return array($uid, $filename); + } + + /** + * store a new version of a file. + */ + public function store($filename) { + if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { + list($uid, $filename) = self::getUidAndFilename($filename); + $files_view = new \OC_FilesystemView('/'.\OCP\User::getUser() .'/files'); + $users_view = new \OC_FilesystemView('/'.\OCP\User::getUser()); + + //check if source file already exist as version to avoid recursions. + // todo does this check work? + if ($users_view->file_exists($filename)) { + return false; + } + + // check if filename is a directory + if($files_view->is_dir($filename)) { + return false; + } + + // check filetype blacklist + $blacklist=explode(' ',\OCP\Config::getSystemValue('files_versionsblacklist', Storage::DEFAULTBLACKLIST)); + foreach($blacklist as $bl) { + $parts=explode('.', $filename); + $ext=end($parts); + if(strtolower($ext)==$bl) { + return false; + } + } + // we should have a source file to work with + if (!$files_view->file_exists($filename)) { + return false; + } + + // check filesize + if($files_view->filesize($filename)>\OCP\Config::getSystemValue('files_versionsmaxfilesize', Storage::DEFAULTMAXFILESIZE)) { + return false; + } + + + // check mininterval if the file is being modified by the owner (all shared files should be versioned despite mininterval) + if ($uid == \OCP\User::getUser()) { + $versions_fileview = new \OC_FilesystemView('/'.\OCP\User::getUser().'/files_versions'); + $versionsName=\OCP\Config::getSystemValue('datadirectory').$versions_fileview->getAbsolutePath($filename); + $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').$versions_fileview->getAbsolutePath(''); + $matches=glob($versionsName.'.v*'); + sort($matches); + $parts=explode('.v',end($matches)); + if((end($parts)+Storage::DEFAULTMININTERVAL)>time()) { + return false; + } + } + + + // create all parent folders + $info=pathinfo($filename); + if(!file_exists($versionsFolderName.'/'.$info['dirname'])) { + mkdir($versionsFolderName.'/'.$info['dirname'],0750,true); + } + + // store a new version of a file + $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.time()); + + // expire old revisions if necessary + Storage::expire($filename); + } + } + + + /** + * rollback to an old version of a file. + */ + public static function rollback($filename,$revision) { + + if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { + list($uid, $filename) = self::getUidAndFilename($filename); + $users_view = new \OC_FilesystemView('/'.\OCP\User::getUser()); + + // rollback + if( @$users_view->copy('files_versions'.$filename.'.v'.$revision, 'files'.$filename) ) { + + return true; + + }else{ + + return false; + + } + + } + + } + + /** + * check if old versions of a file exist. + */ + public static function isversioned($filename) { + if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { + list($uid, $filename) = self::getUidAndFilename($filename); + $versions_fileview = new \OC_FilesystemView('/'.\OCP\User::getUser().'/files_versions'); + + $versionsName=\OCP\Config::getSystemValue('datadirectory').$versions_fileview->getAbsolutePath($filename); + + // check for old versions + $matches=glob($versionsName.'.v*'); + if(count($matches)>0) { + return true; + }else{ + return false; + } + }else{ + return(false); + } + } + + + + /** + * @brief get a list of all available versions of a file in descending chronological order + * @param $filename file to find versions of, relative to the user files dir + * @param $count number of versions to return + * @returns array + */ + public static function getVersions( $filename, $count = 0 ) { + if( \OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' ) { + list($uid, $filename) = self::getUidAndFilename($filename); + $versions_fileview = new \OC_FilesystemView('/'.\OCP\User::getUser().'/files_versions'); + + $versionsName = \OCP\Config::getSystemValue('datadirectory').$versions_fileview->getAbsolutePath($filename); + $versions = array(); + // fetch for old versions + $matches = glob( $versionsName.'.v*' ); + + sort( $matches ); + + $i = 0; + + $files_view = new \OC_FilesystemView('/'.\OCP\User::getUser().'/files'); + $local_file = $files_view->getLocalFile($filename); + foreach( $matches as $ma ) { + + $i++; + $versions[$i]['cur'] = 0; + $parts = explode( '.v', $ma ); + $versions[$i]['version'] = ( end( $parts ) ); + + // if file with modified date exists, flag it in array as currently enabled version + ( \md5_file( $ma ) == \md5_file( $local_file ) ? $versions[$i]['fileMatch'] = 1 : $versions[$i]['fileMatch'] = 0 ); + + } + + $versions = array_reverse( $versions ); + + foreach( $versions as $key => $value ) { + + // flag the first matched file in array (which will have latest modification date) as current version + if ( $value['fileMatch'] ) { + + $value['cur'] = 1; + break; + + } + + } + + $versions = array_reverse( $versions ); + + // only show the newest commits + if( $count != 0 and ( count( $versions )>$count ) ) { + + $versions = array_slice( $versions, count( $versions ) - $count ); + + } + + return( $versions ); + + + } else { + + // if versioning isn't enabled then return an empty array + return( array() ); + + } + + } + + /** + * @brief Erase a file's versions which exceed the set quota + */ + public static function expire($filename) { + if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') { + list($uid, $filename) = self::getUidAndFilename($filename); + $versions_fileview = new \OC_FilesystemView('/'.$uid.'/files_versions'); + + $versionsName=\OCP\Config::getSystemValue('datadirectory').$versions_fileview->getAbsolutePath($filename); + + // check for old versions + $matches = glob( $versionsName.'.v*' ); + + if( count( $matches ) > \OCP\Config::getSystemValue( 'files_versionmaxversions', Storage::DEFAULTMAXVERSIONS ) ) { + + $numberToDelete = count($matches) - \OCP\Config::getSystemValue( 'files_versionmaxversions', Storage::DEFAULTMAXVERSIONS ); + + // delete old versions of a file + $deleteItems = array_slice( $matches, 0, $numberToDelete ); + + foreach( $deleteItems as $de ) { + + unlink( $versionsName.'.v'.$de ); + + } + } + } + } + + /** + * @brief Erase all old versions of all user files + * @return true/false + */ + public function expireAll() { + $view = \OCP\Files::getStorage('files_versions'); + return $view->deleteAll('', true); + } +} +>>>>>>> 12ea922... fix broken paths in versions app From 8a3eda16f2b134477db5023095ca8744f7e9c4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schie=C3=9Fle?= Date: Sat, 27 Oct 2012 15:23:35 +0200 Subject: [PATCH 04/15] fix function documentation --- lib/user.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/user.php b/lib/user.php index 064fcbad96..869984a16e 100644 --- a/lib/user.php +++ b/lib/user.php @@ -369,8 +369,7 @@ class OC_User { * @param $password The password * @returns string * - * Check if the password is correct without logging in the user - * returns the user id or false + * returns the path to the users home directory */ public static function getHome($uid) { foreach(self::$_usedBackends as $backend) { From c1c76539cc2878c058de15892dd05d6dd8b4b9a5 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Sat, 27 Oct 2012 16:24:24 +0200 Subject: [PATCH 05/15] Include copy of Symfony routing component, and don't use composer --- .gitignore | 4 - 3rdparty/bin/composer | Bin 631359 -> 0 bytes .../Component/Routing/Annotation/Route.php | 103 ++++++ .../Component/Routing/CompiledRoute.php | 134 ++++++++ .../Routing/Exception/ExceptionInterface.php | 23 ++ .../Exception/InvalidParameterException.php | 23 ++ .../Exception/MethodNotAllowedException.php | 38 +++ .../MissingMandatoryParametersException.php | 24 ++ .../Exception/ResourceNotFoundException.php | 25 ++ .../Exception/RouteNotFoundException.php | 23 ++ .../Generator/Dumper/GeneratorDumper.php | 39 +++ .../Dumper/GeneratorDumperInterface.php | 45 +++ .../Generator/Dumper/PhpGeneratorDumper.php | 150 +++++++++ .../Routing/Generator/UrlGenerator.php | 176 ++++++++++ .../Generator/UrlGeneratorInterface.php | 37 +++ .../routing/Symfony/Component/Routing/LICENSE | 19 ++ .../Routing/Loader/AnnotationClassLoader.php | 213 ++++++++++++ .../Loader/AnnotationDirectoryLoader.php | 77 +++++ .../Routing/Loader/AnnotationFileLoader.php | 125 +++++++ .../Routing/Loader/ClosureLoader.php | 54 +++ .../Routing/Loader/PhpFileLoader.php | 64 ++++ .../Routing/Loader/XmlFileLoader.php | 224 +++++++++++++ .../Routing/Loader/YamlFileLoader.php | 142 ++++++++ .../Loader/schema/routing/routing-1.0.xsd | 38 +++ .../Routing/Matcher/ApacheUrlMatcher.php | 76 +++++ .../Matcher/Dumper/ApacheMatcherDumper.php | 155 +++++++++ .../Routing/Matcher/Dumper/MatcherDumper.php | 44 +++ .../Matcher/Dumper/MatcherDumperInterface.php | 41 +++ .../Matcher/Dumper/PhpMatcherDumper.php | 293 ++++++++++++++++ .../Matcher/RedirectableUrlMatcher.php | 53 +++ .../RedirectableUrlMatcherInterface.php | 35 ++ .../Component/Routing/Matcher/UrlMatcher.php | 151 +++++++++ .../Routing/Matcher/UrlMatcherInterface.php | 38 +++ .../Symfony/Component/Routing/README.md | 32 ++ .../Component/Routing/RequestContext.php | 250 ++++++++++++++ .../Routing/RequestContextAwareInterface.php | 27 ++ .../Symfony/Component/Routing/Route.php | 312 ++++++++++++++++++ .../Component/Routing/RouteCollection.php | 259 +++++++++++++++ .../Component/Routing/RouteCompiler.php | 128 +++++++ .../Routing/RouteCompilerInterface.php | 29 ++ .../Symfony/Component/Routing/Router.php | 263 +++++++++++++++ .../Component/Routing/RouterInterface.php | 26 ++ .../Symfony/Component/Routing/composer.json | 29 ++ composer.json | 18 - 44 files changed, 4037 insertions(+), 22 deletions(-) delete mode 100755 3rdparty/bin/composer create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Annotation/Route.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/CompiledRoute.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Exception/InvalidParameterException.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Exception/MethodNotAllowedException.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Exception/MissingMandatoryParametersException.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Exception/ResourceNotFoundException.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Exception/RouteNotFoundException.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/GeneratorDumper.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/GeneratorDumperInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Generator/UrlGenerator.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Generator/UrlGeneratorInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/LICENSE create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationClassLoader.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationFileLoader.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Loader/ClosureLoader.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Loader/PhpFileLoader.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Loader/XmlFileLoader.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Loader/YamlFileLoader.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/MatcherDumper.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/MatcherDumperInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/RedirectableUrlMatcherInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcher.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcherInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/README.md create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/RequestContext.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/RequestContextAwareInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Route.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/RouteCollection.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/RouteCompiler.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/RouteCompilerInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/Router.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/RouterInterface.php create mode 100644 3rdparty/symfony/routing/Symfony/Component/Routing/composer.json delete mode 100644 composer.json diff --git a/.gitignore b/.gitignore index 4ae39ed7fa..4749dea19d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,3 @@ nbproject # WebFinger .well-known /.buildpath -3rdparty/autoload.php -3rdparty/composer/ -3rdparty/symfony/ -composer.lock diff --git a/3rdparty/bin/composer b/3rdparty/bin/composer deleted file mode 100755 index 7999bd4ff6875d721d50c01d28e4b371c78d21d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631359 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≧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

J{fW+SfXRq-eaET&qYo^DAbnH>1R3g` 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 diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Annotation/Route.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Annotation/Route.php new file mode 100644 index 0000000000..f60af463f2 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Annotation/Route.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Annotation; + +/** + * Annotation class for @Route(). + * + * @Annotation + * + * @author Fabien Potencier + */ +class Route +{ + private $pattern; + private $name; + private $requirements; + private $options; + private $defaults; + + /** + * Constructor. + * + * @param array $data An array of key/value parameters. + */ + public function __construct(array $data) + { + $this->requirements = array(); + $this->options = array(); + $this->defaults = array(); + + if (isset($data['value'])) { + $data['pattern'] = $data['value']; + unset($data['value']); + } + + foreach ($data as $key => $value) { + $method = 'set'.$key; + if (!method_exists($this, $method)) { + throw new \BadMethodCallException(sprintf("Unknown property '%s' on annotation '%s'.", $key, get_class($this))); + } + $this->$method($value); + } + } + + public function setPattern($pattern) + { + $this->pattern = $pattern; + } + + public function getPattern() + { + return $this->pattern; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + public function setRequirements($requirements) + { + $this->requirements = $requirements; + } + + public function getRequirements() + { + return $this->requirements; + } + + public function setOptions($options) + { + $this->options = $options; + } + + public function getOptions() + { + return $this->options; + } + + public function setDefaults($defaults) + { + $this->defaults = $defaults; + } + + public function getDefaults() + { + return $this->defaults; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/CompiledRoute.php b/3rdparty/symfony/routing/Symfony/Component/Routing/CompiledRoute.php new file mode 100644 index 0000000000..c86c9eca5b --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/CompiledRoute.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * CompiledRoutes are returned by the RouteCompiler class. + * + * @author Fabien Potencier + */ +class CompiledRoute +{ + private $route; + private $variables; + private $tokens; + private $staticPrefix; + private $regex; + + /** + * Constructor. + * + * @param Route $route A original Route instance + * @param string $staticPrefix The static prefix of the compiled route + * @param string $regex The regular expression to use to match this route + * @param array $tokens An array of tokens to use to generate URL for this route + * @param array $variables An array of variables + */ + public function __construct(Route $route, $staticPrefix, $regex, array $tokens, array $variables) + { + $this->route = $route; + $this->staticPrefix = $staticPrefix; + $this->regex = $regex; + $this->tokens = $tokens; + $this->variables = $variables; + } + + /** + * Returns the Route instance. + * + * @return Route A Route instance + */ + public function getRoute() + { + return $this->route; + } + + /** + * Returns the static prefix. + * + * @return string The static prefix + */ + public function getStaticPrefix() + { + return $this->staticPrefix; + } + + /** + * Returns the regex. + * + * @return string The regex + */ + public function getRegex() + { + return $this->regex; + } + + /** + * Returns the tokens. + * + * @return array The tokens + */ + public function getTokens() + { + return $this->tokens; + } + + /** + * Returns the variables. + * + * @return array The variables + */ + public function getVariables() + { + return $this->variables; + } + + /** + * Returns the pattern. + * + * @return string The pattern + */ + public function getPattern() + { + return $this->route->getPattern(); + } + + /** + * Returns the options. + * + * @return array The options + */ + public function getOptions() + { + return $this->route->getOptions(); + } + + /** + * Returns the defaults. + * + * @return array The defaults + */ + public function getDefaults() + { + return $this->route->getDefaults(); + } + + /** + * Returns the requirements. + * + * @return array The requirements + */ + public function getRequirements() + { + return $this->route->getRequirements(); + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/ExceptionInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..5289f525fc --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/ExceptionInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * ExceptionInterface + * + * @author Alexandre Salomé + * + * @api + */ +interface ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/InvalidParameterException.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/InvalidParameterException.php new file mode 100644 index 0000000000..4f12469529 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/InvalidParameterException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * Exception thrown when a parameter is not valid + * + * @author Alexandre Salomé + * + * @api + */ +class InvalidParameterException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/MethodNotAllowedException.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/MethodNotAllowedException.php new file mode 100644 index 0000000000..470ce52216 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/MethodNotAllowedException.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * The resource was found but the request method is not allowed. + * + * This exception should trigger an HTTP 405 response in your application code. + * + * @author Kris Wallsmith + * + * @api + */ +class MethodNotAllowedException extends \RuntimeException implements ExceptionInterface +{ + protected $allowedMethods; + + public function __construct(array $allowedMethods, $message = null, $code = 0, \Exception $previous = null) + { + $this->allowedMethods = array_map('strtoupper', $allowedMethods); + + parent::__construct($message, $code, $previous); + } + + public function getAllowedMethods() + { + return $this->allowedMethods; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/MissingMandatoryParametersException.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/MissingMandatoryParametersException.php new file mode 100644 index 0000000000..5a523fa559 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/MissingMandatoryParametersException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * Exception thrown when a route cannot be generated because of missing + * mandatory parameters. + * + * @author Alexandre Salomé + * + * @api + */ +class MissingMandatoryParametersException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/ResourceNotFoundException.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/ResourceNotFoundException.php new file mode 100644 index 0000000000..362a0d61f3 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/ResourceNotFoundException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * The resource was not found. + * + * This exception should trigger an HTTP 404 response in your application code. + * + * @author Kris Wallsmith + * + * @api + */ +class ResourceNotFoundException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/RouteNotFoundException.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/RouteNotFoundException.php new file mode 100644 index 0000000000..4d5f288e7e --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Exception/RouteNotFoundException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * Exception thrown when a route does not exists + * + * @author Alexandre Salomé + * + * @api + */ +class RouteNotFoundException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/GeneratorDumper.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/GeneratorDumper.php new file mode 100644 index 0000000000..1291bd12d0 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/GeneratorDumper.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator\Dumper; + +use Symfony\Component\Routing\RouteCollection; + +/** + * GeneratorDumper is the base class for all built-in generator dumpers. + * + * @author Fabien Potencier + */ +abstract class GeneratorDumper implements GeneratorDumperInterface +{ + private $routes; + + /** + * Constructor. + * + * @param RouteCollection $routes The RouteCollection to dump + */ + public function __construct(RouteCollection $routes) + { + $this->routes = $routes; + } + + public function getRoutes() + { + return $this->routes; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/GeneratorDumperInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/GeneratorDumperInterface.php new file mode 100644 index 0000000000..6f5353caf2 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/GeneratorDumperInterface.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\Routing\Generator\Dumper; + +use Symfony\Component\Routing\RouteCollection; + +/** + * GeneratorDumperInterface is the interface that all generator dumper classes must implement. + * + * @author Fabien Potencier + * + * @api + */ +interface GeneratorDumperInterface +{ + /** + * Dumps a set of routes to a PHP class. + * + * Available options: + * + * * class: The class name + * * base_class: The base class name + * + * @param array $options An array of options + * + * @return string A PHP class representing the generator class + */ + public function dump(array $options = array()); + + /** + * Gets the routes to dump. + * + * @return RouteCollection A RouteCollection instance + */ + public function getRoutes(); +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php new file mode 100644 index 0000000000..080dd3a253 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator\Dumper; + +use Symfony\Component\Routing\Route; + +/** + * PhpGeneratorDumper creates a PHP class able to generate URLs for a given set of routes. + * + * @author Fabien Potencier + * + * @api + */ +class PhpGeneratorDumper extends GeneratorDumper +{ + /** + * Dumps a set of routes to a PHP class. + * + * Available options: + * + * * class: The class name + * * base_class: The base class name + * + * @param array $options An array of options + * + * @return string A PHP class representing the generator class + * + * @api + */ + public function dump(array $options = array()) + { + $options = array_merge(array( + 'class' => 'ProjectUrlGenerator', + 'base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator', + ), $options); + + return + $this->startClass($options['class'], $options['base_class']). + $this->addConstructor(). + $this->addGenerator(). + $this->endClass() + ; + } + + private function addGenerator() + { + $methods = array(); + foreach ($this->getRoutes()->all() as $name => $route) { + $compiledRoute = $route->compile(); + + $variables = str_replace("\n", '', var_export($compiledRoute->getVariables(), true)); + $defaults = str_replace("\n", '', var_export($compiledRoute->getDefaults(), true)); + $requirements = str_replace("\n", '', var_export($compiledRoute->getRequirements(), true)); + $tokens = str_replace("\n", '', var_export($compiledRoute->getTokens(), true)); + + $escapedName = str_replace('.', '__', $name); + + $methods[] = <<{'get'.\$escapedName.'RouteInfo'}(); + + return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$absolute); + } + +$methods +EOF; + } + + private function startClass($class, $baseClass) + { + $routes = array(); + foreach ($this->getRoutes()->all() as $name => $route) { + $routes[] = " '$name' => true,"; + } + $routes = implode("\n", $routes); + + return <<context = \$context; + } + +EOF; + } + + private function endClass() + { + return << + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; + +/** + * UrlGenerator generates URL based on a set of routes. + * + * @author Fabien Potencier + * + * @api + */ +class UrlGenerator implements UrlGeneratorInterface +{ + protected $context; + protected $decodedChars = array( + // %2F is not valid in a URL, so we don't encode it (which is fine as the requirements explicitly allowed it) + '%2F' => '/', + ); + + protected $routes; + protected $cache; + + /** + * Constructor. + * + * @param RouteCollection $routes A RouteCollection instance + * @param RequestContext $context The context + * + * @api + */ + public function __construct(RouteCollection $routes, RequestContext $context) + { + $this->routes = $routes; + $this->context = $context; + $this->cache = array(); + } + + /** + * Sets the request context. + * + * @param RequestContext $context The context + * + * @api + */ + public function setContext(RequestContext $context) + { + $this->context = $context; + } + + /** + * Gets the request context. + * + * @return RequestContext The context + */ + public function getContext() + { + return $this->context; + } + + /** + * Generates a URL from the given parameters. + * + * @param string $name The name of the route + * @param mixed $parameters An array of parameters + * @param Boolean $absolute Whether to generate an absolute URL + * + * @return string The generated URL + * + * @throws Symfony\Component\Routing\Exception\RouteNotFoundException When route doesn't exist + * + * @api + */ + public function generate($name, $parameters = array(), $absolute = false) + { + if (null === $route = $this->routes->get($name)) { + throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); + } + + if (!isset($this->cache[$name])) { + $this->cache[$name] = $route->compile(); + } + + return $this->doGenerate($this->cache[$name]->getVariables(), $route->getDefaults(), $route->getRequirements(), $this->cache[$name]->getTokens(), $parameters, $name, $absolute); + } + + /** + * @throws Symfony\Component\Routing\Exception\MissingMandatoryParametersException When route has some missing mandatory parameters + * @throws Symfony\Component\Routing\Exception\InvalidParameterException When a parameter value is not correct + */ + protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute) + { + $variables = array_flip($variables); + + $originParameters = $parameters; + $parameters = array_replace($this->context->getParameters(), $parameters); + $tparams = array_replace($defaults, $parameters); + + // all params must be given + if ($diff = array_diff_key($variables, $tparams)) { + throw new MissingMandatoryParametersException(sprintf('The "%s" route has some missing mandatory parameters ("%s").', $name, implode('", "', array_keys($diff)))); + } + + $url = ''; + $optional = true; + foreach ($tokens as $token) { + if ('variable' === $token[0]) { + if (false === $optional || !array_key_exists($token[3], $defaults) || (isset($parameters[$token[3]]) && (string) $parameters[$token[3]] != (string) $defaults[$token[3]])) { + if (!$isEmpty = in_array($tparams[$token[3]], array(null, '', false), true)) { + // check requirement + if ($tparams[$token[3]] && !preg_match('#^'.$token[2].'$#', $tparams[$token[3]])) { + throw new InvalidParameterException(sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given).', $token[3], $name, $token[2], $tparams[$token[3]])); + } + } + + if (!$isEmpty || !$optional) { + $url = $token[1].strtr(rawurlencode($tparams[$token[3]]), $this->decodedChars).$url; + } + + $optional = false; + } + } elseif ('text' === $token[0]) { + $url = $token[1].$url; + $optional = false; + } + } + + if (!$url) { + $url = '/'; + } + + // add a query string if needed + $extra = array_diff_key($originParameters, $variables, $defaults); + if ($extra && $query = http_build_query($extra, '', '&')) { + $url .= '?'.$query; + } + + $url = $this->context->getBaseUrl().$url; + + if ($this->context->getHost()) { + $scheme = $this->context->getScheme(); + if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme != $req) { + $absolute = true; + $scheme = $req; + } + + if ($absolute) { + $port = ''; + if ('http' === $scheme && 80 != $this->context->getHttpPort()) { + $port = ':'.$this->context->getHttpPort(); + } elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) { + $port = ':'.$this->context->getHttpsPort(); + } + + $url = $scheme.'://'.$this->context->getHost().$port.$url; + } + } + + return $url; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/UrlGeneratorInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/UrlGeneratorInterface.php new file mode 100644 index 0000000000..6f2800c27c --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Generator/UrlGeneratorInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +use Symfony\Component\Routing\RequestContextAwareInterface; + +/** + * UrlGeneratorInterface is the interface that all URL generator classes must implements. + * + * @author Fabien Potencier + * + * @api + */ +interface UrlGeneratorInterface extends RequestContextAwareInterface +{ + /** + * Generates a URL from the given parameters. + * + * @param string $name The name of the route + * @param mixed $parameters An array of parameters + * @param Boolean $absolute Whether to generate an absolute URL + * + * @return string The generated URL + * + * @api + */ + public function generate($name, $parameters = array(), $absolute = false); +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/LICENSE b/3rdparty/symfony/routing/Symfony/Component/Routing/LICENSE new file mode 100644 index 0000000000..cdffe7aebc --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2012 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/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationClassLoader.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationClassLoader.php new file mode 100644 index 0000000000..5f292d4589 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationClassLoader.php @@ -0,0 +1,213 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Config\Loader\LoaderResolver; + +/** + * AnnotationClassLoader loads routing information from a PHP class and its methods. + * + * You need to define an implementation for the getRouteDefaults() method. Most of the + * time, this method should define some PHP callable to be called for the route + * (a controller in MVC speak). + * + * The @Route annotation can be set on the class (for global parameters), + * and on each method. + * + * The @Route annotation main value is the route pattern. The annotation also + * recognizes three parameters: requirements, options, and name. The name parameter + * is mandatory. Here is an example of how you should be able to use it: + * + * /** + * * @Route("/Blog") + * * / + * class Blog + * { + * /** + * * @Route("/", name="blog_index") + * * / + * public function index() + * { + * } + * + * /** + * * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"}) + * * / + * public function show() + * { + * } + * } + * + * @author Fabien Potencier + */ +abstract class AnnotationClassLoader implements LoaderInterface +{ + protected $reader; + protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route'; + protected $defaultRouteIndex; + + /** + * Constructor. + * + * @param Reader $reader + */ + public function __construct(Reader $reader) + { + $this->reader = $reader; + } + + /** + * Sets the annotation class to read route properties from. + * + * @param string $class A fully-qualified class name + */ + public function setRouteAnnotationClass($class) + { + $this->routeAnnotationClass = $class; + } + + /** + * Loads from annotations from a class. + * + * @param string $class A class name + * @param string $type The resource type + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When route can't be parsed + */ + public function load($class, $type = null) + { + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + } + + $globals = array( + 'pattern' => '', + 'requirements' => array(), + 'options' => array(), + 'defaults' => array(), + ); + + $class = new \ReflectionClass($class); + if ($class->isAbstract()) { + throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class)); + } + + if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { + if (null !== $annot->getPattern()) { + $globals['pattern'] = $annot->getPattern(); + } + + if (null !== $annot->getRequirements()) { + $globals['requirements'] = $annot->getRequirements(); + } + + if (null !== $annot->getOptions()) { + $globals['options'] = $annot->getOptions(); + } + + if (null !== $annot->getDefaults()) { + $globals['defaults'] = $annot->getDefaults(); + } + } + + $collection = new RouteCollection(); + $collection->addResource(new FileResource($class->getFileName())); + + foreach ($class->getMethods() as $method) { + $this->defaultRouteIndex = 0; + foreach ($this->reader->getMethodAnnotations($method) as $annot) { + if ($annot instanceof $this->routeAnnotationClass) { + $this->addRoute($collection, $annot, $globals, $class, $method); + } + } + } + + return $collection; + } + + protected function addRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method) + { + $name = $annot->getName(); + if (null === $name) { + $name = $this->getDefaultRouteName($class, $method); + } + + $defaults = array_merge($globals['defaults'], $annot->getDefaults()); + $requirements = array_merge($globals['requirements'], $annot->getRequirements()); + $options = array_merge($globals['options'], $annot->getOptions()); + + $route = new Route($globals['pattern'].$annot->getPattern(), $defaults, $requirements, $options); + + $this->configureRoute($route, $class, $method, $annot); + + $collection->add($name, $route); + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * @param string $type The resource type + * + * @return Boolean True if this class supports the given resource, false otherwise + */ + public function supports($resource, $type = null) + { + return is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type); + } + + /** + * Sets the loader resolver. + * + * @param LoaderResolver $resolver A LoaderResolver instance + */ + public function setResolver(LoaderResolver $resolver) + { + } + + /** + * Gets the loader resolver. + * + * @return LoaderResolver A LoaderResolver instance + */ + public function getResolver() + { + } + + /** + * Gets the default route name for a class method. + * + * @param \ReflectionClass $class + * @param \ReflectionMethod $method + * + * @return string + */ + protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) + { + $name = strtolower(str_replace('\\', '_', $class->name).'_'.$method->name); + if ($this->defaultRouteIndex > 0) { + $name .= '_'.$this->defaultRouteIndex; + } + $this->defaultRouteIndex++; + + return $name; + } + + abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot); +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php new file mode 100644 index 0000000000..8097cd67f9 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Config\Resource\DirectoryResource; + +/** + * AnnotationDirectoryLoader loads routing information from annotations set + * on PHP classes and methods. + * + * @author Fabien Potencier + */ +class AnnotationDirectoryLoader extends AnnotationFileLoader +{ + /** + * Loads from annotations from a directory. + * + * @param string $path A directory path + * @param string $type The resource type + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed + */ + public function load($path, $type = null) + { + $dir = $this->locator->locate($path); + + $collection = new RouteCollection(); + $collection->addResource(new DirectoryResource($dir, '/\.php$/')); + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if (!$file->isFile() || '.php' !== substr($file->getFilename(), -4)) { + continue; + } + + if ($class = $this->findClass($file)) { + $refl = new \ReflectionClass($class); + if ($refl->isAbstract()) { + continue; + } + + $collection->addCollection($this->loader->load($class, $type)); + } + } + + return $collection; + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * @param string $type The resource type + * + * @return Boolean True if this class supports the given resource, false otherwise + */ + public function supports($resource, $type = null) + { + try { + $path = $this->locator->locate($resource); + } catch (\Exception $e) { + return false; + } + + return is_string($resource) && is_dir($path) && (!$type || 'annotation' === $type); + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationFileLoader.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationFileLoader.php new file mode 100644 index 0000000000..49e1cb2f77 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/AnnotationFileLoader.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\FileLocator; + +/** + * AnnotationFileLoader loads routing information from annotations set + * on a PHP class and its methods. + * + * @author Fabien Potencier + */ +class AnnotationFileLoader extends FileLoader +{ + protected $loader; + + /** + * Constructor. + * + * @param FileLocator $locator A FileLocator instance + * @param AnnotationClassLoader $loader An AnnotationClassLoader instance + * @param string|array $paths A path or an array of paths where to look for resources + */ + public function __construct(FileLocator $locator, AnnotationClassLoader $loader, $paths = array()) + { + if (!function_exists('token_get_all')) { + throw new \RuntimeException('The Tokenizer extension is required for the routing annotation loaders.'); + } + + parent::__construct($locator, $paths); + + $this->loader = $loader; + } + + /** + * Loads from annotations from a file. + * + * @param string $file A PHP file path + * @param string $type The resource type + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed + */ + public function load($file, $type = null) + { + $path = $this->locator->locate($file); + + $collection = new RouteCollection(); + if ($class = $this->findClass($path)) { + $collection->addResource(new FileResource($path)); + $collection->addCollection($this->loader->load($class, $type)); + } + + return $collection; + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * @param string $type The resource type + * + * @return Boolean True if this class supports the given resource, false otherwise + */ + public function supports($resource, $type = null) + { + return is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'annotation' === $type); + } + + /** + * Returns the full class name for the first class in the file. + * + * @param string $file A PHP file path + * + * @return string|false Full class name if found, false otherwise + */ + protected function findClass($file) + { + $class = false; + $namespace = false; + $tokens = token_get_all(file_get_contents($file)); + for ($i = 0, $count = count($tokens); $i < $count; $i++) { + $token = $tokens[$i]; + + if (!is_array($token)) { + continue; + } + + if (true === $class && T_STRING === $token[0]) { + return $namespace.'\\'.$token[1]; + } + + if (true === $namespace && T_STRING === $token[0]) { + $namespace = ''; + do { + $namespace .= $token[1]; + $token = $tokens[++$i]; + } while ($i < $count && is_array($token) && in_array($token[0], array(T_NS_SEPARATOR, T_STRING))); + } + + if (T_CLASS === $token[0]) { + $class = true; + } + + if (T_NAMESPACE === $token[0]) { + $namespace = true; + } + } + + return false; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/ClosureLoader.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/ClosureLoader.php new file mode 100644 index 0000000000..ca49c8fa35 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/ClosureLoader.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\Routing\Loader; + +use Symfony\Component\Config\Loader\Loader; + +/** + * ClosureLoader loads routes from a PHP closure. + * + * The Closure must return a RouteCollection instance. + * + * @author Fabien Potencier + * + * @api + */ +class ClosureLoader extends Loader +{ + /** + * Loads a Closure. + * + * @param \Closure $closure A Closure + * @param string $type The resource type + * + * @api + */ + public function load($closure, $type = null) + { + return call_user_func($closure); + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * @param string $type The resource type + * + * @return Boolean True if this class supports the given resource, false otherwise + * + * @api + */ + public function supports($resource, $type = null) + { + return $resource instanceof \Closure && (!$type || 'closure' === $type); + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/PhpFileLoader.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/PhpFileLoader.php new file mode 100644 index 0000000000..ffd31f9444 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/PhpFileLoader.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Loader\FileLoader; + +/** + * PhpFileLoader loads routes from a PHP file. + * + * The file must return a RouteCollection instance. + * + * @author Fabien Potencier + * + * @api + */ +class PhpFileLoader extends FileLoader +{ + /** + * Loads a PHP file. + * + * @param mixed $file A PHP file path + * @param string $type The resource type + * + * @api + */ + public function load($file, $type = null) + { + // the loader variable is exposed to the included file below + $loader = $this; + + $path = $this->locator->locate($file); + $this->setCurrentDir(dirname($path)); + + $collection = include $path; + $collection->addResource(new FileResource($path)); + + return $collection; + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * @param string $type The resource type + * + * @return Boolean True if this class supports the given resource, false otherwise + * + * @api + */ + public function supports($resource, $type = null) + { + return is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'php' === $type); + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/XmlFileLoader.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/XmlFileLoader.php new file mode 100644 index 0000000000..5dad9db3fa --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\Route; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Loader\FileLoader; + +/** + * XmlFileLoader loads XML routing files. + * + * @author Fabien Potencier + * + * @api + */ +class XmlFileLoader extends FileLoader +{ + /** + * Loads an XML file. + * + * @param string $file An XML file path + * @param string $type The resource type + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When a tag can't be parsed + * + * @api + */ + public function load($file, $type = null) + { + $path = $this->locator->locate($file); + + $xml = $this->loadFile($path); + + $collection = new RouteCollection(); + $collection->addResource(new FileResource($path)); + + // process routes and imports + foreach ($xml->documentElement->childNodes as $node) { + if (!$node instanceof \DOMElement) { + continue; + } + + $this->parseNode($collection, $node, $path, $file); + } + + return $collection; + } + + /** + * Parses a node from a loaded XML file. + * + * @param RouteCollection $collection the collection to associate with the node + * @param DOMElement $node the node to parse + * @param string $path the path of the XML file being processed + * @param string $file + */ + protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file) + { + switch ($node->tagName) { + case 'route': + $this->parseRoute($collection, $node, $path); + break; + case 'import': + $resource = (string) $node->getAttribute('resource'); + $type = (string) $node->getAttribute('type'); + $prefix = (string) $node->getAttribute('prefix'); + $this->setCurrentDir(dirname($path)); + $collection->addCollection($this->import($resource, ('' !== $type ? $type : null), false, $file), $prefix); + break; + default: + throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $node->tagName)); + } + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * @param string $type The resource type + * + * @return Boolean True if this class supports the given resource, false otherwise + * + * @api + */ + public function supports($resource, $type = null) + { + return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'xml' === $type); + } + + /** + * Parses a route and adds it to the RouteCollection. + * + * @param RouteCollection $collection A RouteCollection instance + * @param \DOMElement $definition Route definition + * @param string $file An XML file path + * + * @throws \InvalidArgumentException When the definition cannot be parsed + */ + protected function parseRoute(RouteCollection $collection, \DOMElement $definition, $file) + { + $defaults = array(); + $requirements = array(); + $options = array(); + + foreach ($definition->childNodes as $node) { + if (!$node instanceof \DOMElement) { + continue; + } + + switch ($node->tagName) { + case 'default': + $defaults[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue); + break; + case 'option': + $options[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue); + break; + case 'requirement': + $requirements[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue); + break; + default: + throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $node->tagName)); + } + } + + $route = new Route((string) $definition->getAttribute('pattern'), $defaults, $requirements, $options); + + $collection->add((string) $definition->getAttribute('id'), $route); + } + + /** + * Loads an XML file. + * + * @param string $file An XML file path + * + * @return \DOMDocument + * + * @throws \InvalidArgumentException When loading of XML file returns error + */ + protected function loadFile($file) + { + $internalErrors = libxml_use_internal_errors(true); + $disableEntities = libxml_disable_entity_loader(true); + libxml_clear_errors(); + + $dom = new \DOMDocument(); + $dom->validateOnParse = true; + if (!$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) { + libxml_disable_entity_loader($disableEntities); + + throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors($internalErrors))); + } + $dom->normalizeDocument(); + + libxml_use_internal_errors($internalErrors); + libxml_disable_entity_loader($disableEntities); + + foreach ($dom->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + throw new \InvalidArgumentException('Document types are not allowed.'); + } + } + + $this->validate($dom); + + return $dom; + } + + /** + * Validates a loaded XML file. + * + * @param \DOMDocument $dom A loaded XML file + * + * @throws \InvalidArgumentException When XML doesn't validate its XSD schema + */ + protected function validate(\DOMDocument $dom) + { + $location = __DIR__.'/schema/routing/routing-1.0.xsd'; + + $current = libxml_use_internal_errors(true); + libxml_clear_errors(); + + if (!$dom->schemaValidate($location)) { + throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors($current))); + } + libxml_use_internal_errors($current); + } + + /** + * Retrieves libxml errors and clears them. + * + * @return array An array of libxml error strings + */ + private function getXmlErrors($internalErrors) + { + $errors = array(); + foreach (libxml_get_errors() as $error) { + $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', + LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', + $error->code, + trim($error->message), + $error->file ? $error->file : 'n/a', + $error->line, + $error->column + ); + } + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return $errors; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/YamlFileLoader.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/YamlFileLoader.php new file mode 100644 index 0000000000..ee72383435 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/YamlFileLoader.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\Route; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Yaml\Yaml; +use Symfony\Component\Config\Loader\FileLoader; + +/** + * YamlFileLoader loads Yaml routing files. + * + * @author Fabien Potencier + * + * @api + */ +class YamlFileLoader extends FileLoader +{ + private static $availableKeys = array( + 'type', 'resource', 'prefix', 'pattern', 'options', 'defaults', 'requirements' + ); + + /** + * Loads a Yaml file. + * + * @param string $file A Yaml file path + * @param string $type The resource type + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When route can't be parsed + * + * @api + */ + public function load($file, $type = null) + { + $path = $this->locator->locate($file); + + $config = Yaml::parse($path); + + $collection = new RouteCollection(); + $collection->addResource(new FileResource($path)); + + // empty file + if (null === $config) { + $config = array(); + } + + // not an array + if (!is_array($config)) { + throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $file)); + } + + foreach ($config as $name => $config) { + $config = $this->normalizeRouteConfig($config); + + if (isset($config['resource'])) { + $type = isset($config['type']) ? $config['type'] : null; + $prefix = isset($config['prefix']) ? $config['prefix'] : null; + $this->setCurrentDir(dirname($path)); + $collection->addCollection($this->import($config['resource'], $type, false, $file), $prefix); + } else { + $this->parseRoute($collection, $name, $config, $path); + } + } + + return $collection; + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * @param string $type The resource type + * + * @return Boolean True if this class supports the given resource, false otherwise + * + * @api + */ + public function supports($resource, $type = null) + { + return is_string($resource) && 'yml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'yaml' === $type); + } + + /** + * Parses a route and adds it to the RouteCollection. + * + * @param RouteCollection $collection A RouteCollection instance + * @param string $name Route name + * @param array $config Route definition + * @param string $file A Yaml file path + * + * @throws \InvalidArgumentException When config pattern is not defined for the given route + */ + protected function parseRoute(RouteCollection $collection, $name, $config, $file) + { + $defaults = isset($config['defaults']) ? $config['defaults'] : array(); + $requirements = isset($config['requirements']) ? $config['requirements'] : array(); + $options = isset($config['options']) ? $config['options'] : array(); + + if (!isset($config['pattern'])) { + throw new \InvalidArgumentException(sprintf('You must define a "pattern" for the "%s" route.', $name)); + } + + $route = new Route($config['pattern'], $defaults, $requirements, $options); + + $collection->add($name, $route); + } + + /** + * Normalize route configuration. + * + * @param array $config A resource config + * + * @return array + * + * @throws InvalidArgumentException if one of the provided config keys is not supported + */ + private function normalizeRouteConfig(array $config) + { + foreach ($config as $key => $value) { + if (!in_array($key, self::$availableKeys)) { + throw new \InvalidArgumentException(sprintf( + 'Yaml routing loader does not support given key: "%s". Expected one of the (%s).', + $key, implode(', ', self::$availableKeys) + )); + } + } + + return $config; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd new file mode 100644 index 0000000000..a9554e64df --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php new file mode 100644 index 0000000000..3003dfdebb --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.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\Routing\Matcher; + +use Symfony\Component\Routing\Exception\MethodNotAllowedException; + +/** + * ApacheUrlMatcher matches URL based on Apache mod_rewrite matching (see ApacheMatcherDumper). + * + * @author Fabien Potencier + */ +class ApacheUrlMatcher extends UrlMatcher +{ + /** + * Tries to match a URL based on Apache mod_rewrite matching. + * + * Returns false if no route matches the URL. + * + * @param string $pathinfo The pathinfo to be parsed + * + * @return array An array of parameters + * + * @throws MethodNotAllowedException If the current method is not allowed + */ + public function match($pathinfo) + { + $parameters = array(); + $defaults = array(); + $allow = array(); + $match = false; + + foreach ($_SERVER as $key => $value) { + $name = $key; + + if (0 === strpos($name, 'REDIRECT_')) { + $name = substr($name, 9); + } + + if (0 === strpos($name, '_ROUTING_DEFAULTS_')) { + $name = substr($name, 18); + $defaults[$name] = $value; + } elseif (0 === strpos($name, '_ROUTING_')) { + $name = substr($name, 9); + if ('_route' == $name) { + $match = true; + $parameters[$name] = $value; + } elseif (0 === strpos($name, '_allow_')) { + $allow[] = substr($name, 7); + } else { + $parameters[$name] = $value; + } + } else { + continue; + } + + unset($_SERVER[$key]); + } + + if ($match) { + return $this->mergeDefaults($parameters, $defaults); + } elseif (0 < count($allow)) { + throw new MethodNotAllowedException($allow); + } else { + return parent::match($pathinfo); + } + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php new file mode 100644 index 0000000000..4f03b8d393 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +/** + * Dumps a set of Apache mod_rewrite rules. + * + * @author Fabien Potencier + * @author Kris Wallsmith + */ +class ApacheMatcherDumper extends MatcherDumper +{ + /** + * Dumps a set of Apache mod_rewrite rules. + * + * Available options: + * + * * script_name: The script name (app.php by default) + * * base_uri: The base URI ("" by default) + * + * @param array $options An array of options + * + * @return string A string to be used as Apache rewrite rules + * + * @throws \LogicException When the route regex is invalid + */ + public function dump(array $options = array()) + { + $options = array_merge(array( + 'script_name' => 'app.php', + 'base_uri' => '', + ), $options); + + $options['script_name'] = self::escape($options['script_name'], ' ', '\\'); + + $rules = array("# skip \"real\" requests\nRewriteCond %{REQUEST_FILENAME} -f\nRewriteRule .* - [QSA,L]"); + $methodVars = array(); + + foreach ($this->getRoutes()->all() as $name => $route) { + $compiledRoute = $route->compile(); + + // prepare the apache regex + $regex = $compiledRoute->getRegex(); + $delimiter = $regex[0]; + $regexPatternEnd = strrpos($regex, $delimiter); + if (strlen($regex) < 2 || 0 === $regexPatternEnd) { + throw new \LogicException('The "%s" route regex "%s" is invalid', $name, $regex); + } + $regex = preg_replace('/\?P<.+?>/', '', substr($regex, 1, $regexPatternEnd - 1)); + $regex = '^'.self::escape(preg_quote($options['base_uri']).substr($regex, 1), ' ', '\\'); + + $hasTrailingSlash = '/$' == substr($regex, -2) && '^/$' != $regex; + + $variables = array('E=_ROUTING__route:'.$name); + foreach ($compiledRoute->getVariables() as $i => $variable) { + $variables[] = 'E=_ROUTING_'.$variable.':%'.($i + 1); + } + foreach ($route->getDefaults() as $key => $value) { + $variables[] = 'E=_ROUTING_DEFAULTS_'.$key.':'.strtr($value, array( + ':' => '\\:', + '=' => '\\=', + '\\' => '\\\\', + ' ' => '\\ ', + )); + } + $variables = implode(',', $variables); + + $rule = array("# $name"); + + // method mismatch + if ($req = $route->getRequirement('_method')) { + $methods = explode('|', strtoupper($req)); + // GET and HEAD are equivalent + if (in_array('GET', $methods) && !in_array('HEAD', $methods)) { + $methods[] = 'HEAD'; + } + $allow = array(); + foreach ($methods as $method) { + $methodVars[] = $method; + $allow[] = 'E=_ROUTING__allow_'.$method.':1'; + } + + $rule[] = "RewriteCond %{REQUEST_URI} $regex"; + $rule[] = sprintf("RewriteCond %%{REQUEST_METHOD} !^(%s)$ [NC]", implode('|', $methods)); + $rule[] = sprintf('RewriteRule .* - [S=%d,%s]', $hasTrailingSlash ? 2 : 1, implode(',', $allow)); + } + + // redirect with trailing slash appended + if ($hasTrailingSlash) { + $rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$'; + $rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]'; + } + + // the main rule + $rule[] = "RewriteCond %{REQUEST_URI} $regex"; + $rule[] = "RewriteRule .* {$options['script_name']} [QSA,L,$variables]"; + + $rules[] = implode("\n", $rule); + } + + if (0 < count($methodVars)) { + $rule = array('# 405 Method Not Allowed'); + $methodVars = array_values(array_unique($methodVars)); + foreach ($methodVars as $i => $methodVar) { + $rule[] = sprintf('RewriteCond %%{_ROUTING__allow_%s} !-z%s', $methodVar, isset($methodVars[$i + 1]) ? ' [OR]' : ''); + } + $rule[] = sprintf('RewriteRule .* %s [QSA,L]', $options['script_name']); + + $rules[] = implode("\n", $rule); + } + + return implode("\n\n", $rules)."\n"; + } + + /** + * Escapes a string. + * + * @param string $string The string to be escaped + * @param string $char The character to be escaped + * @param string $with The character to be used for escaping + * + * @return string The escaped string + */ + private static function escape($string, $char, $with) + { + $escaped = false; + $output = ''; + foreach (str_split($string) as $symbol) { + if ($escaped) { + $output .= $symbol; + $escaped = false; + continue; + } + if ($symbol === $char) { + $output .= $with.$char; + continue; + } + if ($symbol === $with) { + $escaped = true; + } + $output .= $symbol; + } + + return $output; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/MatcherDumper.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/MatcherDumper.php new file mode 100644 index 0000000000..423368b57e --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/MatcherDumper.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +use Symfony\Component\Routing\RouteCollection; + +/** + * MatcherDumper is the abstract class for all built-in matcher dumpers. + * + * @author Fabien Potencier + */ +abstract class MatcherDumper implements MatcherDumperInterface +{ + private $routes; + + /** + * Constructor. + * + * @param RouteCollection $routes The RouteCollection to dump + */ + public function __construct(RouteCollection $routes) + { + $this->routes = $routes; + } + + /** + * Gets the routes to dump. + * + * @return RouteCollection A RouteCollection instance + */ + public function getRoutes() + { + return $this->routes; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/MatcherDumperInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/MatcherDumperInterface.php new file mode 100644 index 0000000000..fe09e067d7 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/MatcherDumperInterface.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +/** + * MatcherDumperInterface is the interface that all matcher dumper classes must implement. + * + * @author Fabien Potencier + */ +interface MatcherDumperInterface +{ + /** + * Dumps a set of routes to a PHP class. + * + * Available options: + * + * * class: The class name + * * base_class: The base class name + * + * @param array $options An array of options + * + * @return string A PHP class representing the matcher class + */ + public function dump(array $options = array()); + + /** + * Gets the routes to match. + * + * @return RouteCollection A RouteCollection instance + */ + public function getRoutes(); +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php new file mode 100644 index 0000000000..fdaad513a1 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -0,0 +1,293 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes. + * + * @author Fabien Potencier + */ +class PhpMatcherDumper extends MatcherDumper +{ + /** + * Dumps a set of routes to a PHP class. + * + * Available options: + * + * * class: The class name + * * base_class: The base class name + * + * @param array $options An array of options + * + * @return string A PHP class representing the matcher class + */ + public function dump(array $options = array()) + { + $options = array_merge(array( + 'class' => 'ProjectUrlMatcher', + 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher', + ), $options); + + // trailing slash support is only enabled if we know how to redirect the user + $interfaces = class_implements($options['base_class']); + $supportsRedirections = isset($interfaces['Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface']); + + return + $this->startClass($options['class'], $options['base_class']). + $this->addConstructor(). + $this->addMatcher($supportsRedirections). + $this->endClass() + ; + } + + private function addMatcher($supportsRedirections) + { + // we need to deep clone the routes as we will modify the structure to optimize the dump + $code = implode("\n", $this->compileRoutes(clone $this->getRoutes(), $supportsRedirections)); + + return <<getIterator(); + $keys = array_keys($routeIterator->getArrayCopy()); + $keysCount = count($keys); + + $i = 0; + foreach ($routeIterator as $name => $route) { + $i++; + + if ($route instanceof RouteCollection) { + $prefix = $route->getPrefix(); + $optimizable = $prefix && count($route->all()) > 1 && false === strpos($route->getPrefix(), '{'); + $indent = ''; + if ($optimizable) { + for ($j = $i; $j < $keysCount; $j++) { + if ($keys[$j] === null) { + continue; + } + + $testRoute = $routeIterator->offsetGet($keys[$j]); + $isCollection = ($testRoute instanceof RouteCollection); + + $testPrefix = $isCollection ? $testRoute->getPrefix() : $testRoute->getPattern(); + + if (0 === strpos($testPrefix, $prefix)) { + $routeIterator->offsetUnset($keys[$j]); + + if ($isCollection) { + $route->addCollection($testRoute); + } else { + $route->add($keys[$j], $testRoute); + } + + $i++; + $keys[$j] = null; + } + } + + if ($prefix !== $parentPrefix) { + $code[] = sprintf(" if (0 === strpos(\$pathinfo, %s)) {", var_export($prefix, true)); + $indent = ' '; + } + } + + foreach ($this->compileRoutes($route, $supportsRedirections, $prefix) as $line) { + foreach (explode("\n", $line) as $l) { + if ($l) { + $code[] = $indent.$l; + } else { + $code[] = $l; + } + } + } + + if ($optimizable && $prefix !== $parentPrefix) { + $code[] = " }\n"; + } + } else { + foreach ($this->compileRoute($route, $name, $supportsRedirections, $parentPrefix) as $line) { + $code[] = $line; + } + } + } + + return $code; + } + + private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null) + { + $code = array(); + $compiledRoute = $route->compile(); + $conditions = array(); + $hasTrailingSlash = false; + $matches = false; + if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P.*?)\$\1#', $compiledRoute->getRegex(), $m)) { + if ($supportsRedirections && substr($m['url'], -1) === '/') { + $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true)); + $hasTrailingSlash = true; + } else { + $conditions[] = sprintf("\$pathinfo === %s", var_export(str_replace('\\', '', $m['url']), true)); + } + } else { + if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() != $parentPrefix) { + $conditions[] = sprintf("0 === strpos(\$pathinfo, %s)", var_export($compiledRoute->getStaticPrefix(), true)); + } + + $regex = $compiledRoute->getRegex(); + if ($supportsRedirections && $pos = strpos($regex, '/$')) { + $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); + $hasTrailingSlash = true; + } + $conditions[] = sprintf("preg_match(%s, \$pathinfo, \$matches)", var_export($regex, true)); + + $matches = true; + } + + $conditions = implode(' && ', $conditions); + + $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name); + + $code[] = <<getRequirement('_method')) { + $methods = explode('|', strtoupper($req)); + // GET and HEAD are equivalent + if (in_array('GET', $methods) && !in_array('HEAD', $methods)) { + $methods[] = 'HEAD'; + } + if (1 === count($methods)) { + $code[] = <<context->getMethod() != '$methods[0]') { + \$allow[] = '$methods[0]'; + goto $gotoname; + } +EOF; + } else { + $methods = implode('\', \'', $methods); + $code[] = <<context->getMethod(), array('$methods'))) { + \$allow = array_merge(\$allow, array('$methods')); + goto $gotoname; + } +EOF; + } + } + + if ($hasTrailingSlash) { + $code[] = sprintf(<<redirect(\$pathinfo.'/', '%s'); + } +EOF + , $name); + } + + if ($scheme = $route->getRequirement('_scheme')) { + if (!$supportsRedirections) { + throw new \LogicException('The "_scheme" requirement is only supported for route dumper that implements RedirectableUrlMatcherInterface.'); + } + + $code[] = sprintf(<<context->getScheme() !== '$scheme') { + return \$this->redirect(\$pathinfo, '%s', '$scheme'); + } +EOF + , $name); + } + + // optimize parameters array + if (true === $matches && $compiledRoute->getDefaults()) { + $code[] = sprintf(" return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));" + , str_replace("\n", '', var_export($compiledRoute->getDefaults(), true)), $name); + } elseif (true === $matches) { + $code[] = sprintf(" \$matches['_route'] = '%s';", $name); + $code[] = sprintf(" return \$matches;", $name); + } elseif ($compiledRoute->getDefaults()) { + $code[] = sprintf(' return %s;', str_replace("\n", '', var_export(array_merge($compiledRoute->getDefaults(), array('_route' => $name)), true))); + } else { + $code[] = sprintf(" return array('_route' => '%s');", $name); + } + $code[] = " }"; + + if ($req) { + $code[] = " $gotoname:"; + } + + $code[] = ''; + + return $code; + } + + private function startClass($class, $baseClass) + { + return <<context = \$context; + } + +EOF; + } + + private function endClass() + { + return << + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\Routing\Exception\ResourceNotFoundException; + +/** + * @author Fabien Potencier + * + * @api + */ +abstract class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface +{ + private $trailingSlashTest = false; + + /** + * @see UrlMatcher::match() + * + * @api + */ + public function match($pathinfo) + { + try { + $parameters = parent::match($pathinfo); + } catch (ResourceNotFoundException $e) { + if ('/' === substr($pathinfo, -1)) { + throw $e; + } + + // try with a / at the end + $this->trailingSlashTest = true; + + return $this->match($pathinfo.'/'); + } + + if ($this->trailingSlashTest) { + $this->trailingSlashTest = false; + + return $this->redirect($pathinfo, null); + } + + return $parameters; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/RedirectableUrlMatcherInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/RedirectableUrlMatcherInterface.php new file mode 100644 index 0000000000..929ae9cc78 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/RedirectableUrlMatcherInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +/** + * RedirectableUrlMatcherInterface knows how to redirect the user. + * + * @author Fabien Potencier + * + * @api + */ +interface RedirectableUrlMatcherInterface +{ + /** + * Redirects the user to another URL. + * + * @param string $path The path info to redirect to. + * @param string $route The route that matched + * @param string $scheme The URL scheme (null to keep the current one) + * + * @return array An array of parameters + * + * @api + */ + public function redirect($path, $route, $scheme = null); +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcher.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcher.php new file mode 100644 index 0000000000..5ff8070ea0 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\Routing\Exception\MethodNotAllowedException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RequestContext; + +/** + * UrlMatcher matches URL based on a set of routes. + * + * @author Fabien Potencier + * + * @api + */ +class UrlMatcher implements UrlMatcherInterface +{ + protected $context; + protected $allow; + + private $routes; + + /** + * Constructor. + * + * @param RouteCollection $routes A RouteCollection instance + * @param RequestContext $context The context + * + * @api + */ + public function __construct(RouteCollection $routes, RequestContext $context) + { + $this->routes = $routes; + $this->context = $context; + } + + /** + * Sets the request context. + * + * @param RequestContext $context The context + * + * @api + */ + public function setContext(RequestContext $context) + { + $this->context = $context; + } + + /** + * Gets the request context. + * + * @return RequestContext The context + */ + public function getContext() + { + return $this->context; + } + + /** + * Tries to match a URL with a set of routes. + * + * @param string $pathinfo The path info to be parsed + * + * @return array An array of parameters + * + * @throws ResourceNotFoundException If the resource could not be found + * @throws MethodNotAllowedException If the resource was found but the request method is not allowed + * + * @api + */ + public function match($pathinfo) + { + $this->allow = array(); + + if ($ret = $this->matchCollection($pathinfo, $this->routes)) { + return $ret; + } + + throw 0 < count($this->allow) + ? new MethodNotAllowedException(array_unique(array_map('strtoupper', $this->allow))) + : new ResourceNotFoundException(); + } + + protected function matchCollection($pathinfo, RouteCollection $routes) + { + $pathinfo = urldecode($pathinfo); + + foreach ($routes as $name => $route) { + if ($route instanceof RouteCollection) { + if (false === strpos($route->getPrefix(), '{') && $route->getPrefix() !== substr($pathinfo, 0, strlen($route->getPrefix()))) { + continue; + } + + if (!$ret = $this->matchCollection($pathinfo, $route)) { + continue; + } + + return $ret; + } + + $compiledRoute = $route->compile(); + + // check the static prefix of the URL first. Only use the more expensive preg_match when it matches + if ('' !== $compiledRoute->getStaticPrefix() && 0 !== strpos($pathinfo, $compiledRoute->getStaticPrefix())) { + continue; + } + + if (!preg_match($compiledRoute->getRegex(), $pathinfo, $matches)) { + continue; + } + + // check HTTP method requirement + if ($req = $route->getRequirement('_method')) { + // HEAD and GET are equivalent as per RFC + if ('HEAD' === $method = $this->context->getMethod()) { + $method = 'GET'; + } + + if (!in_array($method, $req = explode('|', strtoupper($req)))) { + $this->allow = array_merge($this->allow, $req); + + continue; + } + } + + return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $name)); + } + } + + protected function mergeDefaults($params, $defaults) + { + $parameters = $defaults; + foreach ($params as $key => $value) { + if (!is_int($key)) { + $parameters[$key] = rawurldecode($value); + } + } + + return $parameters; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcherInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcherInterface.php new file mode 100644 index 0000000000..5823d3201b --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Matcher/UrlMatcherInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\Routing\RequestContextAwareInterface; + +/** + * UrlMatcherInterface is the interface that all URL matcher classes must implement. + * + * @author Fabien Potencier + * + * @api + */ +interface UrlMatcherInterface extends RequestContextAwareInterface +{ + /** + * Tries to match a URL with a set of routes. + * + * @param string $pathinfo The path info to be parsed + * + * @return array An array of parameters + * + * @throws ResourceNotFoundException If the resource could not be found + * @throws MethodNotAllowedException If the resource was found but the request method is not allowed + * + * @api + */ + public function match($pathinfo); +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/README.md b/3rdparty/symfony/routing/Symfony/Component/Routing/README.md new file mode 100644 index 0000000000..eb72334d2e --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/README.md @@ -0,0 +1,32 @@ +Routing Component +================= + +Routing associates a request with the code that will convert it to a response. + +The example below demonstrates how you can set up a fully working routing +system: + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Routing\Matcher\UrlMatcher; + use Symfony\Component\Routing\RequestContext; + use Symfony\Component\Routing\RouteCollection; + use Symfony\Component\Routing\Route; + + $routes = new RouteCollection(); + $routes->add('hello', new Route('/hello', array('controller' => 'foo'))); + + $context = new RequestContext(); + + // this is optional and can be done without a Request instance + $context->fromRequest(Request::createFromGlobals()); + + $matcher = new UrlMatcher($routes, $context); + + $parameters = $matcher->match('/hello'); + +Resources +--------- + +Unit tests: + +https://github.com/symfony/symfony/tree/master/tests/Symfony/Tests/Component/Routing diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/RequestContext.php b/3rdparty/symfony/routing/Symfony/Component/Routing/RequestContext.php new file mode 100644 index 0000000000..fef85b6bb3 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/RequestContext.php @@ -0,0 +1,250 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * Holds information about the current request. + * + * @author Fabien Potencier + * + * @api + */ +class RequestContext +{ + private $baseUrl; + private $method; + private $host; + private $scheme; + private $httpPort; + private $httpsPort; + private $parameters; + + /** + * Constructor. + * + * @param string $baseUrl The base URL + * @param string $method The HTTP method + * @param string $host The HTTP host name + * @param string $scheme The HTTP scheme + * @param integer $httpPort The HTTP port + * @param integer $httpsPort The HTTPS port + * + * @api + */ + public function __construct($baseUrl = '', $method = 'GET', $host = 'localhost', $scheme = 'http', $httpPort = 80, $httpsPort = 443) + { + $this->baseUrl = $baseUrl; + $this->method = strtoupper($method); + $this->host = $host; + $this->scheme = strtolower($scheme); + $this->httpPort = $httpPort; + $this->httpsPort = $httpsPort; + $this->parameters = array(); + } + + /** + * Gets the base URL. + * + * @return string The base URL + */ + public function getBaseUrl() + { + return $this->baseUrl; + } + + /** + * Sets the base URL. + * + * @param string $baseUrl The base URL + * + * @api + */ + public function setBaseUrl($baseUrl) + { + $this->baseUrl = $baseUrl; + } + + /** + * Gets the HTTP method. + * + * The method is always an uppercased string. + * + * @return string The HTTP method + */ + public function getMethod() + { + return $this->method; + } + + /** + * Sets the HTTP method. + * + * @param string $method The HTTP method + * + * @api + */ + public function setMethod($method) + { + $this->method = strtoupper($method); + } + + /** + * Gets the HTTP host. + * + * @return string The HTTP host + */ + public function getHost() + { + return $this->host; + } + + /** + * Sets the HTTP host. + * + * @param string $host The HTTP host + * + * @api + */ + public function setHost($host) + { + $this->host = $host; + } + + /** + * Gets the HTTP scheme. + * + * @return string The HTTP scheme + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * Sets the HTTP scheme. + * + * @param string $scheme The HTTP scheme + * + * @api + */ + public function setScheme($scheme) + { + $this->scheme = strtolower($scheme); + } + + /** + * Gets the HTTP port. + * + * @return string The HTTP port + */ + public function getHttpPort() + { + return $this->httpPort; + } + + /** + * Sets the HTTP port. + * + * @param string $httpPort The HTTP port + * + * @api + */ + public function setHttpPort($httpPort) + { + $this->httpPort = $httpPort; + } + + /** + * Gets the HTTPS port. + * + * @return string The HTTPS port + */ + public function getHttpsPort() + { + return $this->httpsPort; + } + + /** + * Sets the HTTPS port. + * + * @param string $httpsPort The HTTPS port + * + * @api + */ + public function setHttpsPort($httpsPort) + { + $this->httpsPort = $httpsPort; + } + + /** + * Returns the parameters. + * + * @return array The parameters + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * Sets the parameters. + * + * This method implements a fluent interface. + * + * @param array $parameters The parameters + * + * @return Route The current Route instance + */ + public function setParameters(array $parameters) + { + $this->parameters = $parameters; + + return $this; + } + + /** + * Gets a parameter value. + * + * @param string $name A parameter name + * + * @return mixed The parameter value + */ + public function getParameter($name) + { + return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + } + + /** + * Checks if a parameter value is set for the given parameter. + * + * @param string $name A parameter name + * + * @return Boolean true if the parameter value is set, false otherwise + */ + public function hasParameter($name) + { + return array_key_exists($name, $this->parameters); + } + + /** + * Sets a parameter value. + * + * @param string $name A parameter name + * @param mixed $parameter The parameter value + * + * @api + */ + public function setParameter($name, $parameter) + { + $this->parameters[$name] = $parameter; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/RequestContextAwareInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/RequestContextAwareInterface.php new file mode 100644 index 0000000000..38443a88b7 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/RequestContextAwareInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * @api + */ +interface RequestContextAwareInterface +{ + /** + * Sets the request context. + * + * @param RequestContext $context The context + * + * @api + */ + public function setContext(RequestContext $context); +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Route.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Route.php new file mode 100644 index 0000000000..548568334d --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Route.php @@ -0,0 +1,312 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * A Route describes a route and its parameters. + * + * @author Fabien Potencier + * + * @api + */ +class Route +{ + private $pattern; + private $defaults; + private $requirements; + private $options; + private $compiled; + + private static $compilers = array(); + + /** + * Constructor. + * + * Available options: + * + * * compiler_class: A class name able to compile this route instance (RouteCompiler by default) + * + * @param string $pattern The pattern to match + * @param array $defaults An array of default parameter values + * @param array $requirements An array of requirements for parameters (regexes) + * @param array $options An array of options + * + * @api + */ + public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array()) + { + $this->setPattern($pattern); + $this->setDefaults($defaults); + $this->setRequirements($requirements); + $this->setOptions($options); + } + + public function __clone() + { + $this->compiled = null; + } + + /** + * Returns the pattern. + * + * @return string The pattern + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * Sets the pattern. + * + * This method implements a fluent interface. + * + * @param string $pattern The pattern + * + * @return Route The current Route instance + */ + public function setPattern($pattern) + { + $this->pattern = trim($pattern); + + // a route must start with a slash + if (empty($this->pattern) || '/' !== $this->pattern[0]) { + $this->pattern = '/'.$this->pattern; + } + + return $this; + } + + /** + * Returns the options. + * + * @return array The options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Sets the options. + * + * This method implements a fluent interface. + * + * @param array $options The options + * + * @return Route The current Route instance + */ + public function setOptions(array $options) + { + $this->options = array_merge(array( + 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler', + ), $options); + + return $this; + } + + /** + * Sets an option value. + * + * This method implements a fluent interface. + * + * @param string $name An option name + * @param mixed $value The option value + * + * @return Route The current Route instance + * + * @api + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * Get an option value. + * + * @param string $name An option name + * + * @return mixed The option value + */ + public function getOption($name) + { + return isset($this->options[$name]) ? $this->options[$name] : null; + } + + /** + * Returns the defaults. + * + * @return array The defaults + */ + public function getDefaults() + { + return $this->defaults; + } + + /** + * Sets the defaults. + * + * This method implements a fluent interface. + * + * @param array $defaults The defaults + * + * @return Route The current Route instance + */ + public function setDefaults(array $defaults) + { + $this->defaults = array(); + foreach ($defaults as $name => $default) { + $this->defaults[(string) $name] = $default; + } + + return $this; + } + + /** + * Gets a default value. + * + * @param string $name A variable name + * + * @return mixed The default value + */ + public function getDefault($name) + { + return isset($this->defaults[$name]) ? $this->defaults[$name] : null; + } + + /** + * Checks if a default value is set for the given variable. + * + * @param string $name A variable name + * + * @return Boolean true if the default value is set, false otherwise + */ + public function hasDefault($name) + { + return array_key_exists($name, $this->defaults); + } + + /** + * Sets a default value. + * + * @param string $name A variable name + * @param mixed $default The default value + * + * @return Route The current Route instance + * + * @api + */ + public function setDefault($name, $default) + { + $this->defaults[(string) $name] = $default; + + return $this; + } + + /** + * Returns the requirements. + * + * @return array The requirements + */ + public function getRequirements() + { + return $this->requirements; + } + + /** + * Sets the requirements. + * + * This method implements a fluent interface. + * + * @param array $requirements The requirements + * + * @return Route The current Route instance + */ + public function setRequirements(array $requirements) + { + $this->requirements = array(); + foreach ($requirements as $key => $regex) { + $this->requirements[$key] = $this->sanitizeRequirement($key, $regex); + } + + return $this; + } + + /** + * Returns the requirement for the given key. + * + * @param string $key The key + * + * @return string The regex + */ + public function getRequirement($key) + { + return isset($this->requirements[$key]) ? $this->requirements[$key] : null; + } + + /** + * Sets a requirement for the given key. + * + * @param string $key The key + * @param string $regex The regex + * + * @return Route The current Route instance + * + * @api + */ + public function setRequirement($key, $regex) + { + $this->requirements[$key] = $this->sanitizeRequirement($key, $regex); + + return $this; + } + + /** + * Compiles the route. + * + * @return CompiledRoute A CompiledRoute instance + */ + public function compile() + { + if (null !== $this->compiled) { + return $this->compiled; + } + + $class = $this->getOption('compiler_class'); + + if (!isset(self::$compilers[$class])) { + self::$compilers[$class] = new $class; + } + + return $this->compiled = self::$compilers[$class]->compile($this); + } + + private function sanitizeRequirement($key, $regex) + { + if (is_array($regex)) { + throw new \InvalidArgumentException(sprintf('Routing requirements must be a string, array given for "%s"', $key)); + } + + if ('^' == $regex[0]) { + $regex = substr($regex, 1); + } + + if ('$' == substr($regex, -1)) { + $regex = substr($regex, 0, -1); + } + + return $regex; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCollection.php b/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCollection.php new file mode 100644 index 0000000000..b3289d337c --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCollection.php @@ -0,0 +1,259 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +use Symfony\Component\Config\Resource\ResourceInterface; + +/** + * A RouteCollection represents a set of Route instances. + * + * When adding a route, it overrides existing routes with the + * same name defined in the instance or its children and parents. + * + * @author Fabien Potencier + * + * @api + */ +class RouteCollection implements \IteratorAggregate +{ + private $routes; + private $resources; + private $prefix; + private $parent; + + /** + * Constructor. + * + * @api + */ + public function __construct() + { + $this->routes = array(); + $this->resources = array(); + $this->prefix = ''; + } + + public function __clone() + { + foreach ($this->routes as $name => $route) { + $this->routes[$name] = clone $route; + if ($route instanceof RouteCollection) { + $this->routes[$name]->setParent($this); + } + } + } + + /** + * Gets the parent RouteCollection. + * + * @return RouteCollection The parent RouteCollection + */ + public function getParent() + { + return $this->parent; + } + + /** + * Sets the parent RouteCollection. + * + * @param RouteCollection $parent The parent RouteCollection + */ + public function setParent(RouteCollection $parent) + { + $this->parent = $parent; + } + + /** + * Gets the current RouteCollection as an Iterator. + * + * @return \ArrayIterator An \ArrayIterator interface + */ + public function getIterator() + { + return new \ArrayIterator($this->routes); + } + + /** + * Adds a route. + * + * @param string $name The route name + * @param Route $route A Route instance + * + * @throws \InvalidArgumentException When route name contains non valid characters + * + * @api + */ + public function add($name, Route $route) + { + if (!preg_match('/^[a-z0-9A-Z_.]+$/', $name)) { + throw new \InvalidArgumentException(sprintf('The provided route name "%s" contains non valid characters. A route name must only contain digits (0-9), letters (a-z and A-Z), underscores (_) and dots (.).', $name)); + } + + $parent = $this; + while ($parent->getParent()) { + $parent = $parent->getParent(); + } + + if ($parent) { + $parent->remove($name); + } + + $this->routes[$name] = $route; + } + + /** + * Returns the array of routes. + * + * @return array An array of routes + */ + public function all() + { + $routes = array(); + foreach ($this->routes as $name => $route) { + if ($route instanceof RouteCollection) { + $routes = array_merge($routes, $route->all()); + } else { + $routes[$name] = $route; + } + } + + return $routes; + } + + /** + * Gets a route by name. + * + * @param string $name The route name + * + * @return Route $route A Route instance + */ + public function get($name) + { + // get the latest defined route + foreach (array_reverse($this->routes) as $routes) { + if (!$routes instanceof RouteCollection) { + continue; + } + + if (null !== $route = $routes->get($name)) { + return $route; + } + } + + if (isset($this->routes[$name])) { + return $this->routes[$name]; + } + } + + /** + * Removes a route by name. + * + * @param string $name The route name + */ + public function remove($name) + { + if (isset($this->routes[$name])) { + unset($this->routes[$name]); + } + + foreach ($this->routes as $routes) { + if ($routes instanceof RouteCollection) { + $routes->remove($name); + } + } + } + + /** + * Adds a route collection to the current set of routes (at the end of the current set). + * + * @param RouteCollection $collection A RouteCollection instance + * @param string $prefix An optional prefix to add before each pattern of the route collection + * + * @api + */ + public function addCollection(RouteCollection $collection, $prefix = '') + { + $collection->setParent($this); + $collection->addPrefix($prefix); + + // remove all routes with the same name in all existing collections + foreach (array_keys($collection->all()) as $name) { + $this->remove($name); + } + + $this->routes[] = $collection; + } + + /** + * Adds a prefix to all routes in the current set. + * + * @param string $prefix An optional prefix to add before each pattern of the route collection + * + * @api + */ + public function addPrefix($prefix) + { + // a prefix must not end with a slash + $prefix = rtrim($prefix, '/'); + + if (!$prefix) { + return; + } + + // a prefix must start with a slash + if ('/' !== $prefix[0]) { + $prefix = '/'.$prefix; + } + + $this->prefix = $prefix.$this->prefix; + + foreach ($this->routes as $name => $route) { + if ($route instanceof RouteCollection) { + $route->addPrefix($prefix); + } else { + $route->setPattern($prefix.$route->getPattern()); + } + } + } + + public function getPrefix() + { + return $this->prefix; + } + + /** + * Returns an array of resources loaded to build this collection. + * + * @return ResourceInterface[] An array of resources + */ + public function getResources() + { + $resources = $this->resources; + foreach ($this as $routes) { + if ($routes instanceof RouteCollection) { + $resources = array_merge($resources, $routes->getResources()); + } + } + + return array_unique($resources); + } + + /** + * Adds a resource for this collection. + * + * @param ResourceInterface $resource A resource instance + */ + public function addResource(ResourceInterface $resource) + { + $this->resources[] = $resource; + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCompiler.php b/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCompiler.php new file mode 100644 index 0000000000..72ececc5ca --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCompiler.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * RouteCompiler compiles Route instances to CompiledRoute instances. + * + * @author Fabien Potencier + */ +class RouteCompiler implements RouteCompilerInterface +{ + /** + * Compiles the current route instance. + * + * @param Route $route A Route instance + * + * @return CompiledRoute A CompiledRoute instance + */ + public function compile(Route $route) + { + $pattern = $route->getPattern(); + $len = strlen($pattern); + $tokens = array(); + $variables = array(); + $pos = 0; + preg_match_all('#.\{([\w\d_]+)\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); + foreach ($matches as $match) { + if ($text = substr($pattern, $pos, $match[0][1] - $pos)) { + $tokens[] = array('text', $text); + } + $seps = array($pattern[$pos]); + $pos = $match[0][1] + strlen($match[0][0]); + $var = $match[1][0]; + + if ($req = $route->getRequirement($var)) { + $regexp = $req; + } else { + if ($pos !== $len) { + $seps[] = $pattern[$pos]; + } + $regexp = sprintf('[^%s]+?', preg_quote(implode('', array_unique($seps)), '#')); + } + + $tokens[] = array('variable', $match[0][0][0], $regexp, $var); + + if (in_array($var, $variables)) { + throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $route->getPattern(), $var)); + } + + $variables[] = $var; + } + + if ($pos < $len) { + $tokens[] = array('text', substr($pattern, $pos)); + } + + // find the first optional token + $firstOptional = INF; + for ($i = count($tokens) - 1; $i >= 0; $i--) { + $token = $tokens[$i]; + if ('variable' === $token[0] && $route->hasDefault($token[3])) { + $firstOptional = $i; + } else { + break; + } + } + + // compute the matching regexp + $regexp = ''; + for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) { + $regexp .= $this->computeRegexp($tokens, $i, $firstOptional); + } + + return new CompiledRoute( + $route, + 'text' === $tokens[0][0] ? $tokens[0][1] : '', + sprintf("#^%s$#s", $regexp), + array_reverse($tokens), + $variables + ); + } + + /** + * Computes the regexp used to match the token. + * + * @param array $tokens The route tokens + * @param integer $index The index of the current token + * @param integer $firstOptional The index of the first optional token + * + * @return string The regexp + */ + private function computeRegexp(array $tokens, $index, $firstOptional) + { + $token = $tokens[$index]; + if ('text' === $token[0]) { + // Text tokens + return preg_quote($token[1], '#'); + } else { + // Variable tokens + if (0 === $index && 0 === $firstOptional && 1 == count($tokens)) { + // When the only token is an optional variable token, the separator is required + return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], '#'), $token[3], $token[2]); + } else { + $nbTokens = count($tokens); + $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], '#'), $token[3], $token[2]); + if ($index >= $firstOptional) { + // Enclose each optional tokens in a subpattern to make it optional + $regexp = "(?:$regexp"; + if ($nbTokens - 1 == $index) { + // Close the optional subpatterns + $regexp .= str_repeat(")?", $nbTokens - $firstOptional); + } + } + + return $regexp; + } + } + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCompilerInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCompilerInterface.php new file mode 100644 index 0000000000..5c988adafb --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/RouteCompilerInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * RouteCompilerInterface is the interface that all RouteCompiler classes must implements. + * + * @author Fabien Potencier + */ +interface RouteCompilerInterface +{ + /** + * Compiles the current route instance. + * + * @param Route $route A Route instance + * + * @return CompiledRoute A CompiledRoute instance + */ + public function compile(Route $route); +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/Router.php b/3rdparty/symfony/routing/Symfony/Component/Routing/Router.php new file mode 100644 index 0000000000..eadb2231e1 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/Router.php @@ -0,0 +1,263 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Config\ConfigCache; + +/** + * The Router class is an example of the integration of all pieces of the + * routing system for easier use. + * + * @author Fabien Potencier + */ +class Router implements RouterInterface +{ + protected $matcher; + protected $generator; + protected $defaults; + protected $context; + protected $loader; + protected $collection; + protected $resource; + protected $options; + + /** + * Constructor. + * + * @param LoaderInterface $loader A LoaderInterface instance + * @param mixed $resource The main resource to load + * @param array $options An array of options + * @param RequestContext $context The context + * @param array $defaults The default values + */ + public function __construct(LoaderInterface $loader, $resource, array $options = array(), RequestContext $context = null, array $defaults = array()) + { + $this->loader = $loader; + $this->resource = $resource; + $this->context = null === $context ? new RequestContext() : $context; + $this->defaults = $defaults; + $this->setOptions($options); + } + + /** + * Sets options. + * + * Available options: + * + * * cache_dir: The cache directory (or null to disable caching) + * * debug: Whether to enable debugging or not (false by default) + * * resource_type: Type hint for the main resource (optional) + * + * @param array $options An array of options + * + * @throws \InvalidArgumentException When unsupported option is provided + */ + public function setOptions(array $options) + { + $this->options = array( + 'cache_dir' => null, + 'debug' => false, + 'generator_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator', + 'generator_base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator', + 'generator_dumper_class' => 'Symfony\\Component\\Routing\\Generator\\Dumper\\PhpGeneratorDumper', + 'generator_cache_class' => 'ProjectUrlGenerator', + 'matcher_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher', + 'matcher_base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher', + 'matcher_dumper_class' => 'Symfony\\Component\\Routing\\Matcher\\Dumper\\PhpMatcherDumper', + 'matcher_cache_class' => 'ProjectUrlMatcher', + 'resource_type' => null, + ); + + // check option names and live merge, if errors are encountered Exception will be thrown + $invalid = array(); + $isInvalid = false; + foreach ($options as $key => $value) { + if (array_key_exists($key, $this->options)) { + $this->options[$key] = $value; + } else { + $isInvalid = true; + $invalid[] = $key; + } + } + + if ($isInvalid) { + throw new \InvalidArgumentException(sprintf('The Router does not support the following options: "%s".', implode('\', \'', $invalid))); + } + } + + /** + * Sets an option. + * + * @param string $key The key + * @param mixed $value The value + * + * @throws \InvalidArgumentException + */ + public function setOption($key, $value) + { + if (!array_key_exists($key, $this->options)) { + throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); + } + + $this->options[$key] = $value; + } + + /** + * Gets an option value. + * + * @param string $key The key + * + * @return mixed The value + * + * @throws \InvalidArgumentException + */ + public function getOption($key) + { + if (!array_key_exists($key, $this->options)) { + throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); + } + + return $this->options[$key]; + } + + /** + * Gets the RouteCollection instance associated with this Router. + * + * @return RouteCollection A RouteCollection instance + */ + public function getRouteCollection() + { + if (null === $this->collection) { + $this->collection = $this->loader->load($this->resource, $this->options['resource_type']); + } + + return $this->collection; + } + + /** + * Sets the request context. + * + * @param RequestContext $context The context + */ + public function setContext(RequestContext $context) + { + $this->context = $context; + + $this->getMatcher()->setContext($context); + $this->getGenerator()->setContext($context); + } + + /** + * Gets the request context. + * + * @return RequestContext The context + */ + public function getContext() + { + return $this->context; + } + + /** + * Generates a URL from the given parameters. + * + * @param string $name The name of the route + * @param mixed $parameters An array of parameters + * @param Boolean $absolute Whether to generate an absolute URL + * + * @return string The generated URL + */ + public function generate($name, $parameters = array(), $absolute = false) + { + return $this->getGenerator()->generate($name, $parameters, $absolute); + } + + /** + * Tries to match a URL with a set of routes. + * + * Returns false if no route matches the URL. + * + * @param string $url URL to be parsed + * + * @return array|false An array of parameters or false if no route matches + */ + public function match($url) + { + return $this->getMatcher()->match($url); + } + + /** + * Gets the UrlMatcher instance associated with this Router. + * + * @return UrlMatcherInterface A UrlMatcherInterface instance + */ + public function getMatcher() + { + if (null !== $this->matcher) { + return $this->matcher; + } + + if (null === $this->options['cache_dir'] || null === $this->options['matcher_cache_class']) { + return $this->matcher = new $this->options['matcher_class']($this->getRouteCollection(), $this->context, $this->defaults); + } + + $class = $this->options['matcher_cache_class']; + $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']); + if (!$cache->isFresh($class)) { + $dumper = new $this->options['matcher_dumper_class']($this->getRouteCollection()); + + $options = array( + 'class' => $class, + 'base_class' => $this->options['matcher_base_class'], + ); + + $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources()); + } + + require_once $cache; + + return $this->matcher = new $class($this->context, $this->defaults); + } + + /** + * Gets the UrlGenerator instance associated with this Router. + * + * @return UrlGeneratorInterface A UrlGeneratorInterface instance + */ + public function getGenerator() + { + if (null !== $this->generator) { + return $this->generator; + } + + if (null === $this->options['cache_dir'] || null === $this->options['generator_cache_class']) { + return $this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->defaults); + } + + $class = $this->options['generator_cache_class']; + $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']); + if (!$cache->isFresh($class)) { + $dumper = new $this->options['generator_dumper_class']($this->getRouteCollection()); + + $options = array( + 'class' => $class, + 'base_class' => $this->options['generator_base_class'], + ); + + $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources()); + } + + require_once $cache; + + return $this->generator = new $class($this->context, $this->defaults); + } +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/RouterInterface.php b/3rdparty/symfony/routing/Symfony/Component/Routing/RouterInterface.php new file mode 100644 index 0000000000..961342bf09 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/RouterInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; + +/** + * RouterInterface is the interface that all Router classes must implements. + * + * This interface is the concatenation of UrlMatcherInterface and UrlGeneratorInterface. + * + * @author Fabien Potencier + */ +interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface +{ +} diff --git a/3rdparty/symfony/routing/Symfony/Component/Routing/composer.json b/3rdparty/symfony/routing/Symfony/Component/Routing/composer.json new file mode 100644 index 0000000000..8d29398b24 --- /dev/null +++ b/3rdparty/symfony/routing/Symfony/Component/Routing/composer.json @@ -0,0 +1,29 @@ +{ + "name": "symfony/routing", + "type": "library", + "description": "Symfony Routing Component", + "keywords": [], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.2" + }, + "suggest": { + "symfony/config": "self.version", + "symfony/yaml": "self.version" + }, + "autoload": { + "psr-0": { "Symfony\\Component\\Routing": "" } + }, + "target-dir": "Symfony/Component/Routing" +} diff --git a/composer.json b/composer.json deleted file mode 100644 index 7916b15cb9..0000000000 --- a/composer.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "description": "ownCloud gives you universal access to your files/contacts/calendar through a web interface or WebDAV.", - "homepage": "http://owncloud.org", - "license": "AGPL-3.0+", - "support": { - "email": "owncloud@kde.org", - "irc": "irc://irc.freenode.org/owncloud", - "forum": "http://forum.owncloud.org/", - "issues": "https://github.com/owncloud/core/issues" - }, - "require": { - "php": ">=5.3.2", - "symfony/routing": "2.0.*" - }, - "config": { - "vendor-dir": "3rdparty" - } -} From f151376ad5beff46043645578add910b7e98a0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Schie=C3=9Fle?= Date: Sat, 27 Oct 2012 17:05:00 +0200 Subject: [PATCH 06/15] remove remaining line from merge conflict --- apps/files_versions/lib/versions.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/files_versions/lib/versions.php b/apps/files_versions/lib/versions.php index 56664427fb..2f27cd0e66 100644 --- a/apps/files_versions/lib/versions.php +++ b/apps/files_versions/lib/versions.php @@ -275,5 +275,4 @@ class Storage { $view = \OCP\Files::getStorage('files_versions'); return $view->deleteAll('', true); } -} ->>>>>>> 12ea922... fix broken paths in versions app +} From d6953fa5ed796699d82b82473317b700dfbcfa72 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Sat, 27 Oct 2012 17:44:47 +0200 Subject: [PATCH 07/15] Remove wrongly committed files --- apps/files/templates/index.php_orig | 90 ----------------------------- apps/files/templates/index.php_test | 90 ----------------------------- 2 files changed, 180 deletions(-) delete mode 100644 apps/files/templates/index.php_orig delete mode 100644 apps/files/templates/index.php_test diff --git a/apps/files/templates/index.php_orig b/apps/files/templates/index.php_orig deleted file mode 100644 index aff484f0a7..0000000000 --- a/apps/files/templates/index.php_orig +++ /dev/null @@ -1,90 +0,0 @@ - -

-
- - -
t('Nothing in here. Upload something!')?>
- - - - - - - - - - - - - -
- - t( 'Name' ); ?> - - - - Download" /> t('Download')?> - - - t( 'Size' ); ?> - t( 'Modified' ); ?> - - - - t('Unshare')?> <?php echo $l->t('Unshare')?>" /> - - t('Delete')?> <?php echo $l->t('Delete')?>" /> - - -
-
-
-

- t('The files you are trying to upload exceed the maximum size for file uploads on this server.');?> -

-
-
-

- t('Files are being scanned, please wait.');?> -

-

- t('Current scanning');?> -

-
- - - diff --git a/apps/files/templates/index.php_test b/apps/files/templates/index.php_test deleted file mode 100644 index a509333aa4..0000000000 --- a/apps/files/templates/index.php_test +++ /dev/null @@ -1,90 +0,0 @@ - -
- - -
-
- t('New');?> - -
-
-
- - - - - - - -
-
-
-
- -
- -
-
- - - - -
-
- - -
t('Nothing in here. Upload something!')?>
- - - - - - - - - - - - - -
- - t( 'Name' ); ?> - - - - Download" /> t('Download')?> - - - t( 'Size' ); ?> - t( 'Modified' ); ?> - - - - t('Unshare')?> <?php echo $l->t('Unshare')?>" /> - - t('Delete')?> <?php echo $l->t('Delete')?>" /> - - -
-
-
-

- t('The files you are trying to upload exceed the maximum size for file uploads on this server.');?> -

-
-
-

- t('Files are being scanned, please wait.');?> -

-

- t('Current scanning');?> -

-
- - - From fecfeac55d762ec80c9305b55e7140588bfe5dd5 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Sat, 27 Oct 2012 17:45:09 +0200 Subject: [PATCH 08/15] Fix introduced style errors --- core/lostpassword/controller.php | 12 ++++--- core/routes.php | 2 +- lib/base.php | 3 +- lib/ocs.php | 61 +++++++++++++++++--------------- lib/route.php | 6 +++- lib/router.php | 8 ++--- 6 files changed, 52 insertions(+), 40 deletions(-) diff --git a/core/lostpassword/controller.php b/core/lostpassword/controller.php index e616fe7dff..523520dc75 100644 --- a/core/lostpassword/controller.php +++ b/core/lostpassword/controller.php @@ -8,14 +8,16 @@ class OC_Core_LostPassword_Controller { protected static function displayLostPasswordPage($error, $requested) { - OC_Template::printGuestPage('core/lostpassword', 'lostpassword', array('error' => $error, 'requested' => $requested)); + OC_Template::printGuestPage('core/lostpassword', 'lostpassword', + array('error' => $error, 'requested' => $requested)); } protected static function displayResetPasswordPage($success, $args) { $route_args = array(); $route_args['token'] = $args['token']; $route_args['user'] = $args['user']; - OC_Template::printGuestPage('core/lostpassword', 'resetpassword', array('success' => $success, 'args' => $route_args)); + OC_Template::printGuestPage('core/lostpassword', 'resetpassword', + array('success' => $success, 'args' => $route_args)); } protected static function checkToken($user, $token) { @@ -29,10 +31,12 @@ class OC_Core_LostPassword_Controller { public static function sendEmail($args) { if (OC_User::userExists($_POST['user'])) { $token = hash('sha256', OC_Util::generate_random_bytes(30).OC_Config::getValue('passwordsalt', '')); - OC_Preferences::setValue($_POST['user'], 'owncloud', 'lostpassword', hash('sha256', $token)); // Hash the token again to prevent timing attacks + OC_Preferences::setValue($_POST['user'], 'owncloud', 'lostpassword', + hash('sha256', $token)); // Hash the token again to prevent timing attacks $email = OC_Preferences::getValue($_POST['user'], 'settings', 'email', ''); if (!empty($email)) { - $link = OC_Helper::linkToRoute('core_lostpassword_reset', array('user' => $_POST['user'], 'token' => $token)); + $link = OC_Helper::linkToRoute('core_lostpassword_reset', + array('user' => $_POST['user'], 'token' => $token)); $link = OC_Helper::makeURLAbsolute($link); $tmpl = new OC_Template('core/lostpassword', 'email'); diff --git a/core/routes.php b/core/routes.php index 7cf2749884..186fa8ae56 100644 --- a/core/routes.php +++ b/core/routes.php @@ -6,7 +6,7 @@ * See the COPYING-README file. */ -require_once('settings/routes.php'); +require_once 'settings/routes.php'; // Core ajax actions // AppConfig diff --git a/lib/base.php b/lib/base.php index f810b1f186..d7d5eef325 100644 --- a/lib/base.php +++ b/lib/base.php @@ -516,7 +516,8 @@ class OC{ } $file_ext = substr($param['file'], -3); if ($file_ext != 'php' - || !self::loadAppScriptFile($param)) { + || !self::loadAppScriptFile($param)) + { header('HTTP/1.0 404 Not Found'); } } diff --git a/lib/ocs.php b/lib/ocs.php index 60577ec5d5..1a0abf0e36 100644 --- a/lib/ocs.php +++ b/lib/ocs.php @@ -111,8 +111,8 @@ class OC_OCS { $format = $parameters['format']; $login = OC_OCS::readData('post', 'login', 'text'); $passwd = OC_OCS::readData('post', 'password', 'text'); - OC_OCS::personCheck($format,$login,$passwd); - }) + OC_OCS::personCheck($format, $login, $passwd); + }) ->requirements(array('format'=>'xml|json')); // ACTIVITY @@ -125,7 +125,7 @@ class OC_OCS { $pagesize = OC_OCS::readData('get', 'pagesize', 'int', 10); if($pagesize<1 or $pagesize>100) $pagesize=10; OC_OCS::activityGet($format, $page, $pagesize); - }) + }) ->requirements(array('format'=>'xml|json')); // activityput - POST ACTIVITY $router->create('activity_put', '/activity.{format}') @@ -134,8 +134,8 @@ class OC_OCS { ->action(function ($parameters) { $format = $parameters['format']; $message = OC_OCS::readData('post', 'message', 'text'); - OC_OCS::activityPut($format,$message); - }) + OC_OCS::activityPut($format, $message); + }) ->requirements(array('format'=>'xml|json')); // PRIVATEDATA @@ -148,7 +148,7 @@ class OC_OCS { $app = addslashes(strip_tags($parameters['app'])); $key = addslashes(strip_tags($parameters['key'])); OC_OCS::privateDataGet($format, $app, $key); - }) + }) ->requirements(array('format'=>'xml|json')); // set - POST DATA $router->create('privatedata_set', @@ -161,7 +161,7 @@ class OC_OCS { $key = addslashes(strip_tags($parameters['key'])); $value=OC_OCS::readData('post', 'value', 'text'); OC_OCS::privateDataSet($format, $app, $key, $value); - }) + }) ->requirements(array('format'=>'xml|json')); // delete - POST DATA $router->create('privatedata_delete', @@ -173,7 +173,7 @@ class OC_OCS { $app = addslashes(strip_tags($parameters['app'])); $key = addslashes(strip_tags($parameters['key'])); OC_OCS::privateDataDelete($format, $app, $key); - }) + }) ->requirements(array('format'=>'xml|json')); // CLOUD @@ -184,7 +184,7 @@ class OC_OCS { ->action(function ($parameters) { $format = $parameters['format']; OC_OCS::systemwebapps($format); - }) + }) ->requirements(array('format'=>'xml|json')); // quotaget @@ -195,7 +195,7 @@ class OC_OCS { $format = $parameters['format']; $user = $parameters['user']; OC_OCS::quotaGet($format, $user); - }) + }) ->requirements(array('format'=>'xml|json')); // quotaset $router->create('quota_set', @@ -207,7 +207,7 @@ class OC_OCS { $user = $parameters['user']; $quota = self::readData('post', 'quota', 'int'); OC_OCS::quotaSet($format, $user, $quota); - }) + }) ->requirements(array('format'=>'xml|json')); // keygetpublic @@ -217,8 +217,8 @@ class OC_OCS { ->action(function ($parameters) { $format = $parameters['format']; $user = $parameters['user']; - OC_OCS::publicKeyGet($format,$user); - }) + OC_OCS::publicKeyGet($format, $user); + }) ->requirements(array('format'=>'xml|json')); // keygetprivate @@ -228,8 +228,8 @@ class OC_OCS { ->action(function ($parameters) { $format = $parameters['format']; $user = $parameters['user']; - OC_OCS::privateKeyGet($format,$user); - }) + OC_OCS::privateKeyGet($format, $user); + }) ->requirements(array('format'=>'xml|json')); @@ -247,7 +247,10 @@ class OC_OCS { try { $router->match($_SERVER['PATH_INFO']); } catch (ResourceNotFoundException $e) { - $txt='Invalid query, please check the syntax. API specifications are here: http://www.freedesktop.org/wiki/Specifications/open-collaboration-services. DEBUG OUTPUT:'."\n"; + $txt='Invalid query, please check the syntax. ' + .'API specifications are here: ' + .'http://www.freedesktop.org/wiki/Specifications/open-collaboration-services.' + .'DEBUG OUTPUT:'."\n"; $txt.=OC_OCS::getdebugoutput(); echo(OC_OCS::generatexml($format, 'failed', 999, $txt)); } catch (MethodNotAllowedException $e) { @@ -323,7 +326,7 @@ class OC_OCS { * @param int $itemsperpage * @return string xml/json */ - private static function generateXml($format,$status,$statuscode,$message,$data=array(),$tag='',$tagattribute='',$dimension=-1,$itemscount='',$itemsperpage='') { + private static function generateXml($format, $status, $statuscode, $message, $data=array(), $tag='', $tagattribute='', $dimension=-1, $itemscount='', $itemsperpage='') { if($format=='json') { $json=array(); $json['status']=$status; @@ -343,7 +346,7 @@ class OC_OCS { xmlwriter_write_element($writer, 'status', $status); xmlwriter_write_element($writer, 'statuscode', $statuscode); xmlwriter_write_element($writer, 'message', $message); - if($itemscount<>'') xmlwriter_write_element($writer,'totalitems',$itemscount); + if($itemscount<>'') xmlwriter_write_element($writer, 'totalitems', $itemscount); if(!empty($itemsperpage)) xmlwriter_write_element($writer, 'itemsperpage', $itemsperpage); xmlwriter_end_element($writer); if($dimension=='0') { @@ -358,7 +361,7 @@ class OC_OCS { xmlwriter_end_element($writer); }elseif($dimension=='2') { - xmlwriter_start_element($writer,'data'); + xmlwriter_start_element($writer, 'data'); foreach($data as $entry) { xmlwriter_start_element($writer, $tag); if(!empty($tagattribute)) { @@ -413,14 +416,14 @@ class OC_OCS { } } - public static function toXml($writer,$data,$node) { + public static function toXml($writer, $data, $node) { foreach($data as $key => $value) { if (is_numeric($key)) { $key = $node; } if (is_array($value)) { xmlwriter_start_element($writer, $key); - OC_OCS::toxml($writer,$value, $node); + OC_OCS::toxml($writer, $value, $node); xmlwriter_end_element($writer); }else{ xmlwriter_write_element($writer, $key, $value); @@ -453,7 +456,7 @@ class OC_OCS { * @param string $passwd * @return string xml/json */ - private static function personCheck($format,$login,$passwd) { + private static function personCheck($format, $login, $passwd) { if($login<>'') { if(OC_User::login($login, $passwd)) { $xml['person']['personid']=$login; @@ -480,7 +483,7 @@ class OC_OCS { //TODO - $txt=OC_OCS::generatexml($format, 'ok', 100, '', $xml, 'activity', 'full', 2, $totalcount,$pagesize); + $txt=OC_OCS::generatexml($format, 'ok', 100, '', $xml, 'activity', 'full', 2, $totalcount, $pagesize); echo($txt); } @@ -490,7 +493,7 @@ class OC_OCS { * @param string $message * @return string xml/json */ - private static function activityPut($format,$message) { + private static function activityPut($format, $message) { // not implemented in ownCloud $user=OC_OCS::checkpassword(); echo(OC_OCS::generatexml($format, 'ok', 100, '')); @@ -621,7 +624,7 @@ class OC_OCS { foreach($apps as $app) { $info=OC_App::getAppInfo($app); if(isset($info['standalone'])) { - $newvalue=array('name'=>$info['name'],'url'=>OC_Helper::linkToAbsolute($app,''),'icon'=>''); + $newvalue=array('name'=>$info['name'], 'url'=>OC_Helper::linkToAbsolute($app, ''), 'icon'=>''); $values[]=$newvalue; } @@ -638,7 +641,7 @@ class OC_OCS { * @param string $user * @return string xml/json */ - private static function quotaGet($format,$user) { + private static function quotaGet($format, $user) { $login=OC_OCS::checkpassword(); if(OC_Group::inGroup($login, 'admin') or ($login==$user)) { @@ -677,7 +680,7 @@ class OC_OCS { * @param string $quota * @return string xml/json */ - private static function quotaSet($format,$user,$quota) { + private static function quotaSet($format, $user, $quota) { $login=OC_OCS::checkpassword(); if(OC_Group::inGroup($login, 'admin')) { @@ -700,7 +703,7 @@ class OC_OCS { * @param string $user * @return string xml/json */ - private static function publicKeyGet($format,$user) { + private static function publicKeyGet($format, $user) { $login=OC_OCS::checkpassword(); if(OC_User::userExists($user)) { @@ -718,7 +721,7 @@ class OC_OCS { * @param string $user * @return string xml/json */ - private static function privateKeyGet($format,$user) { + private static function privateKeyGet($format, $user) { $login=OC_OCS::checkpassword(); if(OC_Group::inGroup($login, 'admin') or ($login==$user)) { diff --git a/lib/route.php b/lib/route.php index 89af829d3d..d5233d7986 100644 --- a/lib/route.php +++ b/lib/route.php @@ -106,7 +106,11 @@ class OC_Route extends Route { * @param $file */ public function actionInclude($file) { - $function = create_function('$param', 'unset($param["_route"]);$_GET=array_merge($_GET,$param);unset($param);require_once "'.$file.'";'); + $function = create_function('$param', + 'unset($param["_route"]);' + .'$_GET=array_merge($_GET,$param);' + .'unset($param);' + .'require_once "'.$file.'";'); $this->action($function); } } diff --git a/lib/router.php b/lib/router.php index a471a06802..d5adb72055 100644 --- a/lib/router.php +++ b/lib/router.php @@ -35,9 +35,9 @@ class OC_Router { public function loadRoutes() { foreach(OC_APP::getEnabledApps() as $app){ $file = OC_App::getAppPath($app).'/appinfo/routes.php'; - if(file_exists($file)){ + if(file_exists($file)) { $this->useCollection($app); - require_once($file); + require_once $file; $collection = $this->getCollection($app); $this->root->addCollection($collection, '/apps/'.$app); } @@ -81,7 +81,7 @@ class OC_Router { * * @param string $url The url to find */ - public function match($url) { + public function match($url) { $matcher = new UrlMatcher($this->root, $this->context); $parameters = $matcher->match($url); if (isset($parameters['action'])) { @@ -93,7 +93,7 @@ class OC_Router { unset($parameters['action']); call_user_func($action, $parameters); } elseif (isset($parameters['file'])) { - include ($parameters['file']); + include $parameters['file']; } else { throw new Exception('no action available'); } From b942c1253e5d9acb0a7e3fdbbc3df21246f27f2c Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Sat, 27 Oct 2012 14:40:13 +0200 Subject: [PATCH 09/15] remove TODO comment, it's done --- apps/user_ldap/lib/access.php | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/user_ldap/lib/access.php b/apps/user_ldap/lib/access.php index d343945149..82a375b50e 100644 --- a/apps/user_ldap/lib/access.php +++ b/apps/user_ldap/lib/access.php @@ -526,7 +526,6 @@ abstract class Access { return; } //if count is bigger, then the server does not support paged search. Instead, he did a normal search. We set a flag here, so the callee knows how to deal with it. - //TODO: Instead, slice findings or selection later if($findings['count'] <= $limit) { $this->pagedSearchedSuccessful = true; } From 246221a677373d5ab709395e6f4e08576a3118a8 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 24 Oct 2012 18:24:06 +0200 Subject: [PATCH 10/15] LDAP: fix again proper check if groups are enabled --- apps/user_ldap/group_ldap.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/user_ldap/group_ldap.php b/apps/user_ldap/group_ldap.php index f1411782ad..2aaabc2ab1 100644 --- a/apps/user_ldap/group_ldap.php +++ b/apps/user_ldap/group_ldap.php @@ -28,7 +28,9 @@ class GROUP_LDAP extends lib\Access implements \OCP\GroupInterface { public function setConnector(lib\Connection &$connection) { parent::setConnector($connection); - if(!empty($this->connection->ldapGroupFilter) && !empty($this->connection->ldapGroupMemberAssocAttr)) { + $filter = $this->connection->ldapGroupFilter; + $gassoc = $this->connection->ldapGroupMemberAssocAttr; + if(!empty($filter) && !empty($gassoc)) { $this->enabled = true; } } From 977c4d184402fee27e262dffa00a7ea32ba5fe2c Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Sat, 27 Oct 2012 14:55:17 +0200 Subject: [PATCH 11/15] LDAP: use the correct attribute in filter --- apps/user_ldap/user_ldap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/user_ldap/user_ldap.php b/apps/user_ldap/user_ldap.php index 916d3f1d46..2b78919801 100644 --- a/apps/user_ldap/user_ldap.php +++ b/apps/user_ldap/user_ldap.php @@ -116,7 +116,7 @@ class USER_LDAP extends lib\Access implements \OCP\UserInterface { $search = empty($search) ? '*' : '*'.$search.'*'; $filter = $this->combineFilterWithAnd(array( $this->connection->ldapUserFilter, - $this->connection->ldapGroupDisplayName.'='.$search + $this->connection->ldapUserDisplayName.'='.$search )); \OCP\Util::writeLog('user_ldap', 'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter, \OCP\Util::DEBUG); From a053da58ce1c29071654814ce32d083b054ea542 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Sat, 27 Oct 2012 17:32:55 +0200 Subject: [PATCH 12/15] LDAP: be careful which limit is send to possible paged LDAP search --- apps/user_ldap/user_ldap.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/user_ldap/user_ldap.php b/apps/user_ldap/user_ldap.php index 2b78919801..69e470c78a 100644 --- a/apps/user_ldap/user_ldap.php +++ b/apps/user_ldap/user_ldap.php @@ -112,7 +112,10 @@ class USER_LDAP extends lib\Access implements \OCP\UserInterface { return $ldap_users; } - //prepare search filter + // if we'd pass -1 to LDAP search, we'd end up in a Protocol error. With a limit of 0, we get 0 results. So we pass null. + if($limit <= 0) { + $limit = null; + } $search = empty($search) ? '*' : '*'.$search.'*'; $filter = $this->combineFilterWithAnd(array( $this->connection->ldapUserFilter, From 33aa630af3cedf146b1c51575200f8a723ae1fa3 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Sat, 27 Oct 2012 17:35:39 +0200 Subject: [PATCH 13/15] LDAP: add error handling for failed searches --- apps/user_ldap/lib/access.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/user_ldap/lib/access.php b/apps/user_ldap/lib/access.php index 82a375b50e..fa8546f1ce 100644 --- a/apps/user_ldap/lib/access.php +++ b/apps/user_ldap/lib/access.php @@ -515,6 +515,11 @@ abstract class Access { $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, $limit, $offset); $sr = ldap_search($link_resource, $base, $filter, $attr); + if(!$sr) { + \OCP\Util::writeLog('user_ldap', 'Error when searching: '.ldap_error($link_resource).' code '.ldap_errno($link_resource), \OCP\Util::ERROR); + \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), \OCP\Util::ERROR); + return array(); + } $findings = ldap_get_entries($link_resource, $sr ); if($pagedSearchOK) { \OCP\Util::writeLog('user_ldap', 'Paged search successful', \OCP\Util::INFO); From 5b3c9518dc2e41c2da314c183678e03f799c2415 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Sat, 27 Oct 2012 17:36:08 +0200 Subject: [PATCH 14/15] LDAP: improve slicing --- apps/user_ldap/lib/access.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/user_ldap/lib/access.php b/apps/user_ldap/lib/access.php index fa8546f1ce..9b870a8c6d 100644 --- a/apps/user_ldap/lib/access.php +++ b/apps/user_ldap/lib/access.php @@ -585,10 +585,14 @@ abstract class Access { } $findings = $selection; } - if(!$this->pagedSearchedSuccessful + //we slice the findings, when + //a) paged search insuccessful, though attempted + //b) no paged search, but limit set + if((!$this->pagedSearchedSuccessful + && $pagedSearchOK) || ( - !is_null($limit) - || !is_null($offset) + !$pagedSearchOK + && !is_null($limit) ) ) { $findings = array_slice($findings, intval($offset), $limit); @@ -775,6 +779,7 @@ abstract class Access { $pagedSearchOK = false; if($this->connection->hasPagedResultSupport && !is_null($limit)) { $offset = intval($offset); //can be null + \OCP\Util::writeLog('user_ldap', 'initializing paged search for Filter'.$filter.' base '.$base.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset, \OCP\Util::DEBUG); //get the cookie from the search for the previous search, required by LDAP $cookie = $this->getPagedResultCookie($filter, $limit, $offset); if(empty($cookie) && ($offset > 0)) { From 1b2279c9353f189a5a10d632be5691e147230212 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Sat, 27 Oct 2012 17:36:55 +0200 Subject: [PATCH 15/15] LDAP: getGroups to use paged searches --- apps/user_ldap/group_ldap.php | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/user_ldap/group_ldap.php b/apps/user_ldap/group_ldap.php index 2aaabc2ab1..dafe2c0c31 100644 --- a/apps/user_ldap/group_ldap.php +++ b/apps/user_ldap/group_ldap.php @@ -206,22 +206,30 @@ class GROUP_LDAP extends lib\Access implements \OCP\GroupInterface { if(!$this->enabled) { return array(); } + $cachekey = 'getGroups-'.$search.'-'.$limit.'-'.$offset; - if($this->connection->isCached('getGroups')) { - $ldap_groups = $this->connection->getFromCache('getGroups'); - } else { - $ldap_groups = $this->fetchListOfGroups($this->connection->ldapGroupFilter, array($this->connection->ldapGroupDisplayName, 'dn')); - $ldap_groups = $this->ownCloudGroupNames($ldap_groups); - $this->connection->writeToCache('getGroups', $ldap_groups); + //Check cache before driving unnecessary searches + \OCP\Util::writeLog('user_ldap', 'getGroups '.$cachekey, \OCP\Util::DEBUG); + $ldap_groups = $this->connection->getFromCache($cachekey); + if(!is_null($ldap_groups)) { + return $ldap_groups; } - $this->groupSearch = $search; - if(!empty($this->groupSearch)) { - $ldap_groups = array_filter($ldap_groups, array($this, 'groupMatchesFilter')); - } - if($limit = -1) { + + // if we'd pass -1 to LDAP search, we'd end up in a Protocol error. With a limit of 0, we get 0 results. So we pass null. + if($limit <= 0) { $limit = null; } - return array_slice($ldap_groups, $offset, $limit); + $search = empty($search) ? '*' : '*'.$search.'*'; + $filter = $this->combineFilterWithAnd(array( + $this->connection->ldapGroupFilter, + $this->connection->ldapGroupDisplayName.'='.$search + )); + \OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, \OCP\Util::DEBUG); + $ldap_groups = $this->fetchListOfGroups($filter, array($this->connection->ldapGroupDisplayName, 'dn'), $limit, $offset); + $ldap_groups = $this->ownCloudGroupNames($ldap_groups); + + $this->connection->writeToCache($cachekey, $ldap_groups); + return $ldap_groups; } public function groupMatchesFilter($group) {
- - -
-
- t('New');?> - -
-
-
- - - - - - - -
-
-
-
- -
- -
-
- - - - -