aboutsummaryrefslogtreecommitdiffstats
path: root/calendar/lib
diff options
context:
space:
mode:
authorDaniel Lange <DLange@git.local>2016-03-07 15:53:16 +0100
committerDaniel Lange <DLange@git.local>2016-03-07 15:53:16 +0100
commit50569114acdc64e7c7cae1498635d3f821517c30 (patch)
tree13d6fe76af33134fbfb2286930fb6603047f9299 /calendar/lib
parentc210d30de6c62e7f7867bb32651349ddf455d9e6 (diff)
downloadroundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.tar.gz
roundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.tar.bz2
roundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.zip
Initial commit of the Faster IT roundcube_calendar plugin distribution
This includes: * Kolab plugins 3.2.9 (calendar and libcalendaring) * CalDAV driver 3.2.8 * .htaccess files for at least some security * SabreDAV updated to 1.8.12 (Jan 2015 release) * Support for CURLOPT_SSL_* settings to allow self-signed certificates * Small fixes & improved documentation
Diffstat (limited to 'calendar/lib')
-rw-r--r--calendar/lib/SabreDAV/.htaccess2
-rw-r--r--calendar/lib/SabreDAV/ChangeLog1111
-rw-r--r--calendar/lib/SabreDAV/LICENSE27
-rw-r--r--calendar/lib/SabreDAV/README.md30
-rw-r--r--calendar/lib/SabreDAV/composer.json61
-rw-r--r--calendar/lib/SabreDAV/composer.lock80
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php155
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php233
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php47
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php691
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php243
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php376
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php279
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php298
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php392
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php142
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php48
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php173
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php38
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php44
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php192
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php324
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php218
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php182
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php1338
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php32
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php180
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php180
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php134
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php74
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php227
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php102
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php88
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php40
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php45
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php16
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php163
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php72
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php116
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php526
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php342
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php315
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php221
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php80
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php18
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php166
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php333
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php260
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php20
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php20
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php706
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php72
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php260
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php108
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php26
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php87
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php101
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php63
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php65
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php112
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php99
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php57
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php491
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.icobin0 -> 4286 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.pngbin0 -> 7232 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.pngbin0 -> 4388 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.pngbin0 -> 5695 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.pngbin0 -> 3474 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.pngbin0 -> 2837 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.pngbin0 -> 3474 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.pngbin0 -> 5480 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php578
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php110
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php64
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php37
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php33
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php41
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php73
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php45
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php71
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php32
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php31
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php140
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php91
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php82
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php159
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php146
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php214
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php85
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php46
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php71
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php51
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php193
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php183
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php167
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php81
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php649
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php83
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php55
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php159
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php39
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php48
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php246
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php31
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php78
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php99
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php105
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php25
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php104
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php127
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php157
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php59
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php78
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php2178
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php90
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php108
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php121
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php91
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php289
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php193
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php133
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php124
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php64
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php191
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php155
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php83
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php74
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php42
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php1402
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php281
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php18
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php153
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php428
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php33
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php211
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php34
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php124
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php161
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php94
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php227
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php67
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php240
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php284
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php175
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php82
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/autoload.php25
-rw-r--r--calendar/lib/SabreDAV/vendor/autoload.php7
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/ClassLoader.php387
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php9
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php15
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php9
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_real.php50
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/installed.json52
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog70
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE27
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md384
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json31
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php405
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php108
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php244
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php107
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php70
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php68
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php46
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php68
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php181
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php109
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php172
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php322
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php187
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php100
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php12
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php442
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php125
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php245
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php180
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php223
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php1112
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php111
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php39
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php76
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php61
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php482
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php24
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php41
-rw-r--r--calendar/lib/caldav-client.php379
-rw-r--r--calendar/lib/calendar_itip.php240
-rw-r--r--calendar/lib/calendar_recurrence.php88
-rw-r--r--calendar/lib/calendar_ui.php918
-rw-r--r--calendar/lib/encryption.php166
-rw-r--r--calendar/lib/js/fullcalendar.js6845
232 files changed, 42349 insertions, 0 deletions
diff --git a/calendar/lib/SabreDAV/.htaccess b/calendar/lib/SabreDAV/.htaccess
new file mode 100644
index 0000000..c0800be
--- /dev/null
+++ b/calendar/lib/SabreDAV/.htaccess
@@ -0,0 +1,2 @@
+# Deny all for risky include files of this __CENSORED__ quality plugin
+Require all denied
diff --git a/calendar/lib/SabreDAV/ChangeLog b/calendar/lib/SabreDAV/ChangeLog
new file mode 100644
index 0000000..bc03df9
--- /dev/null
+++ b/calendar/lib/SabreDAV/ChangeLog
@@ -0,0 +1,1111 @@
+1.8.6-stable (2013-06-18)
+ * The zip release ships with sabre/vobject 2.1.0.
+ * Includes changes from version 1.7.8.
+
+1.8.5-stable (2013-04-11)
+ * The zip release ships with sabre/vobject 2.0.7.
+ * Includes changes from version 1.7.7.
+
+1.8.4-stable (2013-04-08)
+ * The zip release ships with sabre/vobject 2.0.7.
+ * Includes changes from version 1.7.6.
+
+1.8.3-stable (2013-03-01)
+ * The zip release ships with sabre/vobject 2.0.6.
+ * Includes changes from version 1.7.5.
+ * Fixed: organizer email-address for shared calendars is now prefixed with
+ mailto:, as it should.
+
+1.8.2-stable (2013-01-19)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Includes changes from version 1.7.4.
+
+1.8.1-stable (2012-12-01)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Includes changes from version 1.7.3.
+ * Fixed: Typo in 1.7 migration script caused it to fail.
+
+1.8.0-stable (2012-11-08)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * BC Break: Moved the entire codebase to PHP namespaces.
+ * BC Break: Every backend package (CalDAV, CardDAV, Auth, Locks,
+ Principals) now has consistent naming conventions. There's a
+ BackendInterface, and an AbstractBackend class.
+ * BC Break: Changed a bunch of constructor signatures in the CalDAV
+ package, to reduce dependencies on the ACL package.
+ * BC Break: Sabre_CalDAV_ISharedCalendar now also has a getShares method,
+ so sharees can figure out who is also on a shared calendar.
+
+ * Added: Sabre_DAVACL_IPrincipalCollection interface, to advertise support
+ for principal-property-search on any node.
+ * Added: Simple console script to fire up a fileserver in the current
+ directory using PHP 5.4's built-in webserver.
+ * Added: Sharee's can now also read out the list of invites for a shared
+ calendar.
+ * Added: The Proxy principal classes now both implement an interface, for
+ greater flexiblity.
+
+1.7.8-stable (2013-06-17)
+ * The zip release ships with sabre/vobject 2.1.0.
+ * Changed: OldSabre\DAV\Client::verifyPeer is now a protected property
+ (instead of private).
+ * Fixed: Text was incorrectly escaped in the Href and HrefList properties,
+ disallowing urls with ampersands (&) in them.
+ * Added: deserializer for OldSabre\DAVACL\Property\CurrentUserPrivilegeSet.
+ * Fixed: Issue 335: Client only deserializes properties with status 200.
+ * Fixed: Issue 341: Escaping xml in 423 Locked error responses.
+ * Added: Issue 339: beforeGetPropertiesForPath event.
+
+1.7.7-stable (2013-04-11)
+ * The zip release ships with sabre/vobject 2.0.7.
+ * Fixed: Assets in the browser plugins were not being served on windows
+ machines.
+
+1.7.6-stable (2013-04-08)
+ * The zip release ships with sabre/vobject 2.0.7.
+ * Fixed: vcardurl in database schema can now hold 255 characters instead
+ of 80 (which is often way to small).
+ * Fixed: The browser plugin potentially allowed people to open any
+ arbitrary file on windows servers (CVE-2013-1939).
+
+1.7.5-stable (2013-03-01)
+ * The zip release ships with sabre/vobject 2.0.6.
+ * Change: No longer advertising support for 4.0 vcards. iOS and OS X
+ address book don't handle this well, and just advertising 3.0 support
+ seems like the most logical course of action.
+ * Added: ->setVerifyPeers to Sabre_DAV_Client (greatly resisting against
+ it, don't use this..).
+
+1.7.4-stable (2013-01-19)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Changed: To be compatibile with MS Office 2011 for Mac, a workaround was
+ removed that was added to support old versions of Windows XP (pre-SP3).
+ Indeed! We needed a crazy workaround to work with one MS product in the
+ past, and we can't keep that workaround to be compatible with another MS
+ product.
+ * Fixed: expand-properties REPORT had incorrect values for the href
+ element.
+ * Fixed: Range requests now work for non-seekable streams. (Thanks Alfred
+ Klomp).
+ * Fixed: Changed serialization of {DAV:}getlastmodified and
+ {DAV:}supportedlock to improve compatiblity with MS Office 2011 for Mac.
+ * Changed: reverted the automatic translation of 'DAV:' xml namespaces to
+ 'urn:DAV' when parsing files. Issues were reported with libxml 2.6.32,
+ on a relatively recent debian release, so we'll wait till 2015 to take
+ this one out again.
+ * Added: Sabre_DAV_Exception_ServiceUnavailable, for emitting 503's.
+
+1.7.3-stable (2012-12-01)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Fixed: Removing double slashes from getPropertiesForPath.
+ * Change: Marked a few more properties in the CardDAV as protected,
+ instead of private.
+ * Fixed: SharingPlugin now plays nicer with other plugins with similar
+ functionality.
+ * Fixed: Issue 174. Sending back HTTP/1.0 for requests with this version.
+
+1.7.2-stable (2012-11-08)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Added: ACL plugin advertises support for 'calendarserver-principal-
+ property-search'.
+ * Fixed: [#153] Allowing for relative http principals in iMip requests.
+ * Added: Support for cs:first-name and cs:last-name properties in sharing
+ invites.
+ * Fixed: Made a bunch of properties protected, where they were private
+ before.
+ * Added: Some non-standard properties for sharing to improve
+ compatibility.
+ * Fixed: some bugfixes in postgres sql script.
+ * Fixed: When requesting some properties using PROPFIND, they could show
+ up as both '200 Ok' and '403 Forbidden'.
+ * Fixed: calendar-proxy principals were not checked for deeper principal
+ membership than 1 level.
+ * Fixed: setGroupMemberSet argument now correctly receives relative
+ principal urls, instead of the absolute ones.
+ * Fixed: Server class will filter out any bonus properties if any extra
+ were returned. This means the implementor of the IProperty class can be
+ a bit lazier when implementing.
+
+Note: bug numbers after this line refer to Google Code tickets. We're using
+github now.
+
+1.7.1-stable (2012-10-07)
+ * Fixed: include path problem in the migration script.
+
+1.7.0-stable (2012-10-06)
+ * BC Break: The calendarobjects database table has a bunch of new
+ fields, and a migration script is required to ensure everything will
+ keep working. Read the wiki for more details.
+ * BC Break: The ICalendar interface now has a new method: calendarQuery.
+ * BC Break: In this version a number of classes have been deleted, that
+ have been previously deprecated. Namely:
+ - Sabre_DAV_Directory (now: Sabre_DAV_Collection)
+ - Sabre_DAV_SimpleDirectory (now: Sabre_DAV_SimpleCollection)
+ * BC Break: Sabre_CalDAV_Schedule_IMip::sendMessage now has an extra
+ argument. If you extended this class, you should fix this method. It's
+ only used for informational purposes.
+ * BC Break: The DAV: namespace is no longer converted to urn:DAV. This was
+ a workaround for a bug in older PHP versions (pre-5.3).
+ * Removed: Sabre.includes.php was deprecated, and is now removed.
+ * Removed: Sabre_CalDAV_Server was deprecated, and is now removed. Please
+ use Sabre_DAV_Server and check the examples in the examples/ directory.
+ * Changed: The Sabre_VObject library now spawned into it's own project!
+ The VObject library is still included in the SabreDAV zip package.
+ * Added: Experimental interfaces to allow implementation of caldav-sharing.
+ Note that no implementation is provided yet, just the api hooks.
+ * Added: Free-busy reporting compliant with the caldav-scheduling
+ standard. This allows iCal and other clients to fetch other users'
+ free-busy data.
+ * Added: Experimental NotificationSupport interface to add
+ caldav notifications.
+ * Added: VCF Export plugin. If enabled, it can generate an export of an
+ entire addressbook.
+ * Added: Support for PATCH using a SabreDAV format, to live-patch files.
+ * Added: Support for Prefer: return-minimal and Brief: t headers for
+ PROPFIND and PROPPATCH requests.
+ * Changed: Responsibility for dealing with the calendar-query is now
+ moved from the CalDAV plugin to the CalDAV backends. This allows for
+ heavy optimizations.
+ * Changed: The CalDAV PDO backend is now a lot faster for common
+ calendar queries.
+ * Changed: We are now using the composer autoloader.
+ * Changed: The CalDAV backend now all implement an interface.
+ * Changed: Instead of Sabre_DAV_Property, Sabre_DAV_PropertyInterface is
+ now the basis of every property class.
+ * Update: Caching results for principal lookups. This should cut down
+ queries and performance for a number of heavy requests.
+ * Update: ObjectTree caches lookups much more aggresively, which will help
+ especially speeding up a bunch of REPORT queries.
+ * Added: Support for the schedule-calendar-transp property.
+ * Fixed: Marking both the text/calendar and text/x-vcard as UTF-8
+ encoded.
+ * Fixed: Workaround for the SOGO connector, as it doesn't understand
+ receiving "text/x-vcard; charset=utf-8" for a contenttype.
+ * Added: Sabre_DAV_Client now throws more specific exceptions in cases
+ where we already has an exception class.
+ * Added: Sabre_DAV_PartialUpdate. This plugin allows you to use the
+ PATCH method to update parts of a file.
+ * Added: Tons of timezone name mappings for Microsoft Exchange.
+ * Added: Support for an 'exception' event in the server class.
+ * Fixed: Uploaded VCards without a UID are now rejected. (thanks Dominik!)
+ * Fixed: Rejecting calendar objects if they are not in the
+ supported-calendar-component list. (thanks Armin!)
+ * Fixed: Issue 219: serialize() now reorders correctly.
+ * Fixed: Sabre_DAV_XMLUtil no longer returns empty $dom->childNodes
+ if there is whitespace in $dom.
+ * Fixed: Returning 409 Conflict instead of 500 when an attempt is made to
+ create a file as a child of something that's not a collection.
+ * Fixed: Issue 237: xml-encoding values in SabreDAV error responses.
+ * Fixed: Returning 403, instead of 501 when an unknown REPORT is
+ requested.
+ * Fixed: Postfixing slash on {DAV:}owner properties.
+ * Fixed: Several embarrassing spelling mistakes in docblocks.
+
+1.6.10-stable (2013-06-17)
+ * Fixed: Text was incorrectly escaped in the Href and HrefList properties,
+ disallowing urls with ampersands (&) in them.
+ * Fixed: Issue 341: Escaping xml in 423 Locked error responses.
+
+1.6.9-stable (2013-04-11)
+ * Fixed: Assets in the browser plugins were not being served on windows
+ machines.
+
+1.6.8-stable (2013-04-08)
+ * Fixed: vcardurl in database schema can now hold 255 characters instead
+ of 80 (which is often way to small).
+ * Fixed: The browser plugin potentially allowed people to open any
+ arbitrary file on windows servers. (CVE-2013-1939).
+
+1.6.7-stable (2013-03-01)
+ * Change: No longer advertising support for 4.0 vcards. iOS and OS X
+ address book don't handle this well, and just advertising 3.0 support
+ seems like the most logical course of action.
+ * Added: ->setVerifyPeers to Sabre_DAV_Client (greatly resisting against
+ it, don't use this..).
+
+1.6.6-stable (2013-01-19)
+ * Fixed: Backported a fix for broken XML serialization in error responses.
+ (Thanks @DeepDiver1975!)
+
+1.6.5-stable (2012-10-04)
+ * Fixed: Workaround for line-ending bug OS X 10.8 addressbook has.
+ * Added: Ability to allow users to set SSL certificates for the Client
+ class. (Thanks schiesbn!).
+ * Fixed: Directory indexes with lots of nodes should be a lot faster.
+ * Fixed: Issue 235: E_NOTICE thrown when doing a propfind request with
+ Sabre_DAV_Client, and no valid properties are returned.
+ * Fixed: Issue with filtering on alarms in tasks.
+
+1.6.4-stable (2012-08-02)
+ * Fixed: Issue 220: Calendar-query filters may fail when filtering on
+ alarms, if an overridden event has it's alarm removed.
+ * Fixed: Compatibility for OS/X 10.8 iCal in the IMipHandler.
+ * Fixed: Issue 222: beforeWriteContent shouldn't be called for lock
+ requests.
+ * Fixed: Problem with POST requests to the outbox if mailto: was not lower
+ cased.
+ * Fixed: Yearly recurrence rule expansion on leap-days no behaves
+ correctly.
+ * Fixed: Correctly checking if recurring, all-day events with no dtstart
+ fall in a timerange if the start of the time-range exceeds the start of
+ the instance of an event, but not the end.
+ * Fixed: All-day recurring events wouldn't match if an occurence ended
+ exactly on the start of a time-range.
+ * Fixed: HTTP basic auth did not correctly deal with passwords containing
+ colons on some servers.
+ * Fixed: Issue 228: DTEND is now non-inclusive for all-day events in the
+ calendar-query REPORT and free-busy calculations.
+
+1.6.3-stable (2012-06-12)
+ * Added: It's now possible to specify in Sabre_DAV_Client which type of
+ authentication is to be used.
+ * Fixed: Issue 206: Sabre_DAV_Client PUT requests are fixed.
+ * Fixed: Issue 205: Parsing an iCalendar 0-second date interval.
+ * Fixed: Issue 112: Stronger validation of iCalendar objects. Now making
+ sure every iCalendar object only contains 1 component, and disallowing
+ vcards, forcing every component to have a UID.
+ * Fixed: Basic validation for vcards in the CardDAV plugin.
+ * Fixed: Issue 213: Workaround for an Evolution bug, that prevented it
+ from updating events.
+ * Fixed: Issue 211: A time-limit query on a non-relative alarm trigger in
+ a recurring event could result in an endless loop.
+ * Fixed: All uri fields are now a maximum of 200 characters. The Bynari
+ outlook plugin used much longer strings so this should improve
+ compatibility.
+ * Fixed: Added a workaround for a bug in KDE 4.8.2 contact syncing. See
+ https://bugs.kde.org/show_bug.cgi?id=300047
+ * Fixed: Issue 217: Sabre_DAV_Tree_FileSystem was pretty broken.
+
+1.6.2-stable (2012-04-16)
+ * Fixed: Sabre_VObject_Node::$parent should have been public.
+ * Fixed: Recurrence rules of events are now taken into consideration when
+ doing time-range queries on alarms.
+ * Fixed: Added a workaround for the fact that php's DateInterval cannot
+ parse weeks and days at the same time.
+ * Added: Sabre_DAV_Server::$exposeVersion, allowing you to hide SabreDAV's
+ version number from various outputs.
+ * Fixed: DTSTART values would be incorrect when expanding events.
+ * Fixed: DTSTART and DTEND would be incorrect for expansion of WEEKLY
+ BYDAY recurrences.
+ * Fixed: Issue 203: A problem with overridden events hitting the exact
+ date and time of a subsequent event in the recurrence set.
+ * Fixed: There was a problem with recurrence rules, for example the 5th
+ tuesday of the month, if this day did not exist.
+ * Added: New HTTP status codes from draft-nottingham-http-new-status-04.
+
+1.6.1-stable (2012-03-05)
+ * Added: createFile and put() can now return an ETag.
+ * Added: Sending back an ETag on for operations on CardDAV backends. This
+ should help with OS X 10.6 Addressbook compatibility.
+ * Fixed: Fixed a bug where an infinite loop could occur in the recurrence
+ iterator if the recurrence was YEARLY, with a BYMONTH rule, and either
+ BYDAY or BYMONTHDAY match the first day of the month.
+ * Fixed: Events that are excluded using EXDATE are still counted in the
+ COUNT= parameter in the RRULE property.
+ * Added: Support for time-range filters on VALARM components.
+ * Fixed: Correctly filtering all-day events.
+ * Fixed: Sending back correct mimetypes from the browser plugin (thanks
+ Jürgen).
+ * Fixed: Issue 195: Sabre_CardDAV pear package had an incorrect dependency.
+ * Fixed: Calendardata would be destroyed when performing a MOVE request.
+
+1.6.0-stable (2012-02-22)
+ * BC Break: Now requires PHP 5.3
+ * BC Break: Any node that implemented Sabre_DAVACL_IACL must now also
+ implement the getSupportedPrivilegeSet method. See website for details.
+ * BC Break: Moved functions from Sabre_CalDAV_XMLUtil to
+ Sabre_VObject_DateTimeParser.
+ * BC Break: The Sabre_DAVACL_IPrincipalCollection now has two new methods:
+ 'searchPrincipals' and 'updatePrincipal'.
+ * BC Break: Sabre_DAV_ILockable is removed and all related per-node
+ locking functionality.
+ * BC Break: Sabre_DAV_Exception_FileNotFound is now deprecated in favor of
+ Sabre_DAV_Exception_NotFound. The former will be removed in a later
+ version.
+ * BC Break: Removed Sabre_CalDAV_ICalendarUtil, use Sabre_VObject instead.
+ * BC Break: Sabre_CalDAV_Server is now deprecated, check out the
+ documentation on how to setup a caldav server with just
+ Sabre_DAV_Server.
+ * BC Break: Default Principals PDO backend now needs a new field in the
+ 'principals' table. See the website for details.
+ * Added: Ability to create new calendars and addressbooks from within the
+ browser plugin.
+ * Added: Browser plugin: icons for various nodes.
+ * Added: Support for FREEBUSY reports!
+ * Added: Support for creating principals with admin-level privileges.
+ * Added: Possibility to let server send out invitation emails on behalf of
+ CalDAV client, using Sabre_CalDAV_Schedule_IMip.
+ * Changed: beforeCreateFile event now passes data argument by reference.
+ * Changed: The 'propertyMap' property from Sabre_VObject_Reader, must now
+ be specified in Sabre_VObject_Property::$classMap.
+ * Added: Ability for plugins to tell the ACL plugin which principal
+ plugins are searchable.
+ * Added: [DAVACL] Per-node overriding of supported privileges. This allows
+ for custom privileges where needed.
+ * Added: [DAVACL] Public 'principalSearch' method on the DAVACL plugin,
+ which allows for easy searching for principals, based on their
+ properties.
+ * Added: Sabre_VObject_Component::getComponents() to return a list of only
+ components and not properties.
+ * Added: An includes.php file in every sub-package (CalDAV, CardDAV, DAV,
+ DAVACL, HTTP, VObject) as an alternative to the autoloader. This often
+ works much faster.
+ * Added: Support for the 'Me card', which allows Addressbook.app users
+ specify which vcard is their own.
+ * Added: Support for updating principal properties in the DAVACL principal
+ backends.
+ * Changed: Major refactoring in the calendar-query REPORT code. Should
+ make things more flexible and correct.
+ * Changed: The calendar-proxy-[read|write] principals will now only appear
+ in the tree, if they actually exist in the Principal backend. This should
+ reduce some problems people have been having with this.
+ * Changed: Sabre_VObject_Element_* classes are now renamed to
+ Sabre_VObject_Property. Old classes are retained for backwards
+ compatibility, but this will be removed in the future.
+ * Added: Sabre_VObject_FreeBusyGenerator to generate free-busy reports
+ based on lists of events.
+ * Added: Sabre_VObject_RecurrenceIterator to find all the dates and times
+ for recurring events.
+ * Fixed: Issue 97: Correctly handling RRULE for the calendar-query REPORT.
+ * Fixed: Issue 154: Encoding of VObject parameters with no value was
+ incorrect.
+ * Added: Support for {DAV:}acl-restrictions property from RFC3744.
+ * Added: The contentlength for calendar objects can now be supplied by a
+ CalDAV backend, allowing for more optimizations.
+ * Fixed: Much faster implementation of Sabre_DAV_URLUtil::encodePath.
+ * Fixed: {DAV:}getcontentlength may now be not specified.
+ * Fixed: Issue 66: Using rawurldecode instead of urldecode to decode paths
+ from clients. This means that + will now be treated as a literal rather
+ than a space, and this should improve compatibility with the Windows
+ built-in client.
+ * Added: Sabre_DAV_Exception_PaymentRequired exception, to emit HTTP 402
+ status codes.
+ * Added: Some mysql unique constraints to example files.
+ * Fixed: Correctly formatting HTTP dates.
+ * Fixed: Issue 94: Sending back Last-Modified header for 304 responses.
+ * Added: Sabre_VObject_Component_VEvent, Sabre_VObject_Component_VJournal,
+ Sabre_VObject_Component_VTodo and Sabre_VObject_Component_VCalendar.
+ * Changed: Properties are now also automatically mapped to their
+ appropriate classes, if they are created using the add() or __set()
+ methods.
+ * Changed: Cloning VObject objects now clones the entire tree, rather than
+ just the default shallow copy.
+ * Added: Support for recurrence expansion in the CALDAV:calendar-multiget
+ and CALDAV:calendar-query REPORTS.
+ * Changed: CalDAV PDO backend now sorts calendars based on the internal
+ 'calendarorder' field.
+ * Added: Issue 181: Carddav backends may no optionally not supply the carddata in
+ getCards, if etag and size are specified. This may speed up certain
+ requests.
+ * Added: More arguments to beforeWriteContent and beforeCreateFile (see
+ WritingPlugins wiki document).
+ * Added: Hook for iCalendar validation. This allows us to validate
+ iCalendar objects when they're uploaded. At the moment we're just
+ validating syntax.
+ * Added: VObject now support Windows Timezone names correctly (thanks
+ mrpace2).
+ * Added: If a timezonename could not be detected, we fall back on the
+ default PHP timezone.
+ * Added: Now a Composer package (thanks willdurand).
+ * Fixed: Support for \N as a newline character in the VObject reader.
+ * Added: afterWriteContent, afterCreateFile and afterUnbind events.
+ * Added: Postgresql example files. Not part of the unittests though, so
+ use at your own risk.
+ * Fixed: Issue 182: Removed backticks from sql queries, so it will work
+ with Postgres.
+
+1.5.9-stable (2012-04-16)
+ * Fixed: Issue with parsing timezone identifiers that were surrounded by
+ quotes. (Fixes emClient compatibility).
+
+1.5.8-stable (2012-02-22)
+ * Fixed: Issue 95: Another timezone parsing issue, this time in
+ calendar-query.
+
+1.5.7-stable (2012-02-19)
+ * Fixed: VObject properties are now always encoded before components.
+ * Fixed: Sabre_DAVACL had issues with multiple levels of privilege
+ aggregration.
+ * Changed: Added 'GuessContentType' plugin to fileserver.php example.
+ * Fixed: The Browser plugin will now trigger the correct events when
+ creating files.
+ * Fixed: The ICSExportPlugin now considers ACL's.
+ * Added: Made it optional to supply carddata from an Addressbook backend
+ when requesting getCards. This can make some operations much faster, and
+ could result in much lower memory use.
+ * Fixed: Issue 187: Sabre_DAV_UUIDUtil was missing from includes file.
+ * Fixed: Issue 191: beforeUnlock was triggered twice.
+
+1.5.6-stable (2012-01-07)
+ * Fixed: Issue 174: VObject could break UTF-8 characters.
+ * Fixed: pear package installation issues.
+
+1.5.5-stable (2011-12-16)
+ * Fixed: CalDAV time-range filter workaround for recurring events.
+ * Fixed: Bug in Sabre_DAV_Locks_Backend_File that didn't allow multiple
+ files to be locked at the same time.
+
+1.5.4-stable (2011-10-28)
+ * Fixed: GuessContentType plugin now supports mixed case file extensions.
+ * Fixed: DATE-TIME encoding was wrong in VObject. (we used 'DATETIME').
+ * Changed: Sending back HTTP 204 after a PUT request on an existing resource
+ instead of HTTP 200. This should fix Evolution CardDAV client
+ compatibility.
+ * Fixed: Issue 95: Parsing X-LIC-LOCATION if it's available.
+ * Added: All VObject elements now have a reference to their parent node.
+
+1.5.3-stable (2011-09-28)
+ * Fixed: Sabre_DAV_Collection was missing from the includes file.
+ * Fixed: Issue 152. iOS 1.4.2 apparantly requires HTTP/1.1 200 OK to be in
+ uppercase.
+ * Fixed: Issue 153: Support for files with mixed newline styles in
+ Sabre_VObject.
+ * Fixed: Issue 159: Automatically converting any vcard and icalendardata
+ to UTF-8.
+ * Added: Sabre_DAV_SimpleFile class for easy static file creation.
+ * Added: Issue 158: Support for the CARDDAV:supported-address-data
+ property.
+
+1.5.2-stable (2011-09-21)
+ * Fixed: carddata and calendardata MySQL fields are now of type
+ 'mediumblob'. 'TEXT' was too small sometimes to hold all the data.
+ * Fixed: {DAV:}supported-report-set is now correctly reporting the reports
+ for IAddressBook.
+ * Added: Sabre_VObject_Property::add() to add duplicate parameters to
+ properties.
+ * Added: Issue 151: Sabre_CalDAV_ICalendar and Sabre_CalDAV_ICalendarObject
+ interfaces.
+ * Fixed: Issue 140: Not returning 201 Created if an event cancelled the
+ creation of a file.
+ * Fixed: Issue 150: Faster URLUtil::encodePath() implementation.
+ * Fixed: Issue 144: Browser plugin could interfere with
+ TemporaryFileFilterPlugin if it was loaded first.
+ * Added: It's not possible to specify more 'alternate uris' in principal
+ backends.
+
+1.5.1-stable (2011-08-24)
+ * Fixed: Issue 137. Hiding action interface in HTML browser for
+ non-collections.
+ * Fixed: addressbook-query is now correctly returned from the
+ {DAV:}supported-report-set property.
+ * Fixed: Issue 142: Bugs in groupwareserver.php example.
+ * Fixed: Issue 139: Rejecting PUT requests with Content-Range.
+
+1.5.0-stable (2011-08-12)
+ * Added: CardDAV support.
+ * Added: An experimental WebDAV client.
+ * Added: MIME-Directory grouping support in the VObject library. This is
+ very useful for people attempting to parse vcards.
+ * BC Break: Adding parameters with the VObject libraries now overwrites
+ the previous parameter, rather than just add it. This makes more sense
+ for 99% of the cases.
+ * BC Break: lib/Sabre.autoload.php is now removed in favor of
+ lib/Sabre/autoload.php.
+ * Deprecated: Sabre_DAV_Directory is now deprecated and will be removed in
+ a future version. Use Sabre_DAV_Collection instead.
+ * Deprecated: Sabre_DAV_SimpleDirectory is now deprecated and will be
+ removed in a future version. Use Sabre_DAV_SimpleCollection instead.
+ * Fixed: Problem with overriding tablenames for the CalDAV backend.
+ * Added: Clark-notation parser to XML utility.
+ * Added: unset() support to VObject components.
+ * Fixed: Refactored CalDAV property fetching to be faster and simpler.
+ * Added: Central string-matcher for CalDAV and CardDAV plugins.
+ * Added: i;unicode-casemap support
+ * Fixed: VObject bug: wouldn't parse parameters if they weren't specified
+ in uppercase.
+ * Fixed: VObject bug: Parameters now behave more like Properties.
+ * Fixed: VObject bug: Parameters with no value are now correctly parsed.
+ * Changed: If calendars don't specify which components they allow, 'all'
+ components are assumed (e.g.: VEVENT, VTODO, VJOURNAL).
+ * Changed: Browser plugin now uses POST variable 'sabreAction' instead of
+ 'action' to reduce the chance of collisions.
+
+1.4.4-stable (2011-07-07)
+ * Fixed: Issue 131: Custom CalDAV backends could break in certain cases.
+ * Added: The option to override the default tablename all PDO backends
+ use. (Issue 60).
+ * Fixed: Issue 124: 'File' authentication backend now takes realm into
+ consideration.
+ * Fixed: Sabre_DAV_Property_HrefList now properly deserializes. This
+ allows users to update the {DAV:}group-member-set property.
+ * Added: Helper functions for DateTime-values in Sabre_VObject package.
+ * Added: VObject library can now automatically map iCalendar properties to
+ custom classes.
+
+1.4.3-stable (2011-04-25)
+ * Fixed: Issue 123: Added workaround for Windows 7 UNLOCK bug.
+ * Fixed: datatype of lastmodified field in mysql.calendars.sql. Please
+ change the DATETIME field to an INT to ensure this field will work
+ correctly.
+ * Change: Sabre_DAV_Property_Principal is now renamed to
+ Sabre_DAVACL_Property_Principal.
+ * Added: API level support for ACL HTTP method.
+ * Fixed: Bug in serializing {DAV:}acl property.
+ * Added: deserializer for {DAV:}resourcetype property.
+ * Added: deserializer for {DAV:}acl property.
+ * Added: deserializer for {DAV:}principal property.
+
+1.4.2-beta (2011-04-01)
+ * Added: It's not possible to disable listing of nodes that are denied
+ read access by ACL.
+ * Fixed: Changed a few properties in CalDAV classes from private to
+ protected.
+ * Fixed: Issue 119: Terrible things could happen when relying on
+ guessBaseUri, the server was running on the root of the domain and a user
+ tried to access a file ending in .php. This is a slight BC break.
+ * Fixed: Issue 118: Lock tokens in If headers without a uri should be
+ treated as the request uri, not 'all relevant uri's.
+ * Fixed: Issue 120: PDO backend was incorrectly fetching too much locks in
+ cases where there were similar named locked files in a directory.
+
+1.4.1-beta (2011-02-26)
+ * Fixed: Sabre_DAV_Locks_Backend_PDO returned too many locks.
+ * Fixed: Sabre_HTTP_Request::getHeader didn't return Content-Type when
+ running on apache, so a few workarounds were added.
+ * Change: Slightly changed CalDAV Backend API's, to allow for heavy
+ optimizations. This is non-bc breaking.
+
+1.4.0-beta (2011-02-12)
+ * Added: Partly RFC3744 ACL support.
+ * Added: Calendar-delegation (caldav-proxy) support.
+ * BC break: In order to fix Issue 99, a new argument had to be added to
+ Sabre_DAV_Locks_Backend_*::getLocks classes. Consult the classes for
+ details.
+ * Deprecated: Sabre_DAV_Locks_Backend_FS is now deprecated and will be
+ removed in a later version. Use PDO or the new File class instead.
+ * Deprecated: The Sabre_CalDAV_ICalendarUtil class is now marked
+ deprecated, and will be removed in a future version. Please use
+ Sabre_VObject instead.
+ * Removed: All principal-related functionality has been removed from the
+ Sabre_DAV_Auth_Plugin, and moved to the Sabre_DAVACL_Plugin.
+ * Added: VObject library, for easy vcard/icalendar parsing using a natural
+ interface.
+ * Added: Ability to automatically generate full .ics feeds off calendars.
+ To use: Add the Sabre_CalDAV_ICSExportPlugin, and add ?export to your
+ calendar url.
+ * Added: Plugins can now specify a pluginname, for easy access using
+ Sabre_DAV_Server::getPlugin().
+ * Added: beforeGetProperties event.
+ * Added: updateProperties event.
+ * Added: Principal listings and calendar-access can now be done privately,
+ disallowing users from accessing or modifying other users' data.
+ * Added: You can now pass arrays to the Sabre_DAV_Server constructor. If
+ it's an array with node-objects, a Root collection will automatically be
+ created, and the nodes are used as top-level children.
+ * Added: The principal base uri is now customizable. It used to be
+ hardcoded to 'principals/[user]'.
+ * Added: getSupportedReportSet method in ServerPlugin class. This allows
+ you to easily specify which reports you're implementing.
+ * Added: A '..' link to the HTML browser.
+ * Fixed: Issue 99: Locks on child elements were ignored when their parent
+ nodes were deleted.
+ * Fixed: Issue 90: lockdiscovery property and LOCK response now include a
+ {DAV}lockroot element.
+ * Fixed: Issue 96: support for 'default' collation in CalDAV text-match
+ filters.
+ * Fixed: Issue 102: Ensuring that copy and move with identical source and
+ destination uri's fails.
+ * Fixed: Issue 105: Supporting MKCALENDAR with no body.
+ * Fixed: Issue 109: Small fixes in Sabre_HTTP_Util.
+ * Fixed: Issue 111: Properly catching the ownername in a lock (if it's a
+ string)
+ * Fixed: Sabre_DAV_ObjectTree::nodeExist always returned false for the
+ root node.
+ * Added: Global way to easily supply new resourcetypes for certain node
+ classes.
+ * Fixed: Issue 59: Allowing the user to override the authentication realm
+ in Sabre_CalDAV_Server.
+ * Update: Issue 97: Looser time-range checking if there's a recurrence
+ rule in an event. This fixes 'missing recurring events'.
+
+1.3.0 (2010-10-14)
+ * Added: childExists method to Sabre_DAV_ICollection. This is an api
+ break, so if you implement Sabre_DAV_ICollection directly, add the method.
+ * Changed: Almost all HTTP method implementations now take a uri argument,
+ including events. This allows for internal rerouting of certain calls.
+ If you have custom plugins, make sure they use this argument. If they
+ don't, they will likely still work, but it might get in the way of
+ future changes.
+ * Changed: All getETag methods MUST now surround the etag with
+ double-quotes. This was a mistake made in all previous SabreDAV
+ versions. If you don't do this, any If-Match, If-None-Match and If:
+ headers using Etags will work incorrectly. (Issue 85).
+ * Added: Sabre_DAV_Auth_Backend_AbstractBasic class, which can be used to
+ easily implement basic authentication.
+ * Removed: Sabre_DAV_PermissionDenied class. Use Sabre_DAV_Forbidden
+ instead.
+ * Removed: Sabre_DAV_IDirectory interface, use Sabre_DAV_ICollection
+ instead.
+ * Added: Browser plugin now uses {DAV:}displayname if this property is
+ available.
+ * Added: Cache layer in the ObjectTree.
+ * Added: Tree classes now have a delete and getChildren method.
+ * Fixed: If-Modified-Since and If-Unmodified-Since would be incorrect if
+ the date is an exact match.
+ * Fixed: Support for multiple ETags in If-Match and If-None-Match headers.
+ * Fixed: Improved baseUrl handling.
+ * Fixed: Issue 67: Non-seekable stream support in ::put()/::get().
+ * Fixed: Issue 65: Invalid dates are now ignored.
+ * Updated: Refactoring in Sabre_CalDAV to make everything a bit more
+ ledgable.
+ * Fixed: Issue 88, Issue 89: Fixed compatibility for running SabreDAV on
+ Windows.
+ * Fixed: Issue 86: Fixed Content-Range top-boundary from 'file size' to
+ 'file size'-1.
+
+1.2.4 (2010-07-13)
+ * Fixed: Issue 62: Guessing baseUrl fails when url contains a
+ query-string.
+ * Added: Apache configuration sample for CGI/FastCGI setups.
+ * Fixed: Issue 64: Only returning calendar-data when it was actually
+ requested.
+
+1.2.3 (2010-06-26)
+ * Fixed: Issue 57: Supporting quotes around etags in If-Match and
+ If-None-Match
+
+1.2.2 (2010-06-21)
+ * Updated: SabreDAV now attempts to guess the BaseURI if it's not set.
+ * Updated: Better compatibility with BitKinex
+ * Fixed: Issue 56: Incorrect behaviour for If-None-Match headers and GET
+ requests.
+ * Fixed: Issue with certain encoded paths in Browser Plugin.
+
+1.2.1 (2010-06-07)
+ * Fixed: Issue 50, patch by Mattijs Hoitink.
+ * Fixed: Issue 51, Adding windows 7 lockfiles to TemporaryFileFilter.
+ * Fixed: Issue 38, Allowing custom filters to be added to
+ TemporaryFileFilter.
+ * Fixed: Issue 53, ETags in the If: header were always failing. This
+ behaviour is now corrected.
+ * Added: Apache Authentication backend, in case authentication through
+ .htaccess is desired.
+ * Updated: Small improvements to example files.
+
+1.2.0 (2010-05-24)
+ * Fixed: Browser plugin now displays international characters.
+ * Changed: More properties in CalDAV classes are now protected instead of
+ private.
+
+1.2.0beta3 (2010-05-14)
+ * Fixed: Custom properties were not properly sent back for allprops
+ requests.
+ * Fixed: Issue 49, incorrect parsing of PROPPATCH, affecting Office 2007.
+ * Changed: Removed CalDAV items from includes.php, and added a few missing
+ ones.
+
+1.2.0beta2 (2010-05-04)
+ * Fixed: Issue 46: Fatal error for some non-existent nodes.
+ * Updated: some example sql to include email address.
+ * Added: 208 and 508 statuscodes from RFC5842.
+ * Added: Apache2 configuration examples
+
+1.2.0beta1 (2010-04-28)
+ * Fixed: redundant namespace declaration in resourcetypes.
+ * Fixed: 2 locking bugs triggered by litmus when no Sabre_DAV_ILockable
+ interface is used.
+ * Changed: using http://sabredav.org/ns for all custom xml properties.
+ * Added: email address property to principals.
+ * Updated: CalendarObject validation.
+
+1.2.0alpha4 (2010-04-24)
+ * Added: Support for If-Range, If-Match, If-None-Match, If-Modified-Since,
+ If-Unmodified-Since.
+ * Changed: Brand new build system. Functionality is split up between
+ Sabre, Sabre_HTTP, Sabre_DAV and Sabre_CalDAV packages. In addition to
+ that a new non-pear package will be created with all this functionality
+ combined.
+ * Changed: Autoloader moved to Sabre/autoload.php.
+ * Changed: The Allow: header is now more accurate, with appropriate HTTP
+ methods per uri.
+ * Changed: Now throwing back Sabre_DAV_Exception_MethodNotAllowed on a few
+ places where Sabre_DAV_Exception_NotImplemented was used.
+
+1.2.0alpha3 (2010-04-20)
+ * Update: Complete rewrite of property updating. Now easier to use and
+ atomic.
+ * Fixed: Issue 16, automatically adding trailing / to baseUri.
+ * Added: text/plain is used for .txt files in GuessContentType plugin.
+ * Added: support for principal-property-search and
+ principal-search-property-set reports.
+ * Added: Issue 31: Hiding exception information by default. Can be turned
+ on with the Sabre_DAV_Server::$debugExceptions property.
+
+1.2.0alpha2 (2010-04-08)
+ * Added: Calendars are now private and can only be read by the owner.
+ * Fixed: double namespace declaration in multistatus responses.
+ * Added: MySQL database dumps. MySQL is now also supported next to SQLite.
+ * Added: expand-properties REPORT from RFC 3253.
+ * Added: Sabre_DAV_Property_IHref interface for properties exposing urls.
+ * Added: Issue 25: Throwing error on broken Finder behaviour.
+ * Changed: Authentication backend is now aware of current user.
+
+1.2.0alpha1 (2010-03-31)
+ * Fixed: Issue 26: Workaround for broken GVFS behaviour with encoded
+ special characters.
+ * Fixed: Issue 34: Incorrect Lock-Token response header for LOCK. Fixes
+ Office 2010 compatibility.
+ * Added: Issue 35: SabreDAV version to header to OPTIONS response to ease
+ debugging.
+ * Fixed: Issue 36: Incorrect variable name, throwing error in some
+ requests.
+ * Fixed: Issue 37: Incorrect smultron regex in temporary filefilter.
+ * Fixed: Issue 33: Converting ISO-8859-1 characters to UTF-8.
+ * Fixed: Issue 39 & Issue 40: Basename fails on non-utf-8 locales.
+ * Added: More unittests.
+ * Added: SabreDAV version to all error responses.
+ * Added: URLUtil class for decoding urls.
+ * Changed: Now using pear.sabredav.org pear channel.
+ * Changed: Sabre_DAV_Server::getCopyAndMoveInfo is now a public method.
+
+1.1.2-alpha (2010-03-18)
+ * Added: RFC5397 - current-user-principal support.
+ * Fixed: Issue 27: encoding entities in property responses.
+ * Added: naturalselection script now allows the user to specify a 'minimum
+ number of bytes' for deletion. This should reduce load due to less
+ crawling
+ * Added: Full support for the calendar-query report.
+ * Added: More unittests.
+ * Added: Support for complex property deserialization through the static
+ ::unserialize() method.
+ * Added: Support for modifying calendar-component-set
+ * Fixed: Issue 29: Added TIMEOUT_INFINITE constant
+
+1.1.1-alpha (2010-03-11)
+ * Added: RFC5689 - Extended MKCOL support.
+ * Fixed: Evolution support for CalDAV.
+ * Fixed: PDO-locks backend was pretty much completely broken. This is
+ 100% unittested now.
+ * Added: support for ctags.
+ * Fixed: Comma's between HTTP methods in 'Allow' method.
+ * Changed: default argument for Sabre_DAV_Locks_Backend_FS. This means a
+ datadirectory must always be specified from now on.
+ * Changed: Moved Sabre_DAV_Server::parseProps to
+ Sabre_DAV_XMLUtil::parseProperties.
+ * Changed: Sabre_DAV_IDirectory is now Sabre_DAV_ICollection.
+ * Changed: Sabre_DAV_Exception_PermissionDenied is now
+ Sabre_DAV_Exception_Forbidden.
+ * Changed: Sabre_CalDAV_ICalendarCollection is removed.
+ * Added: Sabre_DAV_IExtendedCollection.
+ * Added: Many more unittests.
+ * Added: support for calendar-timezone property.
+
+1.1.0-alpha (2010-03-01)
+ * Note: This version is forked from version 1.0.5, so release dates may be
+ out of order.
+ * Added: CalDAV - RFC 4791
+ * Removed: Sabre_PHP_Exception. PHP has a built-in ErrorException for
+ this.
+ * Added: PDO authentication backend.
+ * Added: Example sql for auth, caldav, locks for sqlite.
+ * Added: Sabre_DAV_Browser_GuessContentType plugin
+ * Changed: Authentication plugin refactored, making it possible to
+ implement non-digest authentication.
+ * Fixed: Better error display in browser plugin.
+ * Added: Support for {DAV:}supported-report-set
+ * Added: XML utility class with helper functions for the WebDAV protocol.
+ * Added: Tons of unittests
+ * Added: PrincipalCollection and Principal classes
+ * Added: Sabre_DAV_Server::getProperties for easy property retrieval
+ * Changed: {DAV:}resourceType defaults to 0
+ * Changed: Any non-null resourceType now gets a / appended to the href
+ value. Before this was just for {DAV:}collection's, but this is now also
+ the case for for example {DAV:}principal.
+ * Changed: The Href property class can now optionally create non-relative
+ uri's.
+ * Changed: Sabre_HTTP_Response now returns false if headers are already
+ sent and header-methods are called.
+ * Fixed: Issue 19: HEAD requests on Collections
+ * Fixed: Issue 21: Typo in Sabre_DAV_Property_Response
+ * Fixed: Issue 18: Doesn't work with Evolution Contacts
+
+1.0.15-stable (2010-05-28)
+ * Added: Issue 31: Hiding exception information by default. Can be turned
+ on with the Sabre_DAV_Server::$debugExceptions property.
+ * Added: Moved autoload from lib/ to lib/Sabre/autoload.php. This is also
+ the case in the upcoming 1.2.0, so it will improve future compatibility.
+
+1.0.14-stable (2010-04-15)
+ * Fixed: double namespace declaration in multistatus responses.
+
+1.0.13-stable (2010-03-30)
+ * Fixed: Issue 40: Last references to basename/dirname
+
+1.0.12-stable (2010-03-30)
+ * Fixed: Issue 37: Incorrect smultron regex in temporary filefilter.
+ * Fixed: Issue 26: Workaround for broken GVFS behaviour with encoded
+ special characters.
+ * Fixed: Issue 33: Converting ISO-8859-1 characters to UTF-8.
+ * Fixed: Issue 39: Basename fails on non-utf-8 locales.
+ * Added: More unittests.
+ * Added: SabreDAV version to all error responses.
+ * Added: URLUtil class for decoding urls.
+ * Updated: Now using pear.sabredav.org pear channel.
+
+1.0.11-stable (2010-03-23)
+ * Non-public release. This release is identical to 1.0.10, but it is used
+ to test releasing packages to pear.sabredav.org.
+
+1.0.10-stable (2010-03-22)
+ * Fixed: Issue 34: Invalid Lock-Token header response.
+ * Added: Issue 35: Addign SabreDAV version to HTTP OPTIONS responses.
+
+1.0.9-stable (2010-03-19)
+ * Fixed: Issue 27: Entities not being encoded in PROPFIND responses.
+ * Fixed: Issue 29: Added missing TIMEOUT_INFINITE constant.
+
+1.0.8-stable (2010-03-03)
+ * Fixed: Issue 21: typos causing errors
+ * Fixed: Issue 23: Comma's between methods in Allow header.
+ * Added: Sabre_DAV_ICollection interface, to aid in future compatibility.
+ * Added: Sabre_DAV_Exception_Forbidden exception. This will replace
+ Sabre_DAV_Exception_PermissionDenied in the future, and can already be
+ used to ensure future compatibility.
+
+1.0.7-stable (2010-02-24)
+ * Fixed: Issue 19 regression for MS Office
+
+1.0.6-stable (2010-02-23)
+ * Fixed: Issue 19: HEAD requests on Collections
+
+1.0.5-stable (2010-01-22)
+ * Fixed: Fatal error when a malformed url was used for unlocking, in
+ conjuction with Sabre.autoload.php due to a incorrect filename.
+ * Fixed: Improved unittests and build system
+
+1.0.4-stable (2010-01-11)
+ * Fixed: needed 2 different releases. One for googlecode and one for
+ pearfarm. This is to retain the old method to install SabreDAV until
+ pearfarm becomes the standard installation method.
+
+1.0.3-stable (2010-01-11)
+ * Added: RFC4709 support (davmount)
+ * Added: 6 unittests
+ * Added: naturalselection. A tool to keep cache directories below a
+ specified theshold.
+ * Changed: Now using pearfarm.org channel server.
+
+1.0.1-stable (2009-12-22)
+ * Fixed: Issue 15: typos in examples
+ * Fixed: Minor pear installation issues
+
+1.0.0-stable (2009-11-02)
+ * Added: SimpleDirectory class. This class allows creating static
+ directory structures with ease.
+ * Changed: Custom complex properties and exceptions now get an instance of
+ Sabre_DAV_Server as their first argument in serialize()
+ * Changed: Href complex property now prepends server's baseUri
+ * Changed: delete before an overwriting copy/move is now handles by server
+ class instead of tree classes
+ * Changed: events must now explicitly return false to stop execution.
+ Before, execution would be stopped by anything loosely evaluating to
+ false.
+ * Changed: the getPropertiesForPath method now takes a different set of
+ arguments, and returns a different response. This allows plugin
+ developers to return statuses for properties other than 200 and 404. The
+ hrefs are now also always calculated relative to the baseUri, and not
+ the uri of the request.
+ * Changed: generatePropFindResponse is renamed to generateMultiStatus, and
+ now takes a list of properties similar to the response of
+ getPropertiesForPath. This was also needed to improve flexibility for
+ plugin development.
+ * Changed: Auth plugins are no longer included. They were not yet stable
+ quality, so they will probably be reintroduced in a later version.
+ * Changed: PROPPATCH also used generateMultiStatus now.
+ * Removed: unknownProperties event. This is replaced by the
+ afterGetProperties event, which should provide more flexibility.
+ * Fixed: Only calling getSize() on IFile instances in httpHead()
+ * Added: beforeBind event. This is invoked upon file or directory creation
+ * Added: beforeWriteContent event, this is invoked by PUT and LOCK on an
+ existing resource.
+ * Added: beforeUnbind event. This is invoked right before deletion of any
+ resource.
+ * Added: afterGetProperties event. This event can be used to make
+ modifications to property responses.
+ * Added: beforeLock and beforeUnlock events.
+ * Added: afterBind event.
+ * Fixed: Copy and Move could fail in the root directory. This is now
+ fixed.
+ * Added: Plugins can now be retrieved by their classname. This is useful
+ for inter-plugin communication.
+ * Added: The Auth backend can now return usernames and user-id's.
+ * Added: The Auth backend got a getUsers method
+ * Added: Sabre_DAV_FSExt_Directory now returns quota info
+
+0.12.1-beta (2009-09-11)
+ * Fixed: UNLOCK bug. Unlock didn't work at all
+
+0.12-beta (2009-09-10)
+ * Updated: Browser plugin now shows multiple {DAV:}resourcetype values
+ if available.
+ * Added: Experimental PDO backend for Locks Manager
+ * Fixed: Sending Content-Length: 0 for every empty response. This
+ improves NGinx compatibility.
+ * Fixed: Last modification time is reported in UTC timezone. This improves
+ Finder compatibility.
+
+0.11-beta (2009-08-11)
+ * Updated: Now in Beta
+ * Updated: Pear package no longer includes docs/ directory. These just
+ contained rfc's, which are publically available. This reduces the
+ package from ~800k to ~60k
+ * Added: generatePropfindResponse now takes a baseUri argument
+ * Added: ResourceType property can now contain multiple resourcetypes.
+ * Fixed: Issue 13.
+
+0.10-alpha (2009-08-03)
+ * Added: Plugin to automatically map GET requests to non-files to
+ PROPFIND (Sabre_DAV_Browser_MapGetToPropFind). This should allow
+ easier debugging of complicated WebDAV setups.
+ * Added: Sabre_DAV_Property_Href class. For future use.
+ * Added: Ability to choose to use auth-int, auth or both for HTTP Digest
+ authentication. (Issue 11)
+ * Changed: Made more methods in Sabre_DAV_Server public.
+ * Fixed: TemporaryFileFilter plugin now intercepts HTTP LOCK requests
+ to non-existent files. (Issue 12)
+ * Added: Central list of defined xml namespace prefixes. This can reduce
+ Bandwidth and legibility for xml bodies with user-defined namespaces.
+ * Added: now a PEAR-compatible package again, thanks to Michael Gauthier
+ * Changed: moved default copy and move logic from ObjectTree to Tree class
+
+0.9-alpha (2009-07-21)
+ * Changed: Major refactoring, removed most of the logic from the Tree
+ objects. The Server class now directly works with the INode, IFile
+ and IDirectory objects. If you created your own Tree objects,
+ this will most likely break in this release.
+ * Changed: Moved all the Locking logic from the Tree and Server classes
+ into a separate plugin.
+ * Changed: TemporaryFileFilter is now a plugin.
+ * Added: Comes with an autoloader script. This can be used instead of
+ the includer script, and is preferred by some people.
+ * Added: AWS Authentication class.
+ * Added: simpleserversetup.py script. This will quickly get a fileserver
+ up and running.
+ * Added: When subscribing to events, it is now possible to supply a
+ priority. This is for example needed to ensure that the Authentication
+ Plugin is used before any other Plugin.
+ * Added: 22 new tests.
+ * Added: Users-manager plugin for .htdigest files. Experimental and
+ subject to change.
+ * Added: RFC 2324 HTTP 418 status code
+ * Fixed: Exclusive locks could in some cases be picked up as shared locks
+ * Fixed: Digest auth for non-apache servers had a bug (still not actually
+ tested this well).
+
+0.8-alpha (2009-05-30)
+ * Changed: Renamed all exceptions! This is a compatibility break. Every
+ Exception now follows Sabre_DAV_Exception_FileNotFound convention
+ instead of Sabre_DAV_FileNotFoundException.
+ * Added: Browser plugin now allows uploading and creating directories
+ straight from the browser.
+ * Added: 12 more unittests
+ * Fixed: Locking bug, which became prevalent on Windows Vista.
+ * Fixed: Netdrive support
+ * Fixed: TemporaryFileFilter filtered out too many files. Fixed some
+ of the regexes.
+ * Fixed: Added README and ChangeLog to package
+
+0.7-alpha (2009-03-29)
+ * Added: System to return complex properties from PROPFIND.
+ * Added: support for {DAV:}supportedlock.
+ * Added: support for {DAV:}lockdiscovery.
+ * Added: 6 new tests.
+ * Added: New plugin system.
+ * Added: Simple HTML directory plugin, for browser access.
+ * Added: Server class now sends back standard pre-condition error xml
+ bodies. This was new since RFC4918.
+ * Added: Sabre_DAV_Tree_Aggregrate, which can 'host' multiple Tree objects
+ into one.
+ * Added: simple basis for HTTP REPORT method. This method is not used yet,
+ but can be used by plugins to add reports.
+ * Changed: ->getSize is only called for files, no longer for collections.
+ r303
+ * Changed: Sabre_DAV_FilterTree is now Sabre_DAV_Tree_Filter
+ * Changed: Sabre_DAV_TemporaryFileFilter is now called
+ Sabre_DAV_Tree_TemporaryFileFilter.
+ * Changed: removed functions (get(/set)HTTPRequest(/Response)) from Server
+ class, and using a public property instead.
+ * Fixed: bug related to parsing proppatch and propfind requests. Didn't
+ show up in most clients, but it needed fixing regardless. (r255)
+ * Fixed: auth-int is now properly supported within HTTP Digest.
+ * Fixed: Using application/xml for a mimetype vs. text/xml as per RFC4918
+ sec 8.2.
+ * Fixed: TemporaryFileFilter now lets through GET's if they actually
+ exist on the backend. (r274)
+ * FIxed: Some methods didn't get passed through in the FilterTree (r283).
+ * Fixed: LockManager is now slightly more complex, Tree classes slightly
+ less. (r287)
+
+0.6-alpha (2009-02-16)
+ * Added: Now uses streams for files, instead of strings.
+ This means it won't require to hold entire files in memory, which can be
+ an issue if you're dealing with big files. Note that this breaks
+ compatibility for put() and createFile methods.
+ * Added: HTTP Digest Authentication helper class.
+ * Added: Support for HTTP Range header
+ * Added: Support for ETags within If: headers
+ * Added: The API can now return ETags and override the default Content-Type
+ * Added: starting with basic framework for unittesting, using PHPUnit.
+ * Added: 49 unittests.
+ * Added: Abstraction for the HTTP request.
+ * Updated: Using Clark Notation for tags in properties. This means tags
+ are serialized as {namespace}tagName instead of namespace#tagName
+ * Fixed: HTTP_BasicAuth class now works as expected.
+ * Fixed: DAV_Server uses / for a default baseUrl.
+ * Fixed: Last modification date is no longer ignored in PROPFIND.
+ * Fixed: PROPFIND now sends back information about the requestUri even
+ when "Depth: 1" is specified.
+
+0.5-alpha (2009-01-14)
+ * Added: Added a very simple example for implementing a mapping to PHP
+ file streams. This should allow easy implementation of for example a
+ WebDAV to FTP proxy.
+ * Added: HTTP Basic Authentication helper class.
+ * Added: Sabre_HTTP_Response class. This centralizes HTTP operations and
+ will be a start towards the creating of a testing framework.
+ * Updated: Backwards compatibility break: all require_once() statements
+ are removed
+ from all the files. It is now recommended to use autoloading of
+ classes, or just including lib/Sabre.includes.php. This fix was made
+ to allow easier integration into applications not using this standard
+ inclusion model.
+ * Updated: Better in-file documentation.
+ * Updated: Sabre_DAV_Tree can now work with Sabre_DAV_LockManager.
+ * Updated: Fixes a shared-lock bug.
+ * Updated: Removed ?> from the bottom of each php file.
+ * Updated: Split up some operations from Sabre_DAV_Server to
+ Sabre_HTTP_Response.
+ * Fixed: examples are now actually included in the pear package.
+
+0.4-alpha (2008-11-05)
+ * Passes all litmus tests!
+ * Added: more examples
+ * Added: Custom property support
+ * Added: Shared lock support
+ * Added: Depth support to locks
+ * Added: Locking on unmapped urls (non-existent nodes)
+ * Fixed: Advertising as WebDAV class 3 support
+
+0.3-alpha (2008-06-29)
+ * Fully working in MS Windows clients.
+ * Added: temporary file filter: support for smultron files.
+ * Added: Phing build scripts
+ * Added: PEAR package
+ * Fixed: MOVE bug identified using finder.
+ * Fixed: Using gzuncompress instead of gzdecode in the temporary file
+ filter. This seems more common.
+
+0.2-alpha (2008-05-27)
+ * Somewhat working in Windows clients
+ * Added: Working PROPPATCH method (doesn't support custom properties yet)
+ * Added: Temporary filename handling system
+ * Added: Sabre_DAV_IQuota to return quota information
+ * Added: PROPFIND now reads the request body and only supplies the
+ requested properties
+
+0.1-alpha (2008-04-04)
+ * First release!
+ * Passes litmus: basic, http and copymove test.
+ * Fully working in Finder and DavFSv2
+
+Project started: 2007-12-13
diff --git a/calendar/lib/SabreDAV/LICENSE b/calendar/lib/SabreDAV/LICENSE
new file mode 100644
index 0000000..68aceab
--- /dev/null
+++ b/calendar/lib/SabreDAV/LICENSE
@@ -0,0 +1,27 @@
+Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of SabreDAV nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
diff --git a/calendar/lib/SabreDAV/README.md b/calendar/lib/SabreDAV/README.md
new file mode 100644
index 0000000..8346c9a
--- /dev/null
+++ b/calendar/lib/SabreDAV/README.md
@@ -0,0 +1,30 @@
+# What is SabreDAV
+
+SabreDAV allows you to easily add WebDAV support to a PHP application. SabreDAV is meant to cover the entire standard, and attempts to allow integration using an easy to understand API.
+
+### Feature list:
+
+* Fully WebDAV compliant
+* Supports Windows XP, Windows Vista, Mac OS/X, DavFSv2, Cadaver, Netdrive, Open Office, and probably more.
+* Passing all Litmus tests.
+* Supporting class 1, 2 and 3 Webdav servers.
+* Locking support.
+* Custom property support.
+* CalDAV (tested with [Evolution](http://code.google.com/p/sabredav/wiki/Evolution), [iCal](http://code.google.com/p/sabredav/wiki/ICal), [iPhone](http://code.google.com/p/sabredav/wiki/IPhone) and [Lightning](http://code.google.com/p/sabredav/wiki/Lightning)).
+* CardDAV (tested with [OS/X addressbook](http://code.google.com/p/sabredav/wiki/OSXAddressbook), the [iOS addressbook](http://code.google.com/p/sabredav/wiki/iOSCardDAV) and [Evolution](http://code.google.com/p/sabredav/wiki/Evolution)).
+* Over 97% unittest code coverage.
+
+### Supported RFC's:
+
+* [RFC2617](http://www.ietf.org/rfc/rfc2617.txt): Basic/Digest auth.
+* [RFC2518](http://www.ietf.org/rfc/rfc2518.txt): First WebDAV spec.
+* [RFC3744](http://www.ietf.org/rfc/rfc3744.txt): ACL (some features missing).
+* [RFC4709](http://www.ietf.org/rfc/rfc4709.txt): [DavMount](http://code.google.com/p/sabredav/wiki/DavMount).
+* [RFC4791](http://www.ietf.org/rfc/rfc4791.txt): CalDAV.
+* [RFC4918](http://www.ietf.org/rfc/rfc4918.txt): WebDAV revision.
+* [RFC5397](http://www.ietf.org/rfc/rfc5689.txt): current-user-principal.
+* [RFC5689](http://www.ietf.org/rfc/rfc5689.txt): Extended MKCOL.
+* [RFC5789](http://tools.ietf.org/html/rfc5789): PATCH method for HTTP.
+* [RFC6352](http://www.ietf.org/rfc/rfc6352.txt): CardDAV
+* [draft-daboo-carddav-directory-gateway](http://tools.ietf.org/html/draft-daboo-carddav-directory-gateway): CardDAV directory gateway
+* CalDAV ctag, CalDAV-proxy.
diff --git a/calendar/lib/SabreDAV/composer.json b/calendar/lib/SabreDAV/composer.json
new file mode 100644
index 0000000..f202785
--- /dev/null
+++ b/calendar/lib/SabreDAV/composer.json
@@ -0,0 +1,61 @@
+{
+ "name": "sabre/dav",
+ "type": "library",
+ "description": "WebDAV Framework for PHP",
+ "keywords": ["Framework", "WebDAV", "CalDAV", "CardDAV", "iCalendar"],
+ "homepage": "http://code.google.com/p/sabredav/",
+ "license" : "BSD-3-Clause",
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "evert@rooftopsolutions.nl",
+ "homepage" : "http://evertpot.com/",
+ "role" : "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.1",
+ "sabre/vobject" : "~2.1.0",
+ "ext-dom": "*",
+ "ext-pcre": "*",
+ "ext-spl": "*",
+ "ext-simplexml": "*",
+ "ext-mbstring" : "*",
+ "ext-ctype" : "*",
+ "ext-date" : "*",
+ "ext-iconv" : "*",
+ "ext-libxml" : "*"
+ },
+ "require-dev" : {
+ "phpunit/phpunit" : "3.7.*",
+ "evert/phpdoc-md" : "~0.0.7",
+ "phing/phing" : "2.4.14"
+ },
+ "provide" : {
+ "evert/sabredav" : "1.7.*"
+ },
+ "suggest" : {
+ "ext-apc" : "*",
+ "ext-curl" : "*",
+ "ext-pdo" : "*"
+ },
+ "autoload": {
+ "psr-0" : {
+ "OldSabre\\DAV" : "lib/",
+ "OldSabre\\HTTP" : "lib/",
+ "OldSabre\\DAVACL" : "lib/",
+ "OldSabre\\CalDAV" : "lib/",
+ "OldSabre\\CardDAV" : "lib/"
+ }
+ },
+ "support" : {
+ "forum" : "https://groups.google.com/group/sabredav-discuss",
+ "source" : "https://github.com/evert/sabredav"
+ },
+ "bin" : [
+ "bin/sabredav"
+ ],
+ "config" : {
+ "bin-dir" : "./bin"
+ }
+}
diff --git a/calendar/lib/SabreDAV/composer.lock b/calendar/lib/SabreDAV/composer.lock
new file mode 100644
index 0000000..bea8d74
--- /dev/null
+++ b/calendar/lib/SabreDAV/composer.lock
@@ -0,0 +1,80 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
+ ],
+ "hash": "59cc5f06d39080610504a29c9398f95e",
+ "packages": [
+ {
+ "name": "sabre/vobject",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fruux/sabre-vobject.git",
+ "reference": "2.1.0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fruux/sabre-vobject/zipball/2.1.0",
+ "reference": "2.1.0",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.1"
+ },
+ "bin": [
+ "bin/vobjectvalidate.php"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "OldSabre\\VObject": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "evert@rooftopsolutions.nl",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "homepage": "https://github.com/fruux/sabre-vobject",
+ "keywords": [
+ "VObject",
+ "iCalendar",
+ "vCard"
+ ],
+ "time": "2013-06-17 22:25:42"
+ }
+ ],
+ "packages-dev": null,
+ "aliases": [
+
+ ],
+ "minimum-stability": "stable",
+ "stability-flags": [
+
+ ],
+ "platform": {
+ "php": ">=5.3.1",
+ "ext-dom": "*",
+ "ext-pcre": "*",
+ "ext-spl": "*",
+ "ext-simplexml": "*",
+ "ext-mbstring": "*",
+ "ext-ctype": "*",
+ "ext-date": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*"
+ },
+ "platform-dev": [
+
+ ]
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php
new file mode 100644
index 0000000..2586441
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+use OldSabre\VObject;
+use OldSabre\CalDAV;
+
+/**
+ * Abstract Calendaring backend. Extend this class to create your own backends.
+ *
+ * Checkout the BackendInterface for all the methods that must be implemented.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The mutations array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => 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) {
+
+ return false;
+
+ }
+
+ /**
+ * 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 \OldSabre\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 \OldSabre\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) {
+
+ $result = array();
+ $objects = $this->getCalendarObjects($calendarId);
+
+ $validator = new \OldSabre\CalDAV\CalendarQueryValidator();
+
+ foreach($objects as $object) {
+
+ if ($this->validateFilterForObject($object, $filters)) {
+ $result[] = $object['uri'];
+ }
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * This method validates if a filters (as passed to calendarQuery) matches
+ * the given object.
+ *
+ * @param array $object
+ * @param array $filters
+ * @return bool
+ */
+ protected function validateFilterForObject(array $object, array $filters) {
+
+ // Unfortunately, setting the 'calendardata' here is optional. If
+ // it was excluded, we actually need another call to get this as
+ // well.
+ if (!isset($object['calendardata'])) {
+ $object = $this->getCalendarObject($object['calendarid'], $object['uri']);
+ }
+
+ $data = is_resource($object['calendardata'])?stream_get_contents($object['calendardata']):$object['calendardata'];
+ $vObject = VObject\Reader::read($data);
+
+ $validator = new CalDAV\CalendarQueryValidator();
+ return $validator->validate($vObject, $filters);
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php
new file mode 100644
index 0000000..81f7778
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php
@@ -0,0 +1,233 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+/**
+ * Every CalDAV backend must at least implement this interface.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Returns a list of calendars for a principal.
+ *
+ * Every project is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * calendar. This can be the same as the uri or a database key.
+ * * uri, which the basename of the uri with which the calendar is
+ * accessed.
+ * * principaluri. The owner of the calendar. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore it can contain webdav properties in clark notation. A very
+ * common one is '{DAV:}displayname'.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getCalendarsForUser($principalUri);
+
+ /**
+ * Creates a new calendar for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used to reference
+ * this calendar in other methods, such as updateCalendar.
+ *
+ * @param string $principalUri
+ * @param string $calendarUri
+ * @param array $properties
+ * @return void
+ */
+ public function createCalendar($principalUri,$calendarUri,array $properties);
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The mutations array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => 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.
+ *
+ * This method must return null if the object did not exist.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @return array|null
+ */
+ public function getCalendarObject($calendarId,$objectUri);
+
+ /**
+ * 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 OldSabre\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 OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php
new file mode 100644
index 0000000..a99170e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+/**
+ * Adds caldav notification support to a backend.
+ *
+ * Note: This feature is experimental, and may change in between different
+ * SabreDAV versions.
+ *
+ * Notifications are defined at:
+ * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-notifications.txt
+ *
+ * These notifications are basically a list of server-generated notifications
+ * displayed to the user. Users can dismiss notifications by deleting them.
+ *
+ * The primary usecase is to allow for calendar-sharing.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface NotificationSupport extends BackendInterface {
+
+ /**
+ * Returns a list of notifications for a given principal url.
+ *
+ * The returned array should only consist of implementations of
+ * \OldSabre\CalDAV\Notifications\INotificationType.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getNotificationsForPrincipal($principalUri);
+
+ /**
+ * This deletes a specific notifcation.
+ *
+ * This may be called by a client once it deems a notification handled.
+ *
+ * @param string $principalUri
+ * @param \OldSabre\CalDAV\Notifications\INotificationType $notification
+ * @return void
+ */
+ public function deleteNotification($principalUri, \OldSabre\CalDAV\Notifications\INotificationType $notification);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php
new file mode 100644
index 0000000..104deea
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php
@@ -0,0 +1,691 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+use OldSabre\VObject;
+use OldSabre\CalDAV;
+use OldSabre\DAV;
+
+/**
+ * PDO CalDAV backend
+ *
+ * This backend is used to store calendar-data in a PDO database, such as
+ * sqlite or MySQL
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend {
+
+ /**
+ * We need to specify a max date, because we need to stop *somewhere*
+ *
+ * On 32 bit system the maximum for a signed integer is 2147483647, so
+ * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
+ * in 2038-01-19 to avoid problems when the date is converted
+ * to a unix timestamp.
+ */
+ const MAX_DATE = '2038-01-01';
+
+ /**
+ * pdo
+ *
+ * @var \PDO
+ */
+ protected $pdo;
+
+ /**
+ * The table name that will be used for calendars
+ *
+ * @var string
+ */
+ protected $calendarTableName;
+
+ /**
+ * The table name that will be used for calendar objects
+ *
+ * @var string
+ */
+ protected $calendarObjectTableName;
+
+ /**
+ * List of CalDAV properties, and how they map to database fieldnames
+ * Add your own properties by simply adding on to this array.
+ *
+ * Note that only string-based properties are supported here.
+ *
+ * @var array
+ */
+ public $propertyMap = array(
+ '{DAV:}displayname' => 'displayname',
+ '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
+ '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
+ '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
+ );
+
+ /**
+ * Creates the backend
+ *
+ * @param \PDO $pdo
+ * @param string $calendarTableName
+ * @param string $calendarObjectTableName
+ */
+ public function __construct(\PDO $pdo, $calendarTableName = 'calendars', $calendarObjectTableName = 'calendarobjects') {
+
+ $this->pdo = $pdo;
+ $this->calendarTableName = $calendarTableName;
+ $this->calendarObjectTableName = $calendarObjectTableName;
+
+ }
+
+ /**
+ * Returns a list of calendars for a principal.
+ *
+ * Every project is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * calendar. This can be the same as the uri or a database key.
+ * * uri, which the basename of the uri with which the calendar is
+ * accessed.
+ * * principaluri. The owner of the calendar. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore it can contain webdav properties in clark notation. A very
+ * common one is '{DAV:}displayname'.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getCalendarsForUser($principalUri) {
+
+ $fields = array_values($this->propertyMap);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'ctag';
+ $fields[] = 'components';
+ $fields[] = 'principaluri';
+ $fields[] = 'transparent';
+
+ // Making fields a comma-delimited list
+ $fields = implode(', ', $fields);
+ $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM ".$this->calendarTableName." WHERE principaluri = ? ORDER BY calendarorder ASC");
+ $stmt->execute(array($principalUri));
+
+ $calendars = array();
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $components = array();
+ if ($row['components']) {
+ $components = explode(',',$row['components']);
+ }
+
+ $calendar = array(
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $row['ctag']?$row['ctag']:'0',
+ '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Property\SupportedCalendarComponentSet($components),
+ '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new CalDAV\Property\ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
+ );
+
+
+ foreach($this->propertyMap as $xmlName=>$dbName) {
+ $calendar[$xmlName] = $row[$dbName];
+ }
+
+ $calendars[] = $calendar;
+
+ }
+
+ return $calendars;
+
+ }
+
+ /**
+ * Creates a new calendar for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used to reference
+ * this calendar in other methods, such as updateCalendar
+ *
+ * @param string $principalUri
+ * @param string $calendarUri
+ * @param array $properties
+ * @return string
+ */
+ public function createCalendar($principalUri, $calendarUri, array $properties) {
+
+ $fieldNames = array(
+ 'principaluri',
+ 'uri',
+ 'ctag',
+ 'transparent',
+ );
+ $values = array(
+ ':principaluri' => $principalUri,
+ ':uri' => $calendarUri,
+ ':ctag' => 1,
+ ':transparent' => 0,
+ );
+
+ // Default value
+ $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+ $fieldNames[] = 'components';
+ if (!isset($properties[$sccs])) {
+ $values[':components'] = 'VEVENT,VTODO';
+ } else {
+ if (!($properties[$sccs] instanceof CalDAV\Property\SupportedCalendarComponentSet)) {
+ throw new DAV\Exception('The ' . $sccs . ' property must be of type: \OldSabre\CalDAV\Property\SupportedCalendarComponentSet');
+ }
+ $values[':components'] = implode(',',$properties[$sccs]->getValue());
+ }
+ $transp = '{' . 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])) {
+
+ $values[':' . $dbName] = $properties[$xmlName];
+ $fieldNames[] = $dbName;
+ }
+ }
+
+ $stmt = $this->pdo->prepare("INSERT INTO ".$this->calendarTableName." (".implode(', ', $fieldNames).") VALUES (".implode(', ',array_keys($values)).")");
+ $stmt->execute($values);
+
+ return $this->pdo->lastInsertId();
+
+ }
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The mutations array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => 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 string $calendarId
+ * @param array $mutations
+ * @return bool|array
+ */
+ public function updateCalendar($calendarId, array $mutations) {
+
+ $newValues = array();
+ $result = array(
+ 200 => array(), // Ok
+ 403 => array(), // Forbidden
+ 424 => array(), // Failed Dependency
+ );
+
+ $hasError = false;
+
+ foreach($mutations as $propertyName=>$propertyValue) {
+
+ switch($propertyName) {
+ case '{' . 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;
+ }
+
+ }
+
+ // If there were any errors we need to fail the request
+ if ($hasError) {
+ // Properties has the remaining properties
+ foreach($mutations as $propertyName=>$propertyValue) {
+ $result[424][$propertyName] = null;
+ }
+
+ // Removing unused statuscodes for cleanliness
+ foreach($result as $status=>$properties) {
+ if (is_array($properties) && count($properties)===0) unset($result[$status]);
+ }
+
+ return $result;
+
+ }
+
+ // Success
+
+ // Now we're generating the sql query.
+ $valuesSql = array();
+ foreach($newValues as $fieldName=>$value) {
+ $valuesSql[] = $fieldName . ' = ?';
+ }
+ $valuesSql[] = 'ctag = ctag + 1';
+
+ $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ',$valuesSql) . " WHERE id = ?");
+ $newValues['id'] = $calendarId;
+ $stmt->execute(array_values($newValues));
+
+ return true;
+
+ }
+
+ /**
+ * Delete a calendar and all it's objects
+ *
+ * @param string $calendarId
+ * @return void
+ */
+ public function deleteCalendar($calendarId) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
+ $stmt->execute(array($calendarId));
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarTableName.' WHERE id = ?');
+ $stmt->execute(array($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 string $calendarId
+ * @return array
+ */
+ public function getCalendarObjects($calendarId) {
+
+ $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
+ $stmt->execute(array($calendarId));
+
+ $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;
+
+ }
+
+ /**
+ * 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.
+ *
+ * This method must return null if the object did not exist.
+ *
+ * @param string $calendarId
+ * @param string $objectUri
+ * @return array|null
+ */
+ public function getCalendarObject($calendarId,$objectUri) {
+
+ $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
+ $stmt->execute(array($calendarId, $objectUri));
+ $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.
+ *
+ * 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) {
+
+ $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.
+ *
+ * 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) {
+
+ $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 \OldSabre\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->getValue()));
+ $lastOccurence = $endDate->getTimeStamp();
+ } elseif (!$component->DTSTART->hasTime()) {
+ $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,
+ );
+
+ }
+
+ /**
+ * Deletes an existing calendar object.
+ *
+ * @param string $calendarId
+ * @param string $objectUri
+ * @return void
+ */
+ public function deleteCalendarObject($calendarId,$objectUri) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
+ $stmt->execute(array($calendarId,$objectUri));
+ $stmt = $this->pdo->prepare('UPDATE '. $this->calendarTableName .' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt->execute(array($calendarId));
+
+ }
+
+ /**
+ * 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 \OldSabre\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 \OldSabre\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 \OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php
new file mode 100644
index 0000000..626eade
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+/**
+ * Adds support for sharing features to a CalDAV server.
+ *
+ * Note: This feature is experimental, and may change in between different
+ * SabreDAV versions.
+ *
+ * Early warning: Currently SabreDAV provides no implementation for this. This
+ * is, because in it's current state there is no elegant way to do this.
+ * The problem lies in the fact that a real CalDAV server with sharing support
+ * would first need email support (with invite notifications), and really also
+ * a browser-frontend that allows people to accept or reject these shares.
+ *
+ * In addition, the CalDAV backends are currently kept as independent as
+ * possible, and should not be aware of principals, email addresses or
+ * accounts.
+ *
+ * Adding an implementation for Sharing to standard-sabredav would contradict
+ * these goals, so for this reason this is currently not implemented, although
+ * it may very well in the future; but probably not before SabreDAV 2.0.
+ *
+ * The interface works however, so if you implement all this, and do it
+ * correctly sharing _will_ work. It's not particularly easy, and I _urge you_
+ * to make yourself acquainted with the following document first:
+ *
+ * https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
+ *
+ * An overview
+ * ===========
+ *
+ * Implementing this interface will allow a user to share his or her calendars
+ * to other users. Effectively, when a calendar is shared the calendar will
+ * show up in both the Sharer's and Sharee's calendar-home root.
+ * This interface adds a few methods that ensure that this happens, and there
+ * are also a number of new requirements in the base-class you must now follow.
+ *
+ *
+ * How it works
+ * ============
+ *
+ * When a user shares a calendar, the updateShares() method will be called with
+ * a list of sharees that are now added, and a list of sharees that have been
+ * removed.
+ * Removal is instant, but when a sharee is added the sharee first gets a
+ * chance to accept or reject the invitation for a share.
+ *
+ * After a share is accepted, the calendar will be returned from
+ * getUserCalendars for both the sharer, and the sharee.
+ *
+ * If the sharee deletes the calendar, only their share gets deleted. When the
+ * owner deletes a calendar, it will be removed for everybody.
+ *
+ *
+ * Notifications
+ * =============
+ *
+ * During all these sharing operations, a lot of notifications are sent back
+ * and forward.
+ *
+ * Whenever the list of sharees for a calendar has been changed (they have been
+ * added, removed or modified) all sharees should get a notification for this
+ * change.
+ * This notification is always represented by:
+ *
+ * OldSabre\CalDAV\Notifications\Notification\Invite
+ *
+ * In the case of an invite, the sharee may reply with an 'accept' or
+ * 'decline'. These are always represented by:
+ *
+ * OldSabre\CalDAV\Notifications\Notification\Invite
+ *
+ *
+ * Calendar access by sharees
+ * ==========================
+ *
+ * As mentioned earlier, shared calendars must now also be returned for
+ * getCalendarsForUser for sharees. A few things change though.
+ *
+ * The following properties must be specified:
+ *
+ * 1. {http://calendarserver.org/ns/}shared-url
+ *
+ * This property MUST contain the url to the original calendar, that is.. the
+ * path to the calendar from the owner.
+ *
+ * 2. {http://sabredav.org/ns}owner-principal
+ *
+ * This is a url to to the principal who is sharing the calendar.
+ *
+ * 3. {http://sabredav.org/ns}read-only
+ *
+ * This should be either 0 or 1, depending on if the user has read-only or
+ * read-write access to the calendar.
+ *
+ * Only when this is done, the calendar will correctly be marked as a calendar
+ * that's shared to him, thus allowing clients to display the correct interface
+ * and ACL enforcement.
+ *
+ * If a sharee deletes their calendar, only their instance of the calendar
+ * should be deleted, the original should still exists.
+ * Pretty much any 'dead' WebDAV properties on these shared calendars should be
+ * specific to a user. This means that if the displayname is changed by a
+ * sharee, the original is not affected. This is also true for:
+ * * The description
+ * * The color
+ * * The order
+ * * And any other dead properties.
+ *
+ * Properties like a ctag should not be different for multiple instances of the
+ * calendar.
+ *
+ * Lastly, objects *within* calendars should also have user-specific data. The
+ * two things that are user-specific are:
+ * * VALARM objects
+ * * The TRANSP property
+ *
+ * This _also_ implies that if a VALARM is deleted by a sharee for some event,
+ * this has no effect on the original VALARM.
+ *
+ * Understandably, the this last requirement is one of the hardest.
+ * Realisticly, I can see people ignoring this part of the spec, but that could
+ * cause a different set of issues.
+ *
+ *
+ * Publishing
+ * ==========
+ *
+ * When a user publishes a url, the server should generate a 'publish url'.
+ * This is a read-only url, anybody can use to consume the calendar feed.
+ *
+ * Calendars are in one of two states:
+ * * published
+ * * unpublished
+ *
+ * If a calendar is published, the following property should be returned
+ * for each calendar in getCalendarsForPrincipal.
+ *
+ * {http://calendarserver.org/ns/}publish-url
+ *
+ * This element should contain a {DAV:}href element, which points to the
+ * public url that does not require authentication. Unlike every other href,
+ * this url must be absolute.
+ *
+ * Ideally, the following property is always returned
+ *
+ * {http://calendarserver.org/ns/}pre-publish-url
+ *
+ * This property should contain the url that the calendar _would_ have, if it
+ * were to be published. iCal uses this to display the url, before the user
+ * will actually publish it.
+ *
+ *
+ * Selectively disabling publish or share feature
+ * ==============================================
+ *
+ * If OldSabre\CalDAV\Property\AllowedSharingModes is returned from
+ * getCalendarsByUser, this allows the server to specify whether either sharing,
+ * or publishing is supported.
+ *
+ * This allows a client to determine in advance which features are available,
+ * and update the interface appropriately. If this property is not returned by
+ * the backend, the SharingPlugin automatically injects it and assumes both
+ * features are available.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface SharingSupport extends NotificationSupport {
+
+ /**
+ * Updates the list of shares.
+ *
+ * The first array is a list of people that are to be added to the
+ * calendar.
+ *
+ * Every element in the add array has the following properties:
+ * * href - A url. Usually a mailto: address
+ * * commonName - Usually a first and last name, or false
+ * * summary - A description of the share, can also be false
+ * * readOnly - A boolean value
+ *
+ * Every element in the remove array is just the address string.
+ *
+ * Note that if the calendar is currently marked as 'not shared' by and
+ * this method is called, the calendar should be 'upgraded' to a shared
+ * calendar.
+ *
+ * @param mixed $calendarId
+ * @param array $add
+ * @param array $remove
+ * @return void
+ */
+ function updateShares($calendarId, array $add, array $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 OldSabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * This method may be called by either the original instance of the
+ * calendar, as well as the shared instances. In the case of the shared
+ * instances, it is perfectly acceptable to return an empty array in case
+ * there are privacy concerns.
+ *
+ * @param mixed $calendarId
+ * @return array
+ */
+ function getShares($calendarId);
+
+ /**
+ * This method is called when a user replied to a request to share.
+ *
+ * If the user chose to accept the share, this method should return the
+ * newly created calendar url.
+ *
+ * @param string href The sharee who is replying (often a mailto: address)
+ * @param int status One of the SharingPlugin::STATUS_* constants
+ * @param string $calendarUri The url to the calendar thats being shared
+ * @param string $inReplyTo The unique id this message is a response to
+ * @param string $summary A description of the reply
+ * @return null|string
+ */
+ function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null);
+
+ /**
+ * Publishes a calendar
+ *
+ * @param mixed $calendarId
+ * @param bool $value
+ * @return void
+ */
+ function setPublishStatus($calendarId, $value);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php
new file mode 100644
index 0000000..bfd4990
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php
@@ -0,0 +1,376 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * This object represents a CalDAV calendar.
+ *
+ * A calendar can contain multiple TODO and or Events. These are represented
+ * as \OldSabre\CalDAV\CalendarObject objects.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Calendar implements ICalendar, DAV\IProperties, DAVACL\IACL {
+
+ /**
+ * This is an array with calendar information
+ *
+ * @var array
+ */
+ protected $calendarInfo;
+
+ /**
+ * CalDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param array $calendarInfo
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
+
+ $this->caldavBackend = $caldavBackend;
+ $this->calendarInfo = $calendarInfo;
+
+ }
+
+ /**
+ * Returns the name of the calendar
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->calendarInfo['uri'];
+
+ }
+
+ /**
+ * Updates properties such as the display name and description
+ *
+ * @param array $mutations
+ * @return array
+ */
+ public function updateProperties($mutations) {
+
+ return $this->caldavBackend->updateCalendar($this->calendarInfo['id'],$mutations);
+
+ }
+
+ /**
+ * Returns the list of properties
+ *
+ * @param array $requestedProperties
+ * @return array
+ */
+ public function getProperties($requestedProperties) {
+
+ $response = array();
+
+ foreach($requestedProperties as $prop) switch($prop) {
+
+ case '{urn:ietf:params:xml:ns:caldav}supported-calendar-data' :
+ $response[$prop] = new Property\SupportedCalendarData();
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}supported-collation-set' :
+ $response[$prop] = new Property\SupportedCollationSet();
+ break;
+ case '{DAV:}owner' :
+ $response[$prop] = new DAVACL\Property\Principal(DAVACL\Property\Principal::HREF,$this->calendarInfo['principaluri']);
+ break;
+ default :
+ if (isset($this->calendarInfo[$prop])) $response[$prop] = $this->calendarInfo[$prop];
+ break;
+
+ }
+ return $response;
+
+ }
+
+ /**
+ * Returns a calendar object
+ *
+ * The contained calendar objects are for example Events or Todo's.
+ *
+ * @param string $name
+ * @return \OldSabre\CalDAV\ICalendarObject
+ */
+ public function getChild($name) {
+
+ $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name);
+
+ if (!$obj) throw new DAV\Exception\NotFound('Calendar object not found');
+
+ $obj['acl'] = $this->getACL();
+ // Removing the irrelivant
+ foreach($obj['acl'] as $key=>$acl) {
+ if ($acl['privilege'] === '{' . Plugin::NS_CALDAV . '}read-free-busy') {
+ unset($obj['acl'][$key]);
+ }
+ }
+
+ return new CalendarObject($this->caldavBackend,$this->calendarInfo,$obj);
+
+ }
+
+ /**
+ * Returns the full list of calendar objects
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
+ $children = array();
+ foreach($objs as $obj) {
+ $obj['acl'] = $this->getACL();
+ // Removing the irrelivant
+ foreach($obj['acl'] as $key=>$acl) {
+ if ($acl['privilege'] === '{' . Plugin::NS_CALDAV . '}read-free-busy') {
+ unset($obj['acl'][$key]);
+ }
+ }
+ $children[] = new CalendarObject($this->caldavBackend,$this->calendarInfo,$obj);
+ }
+ return $children;
+
+ }
+
+ /**
+ * Checks if a child-node exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name);
+ if (!$obj)
+ return false;
+ else
+ return true;
+
+ }
+
+ /**
+ * Creates a new directory
+ *
+ * We actually block this, as subdirectories are not allowed in calendars.
+ *
+ * @param string $name
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed');
+
+ }
+
+ /**
+ * Creates a new file
+ *
+ * The contents of the new file must be a valid ICalendar string.
+ *
+ * @param string $name
+ * @param resource $calendarData
+ * @return string|null
+ */
+ public function createFile($name,$calendarData = null) {
+
+ if (is_resource($calendarData)) {
+ $calendarData = stream_get_contents($calendarData);
+ }
+ return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'],$name,$calendarData);
+
+ }
+
+ /**
+ * Deletes the calendar.
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->caldavBackend->deleteCalendar($this->calendarInfo['id']);
+
+ }
+
+ /**
+ * Renames the calendar. Note that most calendars use the
+ * {DAV:}displayname to display a name to display a name.
+ *
+ * @param string $newName
+ * @return void
+ */
+ public function setName($newName) {
+
+ throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
+
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp.
+ *
+ * @return void
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->calendarInfo['principaluri'];
+
+ }
+
+ /**
+ * Returns a 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(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner() . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner() . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner() . '/calendar-proxy-read',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See \OldSabre\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() {
+
+ $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+
+ // We need to inject 'read-free-busy' in the tree, aggregated under
+ // {DAV:}read.
+ foreach($default['aggregates'] as &$agg) {
+
+ if ($agg['privilege'] !== '{DAV:}read') continue;
+
+ $agg['aggregates'][] = array(
+ 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
+ );
+
+ }
+ return $default;
+
+ }
+
+ /**
+ * 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 OldSabre\CalDAV\CalendarQueryParser.
+ *
+ * @param array $filters
+ * @return array
+ */
+ public function calendarQuery(array $filters) {
+
+ return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php
new file mode 100644
index 0000000..a60da8f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php
@@ -0,0 +1,279 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * The CalendarObject represents a single VEVENT or VTODO within a Calendar.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarObject extends \OldSabre\DAV\File implements ICalendarObject, \OldSabre\DAVACL\IACL {
+
+ /**
+ * OldSabre\CalDAV\Backend\BackendInterface
+ *
+ * @var OldSabre\CalDAV\Backend\AbstractBackend
+ */
+ protected $caldavBackend;
+
+ /**
+ * Array with information about this CalendarObject
+ *
+ * @var array
+ */
+ protected $objectData;
+
+ /**
+ * Array with information about the containing calendar
+ *
+ * @var array
+ */
+ protected $calendarInfo;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param array $calendarInfo
+ * @param array $objectData
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend,array $calendarInfo,array $objectData) {
+
+ $this->caldavBackend = $caldavBackend;
+
+ if (!isset($objectData['calendarid'])) {
+ throw new \InvalidArgumentException('The objectData argument must contain a \'calendarid\' property');
+ }
+ if (!isset($objectData['uri'])) {
+ throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
+ }
+
+ $this->calendarInfo = $calendarInfo;
+ $this->objectData = $objectData;
+
+ }
+
+ /**
+ * Returns the uri for this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->objectData['uri'];
+
+ }
+
+ /**
+ * Returns the ICalendar-formatted object
+ *
+ * @return string
+ */
+ public function get() {
+
+ // Pre-populating the 'calendardata' is optional, if we don't have it
+ // already we fetch it from the backend.
+ if (!isset($this->objectData['calendardata'])) {
+ $this->objectData = $this->caldavBackend->getCalendarObject($this->objectData['calendarid'], $this->objectData['uri']);
+ }
+ return $this->objectData['calendardata'];
+
+ }
+
+ /**
+ * Updates the ICalendar-formatted object
+ *
+ * @param string|resource $calendarData
+ * @return string
+ */
+ public function put($calendarData) {
+
+ if (is_resource($calendarData)) {
+ $calendarData = stream_get_contents($calendarData);
+ }
+ $etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'],$this->objectData['uri'],$calendarData);
+ $this->objectData['calendardata'] = $calendarData;
+ $this->objectData['etag'] = $etag;
+
+ return $etag;
+
+ }
+
+ /**
+ * Deletes the calendar object
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'],$this->objectData['uri']);
+
+ }
+
+ /**
+ * Returns the mime content-type
+ *
+ * @return string
+ */
+ public function getContentType() {
+
+ return 'text/calendar; charset=utf-8';
+
+ }
+
+ /**
+ * Returns an ETag for this object.
+ *
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * @return string
+ */
+ public function getETag() {
+
+ if (isset($this->objectData['etag'])) {
+ return $this->objectData['etag'];
+ } else {
+ return '"' . md5($this->get()). '"';
+ }
+
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return $this->objectData['lastmodified'];
+
+ }
+
+ /**
+ * Returns the size of this object in bytes
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ if (array_key_exists('size',$this->objectData)) {
+ return $this->objectData['size'];
+ } else {
+ return strlen($this->get());
+ }
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->calendarInfo['principaluri'];
+
+ }
+
+ /**
+ * Returns a 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() {
+
+ // 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',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read',
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new \OldSabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See \OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php
new file mode 100644
index 0000000..a2a2b75
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php
@@ -0,0 +1,298 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\VObject;
+
+/**
+ * Parses the calendar-query report request body.
+ *
+ * Whoever designed this format, and the CalDAV equivalent even more so,
+ * has no feel for design.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarQueryParser {
+
+ /**
+ * List of requested properties the client wanted
+ *
+ * @var array
+ */
+ public $requestedProperties;
+
+ /**
+ * List of property/component filters.
+ *
+ * @var array
+ */
+ public $filters;
+
+ /**
+ * This property will contain null if CALDAV:expand was not specified,
+ * otherwise it will contain an array with 2 elements (start, end). Each
+ * contain a DateTime object.
+ *
+ * If expand is specified, recurring calendar objects are to be expanded
+ * into their individual components, and only the components that fall
+ * within the specified time-range are to be returned.
+ *
+ * For more details, see rfc4791, section 9.6.5.
+ *
+ * @var null|array
+ */
+ public $expand;
+
+ /**
+ * DOM Document
+ *
+ * @var DOMDocument
+ */
+ protected $dom;
+
+ /**
+ * DOM XPath object
+ *
+ * @var DOMXPath
+ */
+ protected $xpath;
+
+ /**
+ * Creates the parser
+ *
+ * @param \DOMDocument $dom
+ */
+ public function __construct(\DOMDocument $dom) {
+
+ $this->dom = $dom;
+ $this->xpath = new \DOMXPath($dom);
+ $this->xpath->registerNameSpace('cal',Plugin::NS_CALDAV);
+ $this->xpath->registerNameSpace('dav','urn:DAV');
+
+ }
+
+ /**
+ * Parses the request.
+ *
+ * @return void
+ */
+ public function parse() {
+
+ $filterNode = null;
+
+ $filter = $this->xpath->query('/cal:calendar-query/cal:filter');
+ if ($filter->length !== 1) {
+ throw new \OldSabre\DAV\Exception\BadRequest('Only one filter element is allowed');
+ }
+
+ $compFilters = $this->parseCompFilters($filter->item(0));
+ if (count($compFilters)!==1) {
+ throw new \OldSabre\DAV\Exception\BadRequest('There must be exactly 1 top-level comp-filter.');
+ }
+
+ $this->filters = $compFilters[0];
+ $this->requestedProperties = array_keys(\OldSabre\DAV\XMLUtil::parseProperties($this->dom->firstChild));
+
+ $expand = $this->xpath->query('/cal:calendar-query/dav:prop/cal:calendar-data/cal:expand');
+ if ($expand->length>0) {
+ $this->expand = $this->parseExpand($expand->item(0));
+ }
+
+
+ }
+
+ /**
+ * Parses all the 'comp-filter' elements from a node
+ *
+ * @param \DOMElement $parentNode
+ * @return array
+ */
+ protected function parseCompFilters(\DOMElement $parentNode) {
+
+ $compFilterNodes = $this->xpath->query('cal:comp-filter', $parentNode);
+ $result = array();
+
+ for($ii=0; $ii < $compFilterNodes->length; $ii++) {
+
+ $compFilterNode = $compFilterNodes->item($ii);
+
+ $compFilter = array();
+ $compFilter['name'] = $compFilterNode->getAttribute('name');
+ $compFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $compFilterNode)->length>0;
+ $compFilter['comp-filters'] = $this->parseCompFilters($compFilterNode);
+ $compFilter['prop-filters'] = $this->parsePropFilters($compFilterNode);
+ $compFilter['time-range'] = $this->parseTimeRange($compFilterNode);
+
+ if ($compFilter['time-range'] && !in_array($compFilter['name'],array(
+ 'VEVENT',
+ 'VTODO',
+ 'VJOURNAL',
+ 'VFREEBUSY',
+ 'VALARM',
+ ))) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The time-range filter is not defined for the ' . $compFilter['name'] . ' component');
+ };
+
+ $result[] = $compFilter;
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Parses all the prop-filter elements from a node
+ *
+ * @param \DOMElement $parentNode
+ * @return array
+ */
+ protected function parsePropFilters(\DOMElement $parentNode) {
+
+ $propFilterNodes = $this->xpath->query('cal:prop-filter', $parentNode);
+ $result = array();
+
+ for ($ii=0; $ii < $propFilterNodes->length; $ii++) {
+
+ $propFilterNode = $propFilterNodes->item($ii);
+ $propFilter = array();
+ $propFilter['name'] = $propFilterNode->getAttribute('name');
+ $propFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $propFilterNode)->length>0;
+ $propFilter['param-filters'] = $this->parseParamFilters($propFilterNode);
+ $propFilter['text-match'] = $this->parseTextMatch($propFilterNode);
+ $propFilter['time-range'] = $this->parseTimeRange($propFilterNode);
+
+ $result[] = $propFilter;
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Parses the param-filter element
+ *
+ * @param \DOMElement $parentNode
+ * @return array
+ */
+ protected function parseParamFilters(\DOMElement $parentNode) {
+
+ $paramFilterNodes = $this->xpath->query('cal:param-filter', $parentNode);
+ $result = array();
+
+ for($ii=0;$ii<$paramFilterNodes->length;$ii++) {
+
+ $paramFilterNode = $paramFilterNodes->item($ii);
+ $paramFilter = array();
+ $paramFilter['name'] = $paramFilterNode->getAttribute('name');
+ $paramFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $paramFilterNode)->length>0;
+ $paramFilter['text-match'] = $this->parseTextMatch($paramFilterNode);
+
+ $result[] = $paramFilter;
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Parses the text-match element
+ *
+ * @param \DOMElement $parentNode
+ * @return array|null
+ */
+ protected function parseTextMatch(\DOMElement $parentNode) {
+
+ $textMatchNodes = $this->xpath->query('cal:text-match', $parentNode);
+
+ if ($textMatchNodes->length === 0)
+ return null;
+
+ $textMatchNode = $textMatchNodes->item(0);
+ $negateCondition = $textMatchNode->getAttribute('negate-condition');
+ $negateCondition = $negateCondition==='yes';
+ $collation = $textMatchNode->getAttribute('collation');
+ if (!$collation) $collation = 'i;ascii-casemap';
+
+ return array(
+ 'negate-condition' => $negateCondition,
+ 'collation' => $collation,
+ 'value' => $textMatchNode->nodeValue
+ );
+
+ }
+
+ /**
+ * Parses the time-range element
+ *
+ * @param \DOMElement $parentNode
+ * @return array|null
+ */
+ protected function parseTimeRange(\DOMElement $parentNode) {
+
+ $timeRangeNodes = $this->xpath->query('cal:time-range', $parentNode);
+ if ($timeRangeNodes->length === 0) {
+ return null;
+ }
+
+ $timeRangeNode = $timeRangeNodes->item(0);
+
+ if ($start = $timeRangeNode->getAttribute('start')) {
+ $start = VObject\DateTimeParser::parseDateTime($start);
+ } else {
+ $start = null;
+ }
+ if ($end = $timeRangeNode->getAttribute('end')) {
+ $end = VObject\DateTimeParser::parseDateTime($end);
+ } else {
+ $end = null;
+ }
+
+ if (!is_null($start) && !is_null($end) && $end <= $start) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The end-date must be larger than the start-date in the time-range filter');
+ }
+
+ return array(
+ 'start' => $start,
+ 'end' => $end,
+ );
+
+ }
+
+ /**
+ * Parses the CALDAV:expand element
+ *
+ * @param \DOMElement $parentNode
+ * @return void
+ */
+ protected function parseExpand(\DOMElement $parentNode) {
+
+ $start = $parentNode->getAttribute('start');
+ if(!$start) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The "start" attribute is required for the CALDAV:expand element');
+ }
+ $start = VObject\DateTimeParser::parseDateTime($start);
+
+ $end = $parentNode->getAttribute('end');
+ if(!$end) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The "end" attribute is required for the CALDAV:expand element');
+ }
+
+ $end = VObject\DateTimeParser::parseDateTime($end);
+
+ if ($end <= $start) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
+ }
+
+ return array(
+ 'start' => $start,
+ 'end' => $end,
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php
new file mode 100644
index 0000000..581a126
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php
@@ -0,0 +1,392 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\VObject;
+use DateTime;
+
+/**
+ * CalendarQuery Validator
+ *
+ * This class is responsible for checking if an iCalendar object matches a set
+ * of filters. The main function to do this is 'validate'.
+ *
+ * This is used to determine which icalendar objects should be returned for a
+ * calendar-query REPORT request.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarQueryValidator {
+
+ /**
+ * Verify if a list of filters applies to the calendar data object
+ *
+ * The list of filters must be formatted as parsed by \OldSabre\CalDAV\CalendarQueryParser
+ *
+ * @param VObject\Component $vObject
+ * @param array $filters
+ * @return bool
+ */
+ public function validate(VObject\Component $vObject,array $filters) {
+
+ // The top level object is always a component filter.
+ // We'll parse it manually, as it's pretty simple.
+ if ($vObject->name !== $filters['name']) {
+ return false;
+ }
+
+ return
+ $this->validateCompFilters($vObject, $filters['comp-filters']) &&
+ $this->validatePropFilters($vObject, $filters['prop-filters']);
+
+
+ }
+
+ /**
+ * This method checks the validity of comp-filters.
+ *
+ * A list of comp-filters needs to be specified. Also the parent of the
+ * component we're checking should be specified, not the component to check
+ * itself.
+ *
+ * @param VObject\Component $parent
+ * @param array $filters
+ * @return bool
+ */
+ protected function validateCompFilters(VObject\Component $parent, array $filters) {
+
+ foreach($filters as $filter) {
+
+ $isDefined = isset($parent->$filter['name']);
+
+ if ($filter['is-not-defined']) {
+
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if ($filter['time-range']) {
+ foreach($parent->$filter['name'] as $subComponent) {
+ if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
+ continue 2;
+ }
+ }
+ return false;
+ }
+
+ if (!$filter['comp-filters'] && !$filter['prop-filters']) {
+ continue;
+ }
+
+ // If there are sub-filters, we need to find at least one component
+ // for which the subfilters hold true.
+ foreach($parent->$filter['name'] as $subComponent) {
+
+ if (
+ $this->validateCompFilters($subComponent, $filter['comp-filters']) &&
+ $this->validatePropFilters($subComponent, $filter['prop-filters'])) {
+ // We had a match, so this comp-filter succeeds
+ continue 2;
+ }
+
+ }
+
+ // If we got here it means there were sub-comp-filters or
+ // sub-prop-filters and there was no match. This means this filter
+ // needs to return false.
+ return false;
+
+ }
+
+ // If we got here it means we got through all comp-filters alive so the
+ // filters were all true.
+ return true;
+
+ }
+
+ /**
+ * This method checks the validity of prop-filters.
+ *
+ * A list of prop-filters needs to be specified. Also the parent of the
+ * property we're checking should be specified, not the property to check
+ * itself.
+ *
+ * @param VObject\Component $parent
+ * @param array $filters
+ * @return bool
+ */
+ protected function validatePropFilters(VObject\Component $parent, array $filters) {
+
+ foreach($filters as $filter) {
+
+ $isDefined = isset($parent->$filter['name']);
+
+ if ($filter['is-not-defined']) {
+
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if ($filter['time-range']) {
+ foreach($parent->$filter['name'] as $subComponent) {
+ if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
+ continue 2;
+ }
+ }
+ return false;
+ }
+
+ if (!$filter['param-filters'] && !$filter['text-match']) {
+ continue;
+ }
+
+ // If there are sub-filters, we need to find at least one property
+ // for which the subfilters hold true.
+ foreach($parent->$filter['name'] as $subComponent) {
+
+ if(
+ $this->validateParamFilters($subComponent, $filter['param-filters']) &&
+ (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match']))
+ ) {
+ // We had a match, so this prop-filter succeeds
+ continue 2;
+ }
+
+ }
+
+ // If we got here it means there were sub-param-filters or
+ // text-match filters and there was no match. This means the
+ // filter needs to return false.
+ return false;
+
+ }
+
+ // If we got here it means we got through all prop-filters alive so the
+ // filters were all true.
+ return true;
+
+ }
+
+ /**
+ * This method checks the validity of param-filters.
+ *
+ * A list of param-filters needs to be specified. Also the parent of the
+ * parameter we're checking should be specified, not the parameter to check
+ * itself.
+ *
+ * @param VObject\Property $parent
+ * @param array $filters
+ * @return bool
+ */
+ protected function validateParamFilters(VObject\Property $parent, array $filters) {
+
+ foreach($filters as $filter) {
+
+ $isDefined = isset($parent[$filter['name']]);
+
+ if ($filter['is-not-defined']) {
+
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if (!$filter['text-match']) {
+ continue;
+ }
+
+ if (version_compare(VObject\Version::VERSION, '3.0.0beta1', '>=')) {
+
+ // If there are sub-filters, we need to find at least one parameter
+ // for which the subfilters hold true.
+ foreach($parent[$filter['name']]->getParts() as $subParam) {
+
+ if($this->validateTextMatch($subParam,$filter['text-match'])) {
+ // We had a match, so this param-filter succeeds
+ continue 2;
+ }
+
+ }
+
+ } else {
+
+ // If there are sub-filters, we need to find at least one parameter
+ // for which the subfilters hold true.
+ foreach($parent[$filter['name']] as $subParam) {
+
+ if($this->validateTextMatch($subParam,$filter['text-match'])) {
+ // We had a match, so this param-filter succeeds
+ continue 2;
+ }
+
+ }
+
+ }
+
+ // If we got here it means there was a text-match filter and there
+ // were no matches. This means the filter needs to return false.
+ return false;
+
+ }
+
+ // If we got here it means we got through all param-filters alive so the
+ // filters were all true.
+ return true;
+
+ }
+
+ /**
+ * This method checks the validity of a text-match.
+ *
+ * A single text-match should be specified as well as the specific property
+ * or parameter we need to validate.
+ *
+ * @param VObject\Node|string $check Value to check against.
+ * @param array $textMatch
+ * @return bool
+ */
+ protected function validateTextMatch($check, array $textMatch) {
+
+ if ($check instanceof VObject\Node) {
+ $check = (string)$check;
+ }
+
+ $isMatching = \OldSabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']);
+
+ return ($textMatch['negate-condition'] xor $isMatching);
+
+ }
+
+ /**
+ * Validates if a component matches the given time range.
+ *
+ * This is all based on the rules specified in rfc4791, which are quite
+ * complex.
+ *
+ * @param VObject\Node $component
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return bool
+ */
+ protected function validateTimeRange(VObject\Node $component, $start, $end) {
+
+ if (is_null($start)) {
+ $start = new DateTime('1900-01-01');
+ }
+ if (is_null($end)) {
+ $end = new DateTime('3000-01-01');
+ }
+
+ switch($component->name) {
+
+ case 'VEVENT' :
+ case 'VTODO' :
+ case 'VJOURNAL' :
+
+ return $component->isInTimeRange($start, $end);
+
+ case 'VALARM' :
+
+ // If the valarm is wrapped in a recurring event, we need to
+ // expand the recursions, and validate each.
+ //
+ // Our datamodel doesn't easily allow us to do this straight
+ // in the VALARM component code, so this is a hack, and an
+ // expensive one too.
+ if ($component->parent->name === 'VEVENT' && $component->parent->RRULE) {
+
+ // Fire up the iterator!
+ $it = new VObject\RecurrenceIterator($component->parent->parent, (string)$component->parent->UID);
+ while($it->valid()) {
+ $expandedEvent = $it->getEventObject();
+
+ // We need to check from these expanded alarms, which
+ // one is the first to trigger. Based on this, we can
+ // determine if we can 'give up' expanding events.
+ $firstAlarm = null;
+ if ($expandedEvent->VALARM !== null) {
+ foreach($expandedEvent->VALARM as $expandedAlarm) {
+
+ $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
+ if ($expandedAlarm->isInTimeRange($start, $end)) {
+ return true;
+ }
+
+ if ((string)$expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') {
+ // This is an alarm with a non-relative trigger
+ // time, likely created by a buggy client. The
+ // implication is that every alarm in this
+ // recurring event trigger at the exact same
+ // time. It doesn't make sense to traverse
+ // further.
+ } else {
+ // We store the first alarm as a means to
+ // figure out when we can stop traversing.
+ if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
+ $firstAlarm = $effectiveTrigger;
+ }
+ }
+ }
+ }
+ if (is_null($firstAlarm)) {
+ // No alarm was found.
+ //
+ // Or technically: No alarm that will change for
+ // every instance of the recurrence was found,
+ // which means we can assume there was no match.
+ return false;
+ }
+ if ($firstAlarm > $end) {
+ return false;
+ }
+ $it->next();
+ }
+ return false;
+ } else {
+ return $component->isInTimeRange($start, $end);
+ }
+
+ case 'VFREEBUSY' :
+ throw new \OldSabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on ' . $component->name . ' components');
+
+ case 'COMPLETED' :
+ case 'CREATED' :
+ case 'DTEND' :
+ case 'DTSTAMP' :
+ case 'DTSTART' :
+ case 'DUE' :
+ case 'LAST-MODIFIED' :
+ return ($start <= $component->getDateTime() && $end >= $component->getDateTime());
+
+
+
+ default :
+ throw new \OldSabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a ' . $component->name . ' component');
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php
new file mode 100644
index 0000000..810a249
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAVACL\PrincipalBackend;
+
+/**
+ * Calendars collection
+ *
+ * This object is responsible for generating a list of calendar-homes for each
+ * user.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarRootNode extends \OldSabre\DAVACL\AbstractPrincipalCollection {
+
+ /**
+ * CalDAV backend
+ *
+ * @var OldSabre\CalDAV\Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * Constructor
+ *
+ * This constructor needs both an authentication and a caldav backend.
+ *
+ * By default this class will show a list of calendar collections for
+ * principals in the 'principals' collection. If your main principals are
+ * actually located in a different path, use the $principalPrefix argument
+ * to override this.
+ *
+ * @param PrincipalBackend\BackendInterface $principalBackend
+ * @param Backend\BackendInterface $caldavBackend
+ * @param string $principalPrefix
+ */
+ public function __construct(PrincipalBackend\BackendInterface $principalBackend,Backend\BackendInterface $caldavBackend, $principalPrefix = 'principals') {
+
+ parent::__construct($principalBackend, $principalPrefix);
+ $this->caldavBackend = $caldavBackend;
+
+ }
+
+ /**
+ * Returns the nodename
+ *
+ * We're overriding this, because the default will be the 'principalPrefix',
+ * and we want it to be OldSabre\CalDAV\Plugin::CALENDAR_ROOT
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return Plugin::CALENDAR_ROOT;
+
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principal
+ * @return \OldSabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal) {
+
+ return new UserCalendars($this->caldavBackend, $principal);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php
new file mode 100644
index 0000000..c335fcd
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\CalDAV\Exception;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * InvalidComponentType
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InvalidComponentType extends DAV\Exception\Forbidden {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {CALDAV:}supported-calendar-component as defined in rfc4791
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV,'cal:supported-calendar-component');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php
new file mode 100644
index 0000000..4a5a75b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+use OldSabre\VObject;
+
+/**
+ * ICS Exporter
+ *
+ * This plugin adds the ability to export entire calendars as .ics files.
+ * This is useful for clients that don't support CalDAV yet. They often do
+ * support ics files.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ICSExportPlugin extends DAV\ServerPlugin {
+
+ /**
+ * Reference to Server class
+ *
+ * @var \OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin and registers event handlers
+ *
+ * @param \OldSabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->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 Calendar)) return;
+
+ // Checking ACL, if available.
+ if ($aclPlugin = $this->server->getPlugin('acl')) {
+ $aclPlugin->checkPrivileges($uri, '{DAV:}read');
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type','text/calendar');
+ $this->server->httpResponse->sendStatus(200);
+
+ $nodes = $this->server->getPropertiesForPath($uri, array(
+ '{' . Plugin::NS_CALDAV . '}calendar-data',
+ ),1);
+
+ $this->server->httpResponse->sendBody($this->generateICS($nodes));
+
+ // Returning false to break the event chain
+ return false;
+
+ }
+
+ /**
+ * Merges all calendar objects, and builds one big ics export
+ *
+ * @param array $nodes
+ * @return string
+ */
+ public function generateICS(array $nodes) {
+
+ $calendar = new VObject\Component\VCalendar();
+ $calendar->version = '2.0';
+ if (DAV\Server::$exposeVersion) {
+ $calendar->prodid = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
+ } else {
+ $calendar->prodid = '-//SabreDAV//SabreDAV//EN';
+ }
+ $calendar->calscale = 'GREGORIAN';
+
+ $collectedTimezones = array();
+
+ $timezones = array();
+ $objects = array();
+
+ foreach($nodes as $node) {
+
+ if (!isset($node[200]['{' . Plugin::NS_CALDAV . '}calendar-data'])) {
+ continue;
+ }
+ $nodeData = $node[200]['{' . Plugin::NS_CALDAV . '}calendar-data'];
+
+ $nodeComp = VObject\Reader::read($nodeData);
+
+ foreach($nodeComp->children() as $child) {
+
+ switch($child->name) {
+ case 'VEVENT' :
+ case 'VTODO' :
+ case 'VJOURNAL' :
+ $objects[] = $child;
+ break;
+
+ // VTIMEZONE is special, because we need to filter out the duplicates
+ case 'VTIMEZONE' :
+ // Naively just checking tzid.
+ if (in_array((string)$child->TZID, $collectedTimezones)) continue;
+
+ $timezones[] = $child;
+ $collectedTimezones[] = $child->TZID;
+ break;
+
+ }
+
+ }
+
+ }
+
+ foreach($timezones as $tz) $calendar->add($tz);
+ foreach($objects as $obj) $calendar->add($obj);
+
+ return $calendar->serialize();
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php
new file mode 100644
index 0000000..70916bc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace OldSabre\CalDAV;
+use OldSabre\DAV;
+
+/**
+ * Calendar interface
+ *
+ * Implement this interface to allow a node to be recognized as an calendar.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICalendar extends 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 \OldSabre\CalDAV\CalendarQueryParser.
+ *
+ * @param array $filters
+ * @return array
+ */
+ public function calendarQuery(array $filters);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php
new file mode 100644
index 0000000..4e5dcd7
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace OldSabre\CalDAV;
+use OldSabre\DAV;
+
+/**
+ * CalendarObject interface
+ *
+ * Extend the ICalendarObject interface to allow your custom nodes to be picked up as
+ * CalendarObjects.
+ *
+ * Calendar objects are resources such as Events, Todo's or Journals.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICalendarObject extends DAV\IFile {
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php
new file mode 100644
index 0000000..c8fed62
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * This interface represents a Calendar that can be shared with other users.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IShareableCalendar extends ICalendar {
+
+ /**
+ * Updates the list of shares.
+ *
+ * The first array is a list of people that are to be added to the
+ * calendar.
+ *
+ * Every element in the add array has the following properties:
+ * * href - A url. Usually a mailto: address
+ * * commonName - Usually a first and last name, or false
+ * * summary - A description of the share, can also be false
+ * * readOnly - A boolean value
+ *
+ * Every element in the remove array is just the address string.
+ *
+ * @param array $add
+ * @param array $remove
+ * @return void
+ */
+ function updateShares(array $add, array $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 OldSabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ function getShares();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php
new file mode 100644
index 0000000..05ebe79
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * This interface represents a Calendar that is shared by a different user.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ISharedCalendar extends ICalendar {
+
+ /**
+ * This method should return the url of the owners' copy of the shared
+ * calendar.
+ *
+ * @return string
+ */
+ function getSharedUrl();
+
+ /**
+ * 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 OldSabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ function getShares();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php
new file mode 100644
index 0000000..d18a409
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+use OldSabre\DAVACL;
+
+/**
+ * This node represents a list of notifications.
+ *
+ * It provides no additional functionality, but you must implement this
+ * interface to allow the Notifications plugin to mark the collection
+ * as a notifications collection.
+ *
+ * This collection should only return OldSabre\CalDAV\Notifications\INode nodes as
+ * its children.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Collection extends DAV\Collection implements ICollection, DAVACL\IACL {
+
+ /**
+ * The notification backend
+ *
+ * @var OldSabre\CalDAV\Backend\NotificationSupport
+ */
+ protected $caldavBackend;
+
+ /**
+ * Principal uri
+ *
+ * @var string
+ */
+ protected $principalUri;
+
+ /**
+ * Constructor
+ *
+ * @param CalDAV\Backend\NotificationSupport $caldavBackend
+ * @param string $principalUri
+ */
+ public function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri) {
+
+ $this->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 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 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 OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php
new file mode 100644
index 0000000..cabdac4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+
+use OldSabre\DAV;
+
+/**
+ * This node represents a list of notifications.
+ *
+ * It provides no additional functionality, but you must implement this
+ * interface to allow the Notifications plugin to mark the collection
+ * as a notifications collection.
+ *
+ * This collection should only return OldSabre\CalDAV\Notifications\INode nodes as
+ * its children.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICollection extends DAV\ICollection {
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php
new file mode 100644
index 0000000..f63f8cc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+
+/**
+ * This node represents a single notification.
+ *
+ * The signature is mostly identical to that of OldSabre\DAV\IFile, but the get() method
+ * MUST return an xml document that matches the requirements of the
+ * 'caldav-notifications.txt' spec.
+ *
+ * For a complete example, check out the Notification class, which contains
+ * some helper functions.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface INode {
+
+ /**
+ * This method must return an xml element, using the
+ * OldSabre\CalDAV\Notifications\INotificationType classes.
+ *
+ * @return INotificationType
+ */
+ function getNotificationType();
+
+ /**
+ * Returns the etag for the notification.
+ *
+ * The etag must be surrounded by litteral double-quotes.
+ *
+ * @return string
+ */
+ function getETag();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php
new file mode 100644
index 0000000..ef3b3e9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+use OldSabre\DAV;
+
+/**
+ * This interface reflects a single notification type.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface INotificationType extends DAV\PropertyInterface {
+
+ /**
+ * This method serializes the entire notification, as it is used in the
+ * response body.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ function serializeBody(DAV\Server $server, \DOMElement $node);
+
+ /**
+ * Returns a unique id for this notification
+ *
+ * This is just the base url. This should generally be some kind of unique
+ * id.
+ *
+ * @return string
+ */
+ function getId();
+
+ /**
+ * Returns the ETag for this notification.
+ *
+ * The ETag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ function getETag();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php
new file mode 100644
index 0000000..c9b88a4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+use OldSabre\DAVACL;
+
+/**
+ * This node represents a single notification.
+ *
+ * The signature is mostly identical to that of OldSabre\DAV\IFile, but the get() method
+ * MUST return an xml document that matches the requirements of the
+ * 'caldav-notifications.txt' spec.
+
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Node extends DAV\File implements INode, DAVACL\IACL {
+
+ /**
+ * The notification backend
+ *
+ * @var OldSabre\CalDAV\Backend\NotificationSupport
+ */
+ protected $caldavBackend;
+
+ /**
+ * The actual notification
+ *
+ * @var OldSabre\CalDAV\Notifications\INotificationType
+ */
+ protected $notification;
+
+ /**
+ * Owner principal of the notification
+ *
+ * @var string
+ */
+ protected $principalUri;
+
+ /**
+ * Constructor
+ *
+ * @param CalDAV\Backend\NotificationSupport $caldavBackend
+ * @param string $principalUri
+ * @param CalDAV\Notifications\INotificationType $notification
+ */
+ public function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri, INotificationType $notification) {
+
+ $this->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
+ * OldSabre\CalDAV\Notifications\INotificationType classes.
+ *
+ * @return 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 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 OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php
new file mode 100644
index 0000000..3c8fbf5
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php
@@ -0,0 +1,324 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications\Notification;
+
+use OldSabre\CalDAV\SharingPlugin as SharingPlugin;
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * This class represents the cs:invite-notification notification element.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Invite extends DAV\Property implements CalDAV\Notifications\INotificationType {
+
+ /**
+ * A unique id for the message
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * Timestamp of the notification
+ *
+ * @var DateTime
+ */
+ protected $dtStamp;
+
+ /**
+ * A url to the recipient of the notification. This can be an email
+ * address (mailto:), or a principal url.
+ *
+ * @var string
+ */
+ protected $href;
+
+ /**
+ * The type of message, see the SharingPlugin::STATUS_* constants.
+ *
+ * @var int
+ */
+ protected $type;
+
+ /**
+ * True if access to a calendar is read-only.
+ *
+ * @var bool
+ */
+ protected $readOnly;
+
+ /**
+ * A url to the shared calendar.
+ *
+ * @var string
+ */
+ protected $hostUrl;
+
+ /**
+ * Url to the sharer of the calendar
+ *
+ * @var string
+ */
+ protected $organizer;
+
+ /**
+ * The name of the sharer.
+ *
+ * @var string
+ */
+ protected $commonName;
+
+ /**
+ * The name of the sharer.
+ *
+ * @var string
+ */
+ protected $firstName;
+
+ /**
+ * The name of the sharer.
+ *
+ * @var string
+ */
+ protected $lastName;
+
+ /**
+ * A description of the share request
+ *
+ * @var string
+ */
+ protected $summary;
+
+ /**
+ * The Etag for the notification
+ *
+ * @var string
+ */
+ protected $etag;
+
+ /**
+ * The list of supported components
+ *
+ * @var OldSabre\CalDAV\Property\SupportedCalendarComponentSet
+ */
+ protected $supportedComponents;
+
+ /**
+ * Creates the Invite notification.
+ *
+ * This constructor receives an array with the following elements:
+ *
+ * * id - A unique id
+ * * etag - The etag
+ * * dtStamp - A DateTime object with a timestamp for the notification.
+ * * type - The type of notification, see SharingPlugin::STATUS_*
+ * constants for details.
+ * * readOnly - This must be set to true, if this is an invite for
+ * read-only access to a calendar.
+ * * hostUrl - A url to the shared calendar.
+ * * organizer - Url to the sharer principal.
+ * * commonName - The real name of the sharer (optional).
+ * * firstName - The first name of the sharer (optional).
+ * * lastName - The last name of the sharer (optional).
+ * * summary - Description of the share, can be the same as the
+ * calendar, but may also be modified (optional).
+ * * supportedComponents - An instance of
+ * OldSabre\CalDAV\Property\SupportedCalendarComponentSet.
+ * This allows the client to determine which components
+ * will be supported in the shared calendar. This is
+ * also optional.
+ *
+ * @param array $values All the options
+ */
+ public function __construct(array $values) {
+
+ $required = array(
+ 'id',
+ 'etag',
+ 'href',
+ 'dtStamp',
+ 'type',
+ 'readOnly',
+ 'hostUrl',
+ 'organizer',
+ );
+ foreach($required as $item) {
+ if (!isset($values[$item])) {
+ throw new \InvalidArgumentException($item . ' is a required constructor option');
+ }
+ }
+
+ foreach($values as $key=>$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 DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(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 DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serializeBody(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);
+
+ $organizerUrl = $doc->createElement('cs:organizer');
+ // If the organizer contains a 'mailto:' part, it means it should be
+ // treated as absolute.
+ if (strtolower(substr($this->organizer,0,7))==='mailto:') {
+ $organizerHref = new DAV\Property\Href($this->organizer, false);
+ } else {
+ $organizerHref = new DAV\Property\Href($this->organizer, true);
+ }
+ $organizerHref->serialize($server, $organizerUrl);
+
+ if ($this->commonName) {
+ $commonName = $doc->createElement('cs:common-name');
+ $commonName->appendChild($doc->createTextNode($this->commonName));
+ $organizerUrl->appendChild($commonName);
+
+ $commonNameOld = $doc->createElement('cs:organizer-cn');
+ $commonNameOld->appendChild($doc->createTextNode($this->commonName));
+ $prop->appendChild($commonNameOld);
+
+ }
+ if ($this->firstName) {
+ $firstName = $doc->createElement('cs:first-name');
+ $firstName->appendChild($doc->createTextNode($this->firstName));
+ $organizerUrl->appendChild($firstName);
+
+ $firstNameOld = $doc->createElement('cs:organizer-first');
+ $firstNameOld->appendChild($doc->createTextNode($this->firstName));
+ $prop->appendChild($firstNameOld);
+ }
+ if ($this->lastName) {
+ $lastName = $doc->createElement('cs:last-name');
+ $lastName->appendChild($doc->createTextNode($this->lastName));
+ $organizerUrl->appendChild($lastName);
+
+ $lastNameOld = $doc->createElement('cs:organizer-last');
+ $lastNameOld->appendChild($doc->createTextNode($this->lastName));
+ $prop->appendChild($lastNameOld);
+ }
+ $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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php
new file mode 100644
index 0000000..540da86
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications\Notification;
+
+use OldSabre\CalDAV\SharingPlugin as SharingPlugin;
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * This class represents the cs:invite-reply notification element.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InviteReply extends DAV\Property implements CalDAV\Notifications\INotificationType {
+
+ /**
+ * A unique id for the message
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * Timestamp of the notification
+ *
+ * @var DateTime
+ */
+ protected $dtStamp;
+
+ /**
+ * The unique id of the notification this was a reply to.
+ *
+ * @var string
+ */
+ protected $inReplyTo;
+
+ /**
+ * A url to the recipient of the original (!) notification.
+ *
+ * @var string
+ */
+ protected $href;
+
+ /**
+ * The type of message, see the SharingPlugin::STATUS_ constants.
+ *
+ * @var int
+ */
+ protected $type;
+
+ /**
+ * A url to the shared calendar.
+ *
+ * @var string
+ */
+ protected $hostUrl;
+
+ /**
+ * A description of the share request
+ *
+ * @var string
+ */
+ protected $summary;
+
+ /**
+ * Notification Etag
+ *
+ * @var string
+ */
+ protected $etag;
+
+ /**
+ * Creates the Invite Reply Notification.
+ *
+ * This constructor receives an array with the following elements:
+ *
+ * * id - A unique id
+ * * etag - The etag
+ * * dtStamp - A DateTime object with a timestamp for the notification.
+ * * inReplyTo - This should refer to the 'id' of the notification
+ * this is a reply to.
+ * * type - The type of notification, see SharingPlugin::STATUS_*
+ * constants for details.
+ * * hostUrl - A url to the shared calendar.
+ * * summary - Description of the share, can be the same as the
+ * calendar, but may also be modified (optional).
+ */
+ public function __construct(array $values) {
+
+ $required = array(
+ 'id',
+ 'etag',
+ 'href',
+ 'dtStamp',
+ 'inReplyTo',
+ 'type',
+ 'hostUrl',
+ );
+ foreach($required as $item) {
+ if (!isset($values[$item])) {
+ throw new \InvalidArgumentException($item . ' is a required constructor option');
+ }
+ }
+
+ foreach($values as $key=>$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 DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(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 DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serializeBody(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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php
new file mode 100644
index 0000000..aef0ea8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications\Notification;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * SystemStatus notification
+ *
+ * This notification can be used to indicate to the user that the system is
+ * down.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SystemStatus extends DAV\Property implements CalDAV\Notifications\INotificationType {
+
+ const TYPE_LOW = 1;
+ const TYPE_MEDIUM = 2;
+ const TYPE_HIGH = 3;
+
+ /**
+ * A unique id
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * The type of alert. This should be one of the TYPE_ constants.
+ *
+ * @var int
+ */
+ protected $type;
+
+ /**
+ * A human-readable description of the problem.
+ *
+ * @var string
+ */
+ protected $description;
+
+ /**
+ * A url to a website with more information for the user.
+ *
+ * @var string
+ */
+ protected $href;
+
+ /**
+ * Notification Etag
+ *
+ * @var string
+ */
+ protected $etag;
+
+ /**
+ * Creates the notification.
+ *
+ * Some kind of unique id should be provided. This is used to generate a
+ * url.
+ *
+ * @param string $id
+ * @param string $etag
+ * @param int $type
+ * @param string $description
+ * @param string $href
+ */
+ public function __construct($id, $etag, $type = self::TYPE_HIGH, $description = null, $href = null) {
+
+ $this->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 DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(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 DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serializeBody(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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php
new file mode 100644
index 0000000..8d9a279
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php
@@ -0,0 +1,1338 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+use OldSabre\VObject;
+
+/**
+ * CalDAV plugin
+ *
+ * This plugin provides functionality added by CalDAV (RFC 4791)
+ * It implements new reports, and the MKCALENDAR method.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * This is the official CalDAV namespace
+ */
+ const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
+
+ /**
+ * This is the namespace for the proprietary calendarserver extensions
+ */
+ const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
+
+ /**
+ * The hardcoded root for calendar objects. It is unfortunate
+ * that we're stuck with it, but it will have to do for now
+ */
+ const CALENDAR_ROOT = 'calendars';
+
+ /**
+ * Reference to server object
+ *
+ * @var DAV\Server
+ */
+ protected $server;
+
+ /**
+ * The email handler for invites and other scheduling messages.
+ *
+ * @var Schedule\IMip
+ */
+ protected $imipHandler;
+
+ /**
+ * Sets the iMIP handler.
+ *
+ * iMIP = The email transport of iCalendar scheduling messages. Setting
+ * this is optional, but if you want the server to allow invites to be sent
+ * out, you must set a handler.
+ *
+ * Specifically iCal will plain assume that the server supports this. If
+ * the server doesn't, iCal will display errors when inviting people to
+ * events.
+ *
+ * @param Schedule\IMip $imipHandler
+ * @return void
+ */
+ public function setIMipHandler(Schedule\IMip $imipHandler) {
+
+ $this->imipHandler = $imipHandler;
+
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getHTTPMethods($uri) {
+
+ // The MKCALENDAR is only available on unmapped uri's, whose
+ // parents extend IExtendedCollection
+ list($parent, $name) = DAV\URLUtil::splitPath($uri);
+
+ $node = $this->server->tree->getNodeForPath($parent);
+
+ if ($node instanceof DAV\IExtendedCollection) {
+ try {
+ $node->getChild($name);
+ } catch (DAV\Exception\NotFound $e) {
+ return array('MKCALENDAR');
+ }
+ }
+ return array();
+
+ }
+
+ /**
+ * Returns a list of features for the DAV: HTTP header.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('calendar-access', 'calendar-proxy');
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'caldav';
+
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+
+ $node = $this->server->tree->getNodeForPath($uri);
+
+ $reports = array();
+ if ($node instanceof ICalendar || $node instanceof ICalendarObject) {
+ $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
+ $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
+ }
+ if ($node instanceof ICalendar) {
+ $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
+ }
+ return $reports;
+
+ }
+
+ /**
+ * Initializes the plugin
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+
+ $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
+ //$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000);
+ $server->subscribeEvent('report',array($this,'report'));
+ $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
+ $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel'));
+ $server->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'] = 'OldSabre\\CalDAV\\Property\\SupportedCalendarComponentSet';
+ $server->propertyMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'OldSabre\\CalDAV\\Property\\ScheduleCalendarTransp';
+
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\Schedule\\IOutbox'] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox';
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\Notifications\\ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification';
+
+ array_push($server->protectedProperties,
+
+ '{' . self::NS_CALDAV . '}supported-calendar-component-set',
+ '{' . self::NS_CALDAV . '}supported-calendar-data',
+ '{' . self::NS_CALDAV . '}max-resource-size',
+ '{' . self::NS_CALDAV . '}min-date-time',
+ '{' . self::NS_CALDAV . '}max-date-time',
+ '{' . self::NS_CALDAV . '}max-instances',
+ '{' . self::NS_CALDAV . '}max-attendees-per-instance',
+ '{' . self::NS_CALDAV . '}calendar-home-set',
+ '{' . self::NS_CALDAV . '}supported-collation-set',
+ '{' . self::NS_CALDAV . '}calendar-data',
+
+ // scheduling extension
+ '{' . self::NS_CALDAV . '}schedule-inbox-URL',
+ '{' . self::NS_CALDAV . '}schedule-outbox-URL',
+ '{' . self::NS_CALDAV . '}calendar-user-address-set',
+ '{' . self::NS_CALDAV . '}calendar-user-type',
+
+ // CalendarServer extensions
+ '{' . self::NS_CALENDARSERVER . '}getctag',
+ '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
+ '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for',
+ '{' . self::NS_CALENDARSERVER . '}notification-URL',
+ '{' . self::NS_CALENDARSERVER . '}notificationtype'
+
+ );
+ }
+
+ /**
+ * This function handles support for the MKCALENDAR method
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function unknownMethod($method, $uri) {
+
+ switch ($method) {
+ case 'MKCALENDAR' :
+ $this->httpMkCalendar($uri);
+ // false is returned to stop the propagation of the
+ // 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);
+ } catch (DAV\Exception\NotFound $e) {
+ return;
+ }
+ if (!$node instanceof Schedule\IOutbox)
+ return;
+
+ $this->outboxRequest($node, $uri);
+ return false;
+
+ }
+
+ }
+
+ /**
+ * This functions handles REPORT requests specific to CalDAV
+ *
+ * @param string $reportName
+ * @param \DOMNode $dom
+ * @return bool
+ */
+ public function report($reportName,$dom) {
+
+ switch($reportName) {
+ case '{'.self::NS_CALDAV.'}calendar-multiget' :
+ $this->calendarMultiGetReport($dom);
+ return false;
+ case '{'.self::NS_CALDAV.'}calendar-query' :
+ $this->calendarQueryReport($dom);
+ return false;
+ case '{'.self::NS_CALDAV.'}free-busy-query' :
+ $this->freeBusyQueryReport($dom);
+ return false;
+
+ }
+
+
+ }
+
+ /**
+ * This function handles the MKCALENDAR HTTP method, which creates
+ * a new calendar.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function httpMkCalendar($uri) {
+
+ // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support
+ // for clients matching iCal in the user agent
+ //$ua = $this->server->httpRequest->getHeader('User-Agent');
+ //if (strpos($ua,'iCal/')!==false) {
+ // throw new \OldSabre\DAV\Exception\Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.');
+ //}
+
+ $body = $this->server->httpRequest->getBody(true);
+ $properties = array();
+
+ if ($body) {
+
+ $dom = DAV\XMLUtil::loadDOMDocument($body);
+
+ foreach($dom->firstChild->childNodes as $child) {
+
+ if (DAV\XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue;
+ foreach(DAV\XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) {
+ $properties[$k] = $prop;
+ }
+
+ }
+ }
+
+ $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
+
+ $this->server->createCollection($uri,$resourceType,$properties);
+
+ $this->server->httpResponse->sendStatus(201);
+ $this->server->httpResponse->setHeader('Content-Length',0);
+ }
+
+ /**
+ * beforeGetProperties
+ *
+ * This method handler is invoked before any after properties for a
+ * resource are fetched. This allows us to add in any CalDAV specific
+ * properties.
+ *
+ * @param string $path
+ * @param DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @return void
+ */
+ public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
+
+ if ($node instanceof DAVACL\IPrincipal) {
+
+ // calendar-home-set property
+ $calHome = '{' . self::NS_CALDAV . '}calendar-home-set';
+ if (in_array($calHome,$requestedProperties)) {
+ $principalId = $node->getName();
+ $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/';
+
+ unset($requestedProperties[array_search($calHome, $requestedProperties)]);
+ $returnedProperties[200][$calHome] = new DAV\Property\Href($calendarHomePath);
+
+ }
+
+ // schedule-outbox-URL property
+ $scheduleProp = '{' . self::NS_CALDAV . '}schedule-outbox-URL';
+ if (in_array($scheduleProp,$requestedProperties)) {
+ $principalId = $node->getName();
+ $outboxPath = self::CALENDAR_ROOT . '/' . $principalId . '/outbox';
+
+ unset($requestedProperties[array_search($scheduleProp, $requestedProperties)]);
+ $returnedProperties[200][$scheduleProp] = new DAV\Property\Href($outboxPath);
+
+ }
+
+ // calendar-user-address-set property
+ $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set';
+ if (in_array($calProp,$requestedProperties)) {
+
+ $addresses = $node->getAlternateUriSet();
+ $addresses[] = $this->server->getBaseUri() . DAV\URLUtil::encodePath($node->getPrincipalUrl() . '/');
+ unset($requestedProperties[array_search($calProp, $requestedProperties)]);
+ $returnedProperties[200][$calProp] = new DAV\Property\HrefList($addresses, false);
+
+ }
+
+ // These two properties are shortcuts for ical to easily find
+ // other principals this principal has access to.
+ $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
+ $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
+ if (in_array($propRead,$requestedProperties) || in_array($propWrite,$requestedProperties)) {
+
+ $aclPlugin = $this->server->getPlugin('acl');
+ $membership = $aclPlugin->getPrincipalMembership($path);
+ $readList = array();
+ $writeList = array();
+
+ foreach($membership as $group) {
+
+ $groupNode = $this->server->tree->getNodeForPath($group);
+
+ // If the node is either ap proxy-read or proxy-write
+ // group, we grab the parent principal and add it to the
+ // list.
+ if ($groupNode instanceof Principal\IProxyRead) {
+ list($readList[]) = DAV\URLUtil::splitPath($group);
+ }
+ if ($groupNode instanceof Principal\IProxyWrite) {
+ list($writeList[]) = DAV\URLUtil::splitPath($group);
+ }
+
+ }
+ if (in_array($propRead,$requestedProperties)) {
+ unset($requestedProperties[$propRead]);
+ $returnedProperties[200][$propRead] = new DAV\Property\HrefList($readList);
+ }
+ if (in_array($propWrite,$requestedProperties)) {
+ unset($requestedProperties[$propWrite]);
+ $returnedProperties[200][$propWrite] = new DAV\Property\HrefList($writeList);
+ }
+
+ }
+
+ // 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 DAV\Property\Href($calendarHomePath);
+ }
+
+ } // instanceof IPrincipal
+
+ if ($node instanceof 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 ICalendarObject) {
+ // The calendar-data property is not supposed to be a 'real'
+ // property, but in large chunks of the spec it does act as such.
+ // Therefore we simply expose it as a property.
+ $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
+ if (in_array($calDataProp, $requestedProperties)) {
+ unset($requestedProperties[$calDataProp]);
+ $val = $node->get();
+ if (is_resource($val))
+ $val = stream_get_contents($val);
+
+ // Taking out \r to not screw up the xml output
+ $returnedProperties[200][$calDataProp] = str_replace("\r","", $val);
+
+ }
+ }
+
+ }
+
+ /**
+ * This function handles the calendar-multiget REPORT.
+ *
+ * This report is used by the client to fetch the content of a series
+ * of urls. Effectively avoiding a lot of redundant requests.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ public function calendarMultiGetReport($dom) {
+
+ $properties = array_keys(DAV\XMLUtil::parseProperties($dom->firstChild));
+ $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
+
+ $xpath = new \DOMXPath($dom);
+ $xpath->registerNameSpace('cal',Plugin::NS_CALDAV);
+ $xpath->registerNameSpace('dav','urn:DAV');
+
+ $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand');
+ if ($expand->length>0) {
+ $expandElem = $expand->item(0);
+ $start = $expandElem->getAttribute('start');
+ $end = $expandElem->getAttribute('end');
+ if(!$start || !$end) {
+ throw new DAV\Exception\BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element');
+ }
+ $start = VObject\DateTimeParser::parseDateTime($start);
+ $end = VObject\DateTimeParser::parseDateTime($end);
+
+ if ($end <= $start) {
+ throw new DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
+ }
+
+ $expand = true;
+
+ } else {
+
+ $expand = false;
+
+ }
+
+ foreach($hrefElems as $elem) {
+ $uri = $this->server->calculateUri($elem->nodeValue);
+ list($objProps) = $this->server->getPropertiesForPath($uri,$properties);
+
+ if ($expand && isset($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();
+ }
+
+ $propertyList[]=$objProps;
+
+ }
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $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($propertyList, $prefer['return-minimal']));
+
+ }
+
+ /**
+ * This function handles the calendar-query REPORT
+ *
+ * This report is used by clients to request calendar objects based on
+ * complex conditions.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ public function calendarQueryReport($dom) {
+
+ $parser = new CalendarQueryParser($dom);
+ $parser->parse();
+
+ $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
+ $depth = $this->server->getHTTPDepth(0);
+
+ // The default result is an empty array
+ $result = array();
+
+ // The calendarobject was requested directly. In this case we handle
+ // this locally.
+ if ($depth == 0 && $node instanceof 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 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 we're dealing with a calendar, the calendar itself is responsible
+ // for the calendar-query.
+ if ($node instanceof ICalendar && $depth = 1) {
+
+ $nodePaths = $node->calendarQuery($parser->filters);
+
+ foreach($nodePaths as $path) {
+
+ list($properties) =
+ $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties);
+
+ 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']);
+ $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
+ }
+
+ $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->setHeader('Vary','Brief,Prefer');
+ $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal']));
+
+ }
+
+ /**
+ * This method is responsible for parsing the request and generating the
+ * response for the CALDAV:free-busy-query REPORT.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ protected function freeBusyQueryReport(\DOMNode $dom) {
+
+ $start = null;
+ $end = null;
+
+ foreach($dom->firstChild->childNodes as $childNode) {
+
+ $clark = DAV\XMLUtil::toClarkNotation($childNode);
+ if ($clark == '{' . self::NS_CALDAV . '}time-range') {
+ $start = $childNode->getAttribute('start');
+ $end = $childNode->getAttribute('end');
+ break;
+ }
+
+ }
+ if ($start) {
+ $start = VObject\DateTimeParser::parseDateTime($start);
+ }
+ if ($end) {
+ $end = VObject\DateTimeParser::parseDateTime($end);
+ }
+
+ if (!$start && !$end) {
+ throw new DAV\Exception\BadRequest('The freebusy report must have a time-range filter');
+ }
+ $acl = $this->server->getPlugin('acl');
+
+ if (!$acl) {
+ throw new DAV\Exception('The ACL plugin must be loaded for free-busy queries to work');
+ }
+ $uri = $this->server->getRequestUri();
+ $acl->checkPrivileges($uri,'{' . self::NS_CALDAV . '}read-free-busy');
+
+ $calendar = $this->server->tree->getNodeForPath($uri);
+ if (!$calendar instanceof ICalendar) {
+ throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
+ }
+
+ // 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,
+ ));
+
+ $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();
+ $result = $result->serialize();
+
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
+ $this->server->httpResponse->setHeader('Content-Length', strlen($result));
+ $this->server->httpResponse->sendBody($result);
+
+ }
+
+ /**
+ * This method is triggered before a file gets updated with new content.
+ *
+ * This plugin uses this method to ensure that CalDAV objects receive
+ * valid calendar data.
+ *
+ * @param string $path
+ * @param DAV\IFile $node
+ * @param resource $data
+ * @return void
+ */
+ public function beforeWriteContent($path, DAV\IFile $node, &$data) {
+
+ if (!$node instanceof ICalendarObject)
+ return;
+
+ $this->validateICalendar($data, $path);
+
+ }
+
+ /**
+ * This method is triggered before a new file is created.
+ *
+ * This plugin uses this method to ensure that newly created calendar
+ * objects contain valid calendar data.
+ *
+ * @param string $path
+ * @param resource $data
+ * @param DAV\ICollection $parentNode
+ * @return void
+ */
+ public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode) {
+
+ if (!$parentNode instanceof Calendar)
+ return;
+
+ $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 (DAV\Exception\NotFound $e) {
+ return;
+ }
+
+ if (!$node instanceof 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;
+
+ }
+
+ /**
+ * Checks if the submitted iCalendar data is in fact, valid.
+ *
+ * An exception is thrown if it's not.
+ *
+ * @param resource|string $data
+ * @param string $path
+ * @return void
+ */
+ protected function validateICalendar(&$data, $path) {
+
+ // If it's a stream, we convert it to a string first.
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+
+ // Converting the data to unicode, if needed.
+ $data = DAV\StringUtil::ensureUTF8($data);
+
+ try {
+
+ $vobj = VObject\Reader::read($data);
+
+ } catch (VObject\ParseException $e) {
+
+ throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
+
+ }
+
+ if ($vobj->name !== 'VCALENDAR') {
+ throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
+ }
+
+ // Get the Supported Components for the target calendar
+ list($parentPath,$object) = 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) {
+ switch($component->name) {
+ case 'VTIMEZONE' :
+ continue 2;
+ case 'VEVENT' :
+ case 'VTODO' :
+ case 'VJOURNAL' :
+ if (is_null($foundType)) {
+ $foundType = $component->name;
+ if (!in_array($foundType, $supportedComponents)) {
+ throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
+ }
+ if (!isset($component->UID)) {
+ throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
+ }
+ $foundUID = (string)$component->UID;
+ } else {
+ if ($foundType !== $component->name) {
+ throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
+ }
+ if ($foundUID !== (string)$component->UID) {
+ throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
+ }
+ }
+ break;
+ default :
+ throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
+
+ }
+ }
+ if (!$foundType)
+ throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
+
+ }
+
+ /**
+ * 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 Schedule\IOutbox $outboxNode
+ * @param string $outboxUri
+ * @return void
+ */
+ public function outboxRequest(Schedule\IOutbox $outboxNode, $outboxUri) {
+
+ // Parsing the request body
+ try {
+ $vObject = VObject\Reader::read($this->server->httpRequest->getBody(true));
+ } catch (VObject\ParseException $e) {
+ throw new 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 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 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,'{' . 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,'{' . Plugin::NS_CALDAV . '}schedule-post-vevent');
+ $this->handleEventNotification($outboxNode, $vObject);
+
+ } else {
+
+ throw new 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(Schedule\IOutbox $outboxNode, VObject\Component $vObject) {
+
+ $originator = $this->server->httpRequest->getHeader('Originator');
+ $recipients = $this->server->httpRequest->getHeader('Recipient');
+
+ if (!$originator) {
+ throw new DAV\Exception\BadRequest('The Originator: header must be specified when making POST requests');
+ }
+ if (!$recipients) {
+ throw new DAV\Exception\BadRequest('The Recipient: header must be specified when making POST requests');
+ }
+
+ $recipients = explode(',',$recipients);
+ foreach($recipients as $k=>$recipient) {
+
+ $recipient = trim($recipient);
+ if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
+ throw new DAV\Exception\BadRequest('Recipients must start with mailto: and must be valid email address');
+ }
+ $recipient = substr($recipient, 7);
+ $recipients[$k] = $recipient;
+ }
+
+ // We need to make sure that 'originator' matches one of the email
+ // addresses of the selected principal.
+ $principal = $outboxNode->getOwner();
+ $props = $this->server->getProperties($principal,array(
+ '{' . self::NS_CALDAV . '}calendar-user-address-set',
+ ));
+
+ $addresses = array();
+ if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
+ $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
+ }
+
+ $found = false;
+ foreach($addresses as $address) {
+
+ // Trimming the / on both sides, just in case..
+ if (rtrim(strtolower($originator),'/') === rtrim(strtolower($address),'/')) {
+ $found = true;
+ break;
+ }
+
+ }
+
+ if (!$found) {
+ throw new DAV\Exception\Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header');
+ }
+
+ // If the Originator header was a url, and not a mailto: address..
+ // we're going to try to pull the mailto: from the vobject body.
+ if (strtolower(substr($originator,0,7)) !== 'mailto:') {
+ $originator = (string)$vObject->VEVENT->ORGANIZER;
+
+ }
+ if (strtolower(substr($originator,0,7)) !== 'mailto:') {
+ throw new DAV\Exception\Forbidden('Could not find mailto: address in both the Orignator header, and the ORGANIZER property in the VEVENT');
+ }
+ $originator = substr($originator,7);
+
+ $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));
+
+ }
+
+ /**
+ * Sends an iMIP message by email.
+ *
+ * This method must return an array with status codes per recipient.
+ * This should look something like:
+ *
+ * array(
+ * 'user1@example.org' => '2.0;Success'
+ * )
+ *
+ * Formatting for this status code can be found at:
+ * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
+ *
+ * A list of valid status codes can be found at:
+ * https://tools.ietf.org/html/rfc5546#section-3.6
+ *
+ * @param string $originator
+ * @param array $recipients
+ * @param VObject\Component $vObject
+ * @param string $principal Principal url
+ * @return array
+ */
+ 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, $principal);
+ $resultStatus = '2.0;Success';
+ }
+
+ $result = array();
+ foreach($recipients as $recipient) {
+ $result[$recipient] = $resultStatus;
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Generates a schedule-response XML body
+ *
+ * The recipients array is a key->value list, containing email addresses
+ * and iTip status codes. See the iMIPMessage method for a description of
+ * the value.
+ *
+ * @param array $recipients
+ * @return string
+ */
+ public function generateScheduleResponse(array $recipients) {
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+ $xscheduleResponse = $dom->createElement('cal:schedule-response');
+ $dom->appendChild($xscheduleResponse);
+
+ foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+ $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
+
+ }
+
+ foreach($recipients as $recipient=>$status) {
+ $xresponse = $dom->createElement('cal:response');
+
+ $xrecipient = $dom->createElement('cal:recipient');
+ $xrecipient->appendChild($dom->createTextNode($recipient));
+ $xresponse->appendChild($xrecipient);
+
+ $xrequestStatus = $dom->createElement('cal:request-status');
+ $xrequestStatus->appendChild($dom->createTextNode($status));
+ $xresponse->appendChild($xrequestStatus);
+
+ $xscheduleResponse->appendChild($xresponse);
+
+ }
+
+ return $dom->saveXML();
+
+ }
+
+ /**
+ * This method is responsible for parsing a free-busy query request and
+ * returning it's result.
+ *
+ * @param Schedule\IOutbox $outbox
+ * @param string $request
+ * @return string
+ */
+ protected function handleFreeBusyRequest(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 = '{' . 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 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 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 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 VObject\Component $request
+ * @return array
+ */
+ protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
+
+ $caldavNS = '{' . 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 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 = new VObject\Component\VCalendar();
+ $vcalendar->VERSION = '2.0';
+ $vcalendar->METHOD = 'REPLY';
+ $vcalendar->CALSCALE = 'GREGORIAN';
+ $vcalendar->PRODID = '-//SabreDAV//SabreDAV ' . 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
+ * DAV\Browser\Plugin. This allows us to generate an interface users
+ * can use to create new calendars.
+ *
+ * @param DAV\INode $node
+ * @param string $output
+ * @return bool
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output) {
+
+ if (!$node instanceof UserCalendars)
+ return;
+
+ $output.= '<tr><td colspan="2"><form method="post" action="">
+ <h3>Create new calendar</h3>
+ <input type="hidden" name="sabreAction" value="mkcalendar" />
+ <label>Name (uri):</label> <input type="text" name="name" /><br />
+ <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
+ <input type="submit" value="create" />
+ </form>
+ </td></tr>';
+
+ return false;
+
+ }
+
+ /**
+ * This method allows us to intercept the 'mkcalendar' sabreAction. This
+ * action enables the user to create new calendars from the browser plugin.
+ *
+ * @param string $uri
+ * @param string $action
+ * @param array $postVars
+ * @return bool
+ */
+ public function browserPostAction($uri, $action, array $postVars) {
+
+ if ($action!=='mkcalendar')
+ return;
+
+ $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
+ $properties = array();
+ if (isset($postVars['{DAV:}displayname'])) {
+ $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
+ }
+ $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
+ return false;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php
new file mode 100644
index 0000000..e78fc9f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+use OldSabre\DAVACL;
+
+/**
+ * Principal collection
+ *
+ * This is an alternative collection to the standard ACL principal collection.
+ * This collection adds support for the calendar-proxy-read and
+ * calendar-proxy-write sub-principals, as defined by the caldav-proxy
+ * specification.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Collection extends DAVACL\AbstractPrincipalCollection {
+
+ /**
+ * Returns a child object based on principal information
+ *
+ * @param array $principalInfo
+ * @return User
+ */
+ public function getChildForPrincipal(array $principalInfo) {
+
+ return new User($this->principalBackend, $principalInfo);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php
new file mode 100644
index 0000000..5194c0b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+
+use OldSabre\DAVACL;
+
+/**
+ * ProxyRead principal interface
+ *
+ * Any principal node implementing this interface will be picked up as a 'proxy
+ * principal group'.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IProxyRead extends DAVACL\IPrincipal {
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php
new file mode 100644
index 0000000..d6b052e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+
+use OldSabre\DAVACL;
+
+/**
+ * ProxyWrite principal interface
+ *
+ * Any principal node implementing this interface will be picked up as a 'proxy
+ * principal group'.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IProxyWrite extends DAVACL\IPrincipal {
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php
new file mode 100644
index 0000000..109f802
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+use OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * ProxyRead principal
+ *
+ * This class represents a principal group, hosted under the main principal.
+ * This is needed to implement 'Calendar delegation' support. This class is
+ * instantiated by User.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ProxyRead implements IProxyRead {
+
+ /**
+ * Principal information from the parent principal.
+ *
+ * @var array
+ */
+ protected $principalInfo;
+
+ /**
+ * Principal backend
+ *
+ * @var DAVACL\PrincipalBackend\BackendInterface
+ */
+ protected $principalBackend;
+
+ /**
+ * Creates the object.
+ *
+ * Note that you MUST supply the parent principal information.
+ *
+ * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
+ * @param array $principalInfo
+ */
+ public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
+
+ $this->principalInfo = $principalInfo;
+ $this->principalBackend = $principalBackend;
+
+ }
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return 'calendar-proxy-read';
+
+ }
+
+ /**
+ * Returns the last modification time
+ *
+ * @return null
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Deletes the current node
+ *
+ * @throws DAV\Exception\Forbidden
+ * @return void
+ */
+ public function delete() {
+
+ throw new DAV\Exception\Forbidden('Permission denied to delete node');
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @throws DAV\Exception\Forbidden
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new DAV\Exception\Forbidden('Permission denied to rename file');
+
+ }
+
+
+ /**
+ * Returns a list of alternative urls for a principal
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet() {
+
+ return array();
+
+ }
+
+ /**
+ * Returns the full principal url
+ *
+ * @return string
+ */
+ public function getPrincipalUrl() {
+
+ return $this->principalInfo['uri'] . '/' . $this->getName();
+
+ }
+
+ /**
+ * Returns the list of group members
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet() {
+
+ return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
+
+ }
+
+ /**
+ * Returns the list of groups this principal is member of
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership() {
+
+ return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
+
+ }
+
+ /**
+ * Sets a list of group members
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ *
+ * @param array $principals
+ * @return void
+ */
+ public function setGroupMemberSet(array $principals) {
+
+ $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
+
+ }
+
+ /**
+ * Returns the displayname
+ *
+ * This should be a human readable name for the principal.
+ * If none is available, return the nodename.
+ *
+ * @return string
+ */
+ public function getDisplayName() {
+
+ return $this->getName();
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php
new file mode 100644
index 0000000..7295254
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+use OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * ProxyWrite principal
+ *
+ * This class represents a principal group, hosted under the main principal.
+ * This is needed to implement 'Calendar delegation' support. This class is
+ * instantiated by User.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ProxyWrite implements IProxyWrite {
+
+ /**
+ * Parent principal information
+ *
+ * @var array
+ */
+ protected $principalInfo;
+
+ /**
+ * Principal Backend
+ *
+ * @var DAVACL\PrincipalBackend\BackendInterface
+ */
+ protected $principalBackend;
+
+ /**
+ * Creates the object
+ *
+ * Note that you MUST supply the parent principal information.
+ *
+ * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
+ * @param array $principalInfo
+ */
+ public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
+
+ $this->principalInfo = $principalInfo;
+ $this->principalBackend = $principalBackend;
+
+ }
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return 'calendar-proxy-write';
+
+ }
+
+ /**
+ * Returns the last modification time
+ *
+ * @return null
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Deletes the current node
+ *
+ * @throws DAV\Exception\Forbidden
+ * @return void
+ */
+ public function delete() {
+
+ throw new DAV\Exception\Forbidden('Permission denied to delete node');
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @throws DAV\Exception\Forbidden
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new DAV\Exception\Forbidden('Permission denied to rename file');
+
+ }
+
+
+ /**
+ * Returns a list of alternative urls for a principal
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet() {
+
+ return array();
+
+ }
+
+ /**
+ * Returns the full principal url
+ *
+ * @return string
+ */
+ public function getPrincipalUrl() {
+
+ return $this->principalInfo['uri'] . '/' . $this->getName();
+
+ }
+
+ /**
+ * Returns the list of group members
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet() {
+
+ return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
+
+ }
+
+ /**
+ * Returns the list of groups this principal is member of
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership() {
+
+ return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
+
+ }
+
+ /**
+ * Sets a list of group members
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ *
+ * @param array $principals
+ * @return void
+ */
+ public function setGroupMemberSet(array $principals) {
+
+ $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
+
+ }
+
+ /**
+ * Returns the displayname
+ *
+ * This should be a human readable name for the principal.
+ * If none is available, return the nodename.
+ *
+ * @return string
+ */
+ public function getDisplayName() {
+
+ return $this->getName();
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php
new file mode 100644
index 0000000..345562a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * CalDAV principal
+ *
+ * This is a standard user-principal for CalDAV. This principal is also a
+ * collection and returns the caldav-proxy-read and caldav-proxy-write child
+ * principals.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class User extends DAVACL\Principal implements DAV\ICollection {
+
+ /**
+ * Creates a new file in the directory
+ *
+ * @param string $name Name of the file
+ * @param resource $data Initial payload, passed as a readable stream resource.
+ * @throws DAV\Exception\Forbidden
+ * @return void
+ */
+ public function createFile($name, $data = null) {
+
+ throw new DAV\Exception\Forbidden('Permission denied to create file (filename ' . $name . ')');
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @throws DAV\Exception\Forbidden
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ throw new DAV\Exception\Forbidden('Permission denied to create directory');
+
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * @param string $name
+ * @return DAV\INode
+ */
+ public function getChild($name) {
+
+ $principal = $this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/' . $name);
+ if (!$principal) {
+ throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
+ }
+ if ($name === 'calendar-proxy-read')
+ return new ProxyRead($this->principalBackend, $this->principalProperties);
+
+ if ($name === 'calendar-proxy-write')
+ return new ProxyWrite($this->principalBackend, $this->principalProperties);
+
+ throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren() {
+
+ $r = array();
+ if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-read')) {
+ $r[] = new ProxyRead($this->principalBackend, $this->principalProperties);
+ }
+ if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-write')) {
+ $r[] = new ProxyWrite($this->principalBackend, $this->principalProperties);
+ }
+
+ return $r;
+
+ }
+
+ /**
+ * Returns whether or not the child node exists
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ try {
+ $this->getChild($name);
+ return true;
+ } catch (DAV\Exception\NotFound $e) {
+ return false;
+ }
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ $acl = parent::getACL();
+ $acl[] = array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalProperties['uri'] . '/calendar-proxy-read',
+ 'protected' => true,
+ );
+ $acl[] = array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalProperties['uri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ );
+ return $acl;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php
new file mode 100644
index 0000000..918c3d1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * AllowedSharingModes
+ *
+ * This property encodes the 'allowed-sharing-modes' property, as defined by
+ * the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/
+ * namespace.
+ *
+ * This property is a representation of the supported-calendar_component-set
+ * property in the CalDAV namespace. It simply requires an array of components,
+ * such as VEVENT, VTODO
+ *
+ * @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AllowedSharingModes extends DAV\Property {
+
+ /**
+ * Whether or not a calendar can be shared with another user
+ *
+ * @var bool
+ */
+ protected $canBeShared;
+
+ /**
+ * Whether or not the calendar can be placed on a public url.
+ *
+ * @var bool
+ */
+ protected $canBePublished;
+
+ /**
+ * Constructor
+ *
+ * @param bool $canBeShared
+ * @param bool $canBePublished
+ * @return void
+ */
+ public function __construct($canBeShared, $canBePublished) {
+
+ $this->canBeShared = $canBeShared;
+ $this->canBePublished = $canBePublished;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php
new file mode 100644
index 0000000..0e2f8f1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+
+use OldSabre\CalDAV\SharingPlugin as SharingPlugin;
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * Invite property
+ *
+ * This property encodes the 'invite' property, as defined by
+ * the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/
+ * namespace.
+ *
+ * @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Invite extends DAV\Property {
+
+ /**
+ * The list of users a calendar has been shared to.
+ *
+ * @var array
+ */
+ protected $users;
+
+ /**
+ * The organizer contains information about the person who shared the
+ * object.
+ *
+ * @var array
+ */
+ protected $organizer;
+
+ /**
+ * Creates the property.
+ *
+ * Users is an array. Each element of the array has the following
+ * properties:
+ *
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first and lastname for a user.
+ * * status - One of the SharingPlugin::STATUS_* constants.
+ * * readOnly - true or false
+ * * summary - Optional, description of the share
+ *
+ * The organizer key is optional to specify. It's only useful when a
+ * 'sharee' requests the sharing information.
+ *
+ * The organizer may have the following properties:
+ * * href - Often a mailto: address.
+ * * commonName - Optional human-readable name.
+ * * firstName - Optional first name.
+ * * lastName - Optional last name.
+ *
+ * If you wonder why these two structures are so different, I guess a
+ * valid answer is that the current spec is still a draft.
+ *
+ * @param array $users
+ */
+ public function __construct(array $users, array $organizer = null) {
+
+ $this->users = $users;
+ $this->organizer = $organizer;
+
+ }
+
+ /**
+ * 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 DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ if (!is_null($this->organizer)) {
+
+ $xorganizer = $doc->createElement('cs:organizer');
+
+ $href = $doc->createElement('d:href');
+ $href->appendChild($doc->createTextNode($this->organizer['href']));
+ $xorganizer->appendChild($href);
+
+ if (isset($this->organizer['commonName']) && $this->organizer['commonName']) {
+ $commonName = $doc->createElement('cs:common-name');
+ $commonName->appendChild($doc->createTextNode($this->organizer['commonName']));
+ $xorganizer->appendChild($commonName);
+ }
+ if (isset($this->organizer['firstName']) && $this->organizer['firstName']) {
+ $firstName = $doc->createElement('cs:first-name');
+ $firstName->appendChild($doc->createTextNode($this->organizer['firstName']));
+ $xorganizer->appendChild($firstName);
+ }
+ if (isset($this->organizer['lastName']) && $this->organizer['lastName']) {
+ $lastName = $doc->createElement('cs:last-name');
+ $lastName->appendChild($doc->createTextNode($this->organizer['lastName']));
+ $xorganizer->appendChild($lastName);
+ }
+
+ $node->appendChild($xorganizer);
+
+
+ }
+
+ 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 DAV\IProperty
+ */
+ static function unserialize(\DOMElement $prop) {
+
+ $xpath = new \DOMXPath($prop->ownerDocument);
+ $xpath->registerNamespace('cs', CalDAV\Plugin::NS_CALENDARSERVER);
+ $xpath->registerNamespace('d', 'urn: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 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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php
new file mode 100644
index 0000000..bf8f78a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * schedule-calendar-transp property.
+ *
+ * This property is a representation of the schedule-calendar-transp property.
+ * This property is defined in RFC6638 (caldav scheduling).
+ *
+ * Its values are either 'transparent' or 'opaque'. If it's transparent, it
+ * means that this calendar will not be taken into consideration when a
+ * different user queries for free-busy information. If it's 'opaque', it will.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ScheduleCalendarTransp extends DAV\Property {
+
+ const TRANSPARENT = 'transparent';
+ const OPAQUE = 'opaque';
+
+ protected $value;
+
+ /**
+ * Creates the property
+ *
+ * @param string $value
+ */
+ public function __construct($value) {
+
+ if ($value !== self::TRANSPARENT && $value !== self::OPAQUE) {
+ throw new \InvalidArgumentException('The value must either be specified as "transparent" or "opaque"');
+ }
+ $this->value = $value;
+
+ }
+
+ /**
+ * Returns the current value
+ *
+ * @return string
+ */
+ public function getValue() {
+
+ return $this->value;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(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 ScheduleCalendarTransp
+ */
+ static function unserialize(\DOMElement $node) {
+
+ $value = null;
+ foreach($node->childNodes as $childNode) {
+ switch(DAV\XMLUtil::toClarkNotation($childNode)) {
+ case '{' . CalDAV\Plugin::NS_CALDAV . '}opaque' :
+ $value = self::OPAQUE;
+ break;
+ case '{' . CalDAV\Plugin::NS_CALDAV . '}transparent' :
+ $value = self::TRANSPARENT;
+ break;
+ }
+ }
+ if (is_null($value))
+ return null;
+
+ return new self($value);
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php
new file mode 100644
index 0000000..cd4b469
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * Supported component set property
+ *
+ * This property is a representation of the supported-calendar_component-set
+ * property in the CalDAV namespace. It simply requires an array of components,
+ * such as VEVENT, VTODO
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedCalendarComponentSet extends DAV\Property {
+
+ /**
+ * List of supported components, such as "VEVENT, VTODO"
+ *
+ * @var array
+ */
+ private $components;
+
+ /**
+ * Creates the property
+ *
+ * @param array $components
+ */
+ public function __construct(array $components) {
+
+ $this->components = $components;
+
+ }
+
+ /**
+ * Returns the list of supported components
+ *
+ * @return array
+ */
+ public function getValue() {
+
+ return $this->components;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ foreach($this->components as $component) {
+
+ $xcomp = $doc->createElement('cal:comp');
+ $xcomp->setAttribute('name',$component);
+ $node->appendChild($xcomp);
+
+ }
+
+ }
+
+ /**
+ * Unserializes the DOMElement back into a Property class.
+ *
+ * @param \DOMElement $node
+ * @return Property_SupportedCalendarComponentSet
+ */
+ static function unserialize(\DOMElement $node) {
+
+ $components = array();
+ foreach($node->childNodes as $childNode) {
+ if (DAV\XMLUtil::toClarkNotation($childNode)==='{' . CalDAV\Plugin::NS_CALDAV . '}comp') {
+ $components[] = $childNode->getAttribute('name');
+ }
+ }
+ return new self($components);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php
new file mode 100644
index 0000000..fd358c6
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+use OldSabre\DAV;
+use OldSabre\CalDAV\Plugin;
+
+/**
+ * Supported-calendar-data property
+ *
+ * This property is a representation of the supported-calendar-data property
+ * in the CalDAV namespace. SabreDAV only has support for text/calendar;2.0
+ * so the value is currently hardcoded.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedCalendarData extends DAV\Property {
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ $prefix = isset($server->xmlNamespaces[Plugin::NS_CALDAV])?$server->xmlNamespaces[Plugin::NS_CALDAV]:'cal';
+
+ $caldata = $doc->createElement($prefix . ':calendar-data');
+ $caldata->setAttribute('content-type','text/calendar');
+ $caldata->setAttribute('version','2.0');
+
+ $node->appendChild($caldata);
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php
new file mode 100644
index 0000000..9a92c7c
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+use OldSabre\DAV;
+
+/**
+ * supported-collation-set property
+ *
+ * This property is a representation of the supported-collation-set property
+ * in the CalDAV namespace.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedCollationSet extends DAV\Property {
+
+ /**
+ * Serializes the property in a DOM document
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ $prefix = $node->lookupPrefix('urn:ietf:params:xml:ns:caldav');
+ if (!$prefix) $prefix = 'cal';
+
+ $node->appendChild(
+ $doc->createElement($prefix . ':supported-collation','i;ascii-casemap')
+ );
+ $node->appendChild(
+ $doc->createElement($prefix . ':supported-collation','i;octet')
+ );
+ $node->appendChild(
+ $doc->createElement($prefix . ':supported-collation','i;unicode-casemap')
+ );
+
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php
new file mode 100644
index 0000000..5898917
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\CalDAV\Schedule;
+
+use OldSabre\VObject;
+use OldSabre\DAV;
+
+/**
+ * iMIP handler.
+ *
+ * This class is responsible for sending out iMIP messages. iMIP is the
+ * email-based transport for iTIP. iTIP deals with scheduling operations for
+ * iCalendar objects.
+ *
+ * If you want to customize the email that gets sent out, you can do so by
+ * extending this class and overriding the sendMessage method.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class IMip {
+
+ /**
+ * Email address used in From: header.
+ *
+ * @var string
+ */
+ protected $senderEmail;
+
+ /**
+ * Creates the email handler.
+ *
+ * @param string $senderEmail. The 'senderEmail' is the email that shows up
+ * in the 'From:' address. This should
+ * generally be some kind of no-reply email
+ * address you own.
+ */
+ public function __construct($senderEmail) {
+
+ $this->senderEmail = $senderEmail;
+
+ }
+
+ /**
+ * Sends one or more iTip messages through email.
+ *
+ * @param string $originator Originator Email
+ * @param array $recipients Array of email addresses
+ * @param VObject\Component $vObject
+ * @param string $principal Principal Url of the originator
+ * @return void
+ */
+ public function sendMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
+
+ foreach($recipients as $recipient) {
+
+ $to = $recipient;
+ $replyTo = $originator;
+ $subject = 'SabreDAV iTIP message';
+
+ switch(strtoupper($vObject->METHOD)) {
+ case 'REPLY' :
+ $subject = 'Response for: ' . $vObject->VEVENT->SUMMARY;
+ break;
+ case 'REQUEST' :
+ $subject = 'Invitation for: ' .$vObject->VEVENT->SUMMARY;
+ break;
+ case 'CANCEL' :
+ $subject = 'Cancelled event: ' . $vObject->VEVENT->SUMMARY;
+ break;
+ }
+
+ $headers = array();
+ $headers[] = 'Reply-To: ' . $replyTo;
+ $headers[] = 'From: ' . $this->senderEmail;
+ $headers[] = 'Content-Type: text/calendar; method=' . (string)$vObject->method . '; charset=utf-8';
+ if (DAV\Server::$exposeVersion) {
+ $headers[] = 'X-Sabre-Version: ' . DAV\Version::VERSION . '-' . DAV\Version::STABILITY;
+ }
+
+ $vcalBody = $vObject->serialize();
+
+ $this->mail($to, $subject, $vcalBody, $headers);
+
+ }
+
+ }
+
+ // @codeCoverageIgnoreStart
+ // This is deemed untestable in a reasonable manner
+
+ /**
+ * This function is reponsible for sending the actual email.
+ *
+ * @param string $to Recipient email address
+ * @param string $subject Subject of the email
+ * @param string $body iCalendar body
+ * @param array $headers List of headers
+ * @return void
+ */
+ protected function mail($to, $subject, $body, array $headers) {
+
+
+ mail($to, $subject, $body, implode("\r\n", $headers));
+
+ }
+
+ // @codeCoverageIgnoreEnd
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php
new file mode 100644
index 0000000..5040ac9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace OldSabre\CalDAV\Schedule;
+
+/**
+ * Implement this interface to have a node be recognized as a CalDAV scheduling
+ * outbox.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IOutbox extends \OldSabre\DAV\ICollection, \OldSabre\DAVACL\IACL {
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php
new file mode 100644
index 0000000..dc67766
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace OldSabre\CalDAV\Schedule;
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+use OldSabre\DAVACL;
+
+/**
+ * The CalDAV scheduling outbox
+ *
+ * The outbox is mainly used as an endpoint in the tree for a client to do
+ * free-busy requests. This functionality is completely handled by the
+ * Scheduling plugin, so this object is actually mostly static.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Outbox extends DAV\Collection implements IOutbox {
+
+ /**
+ * The principal Uri
+ *
+ * @var string
+ */
+ protected $principalUri;
+
+ /**
+ * Constructor
+ *
+ * @param string $principalUri
+ */
+ public function __construct($principalUri) {
+
+ $this->principalUri = $principalUri;
+
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return 'outbox';
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return \OldSabre\DAV\INode[]
+ */
+ public function getChildren() {
+
+ return array();
+
+ }
+
+ /**
+ * 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(
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\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() {
+
+ $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+ $default['aggregates'][] = array(
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+ );
+ $default['aggregates'][] = array(
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+ );
+
+ return $default;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php
new file mode 100644
index 0000000..2e8f440
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * This object represents a CalDAV calendar that can be shared with other
+ * users.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ShareableCalendar extends Calendar implements IShareableCalendar {
+
+ /**
+ * Updates the list of shares.
+ *
+ * The first array is a list of people that are to be added to the
+ * calendar.
+ *
+ * Every element in the add array has the following properties:
+ * * href - A url. Usually a mailto: address
+ * * commonName - Usually a first and last name, or false
+ * * summary - A description of the share, can also be false
+ * * readOnly - A boolean value
+ *
+ * Every element in the remove array is just the address string.
+ *
+ * @param array $add
+ * @param array $remove
+ * @return void
+ */
+ public function updateShares(array $add, array $remove) {
+
+ $this->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 OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php
new file mode 100644
index 0000000..5586419
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAVACL;
+
+/**
+ * This object represents a CalDAV calendar that is shared by a different user.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SharedCalendar extends Calendar implements ISharedCalendar {
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param array $calendarInfo
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
+
+ $required = array(
+ '{http://calendarserver.org/ns/}shared-url',
+ '{http://sabredav.org/ns}owner-principal',
+ '{http://sabredav.org/ns}read-only',
+ );
+ foreach($required as $r) {
+ if (!isset($calendarInfo[$r])) {
+ throw new \InvalidArgumentException('The ' . $r . ' property must be specified for SharedCalendar(s)');
+ }
+ }
+
+ parent::__construct($caldavBackend, $calendarInfo);
+
+ }
+
+ /**
+ * This method should return the url of the owners' copy of the shared
+ * calendar.
+ *
+ * @return string
+ */
+ public function getSharedUrl() {
+
+ return $this->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;
+
+ }
+
+ /**
+ * 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 OldSabre\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']);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php
new file mode 100644
index 0000000..92942c0
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php
@@ -0,0 +1,526 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+
+/**
+ * This plugin implements support for caldav sharing.
+ *
+ * This spec is defined at:
+ * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
+ *
+ * See:
+ * OldSabre\CalDAV\Backend\SharingSupport for all the documentation.
+ *
+ * Note: This feature is experimental, and may change in between different
+ * SabreDAV versions.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SharingPlugin extends DAV\ServerPlugin {
+
+ /**
+ * These are the various status constants used by sharing-messages.
+ */
+ const STATUS_ACCEPTED = 1;
+ const STATUS_DECLINED = 2;
+ const STATUS_DELETED = 3;
+ const STATUS_NORESPONSE = 4;
+ const STATUS_INVALID = 5;
+
+ /**
+ * Reference to SabreDAV server object.
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * This method should return a list of server-features.
+ *
+ * This is for example 'versioning' and is added to the DAV: header
+ * in an OPTIONS response.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('calendarserver-sharing');
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using OldSabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'caldav-sharing';
+
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by OldSabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $server->resourceTypeMapping['OldSabre\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared';
+
+ array_push(
+ $this->server->protectedProperties,
+ '{' . Plugin::NS_CALENDARSERVER . '}invite',
+ '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes',
+ '{' . 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 DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @return void
+ */
+ public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
+
+ if ($node instanceof IShareableCalendar) {
+ if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}invite'] =
+ new Property\Invite(
+ $node->getShares()
+ );
+
+ }
+
+ }
+
+ if ($node instanceof ISharedCalendar) {
+
+ if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}shared-url', $requestedProperties))!==false) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}shared-url'] =
+ new DAV\Property\Href(
+ $node->getSharedUrl()
+ );
+
+ }
+ // The 'invite' property is slightly different for the 'shared'
+ // instance of the calendar, as it also contains the owner
+ // information.
+ if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) {
+
+ unset($requestedProperties[$index]);
+
+ // Fetching owner information
+ $props = $this->server->getPropertiesForPath($node->getOwner(), array(
+ '{http://sabredav.org/ns}email-address',
+ '{DAV:}displayname',
+ ), 1);
+
+ $ownerInfo = array(
+ 'href' => $node->getOwner(),
+ );
+
+ if (isset($props[0][200])) {
+
+ // We're mapping the internal webdav properties to the
+ // elements caldav-sharing expects.
+ if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) {
+ $ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address'];
+ }
+ if (isset($props[0][200]['{DAV:}displayname'])) {
+ $ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname'];
+ }
+
+ }
+
+ $returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}invite'] =
+ new Property\Invite(
+ $node->getShares(),
+ $ownerInfo
+ );
+
+ }
+
+
+ }
+
+ }
+
+ /**
+ * This method is triggered *after* all properties have been retrieved.
+ * This allows us to inject the correct resourcetype for calendars that
+ * have been shared.
+ *
+ * @param string $path
+ * @param array $properties
+ * @param DAV\INode $node
+ * @return void
+ */
+ public function afterGetProperties($path, &$properties, DAV\INode $node) {
+
+ if ($node instanceof IShareableCalendar) {
+ if (isset($properties[200]['{DAV:}resourcetype'])) {
+ if (count($node->getShares())>0) {
+ $properties[200]['{DAV:}resourcetype']->add(
+ '{' . Plugin::NS_CALENDARSERVER . '}shared-owner'
+ );
+ }
+ }
+ $propName = '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes';
+ if (array_key_exists($propName, $properties[404])) {
+ unset($properties[404][$propName]);
+ $properties[200][$propName] = new 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 DAV\INode $node
+ * @return void
+ */
+ public function updateProperties(array &$mutations, array &$result, DAV\INode $node) {
+
+ if (!$node instanceof 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('{' . 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 (DAV\Exception\NotFound $e) {
+ return;
+ }
+
+ $requestBody = $this->server->httpRequest->getBody(true);
+
+ // If this request handler could not deal with this POST request, it
+ // will return 'null' and other plugins get a chance to handle the
+ // request.
+ //
+ // However, we already requested the full body. This is a problem,
+ // because a body can only be read once. This is why we preemptively
+ // re-populated the request body with the existing data.
+ $this->server->httpRequest->setBody($requestBody);
+
+ $dom = DAV\XMLUtil::loadDOMDocument($requestBody);
+
+ $documentType = DAV\XMLUtil::toClarkNotation($dom->firstChild);
+
+ switch($documentType) {
+
+ // Dealing with the 'share' document, which modified invitees on a
+ // calendar.
+ case '{' . Plugin::NS_CALENDARSERVER . '}share' :
+
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof 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 '{'. Plugin::NS_CALENDARSERVER.'}invite-reply' :
+
+ // This only works on the calendar-home-root node.
+ if (!$node instanceof 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 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 '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' :
+
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof 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 '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' :
+
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof 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', Plugin::NS_CALENDARSERVER);
+ $xpath->registerNamespace('d', 'urn: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', Plugin::NS_CALENDARSERVER);
+ $xpath->registerNamespace('d', 'urn:DAV');
+
+ $hostHref = $xpath->evaluate('string(cs:hosturl/d:href)');
+ if (!$hostHref) {
+ throw new DAV\Exception\BadRequest('The {' . 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/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php
new file mode 100644
index 0000000..a4fbc66
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php
@@ -0,0 +1,342 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * The UserCalenders class contains all calendars associated to one user
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class UserCalendars implements DAV\IExtendedCollection, DAVACL\IACL {
+
+ /**
+ * CalDAV backend
+ *
+ * @var OldSabre\CalDAV\Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * Principal information
+ *
+ * @var array
+ */
+ protected $principalInfo;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param mixed $userUri
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend, $principalInfo) {
+
+ $this->caldavBackend = $caldavBackend;
+ $this->principalInfo = $principalInfo;
+
+ }
+
+ /**
+ * Returns the name of this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ list(,$name) = DAV\URLUtil::splitPath($this->principalInfo['uri']);
+ return $name;
+
+ }
+
+ /**
+ * Updates the name of this object
+ *
+ * @param string $name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new DAV\Exception\Forbidden();
+
+ }
+
+ /**
+ * Deletes this object
+ *
+ * @return void
+ */
+ public function delete() {
+
+ throw new DAV\Exception\Forbidden();
+
+ }
+
+ /**
+ * Returns the last modification date
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Creates a new file under this object.
+ *
+ * This is currently not allowed
+ *
+ * @param string $filename
+ * @param resource $data
+ * @return void
+ */
+ public function createFile($filename, $data=null) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
+
+ }
+
+ /**
+ * Creates a new directory under this object.
+ *
+ * This is currently not allowed.
+ *
+ * @param string $filename
+ * @return void
+ */
+ public function createDirectory($filename) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
+
+ }
+
+ /**
+ * Returns a single calendar, by name
+ *
+ * @param string $name
+ * @todo needs optimizing
+ * @return Calendar
+ */
+ public function getChild($name) {
+
+ foreach($this->getChildren() as $child) {
+ if ($name==$child->getName())
+ return $child;
+
+ }
+ throw new DAV\Exception\NotFound('Calendar with name \'' . $name . '\' could not be found');
+
+ }
+
+ /**
+ * Checks if a calendar exists.
+ *
+ * @param string $name
+ * @todo needs optimizing
+ * @return bool
+ */
+ public function childExists($name) {
+
+ foreach($this->getChildren() as $child) {
+ if ($name==$child->getName())
+ return true;
+
+ }
+ return false;
+
+ }
+
+ /**
+ * Returns a list of calendars
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
+ $objs = array();
+ foreach($calendars as $calendar) {
+ if ($this->caldavBackend instanceof Backend\SharingSupport) {
+ if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
+ $objs[] = new SharedCalendar($this->caldavBackend, $calendar);
+ } else {
+ $objs[] = new ShareableCalendar($this->caldavBackend, $calendar);
+ }
+ } else {
+ $objs[] = new Calendar($this->caldavBackend, $calendar);
+ }
+ }
+ $objs[] = new Schedule\Outbox($this->principalInfo['uri']);
+
+ // We're adding a notifications node, if it's supported by the backend.
+ if ($this->caldavBackend instanceof Backend\NotificationSupport) {
+ $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
+ }
+ return $objs;
+
+ }
+
+ /**
+ * Creates a new calendar
+ *
+ * @param string $name
+ * @param array $resourceType
+ * @param array $properties
+ * @return void
+ */
+ public function createExtendedCollection($name, array $resourceType, array $properties) {
+
+ $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 DAV\Exception\InvalidResourceType('Unknown resourceType: ' . $rt);
+ }
+ }
+ if (!$isCalendar) {
+ throw new DAV\Exception\InvalidResourceType('You can only create calendars in this collection');
+ }
+ $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalInfo['uri'];
+
+ }
+
+ /**
+ * 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(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalInfo['uri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-read',
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\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;
+
+ }
+
+ /**
+ * 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 Backend\SharingSupport) {
+ throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.');
+ }
+
+ return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php
new file mode 100644
index 0000000..ead559a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * This class contains the OldSabre\CalDAV version constants.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.7';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php
new file mode 100644
index 0000000..250c0db
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php
@@ -0,0 +1,315 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * The AddressBook class represents a CardDAV addressbook, owned by a specific user
+ *
+ * The AddressBook can contain multiple vcards
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AddressBook extends DAV\Collection implements IAddressBook, DAV\IProperties, DAVACL\IACL {
+
+ /**
+ * This is an array with addressbook information
+ *
+ * @var array
+ */
+ protected $addressBookInfo;
+
+ /**
+ * CardDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $carddavBackend;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $carddavBackend
+ * @param array $addressBookInfo
+ */
+ public function __construct(Backend\BackendInterface $carddavBackend, array $addressBookInfo) {
+
+ $this->carddavBackend = $carddavBackend;
+ $this->addressBookInfo = $addressBookInfo;
+
+ }
+
+ /**
+ * Returns the name of the addressbook
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->addressBookInfo['uri'];
+
+ }
+
+ /**
+ * Returns a card
+ *
+ * @param string $name
+ * @return \ICard
+ */
+ public function getChild($name) {
+
+ $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'],$name);
+ if (!$obj) throw new DAV\Exception\NotFound('Card not found');
+ return new Card($this->carddavBackend,$this->addressBookInfo,$obj);
+
+ }
+
+ /**
+ * Returns the full list of cards
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $objs = $this->carddavBackend->getCards($this->addressBookInfo['id']);
+ $children = array();
+ foreach($objs as $obj) {
+ $children[] = new Card($this->carddavBackend,$this->addressBookInfo,$obj);
+ }
+ return $children;
+
+ }
+
+ /**
+ * Creates a new directory
+ *
+ * We actually block this, as subdirectories are not allowed in addressbooks.
+ *
+ * @param string $name
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating collections in addressbooks is not allowed');
+
+ }
+
+ /**
+ * Creates a new file
+ *
+ * The contents of the new file must be a valid VCARD.
+ *
+ * This method may return an ETag.
+ *
+ * @param string $name
+ * @param resource $vcardData
+ * @return string|null
+ */
+ public function createFile($name,$vcardData = null) {
+
+ if (is_resource($vcardData)) {
+ $vcardData = stream_get_contents($vcardData);
+ }
+ // Converting to UTF-8, if needed
+ $vcardData = DAV\StringUtil::ensureUTF8($vcardData);
+
+ return $this->carddavBackend->createCard($this->addressBookInfo['id'],$name,$vcardData);
+
+ }
+
+ /**
+ * Deletes the entire addressbook.
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']);
+
+ }
+
+ /**
+ * Renames the addressbook
+ *
+ * @param string $newName
+ * @return void
+ */
+ public function setName($newName) {
+
+ throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported');
+
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp.
+ *
+ * @return void
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Updates properties on this node,
+ *
+ * The properties array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => 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 array $mutations
+ * @return bool|array
+ */
+ public function updateProperties($mutations) {
+
+ return $this->carddavBackend->updateAddressBook($this->addressBookInfo['id'], $mutations);
+
+ }
+
+ /**
+ * Returns a list of properties for this nodes.
+ *
+ * The properties list is a list of propertynames the client requested,
+ * encoded in clark-notation {xmlnamespace}tagname
+ *
+ * If the array is empty, it means 'all properties' were requested.
+ *
+ * @param array $properties
+ * @return array
+ */
+ public function getProperties($properties) {
+
+ $response = array();
+ foreach($properties as $propertyName) {
+
+ if (isset($this->addressBookInfo[$propertyName])) {
+
+ $response[$propertyName] = $this->addressBookInfo[$propertyName];
+
+ }
+
+ }
+
+ return $response;
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->addressBookInfo['principaluri'];
+
+ }
+
+ /**
+ * 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(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php
new file mode 100644
index 0000000..e754147
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php
@@ -0,0 +1,221 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+
+/**
+ * Parses the addressbook-query report request body.
+ *
+ * Whoever designed this format, and the CalDAV equivalent even more so,
+ * has no feel for design.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AddressBookQueryParser {
+
+ const TEST_ANYOF = 'anyof';
+ const TEST_ALLOF = 'allof';
+
+ /**
+ * List of requested properties the client wanted
+ *
+ * @var array
+ */
+ public $requestedProperties;
+
+ /**
+ * The number of results the client wants
+ *
+ * null means it wasn't specified, which in most cases means 'all results'.
+ *
+ * @var int|null
+ */
+ public $limit;
+
+ /**
+ * List of property filters.
+ *
+ * @var array
+ */
+ public $filters;
+
+ /**
+ * Either TEST_ANYOF or TEST_ALLOF
+ *
+ * @var string
+ */
+ public $test;
+
+ /**
+ * DOM Document
+ *
+ * @var DOMDocument
+ */
+ protected $dom;
+
+ /**
+ * DOM XPath object
+ *
+ * @var DOMXPath
+ */
+ protected $xpath;
+
+ /**
+ * Creates the parser
+ *
+ * @param \DOMDocument $dom
+ */
+ public function __construct(\DOMDocument $dom) {
+
+ $this->dom = $dom;
+
+ $this->xpath = new \DOMXPath($dom);
+ $this->xpath->registerNameSpace('card',Plugin::NS_CARDDAV);
+
+ }
+
+ /**
+ * Parses the request.
+ *
+ * @return void
+ */
+ public function parse() {
+
+ $filterNode = null;
+
+ $limit = $this->xpath->evaluate('number(/card:addressbook-query/card:limit/card:nresults)');
+ if (is_nan($limit)) $limit = null;
+
+ $filter = $this->xpath->query('/card:addressbook-query/card:filter');
+
+ // According to the CardDAV spec there needs to be exactly 1 filter
+ // element. However, KDE 4.8.2 contains a bug that will encode 0 filter
+ // elements, so this is a workaround for that.
+ //
+ // See: https://bugs.kde.org/show_bug.cgi?id=300047
+ if ($filter->length === 0) {
+ $test = null;
+ $filter = null;
+ } elseif ($filter->length === 1) {
+ $filter = $filter->item(0);
+ $test = $this->xpath->evaluate('string(@test)', $filter);
+ } else {
+ throw new DAV\Exception\BadRequest('Only one filter element is allowed');
+ }
+
+ if (!$test) $test = self::TEST_ANYOF;
+ if ($test !== self::TEST_ANYOF && $test !== self::TEST_ALLOF) {
+ throw new DAV\Exception\BadRequest('The test attribute must either hold "anyof" or "allof"');
+ }
+
+ $propFilters = array();
+
+ $propFilterNodes = $this->xpath->query('card:prop-filter', $filter);
+ for($ii=0; $ii < $propFilterNodes->length; $ii++) {
+
+ $propFilters[] = $this->parsePropFilterNode($propFilterNodes->item($ii));
+
+
+ }
+
+ $this->filters = $propFilters;
+ $this->limit = $limit;
+ $this->requestedProperties = array_keys(DAV\XMLUtil::parseProperties($this->dom->firstChild));
+ $this->test = $test;
+
+ }
+
+ /**
+ * Parses the prop-filter xml element
+ *
+ * @param \DOMElement $propFilterNode
+ * @return array
+ */
+ protected function parsePropFilterNode(\DOMElement $propFilterNode) {
+
+ $propFilter = array();
+ $propFilter['name'] = $propFilterNode->getAttribute('name');
+ $propFilter['test'] = $propFilterNode->getAttribute('test');
+ if (!$propFilter['test']) $propFilter['test'] = 'anyof';
+
+ $propFilter['is-not-defined'] = $this->xpath->query('card:is-not-defined', $propFilterNode)->length>0;
+
+ $paramFilterNodes = $this->xpath->query('card:param-filter', $propFilterNode);
+
+ $propFilter['param-filters'] = array();
+
+
+ for($ii=0;$ii<$paramFilterNodes->length;$ii++) {
+
+ $propFilter['param-filters'][] = $this->parseParamFilterNode($paramFilterNodes->item($ii));
+
+ }
+ $propFilter['text-matches'] = array();
+ $textMatchNodes = $this->xpath->query('card:text-match', $propFilterNode);
+
+ for($ii=0;$ii<$textMatchNodes->length;$ii++) {
+
+ $propFilter['text-matches'][] = $this->parseTextMatchNode($textMatchNodes->item($ii));
+
+ }
+
+ return $propFilter;
+
+ }
+
+ /**
+ * Parses the param-filter element
+ *
+ * @param \DOMElement $paramFilterNode
+ * @return array
+ */
+ public function parseParamFilterNode(\DOMElement $paramFilterNode) {
+
+ $paramFilter = array();
+ $paramFilter['name'] = $paramFilterNode->getAttribute('name');
+ $paramFilter['is-not-defined'] = $this->xpath->query('card:is-not-defined', $paramFilterNode)->length>0;
+ $paramFilter['text-match'] = null;
+
+ $textMatch = $this->xpath->query('card:text-match', $paramFilterNode);
+ if ($textMatch->length>0) {
+ $paramFilter['text-match'] = $this->parseTextMatchNode($textMatch->item(0));
+ }
+
+ return $paramFilter;
+
+ }
+
+ /**
+ * Text match
+ *
+ * @param \DOMElement $textMatchNode
+ * @return array
+ */
+ public function parseTextMatchNode(\DOMElement $textMatchNode) {
+
+ $matchType = $textMatchNode->getAttribute('match-type');
+ if (!$matchType) $matchType = 'contains';
+
+ if (!in_array($matchType, array('contains', 'equals', 'starts-with', 'ends-with'))) {
+ throw new DAV\Exception\BadRequest('Unknown match-type: ' . $matchType);
+ }
+
+ $negateCondition = $textMatchNode->getAttribute('negate-condition');
+ $negateCondition = $negateCondition==='yes';
+ $collation = $textMatchNode->getAttribute('collation');
+ if (!$collation) $collation = 'i;unicode-casemap';
+
+ return array(
+ 'negate-condition' => $negateCondition,
+ 'collation' => $collation,
+ 'match-type' => $matchType,
+ 'value' => $textMatchNode->nodeValue
+ );
+
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php
new file mode 100644
index 0000000..65c2271
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAVACL;
+
+/**
+ * AddressBook rootnode
+ *
+ * This object lists a collection of users, which can contain addressbooks.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AddressBookRoot extends DAVACL\AbstractPrincipalCollection {
+
+ /**
+ * Principal Backend
+ *
+ * @var OldSabre\DAVACL\PrincipalBackend\BackendInteface
+ */
+ protected $principalBackend;
+
+ /**
+ * CardDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $carddavBackend;
+
+ /**
+ * Constructor
+ *
+ * This constructor needs both a principal and a carddav backend.
+ *
+ * By default this class will show a list of addressbook collections for
+ * principals in the 'principals' collection. If your main principals are
+ * actually located in a different path, use the $principalPrefix argument
+ * to override this.
+ *
+ * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
+ * @param Backend\BackendInterface $carddavBackend
+ * @param string $principalPrefix
+ */
+ public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend,Backend\BackendInterface $carddavBackend, $principalPrefix = 'principals') {
+
+ $this->carddavBackend = $carddavBackend;
+ parent::__construct($principalBackend, $principalPrefix);
+
+ }
+
+ /**
+ * Returns the name of the node
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return Plugin::ADDRESSBOOK_ROOT;
+
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principal
+ * @return \OldSabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal) {
+
+ return new UserAddressBooks($this->carddavBackend, $principal['uri']);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php
new file mode 100644
index 0000000..6b90e0e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace OldSabre\CardDAV\Backend;
+
+/**
+ * CardDAV abstract Backend
+ *
+ * This class serves as a base-class for addressbook backends
+ *
+ * This class doesn't do much, but it was added for consistency.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php
new file mode 100644
index 0000000..9f9e1fa
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace OldSabre\CardDAV\Backend;
+
+/**
+ * CardDAV Backend Interface
+ *
+ * Any CardDAV backend must implement this interface.
+ *
+ * Note that there are references to 'addressBookId' scattered throughout the
+ * class. The value of the addressBookId is completely up to you, it can be any
+ * arbitrary value you can use as an unique identifier.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Returns the list of addressbooks for a specific user.
+ *
+ * Every addressbook should have the following properties:
+ * id - an arbitrary unique id
+ * uri - the 'basename' part of the url
+ * principaluri - Same as the passed parameter
+ *
+ * Any additional clark-notation property may be passed besides this. Some
+ * common ones are :
+ * {DAV:}displayname
+ * {urn:ietf:params:xml:ns:carddav}addressbook-description
+ * {http://calendarserver.org/ns/}getctag
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getAddressBooksForUser($principalUri);
+
+ /**
+ * Updates an addressbook's properties
+ *
+ * See OldSabre\DAV\IProperties for a description of the mutations array, as
+ * well as the return value.
+ *
+ * @param mixed $addressBookId
+ * @param array $mutations
+ * @see OldSabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateAddressBook($addressBookId, array $mutations);
+
+ /**
+ * Creates a new address book
+ *
+ * @param string $principalUri
+ * @param string $url Just the 'basename' of the url.
+ * @param array $properties
+ * @return void
+ */
+ public function createAddressBook($principalUri, $url, array $properties);
+
+ /**
+ * Deletes an entire addressbook and all its contents
+ *
+ * @param mixed $addressBookId
+ * @return void
+ */
+ public function deleteAddressBook($addressBookId);
+
+ /**
+ * Returns all cards for a specific addressbook id.
+ *
+ * This method should return the following properties for each card:
+ * * carddata - raw vcard data
+ * * uri - Some unique url
+ * * lastmodified - A unix timestamp
+ *
+ * It's recommended to also return the following properties:
+ * * etag - A unique etag. This must change every time the card changes.
+ * * size - The size of the card in bytes.
+ *
+ * If these last two properties are provided, less time will be spent
+ * calculating them. If they are specified, you can also ommit carddata.
+ * This may speed up certain requests, especially with large cards.
+ *
+ * @param mixed $addressbookId
+ * @return array
+ */
+ public function getCards($addressbookId);
+
+ /**
+ * Returns a specfic card.
+ *
+ * The same set of properties must be returned as with getCards. The only
+ * exception is that 'carddata' is absolutely required.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return array
+ */
+ public function getCard($addressBookId, $cardUri);
+
+ /**
+ * Creates a new card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag is for the
+ * newly created resource, and must be enclosed with double quotes (that
+ * is, the string itself must contain the double quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function createCard($addressBookId, $cardUri, $cardData);
+
+ /**
+ * Updates a card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag should
+ * match that of the updated resource, and must be enclosed with double
+ * quotes (that is: the string itself must contain the actual quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function updateCard($addressBookId, $cardUri, $cardData);
+
+ /**
+ * Deletes a card
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return bool
+ */
+ public function deleteCard($addressBookId, $cardUri);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php
new file mode 100644
index 0000000..e20b980
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php
@@ -0,0 +1,333 @@
+<?php
+
+namespace OldSabre\CardDAV\Backend;
+
+use OldSabre\CardDAV;
+use OldSabre\DAV;
+
+/**
+ * PDO CardDAV backend
+ *
+ * This CardDAV backend uses PDO to store addressbooks
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend {
+
+ /**
+ * PDO connection
+ *
+ * @var PDO
+ */
+ protected $pdo;
+
+ /**
+ * The PDO table name used to store addressbooks
+ */
+ protected $addressBooksTableName;
+
+ /**
+ * The PDO table name used to store cards
+ */
+ protected $cardsTableName;
+
+ /**
+ * Sets up the object
+ *
+ * @param \PDO $pdo
+ * @param string $addressBooksTableName
+ * @param string $cardsTableName
+ */
+ public function __construct(\PDO $pdo, $addressBooksTableName = 'addressbooks', $cardsTableName = 'cards') {
+
+ $this->pdo = $pdo;
+ $this->addressBooksTableName = $addressBooksTableName;
+ $this->cardsTableName = $cardsTableName;
+
+ }
+
+ /**
+ * Returns the list of addressbooks for a specific user.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getAddressBooksForUser($principalUri) {
+
+ $stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, ctag FROM '.$this->addressBooksTableName.' WHERE principaluri = ?');
+ $stmt->execute(array($principalUri));
+
+ $addressBooks = array();
+
+ foreach($stmt->fetchAll() as $row) {
+
+ $addressBooks[] = array(
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{DAV:}displayname' => $row['displayname'],
+ '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
+ '{http://calendarserver.org/ns/}getctag' => $row['ctag'],
+ '{' . CardDAV\Plugin::NS_CARDDAV . '}supported-address-data' =>
+ new CardDAV\Property\SupportedAddressData(),
+ );
+
+ }
+
+ return $addressBooks;
+
+ }
+
+
+ /**
+ * Updates an addressbook's properties
+ *
+ * See OldSabre\DAV\IProperties for a description of the mutations array, as
+ * well as the return value.
+ *
+ * @param mixed $addressBookId
+ * @param array $mutations
+ * @see OldSabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateAddressBook($addressBookId, array $mutations) {
+
+ $updates = array();
+
+ foreach($mutations as $property=>$newValue) {
+
+ switch($property) {
+ case '{DAV:}displayname' :
+ $updates['displayname'] = $newValue;
+ break;
+ case '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' :
+ $updates['description'] = $newValue;
+ break;
+ default :
+ // If any unsupported values were being updated, we must
+ // let the entire request fail.
+ return false;
+ }
+
+ }
+
+ // No values are being updated?
+ if (!$updates) {
+ return false;
+ }
+
+ $query = 'UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 ';
+ foreach($updates as $key=>$value) {
+ $query.=', `' . $key . '` = :' . $key . ' ';
+ }
+ $query.=' WHERE id = :addressbookid';
+
+ $stmt = $this->pdo->prepare($query);
+ $updates['addressbookid'] = $addressBookId;
+
+ $stmt->execute($updates);
+
+ return true;
+
+ }
+
+ /**
+ * Creates a new address book
+ *
+ * @param string $principalUri
+ * @param string $url Just the 'basename' of the url.
+ * @param array $properties
+ * @return void
+ */
+ public function createAddressBook($principalUri, $url, array $properties) {
+
+ $values = array(
+ 'displayname' => null,
+ 'description' => null,
+ 'principaluri' => $principalUri,
+ 'uri' => $url,
+ );
+
+ foreach($properties as $property=>$newValue) {
+
+ switch($property) {
+ case '{DAV:}displayname' :
+ $values['displayname'] = $newValue;
+ break;
+ case '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' :
+ $values['description'] = $newValue;
+ break;
+ default :
+ throw new DAV\Exception\BadRequest('Unknown property: ' . $property);
+ }
+
+ }
+
+ $query = 'INSERT INTO ' . $this->addressBooksTableName . ' (uri, displayname, description, principaluri, ctag) VALUES (:uri, :displayname, :description, :principaluri, 1)';
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ }
+
+ /**
+ * Deletes an entire addressbook and all its contents
+ *
+ * @param int $addressBookId
+ * @return void
+ */
+ public function deleteAddressBook($addressBookId) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM ' . $this->cardsTableName . ' WHERE addressbookid = ?');
+ $stmt->execute(array($addressBookId));
+
+ $stmt = $this->pdo->prepare('DELETE FROM ' . $this->addressBooksTableName . ' WHERE id = ?');
+ $stmt->execute(array($addressBookId));
+
+ }
+
+ /**
+ * Returns all cards for a specific addressbook id.
+ *
+ * This method should return the following properties for each card:
+ * * carddata - raw vcard data
+ * * uri - Some unique url
+ * * lastmodified - A unix timestamp
+ *
+ * It's recommended to also return the following properties:
+ * * etag - A unique etag. This must change every time the card changes.
+ * * size - The size of the card in bytes.
+ *
+ * If these last two properties are provided, less time will be spent
+ * calculating them. If they are specified, you can also ommit carddata.
+ * This may speed up certain requests, especially with large cards.
+ *
+ * @param mixed $addressbookId
+ * @return array
+ */
+ public function getCards($addressbookId) {
+
+ $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified FROM ' . $this->cardsTableName . ' WHERE addressbookid = ?');
+ $stmt->execute(array($addressbookId));
+
+ return $stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+
+ }
+
+ /**
+ * Returns a specfic card.
+ *
+ * The same set of properties must be returned as with getCards. The only
+ * exception is that 'carddata' is absolutely required.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return array
+ */
+ public function getCard($addressBookId, $cardUri) {
+
+ $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified FROM ' . $this->cardsTableName . ' WHERE addressbookid = ? AND uri = ? LIMIT 1');
+ $stmt->execute(array($addressBookId, $cardUri));
+
+ $result = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+ return (count($result)>0?$result[0]:false);
+
+ }
+
+ /**
+ * Creates a new card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag is for the
+ * newly created resource, and must be enclosed with double quotes (that
+ * is, the string itself must contain the double quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function createCard($addressBookId, $cardUri, $cardData) {
+
+ $stmt = $this->pdo->prepare('INSERT INTO ' . $this->cardsTableName . ' (carddata, uri, lastmodified, addressbookid) VALUES (?, ?, ?, ?)');
+
+ $result = $stmt->execute(array($cardData, $cardUri, time(), $addressBookId));
+
+ $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt2->execute(array($addressBookId));
+
+ return '"' . md5($cardData) . '"';
+
+ }
+
+ /**
+ * Updates a card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag should
+ * match that of the updated resource, and must be enclosed with double
+ * quotes (that is: the string itself must contain the actual quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function updateCard($addressBookId, $cardUri, $cardData) {
+
+ $stmt = $this->pdo->prepare('UPDATE ' . $this->cardsTableName . ' SET carddata = ?, lastmodified = ? WHERE uri = ? AND addressbookid =?');
+ $stmt->execute(array($cardData, time(), $cardUri, $addressBookId));
+
+ $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt2->execute(array($addressBookId));
+
+ return '"' . md5($cardData) . '"';
+
+ }
+
+ /**
+ * Deletes a card
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return bool
+ */
+ public function deleteCard($addressBookId, $cardUri) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM ' . $this->cardsTableName . ' WHERE addressbookid = ? AND uri = ?');
+ $stmt->execute(array($addressBookId, $cardUri));
+
+ $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt2->execute(array($addressBookId));
+
+ return $stmt->rowCount()===1;
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php
new file mode 100644
index 0000000..08ab828
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php
@@ -0,0 +1,260 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAVACL;
+use OldSabre\DAV;
+
+
+/**
+ * The Card object represents a single Card from an addressbook
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Card extends DAV\File implements ICard, DAVACL\IACL {
+
+ /**
+ * CardDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $carddavBackend;
+
+ /**
+ * Array with information about this Card
+ *
+ * @var array
+ */
+ protected $cardData;
+
+ /**
+ * Array with information about the containing addressbook
+ *
+ * @var array
+ */
+ protected $addressBookInfo;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $carddavBackend
+ * @param array $addressBookInfo
+ * @param array $cardData
+ */
+ public function __construct(Backend\BackendInterface $carddavBackend,array $addressBookInfo,array $cardData) {
+
+ $this->carddavBackend = $carddavBackend;
+ $this->addressBookInfo = $addressBookInfo;
+ $this->cardData = $cardData;
+
+ }
+
+ /**
+ * Returns the uri for this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->cardData['uri'];
+
+ }
+
+ /**
+ * Returns the VCard-formatted object
+ *
+ * @return string
+ */
+ public function get() {
+
+ // Pre-populating 'carddata' is optional. If we don't yet have it
+ // already, we fetch it from the backend.
+ if (!isset($this->cardData['carddata'])) {
+ $this->cardData = $this->carddavBackend->getCard($this->addressBookInfo['id'], $this->cardData['uri']);
+ }
+ return $this->cardData['carddata'];
+
+ }
+
+ /**
+ * Updates the VCard-formatted object
+ *
+ * @param string $cardData
+ * @return string|null
+ */
+ public function put($cardData) {
+
+ if (is_resource($cardData))
+ $cardData = stream_get_contents($cardData);
+
+ // Converting to UTF-8, if needed
+ $cardData = DAV\StringUtil::ensureUTF8($cardData);
+
+ $etag = $this->carddavBackend->updateCard($this->addressBookInfo['id'],$this->cardData['uri'],$cardData);
+ $this->cardData['carddata'] = $cardData;
+ $this->cardData['etag'] = $etag;
+
+ return $etag;
+
+ }
+
+ /**
+ * Deletes the card
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->carddavBackend->deleteCard($this->addressBookInfo['id'],$this->cardData['uri']);
+
+ }
+
+ /**
+ * Returns the mime content-type
+ *
+ * @return string
+ */
+ public function getContentType() {
+
+ return 'text/x-vcard; charset=utf-8';
+
+ }
+
+ /**
+ * Returns an ETag for this object
+ *
+ * @return string
+ */
+ public function getETag() {
+
+ if (isset($this->cardData['etag'])) {
+ return $this->cardData['etag'];
+ } else {
+ $data = $this->get();
+ if (is_string($data)) {
+ return '"' . md5($data) . '"';
+ } else {
+ // We refuse to calculate the md5 if it's a stream.
+ return null;
+ }
+ }
+
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return isset($this->cardData['lastmodified'])?$this->cardData['lastmodified']:null;
+
+ }
+
+ /**
+ * Returns the size of this object in bytes
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ if (array_key_exists('size', $this->cardData)) {
+ return $this->cardData['size'];
+ } else {
+ return strlen($this->get());
+ }
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->addressBookInfo['principaluri'];
+
+ }
+
+ /**
+ * Returns a 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(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ),
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php
new file mode 100644
index 0000000..9670c85
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+
+/**
+ * AddressBook interface
+ *
+ * Implement this interface to allow a node to be recognized as an addressbook.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IAddressBook extends DAV\ICollection {
+
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php
new file mode 100644
index 0000000..9d8945f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+
+/**
+ * Card interface
+ *
+ * Extend the ICard interface to allow your custom nodes to be picked up as
+ * 'Cards'.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICard extends DAV\IFile {
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php
new file mode 100644
index 0000000..7d3a3bd
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+/**
+ * IDirectory interface
+ *
+ * Implement this interface to have an addressbook marked as a 'directory'. A
+ * directory is an (often) global addressbook.
+ *
+ * A full description can be found in the IETF draft:
+ * - draft-daboo-carddav-directory-gateway
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IDirectory extends IAddressBook {
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php
new file mode 100644
index 0000000..fa3077b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php
@@ -0,0 +1,706 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+use OldSabre\VObject;
+
+/**
+ * CardDAV plugin
+ *
+ * The CardDAV plugin adds CardDAV functionality to the WebDAV server
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * Url to the addressbooks
+ */
+ const ADDRESSBOOK_ROOT = 'addressbooks';
+
+ /**
+ * xml namespace for CardDAV elements
+ */
+ const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
+
+ /**
+ * Add urls to this property to have them automatically exposed as
+ * 'directories' to the user.
+ *
+ * @var array
+ */
+ public $directories = array();
+
+ /**
+ * Server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ /* Events */
+ $server->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'));
+ $server->subscribeEvent('onBrowserPostAction', array($this,'browserPostAction'));
+ $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent'));
+ $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile'));
+
+ /* Namespaces */
+ $server->xmlNamespaces[self::NS_CARDDAV] = 'card';
+
+ /* Mapping Interfaces to {DAV:}resourcetype values */
+ $server->resourceTypeMapping['OldSabre\\CardDAV\\IAddressBook'] = '{' . self::NS_CARDDAV . '}addressbook';
+ $server->resourceTypeMapping['OldSabre\\CardDAV\\IDirectory'] = '{' . self::NS_CARDDAV . '}directory';
+
+ /* Adding properties that may never be changed */
+ $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-address-data';
+ $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}max-resource-size';
+ $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}addressbook-home-set';
+ $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-collation-set';
+
+ $server->propertyMap['{http://calendarserver.org/ns/}me-card'] = 'OldSabre\\DAV\\Property\\Href';
+
+ $this->server = $server;
+
+ }
+
+ /**
+ * Returns a list of supported features.
+ *
+ * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('addressbook');
+
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+
+ $node = $this->server->tree->getNodeForPath($uri);
+ if ($node instanceof IAddressBook || $node instanceof ICard) {
+ return array(
+ '{' . self::NS_CARDDAV . '}addressbook-multiget',
+ '{' . self::NS_CARDDAV . '}addressbook-query',
+ );
+ }
+ return array();
+
+ }
+
+
+ /**
+ * Adds all CardDAV-specific properties
+ *
+ * @param string $path
+ * @param DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @return void
+ */
+ public function beforeGetProperties($path, DAV\INode $node, array &$requestedProperties, array &$returnedProperties) {
+
+ if ($node instanceof DAVACL\IPrincipal) {
+
+ // calendar-home-set property
+ $addHome = '{' . self::NS_CARDDAV . '}addressbook-home-set';
+ if (in_array($addHome,$requestedProperties)) {
+ $principalId = $node->getName();
+ $addressbookHomePath = self::ADDRESSBOOK_ROOT . '/' . $principalId . '/';
+ unset($requestedProperties[array_search($addHome, $requestedProperties)]);
+ $returnedProperties[200][$addHome] = new DAV\Property\Href($addressbookHomePath);
+ }
+
+ $directories = '{' . self::NS_CARDDAV . '}directory-gateway';
+ if ($this->directories && in_array($directories, $requestedProperties)) {
+ unset($requestedProperties[array_search($directories, $requestedProperties)]);
+ $returnedProperties[200][$directories] = new DAV\Property\HrefList($this->directories);
+ }
+
+ }
+
+ if ($node instanceof ICard) {
+
+ // The address-data property is not supposed to be a 'real'
+ // property, but in large chunks of the spec it does act as such.
+ // Therefore we simply expose it as a property.
+ $addressDataProp = '{' . self::NS_CARDDAV . '}address-data';
+ if (in_array($addressDataProp, $requestedProperties)) {
+ unset($requestedProperties[$addressDataProp]);
+ $val = $node->get();
+ if (is_resource($val))
+ $val = stream_get_contents($val);
+
+ $returnedProperties[200][$addressDataProp] = $val;
+
+ }
+ }
+
+ if ($node instanceof UserAddressBooks) {
+
+ $meCardProp = '{http://calendarserver.org/ns/}me-card';
+ if (in_array($meCardProp, $requestedProperties)) {
+
+ $props = $this->server->getProperties($node->getOwner(), array('{http://sabredav.org/ns}vcard-url'));
+ if (isset($props['{http://sabredav.org/ns}vcard-url'])) {
+
+ $returnedProperties[200][$meCardProp] = new DAV\Property\Href(
+ $props['{http://sabredav.org/ns}vcard-url']
+ );
+ $pos = array_search($meCardProp, $requestedProperties);
+ unset($requestedProperties[$pos]);
+
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * This event is triggered when a PROPPATCH method is executed
+ *
+ * @param array $mutations
+ * @param array $result
+ * @param DAV\INode $node
+ * @return bool
+ */
+ public function updateProperties(&$mutations, &$result, DAV\INode $node) {
+
+ if (!$node instanceof UserAddressBooks) {
+ return true;
+ }
+
+ $meCard = '{http://calendarserver.org/ns/}me-card';
+
+ // The only property we care about
+ if (!isset($mutations[$meCard]))
+ return true;
+
+ $value = $mutations[$meCard];
+ unset($mutations[$meCard]);
+
+ if ($value instanceof DAV\Property\IHref) {
+ $value = $value->getHref();
+ $value = $this->server->calculateUri($value);
+ } elseif (!is_null($value)) {
+ $result[400][$meCard] = null;
+ return false;
+ }
+
+ $innerResult = $this->server->updateProperties(
+ $node->getOwner(),
+ array(
+ '{http://sabredav.org/ns}vcard-url' => $value,
+ )
+ );
+
+ $closureResult = false;
+ foreach($innerResult as $status => $props) {
+ if (is_array($props) && array_key_exists('{http://sabredav.org/ns}vcard-url', $props)) {
+ $result[$status][$meCard] = null;
+ $closureResult = ($status>=200 && $status<300);
+ }
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * This functions handles REPORT requests specific to CardDAV
+ *
+ * @param string $reportName
+ * @param \DOMNode $dom
+ * @return bool
+ */
+ public function report($reportName,$dom) {
+
+ switch($reportName) {
+ case '{'.self::NS_CARDDAV.'}addressbook-multiget' :
+ $this->addressbookMultiGetReport($dom);
+ return false;
+ case '{'.self::NS_CARDDAV.'}addressbook-query' :
+ $this->addressBookQueryReport($dom);
+ return false;
+ default :
+ return;
+
+ }
+
+
+ }
+
+ /**
+ * This function handles the addressbook-multiget REPORT.
+ *
+ * This report is used by the client to fetch the content of a series
+ * of urls. Effectively avoiding a lot of redundant requests.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ public function addressbookMultiGetReport($dom) {
+
+ $properties = array_keys(DAV\XMLUtil::parseProperties($dom->firstChild));
+
+ $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
+ $propertyList = array();
+
+ foreach($hrefElems as $elem) {
+
+ $uri = $this->server->calculateUri($elem->nodeValue);
+ list($propertyList[]) = $this->server->getPropertiesForPath($uri,$properties);
+
+ }
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $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($propertyList, $prefer['return-minimal']));
+
+ }
+
+ /**
+ * This method is triggered before a file gets updated with new content.
+ *
+ * This plugin uses this method to ensure that Card nodes receive valid
+ * vcard data.
+ *
+ * @param string $path
+ * @param DAV\IFile $node
+ * @param resource $data
+ * @return void
+ */
+ public function beforeWriteContent($path, DAV\IFile $node, &$data) {
+
+ if (!$node instanceof ICard)
+ return;
+
+ $this->validateVCard($data);
+
+ }
+
+ /**
+ * This method is triggered before a new file is created.
+ *
+ * This plugin uses this method to ensure that Card nodes receive valid
+ * vcard data.
+ *
+ * @param string $path
+ * @param resource $data
+ * @param DAV\ICollection $parentNode
+ * @return void
+ */
+ public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode) {
+
+ if (!$parentNode instanceof IAddressBook)
+ return;
+
+ $this->validateVCard($data);
+
+ }
+
+ /**
+ * Checks if the submitted iCalendar data is in fact, valid.
+ *
+ * An exception is thrown if it's not.
+ *
+ * @param resource|string $data
+ * @return void
+ */
+ protected function validateVCard(&$data) {
+
+ // If it's a stream, we convert it to a string first.
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+
+ // Converting the data to unicode, if needed.
+ $data = DAV\StringUtil::ensureUTF8($data);
+
+ try {
+
+ $vobj = VObject\Reader::read($data);
+
+ } catch (VObject\ParseException $e) {
+
+ throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
+
+ }
+
+ if ($vobj->name !== 'VCARD') {
+ throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
+ }
+
+ if (!isset($vobj->UID)) {
+ // No UID in vcards is invalid, but we'll just add it in anyway.
+ $vobj->add('UID', DAV\UUIDUtil::getUUID());
+ $data = $vobj->serialize();
+ }
+
+ }
+
+
+ /**
+ * This function handles the addressbook-query REPORT
+ *
+ * This report is used by the client to filter an addressbook based on a
+ * complex query.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ protected function addressbookQueryReport($dom) {
+
+ $query = new AddressBookQueryParser($dom);
+ $query->parse();
+
+ $depth = $this->server->getHTTPDepth(0);
+
+ if ($depth==0) {
+ $candidateNodes = array(
+ $this->server->tree->getNodeForPath($this->server->getRequestUri())
+ );
+ } else {
+ $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
+ }
+
+ $validNodes = array();
+ foreach($candidateNodes as $node) {
+
+ if (!$node instanceof ICard)
+ continue;
+
+ $blob = $node->get();
+ if (is_resource($blob)) {
+ $blob = stream_get_contents($blob);
+ }
+
+ if (!$this->validateFilters($blob, $query->filters, $query->test)) {
+ continue;
+ }
+
+ $validNodes[] = $node;
+
+ if ($query->limit && $query->limit <= count($validNodes)) {
+ // We hit the maximum number of items, we can stop now.
+ break;
+ }
+
+ }
+
+ $result = array();
+ foreach($validNodes as $validNode) {
+
+ if ($depth==0) {
+ $href = $this->server->getRequestUri();
+ } else {
+ $href = $this->server->getRequestUri() . '/' . $validNode->getName();
+ }
+
+ list($result[]) = $this->server->getPropertiesForPath($href, $query->requestedProperties, 0);
+
+ }
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $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']));
+
+ }
+
+ /**
+ * Validates if a vcard makes it throught a list of filters.
+ *
+ * @param string $vcardData
+ * @param array $filters
+ * @param string $test anyof or allof (which means OR or AND)
+ * @return bool
+ */
+ public function validateFilters($vcardData, array $filters, $test) {
+
+ $vcard = VObject\Reader::read($vcardData);
+
+ if (!$filters) return true;
+
+ foreach($filters as $filter) {
+
+ $isDefined = isset($vcard->{$filter['name']});
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ $success = false;
+ } else {
+ $success = true;
+ }
+ } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
+
+ // We only need to check for existence
+ $success = $isDefined;
+
+ } else {
+
+ $vProperties = $vcard->select($filter['name']);
+
+ $results = array();
+ if ($filter['param-filters']) {
+ $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
+ }
+ if ($filter['text-matches']) {
+ $texts = array();
+ foreach($vProperties as $vProperty)
+ $texts[] = $vProperty->getValue();
+
+ $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
+ }
+
+ if (count($results)===1) {
+ $success = $results[0];
+ } else {
+ if ($filter['test'] === 'anyof') {
+ $success = $results[0] || $results[1];
+ } else {
+ $success = $results[0] && $results[1];
+ }
+ }
+
+ } // else
+
+ // There are two conditions where we can already determine whether
+ // or not this filter succeeds.
+ if ($test==='anyof' && $success) {
+ return true;
+ }
+ if ($test==='allof' && !$success) {
+ return false;
+ }
+
+ } // foreach
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return $test==='allof';
+
+ }
+
+ /**
+ * Validates if a param-filter can be applied to a specific property.
+ *
+ * @todo currently we're only validating the first parameter of the passed
+ * property. Any subsequence parameters with the same name are
+ * ignored.
+ * @param array $vProperties
+ * @param array $filters
+ * @param string $test
+ * @return bool
+ */
+ protected function validateParamFilters(array $vProperties, array $filters, $test) {
+
+ foreach($filters as $filter) {
+
+ $isDefined = false;
+ foreach($vProperties as $vProperty) {
+ $isDefined = isset($vProperty[$filter['name']]);
+ if ($isDefined) break;
+ }
+
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ $success = false;
+ } else {
+ $success = true;
+ }
+
+ // If there's no text-match, we can just check for existence
+ } elseif (!$filter['text-match'] || !$isDefined) {
+
+ $success = $isDefined;
+
+ } else {
+
+ $success = false;
+ foreach($vProperties as $vProperty) {
+ // If we got all the way here, we'll need to validate the
+ // text-match filter.
+ $success = DAV\StringUtil::textMatch($vProperty[$filter['name']]->getValue(), $filter['text-match']['value'], $filter['text-match']['collation'], $filter['text-match']['match-type']);
+ if ($success) break;
+ }
+ if ($filter['text-match']['negate-condition']) {
+ $success = !$success;
+ }
+
+ } // else
+
+ // There are two conditions where we can already determine whether
+ // or not this filter succeeds.
+ if ($test==='anyof' && $success) {
+ return true;
+ }
+ if ($test==='allof' && !$success) {
+ return false;
+ }
+
+ }
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return $test==='allof';
+
+ }
+
+ /**
+ * Validates if a text-filter can be applied to a specific property.
+ *
+ * @param array $texts
+ * @param array $filters
+ * @param string $test
+ * @return bool
+ */
+ protected function validateTextMatches(array $texts, array $filters, $test) {
+
+ foreach($filters as $filter) {
+
+ $success = false;
+ foreach($texts as $haystack) {
+ $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
+
+ // Breaking on the first match
+ if ($success) break;
+ }
+ if ($filter['negate-condition']) {
+ $success = !$success;
+ }
+
+ if ($success && $test==='anyof')
+ return true;
+
+ if (!$success && $test=='allof')
+ return false;
+
+
+ }
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return $test==='allof';
+
+ }
+
+ /**
+ * This event is triggered 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
+ * OldSabre\DAV\Browser\Plugin. This allows us to generate an interface users
+ * can use to create new calendars.
+ *
+ * @param DAV\INode $node
+ * @param string $output
+ * @return bool
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output) {
+
+ if (!$node instanceof UserAddressBooks)
+ return;
+
+ $output.= '<tr><td colspan="2"><form method="post" action="">
+ <h3>Create new address book</h3>
+ <input type="hidden" name="sabreAction" value="mkaddressbook" />
+ <label>Name (uri):</label> <input type="text" name="name" /><br />
+ <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
+ <input type="submit" value="create" />
+ </form>
+ </td></tr>';
+
+ return false;
+
+ }
+
+ /**
+ * This method allows us to intercept the 'mkcalendar' sabreAction. This
+ * action enables the user to create new calendars from the browser plugin.
+ *
+ * @param string $uri
+ * @param string $action
+ * @param array $postVars
+ * @return bool
+ */
+ public function browserPostAction($uri, $action, array $postVars) {
+
+ if ($action!=='mkaddressbook')
+ return;
+
+ $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:carddav}addressbook');
+ $properties = array();
+ if (isset($postVars['{DAV:}displayname'])) {
+ $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
+ }
+ $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
+ return false;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php
new file mode 100644
index 0000000..c27c678
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace OldSabre\CardDAV\Property;
+
+use OldSabre\DAV;
+use OldSabre\CardDAV;
+
+/**
+ * Supported-address-data property
+ *
+ * This property is a representation of the supported-address-data property
+ * in the CardDAV namespace.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedAddressData extends DAV\Property {
+
+ /**
+ * supported versions
+ *
+ * @var array
+ */
+ protected $supportedData = array();
+
+ /**
+ * Creates the property
+ *
+ * @param array|null $supportedData
+ */
+ public function __construct(array $supportedData = null) {
+
+ if (is_null($supportedData)) {
+ $supportedData = array(
+ array('contentType' => 'text/vcard', 'version' => '3.0'),
+ // array('contentType' => 'text/vcard', 'version' => '4.0'),
+ );
+ }
+
+ $this->supportedData = $supportedData;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ $prefix =
+ isset($server->xmlNamespaces[CardDAV\Plugin::NS_CARDDAV]) ?
+ $server->xmlNamespaces[CardDAV\Plugin::NS_CARDDAV] :
+ 'card';
+
+ foreach($this->supportedData as $supported) {
+
+ $caldata = $doc->createElementNS(CardDAV\Plugin::NS_CARDDAV, $prefix . ':address-data-type');
+ $caldata->setAttribute('content-type',$supported['contentType']);
+ $caldata->setAttribute('version',$supported['version']);
+ $node->appendChild($caldata);
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php
new file mode 100644
index 0000000..7f3ac6f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php
@@ -0,0 +1,260 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * UserAddressBooks class
+ *
+ * The UserAddressBooks collection contains a list of addressbooks associated with a user
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class UserAddressBooks extends DAV\Collection implements DAV\IExtendedCollection, DAVACL\IACL {
+
+ /**
+ * Principal uri
+ *
+ * @var array
+ */
+ protected $principalUri;
+
+ /**
+ * carddavBackend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $carddavBackend;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $carddavBackend
+ * @param string $principalUri
+ */
+ public function __construct(Backend\BackendInterface $carddavBackend, $principalUri) {
+
+ $this->carddavBackend = $carddavBackend;
+ $this->principalUri = $principalUri;
+
+ }
+
+ /**
+ * Returns the name of this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ list(,$name) = DAV\URLUtil::splitPath($this->principalUri);
+ return $name;
+
+ }
+
+ /**
+ * Updates the name of this object
+ *
+ * @param string $name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new DAV\Exception\MethodNotAllowed();
+
+ }
+
+ /**
+ * Deletes this object
+ *
+ * @return void
+ */
+ public function delete() {
+
+ throw new DAV\Exception\MethodNotAllowed();
+
+ }
+
+ /**
+ * Returns the last modification date
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Creates a new file under this object.
+ *
+ * This is currently not allowed
+ *
+ * @param string $filename
+ * @param resource $data
+ * @return void
+ */
+ public function createFile($filename, $data=null) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
+
+ }
+
+ /**
+ * Creates a new directory under this object.
+ *
+ * This is currently not allowed.
+ *
+ * @param string $filename
+ * @return void
+ */
+ public function createDirectory($filename) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
+
+ }
+
+ /**
+ * Returns a single calendar, by name
+ *
+ * @param string $name
+ * @todo needs optimizing
+ * @return \AddressBook
+ */
+ public function getChild($name) {
+
+ foreach($this->getChildren() as $child) {
+ if ($name==$child->getName())
+ return $child;
+
+ }
+ throw new DAV\Exception\NotFound('Addressbook with name \'' . $name . '\' could not be found');
+
+ }
+
+ /**
+ * Returns a list of addressbooks
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $addressbooks = $this->carddavBackend->getAddressbooksForUser($this->principalUri);
+ $objs = array();
+ foreach($addressbooks as $addressbook) {
+ $objs[] = new AddressBook($this->carddavBackend, $addressbook);
+ }
+ return $objs;
+
+ }
+
+ /**
+ * Creates a new addressbook
+ *
+ * @param string $name
+ * @param array $resourceType
+ * @param array $properties
+ * @return void
+ */
+ public function createExtendedCollection($name, array $resourceType, array $properties) {
+
+ if (!in_array('{'.Plugin::NS_CARDDAV.'}addressbook',$resourceType) || count($resourceType)!==2) {
+ throw new DAV\Exception\InvalidResourceType('Unknown resourceType for this collection');
+ }
+ $this->carddavBackend->createAddressBook($this->principalUri, $name, $properties);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalUri;
+
+ }
+
+ /**
+ * 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(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalUri,
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalUri,
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php
new file mode 100644
index 0000000..bfd3790
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+use OldSabre\VObject;
+
+/**
+ * VCF Exporter
+ *
+ * This plugin adds the ability to export entire address books as .vcf files.
+ * This is useful for clients that don't support CardDAV yet. They often do
+ * support vcf files.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @author Thomas Tanghus (http://tanghus.net/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VCFExportPlugin extends DAV\ServerPlugin {
+
+ /**
+ * Reference to Server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin and registers event handlers
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->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 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(
+ '{' . 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]['{' . Plugin::NS_CARDDAV . '}address-data'])) {
+ continue;
+ }
+ $nodeData = $node[200]['{' . 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/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php
new file mode 100644
index 0000000..9da06a8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+/**
+ * Version Class
+ *
+ * This class contains the OldSabre\CardDAV version information
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.7';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php
new file mode 100644
index 0000000..986bc04
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+use OldSabre\DAV;
+use OldSabre\HTTP;
+
+/**
+ * HTTP Basic authentication backend class
+ *
+ * This class can be used by authentication objects wishing to use HTTP Basic
+ * Most of the digest logic is handled, implementors just need to worry about
+ * the validateUserPass method.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author James David Low (http://jameslow.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBasic implements BackendInterface {
+
+ /**
+ * This variable holds the currently logged in username.
+ *
+ * @var string|null
+ */
+ protected $currentUser;
+
+ /**
+ * Validates a username and password
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ * @return bool
+ */
+ abstract protected function validateUserPass($username, $password);
+
+ /**
+ * Returns information about the currently logged in username.
+ *
+ * If nobody is currently logged in, this method should return null.
+ *
+ * @return string|null
+ */
+ public function getCurrentUser() {
+ return $this->currentUser;
+ }
+
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param DAV\Server $server
+ * @param string $realm
+ * @throws DAV\Exception\NotAuthenticated
+ * @return bool
+ */
+ public function authenticate(DAV\Server $server, $realm) {
+
+ $auth = new HTTP\BasicAuth();
+ $auth->setHTTPRequest($server->httpRequest);
+ $auth->setHTTPResponse($server->httpResponse);
+ $auth->setRealm($realm);
+ $userpass = $auth->getUserPass();
+ if (!$userpass) {
+ $auth->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('No basic authentication headers were found');
+ }
+
+ // Authenticates the user
+ if (!$this->validateUserPass($userpass[0],$userpass[1])) {
+ $auth->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('Username or password does not match');
+ }
+ $this->currentUser = $userpass[0];
+ return true;
+ }
+
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php
new file mode 100644
index 0000000..9513493
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+use OldSabre\HTTP;
+use OldSabre\DAV;
+
+/**
+ * HTTP Digest authentication backend class
+ *
+ * This class can be used by authentication objects wishing to use HTTP Digest
+ * Most of the digest logic is handled, implementors just need to worry about
+ * the getDigestHash method
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractDigest implements BackendInterface {
+
+ /**
+ * This variable holds the currently logged in username.
+ *
+ * @var array|null
+ */
+ protected $currentUser;
+
+ /**
+ * Returns a users digest hash based on the username and realm.
+ *
+ * If the user was not known, null must be returned.
+ *
+ * @param string $realm
+ * @param string $username
+ * @return string|null
+ */
+ abstract public function getDigestHash($realm, $username);
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param DAV\Server $server
+ * @param string $realm
+ * @throws DAV\Exception\NotAuthenticated
+ * @return bool
+ */
+ public function authenticate(DAV\Server $server, $realm) {
+
+ $digest = new HTTP\DigestAuth();
+
+ // Hooking up request and response objects
+ $digest->setHTTPRequest($server->httpRequest);
+ $digest->setHTTPResponse($server->httpResponse);
+
+ $digest->setRealm($realm);
+ $digest->init();
+
+ $username = $digest->getUsername();
+
+ // No username was given
+ if (!$username) {
+ $digest->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('No digest authentication headers were found');
+ }
+
+ $hash = $this->getDigestHash($realm, $username);
+ // If this was false, the user account didn't exist
+ if ($hash===false || is_null($hash)) {
+ $digest->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('The supplied username was not on file');
+ }
+ if (!is_string($hash)) {
+ throw new DAV\Exception('The returned value from getDigestHash must be a string or null');
+ }
+
+ // If this was false, the password or part of the hash was incorrect.
+ if (!$digest->validateA1($hash)) {
+ $digest->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('Incorrect username');
+ }
+
+ $this->currentUser = $username;
+ return true;
+
+ }
+
+ /**
+ * Returns the currently logged in username.
+ *
+ * @return string|null
+ */
+ public function getCurrentUser() {
+
+ return $this->currentUser;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php
new file mode 100644
index 0000000..79d94af
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+use OldSabre\DAV;
+
+/**
+ * Apache authenticator
+ *
+ * This authentication backend assumes that authentication has been
+ * configured in apache, rather than within SabreDAV.
+ *
+ * Make sure apache is properly configured for this to work.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Apache implements BackendInterface {
+
+ /**
+ * Current apache user
+ *
+ * @var string
+ */
+ protected $remoteUser;
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param DAV\Server $server
+ * @param string $realm
+ * @return bool
+ */
+ public function authenticate(DAV\Server $server, $realm) {
+
+ $remoteUser = $server->httpRequest->getRawServerValue('REMOTE_USER');
+ if (is_null($remoteUser)) {
+ throw new DAV\Exception('We did not receive the $_SERVER[REMOTE_USER] property. This means that apache might have been misconfigured');
+ }
+
+ $this->remoteUser = $remoteUser;
+ return true;
+
+ }
+
+ /**
+ * Returns information about the currently logged in user.
+ *
+ * If nobody is currently logged in, this method should return null.
+ *
+ * @return array|null
+ */
+ public function getCurrentUser() {
+
+ return $this->remoteUser;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php
new file mode 100644
index 0000000..91241ab
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+/**
+ * This is the base class for any authentication object.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param \OldSabre\DAV\Server $server
+ * @param string $realm
+ * @return bool
+ */
+ function authenticate(\OldSabre\DAV\Server $server,$realm);
+
+ /**
+ * Returns information about the currently logged in username.
+ *
+ * If nobody is currently logged in, this method should return null.
+ *
+ * @return string|null
+ */
+ function getCurrentUser();
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php
new file mode 100644
index 0000000..8710832
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+use OldSabre\DAV;
+
+/**
+ * This is an authentication backend that uses a file to manage passwords.
+ *
+ * The backend file must conform to Apache's htdigest format
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class File extends AbstractDigest {
+
+ /**
+ * List of users
+ *
+ * @var array
+ */
+ protected $users = array();
+
+ /**
+ * Creates the backend object.
+ *
+ * If the filename argument is passed in, it will parse out the specified file fist.
+ *
+ * @param string|null $filename
+ */
+ public function __construct($filename=null) {
+
+ if (!is_null($filename))
+ $this->loadFile($filename);
+
+ }
+
+ /**
+ * Loads an htdigest-formatted file. This method can be called multiple times if
+ * more than 1 file is used.
+ *
+ * @param string $filename
+ * @return void
+ */
+ public function loadFile($filename) {
+
+ foreach(file($filename,FILE_IGNORE_NEW_LINES) as $line) {
+
+ if (substr_count($line, ":") !== 2)
+ throw new DAV\Exception('Malformed htdigest file. Every line should contain 2 colons');
+
+ list($username,$realm,$A1) = explode(':',$line);
+
+ if (!preg_match('/^[a-zA-Z0-9]{32}$/', $A1))
+ throw new DAV\Exception('Malformed htdigest file. Invalid md5 hash');
+
+ $this->users[$realm . ':' . $username] = $A1;
+
+ }
+
+ }
+
+ /**
+ * Returns a users' information
+ *
+ * @param string $realm
+ * @param string $username
+ * @return string
+ */
+ public function getDigestHash($realm, $username) {
+
+ return isset($this->users[$realm . ':' . $username])?$this->users[$realm . ':' . $username]:false;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php
new file mode 100644
index 0000000..0b8b2be
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+/**
+ * This is an authentication backend that uses a file to manage passwords.
+ *
+ * The backend file must conform to Apache's htdigest format
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractDigest {
+
+ /**
+ * Reference to PDO connection
+ *
+ * @var PDO
+ */
+ protected $pdo;
+
+ /**
+ * PDO table name we'll be using
+ *
+ * @var string
+ */
+ protected $tableName;
+
+
+ /**
+ * Creates the backend object.
+ *
+ * If the filename argument is passed in, it will parse out the specified file fist.
+ *
+ * @param PDO $pdo
+ * @param string $tableName The PDO table name to use
+ */
+ public function __construct(\PDO $pdo, $tableName = 'users') {
+
+ $this->pdo = $pdo;
+ $this->tableName = $tableName;
+
+ }
+
+ /**
+ * Returns the digest hash for a user.
+ *
+ * @param string $realm
+ * @param string $username
+ * @return string|null
+ */
+ public function getDigestHash($realm,$username) {
+
+ $stmt = $this->pdo->prepare('SELECT username, digesta1 FROM '.$this->tableName.' WHERE username = ?');
+ $stmt->execute(array($username));
+ $result = $stmt->fetchAll();
+
+ if (!count($result)) return;
+
+ return $result[0]['digesta1'];
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php
new file mode 100644
index 0000000..17106cb
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace OldSabre\DAV\Auth;
+use OldSabre\DAV;
+
+/**
+ * This plugin provides Authentication for a WebDAV server.
+ *
+ * It relies on a Backend object, which provides user information.
+ *
+ * Additionally, it provides support for:
+ * * {DAV:}current-user-principal property from RFC5397
+ * * {DAV:}principal-collection-set property from RFC3744
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * Reference to main server object
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Authentication backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $authBackend;
+
+ /**
+ * The authentication realm.
+ *
+ * @var string
+ */
+ private $realm;
+
+ /**
+ * __construct
+ *
+ * @param Backend\BackendInterface $authBackend
+ * @param string $realm
+ */
+ public function __construct(Backend\BackendInterface $authBackend, $realm) {
+
+ $this->authBackend = $authBackend;
+ $this->realm = $realm;
+
+ }
+
+ /**
+ * Initializes the plugin. This function is automatically called by the server
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),10);
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'auth';
+
+ }
+
+ /**
+ * Returns the current users' principal uri.
+ *
+ * If nobody is logged in, this will return null.
+ *
+ * @return string|null
+ */
+ public function getCurrentUser() {
+
+ $userInfo = $this->authBackend->getCurrentUser();
+ if (!$userInfo) return null;
+
+ return $userInfo;
+
+ }
+
+ /**
+ * This method is called before any HTTP method and forces users to be authenticated
+ *
+ * @param string $method
+ * @param string $uri
+ * @throws OldSabre\DAV\Exception\NotAuthenticated
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ $this->authBackend->authenticate($this->server,$this->realm);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php
new file mode 100644
index 0000000..050a243
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace OldSabre\DAV\Browser;
+
+use OldSabre\DAV;
+
+/**
+ * GuessContentType plugin
+ *
+ * A lot of the built-in File objects just return application/octet-stream
+ * as a content-type by default. This is a problem for some clients, because
+ * they expect a correct contenttype.
+ *
+ * There's really no accurate, fast and portable way to determine the contenttype
+ * so this extension does what the rest of the world does, and guesses it based
+ * on the file extension.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class GuessContentType extends DAV\ServerPlugin {
+
+ /**
+ * List of recognized file extensions
+ *
+ * Feel free to add more
+ *
+ * @var array
+ */
+ public $extensionMap = array(
+
+ // images
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+
+ // groupware
+ 'ics' => 'text/calendar',
+ 'vcf' => 'text/x-vcard',
+
+ // text
+ 'txt' => 'text/plain',
+
+ );
+
+ /**
+ * Initializes the plugin
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ // Using a relatively low priority (200) to allow other extensions
+ // to set the content-type first.
+ $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties'),200);
+
+ }
+
+ /**
+ * Handler for teh afterGetProperties event
+ *
+ * @param string $path
+ * @param array $properties
+ * @return void
+ */
+ public function afterGetProperties($path, &$properties) {
+
+ if (array_key_exists('{DAV:}getcontenttype', $properties[404])) {
+
+ list(, $fileName) = DAV\URLUtil::splitPath($path);
+ $contentType = $this->getContentType($fileName);
+
+ if ($contentType) {
+ $properties[200]['{DAV:}getcontenttype'] = $contentType;
+ unset($properties[404]['{DAV:}getcontenttype']);
+ }
+
+ }
+
+ }
+
+ /**
+ * Simple method to return the contenttype
+ *
+ * @param string $fileName
+ * @return string
+ */
+ protected function getContentType($fileName) {
+
+ // Just grabbing the extension
+ $extension = strtolower(substr($fileName,strrpos($fileName,'.')+1));
+ if (isset($this->extensionMap[$extension]))
+ return $this->extensionMap[$extension];
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php
new file mode 100644
index 0000000..5d6e3c8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace OldSabre\DAV\Browser;
+
+use OldSabre\DAV;
+
+/**
+ * This is a simple plugin that will map any GET request for non-files to
+ * PROPFIND allprops-requests.
+ *
+ * This should allow easy debugging of PROPFIND
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class MapGetToPropFind extends DAV\ServerPlugin {
+
+ /**
+ * reference to server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin and subscribes to events
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'httpGetInterceptor'));
+ }
+
+ /**
+ * This method intercepts GET requests to non-files, and changes it into an HTTP PROPFIND request
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function httpGetInterceptor($method, $uri) {
+
+ if ($method!='GET') return true;
+
+ $node = $this->server->tree->getNodeForPath($uri);
+ if ($node instanceof DAV\IFile) return;
+
+ $this->server->invokeMethod('PROPFIND',$uri);
+ return false;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php
new file mode 100644
index 0000000..7a2319b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php
@@ -0,0 +1,491 @@
+<?php
+
+namespace OldSabre\DAV\Browser;
+
+use OldSabre\DAV;
+
+/**
+ * Browser Plugin
+ *
+ * This plugin provides a html representation, so that a WebDAV server may be accessed
+ * using a browser.
+ *
+ * The class intercepts GET requests to collection resources and generates a simple
+ * html index.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * List of default icons for nodes.
+ *
+ * This is an array with class / interface names as keys, and asset names
+ * as values.
+ *
+ * The evaluation order is reversed. The last item in the list gets
+ * precendence.
+ *
+ * @var array
+ */
+ public $iconMap = array(
+ 'OldSabre\\DAV\\IFile' => 'icons/file',
+ 'OldSabre\\DAV\\ICollection' => 'icons/collection',
+ 'OldSabre\\DAVACL\\IPrincipal' => 'icons/principal',
+ 'OldSabre\\CalDAV\\ICalendar' => 'icons/calendar',
+ 'OldSabre\\CardDAV\\IAddressBook' => 'icons/addressbook',
+ 'OldSabre\\CardDAV\\ICard' => 'icons/card',
+ );
+
+ /**
+ * The file extension used for all icons
+ *
+ * @var string
+ */
+ public $iconExtension = '.png';
+
+ /**
+ * reference to server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * enablePost turns on the 'actions' panel, which allows people to create
+ * folders and upload files straight from a browser.
+ *
+ * @var bool
+ */
+ protected $enablePost = true;
+
+ /**
+ * By default the browser plugin will generate a favicon and other images.
+ * To turn this off, set this property to false.
+ *
+ * @var bool
+ */
+ protected $enableAssets = true;
+
+ /**
+ * Creates the object.
+ *
+ * By default it will allow file creation and uploads.
+ * Specify the first argument as false to disable this
+ *
+ * @param bool $enablePost
+ * @param bool $enableAssets
+ */
+ public function __construct($enablePost=true, $enableAssets = true) {
+
+ $this->enablePost = $enablePost;
+ $this->enableAssets = $enableAssets;
+
+ }
+
+ /**
+ * Initializes the plugin and subscribes to events
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'httpGetInterceptor'));
+ $this->server->subscribeEvent('onHTMLActionsPanel', array($this, 'htmlActionsPanel'),200);
+ if ($this->enablePost) $this->server->subscribeEvent('unknownMethod',array($this,'httpPOSTHandler'));
+ }
+
+ /**
+ * This method intercepts GET requests to collections and returns the html
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function httpGetInterceptor($method, $uri) {
+
+ if ($method !== 'GET') return true;
+
+ // We're not using straight-up $_GET, because we want everything to be
+ // unit testable.
+ $getVars = array();
+ parse_str($this->server->httpRequest->getQueryString(), $getVars);
+
+ if (isset($getVars['sabreAction']) && $getVars['sabreAction'] === 'asset' && isset($getVars['assetName'])) {
+ $this->serveAsset($getVars['assetName']);
+ return false;
+ }
+
+ try {
+ $node = $this->server->tree->getNodeForPath($uri);
+ } catch (DAV\Exception\NotFound $e) {
+ // We're simply stopping when the file isn't found to not interfere
+ // with other plugins.
+ return;
+ }
+ if ($node instanceof DAV\IFile)
+ return;
+
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type','text/html; charset=utf-8');
+
+ $this->server->httpResponse->sendBody(
+ $this->generateDirectoryIndex($uri)
+ );
+
+ return false;
+
+ }
+
+ /**
+ * Handles POST requests for tree operations.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function httpPOSTHandler($method, $uri) {
+
+ if ($method!='POST') return;
+ $contentType = $this->server->httpRequest->getHeader('Content-Type');
+ list($contentType) = explode(';', $contentType);
+ if ($contentType !== 'application/x-www-form-urlencoded' &&
+ $contentType !== 'multipart/form-data') {
+ return;
+ }
+ $postVars = $this->server->httpRequest->getPostVars();
+
+ if (!isset($postVars['sabreAction']))
+ return;
+
+ if ($this->server->broadcastEvent('onBrowserPostAction', array($uri, $postVars['sabreAction'], $postVars))) {
+
+ switch($postVars['sabreAction']) {
+
+ case 'mkcol' :
+ if (isset($postVars['name']) && trim($postVars['name'])) {
+ // Using basename() because we won't allow slashes
+ list(, $folderName) = DAV\URLUtil::splitPath(trim($postVars['name']));
+ $this->server->createDirectory($uri . '/' . $folderName);
+ }
+ break;
+ case 'put' :
+ if ($_FILES) $file = current($_FILES);
+ else break;
+
+ list(, $newName) = DAV\URLUtil::splitPath(trim($file['name']));
+ if (isset($postVars['name']) && trim($postVars['name']))
+ $newName = trim($postVars['name']);
+
+ // Making sure we only have a 'basename' component
+ list(, $newName) = DAV\URLUtil::splitPath($newName);
+
+ if (is_uploaded_file($file['tmp_name'])) {
+ $this->server->createFile($uri . '/' . $newName, fopen($file['tmp_name'],'r'));
+ }
+ break;
+
+ }
+
+ }
+ $this->server->httpResponse->setHeader('Location',$this->server->httpRequest->getUri());
+ $this->server->httpResponse->sendStatus(302);
+ return false;
+
+ }
+
+ /**
+ * Escapes a string for html.
+ *
+ * @param string $value
+ * @return string
+ */
+ public function escapeHTML($value) {
+
+ return htmlspecialchars($value,ENT_QUOTES,'UTF-8');
+
+ }
+
+ /**
+ * Generates the html directory index for a given url
+ *
+ * @param string $path
+ * @return string
+ */
+ public function generateDirectoryIndex($path) {
+
+ $version = '';
+ if (DAV\Server::$exposeVersion) {
+ $version = DAV\Version::VERSION ."-". DAV\Version::STABILITY;
+ }
+
+ $html = "<html>
+<head>
+ <title>Index for " . $this->escapeHTML($path) . "/ - SabreDAV " . $version . "</title>
+ <style type=\"text/css\">
+ body { Font-family: arial}
+ h1 { font-size: 150% }
+ </style>
+ ";
+
+ if ($this->enableAssets) {
+ $html.='<link rel="shortcut icon" href="'.$this->getAssetUrl('favicon.ico').'" type="image/vnd.microsoft.icon" />';
+ }
+
+ $html .= "</head>
+<body>
+ <h1>Index for " . $this->escapeHTML($path) . "/</h1>
+ <table>
+ <tr><th width=\"24\"></th><th>Name</th><th>Type</th><th>Size</th><th>Last modified</th></tr>
+ <tr><td colspan=\"5\"><hr /></td></tr>";
+
+ $files = $this->server->getPropertiesForPath($path,array(
+ '{DAV:}displayname',
+ '{DAV:}resourcetype',
+ '{DAV:}getcontenttype',
+ '{DAV:}getcontentlength',
+ '{DAV:}getlastmodified',
+ ),1);
+
+ $parent = $this->server->tree->getNodeForPath($path);
+
+
+ if ($path) {
+
+ list($parentUri) = DAV\URLUtil::splitPath($path);
+ $fullPath = DAV\URLUtil::encodePath($this->server->getBaseUri() . $parentUri);
+
+ $icon = $this->enableAssets?'<a href="' . $fullPath . '"><img src="' . $this->getAssetUrl('icons/parent' . $this->iconExtension) . '" width="24" alt="Parent" /></a>':'';
+ $html.= "<tr>
+ <td>$icon</td>
+ <td><a href=\"{$fullPath}\">..</a></td>
+ <td>[parent]</td>
+ <td></td>
+ <td></td>
+ </tr>";
+
+ }
+
+ foreach($files as $file) {
+
+ // This is the current directory, we can skip it
+ if (rtrim($file['href'],'/')==$path) continue;
+
+ list(, $name) = DAV\URLUtil::splitPath($file['href']);
+
+ $type = null;
+
+
+ if (isset($file[200]['{DAV:}resourcetype'])) {
+ $type = $file[200]['{DAV:}resourcetype']->getValue();
+
+ // resourcetype can have multiple values
+ if (!is_array($type)) $type = array($type);
+
+ foreach($type as $k=>$v) {
+
+ // Some name mapping is preferred
+ switch($v) {
+ case '{DAV:}collection' :
+ $type[$k] = 'Collection';
+ break;
+ case '{DAV:}principal' :
+ $type[$k] = 'Principal';
+ break;
+ case '{urn:ietf:params:xml:ns:carddav}addressbook' :
+ $type[$k] = 'Addressbook';
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}calendar' :
+ $type[$k] = 'Calendar';
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}schedule-inbox' :
+ $type[$k] = 'Schedule Inbox';
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}schedule-outbox' :
+ $type[$k] = 'Schedule Outbox';
+ break;
+ case '{http://calendarserver.org/ns/}calendar-proxy-read' :
+ $type[$k] = 'Proxy-Read';
+ break;
+ case '{http://calendarserver.org/ns/}calendar-proxy-write' :
+ $type[$k] = 'Proxy-Write';
+ break;
+ }
+
+ }
+ $type = implode(', ', $type);
+ }
+
+ // If no resourcetype was found, we attempt to use
+ // the contenttype property
+ if (!$type && isset($file[200]['{DAV:}getcontenttype'])) {
+ $type = $file[200]['{DAV:}getcontenttype'];
+ }
+ if (!$type) $type = 'Unknown';
+
+ $size = isset($file[200]['{DAV:}getcontentlength'])?(int)$file[200]['{DAV:}getcontentlength']:'';
+ $lastmodified = isset($file[200]['{DAV:}getlastmodified'])?$file[200]['{DAV:}getlastmodified']->getTime()->format(\DateTime::ATOM):'';
+
+ $fullPath = DAV\URLUtil::encodePath('/' . trim($this->server->getBaseUri() . ($path?$path . '/':'') . $name,'/'));
+
+ $displayName = isset($file[200]['{DAV:}displayname'])?$file[200]['{DAV:}displayname']:$name;
+
+ $displayName = $this->escapeHTML($displayName);
+ $type = $this->escapeHTML($type);
+
+ $icon = '';
+
+ if ($this->enableAssets) {
+ $node = $this->server->tree->getNodeForPath(($path?$path.'/':'') . $name);
+ foreach(array_reverse($this->iconMap) as $class=>$iconName) {
+
+ if ($node instanceof $class) {
+ $icon = '<a href="' . $fullPath . '"><img src="' . $this->getAssetUrl($iconName . $this->iconExtension) . '" alt="" width="24" /></a>';
+ break;
+ }
+
+
+ }
+
+ }
+
+ $html.= "<tr>
+ <td>$icon</td>
+ <td><a href=\"{$fullPath}\">{$displayName}</a></td>
+ <td>{$type}</td>
+ <td>{$size}</td>
+ <td>{$lastmodified}</td>
+ </tr>";
+
+ }
+
+ $html.= "<tr><td colspan=\"5\"><hr /></td></tr>";
+
+ $output = '';
+
+ if ($this->enablePost) {
+ $this->server->broadcastEvent('onHTMLActionsPanel',array($parent, &$output));
+ }
+
+ $html.=$output;
+
+ $html.= "</table>
+ <address>Generated by SabreDAV " . $version . " (c)2007-2015 <a href=\"http://sabre.io/\">http://sabre.io/</a></address>
+ </body>
+ </html>";
+
+ return $html;
+
+ }
+
+ /**
+ * This method is used to generate the 'actions panel' output for
+ * collections.
+ *
+ * This specifically generates the interfaces for creating new files, and
+ * creating new directories.
+ *
+ * @param DAV\INode $node
+ * @param mixed $output
+ * @return void
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output) {
+
+ if (!$node instanceof DAV\ICollection)
+ return;
+
+ // We also know fairly certain that if an object is a non-extended
+ // SimpleCollection, we won't need to show the panel either.
+ if (get_class($node)==='OldSabre\\DAV\\SimpleCollection')
+ return;
+
+ $output.= '<tr><td colspan="2"><form method="post" action="">
+ <h3>Create new folder</h3>
+ <input type="hidden" name="sabreAction" value="mkcol" />
+ Name: <input type="text" name="name" /><br />
+ <input type="submit" value="create" />
+ </form>
+ <form method="post" action="" enctype="multipart/form-data">
+ <h3>Upload file</h3>
+ <input type="hidden" name="sabreAction" value="put" />
+ Name (optional): <input type="text" name="name" /><br />
+ File: <input type="file" name="file" /><br />
+ <input type="submit" value="upload" />
+ </form>
+ </td></tr>';
+
+ }
+
+ /**
+ * This method takes a path/name of an asset and turns it into url
+ * suiteable for http access.
+ *
+ * @param string $assetName
+ * @return string
+ */
+ protected function getAssetUrl($assetName) {
+
+ return $this->server->getBaseUri() . '?sabreAction=asset&assetName=' . urlencode($assetName);
+
+ }
+
+ /**
+ * This method returns a local pathname to an asset.
+ *
+ * @param string $assetName
+ * @return string
+ */
+ protected function getLocalAssetPath($assetName) {
+
+ $assetDir = __DIR__ . '/assets/';
+ $path = $assetDir . $assetName;
+
+ // Making sure people aren't trying to escape from the base path.
+ if (strpos(realpath($path), realpath($assetDir)) === 0) {
+ return $path;
+ }
+ throw new DAV\Exception\Forbidden('Path does not exist, or escaping from the base path was detected');
+ }
+
+ /**
+ * This method reads an asset from disk and generates a full http response.
+ *
+ * @param string $assetName
+ * @return void
+ */
+ protected function serveAsset($assetName) {
+
+ $assetPath = $this->getLocalAssetPath($assetName);
+ if (!file_exists($assetPath)) {
+ throw new DAV\Exception\NotFound('Could not find an asset with this name');
+ }
+ // Rudimentary mime type detection
+ switch(strtolower(substr($assetPath,strpos($assetPath,'.')+1))) {
+
+ case 'ico' :
+ $mime = 'image/vnd.microsoft.icon';
+ break;
+
+ case 'png' :
+ $mime = 'image/png';
+ break;
+
+ default:
+ $mime = 'application/octet-stream';
+ break;
+
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type', $mime);
+ $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath));
+ $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600');
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->sendBody(fopen($assetPath,'r'));
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.ico b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.ico
new file mode 100644
index 0000000..2b2c10a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.ico
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.png
new file mode 100644
index 0000000..c9acc84
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.png
new file mode 100644
index 0000000..3ecd6a8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.png
new file mode 100644
index 0000000..2ce9548
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.png
new file mode 100644
index 0000000..156fa64
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.png
new file mode 100644
index 0000000..3b98551
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.png
new file mode 100644
index 0000000..156fa64
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.png
new file mode 100644
index 0000000..f8988f8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php
new file mode 100644
index 0000000..22a44ba
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php
@@ -0,0 +1,578 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * SabreDAV DAV client
+ *
+ * This client wraps around Curl to provide a convenient API to a WebDAV
+ * server.
+ *
+ * NOTE: This class is experimental, it's api will likely change in the future.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class 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
+ * OldSabre\DAV\Property\ResourceType
+ *
+ * @var array
+ */
+ public $propertyMap = array();
+
+ protected $baseUri;
+ protected $userName;
+ protected $password;
+ protected $proxy;
+ protected $trustedCertificates;
+
+ /**
+ * Basic authentication
+ */
+ const AUTH_BASIC = 1;
+
+ /**
+ * Digest authentication
+ */
+ const AUTH_DIGEST = 2;
+
+ /**
+ * The authentication type we're using.
+ *
+ * This is a bitmask of AUTH_BASIC and AUTH_DIGEST.
+ *
+ * If DIGEST is used, the client makes 1 extra request per request, to get
+ * the authentication tokens.
+ *
+ * @var int
+ */
+ protected $authType;
+
+ /**
+ * Indicates if SSL verification is enabled or not.
+ *
+ * @var boolean
+ */
+ protected $verifyPeer;
+
+ /**
+ * Constructor
+ *
+ * Settings are provided through the 'settings' argument. The following
+ * settings are supported:
+ *
+ * * baseUri
+ * * userName (optional)
+ * * password (optional)
+ * * proxy (optional)
+ *
+ * @param array $settings
+ */
+ public function __construct(array $settings) {
+
+ if (!isset($settings['baseUri'])) {
+ throw new \InvalidArgumentException('A baseUri must be provided');
+ }
+
+ $validSettings = array(
+ 'baseUri',
+ 'userName',
+ 'password',
+ 'proxy',
+ );
+
+ foreach($validSettings as $validSetting) {
+ if (isset($settings[$validSetting])) {
+ $this->$validSetting = $settings[$validSetting];
+ }
+ }
+
+ if (isset($settings['authType'])) {
+ $this->authType = $settings['authType'];
+ } else {
+ $this->authType = self::AUTH_BASIC | self::AUTH_DIGEST;
+ }
+
+ $this->propertyMap['{DAV:}resourcetype'] = 'OldSabre\\DAV\\Property\\ResourceType';
+
+ }
+
+ /**
+ * Add trusted root certificates to the webdav client.
+ *
+ * The parameter certificates should be a absolute path to a file
+ * which contains all trusted certificates
+ *
+ * @param string $certificates
+ */
+ public function addTrustedCertificates($certificates) {
+ $this->trustedCertificates = $certificates;
+ }
+
+ /**
+ * Enables/disables SSL peer verification
+ *
+ * @param boolean $value
+ */
+ public function setVerifyPeer($value) {
+ $this->verifyPeer = $value;
+ }
+
+ /**
+ * Does a PROPFIND request
+ *
+ * The list of requested properties must be specified as an array, in clark
+ * notation.
+ *
+ * The returned array will contain a list of filenames as keys, and
+ * properties as values.
+ *
+ * The properties array will contain the list of properties. Only properties
+ * that are actually returned from the server (without error) will be
+ * returned, anything else is discarded.
+ *
+ * Depth should be either 0 or 1. A depth of 1 will cause a request to be
+ * made to the server to also return all child resources.
+ *
+ * @param string $url
+ * @param array $properties
+ * @param int $depth
+ * @return array
+ */
+ public function propFind($url, array $properties, $depth = 0) {
+
+ $body = '<?xml version="1.0"?>' . "\n";
+ $body.= '<d:propfind xmlns:d="DAV:">' . "\n";
+ $body.= ' <d:prop>' . "\n";
+
+ foreach($properties as $property) {
+
+ list(
+ $namespace,
+ $elementName
+ ) = XMLUtil::parseClarkNotation($property);
+
+ if ($namespace === 'DAV:') {
+ $body.=' <d:' . $elementName . ' />' . "\n";
+ } else {
+ $body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\"/>\n";
+ }
+
+ }
+
+ $body.= ' </d:prop>' . "\n";
+ $body.= '</d:propfind>';
+
+ $response = $this->request('PROPFIND', $url, $body, array(
+ 'Depth' => $depth,
+ 'Content-Type' => 'application/xml'
+ ));
+
+ $result = $this->parseMultiStatus($response['body']);
+
+ // If depth was 0, we only return the top item
+ if ($depth===0) {
+ reset($result);
+ $result = current($result);
+ return isset($result[200])?$result[200]:array();
+ }
+
+ $newResult = array();
+ foreach($result as $href => $statusList) {
+
+ $newResult[$href] = isset($statusList[200])?$statusList[200]:array();
+
+ }
+
+ return $newResult;
+
+ }
+
+ /**
+ * Updates a list of properties on the server
+ *
+ * The list of properties must have clark-notation properties for the keys,
+ * and the actual (string) value for the value. If the value is null, an
+ * attempt is made to delete the property.
+ *
+ * @todo Must be building the request using the DOM, and does not yet
+ * support complex properties.
+ * @param string $url
+ * @param array $properties
+ * @return void
+ */
+ public function propPatch($url, array $properties) {
+
+ $body = '<?xml version="1.0"?>' . "\n";
+ $body.= '<d:propertyupdate xmlns:d="DAV:">' . "\n";
+
+ foreach($properties as $propName => $propValue) {
+
+ list(
+ $namespace,
+ $elementName
+ ) = XMLUtil::parseClarkNotation($propName);
+
+ if ($propValue === null) {
+
+ $body.="<d:remove><d:prop>\n";
+
+ if ($namespace === 'DAV:') {
+ $body.=' <d:' . $elementName . ' />' . "\n";
+ } else {
+ $body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\"/>\n";
+ }
+
+ $body.="</d:prop></d:remove>\n";
+
+ } else {
+
+ $body.="<d:set><d:prop>\n";
+ if ($namespace === 'DAV:') {
+ $body.=' <d:' . $elementName . '>';
+ } else {
+ $body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\">";
+ }
+ // Shitty.. i know
+ $body.=htmlspecialchars($propValue, ENT_NOQUOTES, 'UTF-8');
+ if ($namespace === 'DAV:') {
+ $body.='</d:' . $elementName . '>' . "\n";
+ } else {
+ $body.="</x:" . $elementName . ">\n";
+ }
+ $body.="</d:prop></d:set>\n";
+
+ }
+
+ }
+
+ $body.= '</d:propertyupdate>';
+
+ $this->request('PROPPATCH', $url, $body, array(
+ 'Content-Type' => 'application/xml'
+ ));
+
+ }
+
+ /**
+ * Performs an HTTP options request
+ *
+ * This method returns all the features from the 'DAV:' header as an array.
+ * If there was no DAV header, or no contents this method will return an
+ * empty array.
+ *
+ * @return array
+ */
+ public function options() {
+
+ $result = $this->request('OPTIONS');
+ if (!isset($result['headers']['dav'])) {
+ return array();
+ }
+
+ $features = explode(',', $result['headers']['dav']);
+ foreach($features as &$v) {
+ $v = trim($v);
+ }
+ return $features;
+
+ }
+
+ /**
+ * Performs an actual HTTP request, and returns the result.
+ *
+ * If the specified url is relative, it will be expanded based on the base
+ * url.
+ *
+ * The returned array contains 3 keys:
+ * * body - the response body
+ * * httpCode - a HTTP code (200, 404, etc)
+ * * headers - a list of response http headers. The header names have
+ * been lowercased.
+ *
+ * @param string $method
+ * @param string $url
+ * @param string $body
+ * @param array $headers
+ * @return array
+ */
+ public function request($method, $url = '', $body = null, $headers = array()) {
+
+ $url = $this->getAbsoluteUrl($url);
+
+ $curlSettings = array(
+ CURLOPT_RETURNTRANSFER => true,
+ // Return headers as part of the response
+ CURLOPT_HEADER => true,
+
+ // For security we cast this to a string. If somehow an array could
+ // be passed here, it would be possible for an attacker to use @ to
+ // post local files.
+ CURLOPT_POSTFIELDS => (string)$body,
+ // Automatically follow redirects
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_MAXREDIRS => 5,
+ CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
+ CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS
+ );
+
+ if($this->verifyPeer !== null) {
+ $curlSettings[CURLOPT_SSL_VERIFYPEER] = $this->verifyPeer;
+ $curlSettings[CURLOPT_SSL_VERIFYHOST] = $this->verifyPeer;
+ }
+
+ if($this->trustedCertificates) {
+ $curlSettings[CURLOPT_CAINFO] = $this->trustedCertificates;
+ }
+
+ switch ($method) {
+ case 'HEAD' :
+
+ // do not read body with HEAD requests (this is necessary because cURL does not ignore the body with HEAD
+ // requests when the Content-Length header is given - which in turn is perfectly valid according to HTTP
+ // specs...) cURL does unfortunately return an error in this case ("transfer closed transfer closed with
+ // ... bytes remaining to read") this can be circumvented by explicitly telling cURL to ignore the
+ // response body
+ $curlSettings[CURLOPT_NOBODY] = true;
+ $curlSettings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
+ break;
+
+ default:
+ $curlSettings[CURLOPT_CUSTOMREQUEST] = $method;
+ break;
+
+ }
+
+ // Adding HTTP headers
+ $nHeaders = array();
+ foreach($headers as $key=>$value) {
+
+ $nHeaders[] = $key . ': ' . $value;
+
+ }
+ $curlSettings[CURLOPT_HTTPHEADER] = $nHeaders;
+
+ if ($this->proxy) {
+ $curlSettings[CURLOPT_PROXY] = $this->proxy;
+ }
+
+ if ($this->userName && $this->authType) {
+ $curlType = 0;
+ if ($this->authType & self::AUTH_BASIC) {
+ $curlType |= CURLAUTH_BASIC;
+ }
+ if ($this->authType & self::AUTH_DIGEST) {
+ $curlType |= CURLAUTH_DIGEST;
+ }
+ $curlSettings[CURLOPT_HTTPAUTH] = $curlType;
+ $curlSettings[CURLOPT_USERPWD] = $this->userName . ':' . $this->password;
+ }
+
+ list(
+ $response,
+ $curlInfo,
+ $curlErrNo,
+ $curlError
+ ) = $this->curlRequest($url, $curlSettings);
+
+ $headerBlob = substr($response, 0, $curlInfo['header_size']);
+ $response = substr($response, $curlInfo['header_size']);
+
+ // In the case of 100 Continue, or redirects we'll have multiple lists
+ // of headers for each separate HTTP response. We can easily split this
+ // because they are separated by \r\n\r\n
+ $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
+
+ // We only care about the last set of headers
+ $headerBlob = $headerBlob[count($headerBlob)-1];
+
+ // Splitting headers
+ $headerBlob = explode("\r\n", $headerBlob);
+
+ $headers = array();
+ foreach($headerBlob as $header) {
+ $parts = explode(':', $header, 2);
+ if (count($parts)==2) {
+ $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
+ }
+ }
+
+ $response = array(
+ 'body' => $response,
+ 'statusCode' => $curlInfo['http_code'],
+ 'headers' => $headers
+ );
+
+ if ($curlErrNo) {
+ throw new Exception('[CURL] Error while making request: ' . $curlError . ' (error code: ' . $curlErrNo . ')');
+ }
+
+ if ($response['statusCode']>=400) {
+ switch ($response['statusCode']) {
+ case 400 :
+ throw new Exception\BadRequest('Bad request');
+ case 401 :
+ throw new Exception\NotAuthenticated('Not authenticated');
+ case 402 :
+ throw new Exception\PaymentRequired('Payment required');
+ case 403 :
+ throw new Exception\Forbidden('Forbidden');
+ case 404:
+ throw new Exception\NotFound('Resource not found.');
+ case 405 :
+ throw new Exception\MethodNotAllowed('Method not allowed');
+ case 409 :
+ throw new Exception\Conflict('Conflict');
+ case 412 :
+ throw new Exception\PreconditionFailed('Precondition failed');
+ case 416 :
+ throw new Exception\RequestedRangeNotSatisfiable('Requested Range Not Satisfiable');
+ case 500 :
+ throw new Exception('Internal server error');
+ case 501 :
+ throw new Exception\NotImplemented('Not Implemented');
+ case 507 :
+ throw new Exception\InsufficientStorage('Insufficient storage');
+ default:
+ throw new Exception('HTTP error response. (errorcode ' . $response['statusCode'] . ')');
+ }
+ }
+
+ return $response;
+
+ }
+
+ /**
+ * Wrapper for all curl functions.
+ *
+ * The only reason this was split out in a separate method, is so it
+ * becomes easier to unittest.
+ *
+ * @param string $url
+ * @param array $settings
+ * @return array
+ */
+ // @codeCoverageIgnoreStart
+ protected function curlRequest($url, $settings) {
+
+ $curl = curl_init($url);
+ curl_setopt_array($curl, $settings);
+
+ return array(
+ curl_exec($curl),
+ curl_getinfo($curl),
+ curl_errno($curl),
+ curl_error($curl)
+ );
+
+ }
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Returns the full url based on the given url (which may be relative). All
+ * urls are expanded based on the base url as given by the server.
+ *
+ * @param string $url
+ * @return string
+ */
+ protected function getAbsoluteUrl($url) {
+
+ // If the url starts with http:// or https://, the url is already absolute.
+ if (preg_match('/^http(s?):\/\//', $url)) {
+ return $url;
+ }
+
+ // If the url starts with a slash, we must calculate the url based off
+ // the root of the base url.
+ if (strpos($url,'/') === 0) {
+ $parts = parse_url($this->baseUri);
+ return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
+ }
+
+ // Otherwise...
+ return $this->baseUri . $url;
+
+ }
+
+ /**
+ * Parses a WebDAV multistatus response body
+ *
+ * This method returns an array with the following structure
+ *
+ * array(
+ * 'url/to/resource' => array(
+ * '200' => array(
+ * '{DAV:}property1' => 'value1',
+ * '{DAV:}property2' => 'value2',
+ * ),
+ * '404' => array(
+ * '{DAV:}property1' => null,
+ * '{DAV:}property2' => null,
+ * ),
+ * )
+ * 'url/to/resource2' => array(
+ * .. etc ..
+ * )
+ * )
+ *
+ *
+ * @param string $body xml body
+ * @return array
+ */
+ public function parseMultiStatus($body) {
+
+ $body = XMLUtil::convertDAVNamespace($body);
+
+ // Fixes an XXE vulnerability on PHP versions older than 5.3.23 or
+ // 5.4.13.
+ $previous = libxml_disable_entity_loader(true);
+ $responseXML = simplexml_load_string($body, null, LIBXML_NOBLANKS | LIBXML_NOCDATA);
+ libxml_disable_entity_loader($previous);
+
+ if ($responseXML===false) {
+ throw new \InvalidArgumentException('The passed data is not valid XML');
+ }
+
+ $responseXML->registerXPathNamespace('d', 'urn:DAV');
+
+ $propResult = array();
+
+ foreach($responseXML->xpath('d:response') as $response) {
+ $response->registerXPathNamespace('d', 'urn:DAV');
+ $href = $response->xpath('d:href');
+ $href = (string)$href[0];
+
+ $properties = array();
+
+ foreach($response->xpath('d:propstat') as $propStat) {
+
+ $propStat->registerXPathNamespace('d', 'urn:DAV');
+ $status = $propStat->xpath('d:status');
+ list($httpVersion, $statusCode, $message) = explode(' ', (string)$status[0],3);
+
+ // Only using the propertymap for results with status 200.
+ $propertyMap = $statusCode==='200' ? $this->propertyMap : array();
+
+ $properties[$statusCode] = XMLUtil::parseProperties(dom_import_simplexml($propStat), $propertyMap);
+
+ }
+
+ $propResult[$href] = $properties;
+
+ }
+
+ return $propResult;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php
new file mode 100644
index 0000000..855667e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Collection class
+ *
+ * This is a helper class, that should aid in getting collections classes setup.
+ * Most of its methods are implemented, and throw permission denied exceptions
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Collection extends Node implements ICollection {
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * This method makes use of the getChildren method to grab all the child
+ * nodes, and compares the name.
+ * Generally its wise to override this, as this can usually be optimized
+ *
+ * This method must throw OldSabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws Exception\NotFound
+ * @return INode
+ */
+ public function getChild($name) {
+
+ foreach($this->getChildren() as $child) {
+
+ if ($child->getName()==$name) return $child;
+
+ }
+ throw new Exception\NotFound('File not found: ' . $name);
+
+ }
+
+ /**
+ * Checks is a child-node exists.
+ *
+ * It is generally a good idea to try and override this. Usually it can be optimized.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ try {
+
+ $this->getChild($name);
+ return true;
+
+ } catch(Exception\NotFound $e) {
+
+ return false;
+
+ }
+
+ }
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After succesful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ public function createFile($name, $data = null) {
+
+ throw new Exception\Forbidden('Permission denied to create file (filename ' . $name . ')');
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @throws Exception\Forbidden
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ throw new Exception\Forbidden('Permission denied to create directory');
+
+ }
+
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php
new file mode 100644
index 0000000..357e818
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SabreDAV base exception
+ *
+ * This is SabreDAV's base exception file, use this to implement your own exception.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+
+namespace OldSabre\DAV;
+
+/**
+ * Main Exception class.
+ *
+ * This class defines a getHTTPCode method, which should return the appropriate HTTP code for the Exception occurred.
+ * The default for this is 500.
+ *
+ * This class also allows you to generate custom xml data for your exceptions. This will be displayed
+ * in the 'error' element in the failing response.
+ */
+class Exception extends \Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 500;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(Server $server,\DOMElement $errorNode) {
+
+
+ }
+
+ /**
+ * This method allows the exception to return any extra HTTP response headers.
+ *
+ * The headers must be returned as an array.
+ *
+ * @param Server $server
+ * @return array
+ */
+ public function getHTTPHeaders(Server $server) {
+
+ return array();
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php
new file mode 100644
index 0000000..aeb6350
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * BadRequest
+ *
+ * The BadRequest is thrown when the user submitted an invalid HTTP request
+ * BadRequest
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class BadRequest extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 400;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php
new file mode 100644
index 0000000..97ef2cd
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * Conflict
+ *
+ * A 409 Conflict is thrown when a user tried to make a directory over an existing
+ * file or in a parent directory that doesn't exist.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Conflict extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 409;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php
new file mode 100644
index 0000000..33839d3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * ConflictingLock
+ *
+ * Similar to the Locked exception, this exception thrown when a LOCK request
+ * was made, on a resource which was already locked
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ConflictingLock extends Locked {
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $errorNode) {
+
+ if ($this->lock) {
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:no-conflicting-lock');
+ $errorNode->appendChild($error);
+ if (!is_object($this->lock)) var_dump($this->lock);
+ $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:','d:href',$this->lock->uri));
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php
new file mode 100644
index 0000000..f35f9b3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * FileNotFound
+ *
+ * Deprecated: Warning, this class is deprecated and will be removed in a
+ * future version of SabreDAV. Please use OldSabre\DAV\Exception\NotFound instead.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @deprecated Use OldSabre\DAV\Exception\NotFound instead
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class FileNotFound extends NotFound {
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php
new file mode 100644
index 0000000..003d86b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * Forbidden
+ *
+ * This exception is thrown whenever a user tries to do an operation he's not allowed to
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Forbidden extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 403;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php
new file mode 100644
index 0000000..081a3e4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * InsufficientStorage
+ *
+ * This Exception can be thrown, when for example a harddisk is full or a quota is exceeded
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InsufficientStorage extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 507;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php
new file mode 100644
index 0000000..9526b8d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * InvalidResourceType
+ *
+ * This exception is thrown when the user tried to create a new collection, with
+ * a special resourcetype value that was not recognized by the server.
+ *
+ * See RFC5689 section 3.3
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InvalidResourceType extends Forbidden {
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(\OldSabre\DAV\Server $server,\DOMElement $errorNode) {
+
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:valid-resourcetype');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php
new file mode 100644
index 0000000..39cd639
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * LengthRequired
+ *
+ * This exception is thrown when a request was made that required a
+ * Content-Length header, but did not contain one.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class LengthRequired extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 411;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php
new file mode 100644
index 0000000..08d94b5
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * LockTokenMatchesRequestUri
+ *
+ * This exception is thrown by UNLOCK if a supplied lock-token is invalid
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class LockTokenMatchesRequestUri extends Conflict {
+
+ /**
+ * Creates the exception
+ */
+ public function __construct() {
+
+ $this->message = 'The locktoken supplied does not match any locks on this entity';
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:lock-token-matches-request-uri');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php
new file mode 100644
index 0000000..4d38357
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * Locked
+ *
+ * The 423 is thrown when a client tried to access a resource that was locked, without supplying a valid lock token
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Locked extends DAV\Exception {
+
+ /**
+ * Lock information
+ *
+ * @var OldSabre\DAV\Locks\LockInfo
+ */
+ protected $lock;
+
+ /**
+ * Creates the exception
+ *
+ * A LockInfo object should be passed if the user should be informed
+ * which lock actually has the file locked.
+ *
+ * @param DAV\Locks\LockInfo $lock
+ */
+ public function __construct(DAV\Locks\LockInfo $lock = null) {
+
+ $this->lock = $lock;
+
+ }
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 423;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ if ($this->lock) {
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:lock-token-submitted');
+ $errorNode->appendChild($error);
+
+ $href = $errorNode->ownerDocument->createElementNS('DAV:','d:href');
+ $href->appendChild($errorNode->ownerDocument->createTextNode($this->lock->uri));
+ $error->appendChild(
+ $href
+ );
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php
new file mode 100644
index 0000000..adf91e2
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * MethodNotAllowed
+ *
+ * The 405 is thrown when a client tried to create a directory on an already existing directory
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class MethodNotAllowed extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 405;
+
+ }
+
+ /**
+ * This method allows the exception to return any extra HTTP response headers.
+ *
+ * The headers must be returned as an array.
+ *
+ * @param \OldSabre\DAV\Server $server
+ * @return array
+ */
+ public function getHTTPHeaders(\OldSabre\DAV\Server $server) {
+
+ $methods = $server->getAllowedMethods($server->getRequestUri());
+
+ return array(
+ 'Allow' => strtoupper(implode(', ',$methods)),
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php
new file mode 100644
index 0000000..2e98802
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * NotAuthenticated
+ *
+ * This exception is thrown when the client did not provide valid
+ * authentication credentials.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotAuthenticated extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 401;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php
new file mode 100644
index 0000000..210f21f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * NotFound
+ *
+ * This Exception is thrown when a Node couldn't be found. It returns HTTP error code 404
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotFound extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 404;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php
new file mode 100644
index 0000000..f9624e8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * NotImplemented
+ *
+ * This exception is thrown when the client tried to call an unsupported HTTP method or other feature
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotImplemented extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 501;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php
new file mode 100644
index 0000000..1fd18be
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * Payment Required
+ *
+ * The PaymentRequired exception may be thrown in a case where a user must pay
+ * to access a certain resource or operation.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PaymentRequired extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 402;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php
new file mode 100644
index 0000000..625a7fc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * PreconditionFailed
+ *
+ * This exception is normally thrown when a client submitted a conditional request,
+ * like for example an If, If-None-Match or If-Match header, which caused the HTTP
+ * request to not execute (the condition of the header failed)
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PreconditionFailed extends DAV\Exception {
+
+ /**
+ * When this exception is thrown, the header-name might be set.
+ *
+ * This allows the exception-catching code to determine which HTTP header
+ * caused the exception.
+ *
+ * @var string
+ */
+ public $header = null;
+
+ /**
+ * Create the exception
+ *
+ * @param string $message
+ * @param string $header
+ */
+ public function __construct($message, $header=null) {
+
+ parent::__construct($message);
+ $this->header = $header;
+
+ }
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 412;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ if ($this->header) {
+ $prop = $errorNode->ownerDocument->createElement('s:header');
+ $prop->nodeValue = $this->header;
+ $errorNode->appendChild($prop);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php
new file mode 100644
index 0000000..f1d42ba
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * ReportNotSupported
+ *
+ * This exception is thrown when the client requested an unknown report through the REPORT method
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ReportNotSupported extends Forbidden {
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:supported-report');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php
new file mode 100644
index 0000000..5528cde
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * RequestedRangeNotSatisfiable
+ *
+ * This exception is normally thrown when the user
+ * request a range that is out of the entity bounds.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class RequestedRangeNotSatisfiable extends DAV\Exception {
+
+ /**
+ * returns the http statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 416;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php
new file mode 100644
index 0000000..94993f2
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * ServiceUnavailable
+ *
+ * This exception is thrown in case the service
+ * is currently not available (e.g. down for maintenance).
+ *
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ServiceUnavailable extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 503;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php
new file mode 100644
index 0000000..32ba6f1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * UnSupportedMediaType
+ *
+ * The 415 Unsupported Media Type status code is generally sent back when the client
+ * tried to call an HTTP method, with a body the server didn't understand
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class UnsupportedMediaType extends \OldSabre\DAV\Exception {
+
+ /**
+ * returns the http statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 415;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php
new file mode 100644
index 0000000..a8021cc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace OldSabre\DAV\FS;
+use OldSabre\DAV;
+
+/**
+ * Directory class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Directory extends Node implements DAV\ICollection, DAV\IQuota {
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ public function createFile($name, $data = null) {
+
+ $newPath = $this->path . '/' . $name;
+ file_put_contents($newPath,$data);
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ $newPath = $this->path . '/' . $name;
+ mkdir($newPath);
+
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws DAV\Exception\NotFound
+ * @return DAV\INode
+ */
+ public function getChild($name) {
+
+ $path = $this->path . '/' . $name;
+
+ if (!file_exists($path)) throw new DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
+
+ if (is_dir($path)) {
+
+ return new Directory($path);
+
+ } else {
+
+ return new File($path);
+
+ }
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren() {
+
+ $nodes = array();
+ foreach(scandir($this->path) as $node) if($node!='.' && $node!='..') $nodes[] = $this->getChild($node);
+ return $nodes;
+
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ $path = $this->path . '/' . $name;
+ return file_exists($path);
+
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself
+ *
+ * @return void
+ */
+ public function delete() {
+
+ foreach($this->getChildren() as $child) $child->delete();
+ rmdir($this->path);
+
+ }
+
+ /**
+ * Returns available diskspace information
+ *
+ * @return array
+ */
+ public function getQuotaInfo() {
+
+ return array(
+ disk_total_space($this->path)-disk_free_space($this->path),
+ disk_free_space($this->path)
+ );
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php
new file mode 100644
index 0000000..e58a705
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace OldSabre\DAV\FS;
+
+use OldSabre\DAV;
+
+/**
+ * File class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class File extends Node implements DAV\IFile {
+
+ /**
+ * Updates the data
+ *
+ * @param resource $data
+ * @return void
+ */
+ public function put($data) {
+
+ file_put_contents($this->path,$data);
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * @return string
+ */
+ public function get() {
+
+ return fopen($this->path,'r');
+
+ }
+
+ /**
+ * Delete the current file
+ *
+ * @return void
+ */
+ public function delete() {
+
+ unlink($this->path);
+
+ }
+
+ /**
+ * Returns the size of the node, in bytes
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ return filesize($this->path);
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return mixed
+ */
+ public function getETag() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return mixed
+ */
+ public function getContentType() {
+
+ return null;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php
new file mode 100644
index 0000000..e2bf024
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace OldSabre\DAV\FS;
+
+use OldSabre\DAV;
+
+/**
+ * Base node-class
+ *
+ * The node class implements the method used by both the File and the Directory classes
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Node implements DAV\INode {
+
+ /**
+ * The path to the current node
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Sets up the node, expects a full path name
+ *
+ * @param string $path
+ */
+ public function __construct($path) {
+
+ $this->path = $path;
+
+ }
+
+
+
+ /**
+ * Returns the name of the node
+ *
+ * @return string
+ */
+ public function getName() {
+
+ list(, $name) = DAV\URLUtil::splitPath($this->path);
+ return $name;
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ list($parentPath, ) = DAV\URLUtil::splitPath($this->path);
+ list(, $newName) = DAV\URLUtil::splitPath($name);
+
+ $newPath = $parentPath . '/' . $newName;
+ rename($this->path,$newPath);
+
+ $this->path = $newPath;
+
+ }
+
+
+
+ /**
+ * Returns the last modification time, as a unix timestamp
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return filemtime($this->path);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php
new file mode 100644
index 0000000..d0e899e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace OldSabre\DAV\FSExt;
+
+use OldSabre\DAV;
+
+/**
+ * Directory class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Directory extends Node implements DAV\ICollection, DAV\IQuota {
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ public function createFile($name, $data = null) {
+
+ // We're not allowing dots
+ if ($name=='.' || $name=='..') throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+ $newPath = $this->path . '/' . $name;
+ file_put_contents($newPath,$data);
+
+ return '"' . md5_file($newPath) . '"';
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ // We're not allowing dots
+ if ($name=='.' || $name=='..') throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+ $newPath = $this->path . '/' . $name;
+ mkdir($newPath);
+
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw OldSabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws DAV\Exception\NotFound
+ * @return DAV\INode
+ */
+ public function getChild($name) {
+
+ $path = $this->path . '/' . $name;
+
+ if (!file_exists($path)) throw new DAV\Exception\NotFound('File could not be located');
+ if ($name=='.' || $name=='..') throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+
+ if (is_dir($path)) {
+
+ return new Directory($path);
+
+ } else {
+
+ return new File($path);
+
+ }
+
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ if ($name=='.' || $name=='..')
+ throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+
+ $path = $this->path . '/' . $name;
+ return file_exists($path);
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren() {
+
+ $nodes = array();
+ foreach(scandir($this->path) as $node) if($node!='.' && $node!='..' && $node!='.sabredav') $nodes[] = $this->getChild($node);
+ return $nodes;
+
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself
+ *
+ * @return bool
+ */
+ public function delete() {
+
+ // Deleting all children
+ foreach($this->getChildren() as $child) $child->delete();
+
+ // Removing resource info, if its still around
+ if (file_exists($this->path . '/.sabredav')) unlink($this->path . '/.sabredav');
+
+ // Removing the directory itself
+ rmdir($this->path);
+
+ return parent::delete();
+
+ }
+
+ /**
+ * Returns available diskspace information
+ *
+ * @return array
+ */
+ public function getQuotaInfo() {
+
+ return array(
+ disk_total_space($this->path)-disk_free_space($this->path),
+ disk_free_space($this->path)
+ );
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php
new file mode 100644
index 0000000..d42e486
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace OldSabre\DAV\FSExt;
+use OldSabre\DAV;
+
+/**
+ * File class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class File extends Node implements DAV\PartialUpdate\IPatchSupport {
+
+ /**
+ * Updates the data
+ *
+ * data is a readable stream resource.
+ *
+ * @param resource|string $data
+ * @return string
+ */
+ public function put($data) {
+
+ file_put_contents($this->path,$data);
+ return '"' . md5_file($this->path) . '"';
+
+ }
+
+ /**
+ * Updates the file based on a range specification.
+ *
+ * The first argument is the data, which is either a readable stream
+ * resource or a string.
+ *
+ * The second argument is the type of update we're doing.
+ * This is either:
+ * * 1. append
+ * * 2. update based on a start byte
+ * * 3. update based on an end byte
+ *;
+ * The third argument is the start or end byte.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * @param resource|string $data
+ * @param int $rangeType
+ * @param int $offset
+ * @return string|null
+ */
+ public function patch($data, $rangeType, $offset = null) {
+
+ switch($rangeType) {
+ case 1 :
+ $f = fopen($this->path, 'a');
+ break;
+ case 2 :
+ $f = fopen($this->path, 'c');
+ fseek($f,$offset);
+ break;
+ case 3 :
+ $f = fopen($this->path, 'c');
+ fseek($f, $offset, SEEK_END);
+ break;
+ }
+ if (is_string($data)) {
+ fwrite($f, $data);
+ } else {
+ stream_copy_to_stream($data,$f);
+ }
+ fclose($f);
+ return '"' . md5_file($this->path) . '"';
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * @return resource
+ */
+ public function get() {
+
+ return fopen($this->path,'r');
+
+ }
+
+ /**
+ * Delete the current file
+ *
+ * @return bool
+ */
+ public function delete() {
+
+ unlink($this->path);
+ return parent::delete();
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return string|null
+ */
+ public function getETag() {
+
+ return '"' . md5_file($this->path). '"';
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string|null
+ */
+ public function getContentType() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the size of the file, in bytes
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ return filesize($this->path);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php
new file mode 100644
index 0000000..68f4255
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace OldSabre\DAV\FSExt;
+
+use OldSabre\DAV;
+
+/**
+ * Base node-class
+ *
+ * The node class implements the method used by both the File and the Directory classes
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Node extends DAV\FS\Node implements DAV\IProperties {
+
+ /**
+ * Updates properties on this node,
+ *
+ * @param array $properties
+ * @see OldSabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateProperties($properties) {
+
+ $resourceData = $this->getResourceData();
+
+ foreach($properties as $propertyName=>$propertyValue) {
+
+ // If it was null, we need to delete the property
+ if (is_null($propertyValue)) {
+ if (isset($resourceData['properties'][$propertyName])) {
+ unset($resourceData['properties'][$propertyName]);
+ }
+ } else {
+ $resourceData['properties'][$propertyName] = $propertyValue;
+ }
+
+ }
+
+ $this->putResourceData($resourceData);
+ return true;
+ }
+
+ /**
+ * Returns a list of properties for this nodes.;
+ *
+ * The properties list is a list of propertynames the client requested, encoded as xmlnamespace#tagName, for example: http://www.example.org/namespace#author
+ * If the array is empty, all properties should be returned
+ *
+ * @param array $properties
+ * @return array
+ */
+ function getProperties($properties) {
+
+ $resourceData = $this->getResourceData();
+
+ // if the array was empty, we need to return everything
+ if (!$properties) return $resourceData['properties'];
+
+ $props = array();
+ foreach($properties as $property) {
+ if (isset($resourceData['properties'][$property])) $props[$property] = $resourceData['properties'][$property];
+ }
+
+ return $props;
+
+ }
+
+ /**
+ * Returns the path to the resource file
+ *
+ * @return string
+ */
+ protected function getResourceInfoPath() {
+
+ list($parentDir) = DAV\URLUtil::splitPath($this->path);
+ return $parentDir . '/.sabredav';
+
+ }
+
+ /**
+ * Returns all the stored resource information
+ *
+ * @return array
+ */
+ protected function getResourceData() {
+
+ $path = $this->getResourceInfoPath();
+ if (!file_exists($path)) return array('properties' => array());
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'r');
+ flock($handle,LOCK_SH);
+ $data = '';
+
+ // Reading data until the eof
+ while(!feof($handle)) {
+ $data.=fread($handle,8192);
+ }
+
+ // We're all good
+ fclose($handle);
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (!isset($data[$this->getName()])) {
+ return array('properties' => array());
+ }
+
+ $data = $data[$this->getName()];
+ if (!isset($data['properties'])) $data['properties'] = array();
+ return $data;
+
+ }
+
+ /**
+ * Updates the resource information
+ *
+ * @param array $newData
+ * @return void
+ */
+ protected function putResourceData(array $newData) {
+
+ $path = $this->getResourceInfoPath();
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'a+');
+ flock($handle,LOCK_EX);
+ $data = '';
+
+ rewind($handle);
+
+ // Reading data until the eof
+ while(!feof($handle)) {
+ $data.=fread($handle,8192);
+ }
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ $data[$this->getName()] = $newData;
+ ftruncate($handle,0);
+ rewind($handle);
+
+ fwrite($handle,serialize($data));
+ fclose($handle);
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ list($parentPath, ) = DAV\URLUtil::splitPath($this->path);
+ list(, $newName) = DAV\URLUtil::splitPath($name);
+ $newPath = $parentPath . '/' . $newName;
+
+ // We're deleting the existing resourcedata, and recreating it
+ // for the new path.
+ $resourceData = $this->getResourceData();
+ $this->deleteResourceData();
+
+ rename($this->path,$newPath);
+ $this->path = $newPath;
+ $this->putResourceData($resourceData);
+
+
+ }
+
+ /**
+ * @return bool
+ */
+ public function deleteResourceData() {
+
+ // When we're deleting this node, we also need to delete any resource information
+ $path = $this->getResourceInfoPath();
+ if (!file_exists($path)) return true;
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'a+');
+ flock($handle,LOCK_EX);
+ $data = '';
+
+ rewind($handle);
+
+ // Reading data until the eof
+ while(!feof($handle)) {
+ $data.=fread($handle,8192);
+ }
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (isset($data[$this->getName()])) unset($data[$this->getName()]);
+ ftruncate($handle,0);
+ rewind($handle);
+ fwrite($handle,serialize($data));
+ fclose($handle);
+
+ return true;
+ }
+
+ public function delete() {
+
+ return $this->deleteResourceData();
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php
new file mode 100644
index 0000000..d696eb3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * File class
+ *
+ * This is a helper class, that should aid in getting file classes setup.
+ * Most of its methods are implemented, and throw permission denied exceptions
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class File extends Node implements IFile {
+
+ /**
+ * Updates the data
+ *
+ * data is a readable stream resource.
+ *
+ * @param resource $data
+ * @return void
+ */
+ public function put($data) {
+
+ throw new Exception\Forbidden('Permission denied to change data');
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * This method may either return a string or a readable stream resource
+ *
+ * @return mixed
+ */
+ public function get() {
+
+ throw new Exception\Forbidden('Permission denied to read this file');
+
+ }
+
+ /**
+ * Returns the size of the file, in bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ return 0;
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return string|null
+ */
+ public function getETag() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string|null
+ */
+ public function getContentType() {
+
+ return null;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php
new file mode 100644
index 0000000..0468ee1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * The ICollection Interface
+ *
+ * This interface should be implemented by each class that represents a collection
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICollection extends INode {
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ function createFile($name, $data = null);
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @return void
+ */
+ function createDirectory($name);
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw OldSabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @return DAV\INode
+ */
+ function getChild($name);
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ function getChildren();
+
+ /**
+ * Checks if a child-node with the specified name exists
+ *
+ * @param string $name
+ * @return bool
+ */
+ function childExists($name);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php
new file mode 100644
index 0000000..7894f58
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * The IExtendedCollection interface.
+ *
+ * This interface can be used to create special-type of collection-resources
+ * as defined by RFC 5689.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IExtendedCollection extends ICollection {
+
+ /**
+ * Creates a new collection
+ *
+ * @param string $name
+ * @param array $resourceType
+ * @param array $properties
+ * @return void
+ */
+ function createExtendedCollection($name, array $resourceType, array $properties);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php
new file mode 100644
index 0000000..476ccbd
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * This interface represents a file in the directory tree
+ *
+ * A file is a bit of a broad definition. In general it implies that on
+ * this specific node a PUT or GET method may be performed, to either update,
+ * or retrieve the contents of the file.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IFile extends INode {
+
+ /**
+ * Updates the data
+ *
+ * The data argument is a readable stream resource.
+ *
+ * After a succesful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * If you don't plan to store the file byte-by-byte, and you return a
+ * different object on a subsequent GET you are strongly recommended to not
+ * return an ETag, and just return null.
+ *
+ * @param resource $data
+ * @return string|null
+ */
+ function put($data);
+
+ /**
+ * Returns the data
+ *
+ * This method may either return a string or a readable stream resource
+ *
+ * @return mixed
+ */
+ function get();
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string|null
+ */
+ function getContentType();
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return void
+ */
+ function getETag();
+
+ /**
+ * Returns the size of the node, in bytes
+ *
+ * @return int
+ */
+ function getSize();
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php
new file mode 100644
index 0000000..09c254f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * The INode interface is the base interface, and the parent class of both ICollection and IFile
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface INode {
+
+ /**
+ * Deleted the current node
+ *
+ * @return void
+ */
+ function delete();
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ function getName();
+
+ /**
+ * Renames the node
+ *
+ * @param string $name The new name
+ * @return void
+ */
+ function setName($name);
+
+ /**
+ * Returns the last modification time, as a unix timestamp
+ *
+ * @return int
+ */
+ function getLastModified();
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php
new file mode 100644
index 0000000..8f97ebc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * IProperties interface
+ *
+ * Implement this interface to support custom WebDAV properties requested and sent from clients.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IProperties extends INode {
+
+ /**
+ * Updates properties on this node,
+ *
+ * The properties array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => 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 array $mutations
+ * @return bool|array
+ */
+ function updateProperties($mutations);
+
+ /**
+ * Returns a list of properties for this nodes.
+ *
+ * The properties list is a list of propertynames the client requested,
+ * encoded in clark-notation {xmlnamespace}tagname
+ *
+ * If the array is empty, it means 'all properties' were requested.
+ *
+ * Note that it's fine to liberally give properties back, instead of
+ * conforming to the list of requested properties.
+ * The Server class will filter out the extra.
+ *
+ * @param array $properties
+ * @return void
+ */
+ function getProperties($properties);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php
new file mode 100644
index 0000000..074516e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * IQuota interface
+ *
+ * Implement this interface to add the ability to return quota information. The ObjectTree
+ * will check for quota information on any given node. If the information is not available it will
+ * attempt to fetch the information from the root node.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IQuota extends ICollection {
+
+ /**
+ * Returns the quota information
+ *
+ * This method MUST return an array with 2 values, the first being the total used space,
+ * the second the available space (in bytes)
+ */
+ function getQuotaInfo();
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php
new file mode 100644
index 0000000..c89d37c
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks;
+
+/**
+ * This is an Abstract clas for lock backends.
+ *
+ * Currently this backend has no function, but it exists for consistency, and
+ * to ensure that if default code is required in the backend, there will be a
+ * non-bc-breaking way to do so.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php
new file mode 100644
index 0000000..1a768b0
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks;
+
+/**
+ * If you are defining your own Locks backend, you must implement this
+ * interface.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Returns a list of OldSabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks);
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param Locks\LockInfo $lockInfo
+ * @return bool
+ */
+ public function lock($uri,Locks\LockInfo $lockInfo);
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param Locks\LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlock($uri,Locks\LockInfo $lockInfo);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php
new file mode 100644
index 0000000..3a61f40
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks\LockInfo;
+
+/**
+ * This Lock Backend stores all its data in the filesystem in separate file per
+ * node.
+ *
+ * This Lock Manager is now deprecated. It has a bug that allows parent
+ * collections to be deletes when children deeper in the tree are locked.
+ *
+ * This also means that using this backend means you will not pass the Neon
+ * Litmus test.
+ *
+ * You are recommended to use either the PDO or the File backend instead.
+ *
+ * @deprecated
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class FS extends AbstractBackend {
+
+ /**
+ * The default data directory
+ *
+ * @var string
+ */
+ private $dataDir;
+
+ public function __construct($dataDir) {
+
+ $this->dataDir = $dataDir;
+
+ }
+
+ protected function getFileNameForUri($uri) {
+
+ return $this->dataDir . '/sabredav_' . md5($uri) . '.locks';
+
+ }
+
+
+ /**
+ * Returns a list of OldSabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks) {
+
+ $lockList = array();
+ $currentPath = '';
+
+ foreach(explode('/',$uri) as $uriPart) {
+
+ // weird algorithm that can probably be improved, but we're traversing the path top down
+ if ($currentPath) $currentPath.='/';
+ $currentPath.=$uriPart;
+
+ $uriLocks = $this->getData($currentPath);
+
+ foreach($uriLocks as $uriLock) {
+
+ // Unless we're on the leaf of the uri-tree we should ignore locks with depth 0
+ if($uri==$currentPath || $uriLock->depth!=0) {
+ $uriLock->uri = $currentPath;
+ $lockList[] = $uriLock;
+ }
+
+ }
+
+ }
+
+ // Checking if we can remove any of these locks
+ foreach($lockList as $k=>$lock) {
+ if (time() > $lock->timeout + $lock->created) unset($lockList[$k]);
+ }
+ return $lockList;
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function lock($uri, LockInfo $lockInfo) {
+
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 1800;
+ $lockInfo->created = time();
+
+ $locks = $this->getLocks($uri,false);
+ foreach($locks as $k=>$lock) {
+ if ($lock->token == $lockInfo->token) unset($locks[$k]);
+ }
+ $locks[] = $lockInfo;
+ $this->putData($uri,$locks);
+ return true;
+
+ }
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlock($uri, LockInfo $lockInfo) {
+
+ $locks = $this->getLocks($uri,false);
+ foreach($locks as $k=>$lock) {
+
+ if ($lock->token == $lockInfo->token) {
+
+ unset($locks[$k]);
+ $this->putData($uri,$locks);
+ return true;
+
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * Returns the stored data for a uri
+ *
+ * @param string $uri
+ * @return array
+ */
+ protected function getData($uri) {
+
+ $path = $this->getFilenameForUri($uri);
+ if (!file_exists($path)) return array();
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'r');
+ flock($handle,LOCK_SH);
+ $data = '';
+
+ // Reading data until the eof
+ while(!feof($handle)) {
+ $data.=fread($handle,8192);
+ }
+
+ // We're all good
+ fclose($handle);
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (!$data) return array();
+ return $data;
+
+ }
+
+ /**
+ * Updates the lock information
+ *
+ * @param string $uri
+ * @param array $newData
+ * @return void
+ */
+ protected function putData($uri,array $newData) {
+
+ $path = $this->getFileNameForUri($uri);
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'a+');
+ flock($handle,LOCK_EX);
+ ftruncate($handle,0);
+ rewind($handle);
+
+ fwrite($handle,serialize($newData));
+ fclose($handle);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php
new file mode 100644
index 0000000..b4eb58b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks\LockInfo;
+
+/**
+ * The Lock manager allows you to handle all file-locks centrally.
+ *
+ * This Lock Manager stores all its data in a single file.
+ *
+ * Note that this is not nearly as robust as a database, you are encouraged
+ * to use the PDO backend instead.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class File extends AbstractBackend {
+
+ /**
+ * The storage file
+ *
+ * @var string
+ */
+ private $locksFile;
+
+ /**
+ * Constructor
+ *
+ * @param string $locksFile path to file
+ */
+ public function __construct($locksFile) {
+
+ $this->locksFile = $locksFile;
+
+ }
+
+ /**
+ * Returns a list of OldSabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks) {
+
+ $newLocks = array();
+
+ $locks = $this->getData();
+
+ foreach($locks as $lock) {
+
+ if ($lock->uri === $uri ||
+ //deep locks on parents
+ ($lock->depth!=0 && strpos($uri, $lock->uri . '/')===0) ||
+
+ // locks on children
+ ($returnChildLocks && (strpos($lock->uri, $uri . '/')===0)) ) {
+
+ $newLocks[] = $lock;
+
+ }
+
+ }
+
+ // Checking if we can remove any of these locks
+ foreach($newLocks as $k=>$lock) {
+ if (time() > $lock->timeout + $lock->created) unset($newLocks[$k]);
+ }
+ return $newLocks;
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function lock($uri, LockInfo $lockInfo) {
+
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 1800;
+ $lockInfo->created = time();
+ $lockInfo->uri = $uri;
+
+ $locks = $this->getData();
+
+ foreach($locks as $k=>$lock) {
+ if (
+ ($lock->token == $lockInfo->token) ||
+ (time() > $lock->timeout + $lock->created)
+ ) {
+ unset($locks[$k]);
+ }
+ }
+ $locks[] = $lockInfo;
+ $this->putData($locks);
+ return true;
+
+ }
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlock($uri, LockInfo $lockInfo) {
+
+ $locks = $this->getData();
+ foreach($locks as $k=>$lock) {
+
+ if ($lock->token == $lockInfo->token) {
+
+ unset($locks[$k]);
+ $this->putData($locks);
+ return true;
+
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * Loads the lockdata from the filesystem.
+ *
+ * @return array
+ */
+ protected function getData() {
+
+ if (!file_exists($this->locksFile)) return array();
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($this->locksFile,'r');
+ flock($handle,LOCK_SH);
+
+ // Reading data until the eof
+ $data = stream_get_contents($handle);
+
+ // We're all good
+ fclose($handle);
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (!$data) return array();
+ return $data;
+
+ }
+
+ /**
+ * Saves the lockdata
+ *
+ * @param array $newData
+ * @return void
+ */
+ protected function putData(array $newData) {
+
+ // opening up the file, and creating an exclusive lock
+ $handle = fopen($this->locksFile,'a+');
+ flock($handle,LOCK_EX);
+
+ // We can only truncate and rewind once the lock is acquired.
+ ftruncate($handle,0);
+ rewind($handle);
+
+ fwrite($handle,serialize($newData));
+ fclose($handle);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php
new file mode 100644
index 0000000..520a84b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks\LockInfo;
+
+/**
+ * The Lock manager allows you to handle all file-locks centrally.
+ *
+ * This Lock Manager stores all its data in a database. You must pass a PDO
+ * connection object in the constructor.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend {
+
+ /**
+ * The PDO connection object
+ *
+ * @var pdo
+ */
+ private $pdo;
+
+ /**
+ * The PDO tablename this backend uses.
+ *
+ * @var string
+ */
+ protected $tableName;
+
+ /**
+ * Constructor
+ *
+ * @param PDO $pdo
+ * @param string $tableName
+ */
+ public function __construct(\PDO $pdo, $tableName = 'locks') {
+
+ $this->pdo = $pdo;
+ $this->tableName = $tableName;
+
+ }
+
+ /**
+ * Returns a list of OldSabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks) {
+
+ // NOTE: the following 10 lines or so could be easily replaced by
+ // pure sql. MySQL's non-standard string concatenation prevents us
+ // from doing this though.
+ $query = 'SELECT owner, token, timeout, created, scope, depth, uri FROM '.$this->tableName.' WHERE ((created + timeout) > CAST(? AS UNSIGNED INTEGER)) AND ((uri = ?)';
+ $params = array(time(),$uri);
+
+ // We need to check locks for every part in the uri.
+ $uriParts = explode('/',$uri);
+
+ // We already covered the last part of the uri
+ array_pop($uriParts);
+
+ $currentPath='';
+
+ foreach($uriParts as $part) {
+
+ if ($currentPath) $currentPath.='/';
+ $currentPath.=$part;
+
+ $query.=' OR (depth!=0 AND uri = ?)';
+ $params[] = $currentPath;
+
+ }
+
+ if ($returnChildLocks) {
+
+ $query.=' OR (uri LIKE ?)';
+ $params[] = $uri . '/%';
+
+ }
+ $query.=')';
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($params);
+ $result = $stmt->fetchAll();
+
+ $lockList = array();
+ foreach($result as $row) {
+
+ $lockInfo = new LockInfo();
+ $lockInfo->owner = $row['owner'];
+ $lockInfo->token = $row['token'];
+ $lockInfo->timeout = $row['timeout'];
+ $lockInfo->created = $row['created'];
+ $lockInfo->scope = $row['scope'];
+ $lockInfo->depth = $row['depth'];
+ $lockInfo->uri = $row['uri'];
+ $lockList[] = $lockInfo;
+
+ }
+
+ return $lockList;
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function lock($uri, LockInfo $lockInfo) {
+
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 30*60;
+ $lockInfo->created = time();
+ $lockInfo->uri = $uri;
+
+ $locks = $this->getLocks($uri,false);
+ $exists = false;
+ foreach($locks as $lock) {
+ if ($lock->token == $lockInfo->token) $exists = true;
+ }
+
+ if ($exists) {
+ $stmt = $this->pdo->prepare('UPDATE '.$this->tableName.' SET owner = ?, timeout = ?, scope = ?, depth = ?, uri = ?, created = ? WHERE token = ?');
+ $stmt->execute(array($lockInfo->owner,$lockInfo->timeout,$lockInfo->scope,$lockInfo->depth,$uri,$lockInfo->created,$lockInfo->token));
+ } else {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (owner,timeout,scope,depth,uri,created,token) VALUES (?,?,?,?,?,?,?)');
+ $stmt->execute(array($lockInfo->owner,$lockInfo->timeout,$lockInfo->scope,$lockInfo->depth,$uri,$lockInfo->created,$lockInfo->token));
+ }
+
+ return true;
+
+ }
+
+
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlock($uri, LockInfo $lockInfo) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE uri = ? AND token = ?');
+ $stmt->execute(array($uri,$lockInfo->token));
+
+ return $stmt->rowCount()===1;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php
new file mode 100644
index 0000000..77bba97
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace OldSabre\DAV\Locks;
+
+/**
+ * LockInfo class
+ *
+ * An object of the LockInfo class holds all the information relevant to a
+ * single lock.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class LockInfo {
+
+ /**
+ * A shared lock
+ */
+ const SHARED = 1;
+
+ /**
+ * An exclusive lock
+ */
+ const EXCLUSIVE = 2;
+
+ /**
+ * A never expiring timeout
+ */
+ const TIMEOUT_INFINITE = -1;
+
+ /**
+ * The owner of the lock
+ *
+ * @var string
+ */
+ public $owner;
+
+ /**
+ * The locktoken
+ *
+ * @var string
+ */
+ public $token;
+
+ /**
+ * How long till the lock is expiring
+ *
+ * @var int
+ */
+ public $timeout;
+
+ /**
+ * UNIX Timestamp of when this lock was created
+ *
+ * @var int
+ */
+ public $created;
+
+ /**
+ * Exclusive or shared lock
+ *
+ * @var int
+ */
+ public $scope = self::EXCLUSIVE;
+
+ /**
+ * Depth of lock, can be 0 or OldSabre\DAV\Server::DEPTH_INFINITY
+ */
+ public $depth = 0;
+
+ /**
+ * The uri this lock locks
+ *
+ * TODO: This value is not always set
+ * @var mixed
+ */
+ public $uri;
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php
new file mode 100644
index 0000000..df78b92
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php
@@ -0,0 +1,649 @@
+<?php
+
+namespace OldSabre\DAV\Locks;
+
+use OldSabre\DAV;
+
+/**
+ * Locking plugin
+ *
+ * This plugin provides locking support to a WebDAV server.
+ * The easiest way to get started, is by hooking it up as such:
+ *
+ * $lockBackend = new OldSabre\DAV\Locks\Backend\File('./mylockdb');
+ * $lockPlugin = new OldSabre\DAV\Locks\Plugin($lockBackend);
+ * $server->addPlugin($lockPlugin);
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * locksBackend
+ *
+ * @var Backend\Backend\Interface
+ */
+ protected $locksBackend;
+
+ /**
+ * server
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * __construct
+ *
+ * @param Backend\BackendInterface $locksBackend
+ */
+ public function __construct(Backend\BackendInterface $locksBackend = null) {
+
+ $this->locksBackend = $locksBackend;
+
+ }
+
+ /**
+ * Initializes the plugin
+ *
+ * This method is automatically called by the Server class after addPlugin.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
+ $server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),50);
+ $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties'));
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using OldSabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'locks';
+
+ }
+
+ /**
+ * This method is called by the Server if the user used an HTTP method
+ * the server didn't recognize.
+ *
+ * This plugin intercepts the LOCK and UNLOCK methods.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function unknownMethod($method, $uri) {
+
+ switch($method) {
+
+ case 'LOCK' : $this->httpLock($uri); return false;
+ case 'UNLOCK' : $this->httpUnlock($uri); return false;
+
+ }
+
+ }
+
+ /**
+ * This method is called after most properties have been found
+ * it allows us to add in any Lock-related properties
+ *
+ * @param string $path
+ * @param array $newProperties
+ * @return bool
+ */
+ public function afterGetProperties($path, &$newProperties) {
+
+ foreach($newProperties[404] as $propName=>$discard) {
+
+ switch($propName) {
+
+ case '{DAV:}supportedlock' :
+ $val = false;
+ if ($this->locksBackend) $val = true;
+ $newProperties[200][$propName] = new DAV\Property\SupportedLock($val);
+ unset($newProperties[404][$propName]);
+ break;
+
+ case '{DAV:}lockdiscovery' :
+ $newProperties[200][$propName] = new DAV\Property\LockDiscovery($this->getLocks($path));
+ unset($newProperties[404][$propName]);
+ break;
+
+ }
+
+
+ }
+ return true;
+
+ }
+
+
+ /**
+ * This method is called before the logic for any HTTP method is
+ * handled.
+ *
+ * This plugin uses that feature to intercept access to locked resources.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ switch($method) {
+
+ case 'DELETE' :
+ $lastLock = null;
+ if (!$this->validateLock($uri,$lastLock, true))
+ throw new DAV\Exception\Locked($lastLock);
+ break;
+ case 'MKCOL' :
+ case 'PROPPATCH' :
+ case 'PUT' :
+ case 'PATCH' :
+ $lastLock = null;
+ if (!$this->validateLock($uri,$lastLock))
+ throw new DAV\Exception\Locked($lastLock);
+ break;
+ case 'MOVE' :
+ $lastLock = null;
+ if (!$this->validateLock(array(
+ $uri,
+ $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
+ ),$lastLock, true))
+ throw new DAV\Exception\Locked($lastLock);
+ break;
+ case 'COPY' :
+ $lastLock = null;
+ if (!$this->validateLock(
+ $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
+ $lastLock, true))
+ throw new DAV\Exception\Locked($lastLock);
+ break;
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getHTTPMethods($uri) {
+
+ if ($this->locksBackend)
+ return array('LOCK','UNLOCK');
+
+ return array();
+
+ }
+
+ /**
+ * Returns a list of features for the HTTP OPTIONS Dav: header.
+ *
+ * In this case this is only the number 2. The 2 in the Dav: header
+ * indicates the server supports locks.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array(2);
+
+ }
+
+ /**
+ * Returns all lock information on a particular uri
+ *
+ * This function should return an array with OldSabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array.
+ *
+ * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree
+ * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object
+ * for any possible locks and return those as well.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks = false) {
+
+ $lockList = array();
+
+ if ($this->locksBackend)
+ $lockList = array_merge($lockList,$this->locksBackend->getLocks($uri, $returnChildLocks));
+
+ return $lockList;
+
+ }
+
+ /**
+ * Locks an uri
+ *
+ * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock
+ * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type
+ * of lock (shared or exclusive) and the owner of the lock
+ *
+ * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
+ *
+ * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpLock($uri) {
+
+ $lastLock = null;
+ if (!$this->validateLock($uri,$lastLock)) {
+
+ // If the existing lock was an exclusive lock, we need to fail
+ if (!$lastLock || $lastLock->scope == LockInfo::EXCLUSIVE) {
+ //var_dump($lastLock);
+ throw new DAV\Exception\ConflictingLock($lastLock);
+ }
+
+ }
+
+ if ($body = $this->server->httpRequest->getBody(true)) {
+ // This is a new lock request
+ $lockInfo = $this->parseLockRequest($body);
+ $lockInfo->depth = $this->server->getHTTPDepth();
+ $lockInfo->uri = $uri;
+ if($lastLock && $lockInfo->scope != LockInfo::SHARED) throw new DAV\Exception\ConflictingLock($lastLock);
+
+ } elseif ($lastLock) {
+
+ // This must have been a lock refresh
+ $lockInfo = $lastLock;
+
+ // The resource could have been locked through another uri.
+ if ($uri!=$lockInfo->uri) $uri = $lockInfo->uri;
+
+ } else {
+
+ // There was neither a lock refresh nor a new lock request
+ throw new DAV\Exception\BadRequest('An xml body is required for lock requests');
+
+ }
+
+ if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout;
+
+ $newFile = false;
+
+ // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first
+ try {
+ $this->server->tree->getNodeForPath($uri);
+
+ // We need to call the beforeWriteContent event for RFC3744
+ // Edit: looks like this is not used, and causing problems now.
+ //
+ // See Issue 222
+ // $this->server->broadcastEvent('beforeWriteContent',array($uri));
+
+ } catch (DAV\Exception\NotFound $e) {
+
+ // It didn't, lets create it
+ $this->server->createFile($uri,fopen('php://memory','r'));
+ $newFile = true;
+
+ }
+
+ $this->lockNode($uri,$lockInfo);
+
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Lock-Token','<opaquelocktoken:' . $lockInfo->token . '>');
+ $this->server->httpResponse->sendStatus($newFile?201:200);
+ $this->server->httpResponse->sendBody($this->generateLockResponse($lockInfo));
+
+ }
+
+ /**
+ * Unlocks a uri
+ *
+ * This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header
+ * The server should return 204 (No content) on success
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpUnlock($uri) {
+
+ $lockToken = $this->server->httpRequest->getHeader('Lock-Token');
+
+ // If the locktoken header is not supplied, we need to throw a bad request exception
+ if (!$lockToken) throw new DAV\Exception\BadRequest('No lock token was supplied');
+
+ $locks = $this->getLocks($uri);
+
+ // Windows sometimes forgets to include < and > in the Lock-Token
+ // header
+ if ($lockToken[0]!=='<') $lockToken = '<' . $lockToken . '>';
+
+ foreach($locks as $lock) {
+
+ if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
+
+ $this->unlockNode($uri,$lock);
+ $this->server->httpResponse->setHeader('Content-Length','0');
+ $this->server->httpResponse->sendStatus(204);
+ return;
+
+ }
+
+ }
+
+ // If we got here, it means the locktoken was invalid
+ throw new DAV\Exception\LockTokenMatchesRequestUri();
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored
+ * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function lockNode($uri,LockInfo $lockInfo) {
+
+ if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return;
+
+ if ($this->locksBackend) return $this->locksBackend->lock($uri,$lockInfo);
+ throw new DAV\Exception\MethodNotAllowed('Locking support is not enabled for this resource. No Locking backend was found so if you didn\'t expect this error, please check your configuration.');
+
+ }
+
+ /**
+ * Unlocks a uri
+ *
+ * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlockNode($uri, LockInfo $lockInfo) {
+
+ if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return;
+ if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo);
+
+ }
+
+
+ /**
+ * Returns the contents of the HTTP Timeout header.
+ *
+ * The method formats the header into an integer.
+ *
+ * @return int
+ */
+ public function getTimeoutHeader() {
+
+ $header = $this->server->httpRequest->getHeader('Timeout');
+
+ if ($header) {
+
+ if (stripos($header,'second-')===0) $header = (int)(substr($header,7));
+ else if (strtolower($header)=='infinite') $header = LockInfo::TIMEOUT_INFINITE;
+ else throw new DAV\Exception\BadRequest('Invalid HTTP timeout header');
+
+ } else {
+
+ $header = 0;
+
+ }
+
+ return $header;
+
+ }
+
+ /**
+ * Generates the response for successful LOCK requests
+ *
+ * @param LockInfo $lockInfo
+ * @return string
+ */
+ protected function generateLockResponse(LockInfo $lockInfo) {
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+
+ $prop = $dom->createElementNS('DAV:','d:prop');
+ $dom->appendChild($prop);
+
+ $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery');
+ $prop->appendChild($lockDiscovery);
+
+ $lockObj = new DAV\Property\LockDiscovery(array($lockInfo),true);
+ $lockObj->serialize($this->server,$lockDiscovery);
+
+ return $dom->saveXML();
+
+ }
+
+ /**
+ * validateLock should be called when a write operation is about to happen
+ * It will check if the requested url is locked, and see if the correct lock tokens are passed
+ *
+ * @param mixed $urls List of relevant urls. Can be an array, a string or nothing at all for the current request uri
+ * @param mixed $lastLock This variable will be populated with the last checked lock object (OldSabre\DAV\Locks\LockInfo)
+ * @param bool $checkChildLocks If set to true, this function will also look for any locks set on child resources of the supplied urls. This is needed for for example deletion of entire trees.
+ * @return bool
+ */
+ protected function validateLock($urls = null,&$lastLock = null, $checkChildLocks = false) {
+
+ if (is_null($urls)) {
+ $urls = array($this->server->getRequestUri());
+ } elseif (is_string($urls)) {
+ $urls = array($urls);
+ } elseif (!is_array($urls)) {
+ throw new DAV\Exception('The urls parameter should either be null, a string or an array');
+ }
+
+ $conditions = $this->getIfConditions();
+
+ // We're going to loop through the urls and make sure all lock conditions are satisfied
+ foreach($urls as $url) {
+
+ $locks = $this->getLocks($url, $checkChildLocks);
+
+ // If there were no conditions, but there were locks, we fail
+ if (!$conditions && $locks) {
+ reset($locks);
+ $lastLock = current($locks);
+ return false;
+ }
+
+ // If there were no locks or conditions, we go to the next url
+ if (!$locks && !$conditions) continue;
+
+ foreach($conditions as $condition) {
+
+ if (!$condition['uri']) {
+ $conditionUri = $this->server->getRequestUri();
+ } else {
+ $conditionUri = $this->server->calculateUri($condition['uri']);
+ }
+
+ // If the condition has a url, and it isn't part of the affected url at all, check the next condition
+ if ($conditionUri && strpos($url,$conditionUri)!==0) continue;
+
+ // The tokens array contians arrays with 2 elements. 0=true/false for normal/not condition, 1=locktoken
+ // At least 1 condition has to be satisfied
+ foreach($condition['tokens'] as $conditionToken) {
+
+ $etagValid = true;
+ $lockValid = true;
+
+ // key 2 can contain an etag
+ if ($conditionToken[2]) {
+
+ $uri = $conditionUri?$conditionUri:$this->server->getRequestUri();
+ $node = $this->server->tree->getNodeForPath($uri);
+ $etagValid = $node instanceof DAV\IFile && $node->getETag()==$conditionToken[2];
+
+ }
+
+ // key 1 can contain a lock token
+ if ($conditionToken[1]) {
+
+ $lockValid = false;
+ // Match all the locks
+ foreach($locks as $lockIndex=>$lock) {
+
+ $lockToken = 'opaquelocktoken:' . $lock->token;
+
+ // Checking NOT
+ if (!$conditionToken[0] && $lockToken != $conditionToken[1]) {
+
+ // Condition valid, onto the next
+ $lockValid = true;
+ break;
+ }
+ if ($conditionToken[0] && $lockToken == $conditionToken[1]) {
+
+ $lastLock = $lock;
+ // Condition valid and lock matched
+ unset($locks[$lockIndex]);
+ $lockValid = true;
+ break;
+
+ }
+
+ }
+
+ }
+
+ // If, after checking both etags and locks they are stil valid,
+ // we can continue with the next condition.
+ if ($etagValid && $lockValid) continue 2;
+ }
+ // No conditions matched, so we fail
+ throw new DAV\Exception\PreconditionFailed('The tokens provided in the if header did not match','If');
+ }
+
+ // Conditions were met, we'll also need to check if all the locks are gone
+ if (count($locks)) {
+
+ reset($locks);
+
+ // There's still locks, we fail
+ $lastLock = current($locks);
+ return false;
+
+ }
+
+
+ }
+
+ // We got here, this means every condition was satisfied
+ return true;
+
+ }
+
+ /**
+ * This method is created to extract information from the WebDAV HTTP 'If:' header
+ *
+ * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
+ * The function will return an array, containing structs with the following keys
+ *
+ * * uri - the uri the condition applies to. If this is returned as an
+ * empty string, this implies it's referring to the request url.
+ * * tokens - The lock token. another 2 dimensional array containing 2 elements (0 = true/false.. If this is a negative condition its set to false, 1 = the actual token)
+ * * etag - an etag, if supplied
+ *
+ * @return array
+ */
+ public function getIfConditions() {
+
+ $header = $this->server->httpRequest->getHeader('If');
+ if (!$header) return array();
+
+ $matches = array();
+
+ $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im';
+ preg_match_all($regex,$header,$matches,PREG_SET_ORDER);
+
+ $conditions = array();
+
+ foreach($matches as $match) {
+
+ $condition = array(
+ 'uri' => $match['uri'],
+ 'tokens' => array(
+ array($match['not']?0:1,$match['token'],isset($match['etag'])?$match['etag']:'')
+ ),
+ );
+
+ if (!$condition['uri'] && count($conditions)) $conditions[count($conditions)-1]['tokens'][] = array(
+ $match['not']?0:1,
+ $match['token'],
+ isset($match['etag'])?$match['etag']:''
+ );
+ else {
+ $conditions[] = $condition;
+ }
+
+ }
+
+ return $conditions;
+
+ }
+
+ /**
+ * Parses a webdav lock xml body, and returns a new OldSabre\DAV\Locks\LockInfo object
+ *
+ * @param string $body
+ * @return DAV\Locks\LockInfo
+ */
+ protected function parseLockRequest($body) {
+
+ // Fixes an XXE vulnerability on PHP versions older than 5.3.23 or
+ // 5.4.13.
+ $previous = libxml_disable_entity_loader(true);
+
+
+ $xml = simplexml_load_string(
+ DAV\XMLUtil::convertDAVNamespace($body),
+ null,
+ LIBXML_NOWARNING);
+ libxml_disable_entity_loader($previous);
+
+ $xml->registerXPathNamespace('d','urn:DAV');
+ $lockInfo = new LockInfo();
+
+ $children = $xml->children("urn:DAV");
+ $lockInfo->owner = (string)$children->owner;
+
+ $lockInfo->token = DAV\UUIDUtil::getUUID();
+ $lockInfo->scope = count($xml->xpath('d:lockscope/d:exclusive'))>0 ? LockInfo::EXCLUSIVE : LockInfo::SHARED;
+
+ return $lockInfo;
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php
new file mode 100644
index 0000000..cfd159b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace OldSabre\DAV\Mount;
+
+use OldSabre\DAV;
+
+/**
+ * This plugin provides support for RFC4709: Mounting WebDAV servers
+ *
+ * Simply append ?mount to any collection to generate the davmount response.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * Reference to Server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin and registers event handles
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'), 90);
+
+ }
+
+ /**
+ * 'beforeMethod' event handles. This event handles intercepts GET requests ending
+ * with ?mount
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ if ($method!='GET') return;
+ if ($this->server->httpRequest->getQueryString()!='mount') return;
+
+ $currentUri = $this->server->httpRequest->getAbsoluteUri();
+
+ // Stripping off everything after the ?
+ list($currentUri) = explode('?',$currentUri);
+
+ $this->davMount($currentUri);
+
+ // Returning false to break the event chain
+ return false;
+
+ }
+
+ /**
+ * Generates the davmount response
+ *
+ * @param string $uri absolute uri
+ * @return void
+ */
+ public function davMount($uri) {
+
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type','application/davmount+xml');
+ ob_start();
+ echo '<?xml version="1.0"?>', "\n";
+ echo "<dm:mount xmlns:dm=\"http://purl.org/NET/webdav/mount\">\n";
+ echo " <dm:url>", htmlspecialchars($uri, ENT_NOQUOTES, 'UTF-8'), "</dm:url>\n";
+ echo "</dm:mount>";
+ $this->server->httpResponse->sendBody(ob_get_clean());
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php
new file mode 100644
index 0000000..77d7229
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Node class
+ *
+ * This is a helper class, that should aid in getting nodes setup.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Node implements INode {
+
+ /**
+ * Returns the last modification time
+ *
+ * In this case, it will simply return the current time
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return time();
+
+ }
+
+ /**
+ * Deletes the current node
+ *
+ * @throws OldSabre\DAV\Exception\Forbidden
+ * @return void
+ */
+ public function delete() {
+
+ throw new Exception\Forbidden('Permission denied to delete node');
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @throws OldSabre\DAV\Exception\Forbidden
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new Exception\Forbidden('Permission denied to rename file');
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php
new file mode 100644
index 0000000..57057d9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * ObjectTree class
+ *
+ * This implementation of the Tree class makes use of the INode, IFile and ICollection API's
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ObjectTree extends Tree {
+
+ /**
+ * The root node
+ *
+ * @var ICollection
+ */
+ protected $rootNode;
+
+ /**
+ * This is the node cache. Accessed nodes are stored here
+ *
+ * @var array
+ */
+ protected $cache = array();
+
+ /**
+ * Creates the object
+ *
+ * This method expects the rootObject to be passed as a parameter
+ *
+ * @param ICollection $rootNode
+ */
+ public function __construct(ICollection $rootNode) {
+
+ $this->rootNode = $rootNode;
+
+ }
+
+ /**
+ * Returns the INode object for the requested path
+ *
+ * @param string $path
+ * @return INode
+ */
+ public function getNodeForPath($path) {
+
+ $path = trim($path,'/');
+ if (isset($this->cache[$path])) return $this->cache[$path];
+
+ // Is it the root node?
+ if (!strlen($path)) {
+ return $this->rootNode;
+ }
+
+ // Attempting to fetch its parent
+ list($parentName, $baseName) = URLUtil::splitPath($path);
+
+ // If there was no parent, we must simply ask it from the root node.
+ if ($parentName==="") {
+ $node = $this->rootNode->getChild($baseName);
+ } else {
+ // Otherwise, we recursively grab the parent and ask him/her.
+ $parent = $this->getNodeForPath($parentName);
+
+ if (!($parent instanceof ICollection))
+ throw new Exception\NotFound('Could not find node at path: ' . $path);
+
+ $node = $parent->getChild($baseName);
+
+ }
+
+ $this->cache[$path] = $node;
+ return $node;
+
+ }
+
+ /**
+ * This function allows you to check if a node exists.
+ *
+ * @param string $path
+ * @return bool
+ */
+ public function nodeExists($path) {
+
+ try {
+
+ // The root always exists
+ if ($path==='') return true;
+
+ list($parent, $base) = URLUtil::splitPath($path);
+
+ $parentNode = $this->getNodeForPath($parent);
+ if (!$parentNode instanceof ICollection) return false;
+ return $parentNode->childExists($base);
+
+ } catch (Exception\NotFound $e) {
+
+ return false;
+
+ }
+
+ }
+
+ /**
+ * Returns a list of childnodes for a given path.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getChildren($path) {
+
+ $node = $this->getNodeForPath($path);
+ $children = $node->getChildren();
+ foreach($children as $child) {
+
+ $this->cache[trim($path,'/') . '/' . $child->getName()] = $child;
+
+ }
+ return $children;
+
+ }
+
+ /**
+ * This method is called with every tree update
+ *
+ * Examples of tree updates are:
+ * * node deletions
+ * * node creations
+ * * copy
+ * * move
+ * * renaming nodes
+ *
+ * If Tree classes implement a form of caching, this will allow
+ * them to make sure caches will be expired.
+ *
+ * If a path is passed, it is assumed that the entire subtree is dirty
+ *
+ * @param string $path
+ * @return void
+ */
+ public function markDirty($path) {
+
+ // We don't care enough about sub-paths
+ // flushing the entire cache
+ $path = trim($path,'/');
+ foreach($this->cache as $nodePath=>$node) {
+ if ($nodePath == $path || strpos($nodePath,$path.'/')===0)
+ unset($this->cache[$nodePath]);
+
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php
new file mode 100644
index 0000000..2fbdf1e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace OldSabre\DAV\PartialUpdate;
+
+use OldSabre\DAV;
+
+/**
+ * This interface is deprecated. Use IPatchSupport instead.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
+ * @license http://sabre.io/license/ Modified BSD License
+ * @deprecated
+ */
+interface IFile extends DAV\IFile {
+
+ /**
+ * Updates the data at a given offset
+ *
+ * The data argument is a readable stream resource.
+ * The offset argument is an integer describing the offset. Contrary to
+ * what's sent in the request, the offset here is a 0-based index.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * @param resource $data
+ * @param integer $offset
+ * @return string|null
+ */
+ function putRange($data, $offset);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php
new file mode 100644
index 0000000..e6e0989
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace OldSabre\DAV\PartialUpdate;
+
+use OldSabre\DAV;
+
+/**
+ * This interface provides a way to modify only part of a target resource
+ * It may be used to update a file chunk, upload big a file into smaller
+ * chunks or resume an upload
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IPatchSupport extends DAV\IFile {
+
+ /**
+ * Updates the file based on a range specification.
+ *
+ * The first argument is the data, which is either a readable stream
+ * resource or a string.
+ *
+ * The second argument is the type of update we're doing.
+ * This is either:
+ * * 1. append
+ * * 2. update based on a start byte
+ * * 3. update based on an end byte
+ *;
+ * The third argument is the start or end byte.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * @param resource|string $data
+ * @param int $rangeType
+ * @param int $offset
+ * @return string|null
+ */
+ function patch($data, $rangeType, $offset = null);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php
new file mode 100644
index 0000000..c1e5363
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php
@@ -0,0 +1,246 @@
+<?php
+
+namespace OldSabre\DAV\PartialUpdate;
+
+use OldSabre\DAV;
+
+/**
+ * Partial update plugin (Patch method)
+ *
+ * This plugin provides a way to modify only part of a target resource
+ * It may bu used to update a file chunk, upload big a file into smaller
+ * chunks or resume an upload.
+ *
+ * $patchPlugin = new \OldSabre\DAV\PartialUpdate\Plugin();
+ * $server->addPlugin($patchPlugin);
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ const RANGE_APPEND = 1;
+ const RANGE_START = 2;
+ const RANGE_END = 3;
+
+ /**
+ * Reference to server
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin
+ *
+ * This method is automatically called by the Server class after addPlugin.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(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 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)) {
+ $node = $tree->getNodeForPath($uri);
+ if ($node instanceof IFile || $node instanceof IPatchSupport) {
+ 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 IFile && !$node instanceof IPatchSupport) {
+ throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.');
+ }
+
+ $range = $this->getHTTPUpdateRange();
+
+ if (!$range) {
+ throw new 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 DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "' . $contentType . '"');
+ }
+
+ $len = $this->server->httpRequest->getHeader('Content-Length');
+ if (!$len) throw new DAV\Exception\LengthRequired('A Content-Length header is required');
+
+ switch($range[0]) {
+ case self::RANGE_START :
+ // Calculate the end-range if it doesn't exist.
+ if (!$range[2]) {
+ $range[2] = $range[1] + $len - 1;
+ } else {
+ if ($range[2] < $range[1]) {
+ throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[2] . ') is lower than the start offset (' . $range[1] . ')');
+ }
+ if($range[2] - $range[1] + 1 != $len) {
+ throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length (' . $len . ') is not consistent with begin (' . $range[1] . ') and end (' . $range[2] . ') offsets');
+ }
+ }
+ break;
+ }
+ // 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();
+
+
+ if ($node instanceof IPatchSupport) {
+ $etag = $node->patch($body, $range[0], isset($range[1])?$range[1]:null);
+ } else {
+ // The old interface
+ switch($range[0]) {
+ case self::RANGE_APPEND :
+ throw new DAV\Exception\NotImplemented('This node does not support the append syntax. Please upgrade it to IPatchSupport');
+ case self::RANGE_START :
+ $etag = $node->putRange($body, $range[1]);
+ break;
+ case self::RANGE_END :
+ throw new DAV\Exception\NotImplemented('This node does not support the end-range syntax. Please upgrade it to IPatchSupport');
+ break;
+ }
+ }
+
+ $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. It returns array(1) if it was an append request, array(2,
+ * $start, $end) if it's a start and end range, lastly it's array(3,
+ * $endoffset) if the offset was negative, and should be calculated from
+ * the end of the file.
+ *
+ * Examples:
+ *
+ * null - invalid
+ * array(1) - append
+ * array(2,10,15) - update bytes 10, 11, 12, 13, 14, 15
+ * array(2,10,null) - update bytes 10 until the end of the patch body
+ * array(3,-5) - update from 5 bytes from the end of the file.
+ *
+ * @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('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i',$range,$matches)) return null;
+
+ if ($matches[1]==='append') {
+ return array(self::RANGE_APPEND);
+ } elseif (strlen($matches[2])>0) {
+ return array(self::RANGE_START, $matches[2], $matches[3]?:null);
+ } elseif ($matches[4]) {
+ return array(self::RANGE_END, $matches[4]);
+ } else {
+ return null;
+ }
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php
new file mode 100644
index 0000000..88500c4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Abstract property class
+ *
+ * Extend this class to create custom complex properties
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Property implements PropertyInterface {
+
+ /**
+ * Unserializes the property.
+ *
+ * This static method should return a an instance of this object.
+ *
+ * @param \DOMElement $prop
+ * @return DAV\IProperty
+ */
+ static function unserialize(\DOMElement $prop) {
+
+ throw new Exception('Unserialize has not been implemented for this class');
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php
new file mode 100644
index 0000000..33f53c2
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+use OldSabre\HTTP;
+
+/**
+ * This property represents the {DAV:}getlastmodified property.
+ *
+ * Although this is normally a simple property, windows requires us to add
+ * some new attributes.
+ *
+ * This class uses unix timestamps internally, and converts them to RFC 1123 times for
+ * serialization
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class GetLastModified extends DAV\Property {
+
+ /**
+ * time
+ *
+ * @var int
+ */
+ public $time;
+
+ /**
+ * __construct
+ *
+ * @param int|DateTime $time
+ */
+ public function __construct($time) {
+
+ if ($time instanceof \DateTime) {
+ $this->time = $time;
+ } elseif (is_int($time) || ctype_digit($time)) {
+ $this->time = new \DateTime('@' . $time);
+ } else {
+ $this->time = new \DateTime($time);
+ }
+
+ // Setting timezone to UTC
+ $this->time->setTimezone(new \DateTimeZone('UTC'));
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $prop) {
+
+ $doc = $prop->ownerDocument;
+ //$prop->setAttribute('xmlns:b','urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/');
+ //$prop->setAttribute('b:dt','dateTime.rfc1123');
+ $prop->nodeValue = HTTP\Util::toHTTPDate($this->time);
+
+ }
+
+ /**
+ * getTime
+ *
+ * @return \DateTime
+ */
+ public function getTime() {
+
+ return $this->time;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php
new file mode 100644
index 0000000..81f9bfb
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * Href property
+ *
+ * The href property represents a url within a {DAV:}href element.
+ * This is used by many WebDAV extensions, but not really within the WebDAV core spec
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Href extends DAV\Property implements IHref {
+
+ /**
+ * href
+ *
+ * @var string
+ */
+ private $href;
+
+ /**
+ * Automatically prefix the url with the server base directory
+ *
+ * @var bool
+ */
+ private $autoPrefix = true;
+
+ /**
+ * __construct
+ *
+ * @param string $href
+ * @param bool $autoPrefix
+ */
+ public function __construct($href, $autoPrefix = true) {
+
+ $this->href = $href;
+ $this->autoPrefix = $autoPrefix;
+
+ }
+
+ /**
+ * Returns the uri
+ *
+ * @return string
+ */
+ public function getHref() {
+
+ return $this->href;
+
+ }
+
+ /**
+ * Serializes this property.
+ *
+ * It will additionally prepend the href property with the server's base uri.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $dom) {
+
+ $prefix = $server->xmlNamespaces['DAV:'];
+ $elem = $dom->ownerDocument->createElement($prefix . ':href');
+
+ if ($this->autoPrefix) {
+ $value = $server->getBaseUri() . DAV\URLUtil::encodePath($this->href);
+ } else {
+ $value = $this->href;
+ }
+ $elem->appendChild($dom->ownerDocument->createTextNode($value));
+
+ $dom->appendChild($elem);
+
+ }
+
+ /**
+ * Unserializes this property from a DOM Element
+ *
+ * This method returns an instance of this class.
+ * It will only decode {DAV:}href values. For non-compatible elements null will be returned.
+ *
+ * @param \DOMElement $dom
+ * @return DAV\Property\Href
+ */
+ static function unserialize(\DOMElement $dom) {
+
+ if ($dom->firstChild && DAV\XMLUtil::toClarkNotation($dom->firstChild)==='{DAV:}href') {
+ return new self($dom->firstChild->textContent,false);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php
new file mode 100644
index 0000000..8233db1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * HrefList property
+ *
+ * This property contains multiple {DAV:}href elements, each containing a url.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class HrefList extends DAV\Property {
+
+ /**
+ * hrefs
+ *
+ * @var array
+ */
+ private $hrefs;
+
+ /**
+ * Automatically prefix the url with the server base directory
+ *
+ * @var bool
+ */
+ private $autoPrefix = true;
+
+ /**
+ * __construct
+ *
+ * @param array $hrefs
+ * @param bool $autoPrefix
+ */
+ public function __construct(array $hrefs, $autoPrefix = true) {
+
+ $this->hrefs = $hrefs;
+ $this->autoPrefix = $autoPrefix;
+
+ }
+
+ /**
+ * Returns the uris
+ *
+ * @return array
+ */
+ public function getHrefs() {
+
+ return $this->hrefs;
+
+ }
+
+ /**
+ * Serializes this property.
+ *
+ * It will additionally prepend the href property with the server's base uri.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $dom) {
+
+ $prefix = $server->xmlNamespaces['DAV:'];
+
+ foreach($this->hrefs as $href) {
+
+ $elem = $dom->ownerDocument->createElement($prefix . ':href');
+ if ($this->autoPrefix) {
+ $value = $server->getBaseUri() . DAV\URLUtil::encodePath($href);
+ } else {
+ $value = $href;
+ }
+ $elem->appendChild($dom->ownerDocument->createTextNode($value));
+
+ $dom->appendChild($elem);
+ }
+
+ }
+
+ /**
+ * Unserializes this property from a DOM Element
+ *
+ * This method returns an instance of this class.
+ * It will only decode {DAV:}href values.
+ *
+ * @param \DOMElement $dom
+ * @return DAV\Property\HrefList
+ */
+ static function unserialize(\DOMElement $dom) {
+
+ $hrefs = array();
+ foreach($dom->childNodes as $child) {
+ if (DAV\XMLUtil::toClarkNotation($child)==='{DAV:}href') {
+ $hrefs[] = $child->textContent;
+ }
+ }
+ return new self($hrefs, false);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php
new file mode 100644
index 0000000..ce24250
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+/**
+ * IHref interface
+ *
+ * Any property implementing this interface can expose a related url.
+ * This is used by certain subsystems to aquire more information about for example
+ * the owner of a file
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IHref {
+
+ /**
+ * getHref
+ *
+ * @return string
+ */
+ function getHref();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php
new file mode 100644
index 0000000..0a055db
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * Represents {DAV:}lockdiscovery property
+ *
+ * This property contains all the open locks on a given resource
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class LockDiscovery extends DAV\Property {
+
+ /**
+ * locks
+ *
+ * @var array
+ */
+ public $locks;
+
+ /**
+ * Should we show the locktoken as well?
+ *
+ * @var bool
+ */
+ public $revealLockToken;
+
+ /**
+ * Hides the {DAV:}lockroot element from the response.
+ *
+ * It was reported that showing the lockroot in the response can break
+ * Office 2000 compatibility.
+ */
+ static public $hideLockRoot = false;
+
+ /**
+ * __construct
+ *
+ * @param array $locks
+ * @param bool $revealLockToken
+ */
+ public function __construct($locks, $revealLockToken = false) {
+
+ $this->locks = $locks;
+ $this->revealLockToken = $revealLockToken;
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $prop) {
+
+ $doc = $prop->ownerDocument;
+
+ foreach($this->locks as $lock) {
+
+ $activeLock = $doc->createElementNS('DAV:','d:activelock');
+ $prop->appendChild($activeLock);
+
+ $lockScope = $doc->createElementNS('DAV:','d:lockscope');
+ $activeLock->appendChild($lockScope);
+
+ $lockScope->appendChild($doc->createElementNS('DAV:','d:' . ($lock->scope==DAV\Locks\LockInfo::EXCLUSIVE?'exclusive':'shared')));
+
+ $lockType = $doc->createElementNS('DAV:','d:locktype');
+ $activeLock->appendChild($lockType);
+
+ $lockType->appendChild($doc->createElementNS('DAV:','d:write'));
+
+ /* {DAV:}lockroot */
+ if (!self::$hideLockRoot) {
+ $lockRoot = $doc->createElementNS('DAV:','d:lockroot');
+ $activeLock->appendChild($lockRoot);
+ $href = $doc->createElementNS('DAV:','d:href');
+ $href->appendChild($doc->createTextNode($server->getBaseUri() . $lock->uri));
+ $lockRoot->appendChild($href);
+ }
+
+ $activeLock->appendChild($doc->createElementNS('DAV:','d:depth',($lock->depth == DAV\Server::DEPTH_INFINITY?'infinity':$lock->depth)));
+ $activeLock->appendChild($doc->createElementNS('DAV:','d:timeout','Second-' . $lock->timeout));
+
+ if ($this->revealLockToken) {
+ $lockToken = $doc->createElementNS('DAV:','d:locktoken');
+ $activeLock->appendChild($lockToken);
+ $lockToken->appendChild($doc->createElementNS('DAV:','d:href','opaquelocktoken:' . $lock->token));
+ }
+
+ $activeLock->appendChild($doc->createElementNS('DAV:','d:owner',$lock->owner));
+
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php
new file mode 100644
index 0000000..eee2825
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * This class represents the {DAV:}resourcetype property
+ *
+ * Normally for files this is empty, and for collection {DAV:}collection.
+ * However, other specs define different values for this.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ResourceType extends DAV\Property {
+
+ /**
+ * resourceType
+ *
+ * @var array
+ */
+ public $resourceType = array();
+
+ /**
+ * __construct
+ *
+ * @param mixed $resourceType
+ */
+ public function __construct($resourceType = array()) {
+
+ if ($resourceType === DAV\Server::NODE_FILE)
+ $this->resourceType = array();
+ elseif ($resourceType === DAV\Server::NODE_DIRECTORY)
+ $this->resourceType = array('{DAV:}collection');
+ elseif (is_array($resourceType))
+ $this->resourceType = $resourceType;
+ else
+ $this->resourceType = array($resourceType);
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $prop) {
+
+ $propName = null;
+ $rt = $this->resourceType;
+
+ foreach($rt as $resourceType) {
+ if (preg_match('/^{([^}]*)}(.*)$/',$resourceType,$propName)) {
+
+ if (isset($server->xmlNamespaces[$propName[1]])) {
+ $prop->appendChild($prop->ownerDocument->createElement($server->xmlNamespaces[$propName[1]] . ':' . $propName[2]));
+ } else {
+ $prop->appendChild($prop->ownerDocument->createElementNS($propName[1],'custom:' . $propName[2]));
+ }
+
+ }
+ }
+
+ }
+
+ /**
+ * Returns the values in clark-notation
+ *
+ * For example array('{DAV:}collection')
+ *
+ * @return array
+ */
+ public function getValue() {
+
+ return $this->resourceType;
+
+ }
+
+ /**
+ * Checks if the principal contains a certain value
+ *
+ * @param string $type
+ * @return bool
+ */
+ public function is($type) {
+
+ return in_array($type, $this->resourceType);
+
+ }
+
+ /**
+ * Adds a resourcetype value to this property
+ *
+ * @param string $type
+ * @return void
+ */
+ public function add($type) {
+
+ $this->resourceType[] = $type;
+ $this->resourceType = array_unique($this->resourceType);
+
+ }
+
+ /**
+ * Unserializes a DOM element into a ResourceType property.
+ *
+ * @param \DOMElement $dom
+ * @return DAV\Property\ResourceType
+ */
+ static public function unserialize(\DOMElement $dom) {
+
+ $value = array();
+ foreach($dom->childNodes as $child) {
+
+ $value[] = DAV\XMLUtil::toClarkNotation($child);
+
+ }
+
+ return new self($value);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php
new file mode 100644
index 0000000..0e1b836
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * Response property
+ *
+ * This class represents the {DAV:}response XML element.
+ * This is used by the Server class to encode individual items within a multistatus
+ * response.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Response extends DAV\Property implements IHref {
+
+ /**
+ * Url for the response
+ *
+ * @var string
+ */
+ private $href;
+
+ /**
+ * Propertylist, ordered by HTTP status code
+ *
+ * @var array
+ */
+ private $responseProperties;
+
+ /**
+ * The responseProperties argument is a list of properties
+ * within an array with keys representing HTTP status codes
+ *
+ * @param string $href
+ * @param array $responseProperties
+ */
+ public function __construct($href, array $responseProperties) {
+
+ $this->href = $href;
+ $this->responseProperties = $responseProperties;
+
+ }
+
+ /**
+ * Returns the url
+ *
+ * @return string
+ */
+ public function getHref() {
+
+ return $this->href;
+
+ }
+
+ /**
+ * Returns the property list
+ *
+ * @return array
+ */
+ public function getResponseProperties() {
+
+ return $this->responseProperties;
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $dom) {
+
+ $document = $dom->ownerDocument;
+ $properties = $this->responseProperties;
+
+ $xresponse = $document->createElement('d:response');
+ $dom->appendChild($xresponse);
+
+ $uri = DAV\URLUtil::encodePath($this->href);
+
+ // Adding the baseurl to the beginning of the url
+ $uri = $server->getBaseUri() . $uri;
+
+ $xresponse->appendChild($document->createElement('d:href',$uri));
+
+ // The properties variable is an array containing properties, grouped by
+ // HTTP status
+ foreach($properties as $httpStatus=>$propertyGroup) {
+
+ // The 'href' is also in this array, and it's special cased.
+ // We will ignore it
+ if ($httpStatus=='href') continue;
+
+ // If there are no properties in this group, we can also just carry on
+ if (!count($propertyGroup)) continue;
+
+ $xpropstat = $document->createElement('d:propstat');
+ $xresponse->appendChild($xpropstat);
+
+ $xprop = $document->createElement('d:prop');
+ $xpropstat->appendChild($xprop);
+
+ $nsList = $server->xmlNamespaces;
+
+ foreach($propertyGroup as $propertyName=>$propertyValue) {
+
+ $propName = null;
+ preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName);
+
+ // special case for empty namespaces
+ if ($propName[1]=='') {
+
+ $currentProperty = $document->createElement($propName[2]);
+ $xprop->appendChild($currentProperty);
+ $currentProperty->setAttribute('xmlns','');
+
+ } else {
+
+ if (!isset($nsList[$propName[1]])) {
+ $nsList[$propName[1]] = 'x' . count($nsList);
+ }
+
+ // If the namespace was defined in the top-level xml namespaces, it means
+ // there was already a namespace declaration, and we don't have to worry about it.
+ if (isset($server->xmlNamespaces[$propName[1]])) {
+ $currentProperty = $document->createElement($nsList[$propName[1]] . ':' . $propName[2]);
+ } else {
+ $currentProperty = $document->createElementNS($propName[1],$nsList[$propName[1]].':' . $propName[2]);
+ }
+ $xprop->appendChild($currentProperty);
+
+ }
+
+ if (is_scalar($propertyValue)) {
+ $text = $document->createTextNode($propertyValue);
+ $currentProperty->appendChild($text);
+ } elseif ($propertyValue instanceof DAV\PropertyInterface) {
+ $propertyValue->serialize($server,$currentProperty);
+ } elseif (!is_null($propertyValue)) {
+ throw new DAV\Exception('Unknown property value type: ' . gettype($propertyValue) . ' for property: ' . $propertyName);
+ }
+
+ }
+
+ $xpropstat->appendChild($document->createElement('d:status',$server->httpResponse->getStatusMessage($httpStatus)));
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php
new file mode 100644
index 0000000..ebf9296
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * ResponseList property
+ *
+ * This class represents multiple {DAV:}response XML elements.
+ * This is used by the Server class to encode items within a multistatus
+ * response.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ResponseList extends DAV\Property {
+
+ /**
+ * Response objects.
+ *
+ * @var array
+ */
+ private $responses;
+
+ /**
+ * The only valid argument is a list of OldSabre\DAV\Property\Response
+ * objects.
+ *
+ * @param array $responses;
+ */
+ public function __construct($responses) {
+
+ foreach($responses as $response) {
+ if (!($response instanceof Response)) {
+ throw new \InvalidArgumentException('You must pass an array of OldSabre\DAV\Property\Response objects');
+ }
+ }
+ $this->responses = $responses;
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $dom) {
+
+ foreach($this->responses as $response) {
+ $response->serialize($server, $dom);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php
new file mode 100644
index 0000000..1699826
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * This class represents the {DAV:}supportedlock property
+ *
+ * This property contains information about what kind of locks
+ * this server supports.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedLock extends DAV\Property {
+
+ /**
+ * supportsLocks
+ *
+ * @var mixed
+ */
+ public $supportsLocks = false;
+
+ /**
+ * __construct
+ *
+ * @param mixed $supportsLocks
+ */
+ public function __construct($supportsLocks) {
+
+ $this->supportsLocks = $supportsLocks;
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $prop) {
+
+ $doc = $prop->ownerDocument;
+
+ if (!$this->supportsLocks) return null;
+
+ $lockEntry1 = $doc->createElement('d:lockentry');
+ $lockEntry2 = $doc->createElement('d:lockentry');
+
+ $prop->appendChild($lockEntry1);
+ $prop->appendChild($lockEntry2);
+
+ $lockScope1 = $doc->createElement('d:lockscope');
+ $lockScope2 = $doc->createElement('d:lockscope');
+ $lockType1 = $doc->createElement('d:locktype');
+ $lockType2 = $doc->createElement('d:locktype');
+
+ $lockEntry1->appendChild($lockScope1);
+ $lockEntry1->appendChild($lockType1);
+ $lockEntry2->appendChild($lockScope2);
+ $lockEntry2->appendChild($lockType2);
+
+ $lockScope1->appendChild($doc->createElement('d:exclusive'));
+ $lockScope2->appendChild($doc->createElement('d:shared'));
+
+ $lockType1->appendChild($doc->createElement('d:write'));
+ $lockType2->appendChild($doc->createElement('d:write'));
+
+ //$frag->appendXML('<d:lockentry><d:lockscope><d:exclusive /></d:lockscope><d:locktype><d:write /></d:locktype></d:lockentry>');
+ //$frag->appendXML('<d:lockentry><d:lockscope><d:shared /></d:lockscope><d:locktype><d:write /></d:locktype></d:lockentry>');
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php
new file mode 100644
index 0000000..ecbde7d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * supported-report-set property.
+ *
+ * This property is defined in RFC3253, but since it's
+ * so common in other webdav-related specs, it is part of the core server.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedReportSet extends DAV\Property {
+
+ /**
+ * List of reports
+ *
+ * @var array
+ */
+ protected $reports = array();
+
+ /**
+ * Creates the property
+ *
+ * Any reports passed in the constructor
+ * should be valid report-types in clark-notation.
+ *
+ * Either a string or an array of strings must be passed.
+ *
+ * @param mixed $reports
+ */
+ public function __construct($reports = null) {
+
+ if (!is_null($reports))
+ $this->addReport($reports);
+
+ }
+
+ /**
+ * Adds a report to this property
+ *
+ * The report must be a string in clark-notation.
+ * Multiple reports can be specified as an array.
+ *
+ * @param mixed $report
+ * @return void
+ */
+ public function addReport($report) {
+
+ if (!is_array($report)) $report = array($report);
+
+ foreach($report as $r) {
+
+ if (!preg_match('/^{([^}]*)}(.*)$/',$r))
+ throw new DAV\Exception('Reportname must be in clark-notation');
+
+ $this->reports[] = $r;
+
+ }
+
+ }
+
+ /**
+ * Returns the list of supported reports
+ *
+ * @return array
+ */
+ public function getValue() {
+
+ return $this->reports;
+
+ }
+
+ /**
+ * Serializes the node
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $prop) {
+
+ foreach($this->reports as $reportName) {
+
+ $supportedReport = $prop->ownerDocument->createElement('d:supported-report');
+ $prop->appendChild($supportedReport);
+
+ $report = $prop->ownerDocument->createElement('d:report');
+ $supportedReport->appendChild($report);
+
+ preg_match('/^{([^}]*)}(.*)$/',$reportName,$matches);
+
+ list(, $namespace, $element) = $matches;
+
+ $prefix = isset($server->xmlNamespaces[$namespace])?$server->xmlNamespaces[$namespace]:null;
+
+ if ($prefix) {
+ $report->appendChild($prop->ownerDocument->createElement($prefix . ':' . $element));
+ } else {
+ $report->appendChild($prop->ownerDocument->createElementNS($namespace, 'x:' . $element));
+ }
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php
new file mode 100644
index 0000000..b76f94e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * PropertyInterface
+ *
+ * Implement this interface to create new complex properties
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface PropertyInterface {
+
+ public function serialize(Server $server, \DOMElement $prop);
+
+ static function unserialize(\DOMElement $prop);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php
new file mode 100644
index 0000000..eb33f29
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php
@@ -0,0 +1,2178 @@
+<?php
+
+namespace OldSabre\DAV;
+use OldSabre\HTTP;
+
+/**
+ * Main DAV server class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Server {
+
+ /**
+ * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
+ */
+ const DEPTH_INFINITY = -1;
+
+ /**
+ * Nodes that are files, should have this as the type property
+ */
+ const NODE_FILE = 1;
+
+ /**
+ * Nodes that are directories, should use this value as the type property
+ */
+ const NODE_DIRECTORY = 2;
+
+ /**
+ * XML namespace for all SabreDAV related elements
+ */
+ const NS_SABREDAV = 'http://sabredav.org/ns';
+
+ /**
+ * The tree object
+ *
+ * @var OldSabre\DAV\Tree
+ */
+ public $tree;
+
+ /**
+ * The base uri
+ *
+ * @var string
+ */
+ protected $baseUri = null;
+
+ /**
+ * httpResponse
+ *
+ * @var OldSabre\HTTP\Response
+ */
+ public $httpResponse;
+
+ /**
+ * httpRequest
+ *
+ * @var OldSabre\HTTP\Request
+ */
+ public $httpRequest;
+
+ /**
+ * The list of plugins
+ *
+ * @var array
+ */
+ protected $plugins = array();
+
+ /**
+ * This array contains a list of callbacks we should call when certain events are triggered
+ *
+ * @var array
+ */
+ protected $eventSubscriptions = array();
+
+ /**
+ * This is a default list of namespaces.
+ *
+ * If you are defining your own custom namespace, add it here to reduce
+ * bandwidth and improve legibility of xml bodies.
+ *
+ * @var array
+ */
+ public $xmlNamespaces = array(
+ 'DAV:' => 'd',
+ 'http://sabredav.org/ns' => 's',
+ );
+
+ /**
+ * The propertymap can be used to map properties from
+ * requests to property classes.
+ *
+ * @var array
+ */
+ public $propertyMap = array(
+ '{DAV:}resourcetype' => 'OldSabre\\DAV\\Property\\ResourceType',
+ );
+
+ public $protectedProperties = array(
+ // RFC4918
+ '{DAV:}getcontentlength',
+ '{DAV:}getetag',
+ '{DAV:}getlastmodified',
+ '{DAV:}lockdiscovery',
+ '{DAV:}supportedlock',
+
+ // RFC4331
+ '{DAV:}quota-available-bytes',
+ '{DAV:}quota-used-bytes',
+
+ // RFC3744
+ '{DAV:}supported-privilege-set',
+ '{DAV:}current-user-privilege-set',
+ '{DAV:}acl',
+ '{DAV:}acl-restrictions',
+ '{DAV:}inherited-acl-set',
+
+ );
+
+ /**
+ * This is a flag that allow or not showing file, line and code
+ * of the exception in the returned XML
+ *
+ * @var bool
+ */
+ public $debugExceptions = false;
+
+ /**
+ * This property allows you to automatically add the 'resourcetype' value
+ * based on a node's classname or interface.
+ *
+ * The preset ensures that {DAV:}collection is automaticlly added for nodes
+ * implementing OldSabre\DAV\ICollection.
+ *
+ * @var array
+ */
+ public $resourceTypeMapping = array(
+ 'OldSabre\\DAV\\ICollection' => '{DAV:}collection',
+ );
+
+ /**
+ * If this setting is turned off, SabreDAV's version number will be hidden
+ * from various places.
+ *
+ * Some people feel this is a good security measure.
+ *
+ * @var bool
+ */
+ static public $exposeVersion = true;
+
+ /**
+ * Sets up the server
+ *
+ * If a OldSabre\DAV\Tree object is passed as an argument, it will
+ * use it as the directory tree. If a OldSabre\DAV\INode is passed, it
+ * will create a OldSabre\DAV\ObjectTree and use the node as the root.
+ *
+ * If nothing is passed, a OldSabre\DAV\SimpleCollection is created in
+ * a OldSabre\DAV\ObjectTree.
+ *
+ * If an array is passed, we automatically create a root node, and use
+ * the nodes in the array as top-level children.
+ *
+ * @param Tree|INode|array|null $treeOrNode The tree object
+ */
+ public function __construct($treeOrNode = null) {
+
+ if ($treeOrNode instanceof Tree) {
+ $this->tree = $treeOrNode;
+ } elseif ($treeOrNode instanceof INode) {
+ $this->tree = new ObjectTree($treeOrNode);
+ } elseif (is_array($treeOrNode)) {
+
+ // If it's an array, a list of nodes was passed, and we need to
+ // create the root node.
+ foreach($treeOrNode as $node) {
+ if (!($node instanceof INode)) {
+ throw new Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement OldSabre\\DAV\\INode');
+ }
+ }
+
+ $root = new SimpleCollection('root', $treeOrNode);
+ $this->tree = new ObjectTree($root);
+
+ } elseif (is_null($treeOrNode)) {
+ $root = new SimpleCollection('root');
+ $this->tree = new ObjectTree($root);
+ } else {
+ throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of OldSabre\\DAV\\Tree, OldSabre\\DAV\\INode, an array or null');
+ }
+ $this->httpResponse = new HTTP\Response();
+ $this->httpRequest = new HTTP\Request();
+
+ }
+
+ /**
+ * Starts the DAV Server
+ *
+ * @return void
+ */
+ public function exec() {
+
+ try {
+
+ // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
+ // origin, we must make sure we send back HTTP/1.0 if this was
+ // requested.
+ // This is mainly because nginx doesn't support Chunked Transfer
+ // Encoding, and this forces the webserver SabreDAV is running on,
+ // to buffer entire responses to calculate Content-Length.
+ $this->httpResponse->defaultHttpVersion = $this->httpRequest->getHTTPVersion();
+
+ $this->invokeMethod($this->httpRequest->getMethod(), $this->getRequestUri());
+
+ } catch (Exception $e) {
+
+ try {
+ $this->broadcastEvent('exception', array($e));
+ } catch (Exception $ignore) {
+ }
+ $DOM = new \DOMDocument('1.0','utf-8');
+ $DOM->formatOutput = true;
+
+ $error = $DOM->createElementNS('DAV:','d:error');
+ $error->setAttribute('xmlns:s',self::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ $h = function($v) {
+
+ return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8');
+
+ };
+
+ $error->appendChild($DOM->createElement('s:exception',$h(get_class($e))));
+ $error->appendChild($DOM->createElement('s:message',$h($e->getMessage())));
+ if ($this->debugExceptions) {
+ $error->appendChild($DOM->createElement('s:file',$h($e->getFile())));
+ $error->appendChild($DOM->createElement('s:line',$h($e->getLine())));
+ $error->appendChild($DOM->createElement('s:code',$h($e->getCode())));
+ $error->appendChild($DOM->createElement('s:stacktrace',$h($e->getTraceAsString())));
+
+ }
+ if (self::$exposeVersion) {
+ $error->appendChild($DOM->createElement('s:sabredav-version',$h(Version::VERSION)));
+ }
+
+ if($e instanceof Exception) {
+
+ $httpCode = $e->getHTTPCode();
+ $e->serialize($this,$error);
+ $headers = $e->getHTTPHeaders($this);
+
+ } else {
+
+ $httpCode = 500;
+ $headers = array();
+
+ }
+ $headers['Content-Type'] = 'application/xml; charset=utf-8';
+
+ $this->httpResponse->sendStatus($httpCode);
+ $this->httpResponse->setHeaders($headers);
+ $this->httpResponse->sendBody($DOM->saveXML());
+
+ }
+
+ }
+
+ /**
+ * Sets the base server uri
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function setBaseUri($uri) {
+
+ // If the baseUri does not end with a slash, we must add it
+ if ($uri[strlen($uri)-1]!=='/')
+ $uri.='/';
+
+ $this->baseUri = $uri;
+
+ }
+
+ /**
+ * Returns the base responding uri
+ *
+ * @return string
+ */
+ public function getBaseUri() {
+
+ if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
+ return $this->baseUri;
+
+ }
+
+ /**
+ * This method attempts to detect the base uri.
+ * Only the PATH_INFO variable is considered.
+ *
+ * If this variable is not set, the root (/) is assumed.
+ *
+ * @return string
+ */
+ public function guessBaseUri() {
+
+ $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
+ $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
+
+ // If PATH_INFO is found, we can assume it's accurate.
+ if (!empty($pathInfo)) {
+
+ // We need to make sure we ignore the QUERY_STRING part
+ if ($pos = strpos($uri,'?'))
+ $uri = substr($uri,0,$pos);
+
+ // PATH_INFO is only set for urls, such as: /example.php/path
+ // in that case PATH_INFO contains '/path'.
+ // Note that REQUEST_URI is percent encoded, while PATH_INFO is
+ // not, Therefore they are only comparable if we first decode
+ // REQUEST_INFO as well.
+ $decodedUri = URLUtil::decodePath($uri);
+
+ // A simple sanity check:
+ if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) {
+ $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo));
+ return rtrim($baseUri,'/') . '/';
+ }
+
+ throw new Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
+
+ }
+
+ // The last fallback is that we're just going to assume the server root.
+ return '/';
+
+ }
+
+ /**
+ * Adds a plugin to the server
+ *
+ * For more information, console the documentation of OldSabre\DAV\ServerPlugin
+ *
+ * @param ServerPlugin $plugin
+ * @return void
+ */
+ public function addPlugin(ServerPlugin $plugin) {
+
+ $this->plugins[$plugin->getPluginName()] = $plugin;
+ $plugin->initialize($this);
+
+ }
+
+ /**
+ * Returns an initialized plugin by it's name.
+ *
+ * This function returns null if the plugin was not found.
+ *
+ * @param string $name
+ * @return ServerPlugin
+ */
+ public function getPlugin($name) {
+
+ if (isset($this->plugins[$name]))
+ return $this->plugins[$name];
+
+ // This is a fallback and deprecated.
+ foreach($this->plugins as $plugin) {
+ if (get_class($plugin)===$name) return $plugin;
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Returns all plugins
+ *
+ * @return array
+ */
+ public function getPlugins() {
+
+ return $this->plugins;
+
+ }
+
+
+ /**
+ * Subscribe to an event.
+ *
+ * When the event is triggered, we'll call all the specified callbacks.
+ * It is possible to control the order of the callbacks through the
+ * priority argument.
+ *
+ * This is for example used to make sure that the authentication plugin
+ * is triggered before anything else. If it's not needed to change this
+ * number, it is recommended to ommit.
+ *
+ * @param string $event
+ * @param callback $callback
+ * @param int $priority
+ * @return void
+ */
+ public function subscribeEvent($event, $callback, $priority = 100) {
+
+ if (!isset($this->eventSubscriptions[$event])) {
+ $this->eventSubscriptions[$event] = array();
+ }
+ while(isset($this->eventSubscriptions[$event][$priority])) $priority++;
+ $this->eventSubscriptions[$event][$priority] = $callback;
+ ksort($this->eventSubscriptions[$event]);
+
+ }
+
+ /**
+ * Broadcasts an event
+ *
+ * This method will call all subscribers. If one of the subscribers returns false, the process stops.
+ *
+ * The arguments parameter will be sent to all subscribers
+ *
+ * @param string $eventName
+ * @param array $arguments
+ * @return bool
+ */
+ public function broadcastEvent($eventName,$arguments = array()) {
+
+ if (isset($this->eventSubscriptions[$eventName])) {
+
+ foreach($this->eventSubscriptions[$eventName] as $subscriber) {
+
+ $result = call_user_func_array($subscriber,$arguments);
+ if ($result===false) return false;
+
+ }
+
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Handles a http request, and execute a method based on its name
+ *
+ * @param string $method
+ * @param string $uri
+ * @return void
+ */
+ public function invokeMethod($method, $uri) {
+
+ $method = strtoupper($method);
+
+ if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return;
+
+ // Make sure this is a HTTP method we support
+ $internalMethods = array(
+ 'OPTIONS',
+ 'GET',
+ 'HEAD',
+ 'DELETE',
+ 'PROPFIND',
+ 'MKCOL',
+ 'PUT',
+ 'PROPPATCH',
+ 'COPY',
+ 'MOVE',
+ 'REPORT'
+ );
+
+ if (in_array($method,$internalMethods)) {
+
+ call_user_func(array($this,'http' . $method), $uri);
+
+ } else {
+
+ if ($this->broadcastEvent('unknownMethod',array($method, $uri))) {
+ // Unsupported method
+ throw new Exception\NotImplemented('There was no handler found for this "' . $method . '" method');
+ }
+
+ }
+
+ }
+
+ // {{{ HTTP Method implementations
+
+ /**
+ * HTTP OPTIONS
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpOptions($uri) {
+
+ $methods = $this->getAllowedMethods($uri);
+
+ $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
+ $features = array('1','3', 'extended-mkcol');
+
+ foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
+
+ $this->httpResponse->setHeader('DAV',implode(', ',$features));
+ $this->httpResponse->setHeader('MS-Author-Via','DAV');
+ $this->httpResponse->setHeader('Accept-Ranges','bytes');
+ if (self::$exposeVersion) {
+ $this->httpResponse->setHeader('X-Sabre-Version',Version::VERSION);
+ }
+ $this->httpResponse->setHeader('Content-Length',0);
+ $this->httpResponse->sendStatus(200);
+
+ }
+
+ /**
+ * HTTP GET
+ *
+ * This method simply fetches the contents of a uri, like normal
+ *
+ * @param string $uri
+ * @return bool
+ */
+ protected function httpGet($uri) {
+
+ $node = $this->tree->getNodeForPath($uri,0);
+
+ if (!$this->checkPreconditions(true)) return false;
+ if (!$node instanceof IFile) throw new Exception\NotImplemented('GET is only implemented on File objects');
+
+ $body = $node->get();
+
+ // Converting string into stream, if needed.
+ if (is_string($body)) {
+ $stream = fopen('php://temp','r+');
+ fwrite($stream,$body);
+ rewind($stream);
+ $body = $stream;
+ }
+
+ /*
+ * TODO: getetag, getlastmodified, getsize should also be used using
+ * this method
+ */
+ $httpHeaders = $this->getHTTPHeaders($uri);
+
+ /* ContentType needs to get a default, because many webservers will otherwise
+ * default to text/html, and we don't want this for security reasons.
+ */
+ if (!isset($httpHeaders['Content-Type'])) {
+ $httpHeaders['Content-Type'] = 'application/octet-stream';
+ }
+
+
+ if (isset($httpHeaders['Content-Length'])) {
+
+ $nodeSize = $httpHeaders['Content-Length'];
+
+ // Need to unset Content-Length, because we'll handle that during figuring out the range
+ unset($httpHeaders['Content-Length']);
+
+ } else {
+ $nodeSize = null;
+ }
+
+ $this->httpResponse->setHeaders($httpHeaders);
+
+ $range = $this->getHTTPRange();
+ $ifRange = $this->httpRequest->getHeader('If-Range');
+ $ignoreRangeHeader = false;
+
+ // If ifRange is set, and range is specified, we first need to check
+ // the precondition.
+ if ($nodeSize && $range && $ifRange) {
+
+ // if IfRange is parsable as a date we'll treat it as a DateTime
+ // otherwise, we must treat it as an etag.
+ try {
+ $ifRangeDate = new \DateTime($ifRange);
+
+ // It's a date. We must check if the entity is modified since
+ // the specified date.
+ if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
+ else {
+ $modified = new \DateTime($httpHeaders['Last-Modified']);
+ if($modified > $ifRangeDate) $ignoreRangeHeader = true;
+ }
+
+ } catch (\Exception $e) {
+
+ // It's an entity. We can do a simple comparison.
+ if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
+ elseif ($httpHeaders['ETag']!==$ifRange) $ignoreRangeHeader = true;
+ }
+ }
+
+ // We're only going to support HTTP ranges if the backend provided a filesize
+ if (!$ignoreRangeHeader && $nodeSize && $range) {
+
+ // Determining the exact byte offsets
+ if (!is_null($range[0])) {
+
+ $start = $range[0];
+ $end = $range[1]?$range[1]:$nodeSize-1;
+ if($start >= $nodeSize)
+ throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
+
+ if($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
+ if($end >= $nodeSize) $end = $nodeSize-1;
+
+ } else {
+
+ $start = $nodeSize-$range[1];
+ $end = $nodeSize-1;
+
+ if ($start<0) $start = 0;
+
+ }
+
+ // New read/write stream
+ $newStream = fopen('php://temp','r+');
+
+ // stream_copy_to_stream() has a bug/feature: the `whence` argument
+ // is interpreted as SEEK_SET (count from absolute offset 0), while
+ // for a stream it should be SEEK_CUR (count from current offset).
+ // If a stream is nonseekable, the function fails. So we *emulate*
+ // the correct behaviour with fseek():
+ if ($start > 0) {
+ if (($curOffs = ftell($body)) === false) $curOffs = 0;
+ fseek($body, $start - $curOffs, SEEK_CUR);
+ }
+ stream_copy_to_stream($body, $newStream, $end-$start+1);
+ rewind($newStream);
+
+ $this->httpResponse->setHeader('Content-Length', $end-$start+1);
+ $this->httpResponse->setHeader('Content-Range','bytes ' . $start . '-' . $end . '/' . $nodeSize);
+ $this->httpResponse->sendStatus(206);
+ $this->httpResponse->sendBody($newStream);
+
+
+ } else {
+
+ if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize);
+ $this->httpResponse->sendStatus(200);
+ $this->httpResponse->sendBody($body);
+
+ }
+
+ }
+
+ /**
+ * HTTP HEAD
+ *
+ * This method is normally used to take a peak at a url, and only get the HTTP response headers, without the body
+ * This is used by clients to determine if a remote file was changed, so they can use a local cached version, instead of downloading it again
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpHead($uri) {
+
+ $node = $this->tree->getNodeForPath($uri);
+ /* This information is only collection for File objects.
+ * Ideally we want to throw 405 Method Not Allowed for every
+ * non-file, but MS Office does not like this
+ */
+ if ($node instanceof IFile) {
+ $headers = $this->getHTTPHeaders($this->getRequestUri());
+ if (!isset($headers['Content-Type'])) {
+ $headers['Content-Type'] = 'application/octet-stream';
+ }
+ $this->httpResponse->setHeaders($headers);
+ }
+ $this->httpResponse->sendStatus(200);
+
+ }
+
+ /**
+ * HTTP Delete
+ *
+ * The HTTP delete method, deletes a given uri
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpDelete($uri) {
+
+ // Checking If-None-Match and related headers.
+ if (!$this->checkPreconditions()) return;
+
+ if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
+ $this->tree->delete($uri);
+ $this->broadcastEvent('afterUnbind',array($uri));
+
+ $this->httpResponse->sendStatus(204);
+ $this->httpResponse->setHeader('Content-Length','0');
+
+ }
+
+
+ /**
+ * WebDAV PROPFIND
+ *
+ * This WebDAV method requests information about an uri resource, or a list of resources
+ * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
+ * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
+ *
+ * The request body contains an XML data structure that has a list of properties the client understands
+ * The response body is also an xml document, containing information about every uri resource and the requested properties
+ *
+ * It has to return a HTTP 207 Multi-status status code
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpPropfind($uri) {
+
+ $requestedProperties = $this->parsePropFindRequest($this->httpRequest->getBody(true));
+
+ $depth = $this->getHTTPDepth(1);
+ // The only two options for the depth of a propfind is 0 or 1
+ if ($depth!=0) $depth = 1;
+
+ $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth);
+
+ // 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
+ // this is not harmful, we'll add it.
+ $features = array('1','3', 'extended-mkcol');
+ foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
+ $this->httpResponse->setHeader('DAV',implode(', ',$features));
+
+ $prefer = $this->getHTTPPrefer();
+ $minimal = $prefer['return-minimal'];
+
+ $data = $this->generateMultiStatus($newProperties, $minimal);
+ $this->httpResponse->sendBody($data);
+
+ }
+
+ /**
+ * WebDAV PROPPATCH
+ *
+ * This method is called to update properties on a Node. The request is an XML body with all the mutations.
+ * In this XML body it is specified which properties should be set/updated and/or deleted
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpPropPatch($uri) {
+
+ $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
+
+ $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');
+
+ $this->httpResponse->sendBody(
+ $this->generateMultiStatus(array($result))
+ );
+
+ }
+
+ /**
+ * HTTP PUT method
+ *
+ * This HTTP method updates a file, or creates a new one.
+ *
+ * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content
+ *
+ * @param string $uri
+ * @return bool
+ */
+ protected function httpPut($uri) {
+
+ $body = $this->httpRequest->getBody();
+
+ // Intercepting Content-Range
+ if ($this->httpRequest->getHeader('Content-Range')) {
+ /**
+ Content-Range is dangerous for PUT requests: PUT per definition
+ stores a full resource. draft-ietf-httpbis-p2-semantics-15 says
+ in section 7.6:
+ An origin server SHOULD reject any PUT request that contains a
+ Content-Range header field, since it might be misinterpreted as
+ partial content (or might be partial content that is being mistakenly
+ PUT as a full representation). Partial content updates are possible
+ by targeting a separately identified resource with state that
+ overlaps a portion of the larger resource, or by using a different
+ method that has been specifically defined for partial updates (for
+ example, the PATCH method defined in [RFC5789]).
+ This clarifies RFC2616 section 9.6:
+ The recipient of the entity MUST NOT ignore any Content-*
+ (e.g. Content-Range) headers that it does not understand or implement
+ and MUST return a 501 (Not Implemented) response in such cases.
+ OTOH is a PUT request with a Content-Range currently the only way to
+ continue an aborted upload request and is supported by curl, mod_dav,
+ Tomcat and others. Since some clients do use this feature which results
+ in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
+ all PUT requests with a Content-Range for now.
+ */
+
+ throw new Exception\NotImplemented('PUT with Content-Range is not allowed.');
+ }
+
+ // Intercepting the Finder problem
+ if (($expected = $this->httpRequest->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
+
+ /**
+ Many webservers will not cooperate well with Finder PUT requests,
+ because it uses 'Chunked' transfer encoding for the request body.
+
+ The symptom of this problem is that Finder sends files to the
+ server, but they arrive as 0-length files in PHP.
+
+ If we don't do anything, the user might think they are uploading
+ files successfully, but they end up empty on the server. Instead,
+ we throw back an error if we detect this.
+
+ The reason Finder uses Chunked, is because it thinks the files
+ might change as it's being uploaded, and therefore the
+ Content-Length can vary.
+
+ Instead it sends the X-Expected-Entity-Length header with the size
+ of the file at the very start of the request. If this header is set,
+ but we don't get a request body we will fail the request to
+ protect the end-user.
+ */
+
+ // Only reading first byte
+ $firstByte = fread($body,1);
+ if (strlen($firstByte)!==1) {
+ throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
+ }
+
+ // The body needs to stay intact, so we copy everything to a
+ // temporary stream.
+
+ $newBody = fopen('php://temp','r+');
+ fwrite($newBody,$firstByte);
+ stream_copy_to_stream($body, $newBody);
+ rewind($newBody);
+
+ $body = $newBody;
+
+ }
+
+ // Checking If-None-Match and related headers.
+ if (!$this->checkPreconditions()) return;
+
+ if ($this->tree->nodeExists($uri)) {
+
+ $node = $this->tree->getNodeForPath($uri);
+
+ // If the node is a collection, we'll deny it
+ if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.');
+ if (!$this->broadcastEvent('beforeWriteContent',array($uri, $node, &$body))) return false;
+
+ $etag = $node->put($body);
+
+ $this->broadcastEvent('afterWriteContent',array($uri, $node));
+
+ $this->httpResponse->setHeader('Content-Length','0');
+ if ($etag) $this->httpResponse->setHeader('ETag',$etag);
+ $this->httpResponse->sendStatus(204);
+
+ } else {
+
+ $etag = null;
+ // If we got here, the resource didn't exist yet.
+ if (!$this->createFile($this->getRequestUri(),$body,$etag)) {
+ // For one reason or another the file was not created.
+ return;
+ }
+
+ $this->httpResponse->setHeader('Content-Length','0');
+ if ($etag) $this->httpResponse->setHeader('ETag', $etag);
+ $this->httpResponse->sendStatus(201);
+
+ }
+
+ }
+
+
+ /**
+ * WebDAV MKCOL
+ *
+ * The MKCOL method is used to create a new collection (directory) on the server
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpMkcol($uri) {
+
+ $requestBody = $this->httpRequest->getBody(true);
+
+ if ($requestBody) {
+
+ $contentType = $this->httpRequest->getHeader('Content-Type');
+ if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) {
+
+ // We must throw 415 for unsupported mkcol bodies
+ throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
+
+ }
+
+ $dom = XMLUtil::loadDOMDocument($requestBody);
+ if (XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') {
+
+ // We must throw 415 for unsupported mkcol bodies
+ throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.');
+
+ }
+
+ $properties = array();
+ foreach($dom->firstChild->childNodes as $childNode) {
+
+ if (XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue;
+ $properties = array_merge($properties, XMLUtil::parseProperties($childNode, $this->propertyMap));
+
+ }
+ if (!isset($properties['{DAV:}resourcetype']))
+ throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property');
+
+ $resourceType = $properties['{DAV:}resourcetype']->getValue();
+ unset($properties['{DAV:}resourcetype']);
+
+ } else {
+
+ $properties = array();
+ $resourceType = array('{DAV:}collection');
+
+ }
+
+ $result = $this->createCollection($uri, $resourceType, $properties);
+
+ if (is_array($result)) {
+ $this->httpResponse->sendStatus(207);
+ $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+
+ $this->httpResponse->sendBody(
+ $this->generateMultiStatus(array($result))
+ );
+
+ } else {
+ $this->httpResponse->setHeader('Content-Length','0');
+ $this->httpResponse->sendStatus(201);
+ }
+
+ }
+
+ /**
+ * WebDAV HTTP MOVE method
+ *
+ * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
+ *
+ * @param string $uri
+ * @return bool
+ */
+ protected function httpMove($uri) {
+
+ $moveInfo = $this->getCopyAndMoveInfo();
+
+ // If the destination is part of the source tree, we must fail
+ if ($moveInfo['destination']==$uri)
+ throw new Exception\Forbidden('Source and destination uri are identical.');
+
+ if ($moveInfo['destinationExists']) {
+
+ if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
+ $this->tree->delete($moveInfo['destination']);
+ $this->broadcastEvent('afterUnbind',array($moveInfo['destination']));
+
+ }
+
+ if (!$this->broadcastEvent('beforeUnbind',array($uri))) return false;
+ if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false;
+ $this->tree->move($uri,$moveInfo['destination']);
+ $this->broadcastEvent('afterUnbind',array($uri));
+ $this->broadcastEvent('afterBind',array($moveInfo['destination']));
+
+ // If a resource was overwritten we should send a 204, otherwise a 201
+ $this->httpResponse->setHeader('Content-Length','0');
+ $this->httpResponse->sendStatus($moveInfo['destinationExists']?204:201);
+
+ }
+
+ /**
+ * WebDAV HTTP COPY method
+ *
+ * This method copies one uri to a different uri, and works much like the MOVE request
+ * A lot of the actual request processing is done in getCopyMoveInfo
+ *
+ * @param string $uri
+ * @return bool
+ */
+ protected function httpCopy($uri) {
+
+ $copyInfo = $this->getCopyAndMoveInfo();
+ // If the destination is part of the source tree, we must fail
+ if ($copyInfo['destination']==$uri)
+ throw new Exception\Forbidden('Source and destination uri are identical.');
+
+ if ($copyInfo['destinationExists']) {
+ if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
+ $this->tree->delete($copyInfo['destination']);
+
+ }
+ if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
+ $this->tree->copy($uri,$copyInfo['destination']);
+ $this->broadcastEvent('afterBind',array($copyInfo['destination']));
+
+ // If a resource was overwritten we should send a 204, otherwise a 201
+ $this->httpResponse->setHeader('Content-Length','0');
+ $this->httpResponse->sendStatus($copyInfo['destinationExists']?204:201);
+
+ }
+
+
+
+ /**
+ * HTTP REPORT method implementation
+ *
+ * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
+ * It's used in a lot of extensions, so it made sense to implement it into the core.
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpReport($uri) {
+
+ $body = $this->httpRequest->getBody(true);
+ $dom = XMLUtil::loadDOMDocument($body);
+
+ $reportName = XMLUtil::toClarkNotation($dom->firstChild);
+
+ if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) {
+
+ // If broadcastEvent returned true, it means the report was not supported
+ throw new Exception\ReportNotSupported();
+
+ }
+
+ }
+
+ // }}}
+ // {{{ HTTP/WebDAV protocol helpers
+
+ /**
+ * Returns an array with all the supported HTTP methods for a specific uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getAllowedMethods($uri) {
+
+ $methods = array(
+ 'OPTIONS',
+ 'GET',
+ 'HEAD',
+ 'DELETE',
+ 'PROPFIND',
+ 'PUT',
+ 'PROPPATCH',
+ 'COPY',
+ 'MOVE',
+ 'REPORT'
+ );
+
+ // The MKCOL is only allowed on an unmapped uri
+ try {
+ $this->tree->getNodeForPath($uri);
+ } catch (Exception\NotFound $e) {
+ $methods[] = 'MKCOL';
+ }
+
+ // We're also checking if any of the plugins register any new methods
+ foreach($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($uri));
+ array_unique($methods);
+
+ return $methods;
+
+ }
+
+ /**
+ * Gets the uri for the request, keeping the base uri into consideration
+ *
+ * @return string
+ */
+ public function getRequestUri() {
+
+ return $this->calculateUri($this->httpRequest->getUri());
+
+ }
+
+ /**
+ * Calculates the uri for a request, making sure that the base uri is stripped out
+ *
+ * @param string $uri
+ * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
+ * @return string
+ */
+ public function calculateUri($uri) {
+
+ if ($uri[0]!='/' && strpos($uri,'://')) {
+
+ $uri = parse_url($uri,PHP_URL_PATH);
+
+ }
+
+ $uri = str_replace('//','/',$uri);
+
+ if (strpos($uri,$this->getBaseUri())===0) {
+
+ return trim(URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
+
+ // A special case, if the baseUri was accessed without a trailing
+ // slash, we'll accept it as well.
+ } elseif ($uri.'/' === $this->getBaseUri()) {
+
+ return '';
+
+ } else {
+
+ throw new Exception\Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
+
+ }
+
+ }
+
+ /**
+ * Returns the HTTP depth header
+ *
+ * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the OldSabre\DAV\Server::DEPTH_INFINITY object
+ * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
+ *
+ * @param mixed $default
+ * @return int
+ */
+ public function getHTTPDepth($default = self::DEPTH_INFINITY) {
+
+ // If its not set, we'll grab the default
+ $depth = $this->httpRequest->getHeader('Depth');
+
+ if (is_null($depth)) return $default;
+
+ if ($depth == 'infinity') return self::DEPTH_INFINITY;
+
+
+ // If its an unknown value. we'll grab the default
+ if (!ctype_digit($depth)) return $default;
+
+ return (int)$depth;
+
+ }
+
+ /**
+ * Returns the HTTP range header
+ *
+ * This method returns null if there is no well-formed HTTP range request
+ * header or array($start, $end).
+ *
+ * The first number is the offset of the first byte in the range.
+ * The second number is the offset of the last byte in the range.
+ *
+ * If the second offset is null, it should be treated as the offset of the last byte of the entity
+ * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
+ *
+ * @return array|null
+ */
+ public function getHTTPRange() {
+
+ $range = $this->httpRequest->getHeader('range');
+ if (is_null($range)) return null;
+
+ // Matching "Range: bytes=1234-5678: both numbers are optional
+
+ if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
+
+ if ($matches[1]==='' && $matches[2]==='') return null;
+
+ return array(
+ $matches[1]!==''?$matches[1]:null,
+ $matches[2]!==''?$matches[2]:null,
+ );
+
+ }
+
+ /**
+ * Returns the HTTP Prefer header information.
+ *
+ * The prefer header is defined in:
+ * http://tools.ietf.org/html/draft-snell-http-prefer-14
+ *
+ * This method will return an array with options.
+ *
+ * Currently, the following options may be returned:
+ * 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<token>[a-z0-9-]+)(?:=(?P<value>[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
+ *
+ * This function is created to help getting information about the source and the destination for the
+ * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
+ *
+ * The returned value is an array with the following keys:
+ * * destination - Destination path
+ * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
+ *
+ * @return array
+ */
+ public function getCopyAndMoveInfo() {
+
+ // Collecting the relevant HTTP headers
+ if (!$this->httpRequest->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied');
+ $destination = $this->calculateUri($this->httpRequest->getHeader('Destination'));
+ $overwrite = $this->httpRequest->getHeader('Overwrite');
+ if (!$overwrite) $overwrite = 'T';
+ if (strtoupper($overwrite)=='T') $overwrite = true;
+ elseif (strtoupper($overwrite)=='F') $overwrite = false;
+ // We need to throw a bad request exception, if the header was invalid
+ else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F');
+
+ list($destinationDir) = URLUtil::splitPath($destination);
+
+ try {
+ $destinationParent = $this->tree->getNodeForPath($destinationDir);
+ if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection');
+ } catch (Exception\NotFound $e) {
+
+ // If the destination parent node is not found, we throw a 409
+ throw new Exception\Conflict('The destination node is not found');
+ }
+
+ try {
+
+ $destinationNode = $this->tree->getNodeForPath($destination);
+
+ // If this succeeded, it means the destination already exists
+ // we'll need to throw precondition failed in case overwrite is false
+ if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite');
+
+ } catch (Exception\NotFound $e) {
+
+ // Destination didn't exist, we're all good
+ $destinationNode = false;
+
+
+
+ }
+
+ // These are the three relevant properties we need to return
+ return array(
+ 'destination' => $destination,
+ 'destinationExists' => $destinationNode==true,
+ 'destinationNode' => $destinationNode,
+ );
+
+ }
+
+ /**
+ * Returns a list of properties for a path
+ *
+ * This is a simplified version getPropertiesForPath.
+ * if you aren't interested in status codes, but you just
+ * want to have a flat list of properties. Use this method.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ */
+ public function getProperties($path, $propertyNames) {
+
+ $result = $this->getPropertiesForPath($path,$propertyNames,0);
+ return $result[0][200];
+
+ }
+
+ /**
+ * A kid-friendly way to fetch properties for a node's children.
+ *
+ * The returned array will be indexed by the path of the of child node.
+ * Only properties that are actually found will be returned.
+ *
+ * The parent node will not be returned.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ * @return array
+ */
+ public function getPropertiesForChildren($path, $propertyNames) {
+
+ $result = array();
+ foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
+
+ // Skipping the parent path
+ if ($k === 0) continue;
+
+ $result[$row['href']] = $row[200];
+
+ }
+ return $result;
+
+ }
+
+ /**
+ * Returns a list of HTTP headers for a particular resource
+ *
+ * The generated http headers are based on properties provided by the
+ * resource. The method basically provides a simple mapping between
+ * DAV property and HTTP header.
+ *
+ * The headers are intended to be used for HEAD and GET requests.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getHTTPHeaders($path) {
+
+ $propertyMap = array(
+ '{DAV:}getcontenttype' => 'Content-Type',
+ '{DAV:}getcontentlength' => 'Content-Length',
+ '{DAV:}getlastmodified' => 'Last-Modified',
+ '{DAV:}getetag' => 'ETag',
+ );
+
+ $properties = $this->getProperties($path,array_keys($propertyMap));
+
+ $headers = array();
+ foreach($propertyMap as $property=>$header) {
+ if (!isset($properties[$property])) continue;
+
+ if (is_scalar($properties[$property])) {
+ $headers[$header] = $properties[$property];
+
+ // GetLastModified gets special cased
+ } elseif ($properties[$property] instanceof Property\GetLastModified) {
+ $headers[$header] = HTTP\Util::toHTTPDate($properties[$property]->getTime());
+ }
+
+ }
+
+ return $headers;
+
+ }
+
+ /**
+ * Returns a list of properties for a given path
+ *
+ * The path that should be supplied should have the baseUrl stripped out
+ * The list of properties should be supplied in Clark notation. If the list is empty
+ * 'allprops' is assumed.
+ *
+ * If a depth of 1 is requested child elements will also be returned.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ * @param int $depth
+ * @return array
+ */
+ public function getPropertiesForPath($path, $propertyNames = array(), $depth = 0) {
+
+ if ($depth!=0) $depth = 1;
+
+ $path = rtrim($path,'/');
+
+ // This event allows people to intercept these requests early on in the
+ // process.
+ //
+ // We're not doing anything with the result, but this can be helpful to
+ // pre-fetch certain expensive live properties.
+ $this->broadCastEvent('beforeGetPropertiesForPath', array($path, $propertyNames, $depth));
+
+ $returnPropertyList = array();
+
+ $parentNode = $this->tree->getNodeForPath($path);
+ $nodes = array(
+ $path => $parentNode
+ );
+ if ($depth==1 && $parentNode instanceof ICollection) {
+ foreach($this->tree->getChildren($path) as $childNode)
+ $nodes[$path . '/' . $childNode->getName()] = $childNode;
+ }
+
+ // If the propertyNames array is empty, it means all properties are requested.
+ // We shouldn't actually return everything we know though, and only return a
+ // sensible list.
+ $allProperties = count($propertyNames)==0;
+
+ foreach($nodes as $myPath=>$node) {
+
+ $currentPropertyNames = $propertyNames;
+
+ $newProperties = array(
+ '200' => array(),
+ '404' => array(),
+ );
+
+ if ($allProperties) {
+ // Default list of propertyNames, when all properties were requested.
+ $currentPropertyNames = array(
+ '{DAV:}getlastmodified',
+ '{DAV:}getcontentlength',
+ '{DAV:}resourcetype',
+ '{DAV:}quota-used-bytes',
+ '{DAV:}quota-available-bytes',
+ '{DAV:}getetag',
+ '{DAV:}getcontenttype',
+ );
+ }
+
+ // If the resourceType was not part of the list, we manually add it
+ // and mark it for removal. We need to know the resourcetype in order
+ // to make certain decisions about the entry.
+ // WebDAV dictates we should add a / and the end of href's for collections
+ $removeRT = false;
+ if (!in_array('{DAV:}resourcetype',$currentPropertyNames)) {
+ $currentPropertyNames[] = '{DAV:}resourcetype';
+ $removeRT = true;
+ }
+
+ $result = $this->broadcastEvent('beforeGetProperties',array($myPath, $node, &$currentPropertyNames, &$newProperties));
+ // If this method explicitly returned false, we must ignore this
+ // node as it is inaccessible.
+ if ($result===false) continue;
+
+ if (count($currentPropertyNames) > 0) {
+
+ if ($node instanceof IProperties) {
+ $nodeProperties = $node->getProperties($currentPropertyNames);
+
+ // The getProperties method may give us too much,
+ // properties, in case the implementor was lazy.
+ //
+ // So as we loop through this list, we will only take the
+ // properties that were actually requested and discard the
+ // rest.
+ foreach($currentPropertyNames as $k=>$currentPropertyName) {
+ if (isset($nodeProperties[$currentPropertyName])) {
+ unset($currentPropertyNames[$k]);
+ $newProperties[200][$currentPropertyName] = $nodeProperties[$currentPropertyName];
+ }
+ }
+
+ }
+
+ }
+
+ foreach($currentPropertyNames as $prop) {
+
+ if (isset($newProperties[200][$prop])) continue;
+
+ switch($prop) {
+ case '{DAV:}getlastmodified' : if ($node->getLastModified()) $newProperties[200][$prop] = new Property\GetLastModified($node->getLastModified()); break;
+ case '{DAV:}getcontentlength' :
+ if ($node instanceof IFile) {
+ $size = $node->getSize();
+ if (!is_null($size)) {
+ $newProperties[200][$prop] = (int)$node->getSize();
+ }
+ }
+ break;
+ case '{DAV:}quota-used-bytes' :
+ if ($node instanceof IQuota) {
+ $quotaInfo = $node->getQuotaInfo();
+ $newProperties[200][$prop] = $quotaInfo[0];
+ }
+ break;
+ case '{DAV:}quota-available-bytes' :
+ if ($node instanceof IQuota) {
+ $quotaInfo = $node->getQuotaInfo();
+ $newProperties[200][$prop] = $quotaInfo[1];
+ }
+ break;
+ case '{DAV:}getetag' : if ($node instanceof IFile && $etag = $node->getETag()) $newProperties[200][$prop] = $etag; break;
+ case '{DAV:}getcontenttype' : if ($node instanceof IFile && $ct = $node->getContentType()) $newProperties[200][$prop] = $ct; break;
+ case '{DAV:}supported-report-set' :
+ $reports = array();
+ foreach($this->plugins as $plugin) {
+ $reports = array_merge($reports, $plugin->getSupportedReportSet($myPath));
+ }
+ $newProperties[200][$prop] = new Property\SupportedReportSet($reports);
+ break;
+ case '{DAV:}resourcetype' :
+ $newProperties[200]['{DAV:}resourcetype'] = new Property\ResourceType();
+ foreach($this->resourceTypeMapping as $className => $resourceType) {
+ if ($node instanceof $className) $newProperties[200]['{DAV:}resourcetype']->add($resourceType);
+ }
+ break;
+
+ }
+
+ // If we were unable to find the property, we will list it as 404.
+ if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null;
+
+ }
+
+ $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), 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.
+ if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']);
+
+ $returnPropertyList[] = $newProperties;
+
+ }
+
+ return $returnPropertyList;
+
+ }
+
+ /**
+ * This method is invoked by sub-systems creating a new file.
+ *
+ * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
+ * It was important to get this done through a centralized function,
+ * allowing plugins to intercept this using the beforeCreateFile event.
+ *
+ * This method will return true if the file was actually created
+ *
+ * @param string $uri
+ * @param resource $data
+ * @param string $etag
+ * @return bool
+ */
+ public function createFile($uri,$data, &$etag = null) {
+
+ list($dir,$name) = URLUtil::splitPath($uri);
+
+ if (!$this->broadcastEvent('beforeBind',array($uri))) return false;
+
+ $parent = $this->tree->getNodeForPath($dir);
+ if (!$parent instanceof ICollection) {
+ throw new Exception\Conflict('Files can only be created as children of collections');
+ }
+
+ if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false;
+
+ $etag = $parent->createFile($name,$data);
+ $this->tree->markDirty($dir . '/' . $name);
+
+ $this->broadcastEvent('afterBind',array($uri));
+ $this->broadcastEvent('afterCreateFile',array($uri, $parent));
+
+ return true;
+ }
+
+ /**
+ * This method is invoked by sub-systems creating a new directory.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function createDirectory($uri) {
+
+ $this->createCollection($uri,array('{DAV:}collection'),array());
+
+ }
+
+ /**
+ * Use this method to create a new collection
+ *
+ * The {DAV:}resourcetype is specified using the resourceType array.
+ * At the very least it must contain {DAV:}collection.
+ *
+ * The properties array can contain a list of additional properties.
+ *
+ * @param string $uri The new uri
+ * @param array $resourceType The resourceType(s)
+ * @param array $properties A list of properties
+ * @return array|null
+ */
+ public function createCollection($uri, array $resourceType, array $properties) {
+
+ list($parentUri,$newName) = URLUtil::splitPath($uri);
+
+ // Making sure {DAV:}collection was specified as resourceType
+ if (!in_array('{DAV:}collection', $resourceType)) {
+ throw new Exception\InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection');
+ }
+
+
+ // Making sure the parent exists
+ try {
+
+ $parent = $this->tree->getNodeForPath($parentUri);
+
+ } catch (Exception\NotFound $e) {
+
+ throw new Exception\Conflict('Parent node does not exist');
+
+ }
+
+ // Making sure the parent is a collection
+ if (!$parent instanceof ICollection) {
+ throw new Exception\Conflict('Parent node is not a collection');
+ }
+
+
+
+ // Making sure the child does not already exist
+ try {
+ $parent->getChild($newName);
+
+ // If we got here.. it means there's already a node on that url, and we need to throw a 405
+ throw new Exception\MethodNotAllowed('The resource you tried to create already exists');
+
+ } catch (Exception\NotFound $e) {
+ // This is correct
+ }
+
+
+ if (!$this->broadcastEvent('beforeBind',array($uri))) return;
+
+ // There are 2 modes of operation. The standard collection
+ // creates the directory, and then updates properties
+ // the extended collection can create it directly.
+ if ($parent instanceof IExtendedCollection) {
+
+ $parent->createExtendedCollection($newName, $resourceType, $properties);
+
+ } else {
+
+ // No special resourcetypes are supported
+ if (count($resourceType)>1) {
+ throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
+ }
+
+ $parent->createDirectory($newName);
+ $rollBack = false;
+ $exception = null;
+ $errorResult = null;
+
+ if (count($properties)>0) {
+
+ try {
+
+ $errorResult = $this->updateProperties($uri, $properties);
+ if (!isset($errorResult[200])) {
+ $rollBack = true;
+ }
+
+ } catch (Exception $e) {
+
+ $rollBack = true;
+ $exception = $e;
+
+ }
+
+ }
+
+ if ($rollBack) {
+ if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
+ $this->tree->delete($uri);
+
+ // Re-throwing exception
+ if ($exception) throw $exception;
+
+ return $errorResult;
+ }
+
+ }
+ $this->tree->markDirty($parentUri);
+ $this->broadcastEvent('afterBind',array($uri));
+
+ }
+
+ /**
+ * This method updates a resource's properties
+ *
+ * The properties array must be a list of properties. Array-keys are
+ * property names in clarknotation, array-values are it's values.
+ * If a property must be deleted, the value should be null.
+ *
+ * Note that this request should either completely succeed, or
+ * completely fail.
+ *
+ * The response is an array with statuscodes for keys, which in turn
+ * contain arrays with propertynames. This response can be used
+ * to generate a multistatus body.
+ *
+ * @param string $uri
+ * @param array $properties
+ * @return array
+ */
+ public function updateProperties($uri, array $properties) {
+
+ // we'll start by grabbing the node, this will throw the appropriate
+ // exceptions if it doesn't.
+ $node = $this->tree->getNodeForPath($uri);
+
+ $result = array(
+ 200 => array(),
+ 403 => array(),
+ 424 => array(),
+ );
+ $remainingProperties = $properties;
+ $hasError = false;
+
+ // Running through all properties to make sure none of them are protected
+ if (!$hasError) foreach($properties as $propertyName => $value) {
+ if(in_array($propertyName, $this->protectedProperties)) {
+ $result[403][$propertyName] = null;
+ unset($remainingProperties[$propertyName]);
+ $hasError = true;
+ }
+ }
+
+ if (!$hasError) {
+ // Allowing plugins to take care of property updating
+ $hasError = !$this->broadcastEvent('updateProperties',array(
+ &$remainingProperties,
+ &$result,
+ $node
+ ));
+ }
+
+ // If the node is not an instance of OldSabre\DAV\IProperties, every
+ // property is 403 Forbidden
+ if (!$hasError && count($remainingProperties) && !($node instanceof IProperties)) {
+ $hasError = true;
+ foreach($properties as $propertyName=> $value) {
+ $result[403][$propertyName] = null;
+ }
+ $remainingProperties = array();
+ }
+
+ // Only if there were no errors we may attempt to update the resource
+ if (!$hasError) {
+
+ if (count($remainingProperties)>0) {
+
+ $updateResult = $node->updateProperties($remainingProperties);
+
+ if ($updateResult===true) {
+ // success
+ foreach($remainingProperties as $propertyName=>$value) {
+ $result[200][$propertyName] = null;
+ }
+
+ } elseif ($updateResult===false) {
+ // The node failed to update the properties for an
+ // unknown reason
+ foreach($remainingProperties as $propertyName=>$value) {
+ $result[403][$propertyName] = null;
+ }
+
+ } elseif (is_array($updateResult)) {
+
+ // The node has detailed update information
+ // We need to merge the results with the earlier results.
+ foreach($updateResult as $status => $props) {
+ if (is_array($props)) {
+ if (!isset($result[$status]))
+ $result[$status] = array();
+
+ $result[$status] = array_merge($result[$status], $updateResult[$status]);
+ }
+ }
+
+ } else {
+ throw new Exception('Invalid result from updateProperties');
+ }
+ $remainingProperties = array();
+ }
+
+ }
+
+ foreach($remainingProperties as $propertyName=>$value) {
+ // if there are remaining properties, it must mean
+ // there's a dependency failure
+ $result[424][$propertyName] = null;
+ }
+
+ // Removing empty array values
+ foreach($result as $status=>$props) {
+
+ if (count($props)===0) unset($result[$status]);
+
+ }
+ $result['href'] = $uri;
+ return $result;
+
+ }
+
+ /**
+ * This method checks the main HTTP preconditions.
+ *
+ * Currently these are:
+ * * If-Match
+ * * If-None-Match
+ * * If-Modified-Since
+ * * If-Unmodified-Since
+ *
+ * The method will return true if all preconditions are met
+ * The method will return false, or throw an exception if preconditions
+ * failed. If false is returned the operation should be aborted, and
+ * the appropriate HTTP response headers are already set.
+ *
+ * Normally this method will throw 412 Precondition Failed for failures
+ * related to If-None-Match, If-Match and If-Unmodified Since. It will
+ * set the status to 304 Not Modified for If-Modified_since.
+ *
+ * If the $handleAsGET argument is set to true, it will also return 304
+ * Not Modified for failure of the If-None-Match precondition. This is the
+ * desired behaviour for HTTP GET and HTTP HEAD requests.
+ *
+ * @param bool $handleAsGET
+ * @return bool
+ */
+ public function checkPreconditions($handleAsGET = false) {
+
+ $uri = $this->getRequestUri();
+ $node = null;
+ $lastMod = null;
+ $etag = null;
+
+ if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
+
+ // If-Match contains an entity tag. Only if the entity-tag
+ // matches we are allowed to make the request succeed.
+ // If the entity-tag is '*' we are only allowed to make the
+ // request succeed if a resource exists at that url.
+ try {
+ $node = $this->tree->getNodeForPath($uri);
+ } catch (Exception\NotFound $e) {
+ throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match');
+ }
+
+ // Only need to check entity tags if they are not *
+ if ($ifMatch!=='*') {
+
+ // There can be multiple etags
+ $ifMatch = explode(',',$ifMatch);
+ $haveMatch = false;
+ foreach($ifMatch as $ifMatchItem) {
+
+ // Stripping any extra spaces
+ $ifMatchItem = trim($ifMatchItem,' ');
+
+ $etag = $node->getETag();
+ if ($etag===$ifMatchItem) {
+ $haveMatch = true;
+ } else {
+ // Evolution has a bug where it sometimes prepends the "
+ // with a \. This is our workaround.
+ if (str_replace('\\"','"', $ifMatchItem) === $etag) {
+ $haveMatch = true;
+ }
+ }
+
+ }
+ if (!$haveMatch) {
+ throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
+ }
+ }
+ }
+
+ if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
+
+ // The If-None-Match header contains an etag.
+ // Only if the ETag does not match the current ETag, the request will succeed
+ // The header can also contain *, in which case the request
+ // will only succeed if the entity does not exist at all.
+ $nodeExists = true;
+ if (!$node) {
+ try {
+ $node = $this->tree->getNodeForPath($uri);
+ } catch (Exception\NotFound $e) {
+ $nodeExists = false;
+ }
+ }
+ if ($nodeExists) {
+ $haveMatch = false;
+ if ($ifNoneMatch==='*') $haveMatch = true;
+ else {
+
+ // There might be multiple etags
+ $ifNoneMatch = explode(',', $ifNoneMatch);
+ $etag = $node->getETag();
+
+ foreach($ifNoneMatch as $ifNoneMatchItem) {
+
+ // Stripping any extra spaces
+ $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
+
+ if ($etag===$ifNoneMatchItem) $haveMatch = true;
+
+ }
+
+ }
+
+ if ($haveMatch) {
+ if ($handleAsGET) {
+ $this->httpResponse->sendStatus(304);
+ return false;
+ } else {
+ throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
+ }
+ }
+ }
+
+ }
+
+ if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
+
+ // The If-Modified-Since header contains a date. We
+ // will only return the entity if it has been changed since
+ // that date. If it hasn't been changed, we return a 304
+ // header
+ // Note that this header only has to be checked if there was no If-None-Match header
+ // as per the HTTP spec.
+ $date = HTTP\Util::parseHTTPDate($ifModifiedSince);
+
+ if ($date) {
+ if (is_null($node)) {
+ $node = $this->tree->getNodeForPath($uri);
+ }
+ $lastMod = $node->getLastModified();
+ if ($lastMod) {
+ $lastMod = new \DateTime('@' . $lastMod);
+ if ($lastMod <= $date) {
+ $this->httpResponse->sendStatus(304);
+ $this->httpResponse->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod));
+ return false;
+ }
+ }
+ }
+ }
+
+ if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
+
+ // The If-Unmodified-Since will allow allow the request if the
+ // entity has not changed since the specified date.
+ $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince);
+
+ // We must only check the date if it's valid
+ if ($date) {
+ if (is_null($node)) {
+ $node = $this->tree->getNodeForPath($uri);
+ }
+ $lastMod = $node->getLastModified();
+ if ($lastMod) {
+ $lastMod = new \DateTime('@' . $lastMod);
+ if ($lastMod > $date) {
+ throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since');
+ }
+ }
+ }
+
+ }
+ return true;
+
+ }
+
+ // }}}
+ // {{{ XML Readers & Writers
+
+
+ /**
+ * Generates a WebDAV propfind response body based on a list of nodes.
+ *
+ * If 'strip404s' is set to true, all 404 responses will be removed.
+ *
+ * @param array $fileProperties The list with nodes
+ * @param bool strip404s
+ * @return string
+ */
+ public function generateMultiStatus(array $fileProperties, $strip404s = false) {
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ //$dom->formatOutput = true;
+ $multiStatus = $dom->createElement('d:multistatus');
+ $dom->appendChild($multiStatus);
+
+ // Adding in default namespaces
+ foreach($this->xmlNamespaces as $namespace=>$prefix) {
+
+ $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
+
+ }
+
+ foreach($fileProperties as $entry) {
+
+ $href = $entry['href'];
+ unset($entry['href']);
+
+ if ($strip404s && isset($entry[404])) {
+ unset($entry[404]);
+ }
+
+ $response = new Property\Response($href,$entry);
+ $response->serialize($this,$multiStatus);
+
+ }
+
+ return $dom->saveXML();
+
+ }
+
+ /**
+ * This method parses a PropPatch request
+ *
+ * PropPatch changes the properties for a resource. This method
+ * returns a list of properties.
+ *
+ * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,
+ * and the value contains the property value. If a property is to be removed the value
+ * will be null.
+ *
+ * @param string $body xml body
+ * @return array list of properties in need of updating or deletion
+ */
+ public function parsePropPatchRequest($body) {
+
+ //We'll need to change the DAV namespace declaration to something else in order to make it parsable
+ $dom = XMLUtil::loadDOMDocument($body);
+
+ $newProperties = array();
+
+ foreach($dom->firstChild->childNodes as $child) {
+
+ if ($child->nodeType !== XML_ELEMENT_NODE) continue;
+
+ $operation = XMLUtil::toClarkNotation($child);
+
+ if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
+
+ $innerProperties = XMLUtil::parseProperties($child, $this->propertyMap);
+
+ foreach($innerProperties as $propertyName=>$propertyValue) {
+
+ if ($operation==='{DAV:}remove') {
+ $propertyValue = null;
+ }
+
+ $newProperties[$propertyName] = $propertyValue;
+
+ }
+
+ }
+
+ return $newProperties;
+
+ }
+
+ /**
+ * This method parses the PROPFIND request and returns its information
+ *
+ * This will either be a list of properties, or an empty array; in which case
+ * an {DAV:}allprop was requested.
+ *
+ * @param string $body
+ * @return array
+ */
+ public function parsePropFindRequest($body) {
+
+ // If the propfind body was empty, it means IE is requesting 'all' properties
+ if (!$body) return array();
+
+ $dom = XMLUtil::loadDOMDocument($body);
+ $elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0);
+ return array_keys(XMLUtil::parseProperties($elem));
+
+ }
+
+ // }}}
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php
new file mode 100644
index 0000000..a9061a3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * The baseclass for all server plugins.
+ *
+ * Plugins can modify or extend the servers behaviour.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class ServerPlugin {
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by OldSabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param Server $server
+ * @return void
+ */
+ abstract public function initialize(Server $server);
+
+ /**
+ * This method should return a list of server-features.
+ *
+ * This is for example 'versioning' and is added to the DAV: header
+ * in an OPTIONS response.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array();
+
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getHTTPMethods($uri) {
+
+ return array();
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using \OldSabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return get_class($this);
+
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+
+ return array();
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php
new file mode 100644
index 0000000..959096d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * SimpleCollection
+ *
+ * The SimpleCollection is used to quickly setup static directory structures.
+ * Just create the object with a proper name, and add children to use it.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SimpleCollection extends Collection {
+
+ /**
+ * List of childnodes
+ *
+ * @var array
+ */
+ protected $children = array();
+
+ /**
+ * Name of this resource
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Creates this node
+ *
+ * The name of the node must be passed, child nodes can also be passed.
+ * This nodes must be instances of INode
+ *
+ * @param string $name
+ * @param array $children
+ */
+ public function __construct($name,array $children = array()) {
+
+ $this->name = $name;
+ foreach($children as $child) {
+
+ if (!($child instanceof INode)) throw new Exception('Only instances of OldSabre\DAV\INode are allowed to be passed in the children argument');
+ $this->addChild($child);
+
+ }
+
+ }
+
+ /**
+ * Adds a new childnode to this collection
+ *
+ * @param INode $child
+ * @return void
+ */
+ public function addChild(INode $child) {
+
+ $this->children[$child->getName()] = $child;
+
+ }
+
+ /**
+ * Returns the name of the collection
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->name;
+
+ }
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * This method makes use of the getChildren method to grab all the child nodes, and compares the name.
+ * Generally its wise to override this, as this can usually be optimized
+ *
+ * This method must throw OldSabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws Exception\NotFound
+ * @return INode
+ */
+ public function getChild($name) {
+
+ if (isset($this->children[$name])) return $this->children[$name];
+ throw new Exception\NotFound('File not found: ' . $name . ' in \'' . $this->getName() . '\'');
+
+ }
+
+ /**
+ * Returns a list of children for this collection
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ return array_values($this->children);
+
+ }
+
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php
new file mode 100644
index 0000000..956d020
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * SimpleFile
+ *
+ * The 'SimpleFile' class is used to easily add read-only immutable files to
+ * the directory structure. One usecase would be to add a 'readme.txt' to a
+ * root of a webserver with some standard content.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SimpleFile extends File {
+
+ /**
+ * File contents
+ *
+ * @var string
+ */
+ protected $contents = array();
+
+ /**
+ * Name of this resource
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * A mimetype, such as 'text/plain' or 'text/html'
+ *
+ * @var string
+ */
+ protected $mimeType;
+
+ /**
+ * Creates this node
+ *
+ * The name of the node must be passed, as well as the contents of the
+ * file.
+ *
+ * @param string $name
+ * @param string $contents
+ * @param string|null $mimeType
+ */
+ public function __construct($name, $contents, $mimeType = null) {
+
+ $this->name = $name;
+ $this->contents = $contents;
+ $this->mimeType = $mimeType;
+
+ }
+
+ /**
+ * Returns the node name for this file.
+ *
+ * This name is used to construct the url.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->name;
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * This method may either return a string or a readable stream resource
+ *
+ * @return mixed
+ */
+ public function get() {
+
+ return $this->contents;
+
+ }
+
+ /**
+ * Returns the size of the file, in bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ return strlen($this->contents);
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ * @return string
+ */
+ public function getETag() {
+
+ return '"' . md5($this->contents) . '"';
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ * @return string
+ */
+ public function getContentType() {
+
+ return $this->mimeType;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php
new file mode 100644
index 0000000..57e35c4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * String utility
+ *
+ * This class is mainly used to implement the 'text-match' filter, used by both
+ * the CalDAV calendar-query REPORT, and CardDAV addressbook-query REPORT.
+ * Because they both need it, it was decided to put it in OldSabre\DAV instead.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class StringUtil {
+
+ /**
+ * Checks if a needle occurs in a haystack ;)
+ *
+ * @param string $haystack
+ * @param string $needle
+ * @param string $collation
+ * @param string $matchType
+ * @return bool
+ */
+ static public function textMatch($haystack, $needle, $collation, $matchType = 'contains') {
+
+ switch($collation) {
+
+ case 'i;ascii-casemap' :
+ // default strtolower takes locale into consideration
+ // we don't want this.
+ $haystack = str_replace(range('a','z'), range('A','Z'), $haystack);
+ $needle = str_replace(range('a','z'), range('A','Z'), $needle);
+ break;
+
+ case 'i;octet' :
+ // Do nothing
+ break;
+
+ case 'i;unicode-casemap' :
+ $haystack = mb_strtoupper($haystack, 'UTF-8');
+ $needle = mb_strtoupper($needle, 'UTF-8');
+ break;
+
+ default :
+ throw new Exception\BadRequest('Collation type: ' . $collation . ' is not supported');
+
+ }
+
+ switch($matchType) {
+
+ case 'contains' :
+ return strpos($haystack, $needle)!==false;
+ case 'equals' :
+ return $haystack === $needle;
+ case 'starts-with' :
+ return strpos($haystack, $needle)===0;
+ case 'ends-with' :
+ return strrpos($haystack, $needle)===strlen($haystack)-strlen($needle);
+ default :
+ throw new Exception\BadRequest('Match-type: ' . $matchType . ' is not supported');
+
+ }
+
+ }
+
+ /**
+ * This method takes an input string, checks if it's not valid UTF-8 and
+ * attempts to convert it to UTF-8 if it's not.
+ *
+ * Note that currently this can only convert ISO-8559-1 to UTF-8 (latin-1),
+ * anything else will likely fail.
+ *
+ * @param string $input
+ * @return string
+ */
+ static public function ensureUTF8($input) {
+
+ $encoding = mb_detect_encoding($input , array('UTF-8','ISO-8859-1'), true);
+
+ if ($encoding === 'ISO-8859-1') {
+ return utf8_encode($input);
+ } else {
+ return $input;
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php
new file mode 100644
index 0000000..5a2a2f0
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php
@@ -0,0 +1,289 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Temporary File Filter Plugin
+ *
+ * The purpose of this filter is to intercept some of the garbage files
+ * operation systems and applications tend to generate when mounting
+ * a WebDAV share as a disk.
+ *
+ * It will intercept these files and place them in a separate directory.
+ * these files are not deleted automatically, so it is adviceable to
+ * delete these after they are not accessed for 24 hours.
+ *
+ * Currently it supports:
+ * * OS/X style resource forks and .DS_Store
+ * * desktop.ini and Thumbs.db (windows)
+ * * .*.swp (vim temporary files)
+ * * .dat.* (smultron temporary files)
+ *
+ * Additional patterns can be added, by adding on to the
+ * temporaryFilePatterns property.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class TemporaryFileFilterPlugin extends ServerPlugin {
+
+ /**
+ * This is the list of patterns we intercept.
+ * If new patterns are added, they must be valid patterns for preg_match.
+ *
+ * @var array
+ */
+ public $temporaryFilePatterns = array(
+ '/^\._(.*)$/', // OS/X resource forks
+ '/^.DS_Store$/', // OS/X custom folder settings
+ '/^desktop.ini$/', // Windows custom folder settings
+ '/^Thumbs.db$/', // Windows thumbnail cache
+ '/^.(.*).swp$/', // ViM temporary files
+ '/^\.dat(.*)$/', // Smultron seems to create these
+ '/^~lock.(.*)#$/', // Windows 7 lockfiles
+ );
+
+ /**
+ * A reference to the main Server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * This is the directory where this plugin
+ * will store it's files.
+ *
+ * @var string
+ */
+ private $dataDir;
+
+ /**
+ * Creates the plugin.
+ *
+ * Make sure you specify a directory for your files. If you don't, we
+ * will use PHP's directory for session-storage instead, and you might
+ * not want that.
+ *
+ * @param string|null $dataDir
+ */
+ public function __construct($dataDir = null) {
+
+ if (!$dataDir) $dataDir = ini_get('session.save_path').'/sabredav/';
+ if (!is_dir($dataDir)) mkdir($dataDir);
+ $this->dataDir = $dataDir;
+
+ }
+
+ /**
+ * Initialize the plugin
+ *
+ * This is called automatically be the Server class after this plugin is
+ * added with OldSabre\DAV\Server::addPlugin()
+ *
+ * @param Server $server
+ * @return void
+ */
+ public function initialize(Server $server) {
+
+ $this->server = $server;
+ $server->subscribeEvent('beforeMethod',array($this,'beforeMethod'));
+ $server->subscribeEvent('beforeCreateFile',array($this,'beforeCreateFile'));
+
+ }
+
+ /**
+ * This method is called before any HTTP method handler
+ *
+ * This method intercepts any GET, DELETE, PUT and PROPFIND calls to
+ * filenames that are known to match the 'temporary file' regex.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ if (!$tempLocation = $this->isTempFile($uri))
+ return true;
+
+ switch($method) {
+ case 'GET' :
+ return $this->httpGet($tempLocation);
+ case 'PUT' :
+ return $this->httpPut($tempLocation);
+ case 'PROPFIND' :
+ return $this->httpPropfind($tempLocation, $uri);
+ case 'DELETE' :
+ return $this->httpDelete($tempLocation);
+ }
+ return true;
+
+ }
+
+ /**
+ * This method is invoked if some subsystem creates a new file.
+ *
+ * This is used to deal with HTTP LOCK requests which create a new
+ * file.
+ *
+ * @param string $uri
+ * @param resource $data
+ * @return bool
+ */
+ public function beforeCreateFile($uri,$data) {
+
+ if ($tempPath = $this->isTempFile($uri)) {
+
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp','true');
+ file_put_contents($tempPath,$data);
+ return false;
+ }
+ return true;
+
+ }
+
+ /**
+ * This method will check if the url matches the temporary file pattern
+ * if it does, it will return an path based on $this->dataDir for the
+ * temporary file storage.
+ *
+ * @param string $path
+ * @return boolean|string
+ */
+ protected function isTempFile($path) {
+
+ // We're only interested in the basename.
+ list(, $tempPath) = URLUtil::splitPath($path);
+
+ foreach($this->temporaryFilePatterns as $tempFile) {
+
+ if (preg_match($tempFile,$tempPath)) {
+ return $this->getDataDir() . '/sabredav_' . md5($path) . '.tempfile';
+ }
+
+ }
+
+ return false;
+
+ }
+
+
+ /**
+ * This method handles the GET method for temporary files.
+ * If the file doesn't exist, it will return false which will kick in
+ * the regular system for the GET method.
+ *
+ * @param string $tempLocation
+ * @return bool
+ */
+ public function httpGet($tempLocation) {
+
+ if (!file_exists($tempLocation)) return true;
+
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('Content-Type','application/octet-stream');
+ $hR->setHeader('Content-Length',filesize($tempLocation));
+ $hR->setHeader('X-Sabre-Temp','true');
+ $hR->sendStatus(200);
+ $hR->sendBody(fopen($tempLocation,'r'));
+ return false;
+
+ }
+
+ /**
+ * This method handles the PUT method.
+ *
+ * @param string $tempLocation
+ * @return bool
+ */
+ public function httpPut($tempLocation) {
+
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp','true');
+
+ $newFile = !file_exists($tempLocation);
+
+ if (!$newFile && ($this->server->httpRequest->getHeader('If-None-Match'))) {
+ throw new Exception\PreconditionFailed('The resource already exists, and an If-None-Match header was supplied');
+ }
+
+ file_put_contents($tempLocation,$this->server->httpRequest->getBody());
+ $hR->sendStatus($newFile?201:200);
+ return false;
+
+ }
+
+ /**
+ * This method handles the DELETE method.
+ *
+ * If the file didn't exist, it will return false, which will make the
+ * standard HTTP DELETE handler kick in.
+ *
+ * @param string $tempLocation
+ * @return bool
+ */
+ public function httpDelete($tempLocation) {
+
+ if (!file_exists($tempLocation)) return true;
+
+ unlink($tempLocation);
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp','true');
+ $hR->sendStatus(204);
+ return false;
+
+ }
+
+ /**
+ * This method handles the PROPFIND method.
+ *
+ * It's a very lazy method, it won't bother checking the request body
+ * for which properties were requested, and just sends back a default
+ * set of properties.
+ *
+ * @param string $tempLocation
+ * @param string $uri
+ * @return bool
+ */
+ public function httpPropfind($tempLocation, $uri) {
+
+ if (!file_exists($tempLocation)) return true;
+
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp','true');
+ $hR->sendStatus(207);
+ $hR->setHeader('Content-Type','application/xml; charset=utf-8');
+
+ $this->server->parsePropFindRequest($this->server->httpRequest->getBody(true));
+
+ $properties = array(
+ 'href' => $uri,
+ 200 => array(
+ '{DAV:}getlastmodified' => new Property\GetLastModified(filemtime($tempLocation)),
+ '{DAV:}getcontentlength' => filesize($tempLocation),
+ '{DAV:}resourcetype' => new Property\ResourceType(null),
+ '{'.Server::NS_SABREDAV.'}tempFile' => true,
+
+ ),
+ );
+
+ $data = $this->server->generateMultiStatus(array($properties));
+ $hR->sendBody($data);
+ return false;
+
+ }
+
+
+ /**
+ * This method returns the directory where the temporary files should be stored.
+ *
+ * @return string
+ */
+ protected function getDataDir()
+ {
+ return $this->dataDir;
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php
new file mode 100644
index 0000000..8313918
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Abstract tree object
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Tree {
+
+ /**
+ * This function must return an INode object for a path
+ * If a Path doesn't exist, thrown a Exception_NotFound
+ *
+ * @param string $path
+ * @throws Exception\NotFound
+ * @return INode
+ */
+ abstract function getNodeForPath($path);
+
+ /**
+ * This function allows you to check if a node exists.
+ *
+ * Implementors of this class should override this method to make
+ * it cheaper.
+ *
+ * @param string $path
+ * @return bool
+ */
+ public function nodeExists($path) {
+
+ try {
+
+ $this->getNodeForPath($path);
+ return true;
+
+ } catch (Exception\NotFound $e) {
+
+ return false;
+
+ }
+
+ }
+
+ /**
+ * Copies a file from path to another
+ *
+ * @param string $sourcePath The source location
+ * @param string $destinationPath The full destination path
+ * @return void
+ */
+ public function copy($sourcePath, $destinationPath) {
+
+ $sourceNode = $this->getNodeForPath($sourcePath);
+
+ // grab the dirname and basename components
+ list($destinationDir, $destinationName) = URLUtil::splitPath($destinationPath);
+
+ $destinationParent = $this->getNodeForPath($destinationDir);
+ $this->copyNode($sourceNode,$destinationParent,$destinationName);
+
+ $this->markDirty($destinationDir);
+
+ }
+
+ /**
+ * Moves a file from one location to another
+ *
+ * @param string $sourcePath The path to the file which should be moved
+ * @param string $destinationPath The full destination path, so not just the destination parent node
+ * @return int
+ */
+ public function move($sourcePath, $destinationPath) {
+
+ list($sourceDir, $sourceName) = URLUtil::splitPath($sourcePath);
+ list($destinationDir, $destinationName) = URLUtil::splitPath($destinationPath);
+
+ if ($sourceDir===$destinationDir) {
+ $renameable = $this->getNodeForPath($sourcePath);
+ $renameable->setName($destinationName);
+ } else {
+ $this->copy($sourcePath,$destinationPath);
+ $this->getNodeForPath($sourcePath)->delete();
+ }
+ $this->markDirty($sourceDir);
+ $this->markDirty($destinationDir);
+
+ }
+
+ /**
+ * Deletes a node from the tree
+ *
+ * @param string $path
+ * @return void
+ */
+ public function delete($path) {
+
+ $node = $this->getNodeForPath($path);
+ $node->delete();
+
+ list($parent) = URLUtil::splitPath($path);
+ $this->markDirty($parent);
+
+ }
+
+ /**
+ * Returns a list of childnodes for a given path.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getChildren($path) {
+
+ $node = $this->getNodeForPath($path);
+ return $node->getChildren();
+
+ }
+
+ /**
+ * This method is called with every tree update
+ *
+ * Examples of tree updates are:
+ * * node deletions
+ * * node creations
+ * * copy
+ * * move
+ * * renaming nodes
+ *
+ * If Tree classes implement a form of caching, this will allow
+ * them to make sure caches will be expired.
+ *
+ * If a path is passed, it is assumed that the entire subtree is dirty
+ *
+ * @param string $path
+ * @return void
+ */
+ public function markDirty($path) {
+
+
+ }
+
+ /**
+ * copyNode
+ *
+ * @param INode $source
+ * @param ICollection $destinationParent
+ * @param string $destinationName
+ * @return void
+ */
+ protected function copyNode(INode $source,ICollection $destinationParent,$destinationName = null) {
+
+ if (!$destinationName) $destinationName = $source->getName();
+
+ if ($source instanceof IFile) {
+
+ $data = $source->get();
+
+ // If the body was a string, we need to convert it to a stream
+ if (is_string($data)) {
+ $stream = fopen('php://temp','r+');
+ fwrite($stream,$data);
+ rewind($stream);
+ $data = $stream;
+ }
+ $destinationParent->createFile($destinationName,$data);
+ $destination = $destinationParent->getChild($destinationName);
+
+ } elseif ($source instanceof ICollection) {
+
+ $destinationParent->createDirectory($destinationName);
+
+ $destination = $destinationParent->getChild($destinationName);
+ foreach($source->getChildren() as $child) {
+
+ $this->copyNode($child,$destination);
+
+ }
+
+ }
+ if ($source instanceof IProperties && $destination instanceof IProperties) {
+
+ $props = $source->getProperties(array());
+ $destination->updateProperties($props);
+
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php
new file mode 100644
index 0000000..9d1a638
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace OldSabre\DAV\Tree;
+
+use OldSabre\DAV;
+
+/**
+ * FileSystem Tree
+ *
+ * This class is an alternative to the standard ObjectTree. This tree can only
+ * use OldSabre\DAV\FS\Directory and File classes, but as a result it allows for a few
+ * optimizations that otherwise wouldn't be possible.
+ *
+ * Specifically copying and moving are much, much faster.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Filesystem extends DAV\Tree {
+
+ /**
+ * Base url on the filesystem.
+ *
+ * @var string
+ */
+ protected $basePath;
+
+ /**
+ * Creates this tree
+ *
+ * Supply the path you'd like to share.
+ *
+ * @param string $basePath
+ */
+ public function __construct($basePath) {
+
+ $this->basePath = $basePath;
+
+ }
+
+ /**
+ * Returns a new node for the given path
+ *
+ * @param string $path
+ * @return DAV\FS\Node
+ */
+ public function getNodeForPath($path) {
+
+ $realPath = $this->getRealPath($path);
+ if (!file_exists($realPath)) {
+ throw new DAV\Exception\NotFound('File at location ' . $realPath . ' not found');
+ }
+ if (is_dir($realPath)) {
+ return new DAV\FS\Directory($realPath);
+ } else {
+ return new DAV\FS\File($realPath);
+ }
+
+ }
+
+ /**
+ * Returns the real filesystem path for a webdav url.
+ *
+ * @param string $publicPath
+ * @return string
+ */
+ protected function getRealPath($publicPath) {
+
+ return rtrim($this->basePath,'/') . '/' . trim($publicPath,'/');
+
+ }
+
+ /**
+ * Copies a file or directory.
+ *
+ * This method must work recursively and delete the destination
+ * if it exists
+ *
+ * @param string $source
+ * @param string $destination
+ * @return void
+ */
+ public function copy($source,$destination) {
+
+ $source = $this->getRealPath($source);
+ $destination = $this->getRealPath($destination);
+ $this->realCopy($source,$destination);
+
+ }
+
+ /**
+ * Used by self::copy
+ *
+ * @param string $source
+ * @param string $destination
+ * @return void
+ */
+ protected function realCopy($source,$destination) {
+
+ if (is_file($source)) {
+ copy($source,$destination);
+ } else {
+ mkdir($destination);
+ foreach(scandir($source) as $subnode) {
+
+ if ($subnode=='.' || $subnode=='..') continue;
+ $this->realCopy($source.'/'.$subnode,$destination.'/'.$subnode);
+
+ }
+ }
+
+ }
+
+ /**
+ * Moves a file or directory recursively.
+ *
+ * If the destination exists, delete it first.
+ *
+ * @param string $source
+ * @param string $destination
+ * @return void
+ */
+ public function move($source,$destination) {
+
+ $source = $this->getRealPath($source);
+ $destination = $this->getRealPath($destination);
+ rename($source,$destination);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php
new file mode 100644
index 0000000..f6ebb47
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * URL utility class
+ *
+ * This class provides methods to deal with encoding and decoding url (percent encoded) strings.
+ *
+ * It was not possible to use PHP's built-in methods for this, because some clients don't like
+ * encoding of certain characters.
+ *
+ * Specifically, it was found that GVFS (gnome's webdav client) does not like encoding of ( and
+ * ). Since these are reserved, but don't have a reserved meaning in url, these characters are
+ * kept as-is.
+ *
+ * It was also discovered that versions of the SOGO connector for thunderbird
+ * has issues with urlencoded colons.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class URLUtil {
+
+ /**
+ * Encodes the path of a url.
+ *
+ * slashes (/) are treated as path-separators.
+ *
+ * @param string $path
+ * @return string
+ */
+ static function encodePath($path) {
+
+ return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:])/',function($match) {
+
+ return '%'.sprintf('%02x',ord($match[0]));
+
+ }, $path);
+
+ }
+
+ /**
+ * Encodes a 1 segment of a path
+ *
+ * Slashes are considered part of the name, and are encoded as %2f
+ *
+ * @param string $pathSegment
+ * @return string
+ */
+ static function encodePathSegment($pathSegment) {
+
+ return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):])/',function($match) {
+
+ return '%'.sprintf('%02x',ord($match[0]));
+
+ }, $pathSegment);
+ }
+
+ /**
+ * Decodes a url-encoded path
+ *
+ * @param string $path
+ * @return string
+ */
+ static function decodePath($path) {
+
+ return self::decodePathSegment($path);
+
+ }
+
+ /**
+ * Decodes a url-encoded path segment
+ *
+ * @param string $path
+ * @return string
+ */
+ static function decodePathSegment($path) {
+
+ $path = rawurldecode($path);
+ $encoding = mb_detect_encoding($path, array('UTF-8','ISO-8859-1'));
+
+ switch($encoding) {
+
+ case 'ISO-8859-1' :
+ $path = utf8_encode($path);
+
+ }
+
+ return $path;
+
+ }
+
+ /**
+ * Returns the 'dirname' and 'basename' for a path.
+ *
+ * The reason there is a custom function for this purpose, is because
+ * basename() is locale aware (behaviour changes if C locale or a UTF-8 locale is used)
+ * and we need a method that just operates on UTF-8 characters.
+ *
+ * In addition basename and dirname are platform aware, and will treat backslash (\) as a
+ * directory separator on windows.
+ *
+ * This method returns the 2 components as an array.
+ *
+ * If there is no dirname, it will return an empty string. Any / appearing at the end of the
+ * string is stripped off.
+ *
+ * @param string $path
+ * @return array
+ */
+ static function splitPath($path) {
+
+ $matches = array();
+ if(preg_match('/^(?:(?:(.*)(?:\/+))?([^\/]+))(?:\/?)$/u',$path,$matches)) {
+ return array($matches[1],$matches[2]);
+ } else {
+ return array(null,null);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php
new file mode 100644
index 0000000..aed84e8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * UUID Utility
+ *
+ * This class has static methods to generate and validate UUID's.
+ * UUIDs are used a decent amount within various *DAV standards, so it made
+ * sense to include it.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class UUIDUtil {
+
+ /**
+ * Returns a pseudo-random v4 UUID
+ *
+ * This function is based on a comment by Andrew Moore on php.net
+ *
+ * @see http://www.php.net/manual/en/function.uniqid.php#94959
+ * @return string
+ */
+ static function getUUID() {
+
+ return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+ // 32 bits for "time_low"
+ mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
+
+ // 16 bits for "time_mid"
+ mt_rand( 0, 0xffff ),
+
+ // 16 bits for "time_hi_and_version",
+ // four most significant bits holds version number 4
+ mt_rand( 0, 0x0fff ) | 0x4000,
+
+ // 16 bits, 8 bits for "clk_seq_hi_res",
+ // 8 bits for "clk_seq_low",
+ // two most significant bits holds zero and one for variant DCE1.1
+ mt_rand( 0, 0x3fff ) | 0x8000,
+
+ // 48 bits for "node"
+ mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
+ );
+ }
+
+ /**
+ * Checks if a string is a valid UUID.
+ *
+ * @param string $uuid
+ * @return bool
+ */
+ static function validateUUID($uuid) {
+
+ return preg_match(
+ '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i',
+ $uuid
+ ) == true;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php
new file mode 100644
index 0000000..62e35b3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * This class contains the SabreDAV version constants.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.12';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php
new file mode 100644
index 0000000..5ee0b5d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * XML utilities for WebDAV
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class XMLUtil {
+
+ /**
+ * Returns the 'clark notation' for an element.
+ *
+ * For example, and element encoded as:
+ * <b:myelem xmlns:b="http://www.example.org/" />
+ * will be returned as:
+ * {http://www.example.org}myelem
+ *
+ * This format is used throughout the SabreDAV sourcecode.
+ * Elements encoded with the urn:DAV namespace will
+ * be returned as if they were in the DAV: namespace. This is to avoid
+ * compatibility problems.
+ *
+ * This function will return null if a nodetype other than an Element is passed.
+ *
+ * @param \DOMNode $dom
+ * @return string
+ */
+ static function toClarkNotation(\DOMNode $dom) {
+
+ if ($dom->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;
+
+ // Mapping to clark notation
+ return '{' . $ns . '}' . $dom->localName;
+
+ }
+
+ /**
+ * Parses a clark-notation string, and returns the namespace and element
+ * name components.
+ *
+ * If the string was invalid, it will throw an InvalidArgumentException.
+ *
+ * @param string $str
+ * @throws InvalidArgumentException
+ * @return array
+ */
+ static function parseClarkNotation($str) {
+
+ if (!preg_match('/^{([^}]*)}(.*)$/',$str,$matches)) {
+ throw new \InvalidArgumentException('\'' . $str . '\' is not a valid clark-notation formatted string');
+ }
+
+ return array(
+ $matches[1],
+ $matches[2]
+ );
+
+ }
+
+ /**
+ * 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 OldSabre\DAV\Exception\BadRequest exception for any xml errors.
+ * It does not preserve whitespace, and it converts the DAV: namespace to urn:DAV.
+ *
+ * @param string $xml
+ * @throws OldSabre\DAV\Exception\BadRequest
+ * @return DOMDocument
+ */
+ static function loadDOMDocument($xml) {
+
+ if (empty($xml))
+ throw new Exception\BadRequest('Empty XML document sent');
+
+ // The BitKinex client sends xml documents as UTF-16. PHP 5.3.1 (and presumably lower)
+ // does not support this, so we must intercept this and convert to UTF-8.
+ if (substr($xml,0,12) === "\x3c\x00\x3f\x00\x78\x00\x6d\x00\x6c\x00\x20\x00") {
+
+ // Note: the preceeding byte sequence is "<?xml" encoded as UTF_16, without the BOM.
+ $xml = iconv('UTF-16LE','UTF-8',$xml);
+
+ // Because the xml header might specify the encoding, we must also change this.
+ // This regex looks for the string encoding="UTF-16" and replaces it with
+ // encoding="UTF-8".
+ $xml = preg_replace('|<\?xml([^>]*)encoding="UTF-16"([^>]*)>|u','<?xml\1encoding="UTF-8"\2>',$xml);
+
+ }
+
+ // Retaining old error setting
+ $oldErrorSetting = libxml_use_internal_errors(true);
+ // Fixes an XXE vulnerability on PHP versions older than 5.3.23 or
+ // 5.4.13.
+ $oldEntityLoaderSetting = libxml_disable_entity_loader(true);
+
+ // Clearing any previous errors
+ libxml_clear_errors();
+
+ $dom = new \DOMDocument();
+
+ // We don't generally care about any whitespace
+ $dom->preserveWhiteSpace = false;
+
+ $dom->loadXML(self::convertDAVNamespace($xml),LIBXML_NOWARNING | LIBXML_NOERROR);
+
+ if ($error = libxml_get_last_error()) {
+ libxml_clear_errors();
+ throw new Exception\BadRequest('The request body had an invalid XML body. (message: ' . $error->message . ', errorcode: ' . $error->code . ', line: ' . $error->line . ')');
+ }
+
+ // Restoring old mechanism for error handling
+ if ($oldErrorSetting===false) libxml_use_internal_errors(false);
+ if ($oldEntityLoaderSetting===false) libxml_disable_entity_loader(false);
+
+ return $dom;
+
+ }
+
+ /**
+ * Parses all WebDAV properties out of a DOM Element
+ *
+ * Generally WebDAV properties are enclosed in {DAV:}prop elements. This
+ * method helps by going through all these and pulling out the actual
+ * propertynames, making them array keys and making the property values,
+ * well.. the array values.
+ *
+ * If no value was given (self-closing element) null will be used as the
+ * value. This is used in for example PROPFIND requests.
+ *
+ * Complex values are supported through the propertyMap argument. The
+ * propertyMap should have the clark-notation properties as it's keys, and
+ * classnames as values.
+ *
+ * When any of these properties are found, the unserialize() method will be
+ * (statically) called. The result of this method is used as the value.
+ *
+ * @param \DOMElement $parentNode
+ * @param array $propertyMap
+ * @return array
+ */
+ static function parseProperties(\DOMElement $parentNode, array $propertyMap = array()) {
+
+ $propList = array();
+ foreach($parentNode->childNodes as $propNode) {
+
+ if (self::toClarkNotation($propNode)!=='{DAV:}prop') continue;
+
+ foreach($propNode->childNodes as $propNodeData) {
+
+ /* If there are no elements in here, we actually get 1 text node, this special case is dedicated to netdrive */
+ if ($propNodeData->nodeType != XML_ELEMENT_NODE) continue;
+
+ $propertyName = self::toClarkNotation($propNodeData);
+ if (isset($propertyMap[$propertyName])) {
+ $propList[$propertyName] = call_user_func(array($propertyMap[$propertyName],'unserialize'),$propNodeData);
+ } else {
+ $propList[$propertyName] = $propNodeData->textContent;
+ }
+ }
+
+
+ }
+ return $propList;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php
new file mode 100644
index 0000000..1367037
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * Principals Collection
+ *
+ * This is a helper class that easily allows you to create a collection that
+ * has a childnode for every principal.
+ *
+ * To use this class, simply implement the getChildForPrincipal method.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractPrincipalCollection extends DAV\Collection implements IPrincipalCollection {
+
+ /**
+ * Node or 'directory' name.
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Principal backend
+ *
+ * @var PrincipalBackend\BackendInterface
+ */
+ protected $principalBackend;
+
+ /**
+ * If this value is set to true, it effectively disables listing of users
+ * it still allows user to find other users if they have an exact url.
+ *
+ * @var bool
+ */
+ public $disableListing = false;
+
+ /**
+ * Creates the object
+ *
+ * This object must be passed the principal backend. This object will
+ * filter all principals from a specified prefix ($principalPrefix). The
+ * default is 'principals', if your principals are stored in a different
+ * collection, override $principalPrefix
+ *
+ *
+ * @param PrincipalBackend\BackendInterface $principalBackend
+ * @param string $principalPrefix
+ */
+ public function __construct(PrincipalBackend\BackendInterface $principalBackend, $principalPrefix = 'principals') {
+
+ $this->principalPrefix = $principalPrefix;
+ $this->principalBackend = $principalBackend;
+
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principalInfo
+ * @return IPrincipal
+ */
+ abstract function getChildForPrincipal(array $principalInfo);
+
+ /**
+ * Returns the name of this collection.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ list(,$name) = DAV\URLUtil::splitPath($this->principalPrefix);
+ return $name;
+
+ }
+
+ /**
+ * Return the list of users
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ if ($this->disableListing)
+ throw new DAV\Exception\MethodNotAllowed('Listing members of this collection is disabled');
+
+ $children = array();
+ foreach($this->principalBackend->getPrincipalsByPrefix($this->principalPrefix) as $principalInfo) {
+
+ $children[] = $this->getChildForPrincipal($principalInfo);
+
+
+ }
+ return $children;
+
+ }
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * @param string $name
+ * @throws DAV\Exception\NotFound
+ * @return IPrincipal
+ */
+ public function getChild($name) {
+
+ $principalInfo = $this->principalBackend->getPrincipalByPath($this->principalPrefix . '/' . $name);
+ if (!$principalInfo) throw new DAV\Exception\NotFound('Principal with name ' . $name . ' not found');
+ return $this->getChildForPrincipal($principalInfo);
+
+ }
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * If multiple properties are being searched on, the search should be
+ * AND'ed.
+ *
+ * This method should simply return a list of 'child names', which may be
+ * used to call $this->getChild in the future.
+ *
+ * @param array $searchProperties
+ * @return array
+ */
+ public function searchPrincipals(array $searchProperties) {
+
+ $result = $this->principalBackend->searchPrincipals($this->principalPrefix, $searchProperties);
+ $r = array();
+
+ foreach($result as $row) {
+ list(, $r[]) = DAV\URLUtil::splitPath($row);
+ }
+
+ return $r;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php
new file mode 100644
index 0000000..a636bdc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * This exception is thrown when a client attempts to set conflicting
+ * permissions.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AceConflict extends DAV\Exception\Conflict {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}no-ace-conflict element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:no-ace-conflict');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php
new file mode 100644
index 0000000..c2c3ab1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * NeedPrivileges
+ *
+ * The 403-need privileges is thrown when a user didn't have the appropriate
+ * permissions to perform an operation
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NeedPrivileges extends DAV\Exception\Forbidden {
+
+ /**
+ * The relevant uri
+ *
+ * @var string
+ */
+ protected $uri;
+
+ /**
+ * The privileges the user didn't have.
+ *
+ * @var array
+ */
+ protected $privileges;
+
+ /**
+ * Constructor
+ *
+ * @param string $uri
+ * @param array $privileges
+ */
+ public function __construct($uri,array $privileges) {
+
+ $this->uri = $uri;
+ $this->privileges = $privileges;
+
+ parent::__construct('User did not have the required privileges (' . implode(',', $privileges) . ') for path "' . $uri . '"');
+
+ }
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}need-privileges element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:need-privileges');
+ $errorNode->appendChild($np);
+
+ foreach($this->privileges as $privilege) {
+
+ $resource = $doc->createElementNS('DAV:','d:resource');
+ $np->appendChild($resource);
+
+ $resource->appendChild($doc->createElementNS('DAV:','d:href',$server->getBaseUri() . $this->uri));
+
+ $priv = $doc->createElementNS('DAV:','d:privilege');
+ $resource->appendChild($priv);
+
+ preg_match('/^{([^}]*)}(.*)$/',$privilege,$privilegeParts);
+ $priv->appendChild($doc->createElementNS($privilegeParts[1],'d:' . $privilegeParts[2]));
+
+
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php
new file mode 100644
index 0000000..b7c78d9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * This exception is thrown when a user tries to set a privilege that's marked
+ * as abstract.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NoAbstract extends DAV\Exception\PreconditionFailed {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}no-abstract element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:no-abstract');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php
new file mode 100644
index 0000000..c7f99e8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * If a client tried to set a privilege assigned to a non-existant principal,
+ * this exception will be thrown.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotRecognizedPrincipal extends DAV\Exception\PreconditionFailed {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}recognized-principal element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:recognized-principal');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php
new file mode 100644
index 0000000..7e3adb6
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * If a client tried to set a privilege that doesn't exist, this exception will
+ * be thrown.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotSupportedPrivilege extends DAV\Exception\PreconditionFailed {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}not-supported-privilege element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:not-supported-privilege');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php
new file mode 100644
index 0000000..7e0508f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * ACL-enabled node
+ *
+ * If you want to add WebDAV ACL to a node, you must implement this class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IACL extends DAV\INode {
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getOwner();
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getGroup();
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ function getACL();
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's as an array argument.
+ *
+ * @param array $acl
+ * @return void
+ */
+ function setACL(array $acl);
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ function getSupportedPrivilegeSet();
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php
new file mode 100644
index 0000000..3e2c95a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+use OldSabre\DAV;
+
+/**
+ * IPrincipal interface
+ *
+ * Implement this interface to define your own principals
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IPrincipal extends DAV\INode {
+
+ /**
+ * Returns a list of alternative urls for a principal
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ function getAlternateUriSet();
+
+ /**
+ * Returns the full principal url
+ *
+ * @return string
+ */
+ function getPrincipalUrl();
+
+ /**
+ * Returns the list of group members
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ function getGroupMemberSet();
+
+ /**
+ * Returns the list of groups this principal is member of
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ function getGroupMembership();
+
+ /**
+ * Sets a list of group members
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ *
+ * @param array $principals
+ * @return void
+ */
+ function setGroupMemberSet(array $principals);
+
+ /**
+ * Returns the displayname
+ *
+ * This should be a human readable name for the principal.
+ * If none is available, return the nodename.
+ *
+ * @return string
+ */
+ function getDisplayName();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php
new file mode 100644
index 0000000..13a6c53
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+use OldSabre\DAV;
+
+/**
+ * Principal Collection interface.
+ *
+ * Implement this interface to ensure that your principal collection can be
+ * searched using the principal-property-search REPORT.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IPrincipalCollection extends DAV\INode {
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * If multiple properties are being searched on, the search should be
+ * AND'ed.
+ *
+ * This method should simply return a list of 'child names', which may be
+ * used to call $this->getChild in the future.
+ *
+ * @param array $searchProperties
+ * @return array
+ */
+ function searchPrincipals(array $searchProperties);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php
new file mode 100644
index 0000000..f2cc020
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php
@@ -0,0 +1,1402 @@
+<?php
+
+namespace OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * SabreDAV ACL Plugin
+ *
+ * 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
+ * property, defined in RFC5397 and the {DAV:}expand-property report, as
+ * defined in RFC3253.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * Recursion constants
+ *
+ * This only checks the base node
+ */
+ const R_PARENT = 1;
+
+ /**
+ * Recursion constants
+ *
+ * This checks every node in the tree
+ */
+ const R_RECURSIVE = 2;
+
+ /**
+ * Recursion constants
+ *
+ * This checks every parentnode in the tree, but not leaf-nodes.
+ */
+ const R_RECURSIVEPARENTS = 3;
+
+ /**
+ * Reference to server object.
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * List of urls containing principal collections.
+ * Modify this if your principals are located elsewhere.
+ *
+ * @var array
+ */
+ public $principalCollectionSet = array(
+ 'principals',
+ );
+
+ /**
+ * By default ACL is only enforced for nodes that have ACL support (the
+ * ones that implement IACL). For any other node, access is
+ * always granted.
+ *
+ * To override this behaviour you can turn this setting off. This is useful
+ * if you plan to fully support ACL in the entire tree.
+ *
+ * @var bool
+ */
+ public $allowAccessToNodesWithoutACL = true;
+
+ /**
+ * By default nodes that are inaccessible by the user, can still be seen
+ * in directory listings (PROPFIND on parent with Depth: 1)
+ *
+ * In certain cases it's desirable to hide inaccessible nodes. Setting this
+ * to true will cause these nodes to be hidden from directory listings.
+ *
+ * @var bool
+ */
+ public $hideNodesFromListings = false;
+
+ /**
+ * This string is prepended to the username of the currently logged in
+ * user. This allows the plugin to determine the principal path based on
+ * the username.
+ *
+ * @var string
+ */
+ public $defaultUsernamePath = 'principals';
+
+ /**
+ * This list of properties are the properties a client can search on using
+ * the {DAV:}principal-property-search report.
+ *
+ * The keys are the property names, values are descriptions.
+ *
+ * @var array
+ */
+ public $principalSearchPropertySet = array(
+ '{DAV:}displayname' => 'Display name',
+ '{http://sabredav.org/ns}email-address' => 'Email address',
+ );
+
+ /**
+ * Any principal uri's added here, will automatically be added to the list
+ * of ACL's. They will effectively receive {DAV:}all privileges, as a
+ * protected privilege.
+ *
+ * @var array
+ */
+ public $adminPrincipals = array();
+
+ /**
+ * Returns a list of features added by this plugin.
+ *
+ * This list is used in the response of a HTTP OPTIONS request.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('access-control', 'calendarserver-principal-property-search');
+
+ }
+
+ /**
+ * Returns a list of available methods for a given url
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getMethods($uri) {
+
+ return array('ACL');
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using OldSabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'acl';
+
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+
+ return array(
+ '{DAV:}expand-property',
+ '{DAV:}principal-property-search',
+ '{DAV:}principal-search-property-set',
+ );
+
+ }
+
+
+ /**
+ * Checks if the current user has the specified privilege(s).
+ *
+ * You can specify a single privilege, or a list of privileges.
+ * This method will throw an exception if the privilege is not available
+ * and return true otherwise.
+ *
+ * @param string $uri
+ * @param array|string $privileges
+ * @param int $recursion
+ * @param bool $throwExceptions if set to false, this method won't throw exceptions.
+ * @throws OldSabre\DAVACL\Exception\NeedPrivileges
+ * @return bool
+ */
+ public function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) {
+
+ if (!is_array($privileges)) $privileges = array($privileges);
+
+ $acl = $this->getCurrentUserPrivilegeSet($uri);
+
+ if (is_null($acl)) {
+ if ($this->allowAccessToNodesWithoutACL) {
+ return true;
+ } else {
+ if ($throwExceptions)
+ throw new Exception\NeedPrivileges($uri,$privileges);
+ else
+ return false;
+
+ }
+ }
+
+ $failed = array();
+ foreach($privileges as $priv) {
+
+ if (!in_array($priv, $acl)) {
+ $failed[] = $priv;
+ }
+
+ }
+
+ if ($failed) {
+ if ($throwExceptions)
+ throw new Exception\NeedPrivileges($uri,$failed);
+ else
+ return false;
+ }
+ return true;
+
+ }
+
+ /**
+ * Returns the standard users' principal.
+ *
+ * This is one authorative principal url for the current user.
+ * This method will return null if the user wasn't logged in.
+ *
+ * @return string|null
+ */
+ public function getCurrentUserPrincipal() {
+
+ $authPlugin = $this->server->getPlugin('auth');
+ if (is_null($authPlugin)) return null;
+ /** @var $authPlugin OldSabre\DAV\Auth\Plugin */
+
+ $userName = $authPlugin->getCurrentUser();
+ if (!$userName) return null;
+
+ return $this->defaultUsernamePath . '/' . $userName;
+
+ }
+
+
+ /**
+ * Returns a list of principals that's associated to the current
+ * user, either directly or through group membership.
+ *
+ * @return array
+ */
+ public function getCurrentUserPrincipals() {
+
+ $currentUser = $this->getCurrentUserPrincipal();
+
+ if (is_null($currentUser)) return array();
+
+ return array_merge(
+ array($currentUser),
+ $this->getPrincipalMembership($currentUser)
+ );
+
+ }
+
+ /**
+ * This array holds a cache for all the principals that are associated with
+ * a single principal.
+ *
+ * @var array
+ */
+ protected $principalMembershipCache = array();
+
+
+ /**
+ * Returns all the principal groups the specified principal is a member of.
+ *
+ * @param string $principal
+ * @return array
+ */
+ public function getPrincipalMembership($mainPrincipal) {
+
+ // First check our cache
+ if (isset($this->principalMembershipCache[$mainPrincipal])) {
+ return $this->principalMembershipCache[$mainPrincipal];
+ }
+
+ $check = array($mainPrincipal);
+ $principals = array();
+
+ while(count($check)) {
+
+ $principal = array_shift($check);
+
+ $node = $this->server->tree->getNodeForPath($principal);
+ if ($node instanceof IPrincipal) {
+ foreach($node->getGroupMembership() as $groupMember) {
+
+ if (!in_array($groupMember, $principals)) {
+
+ $check[] = $groupMember;
+ $principals[] = $groupMember;
+
+ }
+
+ }
+
+ }
+
+ }
+
+ // Store the result in the cache
+ $this->principalMembershipCache[$mainPrincipal] = $principals;
+
+ return $principals;
+
+ }
+
+ /**
+ * Returns the supported privilege structure for this ACL plugin.
+ *
+ * See RFC3744 for more details. Currently we default on a simple,
+ * standard structure.
+ *
+ * You can either get the list of privileges by a uri (path) or by
+ * specifying a Node.
+ *
+ * @param string|DAV\INode $node
+ * @return array
+ */
+ public function getSupportedPrivilegeSet($node) {
+
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+
+ if ($node instanceof IACL) {
+ $result = $node->getSupportedPrivilegeSet();
+
+ if ($result)
+ return $result;
+ }
+
+ return self::getDefaultSupportedPrivilegeSet();
+
+ }
+
+ /**
+ * Returns a fairly standard set of privileges, which may be useful for
+ * other systems to use as a basis.
+ *
+ * @return array
+ */
+ static function getDefaultSupportedPrivilegeSet() {
+
+ return array(
+ 'privilege' => '{DAV:}all',
+ 'abstract' => true,
+ 'aggregates' => array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'aggregates' => array(
+ array(
+ 'privilege' => '{DAV:}read-acl',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read-current-user-privilege-set',
+ 'abstract' => true,
+ ),
+ ),
+ ), // {DAV:}read
+ array(
+ 'privilege' => '{DAV:}write',
+ 'aggregates' => array(
+ array(
+ 'privilege' => '{DAV:}write-acl',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write-properties',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write-content',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}bind',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}unbind',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}unlock',
+ 'abstract' => true,
+ ),
+ ),
+ ), // {DAV:}write
+ ),
+ ); // {DAV:}all
+
+ }
+
+ /**
+ * Returns the supported privilege set as a flat list
+ *
+ * This is much easier to parse.
+ *
+ * The returned list will be index by privilege name.
+ * The value is a struct containing the following properties:
+ * - aggregates
+ * - abstract
+ * - concrete
+ *
+ * @param string|DAV\INode $node
+ * @return array
+ */
+ final public function getFlatPrivilegeSet($node) {
+
+ $privs = $this->getSupportedPrivilegeSet($node);
+
+ $flat = array();
+ $this->getFPSTraverse($privs, null, $flat);
+
+ return $flat;
+
+ }
+
+ /**
+ * Traverses the privilege set tree for reordering
+ *
+ * This function is solely used by getFlatPrivilegeSet, and would have been
+ * a closure if it wasn't for the fact I need to support PHP 5.2.
+ *
+ * @param array $priv
+ * @param $concrete
+ * @param array $flat
+ * @return void
+ */
+ final private function getFPSTraverse($priv, $concrete, &$flat) {
+
+ $myPriv = array(
+ 'privilege' => $priv['privilege'],
+ 'abstract' => isset($priv['abstract']) && $priv['abstract'],
+ 'aggregates' => array(),
+ 'concrete' => isset($priv['abstract']) && $priv['abstract']?$concrete:$priv['privilege'],
+ );
+
+ if (isset($priv['aggregates']))
+ foreach($priv['aggregates'] as $subPriv) $myPriv['aggregates'][] = $subPriv['privilege'];
+
+ $flat[$priv['privilege']] = $myPriv;
+
+ if (isset($priv['aggregates'])) {
+
+ foreach($priv['aggregates'] as $subPriv) {
+
+ $this->getFPSTraverse($subPriv, $myPriv['concrete'], $flat);
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Returns the full ACL list.
+ *
+ * Either a uri or a DAV\INode may be passed.
+ *
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|DAV\INode $node
+ * @return array
+ */
+ public function getACL($node) {
+
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+ if (!$node instanceof IACL) {
+ return null;
+ }
+ $acl = $node->getACL();
+ foreach($this->adminPrincipals as $adminPrincipal) {
+ $acl[] = array(
+ 'principal' => $adminPrincipal,
+ 'privilege' => '{DAV:}all',
+ 'protected' => true,
+ );
+ }
+ return $acl;
+
+ }
+
+ /**
+ * Returns a list of privileges the current user has
+ * on a particular node.
+ *
+ * Either a uri or a DAV\INode may be passed.
+ *
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|DAV\INode $node
+ * @return array
+ */
+ public function getCurrentUserPrivilegeSet($node) {
+
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+
+ $acl = $this->getACL($node);
+
+ if (is_null($acl)) return null;
+
+ $principals = $this->getCurrentUserPrincipals();
+
+ $collected = array();
+
+ foreach($acl as $ace) {
+
+ $principal = $ace['principal'];
+
+ switch($principal) {
+
+ case '{DAV:}owner' :
+ $owner = $node->getOwner();
+ if ($owner && in_array($owner, $principals)) {
+ $collected[] = $ace;
+ }
+ break;
+
+
+ // 'all' matches for every user
+ case '{DAV:}all' :
+
+ // 'authenticated' matched for every user that's logged in.
+ // Since it's not possible to use ACL while not being logged
+ // in, this is also always true.
+ case '{DAV:}authenticated' :
+ $collected[] = $ace;
+ break;
+
+ // 'unauthenticated' can never occur either, so we simply
+ // ignore these.
+ case '{DAV:}unauthenticated' :
+ break;
+
+ default :
+ if (in_array($ace['principal'], $principals)) {
+ $collected[] = $ace;
+ }
+ break;
+
+ }
+
+
+ }
+
+ // Now we deduct all aggregated privileges.
+ $flat = $this->getFlatPrivilegeSet($node);
+
+ $collected2 = array();
+ while(count($collected)) {
+
+ $current = array_pop($collected);
+ $collected2[] = $current['privilege'];
+
+ foreach($flat[$current['privilege']]['aggregates'] as $subPriv) {
+ $collected2[] = $subPriv;
+ $collected[] = $flat[$subPriv];
+ }
+
+ }
+
+ return array_values(array_unique($collected2));
+
+ }
+
+ /**
+ * Principal property search
+ *
+ * This method can search for principals matching certain values in
+ * properties.
+ *
+ * This method will return a list of properties for the matched properties.
+ *
+ * @param array $searchProperties The properties to search on. This is a
+ * key-value list. The keys are property
+ * names, and the values the strings to
+ * match them on.
+ * @param array $requestedProperties This is the list of properties to
+ * return for every match.
+ * @param string $collectionUri The principal collection to search on.
+ * If this is ommitted, the standard
+ * principal collection-set will be used.
+ * @return array This method returns an array structure similar to
+ * OldSabre\DAV\Server::getPropertiesForPath. Returned
+ * properties are index by a HTTP status code.
+ *
+ */
+ public function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null) {
+
+ if (!is_null($collectionUri)) {
+ $uris = array($collectionUri);
+ } else {
+ $uris = $this->principalCollectionSet;
+ }
+
+ $lookupResults = array();
+ foreach($uris as $uri) {
+
+ $principalCollection = $this->server->tree->getNodeForPath($uri);
+ if (!$principalCollection instanceof IPrincipalCollection) {
+ // Not a principal collection, we're simply going to ignore
+ // this.
+ continue;
+ }
+
+ $results = $principalCollection->searchPrincipals($searchProperties);
+ foreach($results as $result) {
+ $lookupResults[] = rtrim($uri,'/') . '/' . $result;
+ }
+
+ }
+
+ $matches = array();
+
+ foreach($lookupResults as $lookupResult) {
+
+ list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0);
+
+ }
+
+ return $matches;
+
+ }
+
+ /**
+ * Sets up the plugin
+ *
+ * This method is automatically called by the server class.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
+
+ $server->subscribeEvent('beforeMethod', array($this,'beforeMethod'),20);
+ $server->subscribeEvent('beforeBind', array($this,'beforeBind'),20);
+ $server->subscribeEvent('beforeUnbind', array($this,'beforeUnbind'),20);
+ $server->subscribeEvent('updateProperties',array($this,'updateProperties'));
+ $server->subscribeEvent('beforeUnlock', array($this,'beforeUnlock'),20);
+ $server->subscribeEvent('report',array($this,'report'));
+ $server->subscribeEvent('unknownMethod', array($this, 'unknownMethod'));
+
+ array_push($server->protectedProperties,
+ '{DAV:}alternate-URI-set',
+ '{DAV:}principal-URL',
+ '{DAV:}group-membership',
+ '{DAV:}principal-collection-set',
+ '{DAV:}current-user-principal',
+ '{DAV:}supported-privilege-set',
+ '{DAV:}current-user-privilege-set',
+ '{DAV:}acl',
+ '{DAV:}acl-restrictions',
+ '{DAV:}inherited-acl-set',
+ '{DAV:}owner',
+ '{DAV:}group'
+ );
+
+ // Automatically mapping nodes implementing IPrincipal to the
+ // {DAV:}principal resourcetype.
+ $server->resourceTypeMapping['OldSabre\\DAVACL\\IPrincipal'] = '{DAV:}principal';
+
+ // Mapping the group-member-set property to the HrefList property
+ // class.
+ $server->propertyMap['{DAV:}group-member-set'] = 'OldSabre\\DAV\\Property\\HrefList';
+
+ }
+
+
+ /* {{{ Event handlers */
+
+ /**
+ * Triggered before any method is handled
+ *
+ * @param string $method
+ * @param string $uri
+ * @return void
+ */
+ public function beforeMethod($method, $uri) {
+
+ $exists = $this->server->tree->nodeExists($uri);
+
+ // If the node doesn't exists, none of these checks apply
+ if (!$exists) return;
+
+ switch($method) {
+
+ case 'GET' :
+ case 'HEAD' :
+ case 'OPTIONS' :
+ // For these 3 we only need to know if the node is readable.
+ $this->checkPrivileges($uri,'{DAV:}read');
+ break;
+
+ case 'PUT' :
+ case 'LOCK' :
+ case 'UNLOCK' :
+ // This method requires the write-content priv if the node
+ // already exists, and bind on the parent if the node is being
+ // created.
+ // The bind privilege is handled in the beforeBind event.
+ $this->checkPrivileges($uri,'{DAV:}write-content');
+ break;
+
+
+ case 'PROPPATCH' :
+ $this->checkPrivileges($uri,'{DAV:}write-properties');
+ break;
+
+ case 'ACL' :
+ $this->checkPrivileges($uri,'{DAV:}write-acl');
+ break;
+
+ case 'COPY' :
+ case 'MOVE' :
+ // Copy requires read privileges on the entire source tree.
+ // If the target exists write-content normally needs to be
+ // checked, however, we're deleting the node beforehand and
+ // creating a new one after, so this is handled by the
+ // beforeUnbind event.
+ //
+ // The creation of the new node is handled by the beforeBind
+ // event.
+ //
+ // If MOVE is used beforeUnbind will also be used to check if
+ // the sourcenode can be deleted.
+ $this->checkPrivileges($uri,'{DAV:}read',self::R_RECURSIVE);
+
+ break;
+
+ }
+
+ }
+
+ /**
+ * Triggered before a new node is created.
+ *
+ * This allows us to check permissions for any operation that creates a
+ * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function beforeBind($uri) {
+
+ list($parentUri,$nodeName) = DAV\URLUtil::splitPath($uri);
+ $this->checkPrivileges($parentUri,'{DAV:}bind');
+
+ }
+
+ /**
+ * Triggered before a node is deleted
+ *
+ * This allows us to check permissions for any operation that will delete
+ * an existing node.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function beforeUnbind($uri) {
+
+ list($parentUri,$nodeName) = DAV\URLUtil::splitPath($uri);
+ $this->checkPrivileges($parentUri,'{DAV:}unbind',self::R_RECURSIVEPARENTS);
+
+ }
+
+ /**
+ * Triggered before a node is unlocked.
+ *
+ * @param string $uri
+ * @param DAV\Locks\LockInfo $lock
+ * @TODO: not yet implemented
+ * @return void
+ */
+ public function beforeUnlock($uri, DAV\Locks\LockInfo $lock) {
+
+
+ }
+
+ /**
+ * Triggered before properties are looked up in specific nodes.
+ *
+ * @param string $uri
+ * @param DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @TODO really should be broken into multiple methods, or even a class.
+ * @return bool
+ */
+ public function beforeGetProperties($uri, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
+
+ // Checking the read permission
+ if (!$this->checkPrivileges($uri,'{DAV:}read',self::R_PARENT,false)) {
+
+ // User is not allowed to read properties
+ if ($this->hideNodesFromListings) {
+ return false;
+ }
+
+ // Marking all requested properties as '403'.
+ foreach($requestedProperties as $key=>$requestedProperty) {
+ unset($requestedProperties[$key]);
+ $returnedProperties[403][$requestedProperty] = null;
+ }
+ return;
+
+ }
+
+ /* Adding principal properties */
+ if ($node instanceof IPrincipal) {
+
+ if (false !== ($index = array_search('{DAV:}alternate-URI-set', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}alternate-URI-set'] = new DAV\Property\HrefList($node->getAlternateUriSet());
+
+ }
+ if (false !== ($index = array_search('{DAV:}principal-URL', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}principal-URL'] = new DAV\Property\Href($node->getPrincipalUrl() . '/');
+
+ }
+ if (false !== ($index = array_search('{DAV:}group-member-set', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}group-member-set'] = new DAV\Property\HrefList($node->getGroupMemberSet());
+
+ }
+ if (false !== ($index = array_search('{DAV:}group-membership', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}group-membership'] = new DAV\Property\HrefList($node->getGroupMembership());
+
+ }
+
+ if (false !== ($index = array_search('{DAV:}displayname', $requestedProperties))) {
+
+ $returnedProperties[200]['{DAV:}displayname'] = $node->getDisplayName();
+
+ }
+
+ }
+ if (false !== ($index = array_search('{DAV:}principal-collection-set', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $val = $this->principalCollectionSet;
+ // Ensuring all collections end with a slash
+ foreach($val as $k=>$v) $val[$k] = $v . '/';
+ $returnedProperties[200]['{DAV:}principal-collection-set'] = new DAV\Property\HrefList($val);
+
+ }
+ if (false !== ($index = array_search('{DAV:}current-user-principal', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ if ($url = $this->getCurrentUserPrincipal()) {
+ $returnedProperties[200]['{DAV:}current-user-principal'] = new Property\Principal(Property\Principal::HREF, $url . '/');
+ } else {
+ $returnedProperties[200]['{DAV:}current-user-principal'] = new Property\Principal(Property\Principal::UNAUTHENTICATED);
+ }
+
+ }
+ if (false !== ($index = array_search('{DAV:}supported-privilege-set', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}supported-privilege-set'] = new Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node));
+
+ }
+ if (false !== ($index = array_search('{DAV:}current-user-privilege-set', $requestedProperties))) {
+
+ if (!$this->checkPrivileges($uri, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) {
+ $returnedProperties[403]['{DAV:}current-user-privilege-set'] = null;
+ unset($requestedProperties[$index]);
+ } else {
+ $val = $this->getCurrentUserPrivilegeSet($node);
+ if (!is_null($val)) {
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}current-user-privilege-set'] = new Property\CurrentUserPrivilegeSet($val);
+ }
+ }
+
+ }
+
+ /* The ACL property contains all the permissions */
+ if (false !== ($index = array_search('{DAV:}acl', $requestedProperties))) {
+
+ if (!$this->checkPrivileges($uri, '{DAV:}read-acl', self::R_PARENT, false)) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[403]['{DAV:}acl'] = null;
+
+ } else {
+
+ $acl = $this->getACL($node);
+ if (!is_null($acl)) {
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}acl'] = new Property\Acl($this->getACL($node));
+ }
+
+ }
+
+ }
+
+ /* The acl-restrictions property contains information on how privileges
+ * must behave.
+ */
+ if (false !== ($index = array_search('{DAV:}acl-restrictions', $requestedProperties))) {
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}acl-restrictions'] = new Property\AclRestrictions();
+ }
+
+ /* Adding ACL properties */
+ if ($node instanceof IACL) {
+
+ if (false !== ($index = array_search('{DAV:}owner', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}owner'] = new DAV\Property\Href($node->getOwner() . '/');
+
+ }
+
+ }
+
+ }
+
+ /**
+ * This method intercepts PROPPATCH methods and make sure the
+ * group-member-set is updated correctly.
+ *
+ * @param array $propertyDelta
+ * @param array $result
+ * @param DAV\INode $node
+ * @return bool
+ */
+ public function updateProperties(&$propertyDelta, &$result, DAV\INode $node) {
+
+ if (!array_key_exists('{DAV:}group-member-set', $propertyDelta))
+ return;
+
+ if (is_null($propertyDelta['{DAV:}group-member-set'])) {
+ $memberSet = array();
+ } elseif ($propertyDelta['{DAV:}group-member-set'] instanceof DAV\Property\HrefList) {
+ $memberSet = array_map(
+ array($this->server,'calculateUri'),
+ $propertyDelta['{DAV:}group-member-set']->getHrefs()
+ );
+ } else {
+ throw new DAV\Exception('The group-member-set property MUST be an instance of OldSabre\DAV\Property\HrefList or null');
+ }
+
+ if (!($node instanceof IPrincipal)) {
+ $result[403]['{DAV:}group-member-set'] = null;
+ unset($propertyDelta['{DAV:}group-member-set']);
+
+ // Returning false will stop the updateProperties process
+ return false;
+ }
+
+ $node->setGroupMemberSet($memberSet);
+ // We must also clear our cache, just in case
+
+ $this->principalMembershipCache = array();
+
+ $result[200]['{DAV:}group-member-set'] = null;
+ unset($propertyDelta['{DAV:}group-member-set']);
+
+ }
+
+ /**
+ * This method handles HTTP REPORT requests
+ *
+ * @param string $reportName
+ * @param \DOMNode $dom
+ * @return bool
+ */
+ public function report($reportName, $dom) {
+
+ switch($reportName) {
+
+ case '{DAV:}principal-property-search' :
+ $this->principalPropertySearchReport($dom);
+ return false;
+ case '{DAV:}principal-search-property-set' :
+ $this->principalSearchPropertySetReport($dom);
+ return false;
+ case '{DAV:}expand-property' :
+ $this->expandPropertyReport($dom);
+ return false;
+
+ }
+
+ }
+
+ /**
+ * This event is triggered for any HTTP method that is not known by the
+ * webserver.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function unknownMethod($method, $uri) {
+
+ if ($method!=='ACL') return;
+
+ $this->httpACL($uri);
+ return false;
+
+ }
+
+ /**
+ * This method is responsible for handling the 'ACL' event.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function httpACL($uri) {
+
+ $body = $this->server->httpRequest->getBody(true);
+ $dom = DAV\XMLUtil::loadDOMDocument($body);
+
+ $newAcl =
+ Property\Acl::unserialize($dom->firstChild)
+ ->getPrivileges();
+
+ // Normalizing urls
+ foreach($newAcl as $k=>$newAce) {
+ $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']);
+ }
+
+ $node = $this->server->tree->getNodeForPath($uri);
+
+ if (!($node instanceof IACL)) {
+ throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method');
+ }
+
+ $oldAcl = $this->getACL($node);
+
+ $supportedPrivileges = $this->getFlatPrivilegeSet($node);
+
+ /* Checking if protected principals from the existing principal set are
+ not overwritten. */
+ foreach($oldAcl as $oldAce) {
+
+ if (!isset($oldAce['protected']) || !$oldAce['protected']) continue;
+
+ $found = false;
+ foreach($newAcl as $newAce) {
+ if (
+ $newAce['privilege'] === $oldAce['privilege'] &&
+ $newAce['principal'] === $oldAce['principal'] &&
+ $newAce['protected']
+ )
+ $found = true;
+ }
+
+ if (!$found)
+ throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request');
+
+ }
+
+ foreach($newAcl as $newAce) {
+
+ // Do we recognize the privilege
+ if (!isset($supportedPrivileges[$newAce['privilege']])) {
+ throw new Exception\NotSupportedPrivilege('The privilege you specified (' . $newAce['privilege'] . ') is not recognized by this server');
+ }
+
+ if ($supportedPrivileges[$newAce['privilege']]['abstract']) {
+ throw new Exception\NoAbstract('The privilege you specified (' . $newAce['privilege'] . ') is an abstract privilege');
+ }
+
+ // Looking up the principal
+ try {
+ $principal = $this->server->tree->getNodeForPath($newAce['principal']);
+ } catch (DAV\Exception\NotFound $e) {
+ throw new Exception\NotRecognizedPrincipal('The specified principal (' . $newAce['principal'] . ') does not exist');
+ }
+ if (!($principal instanceof IPrincipal)) {
+ throw new Exception\NotRecognizedPrincipal('The specified uri (' . $newAce['principal'] . ') is not a principal');
+ }
+
+ }
+ $node->setACL($newAcl);
+
+ }
+
+ /* }}} */
+
+ /* Reports {{{ */
+
+ /**
+ * The expand-property report is defined in RFC3253 section 3-8.
+ *
+ * This report is very similar to a standard PROPFIND. The difference is
+ * that it has the additional ability to look at properties containing a
+ * {DAV:}href element, follow that property and grab additional elements
+ * there.
+ *
+ * Other rfc's, such as ACL rely on this report, so it made sense to put
+ * it in this plugin.
+ *
+ * @param \DOMElement $dom
+ * @return void
+ */
+ protected function expandPropertyReport($dom) {
+
+ $requestedProperties = $this->parseExpandPropertyReportRequest($dom->firstChild->firstChild);
+ $depth = $this->server->getHTTPDepth(0);
+ $requestUri = $this->server->getRequestUri();
+
+ $result = $this->expandProperties($requestUri,$requestedProperties,$depth);
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+ $multiStatus = $dom->createElement('d:multistatus');
+ $dom->appendChild($multiStatus);
+
+ // Adding in default namespaces
+ foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+ $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
+
+ }
+
+ foreach($result as $response) {
+ $response->serialize($this->server, $multiStatus);
+ }
+
+ $xml = $dom->saveXML();
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->sendStatus(207);
+ $this->server->httpResponse->sendBody($xml);
+
+ }
+
+ /**
+ * This method is used by expandPropertyReport to parse
+ * out the entire HTTP request.
+ *
+ * @param \DOMElement $node
+ * @return array
+ */
+ protected function parseExpandPropertyReportRequest($node) {
+
+ $requestedProperties = array();
+ do {
+
+ if (DAV\XMLUtil::toClarkNotation($node)!=='{DAV:}property') continue;
+
+ if ($node->firstChild) {
+
+ $children = $this->parseExpandPropertyReportRequest($node->firstChild);
+
+ } else {
+
+ $children = array();
+
+ }
+
+ $namespace = $node->getAttribute('namespace');
+ if (!$namespace) $namespace = 'DAV:';
+
+ $propName = '{'.$namespace.'}' . $node->getAttribute('name');
+ $requestedProperties[$propName] = $children;
+
+ } while ($node = $node->nextSibling);
+
+ return $requestedProperties;
+
+ }
+
+ /**
+ * This method expands all the properties and returns
+ * a list with property values
+ *
+ * @param array $path
+ * @param array $requestedProperties the list of required properties
+ * @param int $depth
+ * @return array
+ */
+ protected function expandProperties($path, array $requestedProperties, $depth) {
+
+ $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth);
+
+ $result = array();
+
+ foreach($foundProperties as $node) {
+
+ foreach($requestedProperties as $propertyName=>$childRequestedProperties) {
+
+ // We're only traversing if sub-properties were requested
+ if(count($childRequestedProperties)===0) continue;
+
+ // We only have to do the expansion if the property was found
+ // and it contains an href element.
+ if (!array_key_exists($propertyName,$node[200])) continue;
+
+ if ($node[200][$propertyName] instanceof DAV\Property\IHref) {
+ $hrefs = array($node[200][$propertyName]->getHref());
+ } elseif ($node[200][$propertyName] instanceof DAV\Property\HrefList) {
+ $hrefs = $node[200][$propertyName]->getHrefs();
+ }
+
+ $childProps = array();
+ foreach($hrefs as $href) {
+ $childProps = array_merge($childProps, $this->expandProperties($href, $childRequestedProperties, 0));
+ }
+ $node[200][$propertyName] = new DAV\Property\ResponseList($childProps);
+
+ }
+ $result[] = new DAV\Property\Response($node['href'], $node);
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * principalSearchPropertySetReport
+ *
+ * This method responsible for handing the
+ * {DAV:}principal-search-property-set report. This report returns a list
+ * of properties the client may search on, using the
+ * {DAV:}principal-property-search report.
+ *
+ * @param \DOMDocument $dom
+ * @return void
+ */
+ protected function principalSearchPropertySetReport(\DOMDocument $dom) {
+
+ $httpDepth = $this->server->getHTTPDepth(0);
+ if ($httpDepth!==0) {
+ throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0');
+ }
+
+ if ($dom->firstChild->hasChildNodes())
+ throw new DAV\Exception\BadRequest('The principal-search-property-set report element is not allowed to have child elements');
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+ $root = $dom->createElement('d:principal-search-property-set');
+ $dom->appendChild($root);
+ // Adding in default namespaces
+ foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+ $root->setAttribute('xmlns:' . $prefix,$namespace);
+
+ }
+
+ $nsList = $this->server->xmlNamespaces;
+
+ foreach($this->principalSearchPropertySet as $propertyName=>$description) {
+
+ $psp = $dom->createElement('d:principal-search-property');
+ $root->appendChild($psp);
+
+ $prop = $dom->createElement('d:prop');
+ $psp->appendChild($prop);
+
+ $propName = null;
+ preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName);
+
+ $currentProperty = $dom->createElement($nsList[$propName[1]] . ':' . $propName[2]);
+ $prop->appendChild($currentProperty);
+
+ $descriptionElem = $dom->createElement('d:description');
+ $descriptionElem->setAttribute('xml:lang','en');
+ $descriptionElem->appendChild($dom->createTextNode($description));
+ $psp->appendChild($descriptionElem);
+
+
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->sendBody($dom->saveXML());
+
+ }
+
+ /**
+ * principalPropertySearchReport
+ *
+ * This method is responsible for handing the
+ * {DAV:}principal-property-search report. This report can be used for
+ * clients to search for groups of principals, based on the value of one
+ * or more properties.
+ *
+ * @param \DOMDocument $dom
+ * @return void
+ */
+ protected function principalPropertySearchReport(\DOMDocument $dom) {
+
+ list($searchProperties, $requestedProperties, $applyToPrincipalCollectionSet) = $this->parsePrincipalPropertySearchReportRequest($dom);
+
+ $uri = null;
+ if (!$applyToPrincipalCollectionSet) {
+ $uri = $this->server->getRequestUri();
+ }
+ $result = $this->principalSearch($searchProperties, $requestedProperties, $uri);
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $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']));
+
+ }
+
+ /**
+ * parsePrincipalPropertySearchReportRequest
+ *
+ * This method parses the request body from a
+ * {DAV:}principal-property-search report.
+ *
+ * This method returns an array with two elements:
+ * 1. an array with properties to search on, and their values
+ * 2. a list of propertyvalues that should be returned for the request.
+ *
+ * @param \DOMDocument $dom
+ * @return array
+ */
+ protected function parsePrincipalPropertySearchReportRequest($dom) {
+
+ $httpDepth = $this->server->getHTTPDepth(0);
+ if ($httpDepth!==0) {
+ throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0');
+ }
+
+ $searchProperties = array();
+
+ $applyToPrincipalCollectionSet = false;
+
+ // Parsing the search request
+ foreach($dom->firstChild->childNodes as $searchNode) {
+
+ if (DAV\XMLUtil::toClarkNotation($searchNode) == '{DAV:}apply-to-principal-collection-set') {
+ $applyToPrincipalCollectionSet = true;
+ }
+
+ if (DAV\XMLUtil::toClarkNotation($searchNode)!=='{DAV:}property-search')
+ continue;
+
+ $propertyName = null;
+ $propertyValue = null;
+
+ foreach($searchNode->childNodes as $childNode) {
+
+ switch(DAV\XMLUtil::toClarkNotation($childNode)) {
+
+ case '{DAV:}prop' :
+ $property = DAV\XMLUtil::parseProperties($searchNode);
+ reset($property);
+ $propertyName = key($property);
+ break;
+
+ case '{DAV:}match' :
+ $propertyValue = $childNode->textContent;
+ break;
+
+ }
+
+
+ }
+
+ if (is_null($propertyName) || is_null($propertyValue))
+ throw new DAV\Exception\BadRequest('Invalid search request. propertyname: ' . $propertyName . '. propertvvalue: ' . $propertyValue);
+
+ $searchProperties[$propertyName] = $propertyValue;
+
+ }
+
+ return array($searchProperties, array_keys(DAV\XMLUtil::parseProperties($dom->firstChild)), $applyToPrincipalCollectionSet);
+
+ }
+
+
+ /* }}} */
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php
new file mode 100644
index 0000000..a78733d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php
@@ -0,0 +1,281 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+use OldSabre\DAV;
+
+/**
+ * Principal class
+ *
+ * This class is a representation of a simple principal
+ *
+ * Many WebDAV specs require a user to show up in the directory
+ * structure.
+ *
+ * This principal also has basic ACL settings, only allowing the principal
+ * access it's own principal.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Principal extends DAV\Node implements IPrincipal, DAV\IProperties, IACL {
+
+ /**
+ * Struct with principal information.
+ *
+ * @var array
+ */
+ protected $principalProperties;
+
+ /**
+ * Principal backend
+ *
+ * @var PrincipalBackend\BackendInterface
+ */
+ protected $principalBackend;
+
+ /**
+ * Creates the principal object
+ *
+ * @param IPrincipalBackend $principalBackend
+ * @param array $principalProperties
+ */
+ public function __construct(PrincipalBackend\BackendInterface $principalBackend, array $principalProperties = array()) {
+
+ if (!isset($principalProperties['uri'])) {
+ throw new DAV\Exception('The principal properties must at least contain the \'uri\' key');
+ }
+ $this->principalBackend = $principalBackend;
+ $this->principalProperties = $principalProperties;
+
+ }
+
+ /**
+ * Returns the full principal url
+ *
+ * @return string
+ */
+ public function getPrincipalUrl() {
+
+ return $this->principalProperties['uri'];
+
+ }
+
+ /**
+ * Returns a list of alternative urls for a principal
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet() {
+
+ $uris = array();
+ if (isset($this->principalProperties['{DAV:}alternate-URI-set'])) {
+
+ $uris = $this->principalProperties['{DAV:}alternate-URI-set'];
+
+ }
+
+ if (isset($this->principalProperties['{http://sabredav.org/ns}email-address'])) {
+ $uris[] = 'mailto:' . $this->principalProperties['{http://sabredav.org/ns}email-address'];
+ }
+
+ return array_unique($uris);
+
+ }
+
+ /**
+ * Returns the list of group members
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet() {
+
+ return $this->principalBackend->getGroupMemberSet($this->principalProperties['uri']);
+
+ }
+
+ /**
+ * Returns the list of groups this principal is member of
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership() {
+
+ return $this->principalBackend->getGroupMemberShip($this->principalProperties['uri']);
+
+ }
+
+
+ /**
+ * Sets a list of group members
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ *
+ * @param array $groupMembers
+ * @return void
+ */
+ public function setGroupMemberSet(array $groupMembers) {
+
+ $this->principalBackend->setGroupMemberSet($this->principalProperties['uri'], $groupMembers);
+
+ }
+
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ $uri = $this->principalProperties['uri'];
+ list(, $name) = DAV\URLUtil::splitPath($uri);
+ return $name;
+
+ }
+
+ /**
+ * Returns the name of the user
+ *
+ * @return string
+ */
+ public function getDisplayName() {
+
+ if (isset($this->principalProperties['{DAV:}displayname'])) {
+ return $this->principalProperties['{DAV:}displayname'];
+ } else {
+ return $this->getName();
+ }
+
+ }
+
+ /**
+ * Returns a list of properties
+ *
+ * @param array $requestedProperties
+ * @return array
+ */
+ public function getProperties($requestedProperties) {
+
+ $newProperties = array();
+ foreach($requestedProperties as $propName) {
+
+ if (isset($this->principalProperties[$propName])) {
+ $newProperties[$propName] = $this->principalProperties[$propName];
+ }
+
+ }
+
+ return $newProperties;
+
+ }
+
+ /**
+ * Updates this principals properties.
+ *
+ * @param array $mutations
+ * @see OldSabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateProperties($mutations) {
+
+ return $this->principalBackend->updatePrincipal($this->principalProperties['uri'], $mutations);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalProperties['uri'];
+
+
+ }
+
+ /**
+ * 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(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getPrincipalUrl(),
+ 'protected' => true,
+ ),
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Updating ACLs is not allowed here');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\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/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php
new file mode 100644
index 0000000..1be74c7
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace OldSabre\DAVACL\PrincipalBackend;
+
+/**
+ * Abstract Principal Backend
+ *
+ * Currently this class has no function. It's here for consistency and so we
+ * have a non-bc-breaking way to add a default generic implementation to
+ * functions we may add in the future.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php
new file mode 100644
index 0000000..52a00b3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace OldSabre\DAVACL\PrincipalBackend;
+
+/**
+ * Implement this interface to create your own principal backends.
+ *
+ * Creating backends for principals is entirely optional. You can also
+ * implement OldSabre\DAVACL\IPrincipal directly. This interface is used solely by
+ * OldSabre\DAVACL\AbstractPrincipalCollection.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Returns a list of principals based on a prefix.
+ *
+ * This prefix will often contain something like 'principals'. You are only
+ * expected to return principals that are in this base path.
+ *
+ * You are expected to return at least a 'uri' for every user, you can
+ * return any additional properties if you wish so. Common properties are:
+ * {DAV:}displayname
+ * {http://sabredav.org/ns}email-address - This is a custom SabreDAV
+ * field that's actually injected in a number of other properties. If
+ * you have an email address, use this property.
+ *
+ * @param string $prefixPath
+ * @return array
+ */
+ function getPrincipalsByPrefix($prefixPath);
+
+ /**
+ * Returns a specific principal, specified by it's path.
+ * The returned structure should be the exact same as from
+ * getPrincipalsByPrefix.
+ *
+ * @param string $path
+ * @return array
+ */
+ function getPrincipalByPath($path);
+
+ /**
+ * Updates one ore more webdav properties on a principal.
+ *
+ * The list of mutations is supplied as an array. Each key in the array is
+ * a propertyname, such as {DAV:}displayname.
+ *
+ * Each value is the actual value to be updated. If a value is null, it
+ * must be deleted.
+ *
+ * This method should be atomic. It must either completely succeed, or
+ * completely fail. Success and failure can simply be returned as 'true' or
+ * 'false'.
+ *
+ * It is also possible to return detailed failure information. In that case
+ * an array such as this should be returned:
+ *
+ * array(
+ * 200 => array(
+ * '{DAV:}prop1' => null,
+ * ),
+ * 201 => array(
+ * '{DAV:}prop2' => null,
+ * ),
+ * 403 => array(
+ * '{DAV:}prop3' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}prop4' => null,
+ * ),
+ * );
+ *
+ * In this previous example prop1 was successfully updated or deleted, and
+ * prop2 was succesfully created.
+ *
+ * prop3 failed to update due to '403 Forbidden' and because of this prop4
+ * also could not be updated with '424 Failed dependency'.
+ *
+ * This last example was actually incorrect. While 200 and 201 could appear
+ * in 1 response, if there's any error (403) the other properties should
+ * always fail with 423 (failed dependency).
+ *
+ * But anyway, if you don't want to scratch your head over this, just
+ * return true or false.
+ *
+ * @param string $path
+ * @param array $mutations
+ * @return array|bool
+ */
+ function updatePrincipal($path, $mutations);
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * If multiple properties are being searched on, the search should be
+ * AND'ed.
+ *
+ * This method should simply return an array with full principal uri's.
+ *
+ * If somebody attempted to search on a property the backend does not
+ * support, you should simply return 0 results.
+ *
+ * You can also just return 0 results if you choose to not support
+ * searching at all, but keep in mind that this may stop certain features
+ * from working.
+ *
+ * @param string $prefixPath
+ * @param array $searchProperties
+ * @return array
+ */
+ function searchPrincipals($prefixPath, array $searchProperties);
+
+ /**
+ * Returns the list of members for a group-principal
+ *
+ * @param string $principal
+ * @return array
+ */
+ function getGroupMemberSet($principal);
+
+ /**
+ * Returns the list of groups a principal is a member of
+ *
+ * @param string $principal
+ * @return array
+ */
+ function getGroupMembership($principal);
+
+ /**
+ * Updates the list of group members for a group principal.
+ *
+ * The principals should be passed as a list of uri's.
+ *
+ * @param string $principal
+ * @param array $members
+ * @return void
+ */
+ function setGroupMemberSet($principal, array $members);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php
new file mode 100644
index 0000000..92e8417
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php
@@ -0,0 +1,428 @@
+<?php
+
+namespace OldSabre\DAVACL\PrincipalBackend;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * PDO principal backend
+ *
+ *
+ * This backend assumes all principals are in a single collection. The default collection
+ * is 'principals/', but this can be overriden.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend {
+
+ /**
+ * pdo
+ *
+ * @var PDO
+ */
+ protected $pdo;
+
+ /**
+ * PDO table name for 'principals'
+ *
+ * @var string
+ */
+ protected $tableName;
+
+ /**
+ * PDO table name for 'group members'
+ *
+ * @var string
+ */
+ protected $groupMembersTableName;
+
+ /**
+ * A list of additional fields to support
+ *
+ * @var array
+ */
+ protected $fieldMap = array(
+
+ /**
+ * This property can be used to display the users' real name.
+ */
+ '{DAV:}displayname' => array(
+ 'dbField' => 'displayname',
+ ),
+
+ /**
+ * This property is actually used by the CardDAV plugin, where it gets
+ * mapped to {http://calendarserver.orgi/ns/}me-card.
+ *
+ * The reason we don't straight-up use that property, is because
+ * me-card is defined as a property on the users' addressbook
+ * collection.
+ */
+ '{http://sabredav.org/ns}vcard-url' => array(
+ 'dbField' => 'vcardurl',
+ ),
+ /**
+ * This is the users' primary email-address.
+ */
+ '{http://sabredav.org/ns}email-address' => array(
+ 'dbField' => 'email',
+ ),
+ );
+
+ /**
+ * Sets up the backend.
+ *
+ * @param PDO $pdo
+ * @param string $tableName
+ * @param string $groupMembersTableName
+ */
+ public function __construct(\PDO $pdo, $tableName = 'principals', $groupMembersTableName = 'groupmembers') {
+
+ $this->pdo = $pdo;
+ $this->tableName = $tableName;
+ $this->groupMembersTableName = $groupMembersTableName;
+
+ }
+
+
+ /**
+ * Returns a list of principals based on a prefix.
+ *
+ * This prefix will often contain something like 'principals'. You are only
+ * expected to return principals that are in this base path.
+ *
+ * You are expected to return at least a 'uri' for every user, you can
+ * return any additional properties if you wish so. Common properties are:
+ * {DAV:}displayname
+ * {http://sabredav.org/ns}email-address - This is a custom SabreDAV
+ * field that's actualy injected in a number of other properties. If
+ * you have an email address, use this property.
+ *
+ * @param string $prefixPath
+ * @return array
+ */
+ public function getPrincipalsByPrefix($prefixPath) {
+
+ $fields = array(
+ 'uri',
+ );
+
+ foreach($this->fieldMap as $key=>$value) {
+ $fields[] = $value['dbField'];
+ }
+ $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '. $this->tableName);
+
+ $principals = array();
+
+ while($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+
+ // Checking if the principal is in the prefix
+ list($rowPrefix) = DAV\URLUtil::splitPath($row['uri']);
+ if ($rowPrefix !== $prefixPath) continue;
+
+ $principal = array(
+ 'uri' => $row['uri'],
+ );
+ foreach($this->fieldMap as $key=>$value) {
+ if ($row[$value['dbField']]) {
+ $principal[$key] = $row[$value['dbField']];
+ }
+ }
+ $principals[] = $principal;
+
+ }
+
+ return $principals;
+
+ }
+
+ /**
+ * Returns a specific principal, specified by it's path.
+ * The returned structure should be the exact same as from
+ * getPrincipalsByPrefix.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getPrincipalByPath($path) {
+
+ $fields = array(
+ 'id',
+ 'uri',
+ );
+
+ foreach($this->fieldMap as $key=>$value) {
+ $fields[] = $value['dbField'];
+ }
+ $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '. $this->tableName . ' WHERE uri = ?');
+ $stmt->execute(array($path));
+
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+ if (!$row) return;
+
+ $principal = array(
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ );
+ foreach($this->fieldMap as $key=>$value) {
+ if ($row[$value['dbField']]) {
+ $principal[$key] = $row[$value['dbField']];
+ }
+ }
+ return $principal;
+
+ }
+
+ /**
+ * Updates one ore more webdav properties on a principal.
+ *
+ * The list of mutations is supplied as an array. Each key in the array is
+ * a propertyname, such as {DAV:}displayname.
+ *
+ * Each value is the actual value to be updated. If a value is null, it
+ * must be deleted.
+ *
+ * This method should be atomic. It must either completely succeed, or
+ * completely fail. Success and failure can simply be returned as 'true' or
+ * 'false'.
+ *
+ * It is also possible to return detailed failure information. In that case
+ * an array such as this should be returned:
+ *
+ * array(
+ * 200 => array(
+ * '{DAV:}prop1' => null,
+ * ),
+ * 201 => array(
+ * '{DAV:}prop2' => null,
+ * ),
+ * 403 => array(
+ * '{DAV:}prop3' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}prop4' => null,
+ * ),
+ * );
+ *
+ * In this previous example prop1 was successfully updated or deleted, and
+ * prop2 was succesfully created.
+ *
+ * prop3 failed to update due to '403 Forbidden' and because of this prop4
+ * also could not be updated with '424 Failed dependency'.
+ *
+ * This last example was actually incorrect. While 200 and 201 could appear
+ * in 1 response, if there's any error (403) the other properties should
+ * always fail with 423 (failed dependency).
+ *
+ * But anyway, if you don't want to scratch your head over this, just
+ * return true or false.
+ *
+ * @param string $path
+ * @param array $mutations
+ * @return array|bool
+ */
+ public function updatePrincipal($path, $mutations) {
+
+ $updateAble = array();
+ foreach($mutations as $key=>$value) {
+
+ // We are not aware of this field, we must fail.
+ if (!isset($this->fieldMap[$key])) {
+
+ $response = array(
+ 403 => array(
+ $key => null,
+ ),
+ 424 => array(),
+ );
+
+ // Adding the rest to the response as a 424
+ foreach($mutations as $subKey=>$subValue) {
+ if ($subKey !== $key) {
+ $response[424][$subKey] = null;
+ }
+ }
+ return $response;
+ }
+
+ $updateAble[$this->fieldMap[$key]['dbField']] = $value;
+
+ }
+
+ // No fields to update
+ $query = "UPDATE " . $this->tableName . " SET ";
+
+ $first = true;
+ foreach($updateAble as $key => $value) {
+ if (!$first) {
+ $query.= ', ';
+ }
+ $first = false;
+ $query.= "$key = :$key ";
+ }
+ $query.='WHERE uri = :uri';
+ $stmt = $this->pdo->prepare($query);
+ $updateAble['uri'] = $path;
+ $stmt->execute($updateAble);
+
+ return true;
+
+ }
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * If multiple properties are being searched on, the search should be
+ * AND'ed.
+ *
+ * This method should simply return an array with full principal uri's.
+ *
+ * If somebody attempted to search on a property the backend does not
+ * support, you should simply return 0 results.
+ *
+ * You can also just return 0 results if you choose to not support
+ * searching at all, but keep in mind that this may stop certain features
+ * from working.
+ *
+ * @param string $prefixPath
+ * @param array $searchProperties
+ * @return array
+ */
+ public function searchPrincipals($prefixPath, array $searchProperties) {
+
+ $query = 'SELECT uri FROM ' . $this->tableName . ' WHERE 1=1 ';
+ $values = array();
+ foreach($searchProperties as $property => $value) {
+
+ switch($property) {
+
+ case '{DAV:}displayname' :
+ $query.=' AND displayname LIKE ?';
+ $values[] = '%' . $value . '%';
+ break;
+ case '{http://sabredav.org/ns}email-address' :
+ $query.=' AND email LIKE ?';
+ $values[] = '%' . $value . '%';
+ break;
+ default :
+ // Unsupported property
+ return array();
+
+ }
+
+ }
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ $principals = array();
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ // Checking if the principal is in the prefix
+ list($rowPrefix) = DAV\URLUtil::splitPath($row['uri']);
+ if ($rowPrefix !== $prefixPath) continue;
+
+ $principals[] = $row['uri'];
+
+ }
+
+ return $principals;
+
+ }
+
+ /**
+ * Returns the list of members for a group-principal
+ *
+ * @param string $principal
+ * @return array
+ */
+ public function getGroupMemberSet($principal) {
+
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) throw new DAV\Exception('Principal not found');
+
+ $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?');
+ $stmt->execute(array($principal['id']));
+
+ $result = array();
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row['uri'];
+ }
+ return $result;
+
+ }
+
+ /**
+ * Returns the list of groups a principal is a member of
+ *
+ * @param string $principal
+ * @return array
+ */
+ public function getGroupMembership($principal) {
+
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) throw new DAV\Exception('Principal not found');
+
+ $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?');
+ $stmt->execute(array($principal['id']));
+
+ $result = array();
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row['uri'];
+ }
+ return $result;
+
+ }
+
+ /**
+ * Updates the list of group members for a group principal.
+ *
+ * The principals should be passed as a list of uri's.
+ *
+ * @param string $principal
+ * @param array $members
+ * @return void
+ */
+ public function setGroupMemberSet($principal, array $members) {
+
+ // Grabbing the list of principal id's.
+ $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? ' . str_repeat(', ? ', count($members)) . ');');
+ $stmt->execute(array_merge(array($principal), $members));
+
+ $memberIds = array();
+ $principalId = null;
+
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row['uri'] == $principal) {
+ $principalId = $row['id'];
+ } else {
+ $memberIds[] = $row['id'];
+ }
+ }
+ if (!$principalId) throw new DAV\Exception('Principal not found');
+
+ // Wiping out old members
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;');
+ $stmt->execute(array($principalId));
+
+ foreach($memberIds as $memberId) {
+
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);');
+ $stmt->execute(array($principalId, $memberId));
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php
new file mode 100644
index 0000000..7cee8dc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+/**
+ * Principals Collection
+ *
+ * This collection represents a list of users.
+ * The users are instances of OldSabre\DAVACL\Principal
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PrincipalCollection extends AbstractPrincipalCollection {
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principal
+ * @return \OldSabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal) {
+
+ return new Principal($this->principalBackend, $principal);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php
new file mode 100644
index 0000000..33f5c2e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+
+use OldSabre\DAV;
+
+/**
+ * This class represents the {DAV:}acl property
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Acl extends DAV\Property {
+
+ /**
+ * List of privileges
+ *
+ * @var array
+ */
+ private $privileges;
+
+ /**
+ * Whether or not the server base url is required to be prefixed when
+ * serializing the property.
+ *
+ * @var boolean
+ */
+ private $prefixBaseUrl;
+
+ /**
+ * Constructor
+ *
+ * This object requires a structure similar to the return value from
+ * OldSabre\DAVACL\Plugin::getACL().
+ *
+ * Each privilege is a an array with at least a 'privilege' property, and a
+ * 'principal' property. A privilege may have a 'protected' property as
+ * well.
+ *
+ * The prefixBaseUrl should be set to false, if the supplied principal urls
+ * are already full urls. If this is kept to true, the servers base url
+ * will automatically be prefixed.
+ *
+ * @param bool $prefixBaseUrl
+ * @param array $privileges
+ */
+ public function __construct(array $privileges, $prefixBaseUrl = true) {
+
+ $this->privileges = $privileges;
+ $this->prefixBaseUrl = $prefixBaseUrl;
+
+ }
+
+ /**
+ * Returns the list of privileges for this property
+ *
+ * @return array
+ */
+ public function getPrivileges() {
+
+ return $this->privileges;
+
+ }
+
+ /**
+ * Serializes the property into a DOMElement
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ foreach($this->privileges as $ace) {
+
+ $this->serializeAce($doc, $node, $ace, $server);
+
+ }
+
+ }
+
+ /**
+ * Unserializes the {DAV:}acl xml element.
+ *
+ * @param \DOMElement $dom
+ * @return Acl
+ */
+ static public function unserialize(\DOMElement $dom) {
+
+ $privileges = array();
+ $xaces = $dom->getElementsByTagNameNS('urn:DAV','ace');
+ for($ii=0; $ii < $xaces->length; $ii++) {
+
+ $xace = $xaces->item($ii);
+ $principal = $xace->getElementsByTagNameNS('urn:DAV','principal');
+ if ($principal->length !== 1) {
+ throw new DAV\Exception\BadRequest('Each {DAV:}ace element must have one {DAV:}principal element');
+ }
+ $principal = Principal::unserialize($principal->item(0));
+
+ switch($principal->getType()) {
+ case Principal::HREF :
+ $principal = $principal->getHref();
+ break;
+ case Principal::AUTHENTICATED :
+ $principal = '{DAV:}authenticated';
+ break;
+ case Principal::UNAUTHENTICATED :
+ $principal = '{DAV:}unauthenticated';
+ break;
+ case Principal::ALL :
+ $principal = '{DAV:}all';
+ break;
+
+ }
+
+ $protected = false;
+
+ if ($xace->getElementsByTagNameNS('urn:DAV','protected')->length > 0) {
+ $protected = true;
+ }
+
+ $grants = $xace->getElementsByTagNameNS('urn:DAV','grant');
+ if ($grants->length < 1) {
+ throw new 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');
+ for($jj=0; $jj<$xprivs->length; $jj++) {
+
+ $xpriv = $xprivs->item($jj);
+
+ $privilegeName = null;
+
+ for ($kk=0;$kk<$xpriv->childNodes->length;$kk++) {
+
+ $childNode = $xpriv->childNodes->item($kk);
+ if ($t = DAV\XMLUtil::toClarkNotation($childNode)) {
+ $privilegeName = $t;
+ break;
+ }
+ }
+ if (is_null($privilegeName)) {
+ throw new DAV\Exception\BadRequest('{DAV:}privilege elements must have a privilege element contained within them.');
+ }
+
+ $privileges[] = array(
+ 'principal' => $principal,
+ 'protected' => $protected,
+ 'privilege' => $privilegeName,
+ );
+
+ }
+
+ }
+
+ return new self($privileges);
+
+ }
+
+ /**
+ * Serializes a single access control entry.
+ *
+ * @param \DOMDocument $doc
+ * @param \DOMElement $node
+ * @param array $ace
+ * @param DAV\Server $server
+ * @return void
+ */
+ private function serializeAce($doc,$node,$ace, DAV\Server $server) {
+
+ $xace = $doc->createElementNS('DAV:','d:ace');
+ $node->appendChild($xace);
+
+ $principal = $doc->createElementNS('DAV:','d:principal');
+ $xace->appendChild($principal);
+ switch($ace['principal']) {
+ case '{DAV:}authenticated' :
+ $principal->appendChild($doc->createElementNS('DAV:','d:authenticated'));
+ break;
+ case '{DAV:}unauthenticated' :
+ $principal->appendChild($doc->createElementNS('DAV:','d:unauthenticated'));
+ break;
+ case '{DAV:}all' :
+ $principal->appendChild($doc->createElementNS('DAV:','d:all'));
+ break;
+ default:
+ $principal->appendChild($doc->createElementNS('DAV:','d:href',($this->prefixBaseUrl?$server->getBaseUri():'') . $ace['principal'] . '/'));
+ }
+
+ $grant = $doc->createElementNS('DAV:','d:grant');
+ $xace->appendChild($grant);
+
+ $privParts = null;
+
+ preg_match('/^{([^}]*)}(.*)$/',$ace['privilege'],$privParts);
+
+ $xprivilege = $doc->createElementNS('DAV:','d:privilege');
+ $grant->appendChild($xprivilege);
+
+ $xprivilege->appendChild($doc->createElementNS($privParts[1],'d:'.$privParts[2]));
+
+ if (isset($ace['protected']) && $ace['protected'])
+ $xace->appendChild($doc->createElement('d:protected'));
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php
new file mode 100644
index 0000000..f9485ef
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+
+use OldSabre\DAV;
+
+/**
+ * AclRestrictions property
+ *
+ * This property represents {DAV:}acl-restrictions, as defined in RFC3744.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AclRestrictions extends DAV\Property {
+
+ /**
+ * Serializes the property into a DOMElement
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $elem
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $elem) {
+
+ $doc = $elem->ownerDocument;
+
+ $elem->appendChild($doc->createElementNS('DAV:','d:grant-only'));
+ $elem->appendChild($doc->createElementNS('DAV:','d:no-invert'));
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php
new file mode 100644
index 0000000..e364121
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+
+use OldSabre\DAV;
+
+/**
+ * CurrentUserPrivilegeSet
+ *
+ * This class represents the current-user-privilege-set property. When
+ * requested, it contain all the privileges a user has on a specific node.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CurrentUserPrivilegeSet extends DAV\Property {
+
+ /**
+ * List of privileges
+ *
+ * @var array
+ */
+ private $privileges;
+
+ /**
+ * Creates the object
+ *
+ * Pass the privileges in clark-notation
+ *
+ * @param array $privileges
+ */
+ public function __construct(array $privileges) {
+
+ $this->privileges = $privileges;
+
+ }
+
+ /**
+ * Serializes the property in the DOM
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ foreach($this->privileges as $privName) {
+
+ $this->serializePriv($doc,$node,$privName);
+
+ }
+
+ }
+
+ /**
+ * Returns true or false, whether the specified principal appears in the
+ * list.
+ *
+ * @return bool
+ */
+ public function has($privilegeName) {
+
+ return in_array($privilegeName, $this->privileges);
+
+ }
+
+ /**
+ * Serializes one privilege
+ *
+ * @param \DOMDocument $doc
+ * @param \DOMElement $node
+ * @param string $privName
+ * @return void
+ */
+ protected function serializePriv($doc,$node,$privName) {
+
+ $xp = $doc->createElementNS('DAV:','d:privilege');
+ $node->appendChild($xp);
+
+ $privParts = null;
+ preg_match('/^{([^}]*)}(.*)$/',$privName,$privParts);
+
+ $xp->appendChild($doc->createElementNS($privParts[1],'d:'.$privParts[2]));
+
+ }
+
+ /**
+ * Unserializes the {DAV:}current-user-privilege-set element.
+ *
+ * @param DOMElement $node
+ * @return CurrentUserPrivilegeSet
+ */
+ static public function unserialize(\DOMElement $node) {
+
+ $result = array();
+
+ $xprivs = $node->getElementsByTagNameNS('urn:DAV','privilege');
+
+ for($jj=0; $jj<$xprivs->length; $jj++) {
+
+ $xpriv = $xprivs->item($jj);
+
+ $privilegeName = null;
+
+ for ($kk=0;$kk<$xpriv->childNodes->length;$kk++) {
+
+ $childNode = $xpriv->childNodes->item($kk);
+ if ($t = DAV\XMLUtil::toClarkNotation($childNode)) {
+ $privilegeName = $t;
+ break;
+ }
+ }
+
+ $result[] = $privilegeName;
+
+ }
+
+ return new self($result);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php
new file mode 100644
index 0000000..fbf7c1f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+use OldSabre\DAV;
+
+/**
+ * Principal property
+ *
+ * The principal property represents a principal from RFC3744 (ACL).
+ * The property can be used to specify a principal or pseudo principals.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Principal extends DAV\Property implements DAV\Property\IHref {
+
+ /**
+ * To specify a not-logged-in user, use the UNAUTHENTICATED principal
+ */
+ const UNAUTHENTICATED = 1;
+
+ /**
+ * To specify any principal that is logged in, use AUTHENTICATED
+ */
+ const AUTHENTICATED = 2;
+
+ /**
+ * Specific principals can be specified with the HREF
+ */
+ const HREF = 3;
+
+ /**
+ * Everybody, basically
+ */
+ const ALL = 4;
+
+ /**
+ * Principal-type
+ *
+ * Must be one of the UNAUTHENTICATED, AUTHENTICATED or HREF constants.
+ *
+ * @var int
+ */
+ private $type;
+
+ /**
+ * Url to principal
+ *
+ * This value is only used for the HREF principal type.
+ *
+ * @var string
+ */
+ private $href;
+
+ /**
+ * Creates the property.
+ *
+ * The 'type' argument must be one of the type constants defined in this class.
+ *
+ * 'href' is only required for the HREF type.
+ *
+ * @param int $type
+ * @param string|null $href
+ */
+ public function __construct($type, $href = null) {
+
+ $this->type = $type;
+
+ if ($type===self::HREF && is_null($href)) {
+ throw new DAV\Exception('The href argument must be specified for the HREF principal type.');
+ }
+ $this->href = $href;
+
+ }
+
+ /**
+ * Returns the principal type
+ *
+ * @return int
+ */
+ public function getType() {
+
+ return $this->type;
+
+ }
+
+ /**
+ * Returns the principal uri.
+ *
+ * @return string
+ */
+ public function getHref() {
+
+ return $this->href;
+
+ }
+
+ /**
+ * Serializes the property into a DOMElement.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $node) {
+
+ $prefix = $server->xmlNamespaces['DAV:'];
+ switch($this->type) {
+
+ case self::UNAUTHENTICATED :
+ $node->appendChild(
+ $node->ownerDocument->createElement($prefix . ':unauthenticated')
+ );
+ break;
+ case self::AUTHENTICATED :
+ $node->appendChild(
+ $node->ownerDocument->createElement($prefix . ':authenticated')
+ );
+ break;
+ case self::HREF :
+ $href = $node->ownerDocument->createElement($prefix . ':href');
+ $href->nodeValue = $server->getBaseUri() . DAV\URLUtil::encodePath($this->href);
+ $node->appendChild($href);
+ break;
+
+ }
+
+ }
+
+ /**
+ * Deserializes a DOM element into a property object.
+ *
+ * @param \DOMElement $dom
+ * @return Principal
+ */
+ static public function unserialize(\DOMElement $dom) {
+
+ $parent = $dom->firstChild;
+ while(!DAV\XMLUtil::toClarkNotation($parent)) {
+ $parent = $parent->nextSibling;
+ }
+
+ switch(DAV\XMLUtil::toClarkNotation($parent)) {
+
+ case '{DAV:}unauthenticated' :
+ return new self(self::UNAUTHENTICATED);
+ case '{DAV:}authenticated' :
+ return new self(self::AUTHENTICATED);
+ case '{DAV:}href':
+ return new self(self::HREF, $parent->textContent);
+ case '{DAV:}all':
+ return new self(self::ALL);
+ default :
+ throw new DAV\Exception\BadRequest('Unexpected element (' . DAV\XMLUtil::toClarkNotation($parent) . '). Could not deserialize');
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php
new file mode 100644
index 0000000..30d1d06
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+
+use OldSabre\DAV;
+
+/**
+ * SupportedPrivilegeSet property
+ *
+ * This property encodes the {DAV:}supported-privilege-set property, as defined
+ * in rfc3744. Please consult the rfc for details about it's structure.
+ *
+ * This class expects a structure like the one given from
+ * OldSabre\DAVACL\Plugin::getSupportedPrivilegeSet as the argument in its
+ * constructor.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedPrivilegeSet extends DAV\Property {
+
+ /**
+ * privileges
+ *
+ * @var array
+ */
+ private $privileges;
+
+ /**
+ * Constructor
+ *
+ * @param array $privileges
+ */
+ public function __construct(array $privileges) {
+
+ $this->privileges = $privileges;
+
+ }
+
+ /**
+ * Serializes the property into a domdocument.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ $this->serializePriv($doc, $node, $this->privileges);
+
+ }
+
+ /**
+ * Serializes a property
+ *
+ * This is a recursive function.
+ *
+ * @param \DOMDocument $doc
+ * @param \DOMElement $node
+ * @param array $privilege
+ * @return void
+ */
+ private function serializePriv($doc,$node,$privilege) {
+
+ $xsp = $doc->createElementNS('DAV:','d:supported-privilege');
+ $node->appendChild($xsp);
+
+ $xp = $doc->createElementNS('DAV:','d:privilege');
+ $xsp->appendChild($xp);
+
+ $privParts = null;
+ preg_match('/^{([^}]*)}(.*)$/',$privilege['privilege'],$privParts);
+
+ $xp->appendChild($doc->createElementNS($privParts[1],'d:'.$privParts[2]));
+
+ if (isset($privilege['abstract']) && $privilege['abstract']) {
+ $xsp->appendChild($doc->createElementNS('DAV:','d:abstract'));
+ }
+
+ if (isset($privilege['description'])) {
+ $xsp->appendChild($doc->createElementNS('DAV:','d:description',$privilege['description']));
+ }
+
+ if (isset($privilege['aggregates'])) {
+ foreach($privilege['aggregates'] as $subPrivilege) {
+ $this->serializePriv($doc,$xsp,$subPrivilege);
+ }
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php
new file mode 100644
index 0000000..e674cf2
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+/**
+ * This class contains the SabreDAV version constants.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.7';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php
new file mode 100644
index 0000000..f080983
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP AWS Authentication handler
+ *
+ * Use this class to leverage amazon's AWS authentication header
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AWSAuth extends AbstractAuth {
+
+ /**
+ * The signature supplied by the HTTP client
+ *
+ * @var string
+ */
+ private $signature = null;
+
+ /**
+ * The accesskey supplied by the HTTP client
+ *
+ * @var string
+ */
+ private $accessKey = null;
+
+ /**
+ * An error code, if any
+ *
+ * This value will be filled with one of the ERR_* constants
+ *
+ * @var int
+ */
+ public $errorCode = 0;
+
+ const ERR_NOAWSHEADER = 1;
+ const ERR_MD5CHECKSUMWRONG = 2;
+ const ERR_INVALIDDATEFORMAT = 3;
+ const ERR_REQUESTTIMESKEWED = 4;
+ const ERR_INVALIDSIGNATURE = 5;
+
+ /**
+ * Gathers all information from the headers
+ *
+ * This method needs to be called prior to anything else.
+ *
+ * @return bool
+ */
+ public function init() {
+
+ $authHeader = $this->httpRequest->getHeader('Authorization');
+ $authHeader = explode(' ',$authHeader);
+
+ if ($authHeader[0]!='AWS' || !isset($authHeader[1])) {
+ $this->errorCode = self::ERR_NOAWSHEADER;
+ return false;
+ }
+
+ list($this->accessKey,$this->signature) = explode(':',$authHeader[1]);
+
+ return true;
+
+ }
+
+ /**
+ * Returns the username for the request
+ *
+ * @return string
+ */
+ public function getAccessKey() {
+
+ return $this->accessKey;
+
+ }
+
+ /**
+ * Validates the signature based on the secretKey
+ *
+ * @param string $secretKey
+ * @return bool
+ */
+ public function validate($secretKey) {
+
+ $contentMD5 = $this->httpRequest->getHeader('Content-MD5');
+
+ if ($contentMD5) {
+ // We need to validate the integrity of the request
+ $body = $this->httpRequest->getBody(true);
+ $this->httpRequest->setBody($body,true);
+
+ if ($contentMD5!=base64_encode(md5($body,true))) {
+ // content-md5 header did not match md5 signature of body
+ $this->errorCode = self::ERR_MD5CHECKSUMWRONG;
+ return false;
+ }
+
+ }
+
+ if (!$requestDate = $this->httpRequest->getHeader('x-amz-date'))
+ $requestDate = $this->httpRequest->getHeader('Date');
+
+ if (!$this->validateRFC2616Date($requestDate))
+ return false;
+
+ $amzHeaders = $this->getAmzHeaders();
+
+ $signature = base64_encode(
+ $this->hmacsha1($secretKey,
+ $this->httpRequest->getMethod() . "\n" .
+ $contentMD5 . "\n" .
+ $this->httpRequest->getHeader('Content-type') . "\n" .
+ $requestDate . "\n" .
+ $amzHeaders .
+ $this->httpRequest->getURI()
+ )
+ );
+
+ if ($this->signature != $signature) {
+
+ $this->errorCode = self::ERR_INVALIDSIGNATURE;
+ return false;
+
+ }
+
+ return true;
+
+ }
+
+
+ /**
+ * Returns an HTTP 401 header, forcing login
+ *
+ * This should be called when username and password are incorrect, or not supplied at all
+ *
+ * @return void
+ */
+ public function requireLogin() {
+
+ $this->httpResponse->setHeader('WWW-Authenticate','AWS');
+ $this->httpResponse->sendStatus(401);
+
+ }
+
+ /**
+ * Makes sure the supplied value is a valid RFC2616 date.
+ *
+ * If we would just use strtotime to get a valid timestamp, we have no way of checking if a
+ * user just supplied the word 'now' for the date header.
+ *
+ * This function also makes sure the Date header is within 15 minutes of the operating
+ * system date, to prevent replay attacks.
+ *
+ * @param string $dateHeader
+ * @return bool
+ */
+ protected function validateRFC2616Date($dateHeader) {
+
+ $date = Util::parseHTTPDate($dateHeader);
+
+ // Unknown format
+ if (!$date) {
+ $this->errorCode = self::ERR_INVALIDDATEFORMAT;
+ return false;
+ }
+
+ $min = new \DateTime('-15 minutes');
+ $max = new \DateTime('+15 minutes');
+
+ // We allow 15 minutes around the current date/time
+ if ($date > $max || $date < $min) {
+ $this->errorCode = self::ERR_REQUESTTIMESKEWED;
+ return false;
+ }
+
+ return $date;
+
+ }
+
+ /**
+ * Returns a list of AMZ headers
+ *
+ * @return string
+ */
+ protected function getAmzHeaders() {
+
+ $amzHeaders = array();
+ $headers = $this->httpRequest->getHeaders();
+ foreach($headers as $headerName => $headerValue) {
+ if (strpos(strtolower($headerName),'x-amz-')===0) {
+ $amzHeaders[strtolower($headerName)] = str_replace(array("\r\n"),array(' '),$headerValue) . "\n";
+ }
+ }
+ ksort($amzHeaders);
+
+ $headerStr = '';
+ foreach($amzHeaders as $h=>$v) {
+ $headerStr.=$h.':'.$v;
+ }
+
+ return $headerStr;
+
+ }
+
+ /**
+ * Generates an HMAC-SHA1 signature
+ *
+ * @param string $key
+ * @param string $message
+ * @return string
+ */
+ private function hmacsha1($key, $message) {
+
+ $blocksize=64;
+ if (strlen($key)>$blocksize)
+ $key=pack('H*', sha1($key));
+ $key=str_pad($key,$blocksize,chr(0x00));
+ $ipad=str_repeat(chr(0x36),$blocksize);
+ $opad=str_repeat(chr(0x5c),$blocksize);
+ $hmac = pack('H*',sha1(($key^$opad).pack('H*',sha1(($key^$ipad).$message))));
+ return $hmac;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php
new file mode 100644
index 0000000..584088d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP Authentication baseclass
+ *
+ * This class has the common functionality for BasicAuth and DigestAuth
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractAuth {
+
+ /**
+ * The realm will be displayed in the dialog boxes
+ *
+ * This identifier can be changed through setRealm()
+ *
+ * @var string
+ */
+ protected $realm = 'SabreDAV';
+
+ /**
+ * HTTP response helper
+ *
+ * @var OldSabre\HTTP\Response
+ */
+ protected $httpResponse;
+
+
+ /**
+ * HTTP request helper
+ *
+ * @var OldSabre\HTTP\Request
+ */
+ protected $httpRequest;
+
+ /**
+ * __construct
+ *
+ */
+ public function __construct() {
+
+ $this->httpResponse = new Response();
+ $this->httpRequest = new Request();
+
+ }
+
+ /**
+ * Sets an alternative HTTP response object
+ *
+ * @param Response $response
+ * @return void
+ */
+ public function setHTTPResponse(Response $response) {
+
+ $this->httpResponse = $response;
+
+ }
+
+ /**
+ * Sets an alternative HTTP request object
+ *
+ * @param Request $request
+ * @return void
+ */
+ public function setHTTPRequest(Request $request) {
+
+ $this->httpRequest = $request;
+
+ }
+
+
+ /**
+ * Sets the realm
+ *
+ * The realm is often displayed in authentication dialog boxes
+ * Commonly an application name displayed here
+ *
+ * @param string $realm
+ * @return void
+ */
+ public function setRealm($realm) {
+
+ $this->realm = $realm;
+
+ }
+
+ /**
+ * Returns the realm
+ *
+ * @return string
+ */
+ public function getRealm() {
+
+ return $this->realm;
+
+ }
+
+ /**
+ * Returns an HTTP 401 header, forcing login
+ *
+ * This should be called when username and password are incorrect, or not supplied at all
+ *
+ * @return void
+ */
+ abstract public function requireLogin();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php
new file mode 100644
index 0000000..e970fb4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP Basic Authentication handler
+ *
+ * Use this class for easy http authentication setup
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class BasicAuth extends AbstractAuth {
+
+ /**
+ * Returns the supplied username and password.
+ *
+ * The returned array has two values:
+ * * 0 - username
+ * * 1 - password
+ *
+ * If nothing was supplied, 'false' will be returned
+ *
+ * @return mixed
+ */
+ public function getUserPass() {
+
+ // Apache and mod_php
+ if (($user = $this->httpRequest->getRawServerValue('PHP_AUTH_USER'))!==null && ($pass = $this->httpRequest->getRawServerValue('PHP_AUTH_PW'))!==null) {
+
+ return array($user,$pass);
+
+ }
+
+ // Most other webservers
+ $auth = $this->httpRequest->getHeader('Authorization');
+
+ // Apache could prefix environment variables with REDIRECT_ when urls
+ // are passed through mod_rewrite
+ if (!$auth) {
+ $auth = $this->httpRequest->getRawServerValue('REDIRECT_HTTP_AUTHORIZATION');
+ }
+
+ if (!$auth) return false;
+
+ if (strpos(strtolower($auth),'basic')!==0) return false;
+
+ return explode(':', base64_decode(substr($auth, 6)),2);
+
+ }
+
+ /**
+ * Returns an HTTP 401 header, forcing login
+ *
+ * This should be called when username and password are incorrect, or not supplied at all
+ *
+ * @return void
+ */
+ public function requireLogin() {
+
+ $this->httpResponse->setHeader('WWW-Authenticate','Basic realm="' . $this->realm . '"');
+ $this->httpResponse->sendStatus(401);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php
new file mode 100644
index 0000000..d21a548
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP Digest Authentication handler
+ *
+ * Use this class for easy http digest authentication.
+ * Instructions:
+ *
+ * 1. Create the object
+ * 2. Call the setRealm() method with the realm you plan to use
+ * 3. Call the init method function.
+ * 4. Call the getUserName() function. This function may return false if no
+ * authentication information was supplied. Based on the username you
+ * should check your internal database for either the associated password,
+ * or the so-called A1 hash of the digest.
+ * 5. Call either validatePassword() or validateA1(). This will return true
+ * or false.
+ * 6. To make sure an authentication prompt is displayed, call the
+ * requireLogin() method.
+ *
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class DigestAuth extends AbstractAuth {
+
+ /**
+ * These constants are used in setQOP();
+ */
+ const QOP_AUTH = 1;
+ const QOP_AUTHINT = 2;
+
+ protected $nonce;
+ protected $opaque;
+ protected $digestParts;
+ protected $A1;
+ protected $qop = self::QOP_AUTH;
+
+ /**
+ * Initializes the object
+ */
+ public function __construct() {
+
+ $this->nonce = uniqid();
+ $this->opaque = md5($this->realm);
+ parent::__construct();
+
+ }
+
+ /**
+ * Gathers all information from the headers
+ *
+ * This method needs to be called prior to anything else.
+ *
+ * @return void
+ */
+ public function init() {
+
+ $digest = $this->getDigest();
+ $this->digestParts = $this->parseDigest($digest);
+
+ }
+
+ /**
+ * Sets the quality of protection value.
+ *
+ * Possible values are:
+ * OldSabre\HTTP\DigestAuth::QOP_AUTH
+ * OldSabre\HTTP\DigestAuth::QOP_AUTHINT
+ *
+ * Multiple values can be specified using logical OR.
+ *
+ * QOP_AUTHINT ensures integrity of the request body, but this is not
+ * supported by most HTTP clients. QOP_AUTHINT also requires the entire
+ * request body to be md5'ed, which can put strains on CPU and memory.
+ *
+ * @param int $qop
+ * @return void
+ */
+ public function setQOP($qop) {
+
+ $this->qop = $qop;
+
+ }
+
+ /**
+ * Validates the user.
+ *
+ * The A1 parameter should be md5($username . ':' . $realm . ':' . $password);
+ *
+ * @param string $A1
+ * @return bool
+ */
+ public function validateA1($A1) {
+
+ $this->A1 = $A1;
+ return $this->validate();
+
+ }
+
+ /**
+ * Validates authentication through a password. The actual password must be provided here.
+ * It is strongly recommended not store the password in plain-text and use validateA1 instead.
+ *
+ * @param string $password
+ * @return bool
+ */
+ public function validatePassword($password) {
+
+ $this->A1 = md5($this->digestParts['username'] . ':' . $this->realm . ':' . $password);
+ return $this->validate();
+
+ }
+
+ /**
+ * Returns the username for the request
+ *
+ * @return string
+ */
+ public function getUsername() {
+
+ return $this->digestParts['username'];
+
+ }
+
+ /**
+ * Validates the digest challenge
+ *
+ * @return bool
+ */
+ protected function validate() {
+
+ $A2 = $this->httpRequest->getMethod() . ':' . $this->digestParts['uri'];
+
+ if ($this->digestParts['qop']=='auth-int') {
+ // Making sure we support this qop value
+ if (!($this->qop & self::QOP_AUTHINT)) return false;
+ // We need to add an md5 of the entire request body to the A2 part of the hash
+ $body = $this->httpRequest->getBody(true);
+ $this->httpRequest->setBody($body,true);
+ $A2 .= ':' . md5($body);
+ } else {
+
+ // We need to make sure we support this qop value
+ if (!($this->qop & self::QOP_AUTH)) return false;
+ }
+
+ $A2 = md5($A2);
+
+ $validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}");
+
+ return $this->digestParts['response']==$validResponse;
+
+
+ }
+
+ /**
+ * Returns an HTTP 401 header, forcing login
+ *
+ * This should be called when username and password are incorrect, or not supplied at all
+ *
+ * @return void
+ */
+ public function requireLogin() {
+
+ $qop = '';
+ switch($this->qop) {
+ case self::QOP_AUTH : $qop = 'auth'; break;
+ case self::QOP_AUTHINT : $qop = 'auth-int'; break;
+ case self::QOP_AUTH | self::QOP_AUTHINT : $qop = 'auth,auth-int'; break;
+ }
+
+ $this->httpResponse->setHeader('WWW-Authenticate','Digest realm="' . $this->realm . '",qop="'.$qop.'",nonce="' . $this->nonce . '",opaque="' . $this->opaque . '"');
+ $this->httpResponse->sendStatus(401);
+
+ }
+
+
+ /**
+ * This method returns the full digest string.
+ *
+ * It should be compatibile with mod_php format and other webservers.
+ *
+ * If the header could not be found, null will be returned
+ *
+ * @return mixed
+ */
+ public function getDigest() {
+
+ // mod_php
+ $digest = $this->httpRequest->getRawServerValue('PHP_AUTH_DIGEST');
+ if ($digest) return $digest;
+
+ // most other servers
+ $digest = $this->httpRequest->getHeader('Authorization');
+
+ // Apache could prefix environment variables with REDIRECT_ when urls
+ // are passed through mod_rewrite
+ if (!$digest) {
+ $digest = $this->httpRequest->getRawServerValue('REDIRECT_HTTP_AUTHORIZATION');
+ }
+
+ if ($digest && strpos(strtolower($digest),'digest')===0) {
+ return substr($digest,7);
+ } else {
+ return null;
+ }
+
+ }
+
+
+ /**
+ * Parses the different pieces of the digest string into an array.
+ *
+ * This method returns false if an incomplete digest was supplied
+ *
+ * @param string $digest
+ * @return mixed
+ */
+ protected function parseDigest($digest) {
+
+ // protect against missing data
+ $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
+ $data = array();
+
+ preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER);
+
+ foreach ($matches as $m) {
+ $data[$m[1]] = $m[2] ? $m[2] : $m[3];
+ unset($needed_parts[$m[1]]);
+ }
+
+ return $needed_parts ? false : $data;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php
new file mode 100644
index 0000000..d4cad65
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php
@@ -0,0 +1,284 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP Request information
+ *
+ * This object can be used to easily access information about an HTTP request.
+ * It can additionally be used to create 'mock' requests.
+ *
+ * This class mostly operates independent, but because of the nature of a single
+ * request per run it can operate as a singleton. For more information check out
+ * the behaviour around 'defaultInputStream'.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Request {
+
+ /**
+ * PHP's $_SERVER data
+ *
+ * @var array
+ */
+ protected $_SERVER;
+
+ /**
+ * PHP's $_POST data
+ *
+ * @var array
+ */
+ protected $_POST;
+
+ /**
+ * The request body, if any.
+ *
+ * This is stored in the form of a stream resource.
+ *
+ * @var resource
+ */
+ protected $body = null;
+
+ /**
+ * This will be set as the 'default' inputStream for a specific HTTP request
+ * We sometimes need to retain, or rebuild this if we need multiple runs
+ * of parsing the original HTTP request.
+ *
+ * @var resource
+ */
+ static $defaultInputStream=null;
+
+ /**
+ * Sets up the object
+ *
+ * The serverData and postData array can be used to override usage of PHP's
+ * global _SERVER and _POST variable respectively.
+ *
+ * @param array $serverData
+ * @param array $postData
+ */
+ public function __construct(array $serverData = null, array $postData = null) {
+
+ if ($serverData) $this->_SERVER = $serverData;
+ else $this->_SERVER =& $_SERVER;
+
+ if ($postData) $this->_POST = $postData;
+ else $this->_POST =& $_POST;
+
+ }
+
+ /**
+ * Returns the value for a specific http header.
+ *
+ * This method returns null if the header did not exist.
+ *
+ * @param string $name
+ * @return string
+ */
+ public function getHeader($name) {
+
+ $name = strtoupper(str_replace(array('-'),array('_'),$name));
+ if (isset($this->_SERVER['HTTP_' . $name])) {
+ return $this->_SERVER['HTTP_' . $name];
+ }
+
+ // There's a few headers that seem to end up in the top-level
+ // server array.
+ switch($name) {
+ case 'CONTENT_TYPE' :
+ case 'CONTENT_LENGTH' :
+ if (isset($this->_SERVER[$name])) {
+ return $this->_SERVER[$name];
+ }
+ break;
+
+ }
+ return;
+
+ }
+
+ /**
+ * Returns all (known) HTTP headers.
+ *
+ * All headers are converted to lower-case, and additionally all underscores
+ * are automatically converted to dashes
+ *
+ * @return array
+ */
+ public function getHeaders() {
+
+ $hdrs = array();
+ foreach($this->_SERVER as $key=>$value) {
+
+ switch($key) {
+ case 'CONTENT_LENGTH' :
+ case 'CONTENT_TYPE' :
+ $hdrs[strtolower(str_replace('_','-',$key))] = $value;
+ break;
+ default :
+ if (strpos($key,'HTTP_')===0) {
+ $hdrs[substr(strtolower(str_replace('_','-',$key)),5)] = $value;
+ }
+ break;
+ }
+
+ }
+
+ return $hdrs;
+
+ }
+
+ /**
+ * Returns the HTTP request method
+ *
+ * This is for example POST or GET
+ *
+ * @return string
+ */
+ public function getMethod() {
+
+ return $this->_SERVER['REQUEST_METHOD'];
+
+ }
+
+ /**
+ * Returns the requested uri
+ *
+ * @return string
+ */
+ public function getUri() {
+
+ return $this->_SERVER['REQUEST_URI'];
+
+ }
+
+ /**
+ * Will return protocol + the hostname + the uri
+ *
+ * @return string
+ */
+ public function getAbsoluteUri() {
+
+ // Checking if the request was made through HTTPS. The last in line is for IIS
+ $protocol = isset($this->_SERVER['HTTPS']) && ($this->_SERVER['HTTPS']) && ($this->_SERVER['HTTPS']!='off');
+ return ($protocol?'https':'http') . '://' . $this->getHeader('Host') . $this->getUri();
+
+ }
+
+ /**
+ * Returns everything after the ? from the current url
+ *
+ * @return string
+ */
+ public function getQueryString() {
+
+ return isset($this->_SERVER['QUERY_STRING'])?$this->_SERVER['QUERY_STRING']:'';
+
+ }
+
+ /**
+ * Returns the HTTP request body body
+ *
+ * This method returns a readable stream resource.
+ * If the asString parameter is set to true, a string is sent instead.
+ *
+ * @param bool $asString
+ * @return resource
+ */
+ public function getBody($asString = false) {
+
+ if (is_null($this->body)) {
+ if (!is_null(self::$defaultInputStream)) {
+ $this->body = self::$defaultInputStream;
+ } else {
+ $this->body = fopen('php://input','r');
+ self::$defaultInputStream = $this->body;
+ }
+ }
+ if ($asString) {
+ $body = stream_get_contents($this->body);
+ return $body;
+ } else {
+ return $this->body;
+ }
+
+ }
+
+ /**
+ * Sets the contents of the HTTP request body
+ *
+ * This method can either accept a string, or a readable stream resource.
+ *
+ * If the setAsDefaultInputStream is set to true, it means for this run of the
+ * script the supplied body will be used instead of php://input.
+ *
+ * @param mixed $body
+ * @param bool $setAsDefaultInputStream
+ * @return void
+ */
+ public function setBody($body,$setAsDefaultInputStream = false) {
+
+ if(is_resource($body)) {
+ $this->body = $body;
+ } else {
+
+ $stream = fopen('php://temp','r+');
+ fputs($stream,$body);
+ rewind($stream);
+ // String is assumed
+ $this->body = $stream;
+ }
+ if ($setAsDefaultInputStream) {
+ self::$defaultInputStream = $this->body;
+ }
+
+ }
+
+ /**
+ * Returns PHP's _POST variable.
+ *
+ * The reason this is in a method is so it can be subclassed and
+ * overridden.
+ *
+ * @return array
+ */
+ public function getPostVars() {
+
+ return $this->_POST;
+
+ }
+
+ /**
+ * Returns a specific item from the _SERVER array.
+ *
+ * Do not rely on this feature, it is for internal use only.
+ *
+ * @param string $field
+ * @return string
+ */
+ public function getRawServerValue($field) {
+
+ return isset($this->_SERVER[$field])?$this->_SERVER[$field]:null;
+
+ }
+
+ /**
+ * Returns the HTTP version specified within the request.
+ *
+ * @return string
+ */
+ public function getHTTPVersion() {
+
+ $protocol = $this->getRawServerValue('SERVER_PROTOCOL');
+ if ($protocol==='HTTP/1.0') {
+ return '1.0';
+ } else {
+ return '1.1';
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php
new file mode 100644
index 0000000..07009d9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * This class represents a HTTP response.
+ *
+ * It contains the HTTP status code, response headers and a message body.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Response {
+
+ /**
+ * The HTTP version to return in the header() line.
+ *
+ * By default you will definitely want this to be HTTP/1.1, but in some
+ * edge cases (most notably pre 1.2 nginx servers acting as a proxy), you
+ * want to force this to 1.0.
+ *
+ * @var string
+ */
+ public $defaultHttpVersion = '1.1';
+
+ /**
+ * Returns a full HTTP status message for an HTTP status code
+ *
+ * @param int $code
+ * @return string
+ */
+ public function getStatusMessage($code, $httpVersion = '1.1') {
+
+ $msg = array(
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authorative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status', // RFC 4918
+ 208 => 'Already Reported', // RFC 5842
+ 226 => 'IM Used', // RFC 3229
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 306 => 'Reserved',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 418 => 'I\'m a teapot', // RFC 2324
+ 422 => 'Unprocessable Entity', // RFC 4918
+ 423 => 'Locked', // RFC 4918
+ 424 => 'Failed Dependency', // RFC 4918
+ 426 => 'Upgrade required',
+ 428 => 'Precondition required', // draft-nottingham-http-new-status
+ 429 => 'Too Many Requests', // draft-nottingham-http-new-status
+ 431 => 'Request Header Fields Too Large', // draft-nottingham-http-new-status
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version not supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage', // RFC 4918
+ 508 => 'Loop Detected', // RFC 5842
+ 509 => 'Bandwidth Limit Exceeded', // non-standard
+ 510 => 'Not extended',
+ 511 => 'Network Authentication Required', // draft-nottingham-http-new-status
+ );
+
+ return 'HTTP/' . $httpVersion . ' ' . $code . ' ' . $msg[$code];
+
+ }
+
+ // @codeCoverageIgnoreStart
+ // We cannot reasonably test header() related methods.
+
+ /**
+ * Sends an HTTP status header to the client.
+ *
+ * @param int $code HTTP status code
+ * @return bool
+ */
+ public function sendStatus($code) {
+
+ if (!headers_sent())
+ return header($this->getStatusMessage($code, $this->defaultHttpVersion));
+ else return false;
+
+ }
+
+ /**
+ * Sets an HTTP header for the response
+ *
+ * @param string $name
+ * @param string $value
+ * @param bool $replace
+ * @return bool
+ */
+ public function setHeader($name, $value, $replace = true) {
+
+ $value = str_replace(array("\r","\n"),array('\r','\n'),$value);
+ if (!headers_sent())
+ return header($name . ': ' . $value, $replace);
+ else return false;
+
+
+ }
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Sets a bunch of HTTP Headers
+ *
+ * headersnames are specified as keys, value in the array value
+ *
+ * @param array $headers
+ * @return void
+ */
+ public function setHeaders(array $headers) {
+
+ foreach($headers as $key=>$value)
+ $this->setHeader($key, $value);
+
+ }
+
+ /**
+ * Sends the entire response body
+ *
+ * This method can accept either an open filestream, or a string.
+ *
+ * @param mixed $body
+ * @return void
+ */
+ public function sendBody($body) {
+
+ if (is_resource($body)) {
+
+ file_put_contents('php://output', $body);
+
+ } else {
+
+ // We assume a string
+ echo $body;
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php
new file mode 100644
index 0000000..d825ad8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP utility methods
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @author Paul Voegler
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Util {
+
+ /**
+ * Parses a RFC2616-compatible date string
+ *
+ * This method returns false if the date is invalid
+ *
+ * @param string $dateHeader
+ * @return bool|DateTime
+ */
+ static function parseHTTPDate($dateHeader) {
+
+ //RFC 2616 section 3.3.1 Full Date
+ //Only the format is checked, valid ranges are checked by strtotime below
+ $month = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)';
+ $weekday = '(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)';
+ $wkday = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)';
+ $time = '[0-2]\d(\:[0-5]\d){2}';
+ $date3 = $month . ' ([1-3]\d| \d)';
+ $date2 = '[0-3]\d\-' . $month . '\-\d\d';
+ //4-digit year cannot begin with 0 - unix timestamp begins in 1970
+ $date1 = '[0-3]\d ' . $month . ' [1-9]\d{3}';
+
+ //ANSI C's asctime() format
+ //4-digit year cannot begin with 0 - unix timestamp begins in 1970
+ $asctime_date = $wkday . ' ' . $date3 . ' ' . $time . ' [1-9]\d{3}';
+ //RFC 850, obsoleted by RFC 1036
+ $rfc850_date = $weekday . ', ' . $date2 . ' ' . $time . ' GMT';
+ //RFC 822, updated by RFC 1123
+ $rfc1123_date = $wkday . ', ' . $date1 . ' ' . $time . ' GMT';
+ //allowed date formats by RFC 2616
+ $HTTP_date = "($rfc1123_date|$rfc850_date|$asctime_date)";
+
+ //allow for space around the string and strip it
+ $dateHeader = trim($dateHeader, ' ');
+ if (!preg_match('/^' . $HTTP_date . '$/', $dateHeader))
+ return false;
+
+ //append implicit GMT timezone to ANSI C time format
+ if (strpos($dateHeader, ' GMT') === false)
+ $dateHeader .= ' GMT';
+
+
+ $realDate = strtotime($dateHeader);
+ //strtotime can return -1 or false in case of error
+ if ($realDate !== false && $realDate >= 0)
+ return new \DateTime('@' . $realDate, new \DateTimeZone('UTC'));
+
+ }
+
+ /**
+ * Transforms a DateTime object to HTTP's most common date format.
+ *
+ * We're serializing it as the RFC 1123 date, which, for HTTP must be
+ * specified as GMT.
+ *
+ * @param \DateTime $dateTime
+ * @return string
+ */
+ static function toHTTPDate(\DateTime $dateTime) {
+
+ // We need to clone it, as we don't want to affect the existing
+ // DateTime.
+ $dateTime = clone $dateTime;
+ $dateTime->setTimeZone(new \DateTimeZone('GMT'));
+ return $dateTime->format('D, d M Y H:i:s \G\M\T');
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php
new file mode 100644
index 0000000..83d5c59
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * This class contains the OldSabre\HTTP version constants.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.12';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/autoload.php b/calendar/lib/SabreDAV/lib/OldSabre/autoload.php
new file mode 100644
index 0000000..6882472
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/autoload.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * SabreDAV's autoloader
+ *
+ * This file is kept for backwards compatibility purposes.
+ * SabreDAV now uses the composer autoloader.
+ *
+ * You should stop including this file, and include 'vendor/autoload.php'
+ * instead.
+ *
+ * @deprecated Will be removed in a future version!
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+
+/**
+ * We are assuming that the composer autoloader is just 2 directories up.
+ *
+ * This is not the case when sabredav is installed as a dependency. But, in
+ * those cases it's not expected that people will look for this file anyway.
+ */
+
+require __DIR__ . '/../../vendor/autoload.php';
diff --git a/calendar/lib/SabreDAV/vendor/autoload.php b/calendar/lib/SabreDAV/vendor/autoload.php
new file mode 100644
index 0000000..01c4e4d
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/autoload.php
@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer' . '/autoload_real.php';
+
+return ComposerAutoloaderInit339e11a867637abea2ce832a4777d9e5::getLoader();
diff --git a/calendar/lib/SabreDAV/vendor/composer/ClassLoader.php b/calendar/lib/SabreDAV/vendor/composer/ClassLoader.php
new file mode 100644
index 0000000..70d78bc
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/ClassLoader.php
@@ -0,0 +1,387 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0 class loader
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class ClassLoader
+{
+ // PSR-4
+ private $prefixLengthsPsr4 = array();
+ private $prefixDirsPsr4 = array();
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ private $prefixesPsr0 = array();
+ private $fallbackDirsPsr0 = array();
+
+ private $useIncludePath = false;
+ private $classMap = array();
+
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', $this->prefixesPsr0);
+ }
+
+ return array();
+ }
+
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ (array) $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-0 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ (array) $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 base directories
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return bool|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ includeFile($file);
+
+ return true;
+ }
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
+ if ('\\' == $class[0]) {
+ $class = substr($class, 1);
+ }
+
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if ($file === null && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if ($file === null) {
+ // Remember that this class does not exist.
+ return $this->classMap[$class] = false;
+ }
+
+ return $file;
+ }
+
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+ }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+ include $file;
+}
diff --git a/calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php b/calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000..7a91153
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php b/calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000..f442fdb
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,15 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+ 'OldSabre\\VObject' => array($vendorDir . '/oldsabre/vobject/lib'),
+ 'OldSabre\\HTTP' => array($baseDir . '/lib'),
+ 'OldSabre\\DAVACL' => array($baseDir . '/lib'),
+ 'OldSabre\\DAV' => array($baseDir . '/lib'),
+ 'OldSabre\\CardDAV' => array($baseDir . '/lib'),
+ 'OldSabre\\CalDAV' => array($baseDir . '/lib'),
+);
diff --git a/calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php b/calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php
new file mode 100644
index 0000000..b265c64
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/calendar/lib/SabreDAV/vendor/composer/autoload_real.php b/calendar/lib/SabreDAV/vendor/composer/autoload_real.php
new file mode 100644
index 0000000..492c0bf
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/autoload_real.php
@@ -0,0 +1,50 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInit339e11a867637abea2ce832a4777d9e5
+{
+ private static $loader;
+
+ public static function loadClassLoader($class)
+ {
+ if ('Composer\Autoload\ClassLoader' === $class) {
+ require __DIR__ . '/ClassLoader.php';
+ }
+ }
+
+ public static function getLoader()
+ {
+ if (null !== self::$loader) {
+ return self::$loader;
+ }
+
+ spl_autoload_register(array('ComposerAutoloaderInit339e11a867637abea2ce832a4777d9e5', 'loadClassLoader'), true, true);
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+ spl_autoload_unregister(array('ComposerAutoloaderInit339e11a867637abea2ce832a4777d9e5', 'loadClassLoader'));
+
+ $map = require __DIR__ . '/autoload_namespaces.php';
+ foreach ($map as $namespace => $path) {
+ $loader->set($namespace, $path);
+ }
+
+ $map = require __DIR__ . '/autoload_psr4.php';
+ foreach ($map as $namespace => $path) {
+ $loader->setPsr4($namespace, $path);
+ }
+
+ $classMap = require __DIR__ . '/autoload_classmap.php';
+ if ($classMap) {
+ $loader->addClassMap($classMap);
+ }
+
+ $loader->register(true);
+
+ return $loader;
+ }
+}
+
+function composerRequire339e11a867637abea2ce832a4777d9e5($file)
+{
+ require $file;
+}
diff --git a/calendar/lib/SabreDAV/vendor/composer/installed.json b/calendar/lib/SabreDAV/vendor/composer/installed.json
new file mode 100644
index 0000000..b4f0389
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/installed.json
@@ -0,0 +1,52 @@
+[
+ {
+ "name": "sabre/vobject",
+ "version": "2.1.7",
+ "version_normalized": "2.1.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fruux/sabre-vobject.git",
+ "reference": "c3ebe643fd1037b656ebcc149e3fd3d38938fe58"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fruux/sabre-vobject/zipball/c3ebe643fd1037b656ebcc149e3fd3d38938fe58",
+ "reference": "c3ebe643fd1037b656ebcc149e3fd3d38938fe58",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.1"
+ },
+ "time": "2015-01-21 20:57:59",
+ "bin": [
+ "bin/vobjectvalidate.php"
+ ],
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "OldSabre\\VObject": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "evert@rooftopsolutions.nl",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "homepage": "http://sabre.io/vobject/",
+ "keywords": [
+ "VObject",
+ "iCalendar",
+ "vCard"
+ ]
+ }
+]
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog
new file mode 100644
index 0000000..c330fa2
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog
@@ -0,0 +1,70 @@
+2.1.0-stable (2013-06-17)
+ * This version is fully backwards compatible with 2.0.*. However, it
+ contains a few new API's that mimic the VObject 3 API. This allows it to
+ be used a 'bridge' version.
+ Specifically, this new version exists so SabreDAV 1.7 and 1.8 can run with
+ both the 2 and 3 versions of this library.
+ * Added: Property\DateTime::hasTime().
+ * Added: Property\MultiDateTime::hasTime().
+ * Added: Property::getValue().
+ * Added: Document class.
+ * Added: Document::createComponent and Document::createProperty.
+ * Added: Parameter::getValue().
+
+
+2.0.7-stable (2013-03-05)
+ * Fixed: Microsoft re-uses their magic numbers for different timezones,
+ specifically id 2 for both Sarajevo and Lisbon). A workaround was added
+ to deal with this.
+
+2.0.6-stable (2013-02-17)
+ * Fixed: The reader now properly parses parameters without a value.
+
+2.0.5-stable (2012-11-05)
+ * Fixed: The FreeBusyGenerator is now properly using the factory methods
+ for creation of components and properties.
+
+2.0.4-stable (2012-11-02)
+ * Added: Known Lotus Notes / Domino timezone id's.
+
+2.0.3-stable (2012-10-29)
+ * Added: Support for 'GMT+????' format in TZID's.
+ * Added: Support for formats like SystemV/EST5EDT in TZID's.
+ * Fixed: RecurrenceIterator now repairs recurrence rules where UNTIL < DTSTART.
+ * Added: Support for BYHOUR in FREQ=DAILY (@hollodk).
+ * Added: Support for BYHOUR and BYDAY in FREQ=WEEKLY.
+
+2.0.2-stable (2012-10-06)
+ * Added: includes.php file, to load the entire library in one go.
+ * Fixed: A problem with determining alarm triggers for TODO's.
+
+2.0.1-stable (2012-09-22)
+ * Removed: Element class. It wasn't used.
+ * Added: Basic validation and repair methods for broken input data.
+ * Fixed: RecurrenceIterator could infinitely loop when an INTERVAL of 0
+ was specified.
+ * Added: A cli script that can validate and automatically repair vcards
+ and iCalendar objects.
+ * Added: A new 'Compound' property, that can automatically split up parts
+ for properties such as N, ADR, ORG and CATEGORIES.
+ * Added: Splitter classes, that can split up large objects (such as exports)
+ into individual objects (thanks @DominikTO and @armin-hackmann).
+ * Added: VFREEBUSY component, which allows easily checking wether
+ timeslots are available.
+ * Added: The Reader class now has a 'FORGIVING' option, which allows it to
+ parse properties with incorrect characters in the name (at this time, it
+ just allows underscores).
+ * Added: Also added the 'IGNORE_INVALID_LINES' option, to completely
+ disregard any invalid lines.
+ * Fixed: A bug in Windows timezone-id mappings for times created in
+ Greenlands timezone (sorry Greenlanders! I do care!).
+ * Fixed: DTEND was not generated correctly for VFREEBUSY reports.
+ * Fixed: Parser is at least 25% faster with real-world data.
+
+2.0.0-stable (2012-08-08)
+ * VObject is now a separate project from SabreDAV. See the SabreDAV
+ changelog for version information before 2.0.
+ * New: VObject library now uses PHP 5.3 namespaces.
+ * New: It's possible to specify lists of parameters when constructing
+ properties.
+ * New: made it easier to construct the FreeBusyGenerator.
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE
new file mode 100644
index 0000000..628c60c
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE
@@ -0,0 +1,27 @@
+Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name Sabre nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md
new file mode 100644
index 0000000..e845cec
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md
@@ -0,0 +1,384 @@
+SabreTooth VObject library
+==========================
+
+[![Build Status](https://secure.travis-ci.org/fruux/sabre-vobject.png?branch=master)](http://travis-ci.org/fruux/sabre-vobject)
+
+The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545)
+and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP.
+The goal of the VObject library is to create a very complete library, with an easy to use API.
+
+This project is a spin-off from [SabreDAV](http://code.google.com/p/sabredav/), where it has
+been used for several years. The VObject library has 100% unittest coverage.
+
+Installation
+------------
+
+VObject requires PHP 5.3, and should be installed using composer.
+The general composer instructions can be found on the [composer website](http://getcomposer.org/doc/00-intro.md composer website).
+
+After that, just declare the vobject dependency as follows:
+
+```
+"require" : {
+ "sabre/vobject" : "2.0.*"
+}
+```
+
+Then, run `composer.phar update` and you should be good.
+
+Usage
+-----
+
+### Parsing
+
+For our example, we will be using the following vcard:
+
+```
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 2.0//EN
+N:Planck;Max;;;
+FN:Max Planck
+EMAIL;TYPE=WORK:mplanck@example.org
+item1.TEL;TYPE=CELL:(+49)3144435678
+item1.X-ABLabel:Private cell
+item2.TEL;TYPE=WORK:(+49)5554564744
+item2.X-ABLabel:Work
+END:VCARD
+```
+
+
+If we want to just print out Max' full name, you can just use property access:
+
+
+```php
+use OldSabre\VObject;
+
+$card = VObject\Reader::read($data);
+echo $card->FN;
+```
+
+### Changing properties
+
+Creating properties is pretty similar. If we like to add his birthday, we just
+set the property:
+
+```php
+$card->BDAY = '1858-04-23';
+```
+
+Note that in the previous example, we're actually updating any existing BDAY that
+may already exist. If we want to add a new property, without overwriting the previous
+we can do this with the `add` method.
+
+```php
+$card->add('EMAIL','max@example.org');
+```
+
+### Parameters
+
+If we want to also specify that this is max' home email addresses, we can do this with
+a third parameter:
+
+```
+$card->add('EMAIL', 'max@example'org', array('type' => 'HOME'));
+```
+
+If we want to read out all the email addresses and their type, this would look something
+like this:
+
+```
+foreach($card->EMAIL as $email) {
+
+ echo $email['TYPE'], ' - ', $email;
+
+}
+```
+
+### Groups
+
+In our example, you can see that the TEL properties are prefixed. These are 'groups' and
+allow you to group multiple related properties together. The group can be any user-defined
+name.
+
+This particular example as generated by the OS X addressbook, which uses the `X-ABLabel`
+to allow the user to specify custom labels for properties. OS X addressbook uses groups
+to tie the label to the property.
+
+The VObject library simply ignores the group if you don't specify it, so this will work:
+
+```php
+foreach($card->TEL as $tel) {
+ echo $tel, "\n";
+}
+```
+
+But if you would like to target a specific group + property, this is possible too:
+
+```php
+echo $card->{'ITEM1.TEL'};
+```
+
+So if you would like to show all the phone numbers, along with their custom label, the
+following syntax is used:
+
+```php
+foreach($card->TEL as $tel) {
+
+ echo $card->{$tel->group . '.X-ABLABEL'}, ": ";
+ echo $tel, "\n";
+
+}
+```
+
+### Serializing / Saving
+
+If you want to generate your updated VObject, you can simply call the serialize() method.
+
+```php
+echo $card->serialize();
+```
+
+### Components
+
+iCalendar, unlike vCards always have sub-components. Where vCards are often just a flat
+list, iCalendar objects tend to have a tree-like structure. For the following paragraphs,
+we will use the following object as the example:
+
+```
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 2.0//EN
+BEGIN:VEVENT
+SUMMARY:Curiosity landing
+DTSTART:20120806T051439Z
+LOCATION:Mars
+END:VEVENT
+END:VCALENDAR
+```
+
+Since events, tasks and journals are always in a sub component, this is also how we
+access them.
+
+```php
+use OldSabre\VObject;
+
+$calendar = VObject\Reader::read($data);
+echo $calendar->VEVENT->SUMMARY;
+```
+
+Adding components to a calendar is done with a factory method:
+
+```php
+$event = VObject\Component::create('VEVENT');
+$calendar->add($event);
+
+$event->SUMMARY = 'Curiosity launch';
+$event->DTSTART = '20111126T150202Z';
+$event->LOCATION = 'Cape Carnival';
+```
+
+By the way.. cloning also works as expected, as the entire structure is cloned along with it:
+
+```php
+$clonedEvent = clone $calendar->VEVENT[0];
+$calendar->add($clonedEvent);
+```
+
+### Date and time handling
+
+If you ever had to deal with iCalendar timezones, you know it can be complicated.
+The way timezones are specified is flawed, which is something I may write an essay about some
+day. VObject does its best to determine the correct timezone. Many standard formats
+have been tested and verified, and special code has been implemented for special-casing
+microsoft generated timezone information, and others.
+
+To get a real php `DateTime` object, you can request this as follows:
+
+```php
+$event = $calendar->VEVENT;
+$start = $event->DTSTART->getDateTime();
+echo $start->format(\DateTime::W3C);
+```
+
+To set the property with a DateTime object, you can use the following syntax:
+
+```php
+$dateTime = new \DateTime('2012-08-07 23:53:00', new DateTimeZone('Europe/Amsterdam'));
+$event->DTSTART->setDateTime($dateTime, VObject\Property\DateTime::DATE);
+```
+
+The second argument specifies the type of date you're setting. The following three
+options exist:
+
+1. `LOCAL` This is a floating time, with no timezone information. This basically specifies that the event happens in whatever the timezone's currently in. This would be encoded as `DTSTART;VALUE=DATE-TIME:20120807235300`
+2. `UTC` This specifies that the time should be encoded as a UTC time. This is encoded as `DTSTART;VALUE=DATE-TIME:20120807205300Z`. Note the extra Z and the fact that it's two hours 'earlier'.
+3. `LOCALTZ` specifies that it's supposed to be encoded in its local timezone. For example `DTSTART;VALUE=DATE-TIME;TZID=Europe/Amsterdam:20120807235300`.
+4. `DATE` This is a date-only, and does not contain the time. In this case this would be encoded as `DTSTART;VALUE=DATE:20120807`.
+
+A few important notes:
+
+* When a `TZID` is specified, there should also be a matching `VTIMEZONE` object with all the timezone information. VObject cannot currently automatically embed this. However, in reality other clients seem to do fine without this information. Yet, for completeness, this will be added in the future.
+* As mentioned, the timezone-determination process may sometimes fail. Report any issues you find, and I'll be quick to add workarounds!
+
+### Recurrence rules
+
+Recurrence rules allow events to recur, for example for a weekly meeting, or an anniversary.
+This is done with the `RRULE` property. The `RRULE` property allows for a LOT of different
+rules. VObject only implements the ones that actually appear in calendar software.
+
+To read more about `RRULE` and all the options, check out [RFC5545](https://tools.ietf.org/html/rfc5545#section-3.8.5).
+VObject supports the following options:
+
+1. `UNTIL` for an end date.
+2. `INTERVAL` for for example "every 2 days".
+3. `COUNT` to stop recurring after x items.
+4. `FREQ=DAILY` to recur every day, and `BYDAY` to limit it to certain days.
+5. `FREQ=WEEKLY` to recur every week, `BYDAY` to expand this to multiple weekdays in every week and `WKST` to specify on which day the week starts.
+6. `FREQ=MONTHLY` to recur every month, `BYMONTHDAY` to expand this to certain days in a month, `BYDAY` to expand it to certain weekdays occuring in a month, and `BYSETPOS` to limit the last two expansions.
+7. `FREQ=YEARLY` to recur every year, `BYMONTH` to expand that to certain months in a year, and `BYDAY` and `BYWEEKDAY` to expand the `BYMONTH` rule even further.
+
+VObject supports the `EXDATE` property for exclusions, but not yet the `RDATE` and `EXRULE`
+properties. If you're interested in this, please file a github issue, as this will put it
+on my radar.
+
+This is a bit of a complex subject to go in excruciating detail. The
+[RFC](https://tools.ietf.org/html/rfc5545#section-3.8.5) has a lot of examples though.
+
+The hard part is not to write the RRULE, it is to expand them. The most complex and
+hard-to-read code is hidden in this component. Dragons be here.
+
+So, if we have a meeting every 2nd monday of the month, this would be specified as such:
+
+```
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 2.0//EN
+BEGIN:VEVENT
+UID:1102c450-e0d7-11e1-9b23-0800200c9a66
+DTSTART:20120109T140000Z
+RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=2
+END:VEVENT
+END:VCALENDAR
+```
+
+Note that normally it's not allowed to indent the object like this, but it does make
+it easier to read. This is also the first time I added in a UID, which is required
+for all VEVENT, VTODO and VJOURNAL objects!
+
+To figure out all the meetings for this year, we can use the following syntax:
+
+```php
+use OldSabre\VObject;
+
+$calendar = VObject\Reader::read($data);
+$calendar->expand(new DateTime('2012-01-01'), new DateTime('2012-12-31'));
+```
+
+What the expand method does, is look at its inner events, and expand the recurring
+rule. Our calendar now contains 12 events. The first will have its RRULE stripped,
+and every subsequent VEVENT has the correct meeting date and a `RECURRENCE-ID` set.
+
+This results in something like this:
+
+```
+BEGIN:VCALENDAR
+ VERSION:2.0
+ PRODID:-//Sabre//Sabre VObject 2.0//EN
+ BEGIN:VEVENT
+ UID:1102c450-e0d7-11e1-9b23-0800200c9a66
+ DTSTART:20120109T140000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:1102c450-e0d7-11e1-9b23-0800200c9a66
+ RECURRENCE-ID:20120213T140000Z
+ DTSTART:20120213T140000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:1102c450-e0d7-11e1-9b23-0800200c9a66
+ RECURRENCE-ID:20120312T140000Z
+ DTSTART:20120312T140000Z
+ END:VEVENT
+ ..etc..
+END:VCALENDAR
+```
+
+To show the list of dates, we would do this as such:
+
+```php
+foreach($calendar->VEVENT as $event) {
+ echo $event->DTSTART->getDateTime()->format(\DateTime::ATOM);
+}
+```
+
+In a recurring event, single instances can also be overriden. VObject also takes these
+into consideration. The reason we needed to specify a start and end-date, is because
+some recurrence rules can be 'never ending'.
+
+You should make sure you pick a sane date-range. Because if you pick a 50 year
+time-range, for a daily recurring event; this would result in over 18K objects.
+
+Free-busy report generation
+---------------------------
+
+Some calendaring software can make use of FREEBUSY reports to show when people are
+available.
+
+You can automatically generate these reports from calendars using the `FreeBusyGenerator`.
+
+Example based on our last event:
+
+```php
+// We're giving it the calendar object. It's also possible to specify multiple objects,
+// by setting them as an array.
+//
+// We must also specify a start and end date, because recurring events are expanded.
+$fbGenerator = new VObject\FreeBusyGenerator(
+ new DateTime('2012-01-01'),
+ new DateTime('2012-12-31'),
+ $calendar
+);
+
+// Grabbing the report
+$freebusy = $fbGenerator->result();
+
+// The freebusy report is another VCALENDAR object, so we can serialize it as usual:
+echo $freebusy->serialize();
+```
+
+The output of this script will look like this:
+
+```
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 2.0//EN
+CALSCALE:GREGORIAN
+BEGIN:VFREEBUSY
+DTSTART;VALUE=DATE-TIME:20111231T230000Z
+DTEND;VALUE=DATE-TIME:20111231T230000Z
+DTSTAMP;VALUE=DATE-TIME:20120808T131628Z
+FREEBUSY;FBTYPE=BUSY:20120109T140000Z/20120109T140000Z
+FREEBUSY;FBTYPE=BUSY:20120213T140000Z/20120213T140000Z
+FREEBUSY;FBTYPE=BUSY:20120312T140000Z/20120312T140000Z
+FREEBUSY;FBTYPE=BUSY:20120409T140000Z/20120409T140000Z
+FREEBUSY;FBTYPE=BUSY:20120514T140000Z/20120514T140000Z
+FREEBUSY;FBTYPE=BUSY:20120611T140000Z/20120611T140000Z
+FREEBUSY;FBTYPE=BUSY:20120709T140000Z/20120709T140000Z
+FREEBUSY;FBTYPE=BUSY:20120813T140000Z/20120813T140000Z
+FREEBUSY;FBTYPE=BUSY:20120910T140000Z/20120910T140000Z
+FREEBUSY;FBTYPE=BUSY:20121008T140000Z/20121008T140000Z
+FREEBUSY;FBTYPE=BUSY:20121112T140000Z/20121112T140000Z
+FREEBUSY;FBTYPE=BUSY:20121210T140000Z/20121210T140000Z
+END:VFREEBUSY
+END:VCALENDAR
+```
+
+Support
+-------
+
+Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions.
+
+Made at fruux
+-------------
+
+This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json
new file mode 100644
index 0000000..0a83d89
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json
@@ -0,0 +1,31 @@
+{
+ "name": "sabre/vobject",
+ "description" : "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "keywords" : [ "VObject", "iCalendar", "vCard" ],
+ "homepage" : "https://github.com/fruux/sabre-vobject",
+ "license" : "BSD-3-Clause",
+ "require" : {
+ "php" : ">=5.3.1",
+ "ext-mbstring" : "*"
+ },
+ "authors" : [
+ {
+ "name" : "Evert Pot",
+ "email" : "evert@rooftopsolutions.nl",
+ "homepage" : "http://evertpot.com/",
+ "role" : "Developer"
+ }
+ ],
+ "support" : {
+ "forum" : "https://groups.google.com/group/sabredav-discuss",
+ "source" : "https://github.com/fruux/sabre-vobject"
+ },
+ "autoload" : {
+ "psr-0" : {
+ "OldSabre\\VObject" : "lib/"
+ }
+ },
+ "bin" : [
+ "bin/vobjectvalidate.php"
+ ]
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php
new file mode 100644
index 0000000..f4921ac
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php
@@ -0,0 +1,405 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Component
+ *
+ * This class represents a VCALENDAR/VCARD component. A component is for example
+ * VEVENT, VTODO and also VCALENDAR. It starts with BEGIN:COMPONENTNAME and
+ * ends with END:COMPONENTNAME
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Component extends Node {
+
+ /**
+ * Name, for example VEVENT
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Children properties and components
+ *
+ * @var array
+ */
+ public $children = array();
+
+ /**
+ * If components are added to this map, they will be automatically mapped
+ * to their respective classes, if parsed by the reader or constructed with
+ * the 'create' method.
+ *
+ * @var array
+ */
+ static public $classMap = array(
+ 'VALARM' => 'OldSabre\\VObject\\Component\\VAlarm',
+ 'VCALENDAR' => 'OldSabre\\VObject\\Component\\VCalendar',
+ 'VCARD' => 'OldSabre\\VObject\\Component\\VCard',
+ 'VEVENT' => 'OldSabre\\VObject\\Component\\VEvent',
+ 'VJOURNAL' => 'OldSabre\\VObject\\Component\\VJournal',
+ 'VTODO' => 'OldSabre\\VObject\\Component\\VTodo',
+ 'VFREEBUSY' => 'OldSabre\\VObject\\Component\\VFreeBusy',
+ );
+
+ /**
+ * Creates the new component by name, but in addition will also see if
+ * there's a class mapped to the property name.
+ *
+ * @param string $name
+ * @param string $value
+ * @return Component
+ */
+ static public function create($name, $value = null) {
+
+ $name = strtoupper($name);
+
+ if (isset(self::$classMap[$name])) {
+ return new self::$classMap[$name]($name, $value);
+ } else {
+ return new self($name, $value);
+ }
+
+ }
+
+ /**
+ * Creates a new component.
+ *
+ * By default this object will iterate over its own children, but this can
+ * be overridden with the iterator argument
+ *
+ * @param string $name
+ * @param ElementList $iterator
+ */
+ public function __construct($name, ElementList $iterator = null) {
+
+ $this->name = strtoupper($name);
+ if (!is_null($iterator)) $this->iterator = $iterator;
+
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ $str = "BEGIN:" . $this->name . "\r\n";
+
+ /**
+ * Gives a component a 'score' for sorting purposes.
+ *
+ * This is solely used by the childrenSort method.
+ *
+ * A higher score means the item will be lower in the list.
+ * To avoid score collisions, each "score category" has a reasonable
+ * space to accomodate elements. The $key is added to the $score to
+ * preserve the original relative order of elements.
+ *
+ * @param int $key
+ * @param array $array
+ * @return int
+ */
+ $sortScore = function($key, $array) {
+
+ if ($array[$key] instanceof Component) {
+
+ // We want to encode VTIMEZONE first, this is a personal
+ // preference.
+ if ($array[$key]->name === 'VTIMEZONE') {
+ $score=300000000;
+ return $score+$key;
+ } else {
+ $score=400000000;
+ return $score+$key;
+ }
+ } else {
+ // Properties get encoded first
+ // VCARD version 4.0 wants the VERSION property to appear first
+ if ($array[$key] instanceof Property) {
+ if ($array[$key]->name === 'VERSION') {
+ $score=100000000;
+ return $score+$key;
+ } else {
+ // All other properties
+ $score=200000000;
+ return $score+$key;
+ }
+ }
+ }
+
+ };
+
+ $tmp = $this->children;
+ uksort($this->children, function($a, $b) use ($sortScore, $tmp) {
+
+ $sA = $sortScore($a, $tmp);
+ $sB = $sortScore($b, $tmp);
+
+ if ($sA === $sB) return 0;
+
+ return ($sA < $sB) ? -1 : 1;
+
+ });
+
+ foreach($this->children as $child) $str.=$child->serialize();
+ $str.= "END:" . $this->name . "\r\n";
+
+ return $str;
+
+ }
+
+ /**
+ * Adds a new component or element
+ *
+ * You can call this method with the following syntaxes:
+ *
+ * add(Node $node)
+ * add(string $name, $value, array $parameters = array())
+ *
+ * The first version adds an Element
+ * The second adds a property as a string.
+ *
+ * @param mixed $item
+ * @param mixed $itemValue
+ * @return void
+ */
+ public function add($item, $itemValue = null, array $parameters = array()) {
+
+ if ($item instanceof Node) {
+ if (!is_null($itemValue)) {
+ 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)) {
+
+ $item = Property::create($item,$itemValue, $parameters);
+ $item->parent = $this;
+ $this->children[] = $item;
+
+ } else {
+
+ throw new \InvalidArgumentException('The first argument must either be a \\OldSabre\\VObject\\Node or a string');
+
+ }
+
+ }
+
+ /**
+ * Returns an iterable list of children
+ *
+ * @return ElementList
+ */
+ public function children() {
+
+ return new ElementList($this->children);
+
+ }
+
+ /**
+ * Returns an array with elements that match the specified name.
+ *
+ * This function is also aware of MIME-Directory groups (as they appear in
+ * vcards). This means that if a property is grouped as "HOME.EMAIL", it
+ * will also be returned when searching for just "EMAIL". If you want to
+ * search for a property in a specific group, you can select on the entire
+ * string ("HOME.EMAIL"). If you want to search on a specific property that
+ * has not been assigned a group, specify ".EMAIL".
+ *
+ * Keys are retained from the 'children' array, which may be confusing in
+ * certain cases.
+ *
+ * @param string $name
+ * @return array
+ */
+ public function select($name) {
+
+ $group = null;
+ $name = strtoupper($name);
+ if (strpos($name,'.')!==false) {
+ list($group,$name) = explode('.', $name, 2);
+ }
+
+ $result = array();
+ foreach($this->children as $key=>$child) {
+
+ if (
+ strtoupper($child->name) === $name &&
+ (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group))
+ ) {
+
+ $result[$key] = $child;
+
+ }
+ }
+
+ reset($result);
+ return $result;
+
+ }
+
+ /**
+ * This method only returns a list of sub-components. Properties are
+ * ignored.
+ *
+ * @return array
+ */
+ public function getComponents() {
+
+ $result = array();
+ foreach($this->children as $child) {
+ if ($child instanceof Component) {
+ $result[] = $child;
+ }
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * 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 {{{ */
+
+ /**
+ * Using 'get' you will either get a property or component,
+ *
+ * If there were no child-elements found with the specified name,
+ * null is returned.
+ *
+ * @param string $name
+ * @return Property
+ */
+ public function __get($name) {
+
+ $matches = $this->select($name);
+ if (count($matches)===0) {
+ return null;
+ } else {
+ $firstMatch = current($matches);
+ /** @var $firstMatch Property */
+ $firstMatch->setIterator(new ElementList(array_values($matches)));
+ return $firstMatch;
+ }
+
+ }
+
+ /**
+ * This method checks if a sub-element with the specified name exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function __isset($name) {
+
+ $matches = $this->select($name);
+ return count($matches)>0;
+
+ }
+
+ /**
+ * Using the setter method you can add properties or subcomponents
+ *
+ * You can either pass a Component, Property
+ * object, or a string to automatically create a Property.
+ *
+ * If the item already exists, it will be removed. If you want to add
+ * a new item with the same name, always use the add() method.
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ */
+ public function __set($name, $value) {
+
+ $matches = $this->select($name);
+ $overWrite = count($matches)?key($matches):null;
+
+ if ($value instanceof Component || $value instanceof Property) {
+ $value->parent = $this;
+ if (!is_null($overWrite)) {
+ $this->children[$overWrite] = $value;
+ } else {
+ $this->children[] = $value;
+ }
+ } elseif (is_scalar($value)) {
+ $property = Property::create($name,$value);
+ $property->parent = $this;
+ if (!is_null($overWrite)) {
+ $this->children[$overWrite] = $property;
+ } else {
+ $this->children[] = $property;
+ }
+ } else {
+ throw new \InvalidArgumentException('You must pass a \\OldSabre\\VObject\\Component, \\OldSabre\\VObject\\Property or scalar type');
+ }
+
+ }
+
+ /**
+ * Removes all properties and components within this component.
+ *
+ * @param string $name
+ * @return void
+ */
+ public function __unset($name) {
+
+ $matches = $this->select($name);
+ foreach($matches as $k=>$child) {
+
+ unset($this->children[$k]);
+ $child->parent = null;
+
+ }
+
+ }
+
+ /* }}} */
+
+ /**
+ * This method is automatically called when the object is cloned.
+ * Specifically, this will ensure all child elements are also cloned.
+ *
+ * @return void
+ */
+ public function __clone() {
+
+ foreach($this->children as $key=>$child) {
+ $this->children[$key] = clone $child;
+ $this->children[$key]->parent = $this;
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php
new file mode 100644
index 0000000..e5aed86
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+use OldSabre\VObject;
+
+/**
+ * VAlarm component
+ *
+ * This component contains some additional functionality specific for VALARMs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VAlarm extends VObject\Component {
+
+ /**
+ * Returns a DateTime object when this alarm is going to trigger.
+ *
+ * This ignores repeated alarm, only the first trigger is returned.
+ *
+ * @return DateTime
+ */
+ public function getEffectiveTriggerTime() {
+
+ $trigger = $this->TRIGGER;
+ if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') {
+ $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
+ $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START';
+
+ $parentComponent = $this->parent;
+ if ($related === 'START') {
+
+ if ($parentComponent->name === 'VTODO') {
+ $propName = 'DUE';
+ } else {
+ $propName = 'DTSTART';
+ }
+
+ $effectiveTrigger = clone $parentComponent->$propName->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ } else {
+ if ($parentComponent->name === 'VTODO') {
+ $endProp = 'DUE';
+ } elseif ($parentComponent->name === 'VEVENT') {
+ $endProp = 'DTEND';
+ } else {
+ 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)) {
+ $effectiveTrigger = clone $parentComponent->$endProp->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ } elseif (isset($parentComponent->DURATION)) {
+ $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
+ $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION);
+ $effectiveTrigger->add($duration);
+ $effectiveTrigger->add($triggerDuration);
+ } else {
+ $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ }
+ }
+ } else {
+ $effectiveTrigger = $trigger->getDateTime();
+ }
+ return $effectiveTrigger;
+
+ }
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $effectiveTrigger = $this->getEffectiveTriggerTime();
+
+ if (isset($this->DURATION)) {
+ $duration = VObject\DateTimeParser::parseDuration($this->DURATION);
+ $repeat = (string)$this->repeat;
+ if (!$repeat) {
+ $repeat = 1;
+ }
+
+ $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat);
+
+ foreach($period as $occurrence) {
+
+ if ($start <= $occurrence && $end > $occurrence) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return ($start <= $effectiveTrigger && $end > $effectiveTrigger);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php
new file mode 100644
index 0000000..e80871c
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php
@@ -0,0 +1,244 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VCalendar component
+ *
+ * This component adds functionality to a component, specific for a VCALENDAR.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCalendar extends VObject\Document {
+
+ static $defaultName = 'VCALENDAR';
+
+ /**
+ * Returns a list of all 'base components'. For instance, if an Event has
+ * a recurrence rule, and one instance is overridden, the overridden event
+ * will have the same UID, but will be excluded from this list.
+ *
+ * VTIMEZONE components will always be excluded.
+ *
+ * @param string $componentName filter by component name
+ * @return array
+ */
+ public function getBaseComponents($componentName = null) {
+
+ $components = array();
+ foreach($this->children as $component) {
+
+ if (!$component instanceof VObject\Component)
+ continue;
+
+ if (isset($component->{'RECURRENCE-ID'}))
+ continue;
+
+ if ($componentName && $component->name !== strtoupper($componentName))
+ continue;
+
+ if ($component->name === 'VTIMEZONE')
+ continue;
+
+ $components[] = $component;
+
+ }
+
+ return $components;
+
+ }
+
+ /**
+ * If this calendar object, has events with recurrence rules, this method
+ * can be used to expand the event into multiple sub-events.
+ *
+ * Each event will be stripped from it's recurrence information, and only
+ * the instances of the event in the specified timerange will be left
+ * alone.
+ *
+ * In addition, this method will cause timezone information to be stripped,
+ * and normalized to UTC.
+ *
+ * This method will alter the VCalendar. This cannot be reversed.
+ *
+ * This functionality is specifically used by the CalDAV standard. It is
+ * possible for clients to request expand events, if they are rather simple
+ * clients and do not have the possibility to calculate recurrences.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return void
+ */
+ public function expand(\DateTime $start, \DateTime $end) {
+
+ $newEvents = array();
+
+ foreach($this->select('VEVENT') as $key=>$vevent) {
+
+ if (isset($vevent->{'RECURRENCE-ID'})) {
+ unset($this->children[$key]);
+ continue;
+ }
+
+
+ if (!$vevent->rrule) {
+ unset($this->children[$key]);
+ if ($vevent->isInTimeRange($start, $end)) {
+ $newEvents[] = $vevent;
+ }
+ continue;
+ }
+
+ $uid = (string)$vevent->uid;
+ if (!$uid) {
+ throw new \LogicException('Event did not have a UID!');
+ }
+
+ $it = new VObject\RecurrenceIterator($this, $vevent->uid);
+ $it->fastForward($start);
+
+ while($it->valid() && $it->getDTStart() < $end) {
+
+ if ($it->getDTEnd() > $start) {
+
+ $newEvents[] = $it->getEventObject();
+
+ }
+ $it->next();
+
+ }
+ unset($this->children[$key]);
+
+ }
+
+ foreach($newEvents as $newEvent) {
+
+ foreach($newEvent->children as $child) {
+ if ($child instanceof VObject\Property\DateTime &&
+ $child->getDateType() == VObject\Property\DateTime::LOCALTZ) {
+ $child->setDateTime($child->getDateTime(),VObject\Property\DateTime::UTC);
+ }
+ }
+
+ $this->add($newEvent);
+
+ }
+
+ // Removing all VTIMEZONE components
+ unset($this->VTIMEZONE);
+
+ }
+
+ /**
+ * 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/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php
new file mode 100644
index 0000000..8a50f38
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VCard component
+ *
+ * This component represents the BEGIN:VCARD and END:VCARD found in every
+ * vcard.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCard extends VObject\Component {
+
+ static $defaultName = 'VCARD';
+
+ /**
+ * VCards with version 2.1, 3.0 and 4.0 are found.
+ *
+ * If the VCARD doesn't know its version, 4.0 is assumed.
+ */
+ const DEFAULT_VERSION = '4.0';
+
+ /**
+ * 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();
+
+ $version = $this->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';
+ }
+ }
+
+ }
+ $fn = $this->select('FN');
+ if (count($fn)!==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($fn) === 0) {
+ // We're going to try to see if we can use the contents of the
+ // N property.
+ if (isset($this->N)) {
+ $value = explode(';', (string)$this->N);
+ if (isset($value[1]) && $value[1]) {
+ $this->FN = $value[1] . ' ' . $value[0];
+ } else {
+ $this->FN = $value[0];
+ }
+
+ // 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/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php
new file mode 100644
index 0000000..a8af08f
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+use OldSabre\VObject;
+
+/**
+ * VEvent component
+ *
+ * This component contains some additional functionality specific for VEVENT's.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VEvent extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ if ($this->RRULE) {
+ $it = new VObject\RecurrenceIterator($this);
+ $it->fastForward($start);
+
+ // We fast-forwarded to a spot where the end-time of the
+ // recurrence instance exceeded the start of the requested
+ // time-range.
+ //
+ // If the starttime of the recurrence did not exceed the
+ // end of the time range as well, we have a match.
+ return ($it->getDTStart() < $end && $it->getDTEnd() > $start);
+
+ }
+
+ $effectiveStart = $this->DTSTART->getDateTime();
+ if (isset($this->DTEND)) {
+
+ // The DTEND property is considered non inclusive. So for a 3 day
+ // event in july, dtstart and dtend would have to be July 1st and
+ // July 4th respectively.
+ //
+ // See:
+ // http://tools.ietf.org/html/rfc5545#page-54
+ $effectiveEnd = $this->DTEND->getDateTime();
+
+ } elseif (isset($this->DURATION)) {
+ $effectiveEnd = clone $effectiveStart;
+ $effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) );
+ } elseif ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
+ $effectiveEnd = clone $effectiveStart;
+ $effectiveEnd->modify('+1 day');
+ } else {
+ $effectiveEnd = clone $effectiveStart;
+ }
+ return (
+ ($start <= $effectiveEnd) && ($end > $effectiveStart)
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php
new file mode 100644
index 0000000..abb622b
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VFreeBusy component
+ *
+ * This component adds functionality to a component, specific for VFREEBUSY
+ * components.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VFreeBusy extends VObject\Component {
+
+ /**
+ * Checks based on the contained FREEBUSY information, if a timeslot is
+ * available.
+ *
+ * @param DateTime $start
+ * @param Datetime $end
+ * @return bool
+ */
+ public function isFree(\DateTime $start, \Datetime $end) {
+
+ foreach($this->select('FREEBUSY') as $freebusy) {
+
+ // We are only interested in FBTYPE=BUSY (the default),
+ // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE.
+ if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') {
+ continue;
+ }
+
+ // The freebusy component can hold more than 1 value, separated by
+ // commas.
+ $periods = explode(',', (string)$freebusy);
+
+ foreach($periods as $period) {
+ // Every period is formatted as [start]/[end]. The start is an
+ // absolute UTC time, the end may be an absolute UTC time, or
+ // duration (relative) value.
+ list($busyStart, $busyEnd) = explode('/', $period);
+
+ $busyStart = VObject\DateTimeParser::parse($busyStart);
+ $busyEnd = VObject\DateTimeParser::parse($busyEnd);
+ if ($busyEnd instanceof \DateInterval) {
+ $tmp = clone $busyStart;
+ $tmp->add($busyEnd);
+ $busyEnd = $tmp;
+ }
+
+ if($start < $busyEnd && $end > $busyStart) {
+ return false;
+ }
+
+ }
+
+ }
+
+ return true;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php
new file mode 100644
index 0000000..25f1593
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * VJournal component
+ *
+ * This component contains some additional functionality specific for VJOURNALs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VJournal extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
+ if ($dtstart) {
+ $effectiveEnd = clone $dtstart;
+ if ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
+ $effectiveEnd->modify('+1 day');
+ }
+
+ return ($start <= $effectiveEnd && $end > $dtstart);
+
+ }
+ return false;
+
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php
new file mode 100644
index 0000000..c1a3eee
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * VTodo component
+ *
+ * This component contains some additional functionality specific for VTODOs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VTodo extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
+ $duration = isset($this->DURATION)?VObject\DateTimeParser::parseDuration($this->DURATION):null;
+ $due = isset($this->DUE)?$this->DUE->getDateTime():null;
+ $completed = isset($this->COMPLETED)?$this->COMPLETED->getDateTime():null;
+ $created = isset($this->CREATED)?$this->CREATED->getDateTime():null;
+
+ if ($dtstart) {
+ if ($duration) {
+ $effectiveEnd = clone $dtstart;
+ $effectiveEnd->add($duration);
+ return $start <= $effectiveEnd && $end > $dtstart;
+ } elseif ($due) {
+ return
+ ($start < $due || $start <= $dtstart) &&
+ ($end > $dtstart || $end >= $due);
+ } else {
+ return $start <= $dtstart && $end > $dtstart;
+ }
+ }
+ if ($due) {
+ return ($start < $due && $end >= $due);
+ }
+ if ($completed && $created) {
+ return
+ ($start <= $created || $start <= $completed) &&
+ ($end >= $created || $end >= $completed);
+ }
+ if ($completed) {
+ return ($start <= $completed && $end >= $completed);
+ }
+ if ($created) {
+ return ($end > $created);
+ }
+ return true;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php
new file mode 100644
index 0000000..1b388fa
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * DateTimeParser
+ *
+ * This class is responsible for parsing the several different date and time
+ * formats iCalendar and vCards have.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class DateTimeParser {
+
+ /**
+ * Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object
+ *
+ * Specifying a reference timezone is optional. It will only be used
+ * if the non-UTC format is used. The argument is used as a reference, the
+ * returned DateTime object will still be in the UTC timezone.
+ *
+ * @param string $dt
+ * @param DateTimeZone $tz
+ * @return DateTime
+ */
+ static public function parseDateTime($dt,\DateTimeZone $tz = null) {
+
+ // Format is YYYYMMDD + "T" + hhmmss
+ $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/',$dt,$matches);
+
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar datetime value is incorrect: ' . $dt);
+ }
+
+ if ($matches[7]==='Z' || is_null($tz)) {
+ $tz = new \DateTimeZone('UTC');
+ }
+ $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] .':' . $matches[6], $tz);
+
+ // Still resetting the timezone, to normalize everything to UTC
+ $date->setTimeZone(new \DateTimeZone('UTC'));
+ return $date;
+
+ }
+
+ /**
+ * Parses an iCalendar (rfc5545) formatted date and returns a DateTime object
+ *
+ * @param string $date
+ * @return DateTime
+ */
+ static public function parseDate($date) {
+
+ // Format is YYYYMMDD
+ $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches);
+
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date);
+ }
+
+ $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new \DateTimeZone('UTC'));
+ return $date;
+
+ }
+
+ /**
+ * Parses an iCalendar (RFC5545) formatted duration value.
+ *
+ * This method will either return a DateTimeInterval object, or a string
+ * suitable for strtotime or DateTime::modify.
+ *
+ * @param string $duration
+ * @param bool $asString
+ * @return DateInterval|string
+ */
+ static public function parseDuration($duration, $asString = false) {
+
+ $result = preg_match('/^(?P<plusminus>\+|-)?P((?P<week>\d+)W)?((?P<day>\d+)D)?(T((?P<hour>\d+)H)?((?P<minute>\d+)M)?((?P<second>\d+)S)?)?$/', $duration, $matches);
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration);
+ }
+
+ if (!$asString) {
+ $invert = false;
+ if ($matches['plusminus']==='-') {
+ $invert = true;
+ }
+
+
+ $parts = array(
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ );
+ foreach($parts as $part) {
+ $matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0;
+ }
+
+
+ // We need to re-construct the $duration string, because weeks and
+ // days are not supported by DateInterval in the same string.
+ $duration = 'P';
+ $days = $matches['day'];
+ if ($matches['week']) {
+ $days+=$matches['week']*7;
+ }
+ if ($days)
+ $duration.=$days . 'D';
+
+ if ($matches['minute'] || $matches['second'] || $matches['hour']) {
+ $duration.='T';
+
+ if ($matches['hour'])
+ $duration.=$matches['hour'].'H';
+
+ if ($matches['minute'])
+ $duration.=$matches['minute'].'M';
+
+ if ($matches['second'])
+ $duration.=$matches['second'].'S';
+
+ }
+
+ if ($duration==='P') {
+ $duration = 'PT0S';
+ }
+ $iv = new \DateInterval($duration);
+ if ($invert) $iv->invert = true;
+
+ return $iv;
+
+ }
+
+
+
+ $parts = array(
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ );
+
+ $newDur = '';
+ foreach($parts as $part) {
+ if (isset($matches[$part]) && $matches[$part]) {
+ $newDur.=' '.$matches[$part] . ' ' . $part . 's';
+ }
+ }
+
+ $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur);
+ if ($newDur === '+') { $newDur = '+0 seconds'; };
+ return $newDur;
+
+ }
+
+ /**
+ * Parses either a Date or DateTime, or Duration value.
+ *
+ * @param string $date
+ * @param DateTimeZone|string $referenceTZ
+ * @return DateTime|DateInterval
+ */
+ static public function parse($date, $referenceTZ = null) {
+
+ if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) {
+ return self::parseDuration($date);
+ } elseif (strlen($date)===8) {
+ return self::parseDate($date);
+ } else {
+ return self::parseDateTime($date, $referenceTZ);
+ }
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php
new file mode 100644
index 0000000..b29d971
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Document
+ *
+ * A document is just like a component, except that it's also the top level
+ * element.
+ *
+ * Both a VCALENDAR and a VCARD are considered documents.
+ *
+ * This class also provides a registry for document types.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+abstract class Document extends Component {
+
+ /**
+ * The default name for this component.
+ *
+ * This should be 'VCALENDAR' or 'VCARD'.
+ *
+ * @var string
+ */
+ static $defaultName;
+
+ /**
+ * Creates a new document.
+ *
+ * We're changing the default behavior slightly here. First, we don't want
+ * to have to specify a name (we already know it), and we want to allow
+ * children to be specified in the first argument.
+ *
+ * But, the default behavior also works.
+ *
+ * So the two sigs:
+ *
+ * new Document(array $children = array());
+ * new Document(string $name, array $children = array())
+ *
+ * @return void
+ */
+ public function __construct() {
+
+ $args = func_get_args();
+ if (count($args)===0 || is_array($args[0])) {
+ array_unshift($args, static::$defaultName);
+ call_user_func_array(array('parent', '__construct'), $args);
+ } else {
+ call_user_func_array(array('parent', '__construct'), $args);
+ }
+
+ }
+
+ /**
+ * Creates a new component
+ *
+ * This method automatically searches for the correct component class, based
+ * on its name.
+ *
+ * You can specify the children either in key=>value syntax, in which case
+ * properties will automatically be created, or you can just pass a list of
+ * Component and Property object.
+ *
+ * @param string $name
+ * @param array $children
+ * @return Component
+ */
+ public function createComponent($name, array $children = array()) {
+
+ $component = Component::create($name);
+ foreach($children as $k=>$v) {
+
+ if ($v instanceof Node) {
+ $component->add($v);
+ } else {
+ $component->add($k, $v);
+ }
+
+ }
+ return $component;
+
+ }
+
+ /**
+ * Factory method for creating new properties
+ *
+ * This method automatically searches for the correct property class, based
+ * on its name.
+ *
+ * You can specify the parameters either in key=>value syntax, in which case
+ * parameters will automatically be created, or you can just pass a list of
+ * Parameter objects.
+ *
+ * @param string $name
+ * @param mixed $value
+ * @param array $parameters
+ * @return Property
+ */
+ public function createProperty($name, $value = null, array $parameters = array()) {
+
+ return Property::create($name, $value, $parameters);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php
new file mode 100644
index 0000000..317d784
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject ElementList
+ *
+ * This class represents a list of elements. Lists are the result of queries,
+ * such as doing $vcalendar->vevent where there's multiple VEVENT objects.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ElementList implements \Iterator, \Countable, \ArrayAccess {
+
+ /**
+ * Inner elements
+ *
+ * @var array
+ */
+ protected $elements = array();
+
+ /**
+ * Creates the element list.
+ *
+ * @param array $elements
+ */
+ public function __construct(array $elements) {
+
+ $this->elements = $elements;
+
+ }
+
+ /* {{{ Iterator interface */
+
+ /**
+ * Current position
+ *
+ * @var int
+ */
+ private $key = 0;
+
+ /**
+ * Returns current item in iteration
+ *
+ * @return Element
+ */
+ public function current() {
+
+ return $this->elements[$this->key];
+
+ }
+
+ /**
+ * To the next item in the iterator
+ *
+ * @return void
+ */
+ public function next() {
+
+ $this->key++;
+
+ }
+
+ /**
+ * Returns the current iterator key
+ *
+ * @return int
+ */
+ public function key() {
+
+ return $this->key;
+
+ }
+
+ /**
+ * Returns true if the current position in the iterator is a valid one
+ *
+ * @return bool
+ */
+ public function valid() {
+
+ return isset($this->elements[$this->key]);
+
+ }
+
+ /**
+ * Rewinds the iterator
+ *
+ * @return void
+ */
+ public function rewind() {
+
+ $this->key = 0;
+
+ }
+
+ /* }}} */
+
+ /* {{{ Countable interface */
+
+ /**
+ * Returns the number of elements
+ *
+ * @return int
+ */
+ public function count() {
+
+ return count($this->elements);
+
+ }
+
+ /* }}} */
+
+ /* {{{ ArrayAccess Interface */
+
+
+ /**
+ * Checks if an item exists through ArrayAccess.
+ *
+ * @param int $offset
+ * @return bool
+ */
+ public function offsetExists($offset) {
+
+ return isset($this->elements[$offset]);
+
+ }
+
+ /**
+ * Gets an item through ArrayAccess.
+ *
+ * @param int $offset
+ * @return mixed
+ */
+ public function offsetGet($offset) {
+
+ return $this->elements[$offset];
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * @param int $offset
+ * @param mixed $value
+ * @return void
+ */
+ public function offsetSet($offset,$value) {
+
+ throw new \LogicException('You can not add new objects to an ElementList');
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return void
+ */
+ public function offsetUnset($offset) {
+
+ throw new \LogicException('You can not remove objects from an ElementList');
+
+ }
+
+ /* }}} */
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php
new file mode 100644
index 0000000..a7a8ef3
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php
@@ -0,0 +1,322 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class helps with generating FREEBUSY reports based on existing sets of
+ * objects.
+ *
+ * It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
+ * generates a single VFREEBUSY object.
+ *
+ * VFREEBUSY components are described in RFC5545, The rules for what should
+ * go in a single freebusy report is taken from RFC4791, section 7.10.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class FreeBusyGenerator {
+
+ /**
+ * Input objects
+ *
+ * @var array
+ */
+ protected $objects;
+
+ /**
+ * Start of range
+ *
+ * @var DateTime|null
+ */
+ protected $start;
+
+ /**
+ * End of range
+ *
+ * @var DateTime|null
+ */
+ protected $end;
+
+ /**
+ * VCALENDAR object
+ *
+ * @var Component
+ */
+ protected $baseObject;
+
+ /**
+ * Creates the generator.
+ *
+ * Check the setTimeRange and setObjects methods for details about the
+ * arguments.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @param mixed $objects
+ * @return void
+ */
+ public function __construct(\DateTime $start = null, \DateTime $end = null, $objects = null) {
+
+ if ($start && $end) {
+ $this->setTimeRange($start, $end);
+ }
+
+ if ($objects) {
+ $this->setObjects($objects);
+ }
+
+ }
+
+ /**
+ * Sets the VCALENDAR object.
+ *
+ * If this is set, it will not be generated for you. You are responsible
+ * for setting things like the METHOD, CALSCALE, VERSION, etc..
+ *
+ * The VFREEBUSY object will be automatically added though.
+ *
+ * @param Component $vcalendar
+ * @return void
+ */
+ public function setBaseObject(Component $vcalendar) {
+
+ $this->baseObject = $vcalendar;
+
+ }
+
+ /**
+ * Sets the input objects
+ *
+ * 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 mixed $objects
+ * @return void
+ */
+ public function setObjects($objects) {
+
+ if (!is_array($objects)) {
+ $objects = array($objects);
+ }
+
+ $this->objects = array();
+ foreach($objects as $object) {
+
+ if (is_string($object)) {
+ $this->objects[] = Reader::read($object);
+ } elseif ($object instanceof Component) {
+ $this->objects[] = $object;
+ } else {
+ throw new \InvalidArgumentException('You can only pass strings or \\OldSabre\\VObject\\Component arguments to setObjects');
+ }
+
+ }
+
+ }
+
+ /**
+ * Sets the time range
+ *
+ * Any freebusy object falling outside of this time range will be ignored.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return void
+ */
+ public function setTimeRange(\DateTime $start = null, \DateTime $end = null) {
+
+ $this->start = $start;
+ $this->end = $end;
+
+ }
+
+ /**
+ * Parses the input data and returns a correct VFREEBUSY object, wrapped in
+ * a VCALENDAR.
+ *
+ * @return Component
+ */
+ public function getResult() {
+
+ $busyTimes = array();
+
+ foreach($this->objects as $object) {
+
+ foreach($object->getBaseComponents() as $component) {
+
+ switch($component->name) {
+
+ case 'VEVENT' :
+
+ $FBTYPE = 'BUSY';
+ if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) {
+ break;
+ }
+ if (isset($component->STATUS)) {
+ $status = strtoupper($component->STATUS);
+ if ($status==='CANCELLED') {
+ break;
+ }
+ if ($status==='TENTATIVE') {
+ $FBTYPE = 'BUSY-TENTATIVE';
+ }
+ }
+
+ $times = array();
+
+ if ($component->RRULE) {
+
+ $iterator = new RecurrenceIterator($object, (string)$component->uid);
+ if ($this->start) {
+ $iterator->fastForward($this->start);
+ }
+
+ $maxRecurrences = 200;
+
+ while($iterator->valid() && --$maxRecurrences) {
+
+ $startTime = $iterator->getDTStart();
+ if ($this->end && $startTime > $this->end) {
+ break;
+ }
+ $times[] = array(
+ $iterator->getDTStart(),
+ $iterator->getDTEnd(),
+ );
+
+ $iterator->next();
+
+ }
+
+ } else {
+
+ $startTime = $component->DTSTART->getDateTime();
+ if ($this->end && $startTime > $this->end) {
+ break;
+ }
+ $endTime = null;
+ if (isset($component->DTEND)) {
+ $endTime = $component->DTEND->getDateTime();
+ } elseif (isset($component->DURATION)) {
+ $duration = DateTimeParser::parseDuration((string)$component->DURATION);
+ $endTime = clone $startTime;
+ $endTime->add($duration);
+ } elseif ($component->DTSTART->getDateType() === Property\DateTime::DATE) {
+ $endTime = clone $startTime;
+ $endTime->modify('+1 day');
+ } else {
+ // The event had no duration (0 seconds)
+ break;
+ }
+
+ $times[] = array($startTime, $endTime);
+
+ }
+
+ foreach($times as $time) {
+
+ if ($this->end && $time[0] > $this->end) break;
+ if ($this->start && $time[1] < $this->start) break;
+
+ $busyTimes[] = array(
+ $time[0],
+ $time[1],
+ $FBTYPE,
+ );
+ }
+ break;
+
+ case 'VFREEBUSY' :
+ foreach($component->FREEBUSY as $freebusy) {
+
+ $fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY';
+
+ // Skipping intervals marked as 'free'
+ if ($fbType==='FREE')
+ continue;
+
+ $values = explode(',', $freebusy);
+ foreach($values as $value) {
+ list($startTime, $endTime) = explode('/', $value);
+ $startTime = DateTimeParser::parseDateTime($startTime);
+
+ if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') {
+ $duration = DateTimeParser::parseDuration($endTime);
+ $endTime = clone $startTime;
+ $endTime->add($duration);
+ } else {
+ $endTime = DateTimeParser::parseDateTime($endTime);
+ }
+
+ if($this->start && $this->start > $endTime) continue;
+ if($this->end && $this->end < $startTime) continue;
+ $busyTimes[] = array(
+ $startTime,
+ $endTime,
+ $fbType
+ );
+
+ }
+
+
+ }
+ break;
+
+
+
+ }
+
+
+ }
+
+ }
+
+ if ($this->baseObject) {
+ $calendar = $this->baseObject;
+ } else {
+ $calendar = Component::create('VCALENDAR');
+ $calendar->version = '2.0';
+ $calendar->prodid = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN';
+ $calendar->calscale = 'GREGORIAN';
+ }
+
+ $vfreebusy = Component::create('VFREEBUSY');
+ $calendar->add($vfreebusy);
+
+ if ($this->start) {
+ $dtstart = Property::create('DTSTART');
+ $dtstart->setDateTime($this->start,Property\DateTime::UTC);
+ $vfreebusy->add($dtstart);
+ }
+ if ($this->end) {
+ $dtend = Property::create('DTEND');
+ $dtend->setDateTime($this->end,Property\DateTime::UTC);
+ $vfreebusy->add($dtend);
+ }
+ $dtstamp = Property::create('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'));
+
+ $prop = Property::create(
+ 'FREEBUSY',
+ $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z')
+ );
+ $prop['FBTYPE'] = $busyTime[2];
+ $vfreebusy->add($prop);
+
+ }
+
+ return $calendar;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php
new file mode 100644
index 0000000..b9e265b
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Base class for all nodes
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
+
+ /**
+ * The following constants are used by the validate() method.
+ */
+ const REPAIR = 1;
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ abstract function serialize();
+
+ /**
+ * Iterator override
+ *
+ * @var ElementList
+ */
+ protected $iterator = null;
+
+ /**
+ * A link to the parent node
+ *
+ * @var Node
+ */
+ public $parent = null;
+
+ /**
+ * 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) {
+
+ return array();
+
+ }
+
+ /* {{{ IteratorAggregator interface */
+
+ /**
+ * Returns the iterator for this object
+ *
+ * @return ElementList
+ */
+ public function getIterator() {
+
+ if (!is_null($this->iterator))
+ return $this->iterator;
+
+ return new ElementList(array($this));
+
+ }
+
+ /**
+ * Sets the overridden iterator
+ *
+ * Note that this is not actually part of the iterator interface
+ *
+ * @param ElementList $iterator
+ * @return void
+ */
+ public function setIterator(ElementList $iterator) {
+
+ $this->iterator = $iterator;
+
+ }
+
+ /* }}} */
+
+ /* {{{ Countable interface */
+
+ /**
+ * Returns the number of elements
+ *
+ * @return int
+ */
+ public function count() {
+
+ $it = $this->getIterator();
+ return $it->count();
+
+ }
+
+ /* }}} */
+
+ /* {{{ ArrayAccess Interface */
+
+
+ /**
+ * Checks if an item exists through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return bool
+ */
+ public function offsetExists($offset) {
+
+ $iterator = $this->getIterator();
+ return $iterator->offsetExists($offset);
+
+ }
+
+ /**
+ * Gets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return mixed
+ */
+ public function offsetGet($offset) {
+
+ $iterator = $this->getIterator();
+ return $iterator->offsetGet($offset);
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @param mixed $value
+ * @return void
+ */
+ public function offsetSet($offset,$value) {
+
+ $iterator = $this->getIterator();
+ $iterator->offsetSet($offset,$value);
+
+ // @codeCoverageIgnoreStart
+ //
+ // This method always throws an exception, so we ignore the closing
+ // brace
+ }
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return void
+ */
+ public function offsetUnset($offset) {
+
+ $iterator = $this->getIterator();
+ $iterator->offsetUnset($offset);
+
+ // @codeCoverageIgnoreStart
+ //
+ // This method always throws an exception, so we ignore the closing
+ // brace
+ }
+ // @codeCoverageIgnoreEnd
+
+ /* }}} */
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php
new file mode 100644
index 0000000..43d2433
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Parameter
+ *
+ * This class represents a parameter. A parameter is always tied to a property.
+ * In the case of:
+ * DTSTART;VALUE=DATE:20101108
+ * VALUE=DATE would be the parameter name and value.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Parameter extends Node {
+
+ /**
+ * Parameter name
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Parameter value
+ *
+ * @var string
+ */
+ public $value;
+
+ /**
+ * Sets up the object
+ *
+ * @param string $name
+ * @param string $value
+ */
+ public function __construct($name, $value = null) {
+
+ if (!is_scalar($value) && !is_null($value)) {
+ throw new \InvalidArgumentException('The value argument must be a scalar value or null');
+ }
+
+ $this->name = strtoupper($name);
+ $this->value = $value;
+
+ }
+
+ /**
+ * Returns the parameter's internal value.
+ *
+ * @return string
+ */
+ public function getValue() {
+
+ return $this->value;
+
+ }
+
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ if (is_null($this->value)) {
+ return $this->name;
+ }
+ $src = array(
+ '\\',
+ "\n",
+ ';',
+ ',',
+ );
+ $out = array(
+ '\\\\',
+ '\n',
+ '\;',
+ '\,',
+ );
+
+ return $this->name . '=' . str_replace($src, $out, $this->value);
+
+ }
+
+ /**
+ * Called when this object is being cast to a string
+ *
+ * @return string
+ */
+ public function __toString() {
+
+ return $this->value;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php
new file mode 100644
index 0000000..8e24d78
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Exception thrown by Reader if an invalid object was attempted to be parsed.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ParseException extends \Exception { }
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php
new file mode 100644
index 0000000..ad99f59
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php
@@ -0,0 +1,442 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Property
+ *
+ * A property in VObject is usually in the form PARAMNAME:paramValue.
+ * An example is : SUMMARY:Weekly meeting
+ *
+ * Properties can also have parameters:
+ * SUMMARY;LANG=en:Weekly meeting.
+ *
+ * Parameters can be accessed using the ArrayAccess interface.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Property extends Node {
+
+ /**
+ * Propertyname
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Group name
+ *
+ * This may be something like 'HOME' for vcards.
+ *
+ * @var string
+ */
+ public $group;
+
+ /**
+ * Property parameters
+ *
+ * @var array
+ */
+ public $parameters = array();
+
+ /**
+ * Property value
+ *
+ * @var string
+ */
+ public $value;
+
+ /**
+ * If properties are added to this map, they will be automatically mapped
+ * to their respective classes, if parsed by the reader or constructed with
+ * the 'create' method.
+ *
+ * @var array
+ */
+ static public $classMap = array(
+ 'COMPLETED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'CREATED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTEND' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTSTAMP' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTSTART' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DUE' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'EXDATE' => 'OldSabre\\VObject\\Property\\MultiDateTime',
+ 'LAST-MODIFIED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'RECURRENCE-ID' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'TRIGGER' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'N' => 'OldSabre\\VObject\\Property\\Compound',
+ 'ORG' => 'OldSabre\\VObject\\Property\\Compound',
+ 'ADR' => 'OldSabre\\VObject\\Property\\Compound',
+ 'CATEGORIES' => 'OldSabre\\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
+ * @param array $parameters
+ * @return Property
+ */
+ static public function create($name, $value = null, array $parameters = array()) {
+
+ $name = strtoupper($name);
+ $shortName = $name;
+ $group = null;
+ if (strpos($shortName,'.')!==false) {
+ list($group, $shortName) = explode('.', $shortName);
+ }
+
+ if (isset(self::$classMap[$shortName])) {
+ return new self::$classMap[$shortName]($name, $value, $parameters);
+ } else {
+ return new self($name, $value, $parameters);
+ }
+
+ }
+
+ /**
+ * Creates a new property object
+ *
+ * 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 array $parameters
+ */
+ 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;
+ if (strpos($name,'.')!==false) {
+ list($group, $name) = explode('.', $name);
+ }
+ $this->name = $name;
+ $this->group = $group;
+ $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
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setValue($value) {
+
+ $this->value = $value;
+
+ }
+
+ /**
+ * Returns the internal value
+ *
+ * @param string $value
+ * @return string
+ */
+ public function getValue() {
+
+ return $this->value;
+
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ $str = $this->name;
+ if ($this->group) $str = $this->group . '.' . $this->name;
+
+ foreach($this->parameters as $param) {
+
+ $str.=';' . $param->serialize();
+
+ }
+
+ $src = array(
+ '\\',
+ "\n",
+ );
+ $out = array(
+ '\\\\',
+ '\n',
+ );
+ $str.=':' . str_replace($src, $out, $this->value);
+
+ $out = '';
+ while(strlen($str)>0) {
+ if (strlen($str)>75) {
+ $out.= mb_strcut($str,0,75,'utf-8') . "\r\n";
+ $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8');
+ } else {
+ $out.=$str . "\r\n";
+ $str='';
+ break;
+ }
+ }
+
+ return $out;
+
+ }
+
+ /**
+ * Adds a new componenten or element
+ *
+ * You can call this method with the following syntaxes:
+ *
+ * add(Parameter $element)
+ * add(string $name, $value)
+ *
+ * The first version adds an Parameter
+ * The second adds a property as a string.
+ *
+ * @param mixed $item
+ * @param mixed $itemValue
+ * @return void
+ */
+ public function add($item, $itemValue = null) {
+
+ if ($item instanceof Parameter) {
+ if (!is_null($itemValue)) {
+ throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject');
+ }
+ $item->parent = $this;
+ $this->parameters[] = $item;
+ } elseif(is_string($item)) {
+
+ $parameter = new Parameter($item,$itemValue);
+ $parameter->parent = $this;
+ $this->parameters[] = $parameter;
+
+ } else {
+
+ throw new \InvalidArgumentException('The first argument must either be a Node a string');
+
+ }
+
+ }
+
+ /* ArrayAccess interface {{{ */
+
+ /**
+ * Checks if an array element exists
+ *
+ * @param mixed $name
+ * @return bool
+ */
+ public function offsetExists($name) {
+
+ if (is_int($name)) return parent::offsetExists($name);
+
+ $name = strtoupper($name);
+
+ foreach($this->parameters as $parameter) {
+ if ($parameter->name == $name) return true;
+ }
+ return false;
+
+ }
+
+ /**
+ * Returns a parameter, or parameter list.
+ *
+ * @param string $name
+ * @return Node
+ */
+ public function offsetGet($name) {
+
+ if (is_int($name)) return parent::offsetGet($name);
+ $name = strtoupper($name);
+
+ $result = array();
+ foreach($this->parameters as $parameter) {
+ if ($parameter->name == $name)
+ $result[] = $parameter;
+ }
+
+ if (count($result)===0) {
+ return null;
+ } elseif (count($result)===1) {
+ return $result[0];
+ } else {
+ $result[0]->setIterator(new ElementList($result));
+ return $result[0];
+ }
+
+ }
+
+ /**
+ * Creates a new parameter
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ */
+ public function 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.');
+
+ $this->offsetUnset($name);
+ $parameter = new Parameter($name, $value);
+ $parameter->parent = $this;
+ $this->parameters[] = $parameter;
+
+ } elseif ($value instanceof Parameter) {
+ if (!is_null($name))
+ throw new \InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a \\OldSabre\\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');
+ }
+
+ }
+
+ /**
+ * Removes one or more parameters with the specified name
+ *
+ * @param string $name
+ * @return void
+ */
+ public function offsetUnset($name) {
+
+ if (is_int($name)) parent::offsetUnset($name);
+ $name = strtoupper($name);
+
+ foreach($this->parameters as $key=>$parameter) {
+ if ($parameter->name == $name) {
+ $parameter->parent = null;
+ unset($this->parameters[$key]);
+ }
+
+ }
+
+ }
+
+ /* }}} */
+
+ /**
+ * Called when this object is being cast to a string
+ *
+ * @return string
+ */
+ public function __toString() {
+
+ return (string)$this->value;
+
+ }
+
+ /**
+ * This method is automatically called when the object is cloned.
+ * Specifically, this will ensure all child elements are also cloned.
+ *
+ * @return void
+ */
+ public function __clone() {
+
+ foreach($this->parameters as $key=>$child) {
+ $this->parameters[$key] = clone $child;
+ $this->parameters[$key]->parent = $this;
+ }
+
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ $warnings = 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/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php
new file mode 100644
index 0000000..e46946e
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * Compound property.
+ *
+ * This class adds (de)serialization of compound properties to/from arrays.
+ *
+ * Currently the following properties from RFC 6350 are mapped to use this
+ * class:
+ *
+ * N: Section 6.2.2
+ * ADR: Section 6.3.1
+ * ORG: Section 6.6.4
+ * CATEGORIES: Section 6.7.1
+ *
+ * In order to use this correctly, you must call setParts and getParts to
+ * retrieve and modify dates respectively.
+ *
+ * @author Thomas Tanghus (http://tanghus.net/)
+ * @author Lars Kneschke
+ * @author Evert Pot (http://evertpot.com/)
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Compound extends VObject\Property {
+
+ /**
+ * If property names are added to this map, they will be (de)serialised as arrays
+ * using the getParts() and setParts() methods.
+ * The keys are the property names, values are delimiter chars.
+ *
+ * @var array
+ */
+ static public $delimiterMap = array(
+ 'N' => ';',
+ '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("/(?<!\\\)$delimiter/", $this->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/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php
new file mode 100644
index 0000000..36299c9
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php
@@ -0,0 +1,245 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * DateTime property
+ *
+ * This element is used for iCalendar properties such as the DTSTART property.
+ * It basically provides a few helper functions that make it easier to deal
+ * with these. It supports both DATE-TIME and DATE values.
+ *
+ * In order to use this correctly, you must call setDateTime and getDateTime to
+ * retrieve and modify dates respectively.
+ *
+ * If you use the 'value' or properties directly, this object does not keep
+ * reference and results might appear incorrectly.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class DateTime extends VObject\Property {
+
+ /**
+ * Local 'floating' time
+ */
+ const LOCAL = 1;
+
+ /**
+ * UTC-based time
+ */
+ const UTC = 2;
+
+ /**
+ * Local time plus timezone
+ */
+ const LOCALTZ = 3;
+
+ /**
+ * Only a date, time is ignored
+ */
+ const DATE = 4;
+
+ /**
+ * DateTime representation
+ *
+ * @var \DateTime
+ */
+ protected $dateTime;
+
+ /**
+ * dateType
+ *
+ * @var int
+ */
+ protected $dateType;
+
+ /**
+ * Updates the Date and Time.
+ *
+ * @param \DateTime $dt
+ * @param int $dateType
+ * @return void
+ */
+ public function setDateTime(\DateTime $dt, $dateType = self::LOCALTZ) {
+
+ switch($dateType) {
+
+ case self::LOCAL :
+ $this->setValue($dt->format('Ymd\\THis'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case self::UTC :
+ $dt->setTimeZone(new \DateTimeZone('UTC'));
+ $this->setValue($dt->format('Ymd\\THis\\Z'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case self::LOCALTZ :
+ $this->setValue($dt->format('Ymd\\THis'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ $this->offsetSet('TZID', $dt->getTimeZone()->getName());
+ break;
+ case self::DATE :
+ $this->setValue($dt->format('Ymd'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE');
+ break;
+ default :
+ throw new \InvalidArgumentException('You must pass a valid dateType constant');
+
+ }
+ $this->dateTime = $dt;
+ $this->dateType = $dateType;
+
+ }
+
+ /**
+ * Returns the current DateTime value.
+ *
+ * If no value was set, this method returns null.
+ *
+ * @return \DateTime|null
+ */
+ public function getDateTime() {
+
+ if ($this->dateTime)
+ return $this->dateTime;
+
+ list(
+ $this->dateType,
+ $this->dateTime
+ ) = self::parseData($this->value, $this);
+ return $this->dateTime;
+
+ }
+
+ /**
+ * Returns the type of Date format.
+ *
+ * This method returns one of the format constants. If no date was set,
+ * this method will return null.
+ *
+ * @return int|null
+ */
+ public function getDateType() {
+
+ if ($this->dateType)
+ return $this->dateType;
+
+ list(
+ $this->dateType,
+ $this->dateTime,
+ ) = self::parseData($this->value, $this);
+ return $this->dateType;
+
+ }
+
+ /**
+ * This method will return true, if the property had a date and a time, as
+ * opposed to only a date.
+ *
+ * @return bool
+ */
+ public function hasTime() {
+
+ return $this->getDateType()!==self::DATE;
+
+ }
+
+ /**
+ * Parses the internal data structure to figure out what the current date
+ * and time is.
+ *
+ * The returned array contains two elements:
+ * 1. A 'DateType' constant (as defined on this class), or null.
+ * 2. A DateTime object (or null)
+ *
+ * @param string|null $propertyValue The string to parse (yymmdd or
+ * ymmddThhmmss, etc..)
+ * @param \OldSabre\VObject\Property|null $property The instance of the
+ * property we're parsing.
+ * @return array
+ */
+ static public function parseData($propertyValue, VObject\Property $property = null) {
+
+ if (is_null($propertyValue)) {
+ return array(null, null);
+ }
+
+ $date = '(?P<year>[1-2][0-9]{3})(?P<month>[0-1][0-9])(?P<date>[0-3][0-9])';
+ $time = '(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9])';
+ $regex = "/^$date(T$time(?P<isutc>Z)?)?$/";
+
+ if (!preg_match($regex, $propertyValue, $matches)) {
+ 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 \DateTimeZone('UTC')),
+ );
+ }
+
+ $dateStr =
+ $matches['year'] .'-' .
+ $matches['month'] . '-' .
+ $matches['date'] . ' ' .
+ $matches['hour'] . ':' .
+ $matches['minute'] . ':' .
+ $matches['second'];
+
+ if (isset($matches['isutc'])) {
+ $dt = new \DateTime($dateStr,new \DateTimeZone('UTC'));
+ $dt->setTimeZone(new \DateTimeZone('UTC'));
+ return array(
+ self::UTC,
+ $dt
+ );
+ }
+
+ // 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)
+ );
+ }
+
+ // To look up the timezone, we must first find the VCALENDAR component.
+ $root = $property;
+ while($root->parent) {
+ $root = $root->parent;
+ }
+ 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(
+ self::LOCALTZ,
+ $dt
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php
new file mode 100644
index 0000000..363bc5d
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * Multi-DateTime property
+ *
+ * This element is used for iCalendar properties such as the EXDATE property.
+ * It basically provides a few helper functions that make it easier to deal
+ * with these. It supports both DATE-TIME and DATE values.
+ *
+ * In order to use this correctly, you must call setDateTimes and getDateTimes
+ * to retrieve and modify dates respectively.
+ *
+ * If you use the 'value' or properties directly, this object does not keep
+ * reference and results might appear incorrectly.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class MultiDateTime extends VObject\Property {
+
+ /**
+ * DateTime representation
+ *
+ * @var DateTime[]
+ */
+ protected $dateTimes;
+
+ /**
+ * dateType
+ *
+ * This is one of the OldSabre\VObject\Property\DateTime constants.
+ *
+ * @var int
+ */
+ protected $dateType;
+
+ /**
+ * Updates the value
+ *
+ * @param array $dt Must be an array of DateTime objects.
+ * @param int $dateType
+ * @return void
+ */
+ public function setDateTimes(array $dt, $dateType = VObject\Property\DateTime::LOCALTZ) {
+
+ foreach($dt as $i)
+ if (!$i instanceof \DateTime)
+ throw new \InvalidArgumentException('You must pass an array of DateTime objects');
+
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ switch($dateType) {
+
+ case DateTime::LOCAL :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd\\THis');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case DateTime::UTC :
+ $val = array();
+ foreach($dt as $i) {
+ $i->setTimeZone(new \DateTimeZone('UTC'));
+ $val[] = $i->format('Ymd\\THis\\Z');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case DateTime::LOCALTZ :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd\\THis');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ $this->offsetSet('TZID', $dt[0]->getTimeZone()->getName());
+ break;
+ case DateTime::DATE :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE');
+ break;
+ default :
+ throw new \InvalidArgumentException('You must pass a valid dateType constant');
+
+ }
+ $this->dateTimes = $dt;
+ $this->dateType = $dateType;
+
+ }
+
+ /**
+ * Returns the current DateTime value.
+ *
+ * If no value was set, this method returns null.
+ *
+ * @return array|null
+ */
+ public function getDateTimes() {
+
+ if ($this->dateTimes)
+ return $this->dateTimes;
+
+ $dts = array();
+
+ if (!$this->value) {
+ $this->dateTimes = null;
+ $this->dateType = null;
+ return null;
+ }
+
+ foreach(explode(',',$this->value) as $val) {
+ list(
+ $type,
+ $dt
+ ) = DateTime::parseData($val, $this);
+ $dts[] = $dt;
+ $this->dateType = $type;
+ }
+ $this->dateTimes = $dts;
+ return $this->dateTimes;
+
+ }
+
+ /**
+ * Returns the type of Date format.
+ *
+ * This method returns one of the format constants. If no date was set,
+ * this method will return null.
+ *
+ * @return int|null
+ */
+ public function getDateType() {
+
+ if ($this->dateType)
+ return $this->dateType;
+
+ if (!$this->value) {
+ $this->dateTimes = null;
+ $this->dateType = null;
+ return null;
+ }
+
+ $dts = array();
+ foreach(explode(',',$this->value) as $val) {
+ list(
+ $type,
+ $dt
+ ) = DateTime::parseData($val, $this);
+ $dts[] = $dt;
+ $this->dateType = $type;
+ }
+ $this->dateTimes = $dts;
+ return $this->dateType;
+
+ }
+
+ /**
+ * This method will return true, if the property had a date and a time, as
+ * opposed to only a date.
+ *
+ * @return bool
+ */
+ public function hasTime() {
+
+ return $this->getDateType()!==DateTime::DATE;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php
new file mode 100644
index 0000000..bc9bfc7
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VCALENDAR/VCARD reader
+ *
+ * This class reads the vobject file, and returns a full element tree.
+ *
+ * TODO: this class currently completely works 'statically'. This is pointless,
+ * and defeats OOP principals. Needs refactoring in a future version.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Reader {
+
+ /**
+ * If this option is passed to the reader, it will be less strict about the
+ * validity of the lines.
+ *
+ * Currently using this option just means, that it will accept underscores
+ * in property names.
+ */
+ const OPTION_FORGIVING = 1;
+
+ /**
+ * If this option is turned on, any lines we cannot parse will be ignored
+ * by the reader.
+ */
+ const OPTION_IGNORE_INVALID_LINES = 2;
+
+ /**
+ * Parses the file and returns the top component
+ *
+ * The options argument is a bitfield. Pass any of the OPTIONS constant to
+ * alter the parsers' behaviour.
+ *
+ * @param string $data
+ * @param int $options
+ * @return Node
+ */
+ static function read($data, $options = 0) {
+
+ // Normalizing newlines
+ $data = str_replace(array("\r","\n\n"), array("\n","\n"), $data);
+
+ $lines = explode("\n", $data);
+
+ // Unfolding lines
+ $lines2 = array();
+ foreach($lines as $line) {
+
+ // Skipping empty lines
+ if (!$line) continue;
+
+ if ($line[0]===" " || $line[0]==="\t") {
+ $lines2[count($lines2)-1].=substr($line,1);
+ } else {
+ $lines2[] = $line;
+ }
+
+ }
+
+ unset($lines);
+
+ reset($lines2);
+
+ return self::readLine($lines2, $options);
+
+ }
+
+ /**
+ * Reads and parses a single line.
+ *
+ * This method receives the full array of lines. The array pointer is used
+ * to traverse.
+ *
+ * This method returns null if an invalid line was encountered, and the
+ * IGNORE_INVALID_LINES option was turned on.
+ *
+ * @param array $lines
+ * @param int $options See the OPTIONS constants.
+ * @return Node
+ */
+ static private function readLine(&$lines, $options = 0) {
+
+ $line = current($lines);
+ $lineNr = key($lines);
+ next($lines);
+
+ // Components
+ if (strtoupper(substr($line,0,6)) === "BEGIN:") {
+
+ $componentName = strtoupper(substr($line,6));
+ $obj = Component::create($componentName);
+
+ $nextLine = current($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 ParseException('Invalid VObject. Document ended prematurely.');
+
+ }
+
+ // Checking component name of the 'END:' line.
+ if (substr($nextLine,4)!==$obj->name) {
+ throw new ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"');
+ }
+ next($lines);
+
+ return $obj;
+
+ }
+
+ // Properties
+ //$result = preg_match('/(?P<name>[A-Z0-9-]+)(?:;(?P<parameters>^(?<!:):))(.*)$/',$line,$matches);
+
+ if ($options & self::OPTION_FORGIVING) {
+ $token = '[A-Z0-9-\._]+';
+ } else {
+ $token = '[A-Z0-9-\.]+';
+ }
+ $parameters = "(?:;(?P<parameters>([^:^\"]|\"([^\"]*)\")*))?";
+ $regex = "/^(?P<name>$token)$parameters:(?P<value>.*)$/i";
+
+ $result = preg_match($regex,$line,$matches);
+
+ if (!$result) {
+ 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) {
+ if ($matches[2]==='n' || $matches[2]==='N') {
+ return "\n";
+ } else {
+ return $matches[2];
+ }
+ }, $matches['value']);
+
+ $obj = Property::create($propertyName, $propertyValue);
+
+ if ($matches['parameters']) {
+
+ foreach(self::readParameters($matches['parameters']) as $param) {
+ $obj->add($param);
+ }
+
+ }
+
+ return $obj;
+
+
+ }
+
+ /**
+ * Reads a parameter list from a property
+ *
+ * This method returns an array of Parameter
+ *
+ * @param string $parameters
+ * @return array
+ */
+ static private function readParameters($parameters) {
+
+ $token = '[A-Z0-9-]+';
+
+ $paramValue = '(?P<paramValue>[^\"^;]*|"[^"]*")';
+
+ $regex = "/(?<=^|;)(?P<paramName>$token)(=$paramValue(?=$|;))?/i";
+ preg_match_all($regex, $parameters, $matches, PREG_SET_ORDER);
+
+ $params = array();
+ foreach($matches as $match) {
+
+ if (!isset($match['paramValue'])) {
+
+ $value = null;
+
+ } else {
+
+ $value = $match['paramValue'];
+
+ if (isset($value[0]) && $value[0]==='"') {
+ // Stripping quotes, if needed
+ $value = substr($value,1,strlen($value)-2);
+ }
+
+ $value = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) {
+ if ($matches[2]==='n' || $matches[2]==='N') {
+ return "\n";
+ } else {
+ return $matches[2];
+ }
+ }, $value);
+
+ }
+
+ $params[] = new Parameter($match['paramName'], $value);
+
+ }
+
+ return $params;
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php
new file mode 100644
index 0000000..64bd994
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php
@@ -0,0 +1,1112 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class is used to determine new for a recurring event, when the next
+ * events occur.
+ *
+ * This iterator may loop infinitely in the future, therefore it is important
+ * that if you use this class, you set hard limits for the amount of iterations
+ * you want to handle.
+ *
+ * Note that currently there is not full support for the entire iCalendar
+ * specification, as it's very complex and contains a lot of permutations
+ * that's not yet used very often in software.
+ *
+ * For the focus has been on features as they actually appear in Calendaring
+ * software, but this may well get expanded as needed / on demand
+ *
+ * The following RRULE properties are supported
+ * * UNTIL
+ * * INTERVAL
+ * * COUNT
+ * * FREQ=DAILY
+ * * BYDAY
+ * * BYHOUR
+ * * FREQ=WEEKLY
+ * * BYDAY
+ * * BYHOUR
+ * * WKST
+ * * FREQ=MONTHLY
+ * * BYMONTHDAY
+ * * BYDAY
+ * * BYSETPOS
+ * * FREQ=YEARLY
+ * * BYMONTH
+ * * BYMONTHDAY (only if BYMONTH is also set)
+ * * BYDAY (only if BYMONTH is also set)
+ *
+ * Anything beyond this is 'undefined', which means that it may get ignored, or
+ * you may get unexpected results. The effect is that in some applications the
+ * specified recurrence may look incorrect, or is missing.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class RecurrenceIterator implements \Iterator {
+
+ /**
+ * The initial event date
+ *
+ * @var DateTime
+ */
+ public $startDate;
+
+ /**
+ * The end-date of the initial event
+ *
+ * @var DateTime
+ */
+ public $endDate;
+
+ /**
+ * The 'current' recurrence.
+ *
+ * This will be increased for every iteration.
+ *
+ * @var DateTime
+ */
+ public $currentDate;
+
+
+ /**
+ * List of dates that are excluded from the rules.
+ *
+ * This list contains the items that have been overriden by the EXDATE
+ * property.
+ *
+ * @var array
+ */
+ public $exceptionDates = array();
+
+ /**
+ * Base event
+ *
+ * @var Component\VEvent
+ */
+ public $baseEvent;
+
+ /**
+ * List of dates that are overridden by other events.
+ * Similar to $overriddenEvents, but this just contains the original dates.
+ *
+ * @var array
+ */
+ public $overriddenDates = array();
+
+ /**
+ * list of events that are 'overridden'.
+ *
+ * This is an array of Component\VEvent objects.
+ *
+ * @var array
+ */
+ public $overriddenEvents = array();
+
+
+ /**
+ * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
+ * yearly.
+ *
+ * @var string
+ */
+ public $frequency;
+
+ /**
+ * The last instance of this recurrence, inclusively
+ *
+ * @var DateTime|null
+ */
+ public $until;
+
+ /**
+ * The number of recurrences, or 'null' if infinitely recurring.
+ *
+ * @var int
+ */
+ public $count;
+
+ /**
+ * The interval.
+ *
+ * If for example frequency is set to daily, interval = 2 would mean every
+ * 2 days.
+ *
+ * @var int
+ */
+ public $interval = 1;
+
+ /**
+ * Which seconds to recur.
+ *
+ * This is an array of integers (between 0 and 60)
+ *
+ * @var array
+ */
+ public $bySecond;
+
+ /**
+ * Which minutes to recur
+ *
+ * This is an array of integers (between 0 and 59)
+ *
+ * @var array
+ */
+ public $byMinute;
+
+ /**
+ * Which hours to recur
+ *
+ * This is an array of integers (between 0 and 23)
+ *
+ * @var array
+ */
+ public $byHour;
+
+ /**
+ * Which weekdays to recur.
+ *
+ * This is an array of weekdays
+ *
+ * This may also be preceeded by a positive or negative integer. If present,
+ * this indicates the nth occurrence of a specific day within the monthly or
+ * yearly rrule. For instance, -2TU indicates the second-last tuesday of
+ * the month, or year.
+ *
+ * @var array
+ */
+ public $byDay;
+
+ /**
+ * Which days of the month to recur
+ *
+ * This is an array of days of the months (1-31). The value can also be
+ * negative. -5 for instance means the 5th last day of the month.
+ *
+ * @var array
+ */
+ public $byMonthDay;
+
+ /**
+ * Which days of the year to recur.
+ *
+ * This is an array with days of the year (1 to 366). The values can also
+ * be negative. For instance, -1 will always represent the last day of the
+ * year. (December 31st).
+ *
+ * @var array
+ */
+ public $byYearDay;
+
+ /**
+ * Which week numbers to recur.
+ *
+ * This is an array of integers from 1 to 53. The values can also be
+ * negative. -1 will always refer to the last week of the year.
+ *
+ * @var array
+ */
+ public $byWeekNo;
+
+ /**
+ * Which months to recur
+ *
+ * This is an array of integers from 1 to 12.
+ *
+ * @var array
+ */
+ public $byMonth;
+
+ /**
+ * Which items in an existing st to recur.
+ *
+ * These numbers work together with an existing by* rule. It specifies
+ * exactly which items of the existing by-rule to filter.
+ *
+ * Valid values are 1 to 366 and -1 to -366. As an example, this can be
+ * used to recur the last workday of the month.
+ *
+ * This would be done by setting frequency to 'monthly', byDay to
+ * 'MO,TU,WE,TH,FR' and bySetPos to -1.
+ *
+ * @var array
+ */
+ public $bySetPos;
+
+ /**
+ * When a week starts
+ *
+ * @var string
+ */
+ public $weekStart = 'MO';
+
+ /**
+ * The current item in the list
+ *
+ * @var int
+ */
+ public $counter = 0;
+
+ /**
+ * Simple mapping from iCalendar day names to day numbers
+ *
+ * @var array
+ */
+ private $dayMap = array(
+ 'SU' => 0,
+ 'MO' => 1,
+ 'TU' => 2,
+ 'WE' => 3,
+ 'TH' => 4,
+ 'FR' => 5,
+ 'SA' => 6,
+ );
+
+ /**
+ * Mappings between the day number and english day name.
+ *
+ * @var array
+ */
+ private $dayNames = array(
+ 0 => 'Sunday',
+ 1 => 'Monday',
+ 2 => 'Tuesday',
+ 3 => 'Wednesday',
+ 4 => 'Thursday',
+ 5 => 'Friday',
+ 6 => 'Saturday',
+ );
+
+ /**
+ * If the current iteration of the event is an overriden event, this
+ * property will hold the VObject
+ *
+ * @var Component
+ */
+ private $currentOverriddenEvent;
+
+ /**
+ * This property may contain the date of the next not-overridden event.
+ * This date is calculated sometimes a bit early, before overridden events
+ * are evaluated.
+ *
+ * @var DateTime
+ */
+ private $nextDate;
+
+ /**
+ * Creates the iterator
+ *
+ * You should pass a VCALENDAR component, as well as the UID of the event
+ * we're going to traverse.
+ *
+ * @param Component $vcal
+ * @param string|null $uid
+ */
+ public function __construct(Component $vcal, $uid=null) {
+
+ if (is_null($uid)) {
+ if ($vcal->name === 'VCALENDAR') {
+ throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well');
+ }
+ $components = array($vcal);
+ $uid = (string)$vcal->uid;
+ } else {
+ $components = $vcal->select('VEVENT');
+ }
+ foreach($components as $component) {
+ if ((string)$component->uid == $uid) {
+ if (isset($component->{'RECURRENCE-ID'})) {
+ $this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component;
+ $this->overriddenDates[] = $component->{'RECURRENCE-ID'}->getDateTime();
+ } else {
+ $this->baseEvent = $component;
+ }
+ }
+ }
+ if (!$this->baseEvent) {
+ throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid);
+ }
+
+ $this->startDate = clone $this->baseEvent->DTSTART->getDateTime();
+
+ $this->endDate = null;
+ if (isset($this->baseEvent->DTEND)) {
+ $this->endDate = clone $this->baseEvent->DTEND->getDateTime();
+ } else {
+ $this->endDate = clone $this->startDate;
+ if (isset($this->baseEvent->DURATION)) {
+ $this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value));
+ } elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) {
+ $this->endDate->modify('+1 day');
+ }
+ }
+ $this->currentDate = clone $this->startDate;
+
+ $rrule = (string)$this->baseEvent->RRULE;
+
+ $parts = explode(';', $rrule);
+
+ // 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);
+
+ switch(strtoupper($key)) {
+
+ case 'FREQ' :
+ if (!in_array(
+ strtolower($value),
+ array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
+ )) {
+ throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
+
+ }
+ $this->frequency = strtolower($value);
+ break;
+
+ case 'UNTIL' :
+ $this->until = DateTimeParser::parse($value);
+
+ // In some cases events are generated with an UNTIL=
+ // parameter before the actual start of the event.
+ //
+ // Not sure why this is happening. We assume that the
+ // intention was that the event only recurs once.
+ //
+ // So we are modifying the parameter so our code doesn't
+ // break.
+ if($this->until < $this->baseEvent->DTSTART->getDateTime()) {
+ $this->until = $this->baseEvent->DTSTART->getDateTime();
+ }
+ break;
+
+ case 'COUNT' :
+ $this->count = (int)$value;
+ break;
+
+ case 'INTERVAL' :
+ $this->interval = (int)$value;
+ if ($this->interval < 1) {
+ throw new \InvalidArgumentException('INTERVAL in RRULE must be a positive integer!');
+ }
+ break;
+
+ case 'BYSECOND' :
+ $this->bySecond = explode(',', $value);
+ break;
+
+ case 'BYMINUTE' :
+ $this->byMinute = explode(',', $value);
+ break;
+
+ case 'BYHOUR' :
+ $this->byHour = explode(',', $value);
+ break;
+
+ case 'BYDAY' :
+ $this->byDay = explode(',', strtoupper($value));
+ break;
+
+ case 'BYMONTHDAY' :
+ $this->byMonthDay = explode(',', $value);
+ break;
+
+ case 'BYYEARDAY' :
+ $this->byYearDay = explode(',', $value);
+ break;
+
+ case 'BYWEEKNO' :
+ $this->byWeekNo = explode(',', $value);
+ break;
+
+ case 'BYMONTH' :
+ $this->byMonth = explode(',', $value);
+ break;
+
+ case 'BYSETPOS' :
+ $this->bySetPos = explode(',', $value);
+ break;
+
+ case 'WKST' :
+ $this->weekStart = strtoupper($value);
+ break;
+
+ }
+
+ }
+
+ // Parsing exception dates
+ if (isset($this->baseEvent->EXDATE)) {
+ foreach($this->baseEvent->EXDATE as $exDate) {
+
+ foreach(explode(',', (string)$exDate) as $exceptionDate) {
+
+ $this->exceptionDates[] =
+ DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone());
+
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Returns the current item in the list
+ *
+ * @return DateTime
+ */
+ public function current() {
+
+ if (!$this->valid()) return null;
+ return clone $this->currentDate;
+
+ }
+
+ /**
+ * This method returns the startdate for the current iteration of the
+ * event.
+ *
+ * @return DateTime
+ */
+ public function getDtStart() {
+
+ if (!$this->valid()) return null;
+ return clone $this->currentDate;
+
+ }
+
+ /**
+ * This method returns the enddate for the current iteration of the
+ * event.
+ *
+ * @return DateTime
+ */
+ public function getDtEnd() {
+
+ if (!$this->valid()) return null;
+ $dtEnd = clone $this->currentDate;
+ $dtEnd->add( $this->startDate->diff( $this->endDate ) );
+ return clone $dtEnd;
+
+ }
+
+ /**
+ * Returns a VEVENT object with the updated start and end date.
+ *
+ * Any recurrence information is removed, and this function may return an
+ * 'overridden' event instead.
+ *
+ * This method always returns a cloned instance.
+ *
+ * @return Component\VEvent
+ */
+ public function getEventObject() {
+
+ if ($this->currentOverriddenEvent) {
+ return clone $this->currentOverriddenEvent;
+ }
+ $event = clone $this->baseEvent;
+ unset($event->RRULE);
+ unset($event->EXDATE);
+ unset($event->RDATE);
+ unset($event->EXRULE);
+
+ $event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType());
+ if (isset($event->DTEND)) {
+ $event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType());
+ }
+ if ($this->counter > 0) {
+ $event->{'RECURRENCE-ID'} = (string)$event->DTSTART;
+ }
+
+ return $event;
+
+ }
+
+ /**
+ * Returns the current item number
+ *
+ * @return int
+ */
+ public function key() {
+
+ return $this->counter;
+
+ }
+
+ /**
+ * Whether or not there is a 'next item'
+ *
+ * @return bool
+ */
+ public function valid() {
+
+ if (!is_null($this->count)) {
+ return $this->counter < $this->count;
+ }
+ if (!is_null($this->until)) {
+ return $this->currentDate <= $this->until;
+ }
+ return true;
+
+ }
+
+ /**
+ * Resets the iterator
+ *
+ * @return void
+ */
+ public function rewind() {
+
+ $this->currentDate = clone $this->startDate;
+ $this->counter = 0;
+
+ }
+
+ /**
+ * This method allows you to quickly go to the next occurrence after the
+ * specified date.
+ *
+ * Note that this checks the current 'endDate', not the 'stardDate'. This
+ * means that if you forward to January 1st, the iterator will stop at the
+ * first event that ends *after* January 1st.
+ *
+ * @param DateTime $dt
+ * @return void
+ */
+ public function fastForward(\DateTime $dt) {
+
+ while($this->valid() && $this->getDTEnd() <= $dt) {
+ $this->next();
+ }
+
+ }
+
+ /**
+ * Returns true if this recurring event never ends.
+ *
+ * @return bool
+ */
+ public function isInfinite() {
+
+ return !$this->count && !$this->until;
+
+ }
+
+ /**
+ * Goes on to the next iteration
+ *
+ * @return void
+ */
+ public function next() {
+
+ /*
+ if (!is_null($this->count) && $this->counter >= $this->count) {
+ $this->currentDate = null;
+ }*/
+
+
+ $previousStamp = $this->currentDate->getTimeStamp();
+
+ while(true) {
+
+ $this->currentOverriddenEvent = null;
+
+ // If we have a next date 'stored', we use that
+ if ($this->nextDate) {
+ $this->currentDate = $this->nextDate;
+ $currentStamp = $this->currentDate->getTimeStamp();
+ $this->nextDate = null;
+ } else {
+
+ // Otherwise, we calculate it
+ switch($this->frequency) {
+
+ case 'hourly' :
+ $this->nextHourly();
+ break;
+
+ case 'daily' :
+ $this->nextDaily();
+ break;
+
+ case 'weekly' :
+ $this->nextWeekly();
+ break;
+
+ case 'monthly' :
+ $this->nextMonthly();
+ break;
+
+ case 'yearly' :
+ $this->nextYearly();
+ break;
+
+ }
+ $currentStamp = $this->currentDate->getTimeStamp();
+
+ // Checking exception dates
+ foreach($this->exceptionDates as $exceptionDate) {
+ if ($this->currentDate == $exceptionDate) {
+ $this->counter++;
+ continue 2;
+ }
+ }
+ foreach($this->overriddenDates as $overriddenDate) {
+ if ($this->currentDate == $overriddenDate) {
+ continue 2;
+ }
+ }
+
+ }
+
+ // Checking overridden events
+ foreach($this->overriddenEvents as $index=>$event) {
+ if ($index > $previousStamp && $index <= $currentStamp) {
+
+ // We're moving the 'next date' aside, for later use.
+ $this->nextDate = clone $this->currentDate;
+
+ $this->currentDate = $event->DTSTART->getDateTime();
+ $this->currentOverriddenEvent = $event;
+
+ break;
+ }
+ }
+
+ break;
+
+ }
+
+ /*
+ if (!is_null($this->until)) {
+ if($this->currentDate > $this->until) {
+ $this->currentDate = null;
+ }
+ }*/
+
+ $this->counter++;
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for hourly frequency.
+ *
+ * @return void
+ */
+ protected function nextHourly() {
+
+ if (!$this->byHour) {
+ $this->currentDate->modify('+' . $this->interval . ' hours');
+ return;
+ }
+ }
+
+ /**
+ * Does the processing for advancing the iterator for daily frequency.
+ *
+ * @return void
+ */
+ protected function nextDaily() {
+
+ if (!$this->byHour && !$this->byDay) {
+ $this->currentDate->modify('+' . $this->interval . ' days');
+ return;
+ }
+
+ if (isset($this->byHour)) {
+ $recurrenceHours = $this->getHours();
+ }
+
+ if (isset($this->byDay)) {
+ $recurrenceDays = $this->getDays();
+ }
+
+ do {
+
+ if ($this->byHour) {
+ if ($this->currentDate->format('G') == '23') {
+ // to obey the interval rule
+ $this->currentDate->modify('+' . $this->interval-1 . ' days');
+ }
+
+ $this->currentDate->modify('+1 hours');
+
+ } else {
+ $this->currentDate->modify('+' . $this->interval . ' days');
+
+ }
+
+ // Current day of the week
+ $currentDay = $this->currentDate->format('w');
+
+ // Current hour of the day
+ $currentHour = $this->currentDate->format('G');
+
+ } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for weekly frequency.
+ *
+ * @return void
+ */
+ protected function nextWeekly() {
+
+ if (!$this->byHour && !$this->byDay) {
+ $this->currentDate->modify('+' . $this->interval . ' weeks');
+ return;
+ }
+
+ if ($this->byHour) {
+ $recurrenceHours = $this->getHours();
+ }
+
+ if ($this->byDay) {
+ $recurrenceDays = $this->getDays();
+ }
+
+ // First day of the week:
+ $firstDay = $this->dayMap[$this->weekStart];
+
+ do {
+
+ if ($this->byHour) {
+ $this->currentDate->modify('+1 hours');
+ } else {
+ $this->currentDate->modify('+1 days');
+ }
+
+ // Current day of the week
+ $currentDay = (int) $this->currentDate->format('w');
+
+ // Current hour of the day
+ $currentHour = (int) $this->currentDate->format('G');
+
+ // We need to roll over to the next week
+ if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) {
+ $this->currentDate->modify('+' . $this->interval-1 . ' weeks');
+
+ // We need to go to the first day of this week, but only if we
+ // are not already on this first day of this week.
+ if($this->currentDate->format('w') != $firstDay) {
+ $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
+ }
+ }
+
+ // We have a match
+ } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+ }
+
+ /**
+ * Does the processing for advancing the iterator for monthly frequency.
+ *
+ * @return void
+ */
+ protected function nextMonthly() {
+
+ $currentDayOfMonth = $this->currentDate->format('j');
+ if (!$this->byMonthDay && !$this->byDay) {
+
+ // If the current day is higher than the 28th, rollover can
+ // occur to the next month. We Must skip these invalid
+ // entries.
+ if ($currentDayOfMonth < 29) {
+ $this->currentDate->modify('+' . $this->interval . ' months');
+ } else {
+ $increase = 0;
+ do {
+ $increase++;
+ $tempDate = clone $this->currentDate;
+ $tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
+ } while ($tempDate->format('j') != $currentDayOfMonth);
+ $this->currentDate = $tempDate;
+ }
+ return;
+ }
+
+ while(true) {
+
+ $occurrences = $this->getMonthlyOccurrences();
+
+ foreach($occurrences as $occurrence) {
+
+ // The first occurrence thats higher than the current
+ // day of the month wins.
+ if ($occurrence > $currentDayOfMonth) {
+ break 2;
+ }
+
+ }
+
+ // If we made it all the way here, it means there were no
+ // valid occurrences, and we need to advance to the next
+ // month.
+ $this->currentDate->modify('first day of this month');
+ $this->currentDate->modify('+ ' . $this->interval . ' months');
+
+ // This goes to 0 because we need to start counting at hte
+ // beginning.
+ $currentDayOfMonth = 0;
+
+ }
+
+ $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for yearly frequency.
+ *
+ * @return void
+ */
+ protected function nextYearly() {
+
+ $currentMonth = $this->currentDate->format('n');
+ $currentYear = $this->currentDate->format('Y');
+ $currentDayOfMonth = $this->currentDate->format('j');
+
+ // No sub-rules, so we just advance by year
+ if (!$this->byMonth) {
+
+ // Unless it was a leap day!
+ if ($currentMonth==2 && $currentDayOfMonth==29) {
+
+ $counter = 0;
+ do {
+ $counter++;
+ // Here we increase the year count by the interval, until
+ // we hit a date that's also in a leap year.
+ //
+ // We could just find the next interval that's dividable by
+ // 4, but that would ignore the rule that there's no leap
+ // year every year that's dividable by a 100, but not by
+ // 400. (1800, 1900, 2100). So we just rely on the datetime
+ // functions instead.
+ $nextDate = clone $this->currentDate;
+ $nextDate->modify('+ ' . ($this->interval*$counter) . ' years');
+ } while ($nextDate->format('n')!=2);
+ $this->currentDate = $nextDate;
+
+ return;
+
+ }
+
+ // The easiest form
+ $this->currentDate->modify('+' . $this->interval . ' years');
+ return;
+
+ }
+
+ $currentMonth = $this->currentDate->format('n');
+ $currentYear = $this->currentDate->format('Y');
+ $currentDayOfMonth = $this->currentDate->format('j');
+
+ $advancedToNewMonth = false;
+
+ // If we got a byDay or getMonthDay filter, we must first expand
+ // further.
+ if ($this->byDay || $this->byMonthDay) {
+
+ while(true) {
+
+ $occurrences = $this->getMonthlyOccurrences();
+
+ foreach($occurrences as $occurrence) {
+
+ // The first occurrence that's higher than the current
+ // day of the month wins.
+ // If we advanced to the next month or year, the first
+ // occurrence is always correct.
+ if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
+ break 2;
+ }
+
+ }
+
+ // If we made it here, it means we need to advance to
+ // the next month or year.
+ $currentDayOfMonth = 1;
+ $advancedToNewMonth = true;
+ do {
+
+ $currentMonth++;
+ if ($currentMonth>12) {
+ $currentYear+=$this->interval;
+ $currentMonth = 1;
+ }
+ } while (!in_array($currentMonth, $this->byMonth));
+
+ $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+ }
+
+ // If we made it here, it means we got a valid occurrence
+ $this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
+ return;
+
+ } else {
+
+ // These are the 'byMonth' rules, if there are no byDay or
+ // byMonthDay sub-rules.
+ do {
+
+ $currentMonth++;
+ if ($currentMonth>12) {
+ $currentYear+=$this->interval;
+ $currentMonth = 1;
+ }
+ } while (!in_array($currentMonth, $this->byMonth));
+ $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+ return;
+
+ }
+
+ }
+
+ /**
+ * Returns all the occurrences for a monthly frequency with a 'byDay' or
+ * 'byMonthDay' expansion for the current month.
+ *
+ * The returned list is an array of integers with the day of month (1-31).
+ *
+ * @return array
+ */
+ protected function getMonthlyOccurrences() {
+
+ $startDate = clone $this->currentDate;
+
+ $byDayResults = array();
+
+ // Our strategy is to simply go through the byDays, advance the date to
+ // that point and add it to the results.
+ if ($this->byDay) foreach($this->byDay as $day) {
+
+ $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
+
+ // Dayname will be something like 'wednesday'. Now we need to find
+ // all wednesdays in this month.
+ $dayHits = array();
+
+ $checkDate = clone $startDate;
+ $checkDate->modify('first day of this month');
+ $checkDate->modify($dayName);
+
+ do {
+ $dayHits[] = $checkDate->format('j');
+ $checkDate->modify('next ' . $dayName);
+ } while ($checkDate->format('n') === $startDate->format('n'));
+
+ // So now we have 'all wednesdays' for month. It is however
+ // possible that the user only really wanted the 1st, 2nd or last
+ // wednesday.
+ if (strlen($day)>2) {
+ $offset = (int)substr($day,0,-2);
+
+ if ($offset>0) {
+ // It is possible that the day does not exist, such as a
+ // 5th or 6th wednesday of the month.
+ if (isset($dayHits[$offset-1])) {
+ $byDayResults[] = $dayHits[$offset-1];
+ }
+ } else {
+
+ // if it was negative we count from the end of the array
+ $byDayResults[] = $dayHits[count($dayHits) + $offset];
+ }
+ } else {
+ // There was no counter (first, second, last wednesdays), so we
+ // just need to add the all to the list).
+ $byDayResults = array_merge($byDayResults, $dayHits);
+
+ }
+
+ }
+
+ $byMonthDayResults = array();
+ if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
+
+ // Removing values that are out of range for this month
+ if ($monthDay > $startDate->format('t') ||
+ $monthDay < 0-$startDate->format('t')) {
+ continue;
+ }
+ if ($monthDay>0) {
+ $byMonthDayResults[] = $monthDay;
+ } else {
+ // Negative values
+ $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
+ }
+ }
+
+ // If there was just byDay or just byMonthDay, they just specify our
+ // (almost) final list. If both were provided, then byDay limits the
+ // list.
+ if ($this->byMonthDay && $this->byDay) {
+ $result = array_intersect($byMonthDayResults, $byDayResults);
+ } elseif ($this->byMonthDay) {
+ $result = $byMonthDayResults;
+ } else {
+ $result = $byDayResults;
+ }
+ $result = array_unique($result);
+ sort($result, SORT_NUMERIC);
+
+ // The last thing that needs checking is the BYSETPOS. If it's set, it
+ // means only certain items in the set survive the filter.
+ if (!$this->bySetPos) {
+ return $result;
+ }
+
+ $filteredResult = array();
+ foreach($this->bySetPos as $setPos) {
+
+ if ($setPos<0) {
+ $setPos = count($result)-($setPos+1);
+ }
+ if (isset($result[$setPos-1])) {
+ $filteredResult[] = $result[$setPos-1];
+ }
+ }
+
+ sort($filteredResult, SORT_NUMERIC);
+ return $filteredResult;
+
+ }
+
+ protected function getHours()
+ {
+ $recurrenceHours = array();
+ foreach($this->byHour as $byHour) {
+ $recurrenceHours[] = $byHour;
+ }
+
+ return $recurrenceHours;
+ }
+
+ protected function getDays()
+ {
+ $recurrenceDays = array();
+ foreach($this->byDay as $byDay) {
+
+ // The day may be preceeded with a positive (+n) or
+ // negative (-n) integer. However, this does not make
+ // sense in 'weekly' so we ignore it here.
+ $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
+
+ }
+
+ return $recurrenceDays;
+ }
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php
new file mode 100644
index 0000000..20b8f42
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+use OldSabre\VObject;
+
+/**
+ * Splitter
+ *
+ * This class is responsible for splitting up iCalendar objects.
+ *
+ * This class expects a single VCALENDAR object with one or more
+ * calendar-objects inside. Objects with identical UID's will be combined into
+ * a single object.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @author Armin Hackmann
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ICalendar implements SplitterInterface {
+
+ /**
+ * Timezones
+ *
+ * @var array
+ */
+ protected $vtimezones = array();
+
+ /**
+ * iCalendar objects
+ *
+ * @var array
+ */
+ protected $objects = array();
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ public function __construct($input) {
+
+ $data = VObject\Reader::read(stream_get_contents($input));
+ $vtimezones = array();
+ $components = array();
+
+ foreach($data->children as $component) {
+ if (!$component instanceof VObject\Component) {
+ continue;
+ }
+
+ // Get all timezones
+ if ($component->name === 'VTIMEZONE') {
+ $this->vtimezones[(string)$component->TZID] = $component;
+ continue;
+ }
+
+ // Get component UID for recurring Events search
+ if($component->UID) {
+ $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 OldSabre\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/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php
new file mode 100644
index 0000000..3696031
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+/**
+ * VObject splitter
+ *
+ * The splitter is responsible for reading a large vCard or iCalendar object,
+ * and splitting it into multiple objects.
+ *
+ * This is for example for Card and CalDAV, which require every event and vcard
+ * to exist in their own objects, instead of one large one.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+interface SplitterInterface {
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ function __construct($input);
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return OldSabre\VObject\Component|null
+ */
+ function getNext();
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php
new file mode 100644
index 0000000..fc5e39b
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+use OldSabre\VObject;
+
+/**
+ * Splitter
+ *
+ * This class is responsible for splitting up VCard objects.
+ *
+ * It is assumed that the input stream contains 1 or more VCARD objects. This
+ * class checks for BEGIN:VCARD and END:VCARD and parses each encountered
+ * component individually.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @author Armin Hackmann
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCard implements SplitterInterface {
+
+ /**
+ * File handle
+ *
+ * @var resource
+ */
+ protected $input;
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ public function __construct($input) {
+
+ $this->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 OldSabre\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/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php
new file mode 100644
index 0000000..1218792
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Useful utilities for working with various strings.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class StringUtil {
+
+ /**
+ * Returns true or false depending on if a string is valid UTF-8
+ *
+ * @param string $str
+ * @return bool
+ */
+ static function isUTF8($str) {
+
+ // First check.. mb_check_encoding
+ if (!mb_check_encoding($str, 'UTF-8')) {
+ return false;
+ }
+
+ // Control characters
+ if (preg_match('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', $str)) {
+ return false;
+ }
+
+ return true;
+
+ }
+
+ /**
+ * This method tries its best to convert the input string to UTF-8.
+ *
+ * Currently only ISO-5991-1 input and UTF-8 input is supported, but this
+ * may be expanded upon if we receive other examples.
+ *
+ * @param string $str
+ * @return string
+ */
+ static function convertToUTF8($str) {
+
+ $encoding = mb_detect_encoding($str , array('UTF-8','ISO-8859-1'), true);
+
+ if ($encoding === 'ISO-8859-1') {
+ $newStr = utf8_encode($str);
+ } else {
+ $newStr = $str;
+ }
+
+ // Removing any control characters
+ return (preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', '', $newStr));
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php
new file mode 100644
index 0000000..dbb77b0
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php
@@ -0,0 +1,482 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Time zone name translation
+ *
+ * This file translates well-known time zone names into "Olson database" time zone names.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Frank Edelhaeuser (fedel@users.sourceforge.net)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class TimeZoneUtil {
+
+ public static $map = array(
+
+ // from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html
+ // snapshot taken on 2012/01/16
+
+ // windows
+ 'AUS Central Standard Time'=>'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+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',
+
+ // The following list are timezone names that could be generated by
+ // Lotus / Domino
+ 'Dateline' => 'Etc/GMT-12',
+ 'Samoa' => 'Pacific/Apia',
+ 'Hawaiian' => 'Pacific/Honolulu',
+ 'Alaskan' => 'America/Anchorage',
+ 'Pacific' => 'America/Los_Angeles',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Mexico Standard Time 2' => 'America/Chihuahua',
+ 'Mountain' => 'America/Denver',
+ 'Mountain Standard Time' => 'America/Chihuahua',
+ 'US Mountain' => 'America/Phoenix',
+ 'Canada Central' => 'America/Edmonton',
+ 'Central America' => 'America/Guatemala',
+ 'Central' => 'America/Chicago',
+ 'Central Standard Time' => 'America/Mexico_City',
+ 'Mexico' => 'America/Mexico_City',
+ 'Eastern' => 'America/New_York',
+ 'SA Pacific' => 'America/Bogota',
+ 'US Eastern' => 'America/Indiana/Indianapolis',
+ 'Venezuela' => 'America/Caracas',
+ 'Atlantic' => 'America/Halifax',
+ 'Central Brazilian' => 'America/Manaus',
+ 'Pacific SA' => 'America/Santiago',
+ 'SA Western' => 'America/La_Paz',
+ 'Newfoundland' => 'America/St_Johns',
+ 'Argentina' => 'America/Argentina/Buenos_Aires',
+ 'E. South America' => 'America/Belem',
+ 'Greenland' => 'America/Godthab',
+ 'Montevideo' => 'America/Montevideo',
+ 'SA Eastern' => 'America/Belem',
+ 'Mid-Atlantic' => 'Etc/GMT-2',
+ 'Azores' => 'Atlantic/Azores',
+ 'Cape Verde' => 'Atlantic/Cape_Verde',
+ 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT.
+ 'Morocco' => 'Africa/Casablanca',
+ 'Central Europe' => 'Europe/Prague',
+ 'Central European' => 'Europe/Sarajevo',
+ 'Romance' => 'Europe/Paris',
+ 'W. Central Africa' => 'Africa/Lagos', // Best guess
+ 'W. Europe' => 'Europe/Amsterdam',
+ 'E. Europe' => 'Europe/Minsk',
+ 'Egypt' => 'Africa/Cairo',
+ 'FLE' => 'Europe/Helsinki',
+ 'GTB' => 'Europe/Athens',
+ 'Israel' => 'Asia/Jerusalem',
+ 'Jordan' => 'Asia/Amman',
+ 'Middle East' => 'Asia/Beirut',
+ 'Namibia' => 'Africa/Windhoek',
+ 'South Africa' => 'Africa/Harare',
+ 'Arab' => 'Asia/Kuwait',
+ 'Arabic' => 'Asia/Baghdad',
+ 'E. Africa' => 'Africa/Nairobi',
+ 'Georgian' => 'Asia/Tbilisi',
+ 'Russian' => 'Europe/Moscow',
+ 'Iran' => 'Asia/Tehran',
+ 'Arabian' => 'Asia/Muscat',
+ 'Armenian' => 'Asia/Yerevan',
+ 'Azerbijan' => 'Asia/Baku',
+ 'Caucasus' => 'Asia/Yerevan',
+ 'Mauritius' => 'Indian/Mauritius',
+ 'Afghanistan' => 'Asia/Kabul',
+ 'Ekaterinburg' => 'Asia/Yekaterinburg',
+ 'Pakistan' => 'Asia/Karachi',
+ 'West Asia' => 'Asia/Tashkent',
+ 'India' => 'Asia/Calcutta',
+ 'Sri Lanka' => 'Asia/Colombo',
+ 'Nepal' => 'Asia/Kathmandu',
+ 'Central Asia' => 'Asia/Dhaka',
+ 'N. Central Asia' => 'Asia/Almaty',
+ 'Myanmar' => 'Asia/Rangoon',
+ 'North Asia' => 'Asia/Krasnoyarsk',
+ 'SE Asia' => 'Asia/Bangkok',
+ 'China' => 'Asia/Shanghai',
+ 'North Asia East' => 'Asia/Irkutsk',
+ 'Singapore' => 'Asia/Singapore',
+ 'Taipei' => 'Asia/Taipei',
+ 'W. Australia' => 'Australia/Perth',
+ 'Korea' => 'Asia/Seoul',
+ 'Tokyo' => 'Asia/Tokyo',
+ 'Yakutsk' => 'Asia/Yakutsk',
+ 'AUS Central' => 'Australia/Darwin',
+ 'Cen. Australia' => 'Australia/Adelaide',
+ 'AUS Eastern' => 'Australia/Sydney',
+ 'E. Australia' => 'Australia/Brisbane',
+ 'Tasmania' => 'Australia/Hobart',
+ 'Vladivostok' => 'Asia/Vladivostok',
+ 'West Pacific' => 'Pacific/Guam',
+ 'Central Pacific' => 'Asia/Magadan',
+ 'Fiji' => 'Pacific/Fiji',
+ 'New Zealand' => 'Pacific/Auckland',
+ 'Tonga' => 'Pacific/Tongatapu',
+ );
+
+ /**
+ * List of microsoft exchange timezone ids.
+ *
+ * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
+ */
+ public static $microsoftExchangeMap = array(
+ 0 => 'UTC',
+ 31 => 'Africa/Casablanca',
+
+ // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
+ // I'm not even kidding.. We handle this special case in the
+ // getTimeZone method.
+ 2 => 'Europe/Lisbon',
+ 1 => 'Europe/London',
+ 4 => 'Europe/Berlin',
+ 6 => 'Europe/Prague',
+ 3 => 'Europe/Paris',
+ 69 => 'Africa/Luanda', // This was a best guess
+ 7 => 'Europe/Athens',
+ 5 => 'Europe/Bucharest',
+ 49 => 'Africa/Cairo',
+ 50 => 'Africa/Harare',
+ 59 => 'Europe/Helsinki',
+ 27 => 'Asia/Jerusalem',
+ 26 => 'Asia/Baghdad',
+ 74 => 'Asia/Kuwait',
+ 51 => 'Europe/Moscow',
+ 56 => 'Africa/Nairobi',
+ 25 => 'Asia/Tehran',
+ 24 => 'Asia/Muscat', // Best guess
+ 54 => 'Asia/Baku',
+ 48 => 'Asia/Kabul',
+ 58 => 'Asia/Yekaterinburg',
+ 47 => 'Asia/Karachi',
+ 23 => 'Asia/Calcutta',
+ 62 => 'Asia/Kathmandu',
+ 46 => 'Asia/Almaty',
+ 71 => 'Asia/Dhaka',
+ 66 => 'Asia/Colombo',
+ 61 => 'Asia/Rangoon',
+ 22 => 'Asia/Bangkok',
+ 64 => 'Asia/Krasnoyarsk',
+ 45 => 'Asia/Shanghai',
+ 63 => 'Asia/Irkutsk',
+ 21 => 'Asia/Singapore',
+ 73 => 'Australia/Perth',
+ 75 => 'Asia/Taipei',
+ 20 => 'Asia/Tokyo',
+ 72 => 'Asia/Seoul',
+ 70 => 'Asia/Yakutsk',
+ 19 => 'Australia/Adelaide',
+ 44 => 'Australia/Darwin',
+ 18 => 'Australia/Brisbane',
+ 76 => 'Australia/Sydney',
+ 43 => 'Pacific/Guam',
+ 42 => 'Australia/Hobart',
+ 68 => 'Asia/Vladivostok',
+ 41 => 'Asia/Magadan',
+ 17 => 'Pacific/Auckland',
+ 40 => 'Pacific/Fiji',
+ 67 => 'Pacific/Tongatapu',
+ 29 => 'Atlantic/Azores',
+ 53 => 'Atlantic/Cape_Verde',
+ 30 => 'America/Noronha',
+ 8 => 'America/Sao_Paulo', // Best guess
+ 32 => 'America/Argentina/Buenos_Aires',
+ 60 => 'America/Godthab',
+ 28 => 'America/St_Johns',
+ 9 => 'America/Halifax',
+ 33 => 'America/Caracas',
+ 65 => 'America/Santiago',
+ 35 => 'America/Bogota',
+ 10 => 'America/New_York',
+ 34 => 'America/Indiana/Indianapolis',
+ 55 => 'America/Guatemala',
+ 11 => 'America/Chicago',
+ 37 => 'America/Mexico_City',
+ 36 => 'America/Edmonton',
+ 38 => 'America/Phoenix',
+ 12 => 'America/Denver', // Best guess
+ 13 => 'America/Los_Angeles', // Best guess
+ 14 => 'America/Anchorage',
+ 15 => 'Pacific/Honolulu',
+ 16 => 'Pacific/Midway',
+ 39 => 'Pacific/Kwajalein',
+ );
+
+ /**
+ * This method will try to find out the correct timezone for an iCalendar
+ * date-time value.
+ *
+ * You must pass the contents of the TZID parameter, as well as the full
+ * calendar.
+ *
+ * If the lookup fails, this method will return the default PHP timezone
+ * (as configured using date_default_timezone_set, or the date.timezone ini
+ * setting).
+ *
+ * Alternatively, if $failIfUncertain is set to true, it will throw an
+ * exception if we cannot accurately determine the timezone.
+ *
+ * @param string $tzid
+ * @param OldSabre\VObject\Component $vcalendar
+ * @return DateTimeZone
+ */
+ static public function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) {
+
+ // 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]);
+ }
+
+ // Maybe the author was hyper-lazy and just included an offset. We
+ // support it, but we aren't happy about it.
+ if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
+ return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2],0,2),'0'));
+ }
+
+ if ($vcalendar) {
+
+ // If that didn't work, we will scan VTIMEZONE objects
+ foreach($vcalendar->select('VTIMEZONE') as $vtimezone) {
+
+ if ((string)$vtimezone->TZID === $tzid) {
+
+ // Some clients add 'X-LIC-LOCATION' with the olson name.
+ if (isset($vtimezone->{'X-LIC-LOCATION'})) {
+
+ $lic = (string)$vtimezone->{'X-LIC-LOCATION'};
+
+ // Libical generators may specify strings like
+ // "SystemV/EST5EDT". For those we must remove the
+ // SystemV part.
+ if (substr($lic,0,8)==='SystemV/') {
+ $lic = substr($lic,8);
+ }
+
+ try {
+ return new \DateTimeZone($lic);
+ } catch (\Exception $e) {
+ }
+
+ }
+ // Microsoft may add a magic number, which we also have an
+ // answer for.
+ if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
+ $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->value;
+
+ // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
+ if ($cdoId===2 && strpos((string)$vtimezone->TZID, 'Sarajevo')!==false) {
+ return new \DateTimeZone('Europe/Sarajevo');
+ }
+
+ if (isset(self::$microsoftExchangeMap[$cdoId])) {
+ return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+ if ($failIfUncertain) {
+ throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid);
+ }
+
+ // If we got all the way here, we default to UTC.
+ return new \DateTimeZone(date_default_timezone_get());
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php
new file mode 100644
index 0000000..8de8885
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class contains the version number for the VObject package
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '2.1.0';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php
new file mode 100644
index 0000000..d15329a
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Includes file
+ *
+ * This file includes the entire VObject library in one go.
+ * The benefit is that an autoloader is not needed, which is often faster.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+
+// Begin includes
+include __DIR__ . '/DateTimeParser.php';
+include __DIR__ . '/ElementList.php';
+include __DIR__ . '/FreeBusyGenerator.php';
+include __DIR__ . '/Node.php';
+include __DIR__ . '/Parameter.php';
+include __DIR__ . '/ParseException.php';
+include __DIR__ . '/Property.php';
+include __DIR__ . '/Reader.php';
+include __DIR__ . '/RecurrenceIterator.php';
+include __DIR__ . '/Splitter/SplitterInterface.php';
+include __DIR__ . '/StringUtil.php';
+include __DIR__ . '/TimeZoneUtil.php';
+include __DIR__ . '/Version.php';
+include __DIR__ . '/Splitter/VCard.php';
+include __DIR__ . '/Component.php';
+include __DIR__ . '/Document.php';
+include __DIR__ . '/Property/Compound.php';
+include __DIR__ . '/Property/DateTime.php';
+include __DIR__ . '/Property/MultiDateTime.php';
+include __DIR__ . '/Splitter/ICalendar.php';
+include __DIR__ . '/Component/VAlarm.php';
+include __DIR__ . '/Component/VCalendar.php';
+include __DIR__ . '/Component/VEvent.php';
+include __DIR__ . '/Component/VFreeBusy.php';
+include __DIR__ . '/Component/VJournal.php';
+include __DIR__ . '/Component/VTodo.php';
+// End includes
diff --git a/calendar/lib/caldav-client.php b/calendar/lib/caldav-client.php
new file mode 100644
index 0000000..f0f1671
--- /dev/null
+++ b/calendar/lib/caldav-client.php
@@ -0,0 +1,379 @@
+<?php
+
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once (dirname(__FILE__).'/SabreDAV/vendor/autoload.php');
+
+
+class caldav_client extends OldSabre\DAV\Client
+{
+ const CLARK_GETCTAG = '{http://calendarserver.org/ns/}getctag';
+ const CLARK_GETETAG = '{DAV:}getetag';
+ const CLARK_CALDATA = '{urn:ietf:params:xml:ns:caldav}calendar-data';
+
+ private $base_uri;
+ private $path;
+ private $libvcal;
+
+ /**
+ * Default constructor for CalDAV client.
+ *
+ * @param string Caldav URI to appropriate calendar.
+ * @param string Username for HTTP basic auth.
+ * @param string Password for HTTP basic auth.
+ */
+ public function __construct($uri, $user = null, $pass = null)
+ {
+
+ // Include libvcalendar on demand ...
+ if(!class_exists("libvcalendar"))
+ require_once (dirname(__FILE__).'/../../libcalendaring/libvcalendar.php');
+
+ $this->libvcal = new libvcalendar();
+
+ $tokens = parse_url($uri);
+ $this->base_uri = $tokens['scheme']."://".$tokens['host'].($tokens['port'] ? ":".$tokens['port'] : null);
+ $this->path = $tokens['path'].($tokens['query'] ? "?".$tokens['query'] : null);
+
+ $settings = array(
+ 'baseUri' => $this->base_uri,
+ 'authType' => OldSabre\DAV\Client::AUTH_BASIC
+ );
+
+ // Forward SSL certificate setting to Sabre\DAV an onwards to CURL
+ $this->rc = rcmail::get_instance();
+ parent::setVerifyPeer($this->rc->config->get('calendar_curl_secure_ssl', true));
+
+ if ($user) $settings['userName'] = $user;
+ if ($pass) $settings['password'] = $pass;
+
+ parent::__construct($settings);
+ }
+
+ /**
+ * Fetches calendar ctag.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Retrieving_calendar_information
+ * @return Calendar ctag or null on error.
+ */
+ public function get_ctag()
+ {
+ try
+ {
+ $arr = $this->propFind($this->path, array(self::CLARK_GETCTAG));
+
+ if (isset($arr[self::CLARK_GETCTAG]))
+ return $arr[self::CLARK_GETCTAG];
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+
+ return null;
+ }
+
+ /**
+ * Fetches event etags and urls.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Finding_out_if_anything_changed
+ *
+ * @param array Optional list of relative event URL's to retrieve specific etags. If not specified, all etags of the current calendar are returned.
+ * @return array List of etag properties with keys:
+ * url: Event ical path relative to the calendar URL.
+ * etag: Current event etag.
+ */
+ public function get_etags(array $event_urls = array())
+ {
+ $etags = array();
+
+ try
+ {
+ $arr = $this->prop_report($this->path, array(self::CLARK_GETETAG), $event_urls);
+ foreach ($arr as $path => $data)
+ {
+ // Some caldav server return an empty calendar as event where etag is missing. Skip this!
+ if($data[self::CLARK_GETETAG])
+ {
+ array_push($etags, array(
+ "url" => $path,
+ "etag" => str_replace('"', null, $data[self::CLARK_GETETAG])
+ ));
+ }
+ }
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+
+ return $etags;
+ }
+
+ /**
+ * Fetches calendar events.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Downloading_objects
+ * @param array $urls = array() Optional list of event URL's to fetch. If non is specified, all
+ * events from the appropriate calendar will be fetched.
+ * @return Array hash list that maps the events URL to the appropriate event properties.
+ */
+ public function get_events($urls = array())
+ {
+ $events = array();
+
+ try
+ {
+ $vcals = $this->prop_report($this->path, array(
+ self::CLARK_GETETAG,
+ self::CLARK_CALDATA
+ ), $urls);
+
+ foreach ($vcals as $path => $response)
+ {
+ $vcal = $response[self::CLARK_CALDATA];
+ foreach ($this->libvcal->import($vcal) as $event) {
+ $events[$path] = $event;
+ }
+ }
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+ return $events;
+ }
+
+ /**
+ * Does a REPORT request
+ *
+ * @param string $url
+ * @param array $properties List of requested properties must be specified as an array, in clark
+ * notation.
+ * @param array $event_urls If specified, a multiget report request will be initiated with the
+ * specified event urls.
+ * @param int $depth = 1 Depth should be either 0 or 1. A depth of 1 will cause a request to be
+ * made to the server to also return all child resources.
+ * @return array Hash with ics event path as key and a hash array with properties and appropriate values.
+ */
+ public function prop_report($url, array $properties, array $event_urls = array(), $depth = 1)
+ {
+ $parent_tag = sizeof($event_urls) > 0 ? "c:calendar-multiget" : "d:propfind";
+ $method = sizeof($event_urls) > 0 ? 'REPORT' : 'PROPFIND';
+
+ $body = '<?xml version="1.0"?>'."\n".'<'.$parent_tag.' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'."\n";
+
+ $body .= ' <d:prop>'."\n";
+ foreach ($properties as $property)
+ {
+
+ list($namespace, $elementName) = OldSabre\DAV\XMLUtil::parseClarkNotation($property);
+
+ if ($namespace === 'DAV:')
+ {
+ $body .= ' <d:'.$elementName.' />'."\n";
+ }
+ else
+ {
+ $body .= ' <x:'.$elementName.' xmlns:x="'.$namespace.'"/>'."\n";
+ }
+ }
+ $body .= ' </d:prop>'."\n";
+
+ // http://tools.ietf.org/html/rfc4791#page-90
+ // http://www.bedework.org/trac/bedework/wiki/Bedework/DevDocs/Filters
+ /*
+ if($start && $end)
+ {
+ $body.= ' <c:filter>'."\n".
+ ' <c:comp-filter name="VCALENDAR">'."\n".
+ ' <c:comp-filter name="VEVENT">'."\n".
+ ' <c:time-range start="'.$start.'" end="'.$end.'" />'."\n".
+ ' </c:comp-filter>'."\n".
+ ' </c:comp-filter>'."\n".
+ ' </c:filter>' . "\n";
+ }
+ */
+
+ foreach ($event_urls as $event_url)
+ {
+ $body .= '<d:href>'.$event_url.'</d:href>'."\n";
+ }
+
+ $body .= '</'.$parent_tag.'>';
+
+ $response = $this->request($method, $url, $body, array(
+ 'Depth' => $depth,
+ 'Content-Type' => 'application/xml'
+ ));
+
+ $result = $this->parseMultiStatus($response['body']);
+
+ // If depth was 0, we only return the top item
+ if ($depth === 0)
+ {
+ reset($result);
+ $result = current($result);
+ return isset($result[200]) ? $result[200] : array();
+ }
+
+ $new_result = array();
+ foreach ($result as $href => $status_list)
+ {
+ $new_result[$href] = isset($status_list[200]) ? $status_list[200] : array();
+ }
+
+ return $new_result;
+ }
+
+ /**
+ * Updates or creates a calendar event.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Updating_a_calendar_object
+ * @param string Event ics path for the event.
+ * @param array Hash array with event properties.
+ * @param string Current event etag to match against server data. Pass null for new events.
+ * @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error.
+ */
+ public function put_event($path, $event, $etag = null)
+ {
+ try
+ {
+ $headers = array("Content-Type" => "text/calendar; charset=utf-8");
+ if ($etag) $headers["If-Match"] = '"'.$etag.'"';
+
+ // Temporarily disable error reporting since libvcal seems not checking array key properly.
+ // TODO: Remove this todo if we could ensure that those errors come not from incomplete event properties.
+ $err_rep = error_reporting(E_ERROR);
+ $vcal = $this->libvcal->export(array($event));
+ if (is_array($vcal))
+ $vcal = array_shift($vcal);
+ error_reporting($err_rep);
+
+ $response = $this->request('PUT', $path, $vcal, $headers);
+
+ // Following http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Creating_a_calendar_object, the
+ // caldav server must not always return the new etag.
+
+ return $response["statusCode"] == 201 || // 201 (created, successfully created)
+ $response["statusCode"] == 204; // 204 (no content, successfully updated)
+ }
+ catch(OldSabre\DAV\Exception\PreconditionFailed $err)
+ {
+ // Event tag not up to date, must be updated first ...
+ return -1;
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+ return false;
+ }
+
+ /**
+ * Removes event of given URL.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Deleting_a_calendar_object
+ * @param string Event ics path for the event.
+ * @param string Current event etag to match against server data. Pass null to force removing the event.
+ * @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error.
+ **/
+ public function remove_event($path, $etag = null)
+ {
+ try
+ {
+ $headers = array("Content-Type" => "text/calendar; charset=utf-8");
+ if ($etag) $headers["If-Match"] = '"'.$etag.'"';
+
+ $response = $this->request('DELETE', $path, null, $headers);
+ return $response["statusCode"] == 204 || // 204 (no content, successfully deleted)
+ $response["statusCode"] == 200; // 200 (OK, successfully deleted)
+ }
+ catch(OldSabre\DAV\Exception\PreconditionFailed $err)
+ {
+ // Event tag not up to date, must be updated first ...
+ return -1;
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+ return false;
+ }
+
+ /**
+ * Make a propFind query to caldav server
+ * @param string $path absolute or relative URL to Resource
+ * @param array $props list of properties to use for the query. Properties must have clark-notation.
+ * @param int $depth 0 means no recurse while 1 means recurse
+ * @return array
+ */
+ public function prop_find($path, $props, $depth)
+ {
+ try {
+ $response = $this->propFind($path, $props, $depth);
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+ return $response;
+ }
+};
+?>
diff --git a/calendar/lib/calendar_itip.php b/calendar/lib/calendar_itip.php
new file mode 100644
index 0000000..e2a2402
--- /dev/null
+++ b/calendar/lib/calendar_itip.php
@@ -0,0 +1,240 @@
+<?php
+
+require_once realpath(__DIR__ . '/../../libcalendaring/lib/libcalendaring_itip.php');
+
+/**
+ * iTIP functions for the Calendar plugin
+ *
+ * Class providing functionality to manage iTIP invitations
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ * @package @package_name@
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class calendar_itip extends libcalendaring_itip
+{
+ /**
+ * Constructor to set text domain to calendar
+ */
+ function __construct($plugin, $domain = 'calendar')
+ {
+ parent::__construct($plugin, $domain);
+
+ $this->db_itipinvitations = $this->rc->db->table_name('itipinvitations', true);
+ }
+
+ /**
+ * Handler for calendar/itip-status requests
+ */
+ public function get_itip_status($event, $existing = null)
+ {
+ $status = parent::get_itip_status($event, $existing);
+
+ // don't ask for deleting events when declining
+ if ($this->rc->config->get('kolab_invitation_calendars'))
+ $status['saved'] = false;
+
+ return $status;
+ }
+
+ /**
+ * Find invitation record by token
+ *
+ * @param string Invitation token
+ * @return mixed Invitation record as hash array or False if not found
+ */
+ public function get_invitation($token)
+ {
+ if ($parts = $this->decode_token($token)) {
+ $result = $this->rc->db->query("SELECT * FROM $this->db_itipinvitations WHERE `token` = ?", $parts['base']);
+ if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+ $rec['event'] = unserialize($rec['event']);
+ $rec['attendee'] = $parts['attendee'];
+ return $rec;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Update the attendee status of the given invitation record
+ *
+ * @param array Invitation record as fetched with calendar_itip::get_invitation()
+ * @param string Attendee email address
+ * @param string New attendee status
+ */
+ public function update_invitation($invitation, $email, $newstatus)
+ {
+ if (is_string($invitation))
+ $invitation = $this->get_invitation($invitation);
+
+ if ($invitation['token'] && $invitation['event']) {
+ // update attendee record in event data
+ foreach ($invitation['event']['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ else if ($attendee['email'] == $email) {
+ // nothing to be done here
+ if ($attendee['status'] == $newstatus)
+ return true;
+
+ $invitation['event']['attendees'][$i]['status'] = $newstatus;
+ $this->sender = $attendee;
+ }
+ }
+ $invitation['event']['changed'] = new DateTime();
+
+ // send iTIP REPLY message to organizer
+ if ($organizer) {
+ $status = strtolower($newstatus);
+ if ($this->send_itip_message($invitation['event'], 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
+ $this->rc->output->command('display_message', $this->plugin->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
+ else
+ $this->rc->output->command('display_message', $this->plugin->gettext('itipresponseerror'), 'error');
+ }
+
+ // update record in DB
+ $query = $this->rc->db->query(
+ "UPDATE $this->db_itipinvitations
+ SET `event` = ?
+ WHERE `token` = ?",
+ self::serialize_event($invitation['event']),
+ $invitation['token']
+ );
+
+ if ($this->rc->db->affected_rows($query))
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Create iTIP invitation token for later replies via URL
+ *
+ * @param array Hash array with event properties
+ * @param string Attendee email address
+ * @return string Invitation token
+ */
+ public function store_invitation($event, $attendee)
+ {
+ static $stored = array();
+
+ if (!$event['uid'] || !$attendee)
+ return false;
+
+ // generate token for this invitation
+ $token = $this->generate_token($event, $attendee);
+ $base = substr($token, 0, 40);
+
+ // already stored this
+ if ($stored[$base])
+ return $token;
+
+ // delete old entry
+ $this->rc->db->query("DELETE FROM $this->db_itipinvitations WHERE `token` = ?", $base);
+
+ $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
+
+ $query = $this->rc->db->query(
+ "INSERT INTO $this->db_itipinvitations
+ (`token`, `event_uid`, `user_id`, `event`, `expires`)
+ VALUES(?, ?, ?, ?, ?)",
+ $base,
+ $event_uid,
+ $this->rc->user->ID,
+ self::serialize_event($event),
+ date('Y-m-d H:i:s', $event['end']->format('U') + 86400 * 2)
+ );
+
+ if ($this->rc->db->affected_rows($query)) {
+ $stored[$base] = 1;
+ return $token;
+ }
+
+ return false;
+ }
+
+ /**
+ * Mark invitations for the given event as cancelled
+ *
+ * @param array Hash array with event properties
+ */
+ public function cancel_itip_invitation($event)
+ {
+ $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
+
+ // flag invitation record as cancelled
+ $this->rc->db->query(
+ "UPDATE $this->db_itipinvitations
+ SET `cancelled` = 1
+ WHERE `event_uid` = ? AND `user_id` = ?",
+ $event_uid,
+ $this->rc->user->ID
+ );
+ }
+
+ /**
+ * Generate an invitation request token for the given event and attendee
+ *
+ * @param array Event hash array
+ * @param string Attendee email address
+ */
+ public function generate_token($event, $attendee)
+ {
+ $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
+ $base = sha1($event_uid . ';' . $this->rc->user->ID);
+ $mail = base64_encode($attendee);
+ $hash = substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6);
+
+ return "$base.$mail.$hash";
+ }
+
+ /**
+ * Decode the given iTIP request token and return its parts
+ *
+ * @param string Request token to decode
+ * @return mixed Hash array with parts or False if invalid
+ */
+ public function decode_token($token)
+ {
+ list($base, $mail, $hash) = explode('.', $token);
+
+ // validate and return parts
+ if ($mail && $hash && $hash == substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6)) {
+ return array('base' => $base, 'attendee' => base64_decode($mail));
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to serialize the given event for storing in invitations table
+ */
+ private static function serialize_event($event)
+ {
+ $ev = $event;
+ $ev['description'] = abbreviate_string($ev['description'], 100);
+ unset($ev['attachments']);
+ return serialize($ev);
+ }
+
+}
diff --git a/calendar/lib/calendar_recurrence.php b/calendar/lib/calendar_recurrence.php
new file mode 100644
index 0000000..44d5b08
--- /dev/null
+++ b/calendar/lib/calendar_recurrence.php
@@ -0,0 +1,88 @@
+<?php
+
+require_once realpath(__DIR__ . '/../../libcalendaring/lib/libcalendaring_recurrence.php');
+
+/**
+ * Recurrence computation class for the Calendar plugin
+ *
+ * Uitility class to compute instances of recurring events.
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class calendar_recurrence extends libcalendaring_recurrence
+{
+ private $event;
+ private $duration;
+
+ /**
+ * Default constructor
+ *
+ * @param object calendar The calendar plugin instance
+ * @param array The event object to operate on
+ */
+ function __construct($cal, $event)
+ {
+ parent::__construct($cal->lib);
+
+ $this->event = $event;
+
+ if (is_object($event['start']) && is_object($event['end']))
+ $this->duration = $event['start']->diff($event['end']);
+
+ $event['start']->_dateonly |= $event['allday'];
+ $this->init($event['recurrence'], $event['start']);
+ }
+
+ /**
+ * Alias of libcalendaring_recurrence::next()
+ *
+ * @return mixed DateTime object or False if recurrence ended
+ */
+ public function next_start()
+ {
+ return $this->next();
+ }
+
+ /**
+ * Get the next recurring instance of this event
+ *
+ * @return mixed Array with event properties or False if recurrence ended
+ */
+ public function next_instance()
+ {
+ if ($next_start = $this->next()) {
+ $next = $this->event;
+ $next['start'] = $next_start;
+
+ if ($this->duration) {
+ $next['end'] = clone $next_start;
+ $next['end']->add($this->duration);
+ }
+
+ $next['recurrence_date'] = clone $next_start;
+ $next['_instance'] = libcalendaring::recurrence_instance_identifier($next);
+
+ unset($next['_formatobj']);
+
+ return $next;
+ }
+
+ return false;
+ }
+
+}
diff --git a/calendar/lib/calendar_ui.php b/calendar/lib/calendar_ui.php
new file mode 100644
index 0000000..aa74f52
--- /dev/null
+++ b/calendar/lib/calendar_ui.php
@@ -0,0 +1,918 @@
+<?php
+/**
+ * User Interface class for the Calendar plugin
+ *
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class calendar_ui
+{
+ private $rc;
+ private $cal;
+ private $ready = false;
+ public $screen;
+
+ function __construct($cal)
+ {
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ $this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ? $this->rc->action: 'calendar') : 'other';
+ }
+
+ /**
+ * Calendar UI initialization and requests handlers
+ */
+ public function init()
+ {
+ if ($this->ready) // already done
+ return;
+
+ // add taskbar button
+ $this->cal->add_button(array(
+ 'command' => 'calendar',
+ 'class' => 'button-calendar',
+ 'classsel' => 'button-calendar button-selected',
+ 'innerclass' => 'button-inner',
+ 'label' => 'calendar.calendar',
+ ), 'taskbar');
+
+ // load basic client script
+ $this->cal->include_script('calendar_base.js');
+
+ $skin_path = $this->cal->local_skin_path();
+ $this->cal->include_stylesheet($skin_path . '/calendar.css');
+
+ $this->ready = true;
+ }
+
+ /**
+ * Register handler methods for the template engine
+ */
+ public function init_templates()
+ {
+ $this->cal->register_handler('plugin.calendar_css', array($this, 'calendar_css'));
+ $this->cal->register_handler('plugin.calendar_list', array($this, 'calendar_list'));
+ $this->cal->register_handler('plugin.calendar_select', array($this, 'calendar_select'));
+ $this->cal->register_handler('plugin.identity_select', array($this, 'identity_select'));
+ $this->cal->register_handler('plugin.category_select', array($this, 'category_select'));
+ $this->cal->register_handler('plugin.status_select', array($this, 'status_select'));
+ $this->cal->register_handler('plugin.freebusy_select', array($this, 'freebusy_select'));
+ $this->cal->register_handler('plugin.priority_select', array($this, 'priority_select'));
+ $this->cal->register_handler('plugin.sensitivity_select', array($this, 'sensitivity_select'));
+ $this->cal->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
+ $this->cal->register_handler('plugin.recurrence_form', array($this->cal->lib, 'recurrence_form'));
+ $this->cal->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
+ $this->cal->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
+ $this->cal->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
+ $this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
+ $this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
+ $this->cal->register_handler('plugin.resources_form', array($this, 'resources_form'));
+ $this->cal->register_handler('plugin.resources_list', array($this, 'resources_list'));
+ $this->cal->register_handler('plugin.resources_searchform', array($this, 'resources_search_form'));
+ $this->cal->register_handler('plugin.resource_info', array($this, 'resource_info'));
+ $this->cal->register_handler('plugin.resource_calendar', array($this, 'resource_calendar'));
+ $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
+ $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
+ $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
+ $this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
+ $this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
+ $this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
+ $this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form'));
+ $this->cal->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
+ $this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
+ $this->cal->register_handler('plugin.calendar_create_menu', array($this, 'calendar_create_menu'));
+ }
+
+ /**
+ * Adds CSS stylesheets to the page header
+ */
+ public function addCSS()
+ {
+ $skin_path = $this->cal->local_skin_path();
+ $this->cal->include_stylesheet($skin_path . '/fullcalendar.css');
+ }
+
+ /**
+ * Adds JS files to the page header
+ */
+ public function addJS()
+ {
+ $this->cal->include_script('calendar_ui.js');
+ $this->cal->include_script('lib/js/fullcalendar.js');
+ $this->rc->output->include_script('treelist.js');
+
+ // include kolab folderlist widget if available
+ if (in_array('libkolab', $this->cal->api->loaded_plugins())) {
+ $this->cal->api->include_script('libkolab/js/folderlist.js');
+ $this->cal->api->include_script('libkolab/js/audittrail.js');
+ }
+
+ jqueryui::miniColors();
+ }
+
+ /**
+ *
+ */
+ function calendar_css($attrib = array())
+ {
+ $mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']);
+ $css = "\n";
+
+ foreach ($this->cal->get_drivers() as $name => $driver) {
+ $categories = $driver->list_categories();
+
+ foreach ((array)$categories as $class => $color) {
+ if (empty($color))
+ continue;
+
+ $class = 'cat-' . asciiwords(strtolower($class), true);
+ $css .= ".$class { color: #$color }\n";
+ if ($mode > 0) {
+ if ($mode == 2) {
+ $css .= ".fc-event-$class .fc-event-bg {";
+ $css .= " opacity: 0.9;";
+ $css .= " filter: alpha(opacity=90);";
+ } else {
+ $css .= ".fc-event-$class.fc-event-skin, ";
+ $css .= ".fc-event-$class .fc-event-skin, ";
+ $css .= ".fc-event-$class .fc-event-inner {";
+ }
+ $css .= " background-color: #" . $color . ";";
+ if ($mode % 2)
+ $css .= " border-color: #$color;";
+ $css .= "}\n";
+ }
+ }
+
+ $calendars = $driver->list_calendars();
+ foreach ((array)$calendars as $id => $prop) {
+ if (!$prop['color'])
+ continue;
+ $css .= $this->calendar_css_classes($id, $prop, $mode);
+ }
+ }
+
+ return html::tag('style', array('type' => 'text/css'), $css);
+ }
+
+ /**
+ *
+ */
+ public function calendar_css_classes($id, $prop, $mode)
+ {
+ $color = $prop['color'];
+ $class = 'cal-' . asciiwords($id, true);
+ $css .= "li .$class, #eventshow .$class { color: #$color }\n";
+
+ if ($mode != 1) {
+ if ($mode == 3) {
+ $css .= ".fc-event-$class .fc-event-bg {";
+ $css .= " opacity: 0.9;";
+ $css .= " filter: alpha(opacity=90);";
+ }
+ else {
+ $css .= ".fc-event-$class, ";
+ $css .= ".fc-event-$class .fc-event-inner {";
+ }
+ if (!$attrib['printmode']) // FIXME
+ $css .= " background-color: #$color;";
+ if ($mode % 2 == 0)
+ $css .= " border-color: #$color;";
+ $css .= "}\n";
+ }
+
+ return $css . ".$class .handle { background-color: #$color; }\n";
+ }
+
+ /**
+ *
+ */
+ function calendar_list($attrib = array())
+ {
+ $html = '';
+ $jsenv = array();
+ $tree = true;
+ // TODO: Check whether get_calendars() exists. Original $calendars = $this->cal->driver->list_calendars(0, $tree);
+ $calendars = $this->cal->get_calendars(false, false, $tree);
+
+ // walk folder tree
+ if (is_object($tree)) {
+ $html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
+
+ // append birthdays calendar which isn't part of $tree
+ if ($bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID]) {
+ $calendars = array(calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal);
+ }
+ else {
+ $calendars = array(); // clear array for flat listing
+ }
+ }
+ else {
+ // fall-back to flat folder listing
+ $attrib['class'] .= ' flat';
+ }
+
+ foreach ((array)$calendars as $id => $prop) {
+ if ($attrib['activeonly'] && !$prop['active'])
+ continue;
+
+ $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
+ $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly'])
+ );
+ }
+
+ $this->rc->output->set_env('calendars', $jsenv);
+ $this->rc->output->add_gui_object('calendarslist', $attrib['id']);
+
+ return html::tag('ul', $attrib, $html, html::$common_attrib);
+ }
+
+ /**
+ * Return html for a structured list <ul> for the folder tree
+ */
+ public function list_tree_html($node, $data, &$jsenv, $attrib)
+ {
+ $out = '';
+ foreach ($node->children as $folder) {
+ $id = $folder->id;
+ $prop = $data[$id];
+ $is_collapsed = false; // TODO: determine this somehow?
+
+ $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+
+ if (!empty($folder->children)) {
+ $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
+ $this->list_tree_html($folder, $data, $jsenv, $attrib));
+ }
+
+ if (strlen($content)) {
+ $out .= html::tag('li', array(
+ 'id' => 'rcmlical' . rcube_utils::html_identifier($id),
+ 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
+ ),
+ $content);
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Helper method to build a calendar list item (HTML content and js data)
+ */
+ public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
+ {
+ // enrich calendar properties with settings from the driver
+ if (!$prop['virtual']) {
+ unset($prop['user_id']);
+ $driver = $this->cal->get_driver_by_cal($id);
+ $prop['alarms'] = $driver->alarms;
+ $prop['attendees'] = $driver->attendees;
+ $prop['freebusy'] = $driver->freebusy;
+ $prop['attachments'] = $driver->attachments;
+ $prop['undelete'] = $driver->undelete;
+ $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
+
+ $jsenv[$id] = $prop;
+ }
+
+ $classes = array('calendar', 'cal-' . asciiwords($id, true));
+ $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+ html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
+
+ if ($prop['virtual'])
+ $classes[] = 'virtual';
+ else if (!$prop['editable'])
+ $classes[] = 'readonly';
+ if ($prop['subscribed'])
+ $classes[] = 'subscribed';
+ if ($prop['subscribed'] === 2)
+ $classes[] = 'partial';
+ if ($prop['class'])
+ $classes[] = $prop['class'];
+
+ $content = '';
+ if (!$activeonly || $prop['active']) {
+ $label_id = 'cl:' . $id;
+ $content = html::div(join(' ', $classes),
+ html::span(array('class' => 'calname', 'id' => $label_id, 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
+ ($prop['virtual'] ? '' :
+ html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id), '') .
+ html::span('actions',
+ ($prop['removable'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->cal->gettext('removelist')), ' ') : '') .
+ html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->cal->gettext('quickview'), 'role' => 'checkbox', 'aria-checked' => 'false'), '') .
+ (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '')
+ ) .
+ html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), '&nbsp;')
+ )
+ );
+ }
+
+ return $content;
+ }
+
+ /**
+ *
+ */
+ function angenda_options($attrib = array())
+ {
+ $attrib += array('id' => 'agendaoptions');
+ $attrib['style'] .= 'display:none';
+
+ $select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange'));
+ $select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->lib->gettext('days')), $days); // FIXME
+ foreach (array(2,5,7,14,30,60,90,180,365) as $days)
+ $select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->gettext('days')), $days);
+
+ $html = html::label('agenda-listrange', $this->cal->gettext('listrange'));
+ $html .= $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range']));
+
+ $select_sections = new html_select(array('name' => 'listsections', 'id' => 'agenda-listsections'));
+ $select_sections->add('---', '');
+ foreach (array('day' => 'libcalendaring.days', 'week' => 'libcalendaring.weeks', 'month' => 'libcalendaring.months', 'smart' => 'calendar.smartsections') as $val => $label)
+ $select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->rc->gettext($label))), $val);
+
+ $html .= html::span('spacer', '&nbsp;');
+ $html .= html::label('agenda-listsections', $this->cal->gettext('listsections'));
+ $html .= $select_sections->show($this->rc->config->get('calendar_agenda_sections', $this->cal->defaults['calendar_agenda_sections']));
+
+ return html::div($attrib, $html);
+ }
+
+ /**
+ * Render a HTML select box for calendar selection
+ */
+ function calendar_select($attrib = array())
+ {
+ $attrib['name'] = 'calendar';
+ $attrib['is_escaped'] = true;
+ $select = new html_select($attrib);
+
+ foreach ((array)$this->cal->get_calendars() as $id => $prop) {
+ if ($prop['editable'] || strpos($prop['rights'], 'i') !== false)
+ $select->add($prop['name'], $id);
+ }
+
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box for user identity selection
+ */
+ function identity_select($attrib = array())
+ {
+ $attrib['name'] = 'identity';
+ $select = new html_select($attrib);
+ $identities = $this->rc->user->list_emails();
+
+ foreach ($identities as $ident) {
+ $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
+ }
+
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box to select an event category
+ */
+ function category_select($attrib = array())
+ {
+ $attrib['name'] = 'categories';
+ $select = new html_select($attrib);
+ $select->add('---', '');
+ $keys = array();
+ foreach ($this->cal->get_drivers() as $driver) {
+ foreach((array)$driver->list_categories() as $key => $color) {
+ if ($color && !in_array($key, $keys)) {
+ $select->add($key, $key);
+ array_push($keys, $key);
+ }
+ }
+ }
+
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box for status property
+ */
+ function status_select($attrib = array())
+ {
+ $attrib['name'] = 'status';
+ $select = new html_select($attrib);
+ $select->add('---', '');
+ $select->add($this->cal->gettext('status-confirmed'), 'CONFIRMED');
+ $select->add($this->cal->gettext('status-cancelled'), 'CANCELLED');
+ //$select->add($this->cal->gettext('tentative'), 'TENTATIVE');
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box for free/busy/out-of-office property
+ */
+ function freebusy_select($attrib = array())
+ {
+ $attrib['name'] = 'freebusy';
+ $select = new html_select($attrib);
+ $select->add($this->cal->gettext('free'), 'free');
+ $select->add($this->cal->gettext('busy'), 'busy');
+ // out-of-office is not supported by libkolabxml (#3220)
+ // $select->add($this->cal->gettext('outofoffice'), 'outofoffice');
+ $select->add($this->cal->gettext('tentative'), 'tentative');
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select for event priorities
+ */
+ function priority_select($attrib = array())
+ {
+ $attrib['name'] = 'priority';
+ $select = new html_select($attrib);
+ $select->add('---', '0');
+ $select->add('1 '.$this->cal->gettext('highest'), '1');
+ $select->add('2 '.$this->cal->gettext('high'), '2');
+ $select->add('3 ', '3');
+ $select->add('4 ', '4');
+ $select->add('5 '.$this->cal->gettext('normal'), '5');
+ $select->add('6 ', '6');
+ $select->add('7 ', '7');
+ $select->add('8 '.$this->cal->gettext('low'), '8');
+ $select->add('9 '.$this->cal->gettext('lowest'), '9');
+ return $select->show(null);
+ }
+
+ /**
+ * Render HTML input for sensitivity selection
+ */
+ function sensitivity_select($attrib = array())
+ {
+ $attrib['name'] = 'sensitivity';
+ $select = new html_select($attrib);
+ $select->add($this->cal->gettext('public'), 'public');
+ $select->add($this->cal->gettext('private'), 'private');
+ $select->add($this->cal->gettext('confidential'), 'confidential');
+ return $select->show(null);
+ }
+
+ /**
+ * Render HTML form for alarm configuration
+ */
+ function alarm_select($attrib = array())
+ {
+ // Try GPC
+ $driver = $this->cal->get_driver_by_gpc(true /* quiet */);
+
+ // We assume that each calendar has equal alarm types, so fallback to default calendar is ok.
+ if(!$driver) $driver = $this->cal->get_default_driver();
+
+ return $this->cal->lib->alarm_select($attrib, $driver->alarm_types, $driver->alarm_absolute);
+ }
+
+ /**
+ *
+ */
+ function edit_attendees_notify($attrib = array())
+ {
+ $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1));
+ return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
+ }
+
+ /**
+ * Generate the form for recurrence settings
+ */
+ function recurring_event_warning($attrib = array())
+ {
+ $attrib['id'] = 'edit-recurring-warning';
+
+ $radio = new html_radiobutton(array('name' => '_savemode', 'class' => 'edit-recurring-savemode'));
+ $form = html::label(null, $radio->show('', array('value' => 'current')) . $this->cal->gettext('currentevent')) . ' ' .
+ html::label(null, $radio->show('', array('value' => 'future')) . $this->cal->gettext('futurevents')) . ' ' .
+ html::label(null, $radio->show('all', array('value' => 'all')) . $this->cal->gettext('allevents')) . ' ' .
+ html::label(null, $radio->show('', array('value' => 'new')) . $this->cal->gettext('saveasnew'));
+
+ return html::div($attrib, html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->cal->gettext('changerecurringeventwarning')) . html::div('savemode', $form));
+ }
+
+ /**
+ * Form for uploading and importing events
+ */
+ function events_import_form($attrib = array())
+ {
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmImportForm';
+
+ // Get max filesize, enable upload progress bar
+ $max_filesize = rcube_upload_init();
+
+ $accept = '.ics, text/calendar, text/x-vcalendar, application/ics';
+ if (class_exists('ZipArchive', false)) {
+ $accept .= ', .zip, application/zip';
+ }
+
+ $input = new html_inputfield(array(
+ 'type' => 'file', 'name' => '_data', 'size' => $attrib['uploadfieldsize'],
+ 'accept' => $accept));
+
+ $select = new html_select(array('name' => '_range', 'id' => 'event-import-range'));
+ $select->add(array(
+ $this->cal->gettext('onemonthback'),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
+ $this->cal->gettext('all'),
+ ),
+ array('1','2','3','6','12',0));
+
+ $html = html::div('form-section',
+ html::div(null, $input->show()) .
+ html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
+ );
+
+ $html .= html::div('form-section',
+ html::label('event-import-calendar', $this->cal->gettext('calendar')) .
+ $this->calendar_select(array('name' => 'calendar', 'id' => 'event-import-calendar'))
+ );
+
+ $html .= html::div('form-section',
+ html::label('event-import-range', $this->cal->gettext('importrange')) .
+ $select->show(1)
+ );
+
+ $this->rc->output->add_gui_object('importform', $attrib['id']);
+ $this->rc->output->add_label('import');
+
+ return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'import_events')),
+ 'method' => "post", 'enctype' => 'multipart/form-data', 'id' => $attrib['id']),
+ $html
+ );
+ }
+
+ /**
+ * Form to select options for exporting events
+ */
+ function events_export_form($attrib = array())
+ {
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmExportForm';
+
+ $html = html::div('form-section',
+ html::label('event-export-calendar', $this->cal->gettext('calendar')) .
+ $this->calendar_select(array('name' => 'calendar', 'id' => 'event-export-calendar'))
+ );
+
+ $select = new html_select(array('name' => 'range', 'id' => 'event-export-range'));
+ $select->add(array(
+ $this->cal->gettext('all'),
+ $this->cal->gettext('onemonthback'),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
+ $this->cal->gettext('customdate'),
+ ),
+ array(0,'1','2','3','6','12','custom'));
+
+ $startdate = new html_inputfield(array('name' => 'start', 'size' => 11, 'id' => 'event-export-startdate'));
+
+ $html .= html::div('form-section',
+ html::label('event-export-range', $this->cal->gettext('exportrange')) .
+ $select->show(0) .
+ html::span(array('style'=>'display:none'), $startdate->show())
+ );
+
+ $checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'event-export-attachments', 'value' => 1));
+ $html .= html::div('form-section',
+ html::label('event-export-range', $this->cal->gettext('exportattachments')) .
+ $checkbox->show(1)
+ );
+
+ $this->rc->output->add_gui_object('exportform', $attrib['id']);
+
+ return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'export_events')),
+ 'method' => "post", 'id' => $attrib['id']),
+ $html
+ );
+ }
+
+ /**
+ * Generate the form for event attachments upload
+ */
+ function attachments_form($attrib = array())
+ {
+ // add ID if not given
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmUploadForm';
+
+ // Get max filesize, enable upload progress bar
+ $max_filesize = rcube_upload_init();
+
+ $button = new html_inputfield(array('type' => 'button'));
+ $input = new html_inputfield(array(
+ 'type' => 'file', 'name' => '_attachments[]',
+ 'multiple' => 'multiple', 'size' => $attrib['attachmentfieldsize']));
+
+ return html::div($attrib,
+ html::div(null, $input->show()) .
+ html::div('formbuttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction',
+ 'onclick' => JS_OBJECT_NAME . ".upload_file(this.form)"))) .
+ html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
+ );
+ }
+
+ /**
+ * Register UI object for HTML5 drag & drop file upload
+ */
+ function file_drop_area($attrib = array())
+ {
+ if ($attrib['id']) {
+ $this->rc->output->add_gui_object('filedrop', $attrib['id']);
+ $this->rc->output->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments'));
+ }
+ }
+
+ /**
+ * Generate HTML element for attachments list
+ */
+ function attachments_list($attrib = array())
+ {
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmAttachmentList';
+
+ $skin_path = $this->cal->local_skin_path();
+ if ($attrib['deleteicon']) {
+ $_SESSION[calendar::SESSION_KEY . '_deleteicon'] = $skin_path . $attrib['deleteicon'];
+ $this->rc->output->set_env('deleteicon', $skin_path . $attrib['deleteicon']);
+ }
+ if ($attrib['cancelicon'])
+ $this->rc->output->set_env('cancelicon', $skin_path . $attrib['cancelicon']);
+ if ($attrib['loadingicon'])
+ $this->rc->output->set_env('loadingicon', $skin_path . $attrib['loadingicon']);
+
+ $this->rc->output->add_gui_object('attachmentlist', $attrib['id']);
+ $this->attachmentlist_id = $attrib['id'];
+
+ return html::tag('ul', $attrib, '', html::$common_attrib);
+ }
+
+ /**
+ * Handler for menu to choose the driver for calendar creation.
+ */
+ function calendar_create_menu($attrib = array())
+ {
+ $content = "";
+ foreach($this->cal->get_drivers() as $name => $driver)
+ {
+ $content .= html::tag('li', null, $this->rc->output->button(
+ array('label' => 'calendar.calendar_'.$name,
+ 'class' => 'active',
+ 'prop' => json_encode(array('driver' => $name)),
+ 'command' => 'calendar-create',
+ 'title' => 'calendar.createcalendar')));
+ }
+
+ return $content;
+ }
+
+ /**
+ * Handler for calendar form template.
+ * The form content could be overriden by the driver
+ */
+ function calendar_editform($action, $calendar = array())
+ {
+ // compose default calendar form fields
+ $input_name = new html_inputfield(array('name' => 'name', 'id' => 'calendar-name', 'size' => 20));
+ $input_color = new html_inputfield(array('name' => 'color', 'id' => 'calendar-color', 'size' => 6));
+ $driver = $this->cal->get_driver_by_gpc();
+
+ $formfields = array(
+ 'name' => array(
+ 'label' => $this->cal->gettext('name'),
+ 'value' => $input_name->show($calendar['name']),
+ 'id' => 'calendar-name',
+ ),
+ 'color' => array(
+ 'label' => $this->cal->gettext('color'),
+ 'value' => $input_color->show($calendar['color']),
+ 'id' => 'calendar-color',
+ ),
+ );
+
+ if ($driver->alarms) {
+ $checkbox = new html_checkbox(array('name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1));
+ $formfields['showalarms'] = array(
+ 'label' => $this->cal->gettext('showalarms'),
+ 'value' => $checkbox->show($calendar['showalarms']?1:0),
+ 'id' => 'calendar-showalarms',
+ );
+ }
+
+ // allow driver to extend or replace the form content
+ return html::tag('form', array('action' => "#", 'method' => "get", 'id' => 'calendarpropform'),
+ $driver->calendar_form($action, $calendar, $formfields)
+ );
+ }
+
+ /**
+ *
+ */
+ function attendees_list($attrib = array())
+ {
+ // add "noreply" checkbox to attendees table only
+ $invitations = strpos($attrib['id'], 'attend') !== false;
+
+ $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
+ $table = new html_table(array('cols' => 5 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
+
+ $table->add_header('role', $this->cal->gettext('role'));
+ $table->add_header('name', $this->cal->gettext($attrib['coltitle'] ?: 'attendee'));
+ $table->add_header('availability', $this->cal->gettext('availability'));
+ $table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
+ if ($invitations) {
+ $table->add_header(array('class' => 'invite', 'title' => $this->cal->gettext('sendinvitations')),
+ $invite->show(1) . html::label('edit-attendees-invite', $this->cal->gettext('sendinvitations')));
+ }
+ $table->add_header('options', '');
+
+ // hide invite column if disabled by config
+ $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->cal->defaults['calendar_itip_send_option']);
+ if ($invitations && !($itip_notify & 2)) {
+ $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']);
+ $this->rc->output->add_footer(html::tag('style', array('type' => 'text/css'), $css));
+ }
+
+ return $table->show($attrib);
+ }
+
+ /**
+ *
+ */
+ function attendees_form($attrib = array())
+ {
+ $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30));
+ $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment',
+ 'rows' => 4, 'cols' => 55, 'title' => $this->cal->gettext('itipcommenttitle')));
+
+ return html::div($attrib,
+ html::div(null, $input->show() . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->cal->gettext('addattendee'))) . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->cal->gettext('scheduletime').'...'))) .
+ html::p('attendees-commentbox', html::label(null, $this->cal->gettext('itipcomment') . $textarea->show()))
+ );
+ }
+
+ /**
+ *
+ */
+ function resources_form($attrib = array())
+ {
+ $input = new html_inputfield(array('name' => 'resource', 'id' => 'edit-resource-name', 'size' => 30));
+
+ return html::div($attrib,
+ html::div(null, $input->show() . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-add', 'value' => $this->cal->gettext('addresource'))) . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-find', 'value' => $this->cal->gettext('findresources').'...')))
+ );
+ }
+
+ /**
+ *
+ */
+ function resources_list($attrib = array())
+ {
+ $attrib += array('id' => 'calendar-resources-list');
+
+ $this->rc->output->add_gui_object('resourceslist', $attrib['id']);
+
+ return html::tag('ul', $attrib, '', html::$common_attrib);
+ }
+
+ /**
+ *
+ */
+ public function resource_info($attrib = array())
+ {
+ $attrib += array('id' => 'calendar-resources-info');
+
+ $this->rc->output->add_gui_object('resourceinfo', $attrib['id']);
+ $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner');
+
+ // copy address book labels for owner details to client
+ $this->rc->output->add_label('name','firstname','surname','department','jobtitle','email','phone','address');
+
+ $table_attrib = array('id','class','style','width','summary','cellpadding','cellspacing','border');
+
+ return html::tag('table', $attrib,
+ html::tag('tbody', null, ''), $table_attrib) .
+
+ html::tag('table', array('id' => $attrib['id'] . '-owner', 'style' => 'display:none') + $attrib,
+ html::tag('thead', null,
+ html::tag('tr', null,
+ html::tag('td', array('colspan' => 2), Q($this->cal->gettext('resourceowner')))
+ )
+ ) .
+ html::tag('tbody', null, ''),
+ $table_attrib);
+ }
+
+ /**
+ *
+ */
+ public function resource_calendar($attrib = array())
+ {
+ $attrib += array('id' => 'calendar-resources-calendar');
+
+ $this->rc->output->add_gui_object('resourceinfocalendar', $attrib['id']);
+
+ return html::div($attrib, '');
+ }
+
+ /**
+ * GUI object 'searchform' for the resource finder dialog
+ *
+ * @param array Named parameters
+ * @return string HTML code for the gui object
+ */
+ function resources_search_form($attrib)
+ {
+ $attrib += array('command' => 'search-resource', 'id' => 'rcmcalresqsearchbox', 'autocomplete' => 'off');
+ $attrib['name'] = '_q';
+
+ $input_q = new html_inputfield($attrib);
+ $out = $input_q->show();
+
+ // add form tag around text field
+ $out = $this->rc->output->form_tag(array(
+ 'name' => "rcmcalresoursqsearchform",
+ 'onsubmit' => rcmail_output::JS_OBJECT_NAME . ".command('" . $attrib['command'] . "'); return false",
+ 'style' => "display:inline"),
+ $out);
+
+ return $out;
+ }
+
+ /**
+ *
+ */
+ function attendees_freebusy_table($attrib = array())
+ {
+ $table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0));
+ $table->add('attendees',
+ html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees')) .
+ html::div('timesheader', '&nbsp;') .
+ html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '')
+ );
+ $table->add('times',
+ html::div('scroll',
+ html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')) .
+ html::div(array('id' => 'schedule-event-time', 'style' => 'display:none'), '&nbsp;')
+ )
+ );
+
+ return $table->show($attrib);
+ }
+
+ /**
+ *
+ */
+ function event_invitebox($attrib = array())
+ {
+ if ($this->cal->event) {
+ return html::div($attrib,
+ $this->cal->itip->itip_object_details_table($this->cal->event, $this->cal->itip->gettext('itipinvitation')) .
+ $this->cal->invitestatus
+ );
+ }
+
+ return '';
+ }
+
+ function event_rsvp_buttons($attrib = array())
+ {
+ $actions = array('accepted','tentative','declined');
+ if ($attrib['delegate'] !== 'false')
+ $actions[] = 'delegated';
+
+ return $this->cal->itip->itip_rsvp_buttons($attrib, $actions);
+ }
+
+}
diff --git a/calendar/lib/encryption.php b/calendar/lib/encryption.php
new file mode 100644
index 0000000..c503109
--- /dev/null
+++ b/calendar/lib/encryption.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * A class to handle secure encryption and decryption of arbitrary data
+ * Copyright http://stackoverflow.com/users/338665/ircmaxell
+ * http://stackoverflow.com/questions/5089841/php-2-way-encryption-i-need-to-store-passwords-that-can-be-retrieved
+ *
+ *
+ * Note that this is not just straight encryption. It also has a few other
+ * features in it to make the encrypted data far more secure. Note that any
+ * other implementations used to decrypt data will have to do the same exact
+ * operations.
+ *
+ * Security Benefits:
+ *
+ * - Uses Key stretching
+ * - Hides the Initialization Vector
+ * - Does HMAC verification of source data
+ *
+ */
+class Encryption {
+
+ /**
+ * @var string $cipher The mcrypt cipher to use for this instance
+ */
+ protected $cipher = '';
+
+ /**
+ * @var int $mode The mcrypt cipher mode to use
+ */
+ protected $mode = '';
+
+ /**
+ * @var int $rounds The number of rounds to feed into PBKDF2 for key generation
+ */
+ protected $rounds = 100;
+
+ /**
+ * Constructor!
+ *
+ * @param string $cipher The MCRYPT_* cypher to use for this instance
+ * @param int $mode The MCRYPT_MODE_* mode to use for this instance
+ * @param int $rounds The number of PBKDF2 rounds to do on the key
+ */
+ public function __construct($cipher, $mode, $rounds = 100) {
+ $this->cipher = $cipher;
+ $this->mode = $mode;
+ $this->rounds = (int) $rounds;
+ }
+
+ /**
+ * Decrypt the data with the provided key
+ *
+ * @param string $data The encrypted datat to decrypt
+ * @param string $key The key to use for decryption
+ *
+ * @returns string|false The returned string if decryption is successful
+ * false if it is not
+ */
+ public function decrypt($data, $key) {
+ $salt = substr($data, 0, 128);
+ $enc = substr($data, 128, -64);
+ $mac = substr($data, -64);
+
+ list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);
+
+ if ($mac !== hash_hmac('sha512', $enc, $macKey, true)) {
+ return false;
+ }
+
+ $dec = mcrypt_decrypt($this->cipher, $cipherKey, $enc, $this->mode, $iv);
+
+ $data = $this->unpad($dec);
+
+ return $data;
+ }
+
+ /**
+ * Encrypt the supplied data using the supplied key
+ *
+ * @param string $data The data to encrypt
+ * @param string $key The key to encrypt with
+ *
+ * @returns string The encrypted data
+ */
+ public function encrypt($data, $key) {
+ $salt = mcrypt_create_iv(128, MCRYPT_DEV_URANDOM);
+ list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);
+
+ $data = $this->pad($data);
+
+ $enc = mcrypt_encrypt($this->cipher, $cipherKey, $data, $this->mode, $iv);
+
+ $mac = hash_hmac('sha512', $enc, $macKey, true);
+ return $salt . $enc . $mac;
+ }
+
+ /**
+ * Generates a set of keys given a random salt and a master key
+ *
+ * @param string $salt A random string to change the keys each encryption
+ * @param string $key The supplied key to encrypt with
+ *
+ * @returns array An array of keys (a cipher key, a mac key, and a IV)
+ */
+ protected function getKeys($salt, $key) {
+ $ivSize = mcrypt_get_iv_size($this->cipher, $this->mode);
+ $keySize = mcrypt_get_key_size($this->cipher, $this->mode);
+ $length = 2 * $keySize + $ivSize;
+
+ $key = $this->pbkdf2('sha512', $key, $salt, $this->rounds, $length);
+
+ $cipherKey = substr($key, 0, $keySize);
+ $macKey = substr($key, $keySize, $keySize);
+ $iv = substr($key, 2 * $keySize);
+ return array($cipherKey, $macKey, $iv);
+ }
+
+ /**
+ * Stretch the key using the PBKDF2 algorithm
+ *
+ * @see http://en.wikipedia.org/wiki/PBKDF2
+ *
+ * @param string $algo The algorithm to use
+ * @param string $key The key to stretch
+ * @param string $salt A random salt
+ * @param int $rounds The number of rounds to derive
+ * @param int $length The length of the output key
+ *
+ * @returns string The derived key.
+ */
+ protected function pbkdf2($algo, $key, $salt, $rounds, $length) {
+ $size = strlen(hash($algo, '', true));
+ $len = ceil($length / $size);
+ $result = '';
+ for ($i = 1; $i <= $len; $i++) {
+ $tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true);
+ $res = $tmp;
+ for ($j = 1; $j < $rounds; $j++) {
+ $tmp = hash_hmac($algo, $tmp, $key, true);
+ $res ^= $tmp;
+ }
+ $result .= $res;
+ }
+ return substr($result, 0, $length);
+ }
+
+ protected function pad($data) {
+ $length = mcrypt_get_block_size($this->cipher, $this->mode);
+ $padAmount = $length - strlen($data) % $length;
+ if ($padAmount == 0) {
+ $padAmount = $length;
+ }
+ return $data . str_repeat(chr($padAmount), $padAmount);
+ }
+
+ protected function unpad($data) {
+ $length = mcrypt_get_block_size($this->cipher, $this->mode);
+ $last = ord($data[strlen($data) - 1]);
+ if ($last > $length) return false;
+ if (substr($data, -1 * $last) !== str_repeat(chr($last), $last)) {
+ return false;
+ }
+ return substr($data, 0, -1 * $last);
+ }
+}
+?>
diff --git a/calendar/lib/js/fullcalendar.js b/calendar/lib/js/fullcalendar.js
new file mode 100644
index 0000000..3b79107
--- /dev/null
+++ b/calendar/lib/js/fullcalendar.js
@@ -0,0 +1,6845 @@
+/*!
+ * FullCalendar v1.6.4-rcube-1.1.3
+ * Docs & License: http://arshaw.com/fullcalendar/
+ * (c) 2013 Adam Shaw, 2014 Kolab Systems AG
+ */
+
+/*
+ * Use fullcalendar.css for basic styling.
+ * For event drag & drop, requires jQuery UI draggable.
+ * For event resizing, requires jQuery UI resizable.
+ */
+
+(function($, undefined) {
+
+
+;;
+
+var defaults = {
+
+ // display
+ defaultView: 'month',
+ aspectRatio: 1.35,
+ header: {
+ left: 'title',
+ center: '',
+ right: 'today prev,next'
+ },
+ weekends: true,
+ weekNumbers: false,
+ weekNumberCalculation: 'iso',
+ weekNumberTitle: 'W',
+ currentTimeIndicator: false,
+
+ // editing
+ //editable: false,
+ //disableDragging: false,
+ //disableResizing: false,
+
+ allDayDefault: true,
+ ignoreTimezone: true,
+
+ // event ajax
+ lazyFetching: true,
+ startParam: 'start',
+ endParam: 'end',
+
+ // time formats
+ titleFormat: {
+ month: 'MMMM yyyy',
+ week: "MMM d[ yyyy]{ '&#8212;'[ MMM] d yyyy}",
+ day: 'dddd, MMM d, yyyy',
+ list: 'MMM d, yyyy',
+ table: 'MMM d, yyyy'
+ },
+ columnFormat: {
+ month: 'ddd',
+ week: 'ddd M/d',
+ day: 'dddd M/d',
+ list: 'dddd, MMM d, yyyy',
+ table: 'MMM d, yyyy'
+ },
+ timeFormat: { // for event elements
+ '': 'h(:mm)t' // default
+ },
+
+ // locale
+ isRTL: false,
+ firstDay: 0,
+ monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
+ monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
+ dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
+ dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
+ buttonText: {
+ prev: "<span class='fc-text-arrow'>&lsaquo;</span>",
+ next: "<span class='fc-text-arrow'>&rsaquo;</span>",
+ prevYear: "<span class='fc-text-arrow'>&laquo;</span>",
+ nextYear: "<span class='fc-text-arrow'>&raquo;</span>",
+ today: 'today',
+ month: 'month',
+ week: 'week',
+ day: 'day',
+ list: 'list',
+ table: 'table'
+ },
+ listTexts: {
+ until: 'until',
+ past: 'Past events',
+ today: 'Today',
+ tomorrow: 'Tomorrow',
+ thisWeek: 'This week',
+ nextWeek: 'Next week',
+ thisMonth: 'This month',
+ nextMonth: 'Next month',
+ future: 'Future events',
+ week: 'W'
+ },
+
+ // list/table options
+ listSections: 'month', // false|'day'|'week'|'month'|'smart'
+ listRange: 30, // number of days to be displayed
+ listPage: 7, // number of days to jump when paging
+ tableCols: ['handle', 'date', 'time', 'title'],
+
+ // jquery-ui theming
+ theme: false,
+ buttonIcons: {
+ prev: 'circle-triangle-w',
+ next: 'circle-triangle-e'
+ },
+
+ //selectable: false,
+ unselectAuto: true,
+
+ dropAccept: '*',
+
+ handleWindowResize: true
+
+};
+
+// right-to-left defaults
+var rtlDefaults = {
+ header: {
+ left: 'next,prev today',
+ center: '',
+ right: 'title'
+ },
+ buttonText: {
+ prev: "<span class='fc-text-arrow'>&rsaquo;</span>",
+ next: "<span class='fc-text-arrow'>&lsaquo;</span>",
+ prevYear: "<span class='fc-text-arrow'>&raquo;</span>",
+ nextYear: "<span class='fc-text-arrow'>&laquo;</span>"
+ },
+ buttonIcons: {
+ prev: 'circle-triangle-e',
+ next: 'circle-triangle-w'
+ }
+};
+
+
+
+;;
+
+var fc = $.fullCalendar = { version: "1.6.4-rcube-1.1.3" };
+var fcViews = fc.views = {};
+
+
+$.fn.fullCalendar = function(options) {
+
+
+ // method calling
+ if (typeof options == 'string') {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var res;
+ this.each(function() {
+ var calendar = $.data(this, 'fullCalendar');
+ if (calendar && $.isFunction(calendar[options])) {
+ var r = calendar[options].apply(calendar, args);
+ if (res === undefined) {
+ res = r;
+ }
+ if (options == 'destroy') {
+ $.removeData(this, 'fullCalendar');
+ }
+ }
+ });
+ if (res !== undefined) {
+ return res;
+ }
+ return this;
+ }
+
+ options = options || {};
+
+ // would like to have this logic in EventManager, but needs to happen before options are recursively extended
+ var eventSources = options.eventSources || [];
+ delete options.eventSources;
+ if (options.events) {
+ eventSources.push(options.events);
+ delete options.events;
+ }
+
+
+ options = $.extend(true, {},
+ defaults,
+ (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {},
+ options
+ );
+
+
+ this.each(function(i, _element) {
+ var element = $(_element);
+ var calendar = new Calendar(element, options, eventSources);
+ element.data('fullCalendar', calendar); // TODO: look into memory leak implications
+ calendar.render();
+ });
+
+
+ return this;
+
+};
+
+
+// function for adding/overriding defaults
+function setDefaults(d) {
+ $.extend(true, defaults, d);
+}
+
+
+
+;;
+
+
+function Calendar(element, options, eventSources) {
+ var t = this;
+
+
+ // exports
+ t.options = options;
+ t.render = render;
+ t.destroy = destroy;
+ t.refetchEvents = refetchEvents;
+ t.reportEvents = reportEvents;
+ t.reportEventChange = reportEventChange;
+ t.rerenderEvents = rerenderEvents;
+ t.changeView = changeView;
+ t.select = select;
+ t.unselect = unselect;
+ t.prev = prev;
+ t.next = next;
+ t.prevYear = prevYear;
+ t.nextYear = nextYear;
+ t.today = today;
+ t.gotoDate = gotoDate;
+ t.incrementDate = incrementDate;
+ t.formatDate = function(format, date) { return formatDate(format, date, options) };
+ t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) };
+ t.getDate = getDate;
+ t.getView = getView;
+ t.option = option;
+ t.trigger = trigger;
+
+
+ // imports
+ EventManager.call(t, options, eventSources);
+ var isFetchNeeded = t.isFetchNeeded;
+ var fetchEvents = t.fetchEvents;
+
+
+ // locals
+ var _element = element[0];
+ var header;
+ var headerElement;
+ var content;
+ var tm; // for making theme classes
+ var currentView;
+ var elementOuterWidth;
+ var suggestedViewHeight;
+ var resizeUID = 0;
+ var ignoreWindowResize = 0;
+ var lazyRendering = false;
+ var date = new Date();
+ var events = [];
+ var _dragElement;
+
+
+
+ /* Main Rendering
+ -----------------------------------------------------------------------------*/
+
+
+ setYMD(date, options.year, options.month, options.date);
+
+
+ function render(inc) {
+ if (!content) {
+ initialRender();
+ }
+ else if (elementVisible()) {
+ // mainly for the public API
+ calcSize();
+ _renderView(inc);
+ }
+ }
+
+
+ function initialRender() {
+ tm = options.theme ? 'ui' : 'fc';
+ element.addClass('fc');
+ if (options.isRTL) {
+ element.addClass('fc-rtl');
+ }
+ else {
+ element.addClass('fc-ltr');
+ }
+ if (options.theme) {
+ element.addClass('ui-widget');
+ }
+
+ content = $("<div class='fc-content' style='position:relative'/>")
+ .prependTo(element);
+
+ header = new Header(t, options);
+ headerElement = header.render();
+ if (headerElement) {
+ element.prepend(headerElement);
+ }
+
+ changeView(options.defaultView);
+
+ if (options.handleWindowResize) {
+ $(window).resize(windowResize);
+ }
+
+ // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
+ if (!bodyVisible()) {
+ lateRender();
+ }
+ }
+
+
+ // called when we know the calendar couldn't be rendered when it was initialized,
+ // but we think it's ready now
+ function lateRender() {
+ setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
+ if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
+ renderView();
+ }
+ },0);
+ }
+
+
+ function destroy() {
+
+ if (currentView) {
+ trigger('viewDestroy', currentView, currentView, currentView.element);
+ currentView.triggerEventDestroy();
+ }
+
+ $(window).unbind('resize', windowResize);
+
+ header.destroy();
+ content.remove();
+ element.removeClass('fc fc-rtl ui-widget');
+ }
+
+
+ function elementVisible() {
+ return element.is(':visible');
+ }
+
+
+ function bodyVisible() {
+ return $('body').is(':visible');
+ }
+
+
+
+ /* View Rendering
+ -----------------------------------------------------------------------------*/
+
+
+ function changeView(newViewName) {
+ if (!currentView || newViewName != currentView.name) {
+ _changeView(newViewName);
+ }
+ }
+
+
+ function _changeView(newViewName) {
+ ignoreWindowResize++;
+
+ if (currentView) {
+ trigger('viewDestroy', currentView, currentView, currentView.element);
+ unselect();
+ currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
+ freezeContentHeight();
+ currentView.element.remove();
+ header.deactivateButton(currentView.name);
+ }
+
+ header.activateButton(newViewName);
+
+ currentView = new fcViews[newViewName](
+ $("<div class='fc-view fc-view-" + newViewName + "' style='position:relative'/>")
+ .appendTo(content),
+ t // the calendar object
+ );
+
+ renderView();
+ unfreezeContentHeight();
+
+ ignoreWindowResize--;
+ }
+
+
+ function renderView(inc) {
+ if (
+ !currentView.start || // never rendered before
+ inc || date < currentView.start || date >= currentView.end // or new date range
+ ) {
+ if (elementVisible()) {
+ _renderView(inc);
+ }
+ }
+ }
+
+
+ function _renderView(inc) { // assumes elementVisible
+ ignoreWindowResize++;
+
+ if (currentView.start) { // already been rendered?
+ trigger('viewDestroy', currentView, currentView, currentView.element);
+ unselect();
+ clearEvents();
+ }
+
+ freezeContentHeight();
+ currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else
+ setSize();
+ unfreezeContentHeight();
+ (currentView.afterRender || noop)();
+
+ updateTitle();
+ updateTodayButton();
+
+ trigger('viewRender', currentView, currentView, currentView.element);
+ currentView.trigger('viewDisplay', _element); // deprecated
+
+ ignoreWindowResize--;
+
+ getAndRenderEvents();
+ }
+
+
+
+ /* Resizing
+ -----------------------------------------------------------------------------*/
+
+
+ function updateSize() {
+ if (elementVisible()) {
+ unselect();
+ clearEvents();
+ calcSize();
+ setSize();
+ unselect();
+ currentView.clearEvents();
+ currentView.trigger('viewRender', currentView);
+ currentView.renderEvents(events);
+ currentView.sizeDirty = false;
+ }
+ }
+
+
+ function calcSize() { // assumes elementVisible
+ if (options.contentHeight) {
+ suggestedViewHeight = options.contentHeight;
+ }
+ else if (options.height) {
+ suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
+ }
+ else {
+ suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
+ }
+ }
+
+
+ function setSize() { // assumes elementVisible
+
+ if (suggestedViewHeight === undefined) {
+ calcSize(); // for first time
+ // NOTE: we don't want to recalculate on every renderView because
+ // it could result in oscillating heights due to scrollbars.
+ }
+
+ ignoreWindowResize++;
+ currentView.setHeight(suggestedViewHeight);
+ currentView.setWidth(content.width());
+ ignoreWindowResize--;
+
+ elementOuterWidth = element.outerWidth();
+ }
+
+
+ function windowResize() {
+ if (!ignoreWindowResize) {
+ if (currentView.start) { // view has already been rendered
+ var uid = ++resizeUID;
+ setTimeout(function() { // add a delay
+ if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
+ if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
+ ignoreWindowResize++; // in case the windowResize callback changes the height
+ updateSize();
+ currentView.trigger('windowResize', _element);
+ ignoreWindowResize--;
+ }
+ }
+ }, 200);
+ }else{
+ // calendar must have been initialized in a 0x0 iframe that has just been resized
+ lateRender();
+ }
+ }
+ }
+
+
+
+ /* Event Fetching/Rendering
+ -----------------------------------------------------------------------------*/
+ // TODO: going forward, most of this stuff should be directly handled by the view
+
+
+ function refetchEvents(source, lazy) { // can be called as an API method
+ lazyRendering = lazy || false;
+ if (!lazyRendering) {
+ clearEvents();
+ }
+ fetchAndRenderEvents(source);
+ }
+
+
+ function rerenderEvents(modifiedEventID) { // can be called as an API method
+ clearEvents();
+ renderEvents(modifiedEventID);
+ }
+
+
+ function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack
+ if (elementVisible()) {
+ currentView.setEventData(events); // for View.js, TODO: unify with renderEvents
+ currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements
+ currentView.trigger('eventAfterAllRender');
+ }
+ }
+
+
+ function clearEvents() {
+ currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
+ currentView.clearEvents(); // actually remove the DOM elements
+ currentView.clearEventData(); // for View.js, TODO: unify with clearEvents
+ }
+
+
+ function getAndRenderEvents() {
+ if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) {
+ fetchAndRenderEvents();
+ }
+ else {
+ renderEvents();
+ }
+ }
+
+
+ function fetchAndRenderEvents(source) {
+ fetchEvents(currentView.visStart, currentView.visEnd, source);
+ // ... will call reportEvents
+ // ... which will call renderEvents
+ }
+
+
+ // called when event data arrives
+ function reportEvents(_events) {
+ if (lazyRendering) {
+ clearEvents();
+ lazyRendering = false;
+ }
+ events = _events;
+ renderEvents();
+ }
+
+
+ // called when a single event's data has been changed
+ function reportEventChange(eventID) {
+ rerenderEvents(eventID);
+ }
+
+
+
+ /* Header Updating
+ -----------------------------------------------------------------------------*/
+
+
+ function updateTitle() {
+ header.updateTitle(currentView.title);
+ }
+
+
+ function updateTodayButton() {
+ var today = new Date();
+ if (today >= currentView.start && today < currentView.end) {
+ header.disableButton('today');
+ }
+ else {
+ header.enableButton('today');
+ }
+ }
+
+
+
+ /* Selection
+ -----------------------------------------------------------------------------*/
+
+
+ function select(start, end, allDay) {
+ currentView.select(start, end, allDay===undefined ? true : allDay);
+ }
+
+
+ function unselect() { // safe to be called before renderView
+ if (currentView) {
+ currentView.unselect();
+ }
+ }
+
+
+
+ /* Date
+ -----------------------------------------------------------------------------*/
+
+
+ function prev() {
+ renderView(-1);
+ }
+
+
+ function next() {
+ renderView(1);
+ }
+
+
+ function prevYear() {
+ addYears(date, -1);
+ renderView();
+ }
+
+
+ function nextYear() {
+ addYears(date, 1);
+ renderView();
+ }
+
+
+ function today() {
+ date = new Date();
+ renderView();
+ }
+
+
+ function gotoDate(year, month, dateOfMonth) {
+ if (year instanceof Date) {
+ date = cloneDate(year); // provided 1 argument, a Date
+ }else{
+ setYMD(date, year, month, dateOfMonth);
+ }
+ renderView();
+ }
+
+
+ function incrementDate(years, months, days) {
+ if (years !== undefined) {
+ addYears(date, years);
+ }
+ if (months !== undefined) {
+ addMonths(date, months);
+ }
+ if (days !== undefined) {
+ addDays(date, days);
+ }
+ renderView();
+ }
+
+
+ function getDate() {
+ return cloneDate(date);
+ }
+
+
+
+ /* Height "Freezing"
+ -----------------------------------------------------------------------------*/
+
+
+ function freezeContentHeight() {
+ content.css({
+ width: '100%',
+ height: content.height(),
+ overflow: 'hidden'
+ });
+ }
+
+
+ function unfreezeContentHeight() {
+ content.css({
+ width: '',
+ height: '',
+ overflow: ''
+ });
+ }
+
+
+
+ /* Misc
+ -----------------------------------------------------------------------------*/
+
+
+ function getView() {
+ return currentView;
+ }
+
+
+ function option(name, value) {
+ if (value === undefined) {
+ return options[name];
+ }
+ if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
+ options[name] = value;
+ updateSize();
+ } else if (name.indexOf('list') == 0 || name == 'tableCols') {
+ options[name] = value;
+ currentView.start = null; // force re-render
+ } else if (name == 'maxHeight') {
+ options[name] = value;
+ }
+ }
+
+
+ function trigger(name, thisObj) {
+ if (options[name]) {
+ return options[name].apply(
+ thisObj || _element,
+ Array.prototype.slice.call(arguments, 2)
+ );
+ }
+ }
+
+
+
+ /* External Dragging
+ ------------------------------------------------------------------------*/
+
+ if (options.droppable) {
+ $(document)
+ .bind('dragstart', function(ev, ui) {
+ var _e = ev.target;
+ var e = $(_e);
+ if (!e.parents('.fc').length) { // not already inside a calendar
+ var accept = options.dropAccept;
+ if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
+ _dragElement = _e;
+ currentView.dragStart(_dragElement, ev, ui);
+ }
+ }
+ })
+ .bind('dragstop', function(ev, ui) {
+ if (_dragElement) {
+ currentView.dragStop(_dragElement, ev, ui);
+ _dragElement = null;
+ }
+ });
+ }
+
+
+}
+
+;;
+
+function Header(calendar, options) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+ t.destroy = destroy;
+ t.updateTitle = updateTitle;
+ t.activateButton = activateButton;
+ t.deactivateButton = deactivateButton;
+ t.disableButton = disableButton;
+ t.enableButton = enableButton;
+
+
+ // locals
+ var element = $([]);
+ var tm;
+
+
+
+ function render() {
+ tm = options.theme ? 'ui' : 'fc';
+ var sections = options.header;
+ if (sections) {
+ element = $("<table class='fc-header' style='width:100%'/>")
+ .append(
+ $("<tr/>")
+ .append(renderSection('left'))
+ .append(renderSection('center'))
+ .append(renderSection('right'))
+ );
+ return element;
+ }
+ }
+
+
+ function destroy() {
+ element.remove();
+ }
+
+
+ function renderSection(position) {
+ var e = $("<td class='fc-header-" + position + "'/>");
+ var buttonStr = options.header[position];
+ if (buttonStr) {
+ $.each(buttonStr.split(' '), function(i) {
+ if (i > 0) {
+ e.append("<span class='fc-header-space'/>");
+ }
+ var prevButton;
+ $.each(this.split(','), function(j, buttonName) {
+ if (buttonName == 'title') {
+ e.append("<span class='fc-header-title'><h2 aria-live='polite' aria-relevant='text' aria-atomic='true'>&nbsp;</h2></span>");
+ if (prevButton) {
+ prevButton.addClass(tm + '-corner-right');
+ }
+ prevButton = null;
+ }else{
+ var buttonClick;
+ if (calendar[buttonName]) {
+ buttonClick = calendar[buttonName]; // calendar method
+ }
+ else if (fcViews[buttonName]) {
+ buttonClick = function() {
+ button.removeClass(tm + '-state-hover'); // forget why
+ calendar.changeView(buttonName);
+ };
+ }
+ if (buttonClick) {
+ var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
+ var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
+ var button = $(
+ "<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default' role='button' tabindex='0'>" +
+ (icon ?
+ "<span class='fc-icon-wrap'>" +
+ "<span class='ui-icon ui-icon-" + icon + "'/>" +
+ "</span>" :
+ text
+ ) +
+ "</span>"
+ )
+ .click(function() {
+ if (!button.hasClass(tm + '-state-disabled')) {
+ buttonClick();
+ }
+ })
+ .mousedown(function() {
+ button
+ .not('.' + tm + '-state-active')
+ .not('.' + tm + '-state-disabled')
+ .addClass(tm + '-state-down');
+ })
+ .mouseup(function() {
+ button.removeClass(tm + '-state-down');
+ })
+ .hover(
+ function() {
+ button
+ .not('.' + tm + '-state-active')
+ .not('.' + tm + '-state-disabled')
+ .addClass(tm + '-state-hover');
+ },
+ function() {
+ button
+ .removeClass(tm + '-state-hover')
+ .removeClass(tm + '-state-down');
+ }
+ )
+ .keypress(function(ev) {
+ if (ev.keyCode == 13)
+ $(ev.target).trigger('click');
+ })
+ .appendTo(e);
+ disableTextSelection(button);
+ if (!prevButton) {
+ button.addClass(tm + '-corner-left');
+ }
+ prevButton = button;
+ }
+ }
+ });
+ if (prevButton) {
+ prevButton.addClass(tm + '-corner-right');
+ }
+ });
+ }
+ return e;
+ }
+
+
+ function updateTitle(html) {
+ element.find('h2')
+ .html(html);
+ }
+
+
+ function activateButton(buttonName) {
+ element.find('span.fc-button-' + buttonName)
+ .addClass(tm + '-state-active').attr('tabindex', '-1');
+ }
+
+
+ function deactivateButton(buttonName) {
+ element.find('span.fc-button-' + buttonName)
+ .removeClass(tm + '-state-active').attr('tabindex', '0');
+ }
+
+
+ function disableButton(buttonName) {
+ element.find('span.fc-button-' + buttonName)
+ .addClass(tm + '-state-disabled').attr('tabindex', '-1');
+ }
+
+
+ function enableButton(buttonName) {
+ element.find('span.fc-button-' + buttonName)
+ .removeClass(tm + '-state-disabled').attr('tabindex', '0');
+ }
+
+
+}
+
+;;
+
+fc.sourceNormalizers = [];
+fc.sourceFetchers = [];
+
+var ajaxDefaults = {
+ dataType: 'json',
+ cache: false
+};
+
+var eventGUID = 1;
+
+
+function EventManager(options, _sources) {
+ var t = this;
+
+
+ // exports
+ t.isFetchNeeded = isFetchNeeded;
+ t.fetchEvents = fetchEvents;
+ t.addEventSource = addEventSource;
+ t.removeEventSource = removeEventSource;
+ t.removeEventSources = removeEventSources;
+ t.updateEvent = updateEvent;
+ t.renderEvent = renderEvent;
+ t.removeEvents = removeEvents;
+ t.clientEvents = clientEvents;
+ t.normalizeEvent = normalizeEvent;
+
+
+ // imports
+ var trigger = t.trigger;
+ var getView = t.getView;
+ var reportEvents = t.reportEvents;
+
+
+ // locals
+ var stickySource = { events: [] };
+ var sources = [ stickySource ];
+ var rangeStart, rangeEnd;
+ var currentFetchID = 0;
+ var pendingSourceCnt = 0;
+ var loadingLevel = 0;
+ var cache = [];
+
+
+ for (var i=0; i<_sources.length; i++) {
+ _addEventSource(_sources[i]);
+ }
+
+
+
+ /* Fetching
+ -----------------------------------------------------------------------------*/
+
+
+ function isFetchNeeded(start, end) {
+ return !rangeStart || start < rangeStart || end > rangeEnd;
+ }
+
+
+ function fetchEvents(start, end, src) {
+ rangeStart = start;
+ rangeEnd = end;
+ // partially clear cache if refreshing one source only (issue #1061)
+ cache = typeof src != 'undefined' ? $.grep(cache, function(e) { return !isSourcesEqual(e.source, src); }) : [];
+ var fetchID = ++currentFetchID;
+ var len = sources.length;
+ pendingSourceCnt = typeof src == 'undefined' ? len : 1;
+ for (var i=0; i<len; i++) {
+ if (typeof src == 'undefined' || isSourcesEqual(sources[i], src))
+ fetchEventSource(sources[i], fetchID);
+ }
+ }
+
+
+
+ function fetchEventSource(source, fetchID) {
+ _fetchEventSource(source, function(events) {
+ if (fetchID == currentFetchID) {
+ if (events) {
+
+ if (options.eventDataTransform) {
+ events = $.map(events, options.eventDataTransform);
+ }
+ if (source.eventDataTransform) {
+ events = $.map(events, source.eventDataTransform);
+ }
+ // TODO: this technique is not ideal for static array event sources.
+ // For arrays, we'll want to process all events right in the beginning, then never again.
+
+ for (var i=0; i<events.length; i++) {
+ events[i].source = source;
+ normalizeEvent(events[i]);
+ }
+ cache = cache.concat(events);
+ }
+ pendingSourceCnt--;
+ if (!pendingSourceCnt) {
+ reportEvents(cache);
+ }
+ }
+ });
+ }
+
+
+ function _fetchEventSource(source, callback) {
+ var i;
+ var fetchers = fc.sourceFetchers;
+ var res;
+ for (i=0; i<fetchers.length; i++) {
+ res = fetchers[i](source, rangeStart, rangeEnd, callback);
+ if (res === true) {
+ // the fetcher is in charge. made its own async request
+ return;
+ }
+ else if (typeof res == 'object') {
+ // the fetcher returned a new source. process it
+ _fetchEventSource(res, callback);
+ return;
+ }
+ }
+ var events = source.events;
+ if (events) {
+ if ($.isFunction(events)) {
+ pushLoading();
+ events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
+ callback(events);
+ popLoading();
+ });
+ }
+ else if ($.isArray(events)) {
+ callback(events);
+ }
+ else {
+ callback();
+ }
+ }else{
+ var url = source.url;
+ if (url) {
+ var success = source.success;
+ var error = source.error;
+ var complete = source.complete;
+
+ // retrieve any outbound GET/POST $.ajax data from the options
+ var customData;
+ if ($.isFunction(source.data)) {
+ // supplied as a function that returns a key/value object
+ customData = source.data();
+ }
+ else {
+ // supplied as a straight key/value object
+ customData = source.data;
+ }
+
+ // use a copy of the custom data so we can modify the parameters
+ // and not affect the passed-in object.
+ var data = $.extend({}, customData || {});
+
+ var startParam = firstDefined(source.startParam, options.startParam);
+ var endParam = firstDefined(source.endParam, options.endParam);
+ if (startParam) {
+ data[startParam] = Math.round(+rangeStart / 1000);
+ }
+ if (endParam) {
+ data[endParam] = Math.round(+rangeEnd / 1000);
+ }
+
+ pushLoading();
+ $.ajax($.extend({}, ajaxDefaults, source, {
+ data: data,
+ success: function(events) {
+ events = events || [];
+ var res = applyAll(success, this, arguments);
+ if ($.isArray(res)) {
+ events = res;
+ }
+ callback(events);
+ },
+ error: function() {
+ applyAll(error, this, arguments);
+ callback();
+ },
+ complete: function() {
+ applyAll(complete, this, arguments);
+ popLoading();
+ }
+ }));
+ }else{
+ callback();
+ }
+ }
+ }
+
+
+
+ /* Sources
+ -----------------------------------------------------------------------------*/
+
+
+ function addEventSource(source) {
+ source = _addEventSource(source);
+ if (source) {
+ pendingSourceCnt++;
+ fetchEventSource(source, currentFetchID); // will eventually call reportEvents
+ }
+ }
+
+
+ function _addEventSource(source) {
+ if ($.isFunction(source) || $.isArray(source)) {
+ source = { events: source };
+ }
+ else if (typeof source == 'string') {
+ source = { url: source };
+ }
+ if (typeof source == 'object') {
+ normalizeSource(source);
+ sources.push(source);
+ return source;
+ }
+ }
+
+
+ function removeEventSource(source) {
+ sources = $.grep(sources, function(src) {
+ return !isSourcesEqual(src, source);
+ });
+ // remove all client events from that source
+ cache = $.grep(cache, function(e) {
+ return !isSourcesEqual(e.source, source);
+ });
+ reportEvents(cache);
+ }
+
+
+ function removeEventSources() {
+ sources = [];
+ removeEvents();
+ }
+
+
+
+ /* Manipulation
+ -----------------------------------------------------------------------------*/
+
+
+ function updateEvent(event) { // update an existing event
+ var i, len = cache.length, e,
+ defaultEventEnd = getView().defaultEventEnd, // getView???
+ startDelta = event.start - event._start,
+ endDelta = event.end ?
+ (event.end - (event._end || defaultEventEnd(event))) // event._end would be null if event.end
+ : 0; // was null and event was just resized
+ for (i=0; i<len; i++) {
+ e = cache[i];
+ if (e._id == event._id && e != event) {
+ e.start = new Date(+e.start + startDelta);
+ if (event.end) {
+ if (e.end) {
+ e.end = new Date(+e.end + endDelta);
+ }else{
+ e.end = new Date(+defaultEventEnd(e) + endDelta);
+ }
+ }else{
+ e.end = null;
+ }
+ e.title = event.title;
+ e.url = event.url;
+ e.allDay = event.allDay;
+ e.className = event.className;
+ e.editable = event.editable;
+ e.color = event.color;
+ e.backgroundColor = event.backgroundColor;
+ e.borderColor = event.borderColor;
+ e.textColor = event.textColor;
+ normalizeEvent(e);
+ }
+ }
+ normalizeEvent(event);
+ reportEvents(cache);
+ }
+
+
+ function renderEvent(event, stick) {
+ normalizeEvent(event);
+ if (!event.source) {
+ if (stick) {
+ stickySource.events.push(event);
+ event.source = stickySource;
+ }
+ }
+ // always push event to cache (issue #1112:)
+ cache.push(event);
+ reportEvents(cache);
+ }
+
+
+ function removeEvents(filter) {
+ if (!filter) { // remove all
+ cache = [];
+ // clear all array sources
+ for (var i=0; i<sources.length; i++) {
+ if ($.isArray(sources[i].events)) {
+ sources[i].events = [];
+ }
+ }
+ }else{
+ if (!$.isFunction(filter)) { // an event ID
+ var id = filter + '';
+ filter = function(e) {
+ return e._id == id;
+ };
+ }
+ cache = $.grep(cache, filter, true);
+ // remove events from array sources
+ for (var i=0; i<sources.length; i++) {
+ if ($.isArray(sources[i].events)) {
+ sources[i].events = $.grep(sources[i].events, filter, true);
+ }
+ }
+ }
+ reportEvents(cache);
+ }
+
+
+ function clientEvents(filter) {
+ if ($.isFunction(filter)) {
+ return $.grep(cache, filter);
+ }
+ else if (filter) { // an event ID
+ filter += '';
+ return $.grep(cache, function(e) {
+ return e._id == filter;
+ });
+ }
+ return cache; // else, return all
+ }
+
+
+
+ /* Loading State
+ -----------------------------------------------------------------------------*/
+
+
+ function pushLoading() {
+ if (!loadingLevel++) {
+ trigger('loading', null, true, getView());
+ }
+ }
+
+
+ function popLoading() {
+ if (!--loadingLevel) {
+ trigger('loading', null, false, getView());
+ }
+ }
+
+
+
+ /* Event Normalization
+ -----------------------------------------------------------------------------*/
+
+
+ function normalizeEvent(event) {
+ var source = event.source || {};
+ var ignoreTimezone = firstDefined(source.ignoreTimezone, options.ignoreTimezone);
+ event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
+ if (event.date) {
+ if (!event.start) {
+ event.start = event.date;
+ }
+ delete event.date;
+ }
+ event._start = cloneDate(event.start = parseDate(event.start, ignoreTimezone));
+ event.end = parseDate(event.end, ignoreTimezone);
+ if (event.end && event.end <= event.start) {
+ event.end = null;
+ }
+ event._end = event.end ? cloneDate(event.end) : null;
+ if (event.allDay === undefined) {
+ event.allDay = firstDefined(source.allDayDefault, options.allDayDefault);
+ }
+ if (event.className) {
+ if (typeof event.className == 'string') {
+ event.className = event.className.split(/\s+/);
+ }
+ }else{
+ event.className = [];
+ }
+ // TODO: if there is no start date, return false to indicate an invalid event
+ }
+
+
+
+ /* Utils
+ ------------------------------------------------------------------------------*/
+
+
+ function normalizeSource(source) {
+ if (source.className) {
+ // TODO: repeat code, same code for event classNames
+ if (typeof source.className == 'string') {
+ source.className = source.className.split(/\s+/);
+ }
+ }else{
+ source.className = [];
+ }
+ var normalizers = fc.sourceNormalizers;
+ for (var i=0; i<normalizers.length; i++) {
+ normalizers[i](source);
+ }
+ }
+
+
+ function isSourcesEqual(source1, source2) {
+ return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
+ }
+
+
+ function getSourcePrimitive(source) {
+ return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
+ }
+
+
+}
+
+;;
+
+
+fc.addDays = addDays;
+fc.cloneDate = cloneDate;
+fc.parseDate = parseDate;
+fc.parseISO8601 = parseISO8601;
+fc.parseTime = parseTime;
+fc.formatDate = formatDate;
+fc.formatDates = formatDates;
+
+
+
+/* Date Math
+-----------------------------------------------------------------------------*/
+
+var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
+ DAY_MS = 86400000,
+ HOUR_MS = 3600000,
+ MINUTE_MS = 60000;
+
+
+function addYears(d, n, keepTime) {
+ d.setFullYear(d.getFullYear() + n);
+ if (!keepTime) {
+ clearTime(d);
+ }
+ return d;
+}
+
+
+function addMonths(d, n, keepTime) { // prevents day overflow/underflow
+ if (+d) { // prevent infinite looping on invalid dates
+ var m = d.getMonth() + n,
+ check = cloneDate(d);
+ check.setDate(1);
+ check.setMonth(m);
+ d.setMonth(m);
+ if (!keepTime) {
+ clearTime(d);
+ }
+ while (d.getMonth() != check.getMonth()) {
+ d.setDate(d.getDate() + (d < check ? 1 : -1));
+ }
+ }
+ return d;
+}
+
+
+function addDays(d, n, keepTime) { // deals with daylight savings
+ if (+d) {
+ var dd = d.getDate() + n,
+ check = cloneDate(d);
+ check.setHours(9); // set to middle of day
+ check.setDate(dd);
+ d.setDate(dd);
+ if (!keepTime) {
+ clearTime(d);
+ }
+ fixDate(d, check);
+ }
+ return d;
+}
+
+
+function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes
+ if (+d) { // prevent infinite looping on invalid dates
+ while (d.getDate() != check.getDate()) {
+ d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
+ }
+ }
+}
+
+
+function addMinutes(d, n) {
+ d.setMinutes(d.getMinutes() + n);
+ return d;
+}
+
+
+function clearTime(d) {
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(0);
+ d.setMilliseconds(0);
+ return d;
+}
+
+
+function cloneDate(d, dontKeepTime) {
+ if (dontKeepTime) {
+ return clearTime(new Date(+d));
+ }
+ return new Date(+d);
+}
+
+
+function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1
+ var i=0, d;
+ do {
+ d = new Date(1970, i++, 1);
+ } while (d.getHours()); // != 0
+ return d;
+}
+
+
+function dayDiff(d1, d2) { // d1 - d2
+ return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS);
+}
+
+
+function setYMD(date, y, m, d) {
+ if (y !== undefined && y != date.getFullYear()) {
+ date.setDate(1);
+ date.setMonth(0);
+ date.setFullYear(y);
+ }
+ if (m !== undefined && m != date.getMonth()) {
+ date.setDate(1);
+ date.setMonth(m);
+ }
+ if (d !== undefined) {
+ date.setDate(d);
+ }
+}
+
+
+
+/* Date Parsing
+-----------------------------------------------------------------------------*/
+
+
+function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true
+ if (typeof s == 'object') { // already a Date object
+ return s;
+ }
+ if (typeof s == 'number') { // a UNIX timestamp
+ return new Date(s * 1000);
+ }
+ if (typeof s == 'string') {
+ if (s.match(/^\d+(\.\d+)?$/)) { // a UNIX timestamp
+ return new Date(parseFloat(s) * 1000);
+ }
+ if (ignoreTimezone === undefined) {
+ ignoreTimezone = true;
+ }
+ return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null);
+ }
+ // TODO: never return invalid dates (like from new Date(<string>)), return null instead
+ return null;
+}
+
+
+function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false
+ // derived from http://delete.me.uk/2005/03/iso8601.html
+ // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html
+ var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
+ if (!m) {
+ return null;
+ }
+ var date = new Date(m[1], 0, 2);
+ if (ignoreTimezone || !m[13]) {
+ var check = new Date(m[1], 0, 2, 9, 0);
+ if (m[3]) {
+ date.setMonth(m[3] - 1);
+ check.setMonth(m[3] - 1);
+ }
+ if (m[5]) {
+ date.setDate(m[5]);
+ check.setDate(m[5]);
+ }
+ fixDate(date, check);
+ if (m[7]) {
+ date.setHours(m[7]);
+ }
+ if (m[8]) {
+ date.setMinutes(m[8]);
+ }
+ if (m[10]) {
+ date.setSeconds(m[10]);
+ }
+ if (m[12]) {
+ date.setMilliseconds(Number("0." + m[12]) * 1000);
+ }
+ fixDate(date, check);
+ }else{
+ date.setUTCFullYear(
+ m[1],
+ m[3] ? m[3] - 1 : 0,
+ m[5] || 1
+ );
+ date.setUTCHours(
+ m[7] || 0,
+ m[8] || 0,
+ m[10] || 0,
+ m[12] ? Number("0." + m[12]) * 1000 : 0
+ );
+ if (m[14]) {
+ var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0);
+ offset *= m[15] == '-' ? 1 : -1;
+ date = new Date(+date + (offset * 60 * 1000));
+ }
+ }
+ return date;
+}
+
+
+function parseTime(s) { // returns minutes since start of day
+ if (typeof s == 'number') { // an hour
+ return s * 60;
+ }
+ if (typeof s == 'object') { // a Date object
+ return s.getHours() * 60 + s.getMinutes();
+ }
+ var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/);
+ if (m) {
+ var h = parseInt(m[1], 10);
+ if (m[3]) {
+ h %= 12;
+ if (m[3].toLowerCase().charAt(0) == 'p') {
+ h += 12;
+ }
+ }
+ return h * 60 + (m[2] ? parseInt(m[2], 10) : 0);
+ }
+}
+
+
+
+/* Date Formatting
+-----------------------------------------------------------------------------*/
+// TODO: use same function formatDate(date, [date2], format, [options])
+
+
+function formatDate(date, format, options) {
+ return formatDates(date, null, format, options);
+}
+
+
+function formatDates(date1, date2, format, options) {
+ options = options || defaults;
+ var date = date1,
+ otherDate = date2,
+ i, len = format.length, c,
+ i2, formatter,
+ res = '';
+ for (i=0; i<len; i++) {
+ c = format.charAt(i);
+ if (c == "'") {
+ for (i2=i+1; i2<len; i2++) {
+ if (format.charAt(i2) == "'") {
+ if (date) {
+ if (i2 == i+1) {
+ res += "'";
+ }else{
+ res += format.substring(i+1, i2);
+ }
+ i = i2;
+ }
+ break;
+ }
+ }
+ }
+ else if (c == '(') {
+ for (i2=i+1; i2<len; i2++) {
+ if (format.charAt(i2) == ')') {
+ var subres = formatDate(date, format.substring(i+1, i2), options);
+ if (parseInt(subres.replace(/\D/, ''), 10)) {
+ res += subres;
+ }
+ i = i2;
+ break;
+ }
+ }
+ }
+ else if (c == '[') {
+ for (i2=i+1; i2<len; i2++) {
+ if (format.charAt(i2) == ']') {
+ var subformat = format.substring(i+1, i2);
+ var subres = formatDate(date, subformat, options);
+ if (subres != formatDate(otherDate, subformat, options)) {
+ res += subres;
+ }
+ i = i2;
+ break;
+ }
+ }
+ }
+ else if (c == '{') {
+ date = date2;
+ otherDate = date1;
+ }
+ else if (c == '}') {
+ date = date1;
+ otherDate = date2;
+ }
+ else {
+ for (i2=len; i2>i; i2--) {
+ if (formatter = dateFormatters[format.substring(i, i2)]) {
+ if (date) {
+ res += formatter(date, options);
+ }
+ i = i2 - 1;
+ break;
+ }
+ }
+ if (i2 == i) {
+ if (date) {
+ res += c;
+ }
+ }
+ }
+ }
+ return res;
+};
+
+
+var dateFormatters = {
+ s : function(d) { return d.getSeconds() },
+ ss : function(d) { return zeroPad(d.getSeconds()) },
+ m : function(d) { return d.getMinutes() },
+ mm : function(d) { return zeroPad(d.getMinutes()) },
+ h : function(d) { return d.getHours() % 12 || 12 },
+ hh : function(d) { return zeroPad(d.getHours() % 12 || 12) },
+ H : function(d) { return d.getHours() },
+ HH : function(d) { return zeroPad(d.getHours()) },
+ d : function(d) { return d.getDate() },
+ dd : function(d) { return zeroPad(d.getDate()) },
+ ddd : function(d,o) { return o.dayNamesShort[d.getDay()] },
+ dddd: function(d,o) { return o.dayNames[d.getDay()] },
+ M : function(d) { return d.getMonth() + 1 },
+ MM : function(d) { return zeroPad(d.getMonth() + 1) },
+ MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] },
+ MMMM: function(d,o) { return o.monthNames[d.getMonth()] },
+ yy : function(d) { return (d.getFullYear()+'').substring(2) },
+ yyyy: function(d) { return d.getFullYear() },
+ t : function(d) { return d.getHours() < 12 ? 'a' : 'p' },
+ tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
+ T : function(d) { return d.getHours() < 12 ? 'A' : 'P' },
+ TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' },
+ u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") },
+ S : function(d) {
+ var date = d.getDate();
+ if (date > 10 && date < 20) {
+ return 'th';
+ }
+ return ['st', 'nd', 'rd'][date%10-1] || 'th';
+ },
+ w : function(d, o) { // local
+ return o.weekNumberCalculation(d);
+ },
+ W : function(d) { // ISO
+ return iso8601Week(d);
+ }
+};
+fc.dateFormatters = dateFormatters;
+
+
+/* thanks jQuery UI (https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js)
+ *
+ * Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
+ * `date` - the date to get the week for
+ * `number` - the number of the week within the year that contains this date
+ */
+function iso8601Week(date) {
+ var time;
+ var checkDate = new Date(date.getTime());
+
+ // Find Thursday of this week starting on Monday
+ checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
+
+ time = checkDate.getTime();
+ checkDate.setMonth(0); // Compare with Jan 1
+ checkDate.setDate(1);
+ return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+}
+
+// Determine the week of the year based on the ISO 8601 definition.
+// copied from jquery UI Datepicker
+var iso8601Week = function(date) {
+ var checkDate = cloneDate(date);
+ // Find Thursday of this week starting on Monday
+ checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
+ var time = checkDate.getTime();
+ checkDate.setMonth(0); // Compare with Jan 1
+ checkDate.setDate(1);
+ return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+};
+
+
+;;
+
+fc.applyAll = applyAll;
+
+
+/* Event Date Math
+-----------------------------------------------------------------------------*/
+
+
+function exclEndDay(event) {
+ if (event.end) {
+ return _exclEndDay(event.end, event.allDay);
+ }else{
+ return addDays(cloneDate(event.start), 1);
+ }
+}
+
+
+function _exclEndDay(end, allDay) {
+ end = cloneDate(end);
+ return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
+ // why don't we check for seconds/ms too?
+}
+
+
+
+/* Event Element Binding
+-----------------------------------------------------------------------------*/
+
+
+function lazySegBind(container, segs, bindHandlers) {
+ container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
+ var parent=ev.target, e,
+ i, seg;
+ while (parent != this) {
+ e = parent;
+ parent = parent.parentNode;
+ }
+ if ((i = e._fci) !== undefined) {
+ e._fci = undefined;
+ seg = segs[i];
+ bindHandlers(seg.event, seg.element, seg);
+ $(ev.target).trigger(ev);
+ }
+ ev.stopPropagation();
+ });
+}
+
+
+
+/* Element Dimensions
+-----------------------------------------------------------------------------*/
+
+
+function setOuterWidth(element, width, includeMargins) {
+ for (var i=0, e; i<element.length; i++) {
+ e = $(element[i]);
+ e.width(Math.max(0, width - hsides(e, includeMargins)));
+ }
+}
+
+
+function setOuterHeight(element, height, includeMargins) {
+ for (var i=0, e; i<element.length; i++) {
+ e = $(element[i]);
+ e.height(Math.max(0, height - vsides(e, includeMargins)));
+ }
+}
+
+
+function hsides(element, includeMargins) {
+ return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
+}
+
+
+function hpadding(element) {
+ return (parseFloat($.css(element[0], 'paddingLeft', true)) || 0) +
+ (parseFloat($.css(element[0], 'paddingRight', true)) || 0);
+}
+
+
+function hmargins(element) {
+ return (parseFloat($.css(element[0], 'marginLeft', true)) || 0) +
+ (parseFloat($.css(element[0], 'marginRight', true)) || 0);
+}
+
+
+function hborders(element) {
+ return (parseFloat($.css(element[0], 'borderLeftWidth', true)) || 0) +
+ (parseFloat($.css(element[0], 'borderRightWidth', true)) || 0);
+}
+
+
+function vsides(element, includeMargins) {
+ return vpadding(element) + vborders(element) + (includeMargins ? vmargins(element) : 0);
+}
+
+
+function vpadding(element) {
+ return (parseFloat($.css(element[0], 'paddingTop', true)) || 0) +
+ (parseFloat($.css(element[0], 'paddingBottom', true)) || 0);
+}
+
+
+function vmargins(element) {
+ return (parseFloat($.css(element[0], 'marginTop', true)) || 0) +
+ (parseFloat($.css(element[0], 'marginBottom', true)) || 0);
+}
+
+
+function vborders(element) {
+ return (parseFloat($.css(element[0], 'borderTopWidth', true)) || 0) +
+ (parseFloat($.css(element[0], 'borderBottomWidth', true)) || 0);
+}
+
+
+
+/* Misc Utils
+-----------------------------------------------------------------------------*/
+
+
+//TODO: arraySlice
+//TODO: isFunction, grep ?
+
+
+function noop() { }
+
+
+function dateCompare(a, b) {
+ return a - b;
+}
+
+
+function arrayMax(a) {
+ return Math.max.apply(Math, a);
+}
+
+
+function zeroPad(n) {
+ return (n < 10 ? '0' : '') + n;
+}
+
+
+function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
+ if (obj[name] !== undefined) {
+ return obj[name];
+ }
+ var parts = name.split(/(?=[A-Z])/),
+ i=parts.length-1, res;
+ for (; i>=0; i--) {
+ res = obj[parts[i].toLowerCase()];
+ if (res !== undefined) {
+ return res;
+ }
+ }
+ return obj[''];
+}
+
+
+function htmlEscape(s) {
+ return s.replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/'/g, '&#039;')
+ .replace(/"/g, '&quot;')
+ .replace(/\n/g, '<br />');
+}
+
+
+function disableTextSelection(element) {
+ element
+ .attr('unselectable', 'on')
+ .css('MozUserSelect', 'none')
+ .bind('selectstart.ui', function() { return false; });
+}
+
+
+/*
+function enableTextSelection(element) {
+ element
+ .attr('unselectable', 'off')
+ .css('MozUserSelect', '')
+ .unbind('selectstart.ui');
+}
+*/
+
+
+function markFirstLast(e) {
+ e.children()
+ .removeClass('fc-first fc-last')
+ .filter(':first-child')
+ .addClass('fc-first')
+ .end()
+ .filter(':last-child')
+ .addClass('fc-last');
+}
+
+
+function setDayID(cell, date) {
+ cell.each(function(i, _cell) {
+ _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]);
+ // TODO: make a way that doesn't rely on order of classes
+ });
+}
+
+
+function getSkinCss(event, opt) {
+ var source = event.source || {};
+ var eventColor = event.color;
+ var sourceColor = source.color;
+ var optionColor = opt('eventColor');
+ var backgroundColor =
+ event.backgroundColor ||
+ eventColor ||
+ source.backgroundColor ||
+ sourceColor ||
+ opt('eventBackgroundColor') ||
+ optionColor;
+ var borderColor =
+ event.borderColor ||
+ eventColor ||
+ source.borderColor ||
+ sourceColor ||
+ opt('eventBorderColor') ||
+ optionColor;
+ var textColor =
+ event.textColor ||
+ source.textColor ||
+ opt('eventTextColor');
+ var statements = [];
+ if (backgroundColor) {
+ statements.push('background-color:' + backgroundColor);
+ }
+ if (borderColor) {
+ statements.push('border-color:' + borderColor);
+ }
+ if (textColor) {
+ statements.push('color:' + textColor);
+ }
+ return statements.join(';');
+}
+
+
+function applyAll(functions, thisObj, args) {
+ if ($.isFunction(functions)) {
+ functions = [ functions ];
+ }
+ if (functions) {
+ var i;
+ var ret;
+ for (i=0; i<functions.length; i++) {
+ ret = functions[i].apply(thisObj, args) || ret;
+ }
+ return ret;
+ }
+}
+
+
+function firstDefined() {
+ for (var i=0; i<arguments.length; i++) {
+ if (arguments[i] !== undefined) {
+ return arguments[i];
+ }
+ }
+}
+
+
+;;
+
+fcViews.month = MonthView;
+
+function MonthView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ BasicView.call(t, element, calendar, 'month');
+ var opt = t.opt;
+ var renderBasic = t.renderBasic;
+ var skipHiddenDays = t.skipHiddenDays;
+ var getCellsPerWeek = t.getCellsPerWeek;
+ var formatDate = calendar.formatDate;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addMonths(date, delta);
+ date.setDate(1);
+ }
+
+ var firstDay = opt('firstDay');
+
+ var start = cloneDate(date, true);
+ start.setDate(1);
+
+ var end = addMonths(cloneDate(start), 1);
+
+ var visStart = cloneDate(start);
+ addDays(visStart, -((visStart.getDay() - firstDay + 7) % 7));
+ skipHiddenDays(visStart);
+
+ var visEnd = cloneDate(end);
+ addDays(visEnd, (7 - visEnd.getDay() + firstDay) % 7);
+ skipHiddenDays(visEnd, -1, true);
+
+ var colCnt = getCellsPerWeek();
+ var rowCnt = Math.round(dayDiff(visEnd, visStart) / 7); // should be no need for Math.round
+
+ if (opt('weekMode') == 'fixed') {
+ addDays(visEnd, (6 - rowCnt) * 7); // add weeks to make up for it
+ rowCnt = 6;
+ }
+
+ t.title = formatDate(start, opt('titleFormat'));
+
+ t.start = start;
+ t.end = end;
+ t.visStart = visStart;
+ t.visEnd = visEnd;
+
+ renderBasic(rowCnt, colCnt, true);
+ }
+
+
+}
+
+;;
+
+fcViews.basicWeek = BasicWeekView;
+
+function BasicWeekView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ BasicView.call(t, element, calendar, 'basicWeek');
+ var opt = t.opt;
+ var renderBasic = t.renderBasic;
+ var skipHiddenDays = t.skipHiddenDays;
+ var getCellsPerWeek = t.getCellsPerWeek;
+ var formatDates = calendar.formatDates;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addDays(date, delta * 7);
+ }
+
+ var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
+ var end = addDays(cloneDate(start), 7);
+
+ var visStart = cloneDate(start);
+ skipHiddenDays(visStart);
+
+ var visEnd = cloneDate(end);
+ skipHiddenDays(visEnd, -1, true);
+
+ var colCnt = getCellsPerWeek();
+
+ t.start = start;
+ t.end = end;
+ t.visStart = visStart;
+ t.visEnd = visEnd;
+
+ t.title = formatDates(
+ visStart,
+ addDays(cloneDate(visEnd), -1),
+ opt('titleFormat')
+ );
+
+ renderBasic(1, colCnt, false);
+ }
+
+
+}
+
+;;
+
+fcViews.basicDay = BasicDayView;
+
+
+function BasicDayView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ BasicView.call(t, element, calendar, 'basicDay');
+ var opt = t.opt;
+ var renderBasic = t.renderBasic;
+ var skipHiddenDays = t.skipHiddenDays;
+ var formatDate = calendar.formatDate;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addDays(date, delta);
+ }
+ skipHiddenDays(date, delta < 0 ? -1 : 1);
+
+ var start = cloneDate(date, true);
+ var end = addDays(cloneDate(start), 1);
+
+ t.title = formatDate(date, opt('titleFormat'));
+
+ t.start = t.visStart = start;
+ t.end = t.visEnd = end;
+
+ renderBasic(1, 1, false);
+ }
+
+
+}
+
+;;
+
+setDefaults({
+ weekMode: 'fixed'
+});
+
+
+function BasicView(element, calendar, viewName) {
+ var t = this;
+
+
+ // exports
+ t.renderBasic = renderBasic;
+ t.setHeight = setHeight;
+ t.setWidth = setWidth;
+ t.renderDayOverlay = renderDayOverlay;
+ t.defaultSelectionEnd = defaultSelectionEnd;
+ t.renderSelection = renderSelection;
+ t.clearSelection = clearSelection;
+ t.reportDayClick = reportDayClick; // for selection (kinda hacky)
+ t.dragStart = dragStart;
+ t.dragStop = dragStop;
+ t.defaultEventEnd = defaultEventEnd;
+ t.getHoverListener = function() { return hoverListener };
+ t.colLeft = colLeft;
+ t.colRight = colRight;
+ t.colContentLeft = colContentLeft;
+ t.colContentRight = colContentRight;
+ t.getIsCellAllDay = function() { return true };
+ t.allDayRow = allDayRow;
+ t.getRowCnt = function() { return rowCnt };
+ t.getColCnt = function() { return colCnt };
+ t.getColWidth = function() { return colWidth };
+ t.getDaySegmentContainer = function() { return daySegmentContainer };
+
+
+ // imports
+ View.call(t, element, calendar, viewName);
+ OverlayManager.call(t);
+ SelectionManager.call(t);
+ BasicEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var renderOverlay = t.renderOverlay;
+ var clearOverlays = t.clearOverlays;
+ var daySelectionMousedown = t.daySelectionMousedown;
+ var cellToDate = t.cellToDate;
+ var dateToCell = t.dateToCell;
+ var rangeToSegments = t.rangeToSegments;
+ var formatDate = calendar.formatDate;
+
+
+ // locals
+
+ var table;
+ var head;
+ var headCells;
+ var body;
+ var bodyRows;
+ var bodyCells;
+ var bodyFirstCells;
+ var firstRowCellInners;
+ var firstRowCellContentInners;
+ var daySegmentContainer;
+
+ var viewWidth;
+ var viewHeight;
+ var colWidth;
+ var weekNumberWidth;
+
+ var rowCnt, colCnt;
+ var showNumbers;
+ var coordinateGrid;
+ var hoverListener;
+ var colPositions;
+ var colContentPositions;
+
+ var tm;
+ var colFormat;
+ var showWeekNumbers;
+ var weekNumberTitle;
+ var weekNumberFormat;
+
+
+
+ /* Rendering
+ ------------------------------------------------------------*/
+
+
+ disableTextSelection(element.addClass('fc-grid'));
+
+
+ function renderBasic(_rowCnt, _colCnt, _showNumbers) {
+ rowCnt = _rowCnt;
+ colCnt = _colCnt;
+ showNumbers = _showNumbers;
+ updateOptions();
+
+ if (!body) {
+ buildEventContainer();
+ }
+
+ buildTable();
+ }
+
+
+ function updateOptions() {
+ tm = opt('theme') ? 'ui' : 'fc';
+ colFormat = opt('columnFormat');
+
+ // week # options. (TODO: bad, logic also in other views)
+ showWeekNumbers = opt('weekNumbers');
+ weekNumberTitle = opt('weekNumberTitle');
+ if (opt('weekNumberCalculation') != 'iso') {
+ weekNumberFormat = "w";
+ }
+ else {
+ weekNumberFormat = "W";
+ }
+ }
+
+
+ function buildEventContainer() {
+ daySegmentContainer =
+ $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
+ .appendTo(element);
+ }
+
+
+ function buildTable() {
+ var html = buildTableHTML();
+
+ if (table) {
+ table.remove();
+ }
+ table = $(html).appendTo(element);
+
+ head = table.find('thead');
+ headCells = head.find('.fc-day-header');
+ body = table.find('tbody');
+ bodyRows = body.find('tr');
+ bodyCells = body.find('.fc-day');
+ bodyFirstCells = bodyRows.find('td:first-child');
+
+ firstRowCellInners = bodyRows.eq(0).find('.fc-day > div');
+ firstRowCellContentInners = bodyRows.eq(0).find('.fc-day-content > div');
+
+ markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
+ markFirstLast(bodyRows); // marks first+last td's
+ bodyRows.eq(0).addClass('fc-first');
+ bodyRows.filter(':last').addClass('fc-last');
+
+ bodyCells.each(function(i, _cell) {
+ var date = cellToDate(
+ Math.floor(i / colCnt),
+ i % colCnt
+ );
+ trigger('dayRender', t, date, $(_cell));
+ });
+
+ dayBind(bodyCells);
+ }
+
+
+
+ /* HTML Building
+ -----------------------------------------------------------*/
+
+
+ function buildTableHTML() {
+ var html =
+ "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
+ buildHeadHTML() +
+ buildBodyHTML() +
+ "</table>";
+
+ return html;
+ }
+
+
+ function buildHeadHTML() {
+ var headerClass = tm + "-widget-header";
+ var html = '';
+ var col;
+ var date;
+
+ html += "<thead><tr>";
+
+ if (showWeekNumbers) {
+ html +=
+ "<th class='fc-week-number " + headerClass + "'>" +
+ htmlEscape(weekNumberTitle) +
+ "</th>";
+ }
+
+ for (col=0; col<colCnt; col++) {
+ date = cellToDate(0, col);
+ html +=
+ "<th class='fc-day-header fc-" + dayIDs[date.getDay()] + " " + headerClass + "'>" +
+ htmlEscape(formatDate(date, colFormat)) +
+ "</th>";
+ }
+
+ html += "</tr></thead>";
+
+ return html;
+ }
+
+
+ function buildBodyHTML() {
+ var contentClass = tm + "-widget-content";
+ var html = '';
+ var row;
+ var col;
+ var date;
+
+ html += "<tbody>";
+
+ for (row=0; row<rowCnt; row++) {
+
+ html += "<tr class='fc-week'>";
+
+ if (showWeekNumbers) {
+ date = cellToDate(row, 0);
+ html +=
+ "<td class='fc-week-number " + contentClass + "'>" +
+ "<div>" +
+ htmlEscape(formatDate(date, weekNumberFormat)) +
+ "</div>" +
+ "</td>";
+ }
+
+ for (col=0; col<colCnt; col++) {
+ date = cellToDate(row, col);
+ html += buildCellHTML(date);
+ }
+
+ html += "</tr>";
+ }
+
+ html += "</tbody>";
+
+ return html;
+ }
+
+
+ function buildCellHTML(date) {
+ var contentClass = tm + "-widget-content";
+ var month = t.start.getMonth();
+ var today = clearTime(new Date());
+ var html = '';
+ var classNames = [
+ 'fc-day',
+ 'fc-' + dayIDs[date.getDay()],
+ contentClass
+ ];
+
+ if (date.getMonth() != month) {
+ classNames.push('fc-other-month');
+ }
+ if (+date == +today) {
+ classNames.push(
+ 'fc-today',
+ tm + '-state-highlight'
+ );
+ }
+ else if (date < today) {
+ classNames.push('fc-past');
+ }
+ else {
+ classNames.push('fc-future');
+ }
+
+ html +=
+ "<td" +
+ " class='" + classNames.join(' ') + "'" +
+ " data-date='" + formatDate(date, 'yyyy-MM-dd') + "'" +
+ ">" +
+ "<div>";
+
+ if (showNumbers) {
+ html += "<div class='fc-day-number'>" + date.getDate() + "</div>";
+ }
+
+ html +=
+ "<div class='fc-day-content'>" +
+ "<div style='position:relative'>&nbsp;</div>" +
+ "</div>" +
+ "</div>" +
+ "</td>";
+
+ return html;
+ }
+
+
+
+ /* Dimensions
+ -----------------------------------------------------------*/
+
+
+ function setHeight(height) {
+ viewHeight = height;
+
+ var bodyHeight = viewHeight - head.height();
+ var rowHeight;
+ var rowHeightLast;
+ var cell;
+
+ if (opt('weekMode') == 'variable') {
+ rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
+ }else{
+ rowHeight = Math.floor(bodyHeight / rowCnt);
+ rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
+ }
+
+ bodyFirstCells.each(function(i, _cell) {
+ if (i < rowCnt) {
+ cell = $(_cell);
+ cell.find('> div').css(
+ 'min-height',
+ (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
+ );
+ }
+ });
+
+ }
+
+
+ function setWidth(width) {
+ viewWidth = width;
+ colPositions.clear();
+ colContentPositions.clear();
+
+ weekNumberWidth = 0;
+ if (showWeekNumbers) {
+ weekNumberWidth = head.find('th.fc-week-number').outerWidth();
+ }
+
+ colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt);
+ setOuterWidth(headCells.slice(0, -1), colWidth);
+ }
+
+
+
+ /* Day clicking and binding
+ -----------------------------------------------------------*/
+
+
+ function dayBind(days) {
+ days.click(dayClick)
+ .mousedown(daySelectionMousedown);
+ }
+
+
+ function dayClick(ev) {
+ if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
+ var date = parseISO8601($(this).data('date'));
+ trigger('dayClick', this, date, true, ev);
+ }
+ }
+
+
+
+ /* Semi-transparent Overlay Helpers
+ ------------------------------------------------------*/
+ // TODO: should be consolidated with AgendaView's methods
+
+
+ function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
+
+ if (refreshCoordinateGrid) {
+ coordinateGrid.build();
+ }
+
+ var segments = rangeToSegments(overlayStart, overlayEnd);
+
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+ dayBind(
+ renderCellOverlay(
+ segment.row,
+ segment.leftCol,
+ segment.row,
+ segment.rightCol
+ )
+ );
+ }
+ }
+
+
+ function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
+ var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
+ return renderOverlay(rect, element);
+ }
+
+
+
+ /* Selection
+ -----------------------------------------------------------------------*/
+
+
+ function defaultSelectionEnd(startDate, allDay) {
+ return cloneDate(startDate);
+ }
+
+
+ function renderSelection(startDate, endDate, allDay) {
+ renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
+ }
+
+
+ function clearSelection() {
+ clearOverlays();
+ }
+
+
+ function reportDayClick(date, allDay, ev) {
+ var cell = dateToCell(date);
+ var _element = bodyCells[cell.row*colCnt + cell.col];
+ trigger('dayClick', _element, date, allDay, ev);
+ }
+
+
+
+ /* External Dragging
+ -----------------------------------------------------------------------*/
+
+
+ function dragStart(_dragElement, ev, ui) {
+ hoverListener.start(function(cell) {
+ clearOverlays();
+ if (cell) {
+ renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
+ }
+ }, ev);
+ }
+
+
+ function dragStop(_dragElement, ev, ui) {
+ var cell = hoverListener.stop();
+ clearOverlays();
+ if (cell) {
+ var d = cellToDate(cell);
+ trigger('drop', _dragElement, d, true, ev, ui);
+ }
+ }
+
+
+
+ /* Utilities
+ --------------------------------------------------------*/
+
+
+ function defaultEventEnd(event) {
+ return cloneDate(event.start);
+ }
+
+
+ coordinateGrid = new CoordinateGrid(function(rows, cols) {
+ var e, n, p;
+ headCells.each(function(i, _e) {
+ e = $(_e);
+ n = e.offset().left;
+ if (i) {
+ p[1] = n;
+ }
+ p = [n];
+ cols[i] = p;
+ });
+ p[1] = n + e.outerWidth();
+ bodyRows.each(function(i, _e) {
+ if (i < rowCnt) {
+ e = $(_e);
+ n = e.offset().top;
+ if (i) {
+ p[1] = n;
+ }
+ p = [n];
+ rows[i] = p;
+ }
+ });
+ p[1] = n + e.outerHeight();
+ });
+
+
+ hoverListener = new HoverListener(coordinateGrid);
+
+ colPositions = new HorizontalPositionCache(function(col) {
+ return firstRowCellInners.eq(col);
+ });
+
+ colContentPositions = new HorizontalPositionCache(function(col) {
+ return firstRowCellContentInners.eq(col);
+ });
+
+
+ function colLeft(col) {
+ return colPositions.left(col);
+ }
+
+
+ function colRight(col) {
+ return colPositions.right(col);
+ }
+
+
+ function colContentLeft(col) {
+ return colContentPositions.left(col);
+ }
+
+
+ function colContentRight(col) {
+ return colContentPositions.right(col);
+ }
+
+
+ function allDayRow(i) {
+ return bodyRows.eq(i);
+ }
+
+}
+
+;;
+
+function BasicEventRenderer() {
+ var t = this;
+
+
+ // exports
+ t.renderEvents = renderEvents;
+ t.clearEvents = clearEvents;
+
+
+ // imports
+ DayEventRenderer.call(t);
+
+
+ function renderEvents(events, modifiedEventId) {
+ t.renderDayEvents(events, modifiedEventId);
+ }
+
+
+ function clearEvents() {
+ t.getDaySegmentContainer().empty();
+ }
+
+
+ // TODO: have this class (and AgendaEventRenderer) be responsible for creating the event container div
+
+}
+
+;;
+
+fcViews.agendaWeek = AgendaWeekView;
+
+function AgendaWeekView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ AgendaView.call(t, element, calendar, 'agendaWeek');
+ var opt = t.opt;
+ var renderAgenda = t.renderAgenda;
+ var skipHiddenDays = t.skipHiddenDays;
+ var getCellsPerWeek = t.getCellsPerWeek;
+ var formatDates = calendar.formatDates;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addDays(date, delta * 7);
+ }
+
+ var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
+ var end = addDays(cloneDate(start), 7);
+
+ var visStart = cloneDate(start);
+ skipHiddenDays(visStart);
+
+ var visEnd = cloneDate(end);
+ skipHiddenDays(visEnd, -1, true);
+
+ var colCnt = getCellsPerWeek();
+
+ t.title = formatDates(
+ visStart,
+ addDays(cloneDate(visEnd), -1),
+ opt('titleFormat')
+ );
+
+ t.start = start;
+ t.end = end;
+ t.visStart = visStart;
+ t.visEnd = visEnd;
+
+ renderAgenda(colCnt);
+ }
+
+}
+
+;;
+
+fcViews.agendaDay = AgendaDayView;
+
+
+function AgendaDayView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ AgendaView.call(t, element, calendar, 'agendaDay');
+ var opt = t.opt;
+ var renderAgenda = t.renderAgenda;
+ var skipHiddenDays = t.skipHiddenDays;
+ var formatDate = calendar.formatDate;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addDays(date, delta);
+ }
+ skipHiddenDays(date, delta < 0 ? -1 : 1);
+
+ var start = cloneDate(date, true);
+ var end = addDays(cloneDate(start), 1);
+
+ t.title = formatDate(date, opt('titleFormat'));
+
+ t.start = t.visStart = start;
+ t.end = t.visEnd = end;
+
+ renderAgenda(1);
+ }
+
+
+}
+
+;;
+
+setDefaults({
+ allDaySlot: true,
+ allDayText: 'all-day',
+ firstHour: 6,
+ slotMinutes: 30,
+ defaultEventMinutes: 120,
+ axisFormat: 'h(:mm)tt',
+ timeFormat: {
+ agenda: 'h:mm{ - h:mm}'
+ },
+ dragOpacity: {
+ agenda: .5
+ },
+ minTime: 0,
+ maxTime: 24,
+ slotEventOverlap: true
+});
+
+
+// TODO: make it work in quirks mode (event corners, all-day height)
+// TODO: test liquid width, especially in IE6
+
+
+function AgendaView(element, calendar, viewName) {
+ var t = this;
+
+
+ // exports
+ t.renderAgenda = renderAgenda;
+ t.setWidth = setWidth;
+ t.setHeight = setHeight;
+ t.afterRender = afterRender;
+ t.defaultEventEnd = defaultEventEnd;
+ t.timePosition = timePosition;
+ t.getIsCellAllDay = getIsCellAllDay;
+ t.allDayRow = getAllDayRow;
+ t.getCoordinateGrid = function() { return coordinateGrid }; // specifically for AgendaEventRenderer
+ t.getHoverListener = function() { return hoverListener };
+ t.colLeft = colLeft;
+ t.colRight = colRight;
+ t.colContentLeft = colContentLeft;
+ t.colContentRight = colContentRight;
+ t.getDaySegmentContainer = function() { return daySegmentContainer };
+ t.getSlotSegmentContainer = function() { return slotSegmentContainer };
+ t.getMinMinute = function() { return minMinute };
+ t.getMaxMinute = function() { return maxMinute };
+ t.getSlotContainer = function() { return slotContainer };
+ t.getRowCnt = function() { return 1 };
+ t.getColCnt = function() { return colCnt };
+ t.getColWidth = function() { return colWidth };
+ t.getSnapHeight = function() { return snapHeight };
+ t.getSnapMinutes = function() { return snapMinutes };
+ t.defaultSelectionEnd = defaultSelectionEnd;
+ t.renderDayOverlay = renderDayOverlay;
+ t.renderSelection = renderSelection;
+ t.clearSelection = clearSelection;
+ t.reportDayClick = reportDayClick; // selection mousedown hack
+ t.dragStart = dragStart;
+ t.dragStop = dragStop;
+
+
+ // imports
+ View.call(t, element, calendar, viewName);
+ OverlayManager.call(t);
+ SelectionManager.call(t);
+ AgendaEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var renderOverlay = t.renderOverlay;
+ var clearOverlays = t.clearOverlays;
+ var reportSelection = t.reportSelection;
+ var unselect = t.unselect;
+ var daySelectionMousedown = t.daySelectionMousedown;
+ var slotSegHtml = t.slotSegHtml;
+ var cellToDate = t.cellToDate;
+ var dateToCell = t.dateToCell;
+ var rangeToSegments = t.rangeToSegments;
+ var formatDate = calendar.formatDate;
+
+
+ // locals
+
+ var dayTable;
+ var dayHead;
+ var dayHeadCells;
+ var dayBody;
+ var dayBodyCells;
+ var dayBodyCellInners;
+ var dayBodyCellContentInners;
+ var dayBodyFirstCell;
+ var dayBodyFirstCellStretcher;
+ var slotLayer;
+ var daySegmentContainer;
+ var allDayTable;
+ var allDayRow;
+ var slotScroller;
+ var slotContainer;
+ var slotSegmentContainer;
+ var slotTable;
+ var selectionHelper;
+
+ var viewWidth;
+ var viewHeight;
+ var axisWidth;
+ var colWidth;
+ var gutterWidth;
+ var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
+
+ var snapMinutes;
+ var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4)
+ var snapHeight; // holds the pixel hight of a "selection" slot
+
+ var colCnt;
+ var slotCnt;
+ var coordinateGrid;
+ var hoverListener;
+ var colPositions;
+ var colContentPositions;
+ var slotTopCache = {};
+
+ var tm;
+ var rtl;
+ var minMinute, maxMinute;
+ var colFormat;
+ var showWeekNumbers;
+ var weekNumberTitle;
+ var weekNumberFormat;
+
+
+
+ /* Rendering
+ -----------------------------------------------------------------------------*/
+
+
+ disableTextSelection(element.addClass('fc-agenda'));
+
+
+ function renderAgenda(c) {
+ colCnt = c;
+ updateOptions();
+
+ if (!dayTable) { // first time rendering?
+ buildSkeleton(); // builds day table, slot area, events containers
+ }
+ else {
+ buildDayTable(); // rebuilds day table
+ }
+ }
+
+
+ function updateOptions() {
+
+ tm = opt('theme') ? 'ui' : 'fc';
+ rtl = opt('isRTL')
+ minMinute = parseTime(opt('minTime'));
+ maxMinute = parseTime(opt('maxTime'));
+ colFormat = opt('columnFormat');
+
+ // week # options. (TODO: bad, logic also in other views)
+ showWeekNumbers = opt('weekNumbers');
+ weekNumberTitle = opt('weekNumberTitle');
+ if (opt('weekNumberCalculation') != 'iso') {
+ weekNumberFormat = "w";
+ }
+ else {
+ weekNumberFormat = "W";
+ }
+
+ snapMinutes = opt('snapMinutes') || opt('slotMinutes');
+ }
+
+
+
+ /* Build DOM
+ -----------------------------------------------------------------------*/
+
+
+ function buildSkeleton() {
+ var headerClass = tm + "-widget-header";
+ var contentClass = tm + "-widget-content";
+ var s;
+ var d;
+ var i;
+ var maxd;
+ var minutes;
+ var slotNormal = opt('slotMinutes') % 15 == 0;
+
+ buildDayTable();
+
+ slotLayer =
+ $("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
+ .appendTo(element);
+
+ if (opt('allDaySlot')) {
+
+ daySegmentContainer =
+ $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
+ .appendTo(slotLayer);
+
+ s =
+ "<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
+ "<tr>" +
+ "<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" +
+ "<td>" +
+ "<div class='fc-day-content'><div style='position:relative'/></div>" +
+ "</td>" +
+ "<th class='" + headerClass + " fc-agenda-gutter'>&nbsp;</th>" +
+ "</tr>" +
+ "</table>";
+ allDayTable = $(s).appendTo(slotLayer);
+ allDayRow = allDayTable.find('tr');
+
+ dayBind(allDayRow.find('td'));
+
+ slotLayer.append(
+ "<div class='fc-agenda-divider " + headerClass + "'>" +
+ "<div class='fc-agenda-divider-inner'/>" +
+ "</div>"
+ );
+
+ }else{
+
+ daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
+
+ }
+
+ slotScroller =
+ $("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
+ .appendTo(slotLayer);
+
+ slotContainer =
+ $("<div style='position:relative;width:100%;overflow:hidden'/>")
+ .appendTo(slotScroller);
+
+ slotSegmentContainer =
+ $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
+ .appendTo(slotContainer);
+
+ s =
+ "<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
+ "<tbody>";
+ d = zeroDate();
+ maxd = addMinutes(cloneDate(d), maxMinute);
+ addMinutes(d, minMinute);
+ slotCnt = 0;
+ for (i=0; d < maxd; i++) {
+ minutes = d.getMinutes();
+ s +=
+ "<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
+ "<th class='fc-agenda-axis " + headerClass + "'>" +
+ ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : '&nbsp;') +
+ "</th>" +
+ "<td class='" + contentClass + "'>" +
+ "<div style='position:relative'>&nbsp;</div>" +
+ "</td>" +
+ "</tr>";
+ addMinutes(d, opt('slotMinutes'));
+ slotCnt++;
+ }
+ s +=
+ "</tbody>" +
+ "</table>";
+ slotTable = $(s).appendTo(slotContainer);
+
+ slotBind(slotTable.find('td'));
+ }
+
+
+
+ /* Build Day Table
+ -----------------------------------------------------------------------*/
+
+
+ function buildDayTable() {
+ var html = buildDayTableHTML();
+
+ if (dayTable) {
+ dayTable.remove();
+ }
+ dayTable = $(html).appendTo(element);
+
+ dayHead = dayTable.find('thead');
+ dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter
+ dayBody = dayTable.find('tbody');
+ dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter
+ dayBodyCellInners = dayBodyCells.find('> div');
+ dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div');
+
+ dayBodyFirstCell = dayBodyCells.eq(0);
+ dayBodyFirstCellStretcher = dayBodyCellInners.eq(0);
+
+ markFirstLast(dayHead.add(dayHead.find('tr')));
+ markFirstLast(dayBody.add(dayBody.find('tr')));
+
+ // TODO: now that we rebuild the cells every time, we should call dayRender
+ }
+
+
+ function buildDayTableHTML() {
+ var html =
+ "<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
+ buildDayTableHeadHTML() +
+ buildDayTableBodyHTML() +
+ "</table>";
+
+ return html;
+ }
+
+
+ function buildDayTableHeadHTML() {
+ var headerClass = tm + "-widget-header";
+ var date;
+ var html = '';
+ var weekText;
+ var col;
+
+ html +=
+ "<thead>" +
+ "<tr>";
+
+ if (showWeekNumbers) {
+ date = cellToDate(0, 0);
+ weekText = formatDate(date, weekNumberFormat);
+ if (rtl) {
+ weekText += weekNumberTitle;
+ }
+ else {
+ weekText = weekNumberTitle + weekText;
+ }
+ html +=
+ "<th class='fc-agenda-axis fc-week-number " + headerClass + "'>" +
+ htmlEscape(weekText) +
+ "</th>";
+ }
+ else {
+ html += "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
+ }
+
+ for (col=0; col<colCnt; col++) {
+ date = cellToDate(0, col);
+ html +=
+ "<th class='fc-" + dayIDs[date.getDay()] + " fc-col" + col + ' ' + headerClass + "'>" +
+ htmlEscape(formatDate(date, colFormat)) +
+ "</th>";
+ }
+
+ html +=
+ "<th class='fc-agenda-gutter " + headerClass + "'>&nbsp;</th>" +
+ "</tr>" +
+ "</thead>";
+
+ return html;
+ }
+
+
+ function buildDayTableBodyHTML() {
+ var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called
+ var contentClass = tm + "-widget-content";
+ var date;
+ var today = clearTime(new Date());
+ var col;
+ var cellsHTML;
+ var cellHTML;
+ var classNames;
+ var html = '';
+
+ html +=
+ "<tbody>" +
+ "<tr>" +
+ "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
+
+ cellsHTML = '';
+
+ for (col=0; col<colCnt; col++) {
+
+ date = cellToDate(0, col);
+
+ classNames = [
+ 'fc-col' + col,
+ 'fc-' + dayIDs[date.getDay()],
+ contentClass
+ ];
+ if (+date == +today) {
+ classNames.push(
+ tm + '-state-highlight',
+ 'fc-today'
+ );
+ }
+ else if (date < today) {
+ classNames.push('fc-past');
+ }
+ else {
+ classNames.push('fc-future');
+ }
+
+ cellHTML =
+ "<td class='" + classNames.join(' ') + "'>" +
+ "<div>" +
+ "<div class='fc-day-content'>" +
+ "<div style='position:relative'>&nbsp;</div>" +
+ "</div>" +
+ "</div>" +
+ "</td>";
+
+ cellsHTML += cellHTML;
+ }
+
+ html += cellsHTML;
+ html +=
+ "<td class='fc-agenda-gutter " + contentClass + "'>&nbsp;</td>" +
+ "</tr>" +
+ "</tbody>";
+
+ return html;
+ }
+
+
+ // TODO: data-date on the cells
+
+
+
+ /* Dimensions
+ -----------------------------------------------------------------------*/
+
+
+ function setHeight(height) {
+ if (height === undefined) {
+ height = viewHeight;
+ }
+ viewHeight = height;
+ slotTopCache = {};
+
+ var headHeight = dayBody.position().top;
+ var allDayHeight = slotScroller.position().top; // including divider
+ var bodyHeight = Math.min( // total body height, including borders
+ height - headHeight, // when scrollbars
+ slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
+ );
+
+ dayBodyFirstCellStretcher
+ .height(bodyHeight - vsides(dayBodyFirstCell));
+
+ slotLayer.css('top', headHeight);
+
+ slotScroller.height(bodyHeight - allDayHeight - 1);
+
+ // the stylesheet guarantees that the first row has no border.
+ // this allows .height() to work well cross-browser.
+ slotHeight = slotTable.find('tr:first').height() + 1; // +1 for bottom border
+
+ snapRatio = opt('slotMinutes') / snapMinutes;
+ snapHeight = slotHeight / snapRatio;
+ }
+
+
+ function setWidth(width) {
+ viewWidth = width;
+ colPositions.clear();
+ colContentPositions.clear();
+
+ var axisFirstCells = dayHead.find('th:first');
+ if (allDayTable) {
+ axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
+ }
+ axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
+
+ axisWidth = 0;
+ setOuterWidth(
+ axisFirstCells
+ .width('')
+ .each(function(i, _cell) {
+ axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
+ }),
+ axisWidth
+ );
+
+ var gutterCells = dayTable.find('.fc-agenda-gutter');
+ if (allDayTable) {
+ gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
+ }
+
+ var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
+
+ gutterWidth = slotScroller.width() - slotTableWidth;
+ if (gutterWidth) {
+ setOuterWidth(gutterCells, gutterWidth);
+ gutterCells
+ .show()
+ .prev()
+ .removeClass('fc-last');
+ }else{
+ gutterCells
+ .hide()
+ .prev()
+ .addClass('fc-last');
+ }
+
+ colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
+ setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
+ }
+
+
+
+ /* Scrolling
+ -----------------------------------------------------------------------*/
+
+
+ function resetScroll() {
+ var d0 = zeroDate();
+ var scrollDate = cloneDate(d0);
+ scrollDate.setHours(opt('firstHour'));
+ var top = timePosition(d0, scrollDate) + 1; // +1 for the border
+ function scroll() {
+ slotScroller.scrollTop(top);
+ }
+ scroll();
+ setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
+ }
+
+
+ function afterRender() { // after the view has been freshly rendered and sized
+ resetScroll();
+ }
+
+
+
+ /* Slot/Day clicking and binding
+ -----------------------------------------------------------------------*/
+
+
+ function dayBind(cells) {
+ cells.click(slotClick)
+ .mousedown(daySelectionMousedown);
+ }
+
+
+ function slotBind(cells) {
+ cells.click(slotClick)
+ .mousedown(slotSelectionMousedown);
+ }
+
+
+ function slotClick(ev) {
+ if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
+ var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
+ var date = cellToDate(0, col);
+ var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
+ if (rowMatch) {
+ var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
+ var hours = Math.floor(mins/60);
+ date.setHours(hours);
+ date.setMinutes(mins%60 + minMinute);
+ trigger('dayClick', dayBodyCells[col], date, false, ev);
+ }else{
+ trigger('dayClick', dayBodyCells[col], date, true, ev);
+ }
+ }
+ }
+
+
+
+ /* Semi-transparent Overlay Helpers
+ -----------------------------------------------------*/
+ // TODO: should be consolidated with BasicView's methods
+
+
+ function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
+
+ if (refreshCoordinateGrid) {
+ coordinateGrid.build();
+ }
+
+ var segments = rangeToSegments(overlayStart, overlayEnd);
+
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+ dayBind(
+ renderCellOverlay(
+ segment.row,
+ segment.leftCol,
+ segment.row,
+ segment.rightCol
+ )
+ );
+ }
+ }
+
+
+ function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
+ var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
+ return renderOverlay(rect, slotLayer);
+ }
+
+
+ function renderSlotOverlay(overlayStart, overlayEnd) {
+ for (var i=0; i<colCnt; i++) {
+ var dayStart = cellToDate(0, i);
+ var dayEnd = addDays(cloneDate(dayStart), 1);
+ var stretchStart = new Date(Math.max(dayStart, overlayStart));
+ var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
+ if (stretchStart < stretchEnd) {
+ var rect = coordinateGrid.rect(0, i, 0, i, slotContainer); // only use it for horizontal coords
+ var top = timePosition(dayStart, stretchStart);
+ var bottom = timePosition(dayStart, stretchEnd);
+ rect.top = top;
+ rect.height = bottom - top;
+ slotBind(
+ renderOverlay(rect, slotContainer)
+ );
+ }
+ }
+ }
+
+
+
+ /* Coordinate Utilities
+ -----------------------------------------------------------------------------*/
+
+
+ coordinateGrid = new CoordinateGrid(function(rows, cols) {
+ var e, n, p;
+ dayHeadCells.each(function(i, _e) {
+ e = $(_e);
+ n = e.offset().left;
+ if (i) {
+ p[1] = n;
+ }
+ p = [n];
+ cols[i] = p;
+ });
+ p[1] = n + e.outerWidth();
+ if (opt('allDaySlot')) {
+ e = allDayRow;
+ n = e.offset().top;
+ rows[0] = [n, n+e.outerHeight()];
+ }
+ var slotTableTop = slotContainer.offset().top;
+ var slotScrollerTop = slotScroller.offset().top;
+ var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
+ function constrain(n) {
+ return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
+ }
+ for (var i=0; i<slotCnt*snapRatio; i++) { // adapt slot count to increased/decreased selection slot count
+ rows.push([
+ constrain(slotTableTop + snapHeight*i),
+ constrain(slotTableTop + snapHeight*(i+1))
+ ]);
+ }
+ });
+
+
+ hoverListener = new HoverListener(coordinateGrid);
+
+ colPositions = new HorizontalPositionCache(function(col) {
+ return dayBodyCellInners.eq(col);
+ });
+
+ colContentPositions = new HorizontalPositionCache(function(col) {
+ return dayBodyCellContentInners.eq(col);
+ });
+
+
+ function colLeft(col) {
+ return colPositions.left(col);
+ }
+
+
+ function colContentLeft(col) {
+ return colContentPositions.left(col);
+ }
+
+
+ function colRight(col) {
+ return colPositions.right(col);
+ }
+
+
+ function colContentRight(col) {
+ return colContentPositions.right(col);
+ }
+
+
+ function getIsCellAllDay(cell) {
+ return opt('allDaySlot') && !cell.row;
+ }
+
+
+ function realCellToDate(cell) { // ugh "real" ... but blame it on our abuse of the "cell" system
+ var d = cellToDate(0, cell.col);
+ var slotIndex = cell.row;
+ if (opt('allDaySlot')) {
+ slotIndex--;
+ }
+ if (slotIndex >= 0) {
+ addMinutes(d, minMinute + slotIndex * snapMinutes);
+ }
+ return d;
+ }
+
+
+ // get the Y coordinate of the given time on the given day (both Date objects)
+ function timePosition(day, time) { // both date objects. day holds 00:00 of current day
+ day = cloneDate(day, true);
+ if (time < addMinutes(cloneDate(day), minMinute)) {
+ return 0;
+ }
+ if (time >= addMinutes(cloneDate(day), maxMinute)) {
+ return slotTable.height();
+ }
+ var slotMinutes = opt('slotMinutes'),
+ minutes = time.getHours()*60 + time.getMinutes() - minMinute,
+ slotI = Math.floor(minutes / slotMinutes),
+ slotTop = slotTopCache[slotI];
+ if (slotTop === undefined) {
+ slotTop = slotTopCache[slotI] =
+ slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop;
+ // .eq() is faster than ":eq()" selector
+ // [0].offsetTop is faster than .position().top (do we really need this optimization?)
+ // a better optimization would be to cache all these divs
+ }
+ return Math.max(0, Math.round(
+ slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
+ ));
+ }
+
+
+ function getAllDayRow(index) {
+ return allDayRow;
+ }
+
+
+ function defaultEventEnd(event) {
+ var start = cloneDate(event.start);
+ if (event.allDay) {
+ return start;
+ }
+ return addMinutes(start, opt('defaultEventMinutes'));
+ }
+
+
+
+ /* Selection
+ ---------------------------------------------------------------------------------*/
+
+
+ function defaultSelectionEnd(startDate, allDay) {
+ if (allDay) {
+ return cloneDate(startDate);
+ }
+ return addMinutes(cloneDate(startDate), opt('slotMinutes'));
+ }
+
+
+ function renderSelection(startDate, endDate, allDay) { // only for all-day
+ if (allDay) {
+ if (opt('allDaySlot')) {
+ renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
+ }
+ }else{
+ renderSlotSelection(startDate, endDate);
+ }
+ }
+
+
+ function renderSlotSelection(startDate, endDate) {
+ var helperOption = opt('selectHelper');
+ coordinateGrid.build();
+ if (helperOption) {
+ var col = dateToCell(startDate).col;
+ if (col >= 0 && col < colCnt) { // only works when times are on same day
+ var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords
+ var top = timePosition(startDate, startDate);
+ var bottom = timePosition(startDate, endDate);
+ if (bottom > top) { // protect against selections that are entirely before or after visible range
+ rect.top = top;
+ rect.height = bottom - top;
+ rect.left += 2;
+ rect.width -= 5;
+ if ($.isFunction(helperOption)) {
+ var helperRes = helperOption(startDate, endDate);
+ if (helperRes) {
+ rect.position = 'absolute';
+ selectionHelper = $(helperRes)
+ .css(rect)
+ .appendTo(slotContainer);
+ }
+ }else{
+ rect.isStart = true; // conside rect a "seg" now
+ rect.isEnd = true; //
+ selectionHelper = $(slotSegHtml(
+ {
+ title: '',
+ start: startDate,
+ end: endDate,
+ className: ['fc-select-helper'],
+ editable: false
+ },
+ rect
+ ));
+ selectionHelper.css('opacity', opt('dragOpacity'));
+ }
+ if (selectionHelper) {
+ slotBind(selectionHelper);
+ slotContainer.append(selectionHelper);
+ setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
+ setOuterHeight(selectionHelper, rect.height, true);
+ }
+ }
+ }
+ }else{
+ renderSlotOverlay(startDate, endDate);
+ }
+ }
+
+
+ function clearSelection() {
+ clearOverlays();
+ if (selectionHelper) {
+ selectionHelper.remove();
+ selectionHelper = null;
+ }
+ }
+
+
+ function slotSelectionMousedown(ev) {
+ if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
+ unselect(ev);
+ var dates, helperOption = opt('selectHelper');
+ hoverListener.start(function(cell, origCell) {
+ clearSelection();
+ if (cell && (cell.col == origCell.col || !helperOption) && !getIsCellAllDay(cell)) {
+ var d1 = realCellToDate(origCell);
+ var d2 = realCellToDate(cell);
+ dates = [
+ d1,
+ addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes
+ d2,
+ addMinutes(cloneDate(d2), snapMinutes)
+ ].sort(dateCompare);
+ renderSlotSelection(dates[0], dates[3]);
+ }else{
+ dates = null;
+ }
+ }, ev);
+ $(document).one('mouseup', function(ev) {
+ hoverListener.stop();
+ if (dates) {
+ if (+dates[0] == +dates[1]) {
+ reportDayClick(dates[0], false, ev);
+ }
+ reportSelection(dates[0], dates[3], false, ev);
+ }
+ });
+ }
+ }
+
+
+ function reportDayClick(date, allDay, ev) {
+ trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev);
+ }
+
+
+
+ /* External Dragging
+ --------------------------------------------------------------------------------*/
+
+
+ function dragStart(_dragElement, ev, ui) {
+ hoverListener.start(function(cell) {
+ clearOverlays();
+ if (cell) {
+ if (getIsCellAllDay(cell)) {
+ renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
+ }else{
+ var d1 = realCellToDate(cell);
+ var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
+ renderSlotOverlay(d1, d2);
+ }
+ }
+ }, ev);
+ }
+
+
+ function dragStop(_dragElement, ev, ui) {
+ var cell = hoverListener.stop();
+ clearOverlays();
+ if (cell) {
+ trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui);
+ }
+ }
+
+
+}
+
+;;
+
+function AgendaEventRenderer() {
+ var t = this;
+
+
+ // exports
+ t.renderEvents = renderEvents;
+ t.clearEvents = clearEvents;
+ t.slotSegHtml = slotSegHtml;
+
+
+ // imports
+ DayEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var isEventDraggable = t.isEventDraggable;
+ var isEventResizable = t.isEventResizable;
+ var eventEnd = t.eventEnd;
+ var eventElementHandlers = t.eventElementHandlers;
+ var setHeight = t.setHeight;
+ var getDaySegmentContainer = t.getDaySegmentContainer;
+ var getSlotSegmentContainer = t.getSlotSegmentContainer;
+ var getHoverListener = t.getHoverListener;
+ var getMaxMinute = t.getMaxMinute;
+ var getMinMinute = t.getMinMinute;
+ var timePosition = t.timePosition;
+ var getIsCellAllDay = t.getIsCellAllDay;
+ var colContentLeft = t.colContentLeft;
+ var colContentRight = t.colContentRight;
+ var cellToDate = t.cellToDate;
+ var getColCnt = t.getColCnt;
+ var getColWidth = t.getColWidth;
+ var getSnapHeight = t.getSnapHeight;
+ var getSnapMinutes = t.getSnapMinutes;
+ var getSlotContainer = t.getSlotContainer;
+ var reportEventElement = t.reportEventElement;
+ var showEvents = t.showEvents;
+ var hideEvents = t.hideEvents;
+ var eventDrop = t.eventDrop;
+ var eventResize = t.eventResize;
+ var renderDayOverlay = t.renderDayOverlay;
+ var clearOverlays = t.clearOverlays;
+ var renderDayEvents = t.renderDayEvents;
+ var calendar = t.calendar;
+ var formatDate = calendar.formatDate;
+ var formatDates = calendar.formatDates;
+ var timeLineInterval;
+
+
+ // overrides
+ t.draggableDayEvent = draggableDayEvent;
+
+
+ /* Rendering
+ ----------------------------------------------------------------------------*/
+
+
+ function renderEvents(events, modifiedEventId) {
+ var i, len=events.length,
+ dayEvents=[],
+ slotEvents=[];
+ for (i=0; i<len; i++) {
+ if (events[i].allDay) {
+ dayEvents.push(events[i]);
+ }else{
+ slotEvents.push(events[i]);
+ }
+ }
+
+ if (opt('allDaySlot')) {
+ renderDayEvents(dayEvents, modifiedEventId);
+ setHeight(); // no params means set to viewHeight
+ }
+
+ renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
+
+ if (opt('currentTimeIndicator')) {
+ window.clearInterval(timeLineInterval);
+ timeLineInterval = window.setInterval(setTimeIndicator, 30000);
+ setTimeIndicator();
+ }
+ }
+
+
+ function clearEvents() {
+ getDaySegmentContainer().empty();
+ getSlotSegmentContainer().empty();
+ }
+
+
+ function compileSlotSegs(events) {
+ var colCnt = getColCnt(),
+ minMinute = getMinMinute(),
+ maxMinute = getMaxMinute(),
+ d,
+ visEventEnds = $.map(events, slotEventEnd),
+ i,
+ j, seg,
+ colSegs,
+ segs = [];
+
+ for (i=0; i<colCnt; i++) {
+
+ d = cellToDate(0, i);
+ addMinutes(d, minMinute);
+
+ colSegs = sliceSegs(
+ events,
+ visEventEnds,
+ d,
+ addMinutes(cloneDate(d), maxMinute-minMinute)
+ );
+
+ colSegs = placeSlotSegs(colSegs); // returns a new order
+
+ for (j=0; j<colSegs.length; j++) {
+ seg = colSegs[j];
+ seg.col = i;
+ segs.push(seg);
+ }
+ }
+
+ return segs;
+ }
+
+
+ function sliceSegs(events, visEventEnds, start, end) {
+ var segs = [],
+ i, len=events.length, event,
+ eventStart, eventEnd,
+ segStart, segEnd,
+ isStart, isEnd;
+ for (i=0; i<len; i++) {
+ event = events[i];
+ eventStart = event.start;
+ eventEnd = visEventEnds[i];
+ if (eventEnd > start && eventStart < end) {
+ if (eventStart < start) {
+ segStart = cloneDate(start);
+ isStart = false;
+ }else{
+ segStart = eventStart;
+ isStart = true;
+ }
+ if (eventEnd > end) {
+ segEnd = cloneDate(end);
+ isEnd = false;
+ }else{
+ segEnd = eventEnd;
+ isEnd = true;
+ }
+ segs.push({
+ event: event,
+ start: segStart,
+ end: segEnd,
+ isStart: isStart,
+ isEnd: isEnd
+ });
+ }
+ }
+ return segs.sort(compareSlotSegs);
+ }
+
+
+ function slotEventEnd(event) {
+ if (event.end) {
+ return cloneDate(event.end);
+ }else{
+ return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
+ }
+ }
+
+
+ // renders events in the 'time slots' at the bottom
+ // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space
+ // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp)
+
+ function renderSlotSegs(segs, modifiedEventId) {
+
+ var i, segCnt=segs.length, seg,
+ event,
+ top,
+ bottom,
+ columnLeft,
+ columnRight,
+ columnWidth,
+ width,
+ left,
+ right,
+ html = '',
+ eventElements,
+ eventElement,
+ triggerRes,
+ contentElement,
+ height,
+ slotSegmentContainer = getSlotSegmentContainer(),
+ isRTL = opt('isRTL'),
+ colCnt = getColCnt();
+
+ // calculate position/dimensions, create html
+ for (i=0; i<segCnt; i++) {
+ seg = segs[i];
+ event = seg.event;
+ top = timePosition(seg.start, seg.start);
+ bottom = timePosition(seg.start, seg.end);
+ columnLeft = colContentLeft(seg.col);
+ columnRight = colContentRight(seg.col);
+ columnWidth = columnRight - columnLeft;
+
+ // shave off space on right near scrollbars (2.5%)
+ // TODO: move this to CSS somehow
+ columnRight -= columnWidth * .025;
+ columnWidth = columnRight - columnLeft;
+
+ width = columnWidth * (seg.forwardCoord - seg.backwardCoord);
+
+ // bruederli@kolabsys.com: always disable slotEventOverlap in single day view
+ if (opt('slotEventOverlap') && colCnt > 1) {
+ // double the width while making sure resize handle is visible
+ // (assumed to be 20px wide)
+ width = Math.max(
+ (width - (20/2)) * 2,
+ width // narrow columns will want to make the segment smaller than
+ // the natural width. don't allow it
+ );
+ }
+
+ if (isRTL) {
+ right = columnRight - seg.backwardCoord * columnWidth;
+ left = right - width;
+ }
+ else {
+ left = columnLeft + seg.backwardCoord * columnWidth;
+ right = left + width;
+ }
+
+ // make sure horizontal coordinates are in bounds
+ left = Math.max(left, columnLeft);
+ right = Math.min(right, columnRight);
+ width = right - left;
+
+ seg.top = top;
+ seg.left = left;
+ seg.outerWidth = width;
+ seg.outerHeight = bottom - top;
+ html += slotSegHtml(event, seg);
+ }
+
+ slotSegmentContainer[0].innerHTML = html; // faster than html()
+ eventElements = slotSegmentContainer.children();
+
+ // retrieve elements, run through eventRender callback, bind event handlers
+ for (i=0; i<segCnt; i++) {
+ seg = segs[i];
+ event = seg.event;
+ eventElement = $(eventElements[i]); // faster than eq()
+ triggerRes = trigger('eventRender', event, event, eventElement);
+ if (triggerRes === false) {
+ eventElement.remove();
+ }else{
+ if (triggerRes && triggerRes !== true) {
+ eventElement.remove();
+ eventElement = $(triggerRes)
+ .css({
+ position: 'absolute',
+ top: seg.top,
+ left: seg.left
+ })
+ .appendTo(slotSegmentContainer);
+ }
+ seg.element = eventElement;
+ if (event._id === modifiedEventId) {
+ bindSlotSeg(event, eventElement, seg);
+ }else{
+ eventElement[0]._fci = i; // for lazySegBind
+ }
+ reportEventElement(event, eventElement);
+ }
+ }
+
+ lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
+
+ // record event sides and title positions
+ for (i=0; i<segCnt; i++) {
+ seg = segs[i];
+ if (eventElement = seg.element) {
+ seg.vsides = vsides(eventElement, true);
+ seg.hsides = hsides(eventElement, true);
+ contentElement = eventElement.find('.fc-event-content');
+ if (contentElement.length) {
+ seg.contentTop = contentElement[0].offsetTop;
+ }
+ }
+ }
+
+ // set all positions/dimensions at once
+ for (i=0; i<segCnt; i++) {
+ seg = segs[i];
+ if (eventElement = seg.element) {
+ eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
+ height = Math.max(0, seg.outerHeight - seg.vsides);
+ eventElement[0].style.height = height + 'px';
+ event = seg.event;
+ if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
+ // not enough room for title, put it in the time (TODO: maybe make both display:inline instead)
+ eventElement.find('div.fc-event-time')
+ .text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
+ eventElement.find('div.fc-event-title')
+ .remove();
+ }
+ trigger('eventAfterRender', event, event, eventElement);
+ }
+ }
+
+ }
+
+
+ function slotSegHtml(event, seg) {
+ var html = "<";
+ var url = event.url;
+ var skinCss = getSkinCss(event, opt);
+ var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
+ var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert'];
+ if (isEventDraggable(event)) {
+ classes.push('fc-event-draggable');
+ }
+ if (seg.isStart) {
+ classes.push('fc-event-start');
+ }
+ if (seg.isEnd) {
+ classes.push('fc-event-end');
+ }
+ classes = classes.concat(event.className);
+ if (event.source) {
+ classes = classes.concat(event.source.className || []);
+ }
+ if (url) {
+ html += "a href='" + htmlEscape(event.url) + "'";
+ }else{
+ html += "div";
+ }
+ html +=
+ " class='" + classes.join(' ') + "'" +
+ " style=" +
+ "'" +
+ "position:absolute;" +
+ "top:" + seg.top + "px;" +
+ "left:" + seg.left + "px;" +
+ skinCss +
+ "'" +
+ " tabindex='0'>" +
+ "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
+ "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
+ "<div class='fc-event-time'>" +
+ htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
+ "</div>" +
+ "</div>" +
+ "<div class='fc-event-content'>" +
+ "<div class='fc-event-title'>" +
+ htmlEscape(event.title || '') +
+ "</div>" +
+ "</div>" +
+ "<div class='fc-event-bg'></div>" +
+ "</div>"; // close inner
+ if (seg.isEnd && isEventResizable(event)) {
+ html +=
+ "<div class='ui-resizable-handle ui-resizable-s' role='presentation'>=</div>";
+ }
+ html +=
+ "</" + (url ? "a" : "div") + ">";
+ return html;
+ }
+
+
+ function bindSlotSeg(event, eventElement, seg) {
+ var timeElement = eventElement.find('div.fc-event-time');
+ if (isEventDraggable(event)) {
+ draggableSlotEvent(event, eventElement, timeElement);
+ }
+ if (seg.isEnd && isEventResizable(event)) {
+ resizableSlotEvent(event, eventElement, timeElement);
+ }
+ eventElementHandlers(event, eventElement);
+ }
+
+
+ // draw a horizontal line indicating the current time (#143)
+ function setTimeIndicator()
+ {
+ var container = getSlotContainer();
+ var timeline = container.children('.fc-timeline');
+ if (timeline.length == 0) { // if timeline isn't there, add it
+ timeline = $('<hr>').addClass('fc-timeline').appendTo(container);
+ }
+
+ var cur_time = new Date();
+ if (t.visStart < cur_time && t.visEnd > cur_time) {
+ timeline.show();
+ }
+ else {
+ timeline.hide();
+ return;
+ }
+
+ var secs = (cur_time.getHours() * 60 * 60) + (cur_time.getMinutes() * 60) + cur_time.getSeconds();
+ var percents = secs / 86400; // 24 * 60 * 60 = 86400, # of seconds in a day
+
+ timeline.css('top', Math.floor(container.height() * percents - 1) + 'px');
+
+ if (t.name == 'agendaWeek') { // week view, don't want the timeline to go the whole way across
+ var daycol = $('.fc-today', t.element);
+ var left = daycol.position().left + 1;
+ var width = daycol.width();
+ timeline.css({ left: left + 'px', width: width + 'px' });
+ }
+ }
+
+
+ /* Dragging
+ -----------------------------------------------------------------------------------*/
+
+
+ // when event starts out FULL-DAY
+ // overrides DayEventRenderer's version because it needs to account for dragging elements
+ // to and from the slot area.
+
+ function draggableDayEvent(event, eventElement, seg) {
+ var isStart = seg.isStart;
+ var origWidth;
+ var revert;
+ var allDay = true;
+ var dayDelta;
+ var hoverListener = getHoverListener();
+ var colWidth = getColWidth();
+ var snapHeight = getSnapHeight();
+ var snapMinutes = getSnapMinutes();
+ var minMinute = getMinMinute();
+ eventElement.draggable({
+ opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
+ revertDuration: opt('dragRevertDuration'),
+ start: function(ev, ui) {
+ trigger('eventDragStart', eventElement, event, ev, ui);
+ hideEvents(event, eventElement);
+ origWidth = eventElement.width();
+ hoverListener.start(function(cell, origCell) {
+ clearOverlays();
+ if (cell) {
+ revert = false;
+ var origDate = cellToDate(0, origCell.col);
+ var date = cellToDate(0, cell.col);
+ dayDelta = dayDiff(date, origDate);
+ if (!cell.row) {
+ // on full-days
+ renderDayOverlay(
+ addDays(cloneDate(event.start), dayDelta),
+ addDays(exclEndDay(event), dayDelta)
+ );
+ resetElement();
+ }else{
+ // mouse is over bottom slots
+ if (isStart) {
+ if (allDay) {
+ // convert event to temporary slot-event
+ eventElement.width(colWidth - 10); // don't use entire width
+ setOuterHeight(
+ eventElement,
+ snapHeight * Math.round(
+ (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) /
+ snapMinutes
+ )
+ );
+ eventElement.draggable('option', 'grid', [colWidth, 1]);
+ allDay = false;
+ }
+ }else{
+ revert = true;
+ }
+ }
+ revert = revert || (allDay && !dayDelta);
+ }else{
+ resetElement();
+ revert = true;
+ }
+ eventElement.draggable('option', 'revert', revert);
+ }, ev, 'drag');
+ },
+ stop: function(ev, ui) {
+ hoverListener.stop();
+ clearOverlays();
+ trigger('eventDragStop', eventElement, event, ev, ui);
+ if (revert) {
+ // hasn't moved or is out of bounds (draggable has already reverted)
+ resetElement();
+ eventElement.css('filter', ''); // clear IE opacity side-effects
+ showEvents(event, eventElement);
+ }else{
+ // changed!
+ var minuteDelta = 0;
+ if (!allDay) {
+ minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight)
+ * snapMinutes
+ + minMinute
+ - (event.start.getHours() * 60 + event.start.getMinutes());
+ }
+ eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
+ }
+ }
+ });
+ function resetElement() {
+ if (!allDay) {
+ eventElement
+ .width(origWidth)
+ .height('')
+ .draggable('option', 'grid', null);
+ allDay = true;
+ }
+ }
+ }
+
+
+ // when event starts out IN TIMESLOTS
+
+ function draggableSlotEvent(event, eventElement, timeElement) {
+ var coordinateGrid = t.getCoordinateGrid();
+ var colCnt = getColCnt();
+ var colWidth = getColWidth();
+ var snapHeight = getSnapHeight();
+ var snapMinutes = getSnapMinutes();
+
+ // states
+ var origPosition; // original position of the element, not the mouse
+ var origCell;
+ var isInBounds, prevIsInBounds;
+ var isAllDay, prevIsAllDay;
+ var colDelta, prevColDelta;
+ var dayDelta; // derived from colDelta
+ var minuteDelta, prevMinuteDelta;
+
+ eventElement.draggable({
+ scroll: false,
+ grid: [ colWidth, snapHeight ],
+ axis: colCnt==1 ? 'y' : false,
+ opacity: opt('dragOpacity'),
+ revertDuration: opt('dragRevertDuration'),
+ start: function(ev, ui) {
+
+ trigger('eventDragStart', eventElement, event, ev, ui);
+ hideEvents(event, eventElement);
+
+ coordinateGrid.build();
+
+ // initialize states
+ origPosition = eventElement.position();
+ origCell = coordinateGrid.cell(ev.pageX, ev.pageY);
+ isInBounds = prevIsInBounds = true;
+ isAllDay = prevIsAllDay = getIsCellAllDay(origCell);
+ colDelta = prevColDelta = 0;
+ dayDelta = 0;
+ minuteDelta = prevMinuteDelta = 0;
+
+ },
+ drag: function(ev, ui) {
+
+ // NOTE: this `cell` value is only useful for determining in-bounds and all-day.
+ // Bad for anything else due to the discrepancy between the mouse position and the
+ // element position while snapping. (problem revealed in PR #55)
+ //
+ // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event.
+ // We should overhaul the dragging system and stop relying on jQuery UI.
+ var cell = coordinateGrid.cell(ev.pageX, ev.pageY);
+
+ // update states
+ isInBounds = !!cell;
+ if (isInBounds) {
+ isAllDay = getIsCellAllDay(cell);
+
+ // calculate column delta
+ colDelta = Math.round((ui.position.left - origPosition.left) / colWidth);
+ if (colDelta != prevColDelta) {
+ // calculate the day delta based off of the original clicked column and the column delta
+ var origDate = cellToDate(0, origCell.col);
+ var col = origCell.col + colDelta;
+ col = Math.max(0, col);
+ col = Math.min(colCnt-1, col);
+ var date = cellToDate(0, col);
+ dayDelta = dayDiff(date, origDate);
+ }
+
+ // calculate minute delta (only if over slots)
+ if (!isAllDay) {
+ minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes;
+ }
+ }
+
+ // any state changes?
+ if (
+ isInBounds != prevIsInBounds ||
+ isAllDay != prevIsAllDay ||
+ colDelta != prevColDelta ||
+ minuteDelta != prevMinuteDelta
+ ) {
+
+ updateUI();
+
+ // update previous states for next time
+ prevIsInBounds = isInBounds;
+ prevIsAllDay = isAllDay;
+ prevColDelta = colDelta;
+ prevMinuteDelta = minuteDelta;
+ }
+
+ // if out-of-bounds, revert when done, and vice versa.
+ eventElement.draggable('option', 'revert', !isInBounds);
+
+ },
+ stop: function(ev, ui) {
+
+ clearOverlays();
+ trigger('eventDragStop', eventElement, event, ev, ui);
+
+ if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed!
+ eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, ev, ui);
+ }
+ else { // either no change or out-of-bounds (draggable has already reverted)
+
+ // reset states for next time, and for updateUI()
+ isInBounds = true;
+ isAllDay = false;
+ colDelta = 0;
+ dayDelta = 0;
+ minuteDelta = 0;
+
+ updateUI();
+ eventElement.css('filter', ''); // clear IE opacity side-effects
+
+ // sometimes fast drags make event revert to wrong position, so reset.
+ // also, if we dragged the element out of the area because of snapping,
+ // but the *mouse* is still in bounds, we need to reset the position.
+ eventElement.css(origPosition);
+
+ showEvents(event, eventElement);
+ }
+ }
+ });
+
+ function updateUI() {
+ clearOverlays();
+ if (isInBounds) {
+ if (isAllDay) {
+ timeElement.hide();
+ eventElement.draggable('option', 'grid', null); // disable grid snapping
+ renderDayOverlay(
+ addDays(cloneDate(event.start), dayDelta),
+ addDays(exclEndDay(event), dayDelta)
+ );
+ }
+ else {
+ updateTimeText(minuteDelta);
+ timeElement.css('display', ''); // show() was causing display=inline
+ eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping
+ }
+ }
+ }
+
+ function updateTimeText(minuteDelta) {
+ var newStart = addMinutes(cloneDate(event.start), minuteDelta);
+ var newEnd;
+ if (event.end) {
+ newEnd = addMinutes(cloneDate(event.end), minuteDelta);
+ }
+ timeElement.text(formatDates(newStart, newEnd, opt('timeFormat')));
+ }
+
+ }
+
+
+
+ /* Resizing
+ --------------------------------------------------------------------------------------*/
+
+
+ function resizableSlotEvent(event, eventElement, timeElement) {
+ var snapDelta, prevSnapDelta;
+ var snapHeight = getSnapHeight();
+ var snapMinutes = getSnapMinutes();
+ eventElement.resizable({
+ handles: {
+ s: '.ui-resizable-handle'
+ },
+ grid: snapHeight,
+ start: function(ev, ui) {
+ snapDelta = prevSnapDelta = 0;
+ hideEvents(event, eventElement);
+ trigger('eventResizeStart', this, event, ev, ui);
+ },
+ resize: function(ev, ui) {
+ // don't rely on ui.size.height, doesn't take grid into account
+ snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight);
+ if (snapDelta != prevSnapDelta) {
+ timeElement.text(
+ formatDates(
+ event.start,
+ (!snapDelta && !event.end) ? null : // no change, so don't display time range
+ addMinutes(eventEnd(event), snapMinutes*snapDelta),
+ opt('timeFormat')
+ )
+ );
+ prevSnapDelta = snapDelta;
+ }
+ },
+ stop: function(ev, ui) {
+ trigger('eventResizeStop', this, event, ev, ui);
+ if (snapDelta) {
+ eventResize(this, event, 0, snapMinutes*snapDelta, ev, ui);
+ }else{
+ showEvents(event, eventElement);
+ // BUG: if event was really short, need to put title back in span
+ }
+ }
+ });
+ }
+
+
+}
+
+
+
+/* Agenda Event Segment Utilities
+-----------------------------------------------------------------------------*/
+
+
+// Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new
+// list in the order they should be placed into the DOM (an implicit z-index).
+function placeSlotSegs(segs) {
+ var levels = buildSlotSegLevels(segs);
+ var level0 = levels[0];
+ var i;
+
+ computeForwardSlotSegs(levels);
+
+ if (level0) {
+
+ for (i=0; i<level0.length; i++) {
+ computeSlotSegPressures(level0[i]);
+ }
+
+ for (i=0; i<level0.length; i++) {
+ computeSlotSegCoords(level0[i], 0, 0);
+ }
+ }
+
+ return flattenSlotSegLevels(levels);
+}
+
+
+// Builds an array of segments "levels". The first level will be the leftmost tier of segments
+// if the calendar is left-to-right, or the rightmost if the calendar is right-to-left.
+function buildSlotSegLevels(segs) {
+ var levels = [];
+ var i, seg;
+ var j;
+
+ for (i=0; i<segs.length; i++) {
+ seg = segs[i];
+
+ // go through all the levels and stop on the first level where there are no collisions
+ for (j=0; j<levels.length; j++) {
+ if (!computeSlotSegCollisions(seg, levels[j]).length) {
+ break;
+ }
+ }
+
+ (levels[j] || (levels[j] = [])).push(seg);
+ }
+
+ return levels;
+}
+
+
+// For every segment, figure out the other segments that are in subsequent
+// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
+function computeForwardSlotSegs(levels) {
+ var i, level;
+ var j, seg;
+ var k;
+
+ for (i=0; i<levels.length; i++) {
+ level = levels[i];
+
+ for (j=0; j<level.length; j++) {
+ seg = level[j];
+
+ seg.forwardSegs = [];
+ for (k=i+1; k<levels.length; k++) {
+ computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
+ }
+ }
+ }
+}
+
+
+// Figure out which path forward (via seg.forwardSegs) results in the longest path until
+// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
+function computeSlotSegPressures(seg) {
+ var forwardSegs = seg.forwardSegs;
+ var forwardPressure = 0;
+ var i, forwardSeg;
+
+ if (seg.forwardPressure === undefined) { // not already computed
+
+ for (i=0; i<forwardSegs.length; i++) {
+ forwardSeg = forwardSegs[i];
+
+ // figure out the child's maximum forward path
+ computeSlotSegPressures(forwardSeg);
+
+ // either use the existing maximum, or use the child's forward pressure
+ // plus one (for the forwardSeg itself)
+ forwardPressure = Math.max(
+ forwardPressure,
+ 1 + forwardSeg.forwardPressure
+ );
+ }
+
+ seg.forwardPressure = forwardPressure;
+ }
+}
+
+
+// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
+// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
+// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
+//
+// The segment might be part of a "series", which means consecutive segments with the same pressure
+// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
+// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
+// coordinate of the first segment in the series.
+function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
+ var forwardSegs = seg.forwardSegs;
+ var i;
+
+ if (seg.forwardCoord === undefined) { // not already computed
+
+ if (!forwardSegs.length) {
+
+ // if there are no forward segments, this segment should butt up against the edge
+ seg.forwardCoord = 1;
+ }
+ else {
+
+ // sort highest pressure first
+ forwardSegs.sort(compareForwardSlotSegs);
+
+ // this segment's forwardCoord will be calculated from the backwardCoord of the
+ // highest-pressure forward segment.
+ computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
+ seg.forwardCoord = forwardSegs[0].backwardCoord;
+ }
+
+ // calculate the backwardCoord from the forwardCoord. consider the series
+ seg.backwardCoord = seg.forwardCoord -
+ (seg.forwardCoord - seriesBackwardCoord) / // available width for series
+ (seriesBackwardPressure + 1); // # of segments in the series
+
+ // use this segment's coordinates to computed the coordinates of the less-pressurized
+ // forward segments
+ for (i=0; i<forwardSegs.length; i++) {
+ computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
+ }
+ }
+}
+
+
+// Outputs a flat array of segments, from lowest to highest level
+function flattenSlotSegLevels(levels) {
+ var segs = [];
+ var i, level;
+ var j;
+
+ for (i=0; i<levels.length; i++) {
+ level = levels[i];
+
+ for (j=0; j<level.length; j++) {
+ segs.push(level[j]);
+ }
+ }
+
+ return segs;
+}
+
+
+// Find all the segments in `otherSegs` that vertically collide with `seg`.
+// Append into an optionally-supplied `results` array and return.
+function computeSlotSegCollisions(seg, otherSegs, results) {
+ results = results || [];
+
+ for (var i=0; i<otherSegs.length; i++) {
+ if (isSlotSegCollision(seg, otherSegs[i])) {
+ results.push(otherSegs[i]);
+ }
+ }
+
+ return results;
+}
+
+
+// Do these segments occupy the same vertical space?
+function isSlotSegCollision(seg1, seg2) {
+ return seg1.end > seg2.start && seg1.start < seg2.end;
+}
+
+
+// A cmp function for determining which forward segment to rely on more when computing coordinates.
+function compareForwardSlotSegs(seg1, seg2) {
+ // put higher-pressure first
+ return seg2.forwardPressure - seg1.forwardPressure ||
+ // put segments that are closer to initial edge first (and favor ones with no coords yet)
+ (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
+ // do normal sorting...
+ compareSlotSegs(seg1, seg2);
+}
+
+
+// A cmp function for determining which segment should be closer to the initial edge
+// (the left edge on a left-to-right calendar).
+function compareSlotSegs(seg1, seg2) {
+ return seg1.start - seg2.start || // earlier start time goes first
+ (seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first
+ (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
+}
+
+
+;;
+
+/* Additional view: list (by bruederli@kolabsys.com)
+---------------------------------------------------------------------------------*/
+
+fcViews.list = ListView;
+
+
+function ListView(element, calendar) {
+ var t = this;
+
+ // exports
+ t.render = render;
+ t.select = dummy;
+ t.unselect = dummy;
+ t.reportSelection = dummy;
+ t.getDaySegmentContainer = function(){ return body; };
+
+ // imports
+ View.call(t, element, calendar, 'list');
+ ListEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var clearEvents = t.clearEvents;
+ var reportEventClear = t.reportEventClear;
+ var formatDates = calendar.formatDates;
+ var formatDate = calendar.formatDate;
+
+ // overrides
+ t.setWidth = setWidth;
+ t.setHeight = setHeight;
+
+ // locals
+ var body;
+ var firstDay;
+ var nwe;
+ var tm;
+ var colFormat;
+
+
+ function render(date, delta) {
+ if (delta) {
+ addDays(date, opt('listPage') * delta);
+ }
+ t.start = t.visStart = cloneDate(date, true);
+ t.end = addDays(cloneDate(t.start), opt('listPage'));
+ t.visEnd = addDays(cloneDate(t.start), opt('listRange'));
+ addMinutes(t.visEnd, -1); // set end to 23:59
+ t.title = formatDates(date, t.visEnd, opt('titleFormat'));
+
+ updateOptions();
+
+ if (!body) {
+ buildSkeleton();
+ } else {
+ clearEvents();
+ }
+ }
+
+
+ function updateOptions() {
+ firstDay = opt('firstDay');
+ nwe = opt('weekends') ? 0 : 1;
+ tm = opt('theme') ? 'ui' : 'fc';
+ colFormat = opt('columnFormat', 'day');
+ }
+
+
+ function buildSkeleton() {
+ body = $('<div>').addClass('fc-list-content').appendTo(element);
+ }
+
+ function setHeight(height, dateChanged) {
+ if (!opt('listNoHeight'))
+ body.css('height', (height-1)+'px').css('overflow', 'auto');
+ }
+
+ function setWidth(width) {
+ // nothing to be done here
+ }
+
+ function dummy() {
+ // Stub.
+ }
+
+}
+
+;;
+
+/* Additional view renderer: list (by bruederli@kolabsys.com)
+---------------------------------------------------------------------------------*/
+
+function ListEventRenderer() {
+ var t = this;
+
+ // exports
+ t.renderEvents = renderEvents;
+ t.renderEventTime = renderEventTime;
+ t.compileDaySegs = compileSegs; // for DayEventRenderer
+ t.clearEvents = clearEvents;
+ t.lazySegBind = lazySegBind;
+ t.sortCmp = sortCmp;
+
+ // imports
+ DayEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var reportEventElement = t.reportEventElement;
+ var eventElementHandlers = t.eventElementHandlers;
+ var showEvents = t.showEvents;
+ var hideEvents = t.hideEvents;
+ var getListContainer = t.getDaySegmentContainer;
+ var calendar = t.calendar;
+ var formatDate = calendar.formatDate;
+ var formatDates = calendar.formatDates;
+
+
+ /* Rendering
+ --------------------------------------------------------------------*/
+
+ function clearEvents() {
+ getListContainer().empty();
+ }
+
+ function renderEvents(events, modifiedEventId) {
+ events.sort(sortCmp);
+ clearEvents();
+ renderSegs(compileSegs(events), modifiedEventId);
+ }
+
+ function compileSegs(events) {
+ var segs = [];
+ var colFormat = opt('titleFormat', 'day');
+ var firstDay = opt('firstDay');
+ var segmode = opt('listSections');
+ var event, i, dd, wd, md, seg, segHash, curSegHash, segDate, curSeg = -1;
+ var today = clearTime(new Date());
+ var weekstart = addDays(cloneDate(today), -((today.getDay() - firstDay + 7) % 7));
+
+ for (i=0; i < events.length; i++) {
+ event = events[i];
+
+ // skip events out of range
+ if ((event.end || event.start) < t.start || event.start > t.visEnd)
+ continue;
+
+ // define sections of this event
+ // create smart sections such as today, tomorrow, this week, next week, next month, ect.
+ segDate = cloneDate(event.start < t.start && event.end > t.start ? t.start : event.start, true);
+ dd = dayDiff(segDate, today);
+ wd = Math.floor(dayDiff(segDate, weekstart) / 7);
+ md = segDate.getMonth() + ((segDate.getYear() - today.getYear()) * 12) - today.getMonth();
+
+ // build section title
+ if (segmode == 'smart') {
+ if (dd < 0) {
+ segHash = opt('listTexts', 'past');
+ } else if (dd == 0) {
+ segHash = opt('listTexts', 'today');
+ } else if (dd == 1) {
+ segHash = opt('listTexts', 'tomorrow');
+ } else if (wd == 0) {
+ segHash = opt('listTexts', 'thisWeek');
+ } else if (wd == 1) {
+ segHash = opt('listTexts', 'nextWeek');
+ } else if (md == 0) {
+ segHash = opt('listTexts', 'thisMonth');
+ } else if (md == 1) {
+ segHash = opt('listTexts', 'nextMonth');
+ } else if (md > 1) {
+ segHash = opt('listTexts', 'future');
+ }
+ } else if (segmode == 'month') {
+ segHash = formatDate(segDate, 'MMMM yyyy');
+ } else if (segmode == 'week') {
+ segHash = opt('listTexts', 'week') + formatDate(segDate, ' W');
+ } else if (segmode == 'day') {
+ segHash = formatDate(segDate, colFormat);
+ } else {
+ segHash = '';
+ }
+
+ // start new segment
+ if (segHash != curSegHash) {
+ segs[++curSeg] = { events: [], start: segDate, title: segHash, daydiff: dd, weekdiff: wd, monthdiff: md };
+ curSegHash = segHash;
+ }
+
+ segs[curSeg].events.push(event);
+ }
+
+ return segs;
+ }
+
+ function sortCmp(a, b) {
+ var sd = a.start.getTime() - b.start.getTime();
+ return sd || (a.end ? a.end.getTime() : 0) - (b.end ? b.end.getTime() : 0);
+ }
+
+ function renderSegs(segs, modifiedEventId) {
+ var tm = opt('theme') ? 'ui' : 'fc';
+ var headerClass = tm + "-widget-header";
+ var contentClass = tm + "-widget-content";
+ var i, j, seg, event, times, s, skinCss, skinCssAttr, classes, segContainer, eventElement, eventElements, triggerRes;
+
+ for (j=0; j < segs.length; j++) {
+ seg = segs[j];
+
+ if (seg.title) {
+ $('<div class="fc-list-header ' + headerClass + '">' + htmlEscape(seg.title) + '</div>').appendTo(getListContainer());
+ }
+ segContainer = $('<div>').addClass('fc-list-section ' + contentClass).appendTo(getListContainer());
+ s = '';
+
+ for (i=0; i < seg.events.length; i++) {
+ event = seg.events[i];
+ times = renderEventTime(event, seg);
+ skinCss = getSkinCss(event, opt);
+ skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
+ classes = ['fc-event', 'fc-event-skin', 'fc-event-vert', 'fc-corner-top', 'fc-corner-bottom'].concat(event.className);
+ if (event.source && event.source.className) {
+ classes = classes.concat(event.source.className);
+ }
+
+ s +=
+ "<div class='" + classes.join(' ') + "'" + skinCssAttr + ">" +
+ "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
+ "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
+ "<div class='fc-event-time'>" +
+ (times[0] ? '<span class="fc-col-date">' + times[0] + '</span> ' : '') +
+ (times[1] ? '<span class="fc-col-time">' + times[1] + '</span>' : '') +
+ "</div>" +
+ "</div>" +
+ "<div class='fc-event-content'>" +
+ "<div class='fc-event-title'>" +
+ htmlEscape(event.title) +
+ "</div>" +
+ "</div>" +
+ "<div class='fc-event-bg'></div>" +
+ "</div>" + // close inner
+ "</div>"; // close outer
+ }
+
+ segContainer[0].innerHTML = s;
+ eventElements = segContainer.children();
+
+ // retrieve elements, run through eventRender callback, bind event handlers
+ for (i=0; i < seg.events.length; i++) {
+ event = seg.events[i];
+ eventElement = $(eventElements[i]); // faster than eq()
+ triggerRes = trigger('eventRender', event, event, eventElement);
+ if (triggerRes === false) {
+ eventElement.remove();
+ } else {
+ if (triggerRes && triggerRes !== true) {
+ eventElement.remove();
+ eventElement = $(triggerRes).appendTo(segContainer);
+ }
+ if (event._id === modifiedEventId) {
+ eventElementHandlers(event, eventElement, seg);
+ } else {
+ eventElement[0]._fci = i; // for lazySegBind
+ }
+ reportEventElement(event, eventElement);
+ }
+ }
+
+ lazySegBind(segContainer, seg, eventElementHandlers);
+ }
+
+ markFirstLast(getListContainer());
+ }
+
+ // event time/date range to display
+ function renderEventTime(event, seg) {
+ var timeFormat = opt('timeFormat');
+ var dateFormat = opt('columnFormat');
+ var segmode = opt('listSections');
+ var duration = event.end ? event.end.getTime() - event.start.getTime() : 0;
+ var datestr = '', timestr = '';
+
+ if (segmode == 'smart') {
+ if (event.start < seg.start) {
+ datestr = opt('listTexts', 'until') + ' ' + formatDate(event.end, (event.allDay || event.end.getDate() != seg.start.getDate()) ? dateFormat : timeFormat);
+ } else if (duration > DAY_MS) {
+ datestr = formatDates(event.start, event.end, dateFormat + '{ - ' + dateFormat + '}');
+ } else if (seg.daydiff == 0) {
+ datestr = opt('listTexts', 'today');
+ } else if (seg.daydiff == 1) {
+ datestr = opt('listTexts', 'tomorrow');
+ } else if (seg.weekdiff == 0 || seg.weekdiff == 1) {
+ datestr = formatDate(event.start, 'dddd');
+ } else if (seg.daydiff > 1 || seg.daydiff < 0) {
+ datestr = formatDate(event.start, dateFormat);
+ }
+ } else if (segmode != 'day') {
+ datestr = formatDates(event.start, event.end, dateFormat + (duration > DAY_MS ? '{ - ' + dateFormat + '}' : ''));
+ }
+
+ if (!datestr && event.allDay) {
+ timestr = opt('allDayText');
+ } else if ((duration < DAY_MS || !datestr) && !event.allDay) {
+ timestr = formatDates(event.start, event.end, timeFormat);
+ }
+
+ return [datestr, timestr];
+ }
+
+ function lazySegBind(container, seg, bindHandlers) {
+ container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
+ var parent = ev.target, e = parent, i, event;
+ while (parent != this) {
+ e = parent;
+ parent = parent.parentNode;
+ }
+ if ((i = e._fci) !== undefined) {
+ e._fci = undefined;
+ event = seg.events[i];
+ bindHandlers(event, container.children().eq(i), seg);
+ $(ev.target).trigger(ev);
+ }
+ ev.stopPropagation();
+ });
+ }
+
+}
+
+
+;;
+
+/* Additional view: table (by bruederli@kolabsys.com)
+---------------------------------------------------------------------------------*/
+
+fcViews.table = TableView;
+
+
+function TableView(element, calendar) {
+ var t = this;
+
+ // exports
+ t.render = render;
+ t.select = dummy;
+ t.unselect = dummy;
+ t.getDaySegmentContainer = function(){ return table; };
+
+ // imports
+ View.call(t, element, calendar, 'table');
+ TableEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var clearEvents = t.clearEvents;
+ var reportEventClear = t.reportEventClear;
+ var formatDates = calendar.formatDates;
+ var formatDate = calendar.formatDate;
+
+ // overrides
+ t.setWidth = setWidth;
+ t.setHeight = setHeight;
+
+ // locals
+ var div;
+ var table;
+ var firstDay;
+ var nwe;
+ var tm;
+ var colFormat;
+
+
+ function render(date, delta) {
+ if (delta) {
+ addDays(date, opt('listPage') * delta);
+ }
+ t.start = t.visStart = cloneDate(date, true);
+ t.end = addDays(cloneDate(t.start), opt('listPage'));
+ t.visEnd = addDays(cloneDate(t.start), opt('listRange'));
+ addMinutes(t.visEnd, -1); // set end to 23:59
+ t.title = (t.visEnd.getTime() - t.visStart.getTime() < DAY_MS) ? formatDate(date, opt('titleFormat')) : formatDates(date, t.visEnd, opt('titleFormat'));
+
+ updateOptions();
+
+ if (!table) {
+ buildSkeleton();
+ } else {
+ clearEvents();
+ }
+ }
+
+
+ function updateOptions() {
+ firstDay = opt('firstDay');
+ nwe = opt('weekends') ? 0 : 1;
+ tm = opt('theme') ? 'ui' : 'fc';
+ colFormat = opt('columnFormat');
+ }
+
+
+ function buildSkeleton() {
+ var tableCols = opt('tableCols');
+ var s =
+ "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
+ "<colgroup>";
+ for (var c=0; c < tableCols.length; c++) {
+ s += "<col class='fc-event-" + tableCols[c] + "' />";
+ }
+ s += "</colgroup>" +
+ "</table>";
+ div = $('<div>').addClass('fc-list-content').appendTo(element);
+ table = $(s).appendTo(div);
+ }
+
+ function setHeight(height, dateChanged) {
+ if (!opt('listNoHeight'))
+ div.css('height', (height-1)+'px').css('overflow', 'auto');
+ }
+
+ function setWidth(width) {
+ // nothing to be done here
+ }
+
+ function dummy() {
+ // Stub.
+ }
+
+}
+
+;;
+
+/* Additional view renderer: table (by bruederli@kolabsys.com)
+---------------------------------------------------------------------------------*/
+
+function TableEventRenderer() {
+ var t = this;
+
+ // imports
+ ListEventRenderer.call(t);
+ var opt = t.opt;
+ var sortCmp = t.sortCmp;
+ var trigger = t.trigger;
+ var compileSegs = t.compileDaySegs;
+ var reportEventElement = t.reportEventElement;
+ var eventElementHandlers = t.eventElementHandlers;
+ var renderEventTime = t.renderEventTime;
+ var showEvents = t.showEvents;
+ var hideEvents = t.hideEvents;
+ var getListContainer = t.getDaySegmentContainer;
+ var lazySegBind = t.lazySegBind;
+ var calendar = t.calendar;
+ var formatDate = calendar.formatDate;
+ var formatDates = calendar.formatDates;
+
+ // exports
+ t.renderEvents = renderEvents;
+ t.clearEvents = clearEvents;
+
+
+ /* Rendering
+ --------------------------------------------------------------------*/
+
+ function clearEvents() {
+ getListContainer().children('tbody').remove();
+ }
+
+ function renderEvents(events, modifiedEventId) {
+ events.sort(sortCmp);
+ clearEvents();
+ renderSegs(compileSegs(events), modifiedEventId);
+ getListContainer().removeClass('fc-list-smart fc-list-day fc-list-month fc-list-week').addClass('fc-list-' + opt('listSections'));
+ }
+
+ function renderSegs(segs, modifiedEventId) {
+ var tm = opt('theme') ? 'ui' : 'fc';
+ var table = getListContainer();
+ var headerClass = tm + "-widget-header";
+ var contentClass = tm + "-widget-content";
+ var tableCols = opt('tableCols');
+ var timecol = $.inArray('time', tableCols) >= 0;
+ var i, j, seg, event, times, s, skinCss, skinCssAttr, skinClasses, rowClasses, segContainer, eventElements, eventElement, triggerRes;
+
+ for (j=0; j < segs.length; j++) {
+ seg = segs[j];
+
+ if (seg.title) {
+ $('<tbody class="fc-list-header"><tr><td class="fc-list-header ' + headerClass + '" colspan="' + tableCols.length + '">' + htmlEscape(seg.title) + '</td></tr></tbody>').appendTo(table);
+ }
+ segContainer = $('<tbody>').addClass('fc-list-section ' + contentClass).appendTo(table);
+ s = '';
+
+ for (i=0; i < seg.events.length; i++) {
+ event = seg.events[i];
+ times = renderEventTime(event, seg);
+ skinCss = getSkinCss(event, opt);
+ skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
+ skinClasses = ['fc-event-skin', 'fc-corner-left', 'fc-corner-right', 'fc-corner-top', 'fc-corner-bottom'].concat(event.className);
+ if (event.source && event.source.className) {
+ skinClasses = skinClasses.concat(event.source.className);
+ }
+ rowClasses = ['fc-event', 'fc-event-row', 'fc-'+dayIDs[event.start.getDay()]].concat(event.className);
+ if (seg.daydiff == 0) {
+ rowClasses.push('fc-today');
+ }
+
+ s += "<tr class='" + rowClasses.join(' ') + "' tabindex='0'>";
+ for (var col, c=0; c < tableCols.length; c++) {
+ col = tableCols[c];
+ if (col == 'handle') {
+ s += "<td class='fc-event-handle'>" +
+ "<div class='" + skinClasses.join(' ') + "'" + skinCssAttr + ">" +
+ "<span class='fc-event-inner'></span>" +
+ "</div></td>";
+ } else if (col == 'date') {
+ s += "<td class='fc-event-date' colspan='" + (times[1] || !timecol ? 1 : 2) + "'>" + htmlEscape(times[0]) + "</td>";
+ } else if (col == 'time') {
+ if (times[1]) {
+ s += "<td class='fc-event-time'>" + htmlEscape(times[1]) + "</td>";
+ }
+ } else {
+ s += "<td class='fc-event-" + col + "'>" + (event[col] ? htmlEscape(event[col]) : '&nbsp;') + "</td>";
+ }
+ }
+ s += "</tr>";
+
+ // IE doesn't like innerHTML on tbody elements so we insert every row individually
+ if (document.all) {
+ $(s).appendTo(segContainer);
+ s = '';
+ }
+ }
+
+ if (!document.all)
+ segContainer[0].innerHTML = s;
+
+ eventElements = segContainer.children();
+
+ // retrieve elements, run through eventRender callback, bind event handlers
+ for (i=0; i < seg.events.length; i++) {
+ event = seg.events[i];
+ eventElement = $(eventElements[i]); // faster than eq()
+ triggerRes = trigger('eventRender', event, event, eventElement);
+ if (triggerRes === false) {
+ eventElement.remove();
+ } else {
+ if (triggerRes && triggerRes !== true) {
+ eventElement.remove();
+ eventElement = $(triggerRes).appendTo(segContainer);
+ }
+ if (event._id === modifiedEventId) {
+ eventElementHandlers(event, eventElement, seg);
+ } else {
+ eventElement[0]._fci = i; // for lazySegBind
+ }
+ reportEventElement(event, eventElement);
+ }
+ }
+
+ lazySegBind(segContainer, seg, eventElementHandlers);
+ markFirstLast(segContainer);
+ }
+
+ //markFirstLast(table);
+ }
+
+}
+;;
+
+
+function View(element, calendar, viewName) {
+ var t = this;
+
+
+ // exports
+ t.element = element;
+ t.calendar = calendar;
+ t.name = viewName;
+ t.opt = opt;
+ t.trigger = trigger;
+ t.isEventDraggable = isEventDraggable;
+ t.isEventResizable = isEventResizable;
+ t.setEventData = setEventData;
+ t.clearEventData = clearEventData;
+ t.eventEnd = eventEnd;
+ t.reportEventElement = reportEventElement;
+ t.triggerEventDestroy = triggerEventDestroy;
+ t.eventElementHandlers = eventElementHandlers;
+ t.showEvents = showEvents;
+ t.hideEvents = hideEvents;
+ t.eventDrop = eventDrop;
+ t.eventResize = eventResize;
+ // t.title
+ // t.start, t.end
+ // t.visStart, t.visEnd
+
+
+ // imports
+ var defaultEventEnd = t.defaultEventEnd;
+ var normalizeEvent = calendar.normalizeEvent; // in EventManager
+ var reportEventChange = calendar.reportEventChange;
+
+
+ // locals
+ var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events)
+ var eventElementsByID = {}; // eventID mapped to array of jQuery elements
+ var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system
+ var options = calendar.options;
+
+
+
+ function opt(name, viewNameOverride) {
+ var v = options[name];
+ if ($.isPlainObject(v)) {
+ return smartProperty(v, viewNameOverride || viewName);
+ }
+ return v;
+ }
+
+
+ function trigger(name, thisObj) {
+ return calendar.trigger.apply(
+ calendar,
+ [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
+ );
+ }
+
+
+
+ /* Event Editable Boolean Calculations
+ ------------------------------------------------------------------------------*/
+
+
+ function isEventDraggable(event) {
+ var source = event.source || {};
+ return firstDefined(
+ event.startEditable,
+ source.startEditable,
+ opt('eventStartEditable'),
+ event.editable,
+ source.editable,
+ opt('editable')
+ )
+ && !opt('disableDragging'); // deprecated
+ }
+
+
+ function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
+ var source = event.source || {};
+ return firstDefined(
+ event.durationEditable,
+ source.durationEditable,
+ opt('eventDurationEditable'),
+ event.editable,
+ source.editable,
+ opt('editable')
+ )
+ && !opt('disableResizing'); // deprecated
+ }
+
+
+
+ /* Event Data
+ ------------------------------------------------------------------------------*/
+
+
+ function setEventData(events) { // events are already normalized at this point
+ eventsByID = {};
+ var i, len=events.length, event;
+ for (i=0; i<len; i++) {
+ event = events[i];
+ if (eventsByID[event._id]) {
+ eventsByID[event._id].push(event);
+ }else{
+ eventsByID[event._id] = [event];
+ }
+ }
+ }
+
+
+ function clearEventData() {
+ eventsByID = {};
+ eventElementsByID = {};
+ eventElementCouples = [];
+ }
+
+
+ // returns a Date object for an event's end
+ function eventEnd(event) {
+ return event.end ? cloneDate(event.end) : defaultEventEnd(event);
+ }
+
+
+
+ /* Event Elements
+ ------------------------------------------------------------------------------*/
+
+
+ // report when view creates an element for an event
+ function reportEventElement(event, element) {
+ eventElementCouples.push({ event: event, element: element });
+ if (eventElementsByID[event._id]) {
+ eventElementsByID[event._id].push(element);
+ }else{
+ eventElementsByID[event._id] = [element];
+ }
+ }
+
+
+ function triggerEventDestroy() {
+ $.each(eventElementCouples, function(i, couple) {
+ t.trigger('eventDestroy', couple.event, couple.event, couple.element);
+ });
+ }
+
+
+ // attaches eventClick, eventMouseover, eventMouseout
+ function eventElementHandlers(event, eventElement) {
+ eventElement
+ .click(function(ev) {
+ if (!eventElement.hasClass('ui-draggable-dragging') &&
+ !eventElement.hasClass('ui-resizable-resizing')) {
+ return trigger('eventClick', this, event, ev);
+ }
+ })
+ .hover(
+ function(ev) {
+ trigger('eventMouseover', this, event, ev);
+ },
+ function(ev) {
+ trigger('eventMouseout', this, event, ev);
+ }
+ )
+ .keypress(function(ev) {
+ if (ev.keyCode == 13)
+ $(this).trigger('click', { pointerType:'keyboard' });
+ });
+ // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
+ // TODO: same for resizing
+ }
+
+
+ function showEvents(event, exceptElement) {
+ eachEventElement(event, exceptElement, 'show');
+ }
+
+
+ function hideEvents(event, exceptElement) {
+ eachEventElement(event, exceptElement, 'hide');
+ }
+
+
+ function eachEventElement(event, exceptElement, funcName) {
+ // NOTE: there may be multiple events per ID (repeating events)
+ // and multiple segments per event
+ var elements = eventElementsByID[event._id],
+ i, len = elements.length;
+ for (i=0; i<len; i++) {
+ if (!exceptElement || elements[i][0] != exceptElement[0]) {
+ elements[i][funcName]();
+ }
+ }
+ }
+
+
+
+ /* Event Modification Reporting
+ ---------------------------------------------------------------------------------*/
+
+
+ function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
+ var oldAllDay = event.allDay;
+ var eventId = event._id;
+ moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
+ trigger(
+ 'eventDrop',
+ e,
+ event,
+ dayDelta,
+ minuteDelta,
+ allDay,
+ function() {
+ // TODO: investigate cases where this inverse technique might not work
+ moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
+ reportEventChange(eventId);
+ },
+ ev,
+ ui
+ );
+ reportEventChange(eventId);
+ }
+
+
+ function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
+ var eventId = event._id;
+ elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
+ trigger(
+ 'eventResize',
+ e,
+ event,
+ dayDelta,
+ minuteDelta,
+ function() {
+ // TODO: investigate cases where this inverse technique might not work
+ elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
+ reportEventChange(eventId);
+ },
+ ev,
+ ui
+ );
+ reportEventChange(eventId);
+ }
+
+
+
+ /* Event Modification Math
+ ---------------------------------------------------------------------------------*/
+
+
+ function moveEvents(events, dayDelta, minuteDelta, allDay) {
+ minuteDelta = minuteDelta || 0;
+ for (var e, len=events.length, i=0; i<len; i++) {
+ e = events[i];
+ if (allDay !== undefined) {
+ e.allDay = allDay;
+ }
+ addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
+ if (e.end) {
+ e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
+ }
+ normalizeEvent(e, options);
+ }
+ }
+
+
+ function elongateEvents(events, dayDelta, minuteDelta) {
+ minuteDelta = minuteDelta || 0;
+ for (var e, len=events.length, i=0; i<len; i++) {
+ e = events[i];
+ e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
+ normalizeEvent(e, options);
+ }
+ }
+
+
+
+ // ====================================================================================================
+ // Utilities for day "cells"
+ // ====================================================================================================
+ // The "basic" views are completely made up of day cells.
+ // The "agenda" views have day cells at the top "all day" slot.
+ // This was the obvious common place to put these utilities, but they should be abstracted out into
+ // a more meaningful class (like DayEventRenderer).
+ // ====================================================================================================
+
+
+ // For determining how a given "cell" translates into a "date":
+ //
+ // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
+ // Keep in mind that column indices are inverted with isRTL. This is taken into account.
+ //
+ // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
+ //
+ // 3. Convert the "day offset" into a "date" (a JavaScript Date object).
+ //
+ // The reverse transformation happens when transforming a date into a cell.
+
+
+ // exports
+ t.isHiddenDay = isHiddenDay;
+ t.skipHiddenDays = skipHiddenDays;
+ t.getCellsPerWeek = getCellsPerWeek;
+ t.dateToCell = dateToCell;
+ t.dateToDayOffset = dateToDayOffset;
+ t.dayOffsetToCellOffset = dayOffsetToCellOffset;
+ t.cellOffsetToCell = cellOffsetToCell;
+ t.cellToDate = cellToDate;
+ t.cellToCellOffset = cellToCellOffset;
+ t.cellOffsetToDayOffset = cellOffsetToDayOffset;
+ t.dayOffsetToDate = dayOffsetToDate;
+ t.rangeToSegments = rangeToSegments;
+
+
+ // internals
+ var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
+ var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
+ var cellsPerWeek;
+ var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
+ var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
+ var isRTL = opt('isRTL');
+
+
+ // initialize important internal variables
+ (function() {
+
+ if (opt('weekends') === false) {
+ hiddenDays.push(0, 6); // 0=sunday, 6=saturday
+ }
+
+ // Loop through a hypothetical week and determine which
+ // days-of-week are hidden. Record in both hashes (one is the reverse of the other).
+ for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
+ dayToCellMap[dayIndex] = cellIndex;
+ isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
+ if (!isHiddenDayHash[dayIndex]) {
+ cellToDayMap[cellIndex] = dayIndex;
+ cellIndex++;
+ }
+ }
+
+ cellsPerWeek = cellIndex;
+ if (!cellsPerWeek) {
+ throw 'invalid hiddenDays'; // all days were hidden? bad.
+ }
+
+ })();
+
+
+ // Is the current day hidden?
+ // `day` is a day-of-week index (0-6), or a Date object
+ function isHiddenDay(day) {
+ if (typeof day == 'object') {
+ day = day.getDay();
+ }
+ return isHiddenDayHash[day];
+ }
+
+
+ function getCellsPerWeek() {
+ return cellsPerWeek;
+ }
+
+
+ // Keep incrementing the current day until it is no longer a hidden day.
+ // If the initial value of `date` is not a hidden day, don't do anything.
+ // Pass `isExclusive` as `true` if you are dealing with an end date.
+ // `inc` defaults to `1` (increment one day forward each time)
+ function skipHiddenDays(date, inc, isExclusive) {
+ inc = inc || 1;
+ while (
+ isHiddenDayHash[ ( date.getDay() + (isExclusive ? inc : 0) + 7 ) % 7 ]
+ ) {
+ addDays(date, inc);
+ }
+ }
+
+
+ //
+ // TRANSFORMATIONS: cell -> cell offset -> day offset -> date
+ //
+
+ // cell -> date (combines all transformations)
+ // Possible arguments:
+ // - row, col
+ // - { row:#, col: # }
+ function cellToDate() {
+ var cellOffset = cellToCellOffset.apply(null, arguments);
+ var dayOffset = cellOffsetToDayOffset(cellOffset);
+ var date = dayOffsetToDate(dayOffset);
+ return date;
+ }
+
+ // cell -> cell offset
+ // Possible arguments:
+ // - row, col
+ // - { row:#, col:# }
+ function cellToCellOffset(row, col) {
+ var colCnt = t.getColCnt();
+
+ // rtl variables. wish we could pre-populate these. but where?
+ var dis = isRTL ? -1 : 1;
+ var dit = isRTL ? colCnt - 1 : 0;
+
+ if (typeof row == 'object') {
+ col = row.col;
+ row = row.row;
+ }
+ var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
+
+ return cellOffset;
+ }
+
+ // cell offset -> day offset
+ function cellOffsetToDayOffset(cellOffset) {
+ var day0 = t.visStart.getDay(); // first date's day of week
+ cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
+ return Math.floor(cellOffset / cellsPerWeek) * 7 // # of days from full weeks
+ + cellToDayMap[ // # of days from partial last week
+ (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
+ ]
+ - day0; // adjustment for beginning-of-week normalization
+ }
+
+ // day offset -> date (JavaScript Date object)
+ function dayOffsetToDate(dayOffset) {
+ var date = cloneDate(t.visStart);
+ addDays(date, dayOffset);
+ return date;
+ }
+
+
+ //
+ // TRANSFORMATIONS: date -> day offset -> cell offset -> cell
+ //
+
+ // date -> cell (combines all transformations)
+ function dateToCell(date) {
+ var dayOffset = dateToDayOffset(date);
+ var cellOffset = dayOffsetToCellOffset(dayOffset);
+ var cell = cellOffsetToCell(cellOffset);
+ return cell;
+ }
+
+ // date -> day offset
+ function dateToDayOffset(date) {
+ return dayDiff(date, t.visStart);
+ }
+
+ // day offset -> cell offset
+ function dayOffsetToCellOffset(dayOffset) {
+ var day0 = t.visStart.getDay(); // first date's day of week
+ dayOffset += day0; // normalize dayOffset to beginning-of-week
+ return Math.floor(dayOffset / 7) * cellsPerWeek // # of cells from full weeks
+ + dayToCellMap[ // # of cells from partial last week
+ (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
+ ]
+ - dayToCellMap[day0]; // adjustment for beginning-of-week normalization
+ }
+
+ // cell offset -> cell (object with row & col keys)
+ function cellOffsetToCell(cellOffset) {
+ var colCnt = t.getColCnt();
+
+ // rtl variables. wish we could pre-populate these. but where?
+ var dis = isRTL ? -1 : 1;
+ var dit = isRTL ? colCnt - 1 : 0;
+
+ var row = Math.floor(cellOffset / colCnt);
+ var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
+ return {
+ row: row,
+ col: col
+ };
+ }
+
+
+ //
+ // Converts a date range into an array of segment objects.
+ // "Segments" are horizontal stretches of time, sliced up by row.
+ // A segment object has the following properties:
+ // - row
+ // - cols
+ // - isStart
+ // - isEnd
+ //
+ function rangeToSegments(startDate, endDate) {
+ var rowCnt = t.getRowCnt();
+ var colCnt = t.getColCnt();
+ var segments = []; // array of segments to return
+
+ // day offset for given date range
+ var rangeDayOffsetStart = dateToDayOffset(startDate);
+ var rangeDayOffsetEnd = dateToDayOffset(endDate); // exclusive
+
+ // first and last cell offset for the given date range
+ // "last" implies inclusivity
+ var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
+ var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
+
+ // loop through all the rows in the view
+ for (var row=0; row<rowCnt; row++) {
+
+ // first and last cell offset for the row
+ var rowCellOffsetFirst = row * colCnt;
+ var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
+
+ // get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
+ var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
+ var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
+
+ // make sure segment's offsets are valid and in view
+ if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
+
+ // translate to cells
+ var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
+ var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
+
+ // view might be RTL, so order by leftmost column
+ var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
+
+ // Determine if segment's first/last cell is the beginning/end of the date range.
+ // We need to compare "day offset" because "cell offsets" are often ambiguous and
+ // can translate to multiple days, and an edge case reveals itself when we the
+ // range's first cell is hidden (we don't want isStart to be true).
+ var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
+ var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; // +1 for comparing exclusively
+
+ segments.push({
+ row: row,
+ leftCol: cols[0],
+ rightCol: cols[1],
+ isStart: isStart,
+ isEnd: isEnd
+ });
+ }
+ }
+
+ return segments;
+ }
+
+
+}
+
+;;
+
+function DayEventRenderer() {
+ var t = this;
+
+
+ // exports
+ t.renderDayEvents = renderDayEvents;
+ t.draggableDayEvent = draggableDayEvent; // made public so that subclasses can override
+ t.resizableDayEvent = resizableDayEvent; // "
+
+
+ // imports
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var isEventDraggable = t.isEventDraggable;
+ var isEventResizable = t.isEventResizable;
+ var eventEnd = t.eventEnd;
+ var reportEventElement = t.reportEventElement;
+ var eventElementHandlers = t.eventElementHandlers;
+ var showEvents = t.showEvents;
+ var hideEvents = t.hideEvents;
+ var eventDrop = t.eventDrop;
+ var eventResize = t.eventResize;
+ var getRowCnt = t.getRowCnt;
+ var getColCnt = t.getColCnt;
+ var getColWidth = t.getColWidth;
+ var allDayRow = t.allDayRow; // TODO: rename
+ var colLeft = t.colLeft;
+ var colRight = t.colRight;
+ var colContentLeft = t.colContentLeft;
+ var colContentRight = t.colContentRight;
+ var dateToCell = t.dateToCell;
+ var getDaySegmentContainer = t.getDaySegmentContainer;
+ var formatDates = t.calendar.formatDates;
+ var renderDayOverlay = t.renderDayOverlay;
+ var clearOverlays = t.clearOverlays;
+ var clearSelection = t.clearSelection;
+ var getHoverListener = t.getHoverListener;
+ var rangeToSegments = t.rangeToSegments;
+ var cellToDate = t.cellToDate;
+ var cellToCellOffset = t.cellToCellOffset;
+ var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
+ var dateToDayOffset = t.dateToDayOffset;
+ var dayOffsetToCellOffset = t.dayOffsetToCellOffset;
+
+
+ // Render `events` onto the calendar, attach mouse event handlers, and call the `eventAfterRender` callback for each.
+ // Mouse event will be lazily applied, except if the event has an ID of `modifiedEventId`.
+ // Can only be called when the event container is empty (because it wipes out all innerHTML).
+ function renderDayEvents(events, modifiedEventId) {
+
+ // do the actual rendering. Receive the intermediate "segment" data structures.
+ var segments = _renderDayEvents(
+ events,
+ false, // don't append event elements
+ true // set the heights of the rows
+ );
+
+ // report the elements to the View, for general drag/resize utilities
+ segmentElementEach(segments, function(segment, element) {
+ reportEventElement(segment.event, element);
+ });
+
+ // attach mouse handlers
+ attachHandlers(segments, modifiedEventId);
+
+ // call `eventAfterRender` callback for each event
+ segmentElementEach(segments, function(segment, element) {
+ trigger('eventAfterRender', segment.event, segment.event, element);
+ });
+ }
+
+
+ // Render an event on the calendar, but don't report them anywhere, and don't attach mouse handlers.
+ // Append this event element to the event container, which might already be populated with events.
+ // If an event's segment will have row equal to `adjustRow`, then explicitly set its top coordinate to `adjustTop`.
+ // This hack is used to maintain continuity when user is manually resizing an event.
+ // Returns an array of DOM elements for the event.
+ function renderTempDayEvent(event, adjustRow, adjustTop) {
+
+ // actually render the event. `true` for appending element to container.
+ // Recieve the intermediate "segment" data structures.
+ var segments = _renderDayEvents(
+ [ event ],
+ true, // append event elements
+ false // don't set the heights of the rows
+ );
+
+ var elements = [];
+
+ // Adjust certain elements' top coordinates
+ segmentElementEach(segments, function(segment, element) {
+ if (segment.row === adjustRow) {
+ element.css('top', adjustTop);
+ }
+ elements.push(element[0]); // accumulate DOM nodes
+ });
+
+ return elements;
+ }
+
+
+ // Render events onto the calendar. Only responsible for the VISUAL aspect.
+ // Not responsible for attaching handlers or calling callbacks.
+ // Set `doAppend` to `true` for rendering elements without clearing the existing container.
+ // Set `doRowHeights` to allow setting the height of each row, to compensate for vertical event overflow.
+ function _renderDayEvents(events, doAppend, doRowHeights) {
+
+ // where the DOM nodes will eventually end up
+ var finalContainer = getDaySegmentContainer();
+
+ // the container where the initial HTML will be rendered.
+ // If `doAppend`==true, uses a temporary container.
+ var renderContainer = doAppend ? $("<div/>") : finalContainer;
+
+ var segments = buildSegments(events);
+ var html;
+ var elements;
+
+ // calculate the desired `left` and `width` properties on each segment object
+ calculateHorizontals(segments);
+
+ // build the HTML string. relies on `left` property
+ html = buildHTML(segments);
+
+ // render the HTML. innerHTML is considerably faster than jQuery's .html()
+ renderContainer[0].innerHTML = html;
+
+ // retrieve the individual elements
+ elements = renderContainer.children();
+
+ // if we were appending, and thus using a temporary container,
+ // re-attach elements to the real container.
+ if (doAppend) {
+ finalContainer.append(elements);
+ }
+
+ // assigns each element to `segment.event`, after filtering them through user callbacks
+ resolveElements(segments, elements);
+
+ // Calculate the left and right padding+margin for each element.
+ // We need this for setting each element's desired outer width, because of the W3C box model.
+ // It's important we do this in a separate pass from acually setting the width on the DOM elements
+ // because alternating reading/writing dimensions causes reflow for every iteration.
+ segmentElementEach(segments, function(segment, element) {
+ segment.hsides = hsides(element, true); // include margins = `true`
+ });
+
+ // Set the width of each element
+ segmentElementEach(segments, function(segment, element) {
+ element.width(
+ Math.max(0, segment.outerWidth - segment.hsides)
+ );
+ });
+
+ // Grab each element's outerHeight (setVerticals uses this).
+ // To get an accurate reading, it's important to have each element's width explicitly set already.
+ segmentElementEach(segments, function(segment, element) {
+ segment.outerHeight = element.outerHeight(true); // include margins = `true`
+ });
+
+ // Set the top coordinate on each element (requires segment.outerHeight)
+ setVerticals(segments, doRowHeights);
+
+ return segments;
+ }
+
+
+ // Generate an array of "segments" for all events.
+ function buildSegments(events) {
+ var segments = [];
+ for (var i=0; i<events.length; i++) {
+ var eventSegments = buildSegmentsForEvent(events[i]);
+ segments.push.apply(segments, eventSegments); // append an array to an array
+ }
+ return segments;
+ }
+
+
+ // Generate an array of segments for a single event.
+ // A "segment" is the same data structure that View.rangeToSegments produces,
+ // with the addition of the `event` property being set to reference the original event.
+ function buildSegmentsForEvent(event) {
+ var startDate = event.start;
+ var endDate = exclEndDay(event);
+ var segments = rangeToSegments(startDate, endDate);
+ for (var i=0; i<segments.length; i++) {
+ segments[i].event = event;
+ }
+ return segments;
+ }
+
+
+ // Sets the `left` and `outerWidth` property of each segment.
+ // These values are the desired dimensions for the eventual DOM elements.
+ function calculateHorizontals(segments) {
+ var isRTL = opt('isRTL');
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+
+ // Determine functions used for calulating the elements left/right coordinates,
+ // depending on whether the view is RTL or not.
+ // NOTE:
+ // colLeft/colRight returns the coordinate butting up the edge of the cell.
+ // colContentLeft/colContentRight is indented a little bit from the edge.
+ var leftFunc = (isRTL ? segment.isEnd : segment.isStart) ? colContentLeft : colLeft;
+ var rightFunc = (isRTL ? segment.isStart : segment.isEnd) ? colContentRight : colRight;
+
+ var left = leftFunc(segment.leftCol);
+ var right = rightFunc(segment.rightCol);
+ segment.left = left;
+ segment.outerWidth = right - left;
+ }
+ }
+
+
+ // Build a concatenated HTML string for an array of segments
+ function buildHTML(segments) {
+ var html = '';
+ for (var i=0; i<segments.length; i++) {
+ html += buildHTMLForSegment(segments[i]);
+ }
+ return html;
+ }
+
+
+ // Build an HTML string for a single segment.
+ // Relies on the following properties:
+ // - `segment.event` (from `buildSegmentsForEvent`)
+ // - `segment.left` (from `calculateHorizontals`)
+ function buildHTMLForSegment(segment) {
+ var html = '';
+ var isRTL = opt('isRTL');
+ var event = segment.event;
+ var url = event.url;
+
+ // generate the list of CSS classNames
+ var classNames = [ 'fc-event', 'fc-event-skin', 'fc-event-hori' ];
+ if (isEventDraggable(event)) {
+ classNames.push('fc-event-draggable');
+ }
+ if (segment.isStart) {
+ classNames.push('fc-event-start');
+ }
+ if (segment.isEnd) {
+ classNames.push('fc-event-end');
+ }
+ // use the event's configured classNames
+ // guaranteed to be an array via `normalizeEvent`
+ classNames = classNames.concat(event.className);
+ if (event.source) {
+ // use the event's source's classNames, if specified
+ classNames = classNames.concat(event.source.className || []);
+ }
+
+ // generate a semicolon delimited CSS string for any of the "skin" properties
+ // of the event object (`backgroundColor`, `borderColor` and such)
+ var skinCss = getSkinCss(event, opt);
+
+ if (url) {
+ html += "<a href='" + htmlEscape(url) + "'";
+ }else{
+ html += "<div";
+ }
+ html +=
+ " class='" + classNames.join(' ') + "'" +
+ " style=" +
+ "'" +
+ "position:absolute;" +
+ "left:" + segment.left + "px;" +
+ skinCss +
+ "'" +
+ " tabindex='0'>" +
+ "<div class='fc-event-inner'>";
+ if (!event.allDay && segment.isStart) {
+ html +=
+ "<span class='fc-event-time'>" +
+ htmlEscape(
+ formatDates(event.start, event.end, opt('timeFormat'))
+ ) +
+ "</span>";
+ }
+ html +=
+ "<span class='fc-event-title'>" +
+ htmlEscape(event.title || '') +
+ "</span>" +
+ "</div>";
+ if (segment.isEnd && isEventResizable(event)) {
+ html +=
+ "<div class='ui-resizable-handle ui-resizable-" + (isRTL ? 'w' : 'e') + "'>" +
+ "&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
+ "</div>";
+ }
+ html += "</" + (url ? "a" : "div") + ">";
+
+ // TODO:
+ // When these elements are initially rendered, they will be briefly visibile on the screen,
+ // even though their widths/heights are not set.
+ // SOLUTION: initially set them as visibility:hidden ?
+
+ return html;
+ }
+
+
+ // Associate each segment (an object) with an element (a jQuery object),
+ // by setting each `segment.element`.
+ // Run each element through the `eventRender` filter, which allows developers to
+ // modify an existing element, supply a new one, or cancel rendering.
+ function resolveElements(segments, elements) {
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+ var event = segment.event;
+ var element = elements.eq(i);
+
+ // call the trigger with the original element
+ var triggerRes = trigger('eventRender', event, event, element);
+
+ if (triggerRes === false) {
+ // if `false`, remove the event from the DOM and don't assign it to `segment.event`
+ element.remove();
+ }
+ else {
+ if (triggerRes && triggerRes !== true) {
+ // the trigger returned a new element, but not `true` (which means keep the existing element)
+
+ // re-assign the important CSS dimension properties that were already assigned in `buildHTMLForSegment`
+ triggerRes = $(triggerRes)
+ .css({
+ position: 'absolute',
+ left: segment.left
+ });
+
+ element.replaceWith(triggerRes);
+ element = triggerRes;
+ }
+
+ segment.element = element;
+ }
+ }
+ }
+
+
+
+ /* Top-coordinate Methods
+ -------------------------------------------------------------------------------------------------*/
+
+
+ // Sets the "top" CSS property for each element.
+ // If `doRowHeights` is `true`, also sets each row's first cell to an explicit height,
+ // so that if elements vertically overflow, the cell expands vertically to compensate.
+ function setVerticals(segments, doRowHeights) {
+ var overflowLinks = {};
+ var rowContentHeights = calculateVerticals(segments, overflowLinks); // also sets segment.top
+ var rowContentElements = getRowContentElements(); // returns 1 inner div per row
+ var rowContentTops = [];
+
+ // Set each row's height by setting height of first inner div
+ if (doRowHeights) {
+ for (var i=0; i<rowContentElements.length; i++) {
+ rowContentElements[i].height(rowContentHeights[i]);
+ if (overflowLinks[i])
+ renderOverflowLinks(overflowLinks[i], rowContentElements[i]);
+ }
+ }
+
+ // Get each row's top, relative to the views's origin.
+ // Important to do this after setting each row's height.
+ for (var i=0; i<rowContentElements.length; i++) {
+ rowContentTops.push(
+ rowContentElements[i].position().top
+ );
+ }
+
+ // Set each segment element's CSS "top" property.
+ // Each segment object has a "top" property, which is relative to the row's top, but...
+ segmentElementEach(segments, function(segment, element) {
+ if (!segment.overflow) {
+ element.css(
+ 'top',
+ rowContentTops[segment.row] + segment.top // ...now, relative to views's origin
+ );
+ }
+ else {
+ element.hide();
+ }
+ });
+ }
+
+
+ // Calculate the "top" coordinate for each segment, relative to the "top" of the row.
+ // Also, return an array that contains the "content" height for each row
+ // (the height displaced by the vertically stacked events in the row).
+ // Requires segments to have their `outerHeight` property already set.
+ function calculateVerticals(segments, overflowLinks) {
+ var rowCnt = getRowCnt();
+ var colCnt = getColCnt();
+ var rowContentHeights = []; // content height for each row
+ var segmentRows = buildSegmentRows(segments); // an array of segment arrays, one for each row
+ var maxHeight = opt('maxHeight');
+ var top;
+
+ for (var rowI=0; rowI<rowCnt; rowI++) {
+ var segmentRow = segmentRows[rowI];
+
+ // an array of running total heights for each column.
+ // initialize with all zeros.
+ overflowLinks[rowI] = {};
+ var colHeights = [];
+ var overflows = [];
+ for (var colI=0; colI<colCnt; colI++) {
+ colHeights.push(0);
+ overflows.push(0);
+ }
+
+ // loop through every segment
+ for (var segmentI=0; segmentI<segmentRow.length; segmentI++) {
+ var segment = segmentRow[segmentI];
+
+ // find the segment's top coordinate by looking at the max height
+ // of all the columns the segment will be in.
+ top = arrayMax(
+ colHeights.slice(
+ segment.leftCol,
+ segment.rightCol + 1 // make exclusive for slice
+ )
+ );
+
+ if (maxHeight && top + segment.outerHeight > maxHeight) {
+ segment.overflow = true;
+ }
+ else {
+ segment.top = top;
+ top += segment.outerHeight;
+ }
+
+ // adjust the columns to account for the segment's height
+ for (var colI=segment.leftCol; colI<=segment.rightCol; colI++) {
+ if (overflows[colI]) {
+ segment.overflow = true;
+ }
+ if (segment.overflow) {
+ if (segment.isStart && !overflowLinks[rowI][colI])
+ overflowLinks[rowI][colI] = { seg:segment, top:top, date:cloneDate(segment.event.start, true), count:0 };
+ if (overflowLinks[rowI][colI])
+ overflowLinks[rowI][colI].count++;
+ overflows[colI]++;
+ }
+ else {
+ colHeights[colI] = top;
+ }
+ }
+ }
+
+ // the tallest column in the row should be the "content height"
+ rowContentHeights.push(arrayMax(colHeights));
+ }
+
+ return rowContentHeights;
+ }
+
+
+ // Build an array of segment arrays, each representing the segments that will
+ // be in a row of the grid, sorted by which event should be closest to the top.
+ function buildSegmentRows(segments) {
+ var rowCnt = getRowCnt();
+ var segmentRows = [];
+ var segmentI;
+ var segment;
+ var rowI;
+
+ // group segments by row
+ for (segmentI=0; segmentI<segments.length; segmentI++) {
+ segment = segments[segmentI];
+ rowI = segment.row;
+ if (segment.element) { // was rendered?
+ if (segmentRows[rowI]) {
+ // already other segments. append to array
+ segmentRows[rowI].push(segment);
+ }
+ else {
+ // first segment in row. create new array
+ segmentRows[rowI] = [ segment ];
+ }
+ }
+ }
+
+ // sort each row
+ for (rowI=0; rowI<rowCnt; rowI++) {
+ segmentRows[rowI] = sortSegmentRow(
+ segmentRows[rowI] || [] // guarantee an array, even if no segments
+ );
+ }
+
+ return segmentRows;
+ }
+
+
+ // Sort an array of segments according to which segment should appear closest to the top
+ function sortSegmentRow(segments) {
+ var sortedSegments = [];
+
+ // build the subrow array
+ var subrows = buildSegmentSubrows(segments);
+
+ // flatten it
+ for (var i=0; i<subrows.length; i++) {
+ sortedSegments.push.apply(sortedSegments, subrows[i]); // append an array to an array
+ }
+
+ return sortedSegments;
+ }
+
+
+ // Take an array of segments, which are all assumed to be in the same row,
+ // and sort into subrows.
+ function buildSegmentSubrows(segments) {
+
+ // Give preference to elements with certain criteria, so they have
+ // a chance to be closer to the top.
+ segments.sort(compareDaySegments);
+
+ var subrows = [];
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+
+ // loop through subrows, starting with the topmost, until the segment
+ // doesn't collide with other segments.
+ for (var j=0; j<subrows.length; j++) {
+ if (!isDaySegmentCollision(segment, subrows[j])) {
+ break;
+ }
+ }
+ // `j` now holds the desired subrow index
+ if (subrows[j]) {
+ subrows[j].push(segment);
+ }
+ else {
+ subrows[j] = [ segment ];
+ }
+ }
+
+ return subrows;
+ }
+
+
+ // Return an array of jQuery objects for the placeholder content containers of each row.
+ // The content containers don't actually contain anything, but their dimensions should match
+ // the events that are overlaid on top.
+ function getRowContentElements() {
+ var i;
+ var rowCnt = getRowCnt();
+ var rowDivs = [];
+ for (i=0; i<rowCnt; i++) {
+ rowDivs[i] = allDayRow(i)
+ .find('div.fc-day-content > div');
+ }
+ return rowDivs;
+ }
+
+
+ function renderOverflowLinks(overflowLinks, rowDiv) {
+ var container = getDaySegmentContainer();
+ var colCnt = getColCnt();
+ var element, triggerRes, link;
+ for (var j=0; j<colCnt; j++) {
+ if ((link = overflowLinks[j])) {
+ if (link.count > 1) {
+ element = $('<a>').addClass('fc-more-link').html('+'+link.count).appendTo(container);
+ element[0].style.position = 'absolute';
+ element[0].style.left = link.seg.left + 'px';
+ element[0].style.top = (link.top + rowDiv[0].offsetTop) + 'px';
+ triggerRes = trigger('overflowRender', link, { count:link.count, date:link.date }, element);
+ if (triggerRes === false)
+ element.remove();
+ }
+ else {
+ link.seg.top = link.top;
+ link.seg.overflow = false;
+ }
+ }
+ }
+ }
+
+
+ /* Mouse Handlers
+ ---------------------------------------------------------------------------------------------------*/
+ // TODO: better documentation!
+
+
+ function attachHandlers(segments, modifiedEventId) {
+ var segmentContainer = getDaySegmentContainer();
+
+ segmentElementEach(segments, function(segment, element, i) {
+ var event = segment.event;
+ if (event._id === modifiedEventId) {
+ bindDaySeg(event, element, segment);
+ }else{
+ element[0]._fci = i; // for lazySegBind
+ }
+ });
+
+ lazySegBind(segmentContainer, segments, bindDaySeg);
+ }
+
+
+ function bindDaySeg(event, eventElement, segment) {
+
+ if (isEventDraggable(event)) {
+ t.draggableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
+ }
+
+ if (
+ segment.isEnd && // only allow resizing on the final segment for an event
+ isEventResizable(event)
+ ) {
+ t.resizableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
+ }
+
+ // attach all other handlers.
+ // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
+ eventElementHandlers(event, eventElement);
+ }
+
+
+ function draggableDayEvent(event, eventElement) {
+ var hoverListener = getHoverListener();
+ var dayDelta;
+ eventElement.draggable({
+ delay: 50,
+ opacity: opt('dragOpacity'),
+ revertDuration: opt('dragRevertDuration'),
+ start: function(ev, ui) {
+ trigger('eventDragStart', eventElement, event, ev, ui);
+ hideEvents(event, eventElement);
+ hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
+ eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
+ clearOverlays();
+ if (cell) {
+ var origDate = cellToDate(origCell);
+ var date = cellToDate(cell);
+ dayDelta = dayDiff(date, origDate);
+ renderDayOverlay(
+ addDays(cloneDate(event.start), dayDelta),
+ addDays(exclEndDay(event), dayDelta)
+ );
+ }else{
+ dayDelta = 0;
+ }
+ }, ev, 'drag');
+ },
+ stop: function(ev, ui) {
+ hoverListener.stop();
+ clearOverlays();
+ trigger('eventDragStop', eventElement, event, ev, ui);
+ if (dayDelta) {
+ eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
+ }else{
+ eventElement.css('filter', ''); // clear IE opacity side-effects
+ showEvents(event, eventElement);
+ }
+ }
+ });
+ }
+
+
+ function resizableDayEvent(event, element, segment) {
+ var isRTL = opt('isRTL');
+ var direction = isRTL ? 'w' : 'e';
+ var handle = element.find('.ui-resizable-' + direction); // TODO: stop using this class because we aren't using jqui for this
+ var isResizing = false;
+
+ // TODO: look into using jquery-ui mouse widget for this stuff
+ disableTextSelection(element); // prevent native <a> selection for IE
+ element
+ .mousedown(function(ev) { // prevent native <a> selection for others
+ ev.preventDefault();
+ })
+ .click(function(ev) {
+ if (isResizing) {
+ ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
+ ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
+ // (eventElementHandlers needs to be bound after resizableDayEvent)
+ }
+ });
+
+ handle.mousedown(function(ev) {
+ if (ev.which != 1) {
+ return; // needs to be left mouse button
+ }
+ isResizing = true;
+ var hoverListener = getHoverListener();
+ var rowCnt = getRowCnt();
+ var colCnt = getColCnt();
+ var elementTop = element.css('top');
+ var dayDelta;
+ var helpers;
+ var eventCopy = $.extend({}, event);
+ var minCellOffset = dayOffsetToCellOffset( dateToDayOffset(event.start) );
+ clearSelection();
+ $('body')
+ .css('cursor', direction + '-resize')
+ .one('mouseup', mouseup);
+ trigger('eventResizeStart', this, event, ev);
+ hoverListener.start(function(cell, origCell) {
+ if (cell) {
+
+ var origCellOffset = cellToCellOffset(origCell);
+ var cellOffset = cellToCellOffset(cell);
+
+ // don't let resizing move earlier than start date cell
+ cellOffset = Math.max(cellOffset, minCellOffset);
+
+ dayDelta =
+ cellOffsetToDayOffset(cellOffset) -
+ cellOffsetToDayOffset(origCellOffset);
+
+ if (dayDelta) {
+ eventCopy.end = addDays(eventEnd(event), dayDelta, true);
+ var oldHelpers = helpers;
+
+ helpers = renderTempDayEvent(eventCopy, segment.row, elementTop);
+ helpers = $(helpers); // turn array into a jQuery object
+
+ helpers.find('*').css('cursor', direction + '-resize');
+ if (oldHelpers) {
+ oldHelpers.remove();
+ }
+
+ hideEvents(event);
+ }
+ else {
+ if (helpers) {
+ showEvents(event);
+ helpers.remove();
+ helpers = null;
+ }
+ }
+ clearOverlays();
+ renderDayOverlay( // coordinate grid already rebuilt with hoverListener.start()
+ event.start,
+ addDays( exclEndDay(event), dayDelta )
+ // TODO: instead of calling renderDayOverlay() with dates,
+ // call _renderDayOverlay (or whatever) with cell offsets.
+ );
+ }
+ }, ev);
+
+ function mouseup(ev) {
+ trigger('eventResizeStop', this, event, ev);
+ $('body').css('cursor', '');
+ hoverListener.stop();
+ clearOverlays();
+ if (dayDelta) {
+ eventResize(this, event, dayDelta, 0, ev);
+ // event redraw will clear helpers
+ }
+ // otherwise, the drag handler already restored the old events
+
+ setTimeout(function() { // make this happen after the element's click event
+ isResizing = false;
+ },0);
+ }
+ });
+ }
+
+
+}
+
+
+
+/* Generalized Segment Utilities
+-------------------------------------------------------------------------------------------------*/
+
+
+function isDaySegmentCollision(segment, otherSegments) {
+ for (var i=0; i<otherSegments.length; i++) {
+ var otherSegment = otherSegments[i];
+ if (
+ otherSegment.leftCol <= segment.rightCol &&
+ otherSegment.rightCol >= segment.leftCol
+ ) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+function segmentElementEach(segments, callback) { // TODO: use in AgendaView?
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+ var element = segment.element;
+ if (element) {
+ callback(segment, element, i);
+ }
+ }
+}
+
+
+// A cmp function for determining which segments should appear higher up
+function compareDaySegments(a, b) {
+ return (b.rightCol - b.leftCol) - (a.rightCol - a.leftCol) || // put wider events first
+ b.event.allDay - a.event.allDay || // if tie, put all-day events first (booleans cast to 0/1)
+ a.event.start - b.event.start || // if a tie, sort by event start date
+ (a.event.title || '').localeCompare(b.event.title) // if a tie, sort by event title
+}
+
+
+;;
+
+//BUG: unselect needs to be triggered when events are dragged+dropped
+
+function SelectionManager() {
+ var t = this;
+
+
+ // exports
+ t.select = select;
+ t.unselect = unselect;
+ t.reportSelection = reportSelection;
+ t.daySelectionMousedown = daySelectionMousedown;
+
+
+ // imports
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var defaultSelectionEnd = t.defaultSelectionEnd;
+ var renderSelection = t.renderSelection;
+ var clearSelection = t.clearSelection;
+
+
+ // locals
+ var selected = false;
+
+
+
+ // unselectAuto
+ if (opt('selectable') && opt('unselectAuto')) {
+ $(document).mousedown(function(ev) {
+ var ignore = opt('unselectCancel');
+ if (ignore) {
+ if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
+ return;
+ }
+ }
+ unselect(ev);
+ });
+ }
+
+
+ function select(startDate, endDate, allDay) {
+ unselect();
+ if (!endDate) {
+ endDate = defaultSelectionEnd(startDate, allDay);
+ }
+ renderSelection(startDate, endDate, allDay);
+ reportSelection(startDate, endDate, allDay);
+ }
+
+
+ function unselect(ev) {
+ if (selected) {
+ selected = false;
+ clearSelection();
+ trigger('unselect', null, ev);
+ }
+ }
+
+
+ function reportSelection(startDate, endDate, allDay, ev) {
+ selected = true;
+ trigger('select', null, startDate, endDate, allDay, ev);
+ }
+
+
+ function daySelectionMousedown(ev) { // not really a generic manager method, oh well
+ var cellToDate = t.cellToDate;
+ var getIsCellAllDay = t.getIsCellAllDay;
+ var hoverListener = t.getHoverListener();
+ var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
+ if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
+ unselect(ev);
+ var _mousedownElement = this;
+ var dates;
+ hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell
+ clearSelection();
+ if (cell && getIsCellAllDay(cell)) {
+ dates = [ cellToDate(origCell), cellToDate(cell) ].sort(dateCompare);
+ renderSelection(dates[0], dates[1], true);
+ }else{
+ dates = null;
+ }
+ }, ev);
+ $(document).one('mouseup', function(ev) {
+ hoverListener.stop();
+ if (dates) {
+ if (+dates[0] == +dates[1]) {
+ reportDayClick(dates[0], true, ev);
+ }
+ reportSelection(dates[0], dates[1], true, ev);
+ }
+ });
+ }
+ }
+
+
+}
+
+;;
+
+function OverlayManager() {
+ var t = this;
+
+
+ // exports
+ t.renderOverlay = renderOverlay;
+ t.clearOverlays = clearOverlays;
+
+
+ // locals
+ var usedOverlays = [];
+ var unusedOverlays = [];
+
+
+ function renderOverlay(rect, parent) {
+ var e = unusedOverlays.shift();
+ if (!e) {
+ e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
+ }
+ if (e[0].parentNode != parent[0]) {
+ e.appendTo(parent);
+ }
+ usedOverlays.push(e.css(rect).show());
+ return e;
+ }
+
+
+ function clearOverlays() {
+ var e;
+ while (e = usedOverlays.shift()) {
+ unusedOverlays.push(e.hide().unbind());
+ }
+ }
+
+
+}
+
+;;
+
+function CoordinateGrid(buildFunc) {
+
+ var t = this;
+ var rows;
+ var cols;
+
+
+ t.build = function() {
+ rows = [];
+ cols = [];
+ buildFunc(rows, cols);
+ };
+
+
+ t.cell = function(x, y) {
+ var rowCnt = rows.length;
+ var colCnt = cols.length;
+ var i, r=-1, c=-1;
+ for (i=0; i<rowCnt; i++) {
+ if (y >= rows[i][0] && y < rows[i][1]) {
+ r = i;
+ break;
+ }
+ }
+ for (i=0; i<colCnt; i++) {
+ if (x >= cols[i][0] && x < cols[i][1]) {
+ c = i;
+ break;
+ }
+ }
+ return (r>=0 && c>=0) ? { row:r, col:c } : null;
+ };
+
+
+ t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
+ var origin = originElement.offset();
+ return {
+ top: rows[row0][0] - origin.top,
+ left: cols[col0][0] - origin.left,
+ width: cols[col1][1] - cols[col0][0],
+ height: rows[row1][1] - rows[row0][0]
+ };
+ };
+
+}
+
+;;
+
+function HoverListener(coordinateGrid) {
+
+
+ var t = this;
+ var bindType;
+ var change;
+ var firstCell;
+ var cell;
+
+
+ t.start = function(_change, ev, _bindType) {
+ change = _change;
+ firstCell = cell = null;
+ coordinateGrid.build();
+ mouse(ev);
+ bindType = _bindType || 'mousemove';
+ $(document).bind(bindType, mouse);
+ };
+
+
+ function mouse(ev) {
+ _fixUIEvent(ev); // see below
+ var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
+ if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
+ if (newCell) {
+ if (!firstCell) {
+ firstCell = newCell;
+ }
+ change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
+ }else{
+ change(newCell, firstCell);
+ }
+ cell = newCell;
+ }
+ }
+
+
+ t.stop = function() {
+ $(document).unbind(bindType, mouse);
+ return cell;
+ };
+
+
+}
+
+
+
+// this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1)
+// upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem
+// but keep this in here for 1.8.16 users
+// and maybe remove it down the line
+
+function _fixUIEvent(event) { // for issue 1168
+ if (event.pageX === undefined) {
+ event.pageX = event.originalEvent.pageX;
+ event.pageY = event.originalEvent.pageY;
+ }
+}
+;;
+
+function HorizontalPositionCache(getElement) {
+
+ var t = this,
+ elements = {},
+ lefts = {},
+ rights = {};
+
+ function e(i) {
+ return elements[i] = elements[i] || getElement(i);
+ }
+
+ t.left = function(i) {
+ return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
+ };
+
+ t.right = function(i) {
+ return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
+ };
+
+ t.clear = function() {
+ elements = {};
+ lefts = {};
+ rights = {};
+ };
+
+}
+
+;;
+
+})(jQuery); \ No newline at end of file

© 2014-2024 Faster IT GmbH | imprint | privacy policy