From dd9ee10bc0699e5dcc17c3ef2181dcda01d9a69b Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 12 May 2016 09:42:40 +0200 Subject: Move dav app to PSR-4 (#24527) * Move Application to correct namespace and PSR-4 it * Move dav app to PSR-4 --- apps/dav/appinfo/app.php | 2 +- apps/dav/appinfo/application.php | 170 --- apps/dav/appinfo/info.xml | 1 + apps/dav/appinfo/install.php | 2 +- apps/dav/appinfo/register_command.php | 2 +- apps/dav/appinfo/update.php | 2 +- apps/dav/command/createaddressbook.php | 77 -- apps/dav/command/createcalendar.php | 81 -- apps/dav/command/syncbirthdaycalendar.php | 85 -- apps/dav/command/syncsystemaddressbook.php | 65 - apps/dav/lib/AppInfo/Application.php | 170 +++ apps/dav/lib/CalDAV/BirthdayService.php | 226 ++++ apps/dav/lib/CalDAV/CalDavBackend.php | 1390 ++++++++++++++++++++ apps/dav/lib/CalDAV/Calendar.php | 171 +++ apps/dav/lib/CalDAV/CalendarHome.php | 106 ++ apps/dav/lib/CalDAV/CalendarRoot.php | 29 + apps/dav/lib/CalDAV/Schedule/IMipPlugin.php | 128 ++ apps/dav/lib/CardDAV/AddressBook.php | 182 +++ apps/dav/lib/CardDAV/AddressBookImpl.php | 224 ++++ apps/dav/lib/CardDAV/AddressBookRoot.php | 54 + apps/dav/lib/CardDAV/CardDavBackend.php | 987 ++++++++++++++ apps/dav/lib/CardDAV/ContactsManager.php | 65 + apps/dav/lib/CardDAV/Converter.php | 171 +++ apps/dav/lib/CardDAV/Plugin.php | 79 ++ apps/dav/lib/CardDAV/SyncJob.php | 40 + apps/dav/lib/CardDAV/SyncService.php | 284 ++++ apps/dav/lib/CardDAV/UserAddressBooks.php | 67 + apps/dav/lib/CardDAV/Xml/Groups.php | 45 + apps/dav/lib/Command/CreateAddressBook.php | 77 ++ apps/dav/lib/Command/CreateCalendar.php | 81 ++ apps/dav/lib/Command/SyncBirthdayCalendar.php | 85 ++ apps/dav/lib/Command/SyncSystemAddressBook.php | 65 + apps/dav/lib/Comments/CommentNode.php | 261 ++++ apps/dav/lib/Comments/CommentsPlugin.php | 255 ++++ apps/dav/lib/Comments/EntityCollection.php | 199 +++ apps/dav/lib/Comments/EntityTypeCollection.php | 125 ++ apps/dav/lib/Comments/RootCollection.php | 205 +++ apps/dav/lib/Connector/LegacyDAVACL.php | 85 ++ apps/dav/lib/Connector/PublicAuth.php | 122 ++ apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php | 90 ++ apps/dav/lib/Connector/Sabre/Auth.php | 229 ++++ .../Connector/Sabre/BlockLegacyClientPlugin.php | 80 ++ apps/dav/lib/Connector/Sabre/ChecksumList.php | 71 + .../Connector/Sabre/CommentPropertiesPlugin.php | 130 ++ .../lib/Connector/Sabre/CopyEtagHeaderPlugin.php | 57 + .../Connector/Sabre/CustomPropertiesBackend.php | 355 +++++ apps/dav/lib/Connector/Sabre/DavAclPlugin.php | 72 + apps/dav/lib/Connector/Sabre/Directory.php | 310 +++++ .../lib/Connector/Sabre/DummyGetResponsePlugin.php | 70 + .../Connector/Sabre/Exception/EntityTooLarge.php | 44 + .../lib/Connector/Sabre/Exception/FileLocked.php | 48 + .../lib/Connector/Sabre/Exception/Forbidden.php | 64 + .../lib/Connector/Sabre/Exception/InvalidPath.php | 77 ++ .../Sabre/Exception/UnsupportedMediaType.php | 44 + .../lib/Connector/Sabre/ExceptionLoggerPlugin.php | 111 ++ apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php | 156 +++ apps/dav/lib/Connector/Sabre/File.php | 567 ++++++++ apps/dav/lib/Connector/Sabre/FilesPlugin.php | 398 ++++++ apps/dav/lib/Connector/Sabre/FilesReportPlugin.php | 333 +++++ apps/dav/lib/Connector/Sabre/LockPlugin.php | 84 ++ apps/dav/lib/Connector/Sabre/MaintenancePlugin.php | 92 ++ apps/dav/lib/Connector/Sabre/Node.php | 348 +++++ apps/dav/lib/Connector/Sabre/ObjectTree.php | 297 +++++ apps/dav/lib/Connector/Sabre/Principal.php | 234 ++++ apps/dav/lib/Connector/Sabre/QuotaPlugin.php | 146 ++ apps/dav/lib/Connector/Sabre/Server.php | 44 + apps/dav/lib/Connector/Sabre/ServerFactory.php | 184 +++ apps/dav/lib/Connector/Sabre/ShareTypeList.php | 87 ++ apps/dav/lib/Connector/Sabre/SharesPlugin.php | 177 +++ apps/dav/lib/Connector/Sabre/TagList.php | 120 ++ apps/dav/lib/Connector/Sabre/TagsPlugin.php | 293 +++++ apps/dav/lib/DAV/GroupPrincipalBackend.php | 200 +++ apps/dav/lib/DAV/Sharing/Backend.php | 206 +++ apps/dav/lib/DAV/Sharing/IShareable.php | 74 ++ apps/dav/lib/DAV/Sharing/Plugin.php | 200 +++ apps/dav/lib/DAV/Sharing/Xml/Invite.php | 168 +++ apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php | 84 ++ apps/dav/lib/DAV/SystemPrincipalBackend.php | 179 +++ apps/dav/lib/Files/BrowserErrorPagePlugin.php | 116 ++ apps/dav/lib/Files/CustomPropertiesBackend.php | 267 ++++ apps/dav/lib/Files/FilesHome.php | 103 ++ apps/dav/lib/Files/RootCollection.php | 46 + .../lib/Files/Sharing/PublicLinkCheckPlugin.php | 63 + apps/dav/lib/HookManager.php | 126 ++ apps/dav/lib/RootCollection.php | 114 ++ apps/dav/lib/Server.php | 164 +++ apps/dav/lib/SystemTag/SystemTagMappingNode.php | 114 ++ apps/dav/lib/SystemTag/SystemTagNode.php | 162 +++ apps/dav/lib/SystemTag/SystemTagPlugin.php | 271 ++++ .../dav/lib/SystemTag/SystemTagsByIdCollection.php | 176 +++ .../SystemTagsObjectMappingCollection.php | 201 +++ .../SystemTag/SystemTagsObjectTypeCollection.php | 183 +++ .../SystemTag/SystemTagsRelationsCollection.php | 74 ++ apps/dav/lib/Upload/AssemblyStream.php | 234 ++++ apps/dav/lib/Upload/FutureFile.php | 103 ++ apps/dav/lib/Upload/RootCollection.php | 23 + apps/dav/lib/Upload/UploadFolder.php | 61 + apps/dav/lib/Upload/UploadHome.php | 74 ++ apps/dav/lib/caldav/birthdayservice.php | 226 ---- apps/dav/lib/caldav/caldavbackend.php | 1390 -------------------- apps/dav/lib/caldav/calendar.php | 171 --- apps/dav/lib/caldav/calendarhome.php | 106 -- apps/dav/lib/caldav/calendarroot.php | 29 - apps/dav/lib/caldav/schedule/imipplugin.php | 128 -- apps/dav/lib/carddav/addressbook.php | 182 --- apps/dav/lib/carddav/addressbookimpl.php | 224 ---- apps/dav/lib/carddav/addressbookroot.php | 54 - apps/dav/lib/carddav/carddavbackend.php | 987 -------------- apps/dav/lib/carddav/contactsmanager.php | 65 - apps/dav/lib/carddav/converter.php | 171 --- apps/dav/lib/carddav/plugin.php | 79 -- apps/dav/lib/carddav/syncjob.php | 40 - apps/dav/lib/carddav/syncservice.php | 284 ---- apps/dav/lib/carddav/useraddressbooks.php | 67 - apps/dav/lib/carddav/xml/groups.php | 45 - apps/dav/lib/comments/commentnode.php | 261 ---- apps/dav/lib/comments/commentsplugin.php | 255 ---- apps/dav/lib/comments/entitycollection.php | 199 --- apps/dav/lib/comments/entitytypecollection.php | 125 -- apps/dav/lib/comments/rootcollection.php | 205 --- apps/dav/lib/connector/legacydavacl.php | 85 -- apps/dav/lib/connector/publicauth.php | 122 -- apps/dav/lib/connector/sabre/appenabledplugin.php | 90 -- apps/dav/lib/connector/sabre/auth.php | 229 ---- .../connector/sabre/blocklegacyclientplugin.php | 80 -- apps/dav/lib/connector/sabre/checksumlist.php | 71 - .../connector/sabre/commentpropertiesplugin.php | 130 -- .../lib/connector/sabre/copyetagheaderplugin.php | 57 - .../connector/sabre/custompropertiesbackend.php | 355 ----- apps/dav/lib/connector/sabre/davaclplugin.php | 72 - apps/dav/lib/connector/sabre/directory.php | 310 ----- .../lib/connector/sabre/dummygetresponseplugin.php | 70 - .../connector/sabre/exception/entitytoolarge.php | 44 - .../lib/connector/sabre/exception/filelocked.php | 48 - .../lib/connector/sabre/exception/forbidden.php | 64 - .../lib/connector/sabre/exception/invalidpath.php | 77 -- .../sabre/exception/unsupportedmediatype.php | 44 - .../lib/connector/sabre/exceptionloggerplugin.php | 111 -- apps/dav/lib/connector/sabre/fakelockerplugin.php | 156 --- apps/dav/lib/connector/sabre/file.php | 567 -------- apps/dav/lib/connector/sabre/filesplugin.php | 398 ------ apps/dav/lib/connector/sabre/filesreportplugin.php | 333 ----- apps/dav/lib/connector/sabre/lockplugin.php | 84 -- apps/dav/lib/connector/sabre/maintenanceplugin.php | 92 -- apps/dav/lib/connector/sabre/node.php | 348 ----- apps/dav/lib/connector/sabre/objecttree.php | 297 ----- apps/dav/lib/connector/sabre/principal.php | 234 ---- apps/dav/lib/connector/sabre/quotaplugin.php | 146 -- apps/dav/lib/connector/sabre/server.php | 44 - apps/dav/lib/connector/sabre/serverfactory.php | 184 --- apps/dav/lib/connector/sabre/sharesplugin.php | 177 --- apps/dav/lib/connector/sabre/sharetypelist.php | 87 -- apps/dav/lib/connector/sabre/taglist.php | 120 -- apps/dav/lib/connector/sabre/tagsplugin.php | 293 ----- apps/dav/lib/dav/groupprincipalbackend.php | 200 --- apps/dav/lib/dav/sharing/backend.php | 206 --- apps/dav/lib/dav/sharing/ishareable.php | 74 -- apps/dav/lib/dav/sharing/plugin.php | 200 --- apps/dav/lib/dav/sharing/xml/invite.php | 168 --- apps/dav/lib/dav/sharing/xml/sharerequest.php | 84 -- apps/dav/lib/dav/systemprincipalbackend.php | 179 --- apps/dav/lib/files/browsererrorpageplugin.php | 116 -- apps/dav/lib/files/custompropertiesbackend.php | 267 ---- apps/dav/lib/files/fileshome.php | 103 -- apps/dav/lib/files/rootcollection.php | 46 - .../lib/files/sharing/publiclinkcheckplugin.php | 63 - apps/dav/lib/hookmanager.php | 126 -- apps/dav/lib/rootcollection.php | 114 -- apps/dav/lib/server.php | 164 --- apps/dav/lib/systemtag/systemtagmappingnode.php | 114 -- apps/dav/lib/systemtag/systemtagnode.php | 162 --- apps/dav/lib/systemtag/systemtagplugin.php | 271 ---- .../dav/lib/systemtag/systemtagsbyidcollection.php | 176 --- .../systemtagsobjectmappingcollection.php | 201 --- .../systemtag/systemtagsobjecttypecollection.php | 183 --- .../systemtag/systemtagsrelationscollection.php | 74 -- apps/dav/lib/upload/assemblystream.php | 234 ---- apps/dav/lib/upload/futurefile.php | 103 -- apps/dav/lib/upload/rootcollection.php | 23 - apps/dav/lib/upload/uploadfolder.php | 61 - apps/dav/lib/upload/uploadhome.php | 74 -- apps/dav/tests/unit/appinfo/applicationtest.php | 2 +- 182 files changed, 15182 insertions(+), 15181 deletions(-) delete mode 100644 apps/dav/appinfo/application.php delete mode 100644 apps/dav/command/createaddressbook.php delete mode 100644 apps/dav/command/createcalendar.php delete mode 100644 apps/dav/command/syncbirthdaycalendar.php delete mode 100644 apps/dav/command/syncsystemaddressbook.php create mode 100644 apps/dav/lib/AppInfo/Application.php create mode 100644 apps/dav/lib/CalDAV/BirthdayService.php create mode 100644 apps/dav/lib/CalDAV/CalDavBackend.php create mode 100644 apps/dav/lib/CalDAV/Calendar.php create mode 100644 apps/dav/lib/CalDAV/CalendarHome.php create mode 100644 apps/dav/lib/CalDAV/CalendarRoot.php create mode 100644 apps/dav/lib/CalDAV/Schedule/IMipPlugin.php create mode 100644 apps/dav/lib/CardDAV/AddressBook.php create mode 100644 apps/dav/lib/CardDAV/AddressBookImpl.php create mode 100644 apps/dav/lib/CardDAV/AddressBookRoot.php create mode 100644 apps/dav/lib/CardDAV/CardDavBackend.php create mode 100644 apps/dav/lib/CardDAV/ContactsManager.php create mode 100644 apps/dav/lib/CardDAV/Converter.php create mode 100644 apps/dav/lib/CardDAV/Plugin.php create mode 100644 apps/dav/lib/CardDAV/SyncJob.php create mode 100644 apps/dav/lib/CardDAV/SyncService.php create mode 100644 apps/dav/lib/CardDAV/UserAddressBooks.php create mode 100644 apps/dav/lib/CardDAV/Xml/Groups.php create mode 100644 apps/dav/lib/Command/CreateAddressBook.php create mode 100644 apps/dav/lib/Command/CreateCalendar.php create mode 100644 apps/dav/lib/Command/SyncBirthdayCalendar.php create mode 100644 apps/dav/lib/Command/SyncSystemAddressBook.php create mode 100644 apps/dav/lib/Comments/CommentNode.php create mode 100644 apps/dav/lib/Comments/CommentsPlugin.php create mode 100644 apps/dav/lib/Comments/EntityCollection.php create mode 100644 apps/dav/lib/Comments/EntityTypeCollection.php create mode 100644 apps/dav/lib/Comments/RootCollection.php create mode 100644 apps/dav/lib/Connector/LegacyDAVACL.php create mode 100644 apps/dav/lib/Connector/PublicAuth.php create mode 100644 apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/Auth.php create mode 100644 apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/ChecksumList.php create mode 100644 apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/CustomPropertiesBackend.php create mode 100644 apps/dav/lib/Connector/Sabre/DavAclPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/Directory.php create mode 100644 apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php create mode 100644 apps/dav/lib/Connector/Sabre/Exception/FileLocked.php create mode 100644 apps/dav/lib/Connector/Sabre/Exception/Forbidden.php create mode 100644 apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php create mode 100644 apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php create mode 100644 apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/File.php create mode 100644 apps/dav/lib/Connector/Sabre/FilesPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/FilesReportPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/LockPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/MaintenancePlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/Node.php create mode 100644 apps/dav/lib/Connector/Sabre/ObjectTree.php create mode 100644 apps/dav/lib/Connector/Sabre/Principal.php create mode 100644 apps/dav/lib/Connector/Sabre/QuotaPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/Server.php create mode 100644 apps/dav/lib/Connector/Sabre/ServerFactory.php create mode 100644 apps/dav/lib/Connector/Sabre/ShareTypeList.php create mode 100644 apps/dav/lib/Connector/Sabre/SharesPlugin.php create mode 100644 apps/dav/lib/Connector/Sabre/TagList.php create mode 100644 apps/dav/lib/Connector/Sabre/TagsPlugin.php create mode 100644 apps/dav/lib/DAV/GroupPrincipalBackend.php create mode 100644 apps/dav/lib/DAV/Sharing/Backend.php create mode 100644 apps/dav/lib/DAV/Sharing/IShareable.php create mode 100644 apps/dav/lib/DAV/Sharing/Plugin.php create mode 100644 apps/dav/lib/DAV/Sharing/Xml/Invite.php create mode 100644 apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php create mode 100644 apps/dav/lib/DAV/SystemPrincipalBackend.php create mode 100644 apps/dav/lib/Files/BrowserErrorPagePlugin.php create mode 100644 apps/dav/lib/Files/CustomPropertiesBackend.php create mode 100644 apps/dav/lib/Files/FilesHome.php create mode 100644 apps/dav/lib/Files/RootCollection.php create mode 100644 apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php create mode 100644 apps/dav/lib/HookManager.php create mode 100644 apps/dav/lib/RootCollection.php create mode 100644 apps/dav/lib/Server.php create mode 100644 apps/dav/lib/SystemTag/SystemTagMappingNode.php create mode 100644 apps/dav/lib/SystemTag/SystemTagNode.php create mode 100644 apps/dav/lib/SystemTag/SystemTagPlugin.php create mode 100644 apps/dav/lib/SystemTag/SystemTagsByIdCollection.php create mode 100644 apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php create mode 100644 apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php create mode 100644 apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php create mode 100644 apps/dav/lib/Upload/AssemblyStream.php create mode 100644 apps/dav/lib/Upload/FutureFile.php create mode 100644 apps/dav/lib/Upload/RootCollection.php create mode 100644 apps/dav/lib/Upload/UploadFolder.php create mode 100644 apps/dav/lib/Upload/UploadHome.php delete mode 100644 apps/dav/lib/caldav/birthdayservice.php delete mode 100644 apps/dav/lib/caldav/caldavbackend.php delete mode 100644 apps/dav/lib/caldav/calendar.php delete mode 100644 apps/dav/lib/caldav/calendarhome.php delete mode 100644 apps/dav/lib/caldav/calendarroot.php delete mode 100644 apps/dav/lib/caldav/schedule/imipplugin.php delete mode 100644 apps/dav/lib/carddav/addressbook.php delete mode 100644 apps/dav/lib/carddav/addressbookimpl.php delete mode 100644 apps/dav/lib/carddav/addressbookroot.php delete mode 100644 apps/dav/lib/carddav/carddavbackend.php delete mode 100644 apps/dav/lib/carddav/contactsmanager.php delete mode 100644 apps/dav/lib/carddav/converter.php delete mode 100644 apps/dav/lib/carddav/plugin.php delete mode 100644 apps/dav/lib/carddav/syncjob.php delete mode 100644 apps/dav/lib/carddav/syncservice.php delete mode 100644 apps/dav/lib/carddav/useraddressbooks.php delete mode 100644 apps/dav/lib/carddav/xml/groups.php delete mode 100644 apps/dav/lib/comments/commentnode.php delete mode 100644 apps/dav/lib/comments/commentsplugin.php delete mode 100644 apps/dav/lib/comments/entitycollection.php delete mode 100644 apps/dav/lib/comments/entitytypecollection.php delete mode 100644 apps/dav/lib/comments/rootcollection.php delete mode 100644 apps/dav/lib/connector/legacydavacl.php delete mode 100644 apps/dav/lib/connector/publicauth.php delete mode 100644 apps/dav/lib/connector/sabre/appenabledplugin.php delete mode 100644 apps/dav/lib/connector/sabre/auth.php delete mode 100644 apps/dav/lib/connector/sabre/blocklegacyclientplugin.php delete mode 100644 apps/dav/lib/connector/sabre/checksumlist.php delete mode 100644 apps/dav/lib/connector/sabre/commentpropertiesplugin.php delete mode 100644 apps/dav/lib/connector/sabre/copyetagheaderplugin.php delete mode 100644 apps/dav/lib/connector/sabre/custompropertiesbackend.php delete mode 100644 apps/dav/lib/connector/sabre/davaclplugin.php delete mode 100644 apps/dav/lib/connector/sabre/directory.php delete mode 100644 apps/dav/lib/connector/sabre/dummygetresponseplugin.php delete mode 100644 apps/dav/lib/connector/sabre/exception/entitytoolarge.php delete mode 100644 apps/dav/lib/connector/sabre/exception/filelocked.php delete mode 100644 apps/dav/lib/connector/sabre/exception/forbidden.php delete mode 100644 apps/dav/lib/connector/sabre/exception/invalidpath.php delete mode 100644 apps/dav/lib/connector/sabre/exception/unsupportedmediatype.php delete mode 100644 apps/dav/lib/connector/sabre/exceptionloggerplugin.php delete mode 100644 apps/dav/lib/connector/sabre/fakelockerplugin.php delete mode 100644 apps/dav/lib/connector/sabre/file.php delete mode 100644 apps/dav/lib/connector/sabre/filesplugin.php delete mode 100644 apps/dav/lib/connector/sabre/filesreportplugin.php delete mode 100644 apps/dav/lib/connector/sabre/lockplugin.php delete mode 100644 apps/dav/lib/connector/sabre/maintenanceplugin.php delete mode 100644 apps/dav/lib/connector/sabre/node.php delete mode 100644 apps/dav/lib/connector/sabre/objecttree.php delete mode 100644 apps/dav/lib/connector/sabre/principal.php delete mode 100644 apps/dav/lib/connector/sabre/quotaplugin.php delete mode 100644 apps/dav/lib/connector/sabre/server.php delete mode 100644 apps/dav/lib/connector/sabre/serverfactory.php delete mode 100644 apps/dav/lib/connector/sabre/sharesplugin.php delete mode 100644 apps/dav/lib/connector/sabre/sharetypelist.php delete mode 100644 apps/dav/lib/connector/sabre/taglist.php delete mode 100644 apps/dav/lib/connector/sabre/tagsplugin.php delete mode 100644 apps/dav/lib/dav/groupprincipalbackend.php delete mode 100644 apps/dav/lib/dav/sharing/backend.php delete mode 100644 apps/dav/lib/dav/sharing/ishareable.php delete mode 100644 apps/dav/lib/dav/sharing/plugin.php delete mode 100644 apps/dav/lib/dav/sharing/xml/invite.php delete mode 100644 apps/dav/lib/dav/sharing/xml/sharerequest.php delete mode 100644 apps/dav/lib/dav/systemprincipalbackend.php delete mode 100644 apps/dav/lib/files/browsererrorpageplugin.php delete mode 100644 apps/dav/lib/files/custompropertiesbackend.php delete mode 100644 apps/dav/lib/files/fileshome.php delete mode 100644 apps/dav/lib/files/rootcollection.php delete mode 100644 apps/dav/lib/files/sharing/publiclinkcheckplugin.php delete mode 100644 apps/dav/lib/hookmanager.php delete mode 100644 apps/dav/lib/rootcollection.php delete mode 100644 apps/dav/lib/server.php delete mode 100644 apps/dav/lib/systemtag/systemtagmappingnode.php delete mode 100644 apps/dav/lib/systemtag/systemtagnode.php delete mode 100644 apps/dav/lib/systemtag/systemtagplugin.php delete mode 100644 apps/dav/lib/systemtag/systemtagsbyidcollection.php delete mode 100644 apps/dav/lib/systemtag/systemtagsobjectmappingcollection.php delete mode 100644 apps/dav/lib/systemtag/systemtagsobjecttypecollection.php delete mode 100644 apps/dav/lib/systemtag/systemtagsrelationscollection.php delete mode 100644 apps/dav/lib/upload/assemblystream.php delete mode 100644 apps/dav/lib/upload/futurefile.php delete mode 100644 apps/dav/lib/upload/rootcollection.php delete mode 100644 apps/dav/lib/upload/uploadfolder.php delete mode 100644 apps/dav/lib/upload/uploadhome.php (limited to 'apps') diff --git a/apps/dav/appinfo/app.php b/apps/dav/appinfo/app.php index d85a3583bf1..66b62f35346 100644 --- a/apps/dav/appinfo/app.php +++ b/apps/dav/appinfo/app.php @@ -20,7 +20,7 @@ * */ -use OCA\Dav\AppInfo\Application; +use OCA\DAV\AppInfo\Application; use Symfony\Component\EventDispatcher\GenericEvent; $app = new Application(); diff --git a/apps/dav/appinfo/application.php b/apps/dav/appinfo/application.php deleted file mode 100644 index 328f86c877f..00000000000 --- a/apps/dav/appinfo/application.php +++ /dev/null @@ -1,170 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\Dav\AppInfo; - -use OCA\DAV\CalDAV\BirthdayService; -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\CardDAV\ContactsManager; -use OCA\DAV\CardDAV\SyncJob; -use OCA\DAV\CardDAV\SyncService; -use OCA\DAV\Connector\Sabre\Principal; -use OCA\DAV\DAV\GroupPrincipalBackend; -use OCA\DAV\HookManager; -use \OCP\AppFramework\App; -use OCP\AppFramework\IAppContainer; -use OCP\Contacts\IManager; -use OCP\IUser; -use Symfony\Component\EventDispatcher\GenericEvent; - -class Application extends App { - - /** - * Application constructor. - * - * @param array $urlParams - */ - public function __construct (array $urlParams=array()) { - parent::__construct('dav', $urlParams); - - $container = $this->getContainer(); - $container->registerService('ContactsManager', function($c) { - /** @var IAppContainer $c */ - return new ContactsManager( - $c->query('CardDavBackend') - ); - }); - - $container->registerService('HookManager', function($c) { - /** @var IAppContainer $c */ - return new HookManager( - $c->getServer()->getUserManager(), - $c->query('SyncService'), - $c->query('CalDavBackend'), - $c->query('CardDavBackend') - ); - }); - - $container->registerService('SyncService', function($c) { - /** @var IAppContainer $c */ - return new SyncService( - $c->query('CardDavBackend'), - $c->getServer()->getUserManager(), - $c->getServer()->getLogger() - ); - }); - - $container->registerService('CardDavBackend', function($c) { - /** @var IAppContainer $c */ - $db = $c->getServer()->getDatabaseConnection(); - $dispatcher = $c->getServer()->getEventDispatcher(); - $principal = new Principal( - $c->getServer()->getUserManager(), - $c->getServer()->getGroupManager() - ); - return new CardDavBackend($db, $principal, $dispatcher); - }); - - $container->registerService('CalDavBackend', function($c) { - /** @var IAppContainer $c */ - $db = $c->getServer()->getDatabaseConnection(); - $principal = new Principal( - $c->getServer()->getUserManager(), - $c->getServer()->getGroupManager() - ); - return new CalDavBackend($db, $principal); - }); - - $container->registerService('BirthdayService', function($c) { - /** @var IAppContainer $c */ - $g = new GroupPrincipalBackend( - $c->getServer()->getGroupManager() - ); - return new BirthdayService( - $c->query('CalDavBackend'), - $c->query('CardDavBackend'), - $g - ); - }); - } - - /** - * @param IManager $contactsManager - * @param string $userID - */ - public function setupContactsProvider(IManager $contactsManager, $userID) { - /** @var ContactsManager $cm */ - $cm = $this->getContainer()->query('ContactsManager'); - $cm->setupContactsProvider($contactsManager, $userID); - } - - public function registerHooks() { - /** @var HookManager $hm */ - $hm = $this->getContainer()->query('HookManager'); - $hm->setup(); - - $listener = function($event) { - if ($event instanceof GenericEvent) { - /** @var BirthdayService $b */ - $b = $this->getContainer()->query('BirthdayService'); - $b->onCardChanged( - $event->getArgument('addressBookId'), - $event->getArgument('cardUri'), - $event->getArgument('cardData') - ); - } - }; - - $dispatcher = $this->getContainer()->getServer()->getEventDispatcher(); - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::createCard', $listener); - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $listener); - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', function($event) { - if ($event instanceof GenericEvent) { - /** @var BirthdayService $b */ - $b = $this->getContainer()->query('BirthdayService'); - $b->onCardDeleted( - $event->getArgument('addressBookId'), - $event->getArgument('cardUri') - ); - } - }); - } - - public function getSyncService() { - return $this->getContainer()->query('SyncService'); - } - - public function generateBirthdays() { - try { - /** @var BirthdayService $migration */ - $migration = $this->getContainer()->query('BirthdayService'); - $userManager = $this->getContainer()->getServer()->getUserManager(); - - $userManager->callForAllUsers(function($user) use($migration) { - /** @var IUser $user */ - $migration->syncUser($user->getUID()); - }); - } catch (\Exception $ex) { - $this->getContainer()->getServer()->getLogger()->logException($ex); - } - } -} diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index e2688e2f923..ca456b03089 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -10,6 +10,7 @@ + DAV appinfo/v1/publicwebdav.php diff --git a/apps/dav/appinfo/install.php b/apps/dav/appinfo/install.php index dbb23022b38..8ee55dbbd11 100644 --- a/apps/dav/appinfo/install.php +++ b/apps/dav/appinfo/install.php @@ -19,7 +19,7 @@ * */ -use OCA\Dav\AppInfo\Application; +use OCA\DAV\AppInfo\Application; $app = new Application(); $app->generateBirthdays(); diff --git a/apps/dav/appinfo/register_command.php b/apps/dav/appinfo/register_command.php index b3ab25a99e3..6010f594e9b 100644 --- a/apps/dav/appinfo/register_command.php +++ b/apps/dav/appinfo/register_command.php @@ -18,7 +18,7 @@ * along with this program. If not, see * */ -use OCA\Dav\AppInfo\Application; +use OCA\DAV\AppInfo\Application; use OCA\DAV\Command\CreateAddressBook; use OCA\DAV\Command\CreateCalendar; use OCA\DAV\Command\SyncBirthdayCalendar; diff --git a/apps/dav/appinfo/update.php b/apps/dav/appinfo/update.php index dbb23022b38..8ee55dbbd11 100644 --- a/apps/dav/appinfo/update.php +++ b/apps/dav/appinfo/update.php @@ -19,7 +19,7 @@ * */ -use OCA\Dav\AppInfo\Application; +use OCA\DAV\AppInfo\Application; $app = new Application(); $app->generateBirthdays(); diff --git a/apps/dav/command/createaddressbook.php b/apps/dav/command/createaddressbook.php deleted file mode 100644 index 48302a2b439..00000000000 --- a/apps/dav/command/createaddressbook.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Command; - -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\Connector\Sabre\Principal; -use OCP\IConfig; -use OCP\IDBConnection; -use OCP\IGroupManager; -use OCP\ILogger; -use OCP\IUserManager; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class CreateAddressBook extends Command { - - /** @var IUserManager */ - private $userManager; - - /** @var CardDavBackend */ - private $cardDavBackend; - - /** - * @param IUserManager $userManager - * @param CardDavBackend $cardDavBackend - */ - function __construct(IUserManager $userManager, - CardDavBackend $cardDavBackend - ) { - parent::__construct(); - $this->userManager = $userManager; - $this->cardDavBackend = $cardDavBackend; - } - - protected function configure() { - $this - ->setName('dav:create-addressbook') - ->setDescription('Create a dav addressbook') - ->addArgument('user', - InputArgument::REQUIRED, - 'User for whom the addressbook will be created') - ->addArgument('name', - InputArgument::REQUIRED, - 'Name of the addressbook'); - } - - protected function execute(InputInterface $input, OutputInterface $output) { - $user = $input->getArgument('user'); - if (!$this->userManager->userExists($user)) { - throw new \InvalidArgumentException("User <$user> in unknown."); - } - - $name = $input->getArgument('name'); - $this->cardDavBackend->createAddressBook("principals/users/$user", $name, []); - } -} diff --git a/apps/dav/command/createcalendar.php b/apps/dav/command/createcalendar.php deleted file mode 100644 index d7f82dd0e52..00000000000 --- a/apps/dav/command/createcalendar.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Command; - -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\Connector\Sabre\Principal; -use OCP\IDBConnection; -use OCP\IGroupManager; -use OCP\IUserManager; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class CreateCalendar extends Command { - - /** @var IUserManager */ - protected $userManager; - - /** @var IGroupManager $groupManager */ - private $groupManager; - - /** @var \OCP\IDBConnection */ - protected $dbConnection; - - /** - * @param IUserManager $userManager - * @param IDBConnection $dbConnection - */ - function __construct(IUserManager $userManager, IGroupManager $groupManager, IDBConnection $dbConnection) { - parent::__construct(); - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->dbConnection = $dbConnection; - } - - protected function configure() { - $this - ->setName('dav:create-calendar') - ->setDescription('Create a dav calendar') - ->addArgument('user', - InputArgument::REQUIRED, - 'User for whom the calendar will be created') - ->addArgument('name', - InputArgument::REQUIRED, - 'Name of the calendar'); - } - - protected function execute(InputInterface $input, OutputInterface $output) { - $user = $input->getArgument('user'); - if (!$this->userManager->userExists($user)) { - throw new \InvalidArgumentException("User <$user> in unknown."); - } - $principalBackend = new Principal( - $this->userManager, - $this->groupManager - ); - - $name = $input->getArgument('name'); - $caldav = new CalDavBackend($this->dbConnection, $principalBackend); - $caldav->createCalendar("principals/users/$user", $name, []); - } -} diff --git a/apps/dav/command/syncbirthdaycalendar.php b/apps/dav/command/syncbirthdaycalendar.php deleted file mode 100644 index 90a73a3eeb3..00000000000 --- a/apps/dav/command/syncbirthdaycalendar.php +++ /dev/null @@ -1,85 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Command; - -use OCA\DAV\CalDAV\BirthdayService; -use OCP\IUser; -use OCP\IUserManager; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\ProgressBar; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class SyncBirthdayCalendar extends Command { - - /** @var BirthdayService */ - private $birthdayService; - - /** @var IUserManager */ - private $userManager; - - /** - * @param IUserManager $userManager - * @param BirthdayService $birthdayService - */ - function __construct(IUserManager $userManager, BirthdayService $birthdayService) { - parent::__construct(); - $this->birthdayService = $birthdayService; - $this->userManager = $userManager; - } - - protected function configure() { - $this - ->setName('dav:sync-birthday-calendar') - ->setDescription('Synchronizes the birthday calendar') - ->addArgument('user', - InputArgument::OPTIONAL, - 'User for whom the birthday calendar will be synchronized'); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - */ - protected function execute(InputInterface $input, OutputInterface $output) { - $user = $input->getArgument('user'); - if (!is_null($user)) { - if (!$this->userManager->userExists($user)) { - throw new \InvalidArgumentException("User <$user> in unknown."); - } - $output->writeln("Start birthday calendar sync for $user"); - $this->birthdayService->syncUser($user); - return; - } - $output->writeln("Start birthday calendar sync for all users ..."); - $p = new ProgressBar($output); - $p->start(); - $this->userManager->callForAllUsers(function($user) use ($p) { - $p->advance(); - /** @var IUser $user */ - $this->birthdayService->syncUser($user->getUID()); - }); - - $p->finish(); - $output->writeln(''); - } -} diff --git a/apps/dav/command/syncsystemaddressbook.php b/apps/dav/command/syncsystemaddressbook.php deleted file mode 100644 index b62a42d7b90..00000000000 --- a/apps/dav/command/syncsystemaddressbook.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Command; - -use OCA\DAV\CardDAV\SyncService; -use OCP\IUserManager; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\ProgressBar; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class SyncSystemAddressBook extends Command { - - /** @var SyncService */ - private $syncService; - - /** - * @param SyncService $syncService - */ - function __construct(SyncService $syncService) { - parent::__construct(); - $this->syncService = $syncService; - } - - protected function configure() { - $this - ->setName('dav:sync-system-addressbook') - ->setDescription('Synchronizes users to the system addressbook'); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - */ - protected function execute(InputInterface $input, OutputInterface $output) { - $output->writeln('Syncing users ...'); - $progress = new ProgressBar($output); - $progress->start(); - $this->syncService->syncInstance(function() use ($progress) { - $progress->advance(); - }); - - $progress->finish(); - $output->writeln(''); - } -} diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php new file mode 100644 index 00000000000..d44ea22e2f3 --- /dev/null +++ b/apps/dav/lib/AppInfo/Application.php @@ -0,0 +1,170 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\AppInfo; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\ContactsManager; +use OCA\DAV\CardDAV\SyncJob; +use OCA\DAV\CardDAV\SyncService; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\GroupPrincipalBackend; +use OCA\DAV\HookManager; +use \OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; +use OCP\Contacts\IManager; +use OCP\IUser; +use Symfony\Component\EventDispatcher\GenericEvent; + +class Application extends App { + + /** + * Application constructor. + * + * @param array $urlParams + */ + public function __construct (array $urlParams=array()) { + parent::__construct('dav', $urlParams); + + $container = $this->getContainer(); + $container->registerService('ContactsManager', function($c) { + /** @var IAppContainer $c */ + return new ContactsManager( + $c->query('CardDavBackend') + ); + }); + + $container->registerService('HookManager', function($c) { + /** @var IAppContainer $c */ + return new HookManager( + $c->getServer()->getUserManager(), + $c->query('SyncService'), + $c->query('CalDavBackend'), + $c->query('CardDavBackend') + ); + }); + + $container->registerService('SyncService', function($c) { + /** @var IAppContainer $c */ + return new SyncService( + $c->query('CardDavBackend'), + $c->getServer()->getUserManager(), + $c->getServer()->getLogger() + ); + }); + + $container->registerService('CardDavBackend', function($c) { + /** @var IAppContainer $c */ + $db = $c->getServer()->getDatabaseConnection(); + $dispatcher = $c->getServer()->getEventDispatcher(); + $principal = new Principal( + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager() + ); + return new CardDavBackend($db, $principal, $dispatcher); + }); + + $container->registerService('CalDavBackend', function($c) { + /** @var IAppContainer $c */ + $db = $c->getServer()->getDatabaseConnection(); + $principal = new Principal( + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager() + ); + return new CalDavBackend($db, $principal); + }); + + $container->registerService('BirthdayService', function($c) { + /** @var IAppContainer $c */ + $g = new GroupPrincipalBackend( + $c->getServer()->getGroupManager() + ); + return new BirthdayService( + $c->query('CalDavBackend'), + $c->query('CardDavBackend'), + $g + ); + }); + } + + /** + * @param IManager $contactsManager + * @param string $userID + */ + public function setupContactsProvider(IManager $contactsManager, $userID) { + /** @var ContactsManager $cm */ + $cm = $this->getContainer()->query('ContactsManager'); + $cm->setupContactsProvider($contactsManager, $userID); + } + + public function registerHooks() { + /** @var HookManager $hm */ + $hm = $this->getContainer()->query('HookManager'); + $hm->setup(); + + $listener = function($event) { + if ($event instanceof GenericEvent) { + /** @var BirthdayService $b */ + $b = $this->getContainer()->query('BirthdayService'); + $b->onCardChanged( + $event->getArgument('addressBookId'), + $event->getArgument('cardUri'), + $event->getArgument('cardData') + ); + } + }; + + $dispatcher = $this->getContainer()->getServer()->getEventDispatcher(); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::createCard', $listener); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $listener); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', function($event) { + if ($event instanceof GenericEvent) { + /** @var BirthdayService $b */ + $b = $this->getContainer()->query('BirthdayService'); + $b->onCardDeleted( + $event->getArgument('addressBookId'), + $event->getArgument('cardUri') + ); + } + }); + } + + public function getSyncService() { + return $this->getContainer()->query('SyncService'); + } + + public function generateBirthdays() { + try { + /** @var BirthdayService $migration */ + $migration = $this->getContainer()->query('BirthdayService'); + $userManager = $this->getContainer()->getServer()->getUserManager(); + + $userManager->callForAllUsers(function($user) use($migration) { + /** @var IUser $user */ + $migration->syncUser($user->getUID()); + }); + } catch (\Exception $ex) { + $this->getContainer()->getServer()->getLogger()->logException($ex); + } + } +} diff --git a/apps/dav/lib/CalDAV/BirthdayService.php b/apps/dav/lib/CalDAV/BirthdayService.php new file mode 100644 index 00000000000..b74116f4083 --- /dev/null +++ b/apps/dav/lib/CalDAV/BirthdayService.php @@ -0,0 +1,226 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CalDAV; + +use Exception; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\DAV\GroupPrincipalBackend; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Reader; + +class BirthdayService { + + const BIRTHDAY_CALENDAR_URI = 'contact_birthdays'; + + /** @var GroupPrincipalBackend */ + private $principalBackend; + + /** + * BirthdayService constructor. + * + * @param CalDavBackend $calDavBackEnd + * @param CardDavBackend $cardDavBackEnd + * @param GroupPrincipalBackend $principalBackend + */ + public function __construct($calDavBackEnd, $cardDavBackEnd, $principalBackend) { + $this->calDavBackEnd = $calDavBackEnd; + $this->cardDavBackEnd = $cardDavBackEnd; + $this->principalBackend = $principalBackend; + } + + /** + * @param int $addressBookId + * @param string $cardUri + * @param string $cardData + */ + public function onCardChanged($addressBookId, $cardUri, $cardData) { + + $targetPrincipals = $this->getAllAffectedPrincipals($addressBookId); + + $book = $this->cardDavBackEnd->getAddressBookById($addressBookId); + $targetPrincipals[] = $book['principaluri']; + foreach ($targetPrincipals as $principalUri) { + $calendar = $this->ensureCalendarExists($principalUri); + $objectUri = $book['uri'] . '-' . $cardUri. '.ics'; + $calendarData = $this->buildBirthdayFromContact($cardData); + $existing = $this->calDavBackEnd->getCalendarObject($calendar['id'], $objectUri); + if (is_null($calendarData)) { + if (!is_null($existing)) { + $this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri); + } + } else { + if (is_null($existing)) { + $this->calDavBackEnd->createCalendarObject($calendar['id'], $objectUri, $calendarData->serialize()); + } else { + if ($this->birthdayEvenChanged($existing['calendardata'], $calendarData)) { + $this->calDavBackEnd->updateCalendarObject($calendar['id'], $objectUri, $calendarData->serialize()); + } + } + } + } + } + + /** + * @param int $addressBookId + * @param string $cardUri + */ + public function onCardDeleted($addressBookId, $cardUri) { + $targetPrincipals = $this->getAllAffectedPrincipals($addressBookId); + $book = $this->cardDavBackEnd->getAddressBookById($addressBookId); + $targetPrincipals[] = $book['principaluri']; + foreach ($targetPrincipals as $principalUri) { + $calendar = $this->ensureCalendarExists($principalUri); + $objectUri = $book['uri'] . '-' . $cardUri . '.ics'; + $this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri); + } + } + + /** + * @param string $principal + * @return array|null + * @throws \Sabre\DAV\Exception\BadRequest + */ + public function ensureCalendarExists($principal) { + $book = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI); + if (!is_null($book)) { + return $book; + } + $this->calDavBackEnd->createCalendar($principal, self::BIRTHDAY_CALENDAR_URI, [ + '{DAV:}displayname' => 'Contact birthdays', + '{http://apple.com/ns/ical/}calendar-color' => '#FFFFCA', + ]); + + return $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI); + } + + /** + * @param string $cardData + * @return null|VCalendar + */ + public function buildBirthdayFromContact($cardData) { + if (empty($cardData)) { + return null; + } + try { + $doc = Reader::read($cardData); + } catch (Exception $e) { + return null; + } + + if (!isset($doc->BDAY)) { + return null; + } + $birthday = $doc->BDAY; + if (!(string)$birthday) { + return null; + } + $title = str_replace('{name}', + strtr((string)$doc->FN, array('\,' => ',', '\;' => ';')), + '{name}' + ); + try { + $date = new \DateTime($birthday); + } catch (Exception $e) { + return null; + } + $vCal = new VCalendar(); + $vCal->VERSION = '2.0'; + $vEvent = $vCal->createComponent('VEVENT'); + $vEvent->add('DTSTART'); + $vEvent->DTSTART->setDateTime( + $date + ); + $vEvent->DTSTART['VALUE'] = 'DATE'; + $vEvent->add('DTEND'); + $date->add(new \DateInterval('P1D')); + $vEvent->DTEND->setDateTime( + $date + ); + $vEvent->DTEND['VALUE'] = 'DATE'; + $vEvent->{'UID'} = $doc->UID; + $vEvent->{'RRULE'} = 'FREQ=YEARLY'; + $vEvent->{'SUMMARY'} = $title . ' (*' . $date->format('Y') . ')'; + $vEvent->{'TRANSP'} = 'TRANSPARENT'; + $alarm = $vCal->createComponent('VALARM'); + $alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION'])); + $alarm->add($vCal->createProperty('ACTION', 'DISPLAY')); + $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'})); + $vEvent->add($alarm); + $vCal->add($vEvent); + return $vCal; + } + + /** + * @param string $user + */ + public function syncUser($user) { + $principal = 'principals/users/'.$user; + $this->ensureCalendarExists($principal); + $books = $this->cardDavBackEnd->getAddressBooksForUser($principal); + foreach($books as $book) { + $cards = $this->cardDavBackEnd->getCards($book['id']); + foreach($cards as $card) { + $this->onCardChanged($book['id'], $card['uri'], $card['carddata']); + } + } + } + + /** + * @param string $existingCalendarData + * @param VCalendar $newCalendarData + * @return bool + */ + public function birthdayEvenChanged($existingCalendarData, $newCalendarData) { + try { + $existingBirthday = Reader::read($existingCalendarData); + } catch (Exception $ex) { + return true; + } + if ($newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() || + $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue() + ) { + return true; + } + return false; + } + + /** + * @param integer $addressBookId + * @return mixed + */ + protected function getAllAffectedPrincipals($addressBookId) { + $targetPrincipals = []; + $shares = $this->cardDavBackEnd->getShares($addressBookId); + foreach ($shares as $share) { + if ($share['{http://owncloud.org/ns}group-share']) { + $users = $this->principalBackend->getGroupMemberSet($share['{http://owncloud.org/ns}principal']); + foreach ($users as $user) { + $targetPrincipals[] = $user['uri']; + } + } else { + $targetPrincipals[] = $share['{http://owncloud.org/ns}principal']; + } + } + return array_values(array_unique($targetPrincipals, SORT_STRING)); + } + +} diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php new file mode 100644 index 00000000000..f0f236de3ff --- /dev/null +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -0,0 +1,1390 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CalDAV; + +use OCA\DAV\DAV\Sharing\IShareable; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\Backend; +use OCP\IDBConnection; +use Sabre\CalDAV\Backend\AbstractBackend; +use Sabre\CalDAV\Backend\SchedulingSupport; +use Sabre\CalDAV\Backend\SubscriptionSupport; +use Sabre\CalDAV\Backend\SyncSupport; +use Sabre\CalDAV\Plugin; +use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; +use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; +use Sabre\DAV; +use Sabre\DAV\Exception\Forbidden; +use Sabre\HTTP\URLUtil; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Reader; +use Sabre\VObject\RecurrenceIterator; + +/** + * Class CalDavBackend + * + * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php + * + * @package OCA\DAV\CalDAV + */ +class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport { + + /** + * 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'; + + /** + * List of CalDAV properties, and how they map to database field names + * Add your own properties by simply adding on to this array. + * + * Note that only string-based properties are supported here. + * + * @var array + */ + public $propertyMap = [ + '{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', + ]; + + /** + * List of subscription properties, and how they map to database field names. + * + * @var array + */ + public $subscriptionPropertyMap = [ + '{DAV:}displayname' => 'displayname', + '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', + ]; + + /** @var IDBConnection */ + private $db; + + /** @var Backend */ + private $sharingBackend; + + /** @var Principal */ + private $principalBackend; + + /** + * CalDavBackend constructor. + * + * @param IDBConnection $db + * @param Principal $principalBackend + */ + public function __construct(IDBConnection $db, Principal $principalBackend) { + $this->db = $db; + $this->principalBackend = $principalBackend; + $this->sharingBackend = new Backend($this->db, $principalBackend, 'calendar'); + } + + /** + * 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'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * @return array + */ + function getCalendarsForUser($principalUri) { + $principalUriOriginal = $principalUri; + $principalUri = $this->convertPrincipal($principalUri, true); + $fields = array_values($this->propertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + + // Making fields a comma-delimited list + $query = $this->db->getQueryBuilder(); + $query->select($fields)->from('calendars') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->orderBy('calendarorder', 'ASC'); + $stmt = $query->execute(); + + $calendars = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $components = []; + if ($row['components']) { + $components = explode(',',$row['components']); + } + + $calendar = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $this->convertPrincipal($row['principaluri'], false), + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), + ]; + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + if (!isset($calendars[$calendar['id']])) { + $calendars[$calendar['id']] = $calendar; + } + } + + $stmt->closeCursor(); + + // query for shared calendars + $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); + $principals[]= $principalUri; + + $fields = array_values($this->propertyMap); + $fields[] = 'a.id'; + $fields[] = 'a.uri'; + $fields[] = 'a.synctoken'; + $fields[] = 'a.components'; + $fields[] = 'a.principaluri'; + $fields[] = 'a.transparent'; + $fields[] = 's.access'; + $query = $this->db->getQueryBuilder(); + $result = $query->select($fields) + ->from('dav_shares', 's') + ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) + ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) + ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) + ->setParameter('type', 'calendar') + ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY) + ->execute(); + + while($row = $result->fetch()) { + list(, $name) = URLUtil::splitPath($row['principaluri']); + $uri = $row['uri'] . '_shared_by_' . $name; + $row['displayname'] = $row['displayname'] . "($name)"; + $components = []; + if ($row['components']) { + $components = explode(',',$row['components']); + } + $calendar = [ + 'id' => $row['id'], + 'uri' => $uri, + 'principaluri' => $principalUri, + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, + ]; + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + if (!isset($calendars[$calendar['id']])) { + $calendars[$calendar['id']] = $calendar; + } + } + $result->closeCursor(); + + return array_values($calendars); + } + + /** + * @param string $principal + * @param string $uri + * @return array|null + */ + public function getCalendarByUri($principal, $uri) { + $fields = array_values($this->propertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + + // Making fields a comma-delimited list + $query = $this->db->getQueryBuilder(); + $query->select($fields)->from('calendars') + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) + ->setMaxResults(1); + $stmt = $query->execute(); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + if ($row === false) { + return null; + } + + $components = []; + if ($row['components']) { + $components = explode(',',$row['components']); + } + + $calendar = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), + ]; + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + return $calendar; + } + + public function getCalendarById($calendarId) { + $fields = array_values($this->propertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + + // Making fields a comma-delimited list + $query = $this->db->getQueryBuilder(); + $query->select($fields)->from('calendars') + ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))) + ->setMaxResults(1); + $stmt = $query->execute(); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + if ($row === false) { + return null; + } + + $components = []; + if ($row['components']) { + $components = explode(',',$row['components']); + } + + $calendar = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), + ]; + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + return $calendar; + } + + /** + * 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 int + */ + function createCalendar($principalUri, $calendarUri, array $properties) { + $values = [ + 'principaluri' => $principalUri, + 'uri' => $calendarUri, + 'synctoken' => 1, + 'transparent' => 0, + 'components' => 'VEVENT,VTODO', + 'displayname' => $calendarUri + ]; + + // Default value + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + if (isset($properties[$sccs])) { + if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) { + throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet'); + } + $values['components'] = implode(',',$properties[$sccs]->getValue()); + } + $transp = '{' . 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]; + } + } + + $query = $this->db->getQueryBuilder(); + $query->insert('calendars'); + foreach($values as $column => $value) { + $query->setValue($column, $query->createNamedParameter($value)); + } + $query->execute(); + return $query->getLastInsertId(); + } + + /** + * Updates properties for a calendar. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { + $supportedProperties = array_keys($this->propertyMap); + $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; + + $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) { + $newValues = []; + foreach ($mutations as $propertyName => $propertyValue) { + + switch ($propertyName) { + case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' : + $fieldName = 'transparent'; + $newValues[$fieldName] = $propertyValue->getValue() === 'transparent'; + break; + default : + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + break; + } + + } + $query = $this->db->getQueryBuilder(); + $query->update('calendars'); + foreach ($newValues as $fieldName => $value) { + $query->set($fieldName, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); + $query->execute(); + + $this->addChange($calendarId, "", 2); + + return true; + }); + } + + /** + * Delete a calendar and all it's objects + * + * @param mixed $calendarId + * @return void + */ + function deleteCalendar($calendarId) { + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?'); + $stmt->execute([$calendarId]); + + $this->sharingBackend->deleteAllShares($calendarId); + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * '"abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param mixed $calendarId + * @return array + */ + function getCalendarObjects($calendarId) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype']) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); + $stmt = $query->execute(); + + $result = []; + foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'component' => strtolower($row['componenttype']), + ]; + } + + return $result; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param mixed $calendarId + * @param string $objectUri + * @return array|null + */ + function getCalendarObject($calendarId, $objectUri) { + + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype']) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))); + $stmt = $query->execute(); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if(!$row) return null; + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'calendardata' => $this->readBlob($row['calendardata']), + 'component' => strtolower($row['componenttype']), + ]; + } + + /** + * Returns a list of calendar objects. + * + * This method should work identical to getCalendarObject, but instead + * return all the calendar objects in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $calendarId + * @param string[] $uris + * @return array + */ + function getMultipleCalendarObjects($calendarId, array $uris) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype']) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) + ->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); + + $stmt = $query->execute(); + + $result = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'calendardata' => $this->readBlob($row['calendardata']), + 'component' => strtolower($row['componenttype']), + ]; + + } + return $result; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string + */ + function createCalendarObject($calendarId, $objectUri, $calendarData) { + $extraData = $this->getDenormalizedData($calendarData); + + $query = $this->db->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'calendarid' => $query->createNamedParameter($calendarId), + 'uri' => $query->createNamedParameter($objectUri), + 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB), + 'lastmodified' => $query->createNamedParameter(time()), + 'etag' => $query->createNamedParameter($extraData['etag']), + 'size' => $query->createNamedParameter($extraData['size']), + 'componenttype' => $query->createNamedParameter($extraData['componentType']), + 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']), + 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']), + 'uid' => $query->createNamedParameter($extraData['uid']), + ]) + ->execute(); + + $this->addChange($calendarId, $objectUri, 1); + + return '"' . $extraData['etag'] . '"'; + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string + */ + function updateCalendarObject($calendarId, $objectUri, $calendarData) { + $extraData = $this->getDenormalizedData($calendarData); + + $query = $this->db->getQueryBuilder(); + $query->update('calendarobjects') + ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) + ->set('lastmodified', $query->createNamedParameter(time())) + ->set('etag', $query->createNamedParameter($extraData['etag'])) + ->set('size', $query->createNamedParameter($extraData['size'])) + ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) + ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) + ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) + ->set('uid', $query->createNamedParameter($extraData['uid'])) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->execute(); + + $this->addChange($calendarId, $objectUri, 2); + + return '"' . $extraData['etag'] . '"'; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param mixed $calendarId + * @param string $objectUri + * @return void + */ + function deleteCalendarObject($calendarId, $objectUri) { + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?'); + $stmt->execute([$calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 3); + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly advised to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on either VEVENT or VTODO. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interprete all these filters can also simply + * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * @param mixed $calendarId + * @param array $filters + * @return array + */ + function calendarQuery($calendarId, array $filters) { + $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; + } + } + + } + $columns = ['uri']; + if ($requirePostFilter) { + $columns = ['uri', 'calendardata']; + } + $query = $this->db->getQueryBuilder(); + $query->select($columns) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); + + if ($componentType) { + $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType))); + } + + if ($timeRange && $timeRange['start']) { + $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp()))); + } + if ($timeRange && $timeRange['end']) { + $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp()))); + } + + $stmt = $query->execute(); + + $result = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($requirePostFilter) { + if (!$this->validateFilterForObject($row, $filters)) { + continue; + } + } + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * @return string|null + */ + function getCalendarObjectByUID($principalUri, $uid) { + + $query = $this->db->getQueryBuilder(); + $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi') + ->from('calendarobjects', 'co') + ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id')) + ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid))); + + $stmt = $query->execute(); + + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $row['calendaruri'] . '/' . $row['objecturi']; + } + + return null; + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified calendar. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ); + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property This is * needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array + */ + function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) { + // Current synctoken + $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?'); + $stmt->execute([ $calendarId ]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) { + return null; + } + + $result = [ + 'syncToken' => $currentToken, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + + $query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`"; + if ($limit>0) { + $query.= " `LIMIT` " . (int)$limit; + } + + // Fetching all changes + $stmt = $this->db->prepare($query); + $stmt->execute([$syncToken, $currentToken, $calendarId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $changes[$row['uri']] = $row['operation']; + + } + + foreach($changes as $uri => $operation) { + + switch($operation) { + case 1 : + $result['added'][] = $uri; + break; + case 2 : + $result['modified'][] = $uri; + break; + case 3 : + $result['deleted'][] = $uri; + break; + } + + } + } else { + // No synctoken supplied, this is the initial sync. + $query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?"; + $stmt = $this->db->prepare($query); + $stmt->execute([$calendarId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + return $result; + + } + + /** + * Returns a list of subscriptions for a principal. + * + * Every subscription is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * subscription. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the subscription. + * * principaluri. The owner of the subscription. Almost always the same as + * principalUri passed to this method. + * + * Furthermore, all the subscription info must be returned too: + * + * 1. {DAV:}displayname + * 2. {http://apple.com/ns/ical/}refreshrate + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos + * should not be stripped). + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms + * should not be stripped). + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if + * attachments should not be stripped). + * 6. {http://calendarserver.org/ns/}source (Must be a + * Sabre\DAV\Property\Href). + * 7. {http://apple.com/ns/ical/}calendar-color + * 8. {http://apple.com/ns/ical/}calendar-order + * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * (should just be an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of + * default components). + * + * @param string $principalUri + * @return array + */ + function getSubscriptionsForUser($principalUri) { + $fields = array_values($this->subscriptionPropertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'source'; + $fields[] = 'principaluri'; + $fields[] = 'lastmodified'; + + $query = $this->db->getQueryBuilder(); + $query->select($fields) + ->from('calendarsubscriptions') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->orderBy('calendarorder', 'asc'); + $stmt =$query->execute(); + + $subscriptions = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $subscription = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + 'source' => $row['source'], + 'lastmodified' => $row['lastmodified'], + + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + ]; + + foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) { + if (!is_null($row[$dbName])) { + $subscription[$xmlName] = $row[$dbName]; + } + } + + $subscriptions[] = $subscription; + + } + + return $subscriptions; + } + + /** + * Creates a new subscription for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this subscription in other methods, such as updateSubscription. + * + * @param string $principalUri + * @param string $uri + * @param array $properties + * @return mixed + */ + function createSubscription($principalUri, $uri, array $properties) { + + if (!isset($properties['{http://calendarserver.org/ns/}source'])) { + throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); + } + + $values = [ + 'principaluri' => $principalUri, + 'uri' => $uri, + 'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), + 'lastmodified' => time(), + ]; + + foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) { + if (isset($properties[$xmlName])) { + + $values[$dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $query = $this->db->getQueryBuilder(); + $query->insert('calendarsubscriptions') + ->values([ + 'principaluri' => $query->createNamedParameter($values['principaluri']), + 'uri' => $query->createNamedParameter($values['uri']), + 'source' => $query->createNamedParameter($values['source']), + 'lastmodified' => $query->createNamedParameter($values['lastmodified']), + ]) + ->execute(); + + return $this->db->lastInsertId('*PREFIX*calendarsubscriptions'); + } + + /** + * Updates a subscription + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $subscriptionId + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) { + $supportedProperties = array_keys($this->subscriptionPropertyMap); + $supportedProperties[] = '{http://calendarserver.org/ns/}source'; + + $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) { + + $newValues = []; + + foreach($mutations as $propertyName=>$propertyValue) { + if ($propertyName === '{http://calendarserver.org/ns/}source') { + $newValues['source'] = $propertyValue->getHref(); + } else { + $fieldName = $this->subscriptionPropertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + } + } + + $query = $this->db->getQueryBuilder(); + $query->update('calendarsubscriptions') + ->set('lastmodified', $query->createNamedParameter(time())); + foreach($newValues as $fieldName=>$value) { + $query->set($fieldName, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) + ->execute(); + + return true; + + }); + } + + /** + * Deletes a subscription. + * + * @param mixed $subscriptionId + * @return void + */ + function deleteSubscription($subscriptionId) { + $query = $this->db->getQueryBuilder(); + $query->delete('calendarsubscriptions') + ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) + ->execute(); + } + + /** + * Returns a single scheduling object for the inbox collection. + * + * The returned array should contain the following elements: + * * uri - A unique basename for the object. This will be used to + * construct a full uri. + * * calendardata - The iCalendar object + * * lastmodified - The last modification date. Can be an int for a unix + * timestamp, or a PHP DateTime object. + * * etag - A unique token that must change if the object changed. + * * size - The size of the object, in bytes. + * + * @param string $principalUri + * @param string $objectUri + * @return array + */ + function getSchedulingObject($principalUri, $objectUri) { + $query = $this->db->getQueryBuilder(); + $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) + ->from('schedulingobjects') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->execute(); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if(!$row) { + return null; + } + + return [ + 'uri' => $row['uri'], + 'calendardata' => $row['calendardata'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'size' => (int)$row['size'], + ]; + } + + /** + * Returns all scheduling objects for the inbox collection. + * + * These objects should be returned as an array. Every item in the array + * should follow the same structure as returned from getSchedulingObject. + * + * The main difference is that 'calendardata' is optional. + * + * @param string $principalUri + * @return array + */ + function getSchedulingObjects($principalUri) { + $query = $this->db->getQueryBuilder(); + $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) + ->from('schedulingobjects') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->execute(); + + $result = []; + foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'calendardata' => $row['calendardata'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'size' => (int)$row['size'], + ]; + } + + return $result; + } + + /** + * Deletes a scheduling object from the inbox collection. + * + * @param string $principalUri + * @param string $objectUri + * @return void + */ + function deleteSchedulingObject($principalUri, $objectUri) { + $query = $this->db->getQueryBuilder(); + $query->delete('schedulingobjects') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->execute(); + } + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string $objectData + * @return void + */ + function createSchedulingObject($principalUri, $objectUri, $objectData) { + $query = $this->db->getQueryBuilder(); + $query->insert('schedulingobjects') + ->values([ + 'principaluri' => $query->createNamedParameter($principalUri), + 'calendardata' => $query->createNamedParameter($objectData), + 'uri' => $query->createNamedParameter($objectUri), + 'lastmodified' => $query->createNamedParameter(time()), + 'etag' => $query->createNamedParameter(md5($objectData)), + 'size' => $query->createNamedParameter(strlen($objectData)) + ]) + ->execute(); + } + + /** + * Adds a change record to the calendarchanges table. + * + * @param mixed $calendarId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete. + * @return void + */ + protected function addChange($calendarId, $objectUri, $operation) { + + $stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?'); + $stmt->execute([ + $objectUri, + $calendarId, + $operation, + $calendarId + ]); + $stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); + $stmt->execute([ + $calendarId + ]); + + } + + /** + * Parses some information from calendar objects, used for optimized + * calendar-queries. + * + * Returns an array with the following keys: + * * etag - An md5 checksum of the object without the quotes. + * * size - Size of the object in bytes + * * componentType - VEVENT, VTODO or VJOURNAL + * * firstOccurence + * * lastOccurence + * * uid - value of the UID property + * + * @param string $calendarData + * @return array + */ + protected function getDenormalizedData($calendarData) { + + $vObject = Reader::read($calendarData); + $componentType = null; + $component = null; + $firstOccurence = null; + $lastOccurence = null; + $uid = null; + foreach($vObject->getComponents() as $component) { + if ($component->name!=='VTIMEZONE') { + $componentType = $component->name; + $uid = (string)$component->UID; + break; + } + } + if (!$componentType) { + throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); + } + if ($componentType === 'VEVENT' && $component->DTSTART) { + $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); + // Finding the last occurrence 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(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 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 [ + 'etag' => md5($calendarData), + 'size' => strlen($calendarData), + 'componentType' => $componentType, + 'firstOccurence' => is_null($firstOccurence) ? null : max(0, $firstOccurence), + 'lastOccurence' => $lastOccurence, + 'uid' => $uid, + ]; + + } + + private function readBlob($cardData) { + if (is_resource($cardData)) { + return stream_get_contents($cardData); + } + + return $cardData; + } + + /** + * @param IShareable $shareable + * @param array $add + * @param array $remove + */ + public function updateShares($shareable, $add, $remove) { + $this->sharingBackend->updateShares($shareable, $add, $remove); + } + + /** + * @param int $resourceId + * @return array + */ + public function getShares($resourceId) { + return $this->sharingBackend->getShares($resourceId); + } + + /** + * @param int $resourceId + * @param array $acl + * @return array + */ + public function applyShareAcl($resourceId, $acl) { + return $this->sharingBackend->applyShareAcl($resourceId, $acl); + } + + private function convertPrincipal($principalUri, $toV2) { + if ($this->principalBackend->getPrincipalPrefix() === 'principals') { + list(, $name) = URLUtil::splitPath($principalUri); + if ($toV2 === true) { + return "principals/users/$name"; + } + return "principals/$name"; + } + return $principalUri; + } +} diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php new file mode 100644 index 00000000000..f3637692e43 --- /dev/null +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -0,0 +1,171 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV; + +use OCA\DAV\DAV\Sharing\IShareable; +use OCP\IL10N; +use Sabre\CalDAV\Backend\BackendInterface; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\PropPatch; + +class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { + + public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n) { + parent::__construct($caldavBackend, $calendarInfo); + + if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) { + $this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays'); + } + } + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * resource. + * + * 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) { + /** @var CalDavBackend $calDavBackend */ + $calDavBackend = $this->caldavBackend; + $calDavBackend->updateShares($this, $add, $remove); + } + + /** + * Returns the list of people whom this resource is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares() { + /** @var CalDavBackend $calDavBackend */ + $calDavBackend = $this->caldavBackend; + return $calDavBackend->getShares($this->getResourceId()); + } + + /** + * @return int + */ + public function getResourceId() { + return $this->calendarInfo['id']; + } + + function getACL() { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ]]; + if ($this->getName() !== BirthdayService::BIRTHDAY_CALENDAR_URI) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + } + if ($this->getOwner() !== parent::getOwner()) { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => parent::getOwner(), + 'protected' => true, + ]; + if ($this->canWrite()) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => parent::getOwner(), + 'protected' => true, + ]; + } + } + + /** @var CalDavBackend $calDavBackend */ + $calDavBackend = $this->caldavBackend; + return $calDavBackend->applyShareAcl($this->getResourceId(), $acl); + } + + function getChildACL() { + return $this->getACL(); + } + + function getOwner() { + if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { + return $this->calendarInfo['{http://owncloud.org/ns}owner-principal']; + } + return parent::getOwner(); + } + + function delete() { + if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { + $principal = 'principal:' . parent::getOwner(); + $shares = $this->getShares(); + $shares = array_filter($shares, function($share) use ($principal){ + return $share['href'] === $principal; + }); + if (empty($shares)) { + throw new Forbidden(); + } + + /** @var CalDavBackend $calDavBackend */ + $calDavBackend = $this->caldavBackend; + $calDavBackend->updateShares($this, [], [ + 'href' => $principal + ]); + return; + } + parent::delete(); + } + + function propPatch(PropPatch $propPatch) { + $mutations = $propPatch->getMutations(); + // If this is a shared calendar, the user can only change the enabled property, to hide it. + if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) && (sizeof($mutations) !== 1 || !isset($mutations['{http://owncloud.org/ns}calendar-enabled']))) { + throw new Forbidden(); + } + parent::propPatch($propPatch); + } + + private function canWrite() { + if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) { + return !$this->calendarInfo['{http://owncloud.org/ns}read-only']; + } + return true; + } + +} diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php new file mode 100644 index 00000000000..a4c485a8983 --- /dev/null +++ b/apps/dav/lib/CalDAV/CalendarHome.php @@ -0,0 +1,106 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV; + +use Sabre\CalDAV\Backend\BackendInterface; +use Sabre\CalDAV\Backend\NotificationSupport; +use Sabre\CalDAV\Backend\SchedulingSupport; +use Sabre\CalDAV\Backend\SubscriptionSupport; +use Sabre\CalDAV\Schedule\Inbox; +use Sabre\CalDAV\Schedule\Outbox; +use Sabre\CalDAV\Subscriptions\Subscription; +use Sabre\DAV\Exception\NotFound; + +class CalendarHome extends \Sabre\CalDAV\CalendarHome { + + /** @var \OCP\IL10N */ + private $l10n; + + public function __construct(BackendInterface $caldavBackend, $principalInfo) { + parent::__construct($caldavBackend, $principalInfo); + $this->l10n = \OC::$server->getL10N('dav'); + } + + /** + * @inheritdoc + */ + function getChildren() { + $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); + $objects = []; + foreach ($calendars as $calendar) { + $objects[] = new Calendar($this->caldavBackend, $calendar, $this->l10n); + } + + if ($this->caldavBackend instanceof SchedulingSupport) { + $objects[] = new Inbox($this->caldavBackend, $this->principalInfo['uri']); + $objects[] = new Outbox($this->principalInfo['uri']); + } + + // We're adding a notifications node, if it's supported by the backend. + if ($this->caldavBackend instanceof NotificationSupport) { + $objects[] = new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // If the backend supports subscriptions, we'll add those as well, + if ($this->caldavBackend instanceof SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + $objects[] = new Subscription($this->caldavBackend, $subscription); + } + } + + return $objects; + } + + /** + * @inheritdoc + */ + function getChild($name) { + // Special nodes + if ($name === 'inbox' && $this->caldavBackend instanceof SchedulingSupport) { + return new Inbox($this->caldavBackend, $this->principalInfo['uri']); + } + if ($name === 'outbox' && $this->caldavBackend instanceof SchedulingSupport) { + return new Outbox($this->principalInfo['uri']); + } + if ($name === 'notifications' && $this->caldavBackend instanceof NotificationSupport) { + return new \Sabre\CalDAv\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // Calendars + foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) { + if ($calendar['uri'] === $name) { + return new Calendar($this->caldavBackend, $calendar, $this->l10n); + } + } + + if ($this->caldavBackend instanceof SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + if ($subscription['uri'] === $name) { + return new Subscription($this->caldavBackend, $subscription); + } + } + + } + + throw new NotFound('Node with name \'' . $name . '\' could not be found'); + } +} diff --git a/apps/dav/lib/CalDAV/CalendarRoot.php b/apps/dav/lib/CalDAV/CalendarRoot.php new file mode 100644 index 00000000000..d302da3d861 --- /dev/null +++ b/apps/dav/lib/CalDAV/CalendarRoot.php @@ -0,0 +1,29 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV; + +class CalendarRoot extends \Sabre\CalDAV\CalendarRoot { + + function getChildForPrincipal(array $principal) { + return new CalendarHome($this->caldavBackend, $principal); + } +} \ No newline at end of file diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php new file mode 100644 index 00000000000..d9f2ec674f0 --- /dev/null +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -0,0 +1,128 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Schedule; + +use OCP\ILogger; +use OCP\Mail\IMailer; +use Sabre\DAV; +use Sabre\VObject\ITip; +use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin; +/** + * 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 IMipPlugin extends SabreIMipPlugin { + + /** @var IMailer */ + private $mailer; + + /** @var ILogger */ + private $logger; + + /** + * Creates the email handler. + * + * @param IMailer $mailer + */ + function __construct(IMailer $mailer, ILogger $logger) { + parent::__construct(''); + $this->mailer = $mailer; + $this->logger = $logger; + } + + /** + * Event handler for the 'schedule' event. + * + * @param ITip\Message $iTipMessage + * @return void + */ + function schedule(ITip\Message $iTipMessage) { + + // Not sending any emails if the system considers the update + // insignificant. + if (!$iTipMessage->significantChange) { + if (!$iTipMessage->scheduleStatus) { + $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; + } + return; + } + + $summary = $iTipMessage->message->VEVENT->SUMMARY; + + if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') { + return; + } + + if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') { + return; + } + + $sender = substr($iTipMessage->sender, 7); + $recipient = substr($iTipMessage->recipient, 7); + + $senderName = ($iTipMessage->senderName) ? $iTipMessage->senderName : null; + $recipientName = ($iTipMessage->recipientName) ? $iTipMessage->recipientName : null; + + $subject = 'SabreDAV iTIP message'; + switch (strtoupper($iTipMessage->method)) { + case 'REPLY' : + $subject = 'Re: ' . $summary; + break; + case 'REQUEST' : + $subject = $summary; + break; + case 'CANCEL' : + $subject = 'Cancelled: ' . $summary; + break; + } + + $contentType = 'text/calendar; charset=UTF-8; method=' . $iTipMessage->method; + + $message = $this->mailer->createMessage(); + + $message->setReplyTo([$sender => $senderName]) + ->setTo([$recipient => $recipientName]) + ->setSubject($subject) + ->setBody($iTipMessage->message->serialize(), $contentType); + try { + $failed = $this->mailer->send($message); + if ($failed) { + $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); + $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; + } + $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; + } catch(\Exception $ex) { + $this->logger->logException($ex, ['app' => 'dav']); + $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; + } + } + +} diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php new file mode 100644 index 00000000000..8b1b600ec3d --- /dev/null +++ b/apps/dav/lib/CardDAV/AddressBook.php @@ -0,0 +1,182 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CardDAV; + +use OCA\DAV\DAV\Sharing\IShareable; +use Sabre\CardDAV\Card; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\PropPatch; + +class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable { + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * addressbook. + * + * 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) { + /** @var CardDavBackend $carddavBackend */ + $carddavBackend = $this->carddavBackend; + $carddavBackend->updateShares($this, $add, $remove); + } + + /** + * Returns the list of people whom this addressbook is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares() { + /** @var CardDavBackend $carddavBackend */ + $carddavBackend = $this->carddavBackend; + return $carddavBackend->getShares($this->getResourceId()); + } + + function getACL() { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ]]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + if ($this->getOwner() !== parent::getOwner()) { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => parent::getOwner(), + 'protected' => true, + ]; + if ($this->canWrite()) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => parent::getOwner(), + 'protected' => true, + ]; + } + } + if ($this->getOwner() === 'principals/system/system') { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + } + + /** @var CardDavBackend $carddavBackend */ + $carddavBackend = $this->carddavBackend; + return $carddavBackend->applyShareAcl($this->getResourceId(), $acl); + } + + function getChildACL() { + return $this->getACL(); + } + + function getChild($name) { + + $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name); + if (!$obj) { + throw new NotFound('Card not found'); + } + $obj['acl'] = $this->getChildACL(); + return new Card($this->carddavBackend, $this->addressBookInfo, $obj); + + } + + /** + * @return int + */ + public function getResourceId() { + return $this->addressBookInfo['id']; + } + + function getOwner() { + if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) { + return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal']; + } + return parent::getOwner(); + } + + function delete() { + if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) { + $principal = 'principal:' . parent::getOwner(); + $shares = $this->getShares(); + $shares = array_filter($shares, function($share) use ($principal){ + return $share['href'] === $principal; + }); + if (empty($shares)) { + throw new Forbidden(); + } + + /** @var CardDavBackend $cardDavBackend */ + $cardDavBackend = $this->carddavBackend; + $cardDavBackend->updateShares($this, [], [ + 'href' => $principal + ]); + return; + } + parent::delete(); + } + + function propPatch(PropPatch $propPatch) { + if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) { + throw new Forbidden(); + } + parent::propPatch($propPatch); + } + + public function getContactsGroups() { + /** @var CardDavBackend $cardDavBackend */ + $cardDavBackend = $this->carddavBackend; + + return $cardDavBackend->collectCardProperties($this->getResourceId(), 'CATEGORIES'); + } + + private function canWrite() { + if (isset($this->addressBookInfo['{http://owncloud.org/ns}read-only'])) { + return !$this->addressBookInfo['{http://owncloud.org/ns}read-only']; + } + return true; + } +} diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php new file mode 100644 index 00000000000..8b29d6d5c0c --- /dev/null +++ b/apps/dav/lib/CardDAV/AddressBookImpl.php @@ -0,0 +1,224 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CardDAV; + +use OCP\Constants; +use OCP\IAddressBook; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; +use Sabre\VObject\Reader; +use Sabre\VObject\UUIDUtil; + +class AddressBookImpl implements IAddressBook { + + /** @var CardDavBackend */ + private $backend; + + /** @var array */ + private $addressBookInfo; + + /** @var AddressBook */ + private $addressBook; + + /** + * AddressBookImpl constructor. + * + * @param AddressBook $addressBook + * @param array $addressBookInfo + * @param CardDavBackend $backend + */ + public function __construct( + AddressBook $addressBook, + array $addressBookInfo, + CardDavBackend $backend) { + + $this->addressBook = $addressBook; + $this->addressBookInfo = $addressBookInfo; + $this->backend = $backend; + } + + /** + * @return string defining the technical unique key + * @since 5.0.0 + */ + public function getKey() { + return $this->addressBookInfo['id']; + } + + /** + * In comparison to getKey() this function returns a human readable (maybe translated) name + * + * @return mixed + * @since 5.0.0 + */ + public function getDisplayName() { + return $this->addressBookInfo['{DAV:}displayname']; + } + + /** + * @param string $pattern which should match within the $searchProperties + * @param array $searchProperties defines the properties within the query pattern should match + * @param array $options - for future use. One should always have options! + * @return array an array of contacts which are arrays of key-value-pairs + * @since 5.0.0 + */ + public function search($pattern, $searchProperties, $options) { + $result = $this->backend->search($this->getKey(), $pattern, $searchProperties); + + $vCards = []; + foreach ($result as $cardData) { + $vCards[] = $this->vCard2Array($this->readCard($cardData)); + } + + return $vCards; + } + + /** + * @param array $properties this array if key-value-pairs defines a contact + * @return array an array representing the contact just created or updated + * @since 5.0.0 + */ + public function createOrUpdate($properties) { + $update = false; + if (!isset($properties['UID'])) { // create a new contact + $uid = $this->createUid(); + $uri = $uid . '.vcf'; + $vCard = $this->createEmptyVCard($uid); + } else { // update existing contact + $uid = $properties['UID']; + $uri = $uid . '.vcf'; + $vCardData = $this->backend->getCard($this->getKey(), $uri); + $vCard = $this->readCard($vCardData['carddata']); + $update = true; + } + + foreach ($properties as $key => $value) { + $vCard->$key = $vCard->createProperty($key, $value); + } + + if ($update) { + $this->backend->updateCard($this->getKey(), $uri, $vCard->serialize()); + } else { + $this->backend->createCard($this->getKey(), $uri, $vCard->serialize()); + } + + return $this->vCard2Array($vCard); + + } + + /** + * @return mixed + * @since 5.0.0 + */ + public function getPermissions() { + $permissions = $this->addressBook->getACL(); + $result = 0; + foreach ($permissions as $permission) { + switch($permission['privilege']) { + case '{DAV:}read': + $result |= Constants::PERMISSION_READ; + break; + case '{DAV:}write': + $result |= Constants::PERMISSION_CREATE; + $result |= Constants::PERMISSION_UPDATE; + break; + case '{DAV:}all': + $result |= Constants::PERMISSION_ALL; + break; + } + } + + return $result; + } + + /** + * @param object $id the unique identifier to a contact + * @return bool successful or not + * @since 5.0.0 + */ + public function delete($id) { + $uri = $this->backend->getCardUri($id); + return $this->backend->deleteCard($this->addressBookInfo['id'], $uri); + } + + /** + * read vCard data into a vCard object + * + * @param string $cardData + * @return VCard + */ + protected function readCard($cardData) { + return Reader::read($cardData); + } + + /** + * create UID for contact + * + * @return string + */ + protected function createUid() { + do { + $uid = $this->getUid(); + $contact = $this->backend->getContact($this->getKey(), $uid . '.vcf'); + } while (!empty($contact)); + + return $uid; + } + + /** + * getUid is only there for testing, use createUid instead + */ + protected function getUid() { + return UUIDUtil::getUUID(); + } + + /** + * create empty vcard + * + * @param string $uid + * @return VCard + */ + protected function createEmptyVCard($uid) { + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'UID', $uid)); + return $vCard; + } + + /** + * create array with all vCard properties + * + * @param VCard $vCard + * @return array + */ + protected function vCard2Array(VCard $vCard) { + $result = []; + foreach ($vCard->children as $property) { + $result[$property->name] = $property->getValue(); + } + if ($this->addressBookInfo['principaluri'] === 'principals/system/system' && + $this->addressBookInfo['uri'] === 'system') { + $result['isLocalSystemBook'] = true; + } + return $result; + } +} diff --git a/apps/dav/lib/CardDAV/AddressBookRoot.php b/apps/dav/lib/CardDAV/AddressBookRoot.php new file mode 100644 index 00000000000..99c36c2e767 --- /dev/null +++ b/apps/dav/lib/CardDAV/AddressBookRoot.php @@ -0,0 +1,54 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CardDAV; + +class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @param array $principal + * @return \Sabre\DAV\INode + */ + function getChildForPrincipal(array $principal) { + + return new UserAddressBooks($this->carddavBackend, $principal['uri']); + + } + + function getName() { + + if ($this->principalPrefix === 'principals') { + return parent::getName(); + } + // Grabbing all the components of the principal path. + $parts = explode('/', $this->principalPrefix); + + // We are only interested in the second part. + return $parts[1]; + + } + +} diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php new file mode 100644 index 00000000000..28d5ed1ae99 --- /dev/null +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -0,0 +1,987 @@ + + * @author Björn Schießle + * @author Joas Schilling + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CardDAV; + +use OCA\DAV\Connector\Sabre\Principal; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCA\DAV\DAV\Sharing\Backend; +use OCA\DAV\DAV\Sharing\IShareable; +use OCP\IDBConnection; +use PDO; +use Sabre\CardDAV\Backend\BackendInterface; +use Sabre\CardDAV\Backend\SyncSupport; +use Sabre\CardDAV\Plugin; +use Sabre\DAV\Exception\BadRequest; +use Sabre\HTTP\URLUtil; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Reader; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; + +class CardDavBackend implements BackendInterface, SyncSupport { + + /** @var Principal */ + private $principalBackend; + + /** @var string */ + private $dbCardsTable = 'cards'; + + /** @var string */ + private $dbCardsPropertiesTable = 'cards_properties'; + + /** @var IDBConnection */ + private $db; + + /** @var Backend */ + private $sharingBackend; + + /** @var array properties to index */ + public static $indexProperties = array( + 'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME', + 'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'); + + /** @var EventDispatcherInterface */ + private $dispatcher; + + /** + * CardDavBackend constructor. + * + * @param IDBConnection $db + * @param Principal $principalBackend + * @param EventDispatcherInterface $dispatcher + */ + public function __construct(IDBConnection $db, + Principal $principalBackend, + EventDispatcherInterface $dispatcher = null) { + $this->db = $db; + $this->principalBackend = $principalBackend; + $this->dispatcher = $dispatcher; + $this->sharingBackend = new Backend($this->db, $principalBackend, 'addressbook'); + } + + /** + * Returns the list of address books 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 + */ + function getAddressBooksForUser($principalUri) { + $principalUriOriginal = $principalUri; + $principalUri = $this->convertPrincipal($principalUri, true); + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) + ->from('addressbooks') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); + + $addressBooks = []; + + $result = $query->execute(); + while($row = $result->fetch()) { + $addressBooks[$row['id']] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $this->convertPrincipal($row['principaluri'], false), + '{DAV:}displayname' => $row['displayname'], + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + ]; + } + $result->closeCursor(); + + // query for shared calendars + $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); + $principals[]= $principalUri; + + $query = $this->db->getQueryBuilder(); + $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) + ->from('dav_shares', 's') + ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id')) + ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) + ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) + ->setParameter('type', 'addressbook') + ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY) + ->execute(); + + while($row = $result->fetch()) { + list(, $name) = URLUtil::splitPath($row['principaluri']); + $uri = $row['uri'] . '_shared_by_' . $name; + $displayName = $row['displayname'] . "($name)"; + if (!isset($addressBooks[$row['id']])) { + $addressBooks[$row['id']] = [ + 'id' => $row['id'], + 'uri' => $uri, + 'principaluri' => $principalUri, + '{DAV:}displayname' => $displayName, + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, + ]; + } + } + $result->closeCursor(); + + return array_values($addressBooks); + } + + /** + * @param int $addressBookId + */ + public function getAddressBookById($addressBookId) { + $query = $this->db->getQueryBuilder(); + $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) + ->from('addressbooks') + ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) + ->execute(); + + $row = $result->fetch(); + $result->closeCursor(); + if ($row === false) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{DAV:}displayname' => $row['displayname'], + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + ]; + } + + /** + * @param $addressBookUri + * @return array|null + */ + public function getAddressBooksByUri($principal, $addressBookUri) { + $query = $this->db->getQueryBuilder(); + $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) + ->from('addressbooks') + ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri))) + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) + ->setMaxResults(1) + ->execute(); + + $row = $result->fetch(); + $result->closeCursor(); + if ($row === false) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{DAV:}displayname' => $row['displayname'], + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + ]; + } + + /** + * Updates properties for an address book. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $addressBookId + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { + $supportedProperties = [ + '{DAV:}displayname', + '{' . Plugin::NS_CARDDAV . '}addressbook-description', + ]; + + $propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) { + + $updates = []; + foreach($mutations as $property=>$newValue) { + + switch($property) { + case '{DAV:}displayname' : + $updates['displayname'] = $newValue; + break; + case '{' . Plugin::NS_CARDDAV . '}addressbook-description' : + $updates['description'] = $newValue; + break; + } + } + $query = $this->db->getQueryBuilder(); + $query->update('addressbooks'); + + foreach($updates as $key=>$value) { + $query->set($key, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) + ->execute(); + + $this->addChange($addressBookId, "", 2); + + return true; + + }); + } + + /** + * Creates a new address book + * + * @param string $principalUri + * @param string $url Just the 'basename' of the url. + * @param array $properties + * @return int + * @throws BadRequest + */ + function createAddressBook($principalUri, $url, array $properties) { + $values = [ + 'displayname' => null, + 'description' => null, + 'principaluri' => $principalUri, + 'uri' => $url, + 'synctoken' => 1 + ]; + + foreach($properties as $property=>$newValue) { + + switch($property) { + case '{DAV:}displayname' : + $values['displayname'] = $newValue; + break; + case '{' . Plugin::NS_CARDDAV . '}addressbook-description' : + $values['description'] = $newValue; + break; + default : + throw new BadRequest('Unknown property: ' . $property); + } + + } + + // Fallback to make sure the displayname is set. Some clients may refuse + // to work with addressbooks not having a displayname. + if(is_null($values['displayname'])) { + $values['displayname'] = $url; + } + + $query = $this->db->getQueryBuilder(); + $query->insert('addressbooks') + ->values([ + 'uri' => $query->createParameter('uri'), + 'displayname' => $query->createParameter('displayname'), + 'description' => $query->createParameter('description'), + 'principaluri' => $query->createParameter('principaluri'), + 'synctoken' => $query->createParameter('synctoken'), + ]) + ->setParameters($values) + ->execute(); + + return $query->getLastInsertId(); + } + + /** + * Deletes an entire addressbook and all its contents + * + * @param mixed $addressBookId + * @return void + */ + function deleteAddressBook($addressBookId) { + $query = $this->db->getQueryBuilder(); + $query->delete('cards') + ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) + ->setParameter('addressbookid', $addressBookId) + ->execute(); + + $query->delete('addressbookchanges') + ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) + ->setParameter('addressbookid', $addressBookId) + ->execute(); + + $query->delete('addressbooks') + ->where($query->expr()->eq('id', $query->createParameter('id'))) + ->setParameter('id', $addressBookId) + ->execute(); + + $this->sharingBackend->deleteAllShares($addressBookId); + + $query->delete($this->dbCardsPropertiesTable) + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->execute(); + + } + + /** + * Returns all cards for a specific addressbook id. + * + * This method should return the following properties for each card: + * * carddata - raw vcard data + * * uri - Some unique url + * * lastmodified - A unix timestamp + * + * It's recommended to also return the following properties: + * * etag - A unique etag. This must change every time the card changes. + * * size - The size of the card in bytes. + * + * If these last two properties are provided, less time will be spent + * calculating them. If they are specified, you can also ommit carddata. + * This may speed up certain requests, especially with large cards. + * + * @param mixed $addressBookId + * @return array + */ + function getCards($addressBookId) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) + ->from('cards') + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); + + $cards = []; + + $result = $query->execute(); + while($row = $result->fetch()) { + $row['etag'] = '"' . $row['etag'] . '"'; + $row['carddata'] = $this->readBlob($row['carddata']); + $cards[] = $row; + } + $result->closeCursor(); + + return $cards; + } + + /** + * Returns a specific card. + * + * The same set of properties must be returned as with getCards. The only + * exception is that 'carddata' is absolutely required. + * + * If the card does not exist, you must return false. + * + * @param mixed $addressBookId + * @param string $cardUri + * @return array + */ + function getCard($addressBookId, $cardUri) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) + ->from('cards') + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) + ->setMaxResults(1); + + $result = $query->execute(); + $row = $result->fetch(); + if (!$row) { + return false; + } + $row['etag'] = '"' . $row['etag'] . '"'; + $row['carddata'] = $this->readBlob($row['carddata']); + + return $row; + } + + /** + * Returns a list of cards. + * + * This method should work identical to getCard, but instead return all the + * cards in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $addressBookId + * @param string[] $uris + * @return array + */ + function getMultipleCards($addressBookId, array $uris) { + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) + ->from('cards') + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) + ->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); + + $cards = []; + + $result = $query->execute(); + while($row = $result->fetch()) { + $row['etag'] = '"' . $row['etag'] . '"'; + $row['carddata'] = $this->readBlob($row['carddata']); + $cards[] = $row; + } + $result->closeCursor(); + + return $cards; + } + + /** + * 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 + */ + function createCard($addressBookId, $cardUri, $cardData) { + $etag = md5($cardData); + + $query = $this->db->getQueryBuilder(); + $query->insert('cards') + ->values([ + 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB), + 'uri' => $query->createNamedParameter($cardUri), + 'lastmodified' => $query->createNamedParameter(time()), + 'addressbookid' => $query->createNamedParameter($addressBookId), + 'size' => $query->createNamedParameter(strlen($cardData)), + 'etag' => $query->createNamedParameter($etag), + ]) + ->execute(); + + $this->addChange($addressBookId, $cardUri, 1); + $this->updateProperties($addressBookId, $cardUri, $cardData); + + if (!is_null($this->dispatcher)) { + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri, + 'cardData' => $cardData])); + } + + return '"' . $etag . '"'; + } + + /** + * Updates a card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag should + * match that of the updated resource, and must be enclosed with double + * quotes (that is: the string itself must contain the actual quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * @return string + */ + function updateCard($addressBookId, $cardUri, $cardData) { + + $etag = md5($cardData); + $query = $this->db->getQueryBuilder(); + $query->update('cards') + ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB)) + ->set('lastmodified', $query->createNamedParameter(time())) + ->set('size', $query->createNamedParameter(strlen($cardData))) + ->set('etag', $query->createNamedParameter($etag)) + ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->execute(); + + $this->addChange($addressBookId, $cardUri, 2); + $this->updateProperties($addressBookId, $cardUri, $cardData); + + if (!is_null($this->dispatcher)) { + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri, + 'cardData' => $cardData])); + } + + return '"' . $etag . '"'; + } + + /** + * Deletes a card + * + * @param mixed $addressBookId + * @param string $cardUri + * @return bool + */ + function deleteCard($addressBookId, $cardUri) { + try { + $cardId = $this->getCardId($addressBookId, $cardUri); + } catch (\InvalidArgumentException $e) { + $cardId = null; + } + $query = $this->db->getQueryBuilder(); + $ret = $query->delete('cards') + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) + ->execute(); + + $this->addChange($addressBookId, $cardUri, 3); + + if (!is_null($this->dispatcher)) { + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri])); + } + + if ($ret === 1) { + if ($cardId !== null) { + $this->purgeProperties($addressBookId, $cardId); + } + return true; + } + + return false; + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified address book. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property. This is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $addressBookId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array + */ + function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { + // Current synctoken + $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?'); + $stmt->execute([ $addressBookId ]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) return null; + + $result = [ + 'syncToken' => $currentToken, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + + $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`"; + if ($limit>0) { + $query .= " `LIMIT` " . (int)$limit; + } + + // Fetching all changes + $stmt = $this->db->prepare($query); + $stmt->execute([$syncToken, $currentToken, $addressBookId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $changes[$row['uri']] = $row['operation']; + + } + + foreach($changes as $uri => $operation) { + + switch($operation) { + case 1: + $result['added'][] = $uri; + break; + case 2: + $result['modified'][] = $uri; + break; + case 3: + $result['deleted'][] = $uri; + break; + } + + } + } else { + // No synctoken supplied, this is the initial sync. + $query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?"; + $stmt = $this->db->prepare($query); + $stmt->execute([$addressBookId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + return $result; + } + + /** + * Adds a change record to the addressbookchanges table. + * + * @param mixed $addressBookId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete + * @return void + */ + protected function addChange($addressBookId, $objectUri, $operation) { + $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?'; + $stmt = $this->db->prepare($sql); + $stmt->execute([ + $objectUri, + $addressBookId, + $operation, + $addressBookId + ]); + $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); + $stmt->execute([ + $addressBookId + ]); + } + + private function readBlob($cardData) { + if (is_resource($cardData)) { + return stream_get_contents($cardData); + } + + return $cardData; + } + + /** + * @param IShareable $shareable + * @param string[] $add + * @param string[] $remove + */ + public function updateShares(IShareable $shareable, $add, $remove) { + $this->sharingBackend->updateShares($shareable, $add, $remove); + } + + /** + * search contact + * + * @param int $addressBookId + * @param string $pattern which should match within the $searchProperties + * @param array $searchProperties defines the properties within the query pattern should match + * @return array an array of contacts which are arrays of key-value-pairs + */ + public function search($addressBookId, $pattern, $searchProperties) { + $query = $this->db->getQueryBuilder(); + $query2 = $this->db->getQueryBuilder(); + $query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp'); + foreach ($searchProperties as $property) { + $query2->orWhere( + $query2->expr()->andX( + $query2->expr()->eq('cp.name', $query->createNamedParameter($property)), + $query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')) + ) + ); + } + $query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId))); + + $query->select('c.carddata')->from($this->dbCardsTable, 'c') + ->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL()))); + + $result = $query->execute(); + $cards = $result->fetchAll(); + + $result->closeCursor(); + + return array_map(function($array) {return $this->readBlob($array['carddata']);}, $cards); + + } + + /** + * @param int $bookId + * @param string $name + * @return array + */ + public function collectCardProperties($bookId, $name) { + $query = $this->db->getQueryBuilder(); + $result = $query->selectDistinct('value') + ->from($this->dbCardsPropertiesTable) + ->where($query->expr()->eq('name', $query->createNamedParameter($name))) + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId))) + ->execute(); + + $all = $result->fetchAll(PDO::FETCH_COLUMN); + $result->closeCursor(); + + return $all; + } + + /** + * get URI from a given contact + * + * @param int $id + * @return string + */ + public function getCardUri($id) { + $query = $this->db->getQueryBuilder(); + $query->select('uri')->from($this->dbCardsTable) + ->where($query->expr()->eq('id', $query->createParameter('id'))) + ->setParameter('id', $id); + + $result = $query->execute(); + $uri = $result->fetch(); + $result->closeCursor(); + + if (!isset($uri['uri'])) { + throw new \InvalidArgumentException('Card does not exists: ' . $id); + } + + return $uri['uri']; + } + + /** + * return contact with the given URI + * + * @param int $addressBookId + * @param string $uri + * @returns array + */ + public function getContact($addressBookId, $uri) { + $result = []; + $query = $this->db->getQueryBuilder(); + $query->select('*')->from($this->dbCardsTable) + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); + $queryResult = $query->execute(); + $contact = $queryResult->fetch(); + $queryResult->closeCursor(); + + if (is_array($contact)) { + $result = $contact; + } + + return $result; + } + + /** + * Returns the list of people whom this address book is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + public function getShares($addressBookId) { + return $this->sharingBackend->getShares($addressBookId); + } + + /** + * update properties table + * + * @param int $addressBookId + * @param string $cardUri + * @param string $vCardSerialized + */ + protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) { + $cardId = $this->getCardId($addressBookId, $cardUri); + $vCard = $this->readCard($vCardSerialized); + + $this->purgeProperties($addressBookId, $cardId); + + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter($addressBookId), + 'cardid' => $query->createNamedParameter($cardId), + 'name' => $query->createParameter('name'), + 'value' => $query->createParameter('value'), + 'preferred' => $query->createParameter('preferred') + ] + ); + + foreach ($vCard->children as $property) { + if(!in_array($property->name, self::$indexProperties)) { + continue; + } + $preferred = 0; + foreach($property->parameters as $parameter) { + if ($parameter->name == 'TYPE' && strtoupper($parameter->getValue()) == 'PREF') { + $preferred = 1; + break; + } + } + $query->setParameter('name', $property->name); + $query->setParameter('value', substr($property->getValue(), 0, 254)); + $query->setParameter('preferred', $preferred); + $query->execute(); + } + } + + /** + * read vCard data into a vCard object + * + * @param string $cardData + * @return VCard + */ + protected function readCard($cardData) { + return Reader::read($cardData); + } + + /** + * delete all properties from a given card + * + * @param int $addressBookId + * @param int $cardId + */ + protected function purgeProperties($addressBookId, $cardId) { + $query = $this->db->getQueryBuilder(); + $query->delete($this->dbCardsPropertiesTable) + ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId))) + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); + $query->execute(); + } + + /** + * get ID from a given contact + * + * @param int $addressBookId + * @param string $uri + * @return int + */ + protected function getCardId($addressBookId, $uri) { + $query = $this->db->getQueryBuilder(); + $query->select('id')->from($this->dbCardsTable) + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) + ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); + + $result = $query->execute(); + $cardIds = $result->fetch(); + $result->closeCursor(); + + if (!isset($cardIds['id'])) { + throw new \InvalidArgumentException('Card does not exists: ' . $uri); + } + + return (int)$cardIds['id']; + } + + /** + * For shared address books the sharee is set in the ACL of the address book + * @param $addressBookId + * @param $acl + * @return array + */ + public function applyShareAcl($addressBookId, $acl) { + return $this->sharingBackend->applyShareAcl($addressBookId, $acl); + } + + private function convertPrincipal($principalUri, $toV2) { + if ($this->principalBackend->getPrincipalPrefix() === 'principals') { + list(, $name) = URLUtil::splitPath($principalUri); + if ($toV2 === true) { + return "principals/users/$name"; + } + return "principals/$name"; + } + return $principalUri; + } +} diff --git a/apps/dav/lib/CardDAV/ContactsManager.php b/apps/dav/lib/CardDAV/ContactsManager.php new file mode 100644 index 00000000000..7900c6ccae0 --- /dev/null +++ b/apps/dav/lib/CardDAV/ContactsManager.php @@ -0,0 +1,65 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CardDAV; + +use OCP\Contacts\IManager; + +class ContactsManager { + + /** + * ContactsManager constructor. + * + * @param CardDavBackend $backend + */ + public function __construct(CardDavBackend $backend) { + $this->backend = $backend; + } + + /** + * @param IManager $cm + * @param string $userId + */ + public function setupContactsProvider(IManager $cm, $userId) { + $addressBooks = $this->backend->getAddressBooksForUser("principals/users/$userId"); + $this->register($cm, $addressBooks); + $addressBooks = $this->backend->getAddressBooksForUser("principals/system/system"); + $this->register($cm, $addressBooks); + } + + /** + * @param IManager $cm + * @param $addressBooks + */ + private function register(IManager $cm, $addressBooks) { + foreach ($addressBooks as $addressBookInfo) { + $addressBook = new \OCA\DAV\CardDAV\AddressBook($this->backend, $addressBookInfo); + $cm->registerAddressBook( + new AddressBookImpl( + $addressBook, + $addressBookInfo, + $this->backend + ) + ); + } + } + +} diff --git a/apps/dav/lib/CardDAV/Converter.php b/apps/dav/lib/CardDAV/Converter.php new file mode 100644 index 00000000000..c8d9b94c267 --- /dev/null +++ b/apps/dav/lib/CardDAV/Converter.php @@ -0,0 +1,171 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CardDAV; + +use OCP\IImage; +use OCP\IUser; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; + +class Converter { + + /** + * @param IUser $user + * @return VCard + */ + public function createCardFromUser(IUser $user) { + + $uid = $user->getUID(); + $displayName = $user->getDisplayName(); + $displayName = empty($displayName ) ? $uid : $displayName; + $emailAddress = $user->getEMailAddress(); + $cloudId = $user->getCloudId(); + $image = $this->getAvatarImage($user); + + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'UID', $uid)); + if (!empty($displayName)) { + $vCard->add(new Text($vCard, 'FN', $displayName)); + $vCard->add(new Text($vCard, 'N', $this->splitFullName($displayName))); + } + if (!empty($emailAddress)) { + $vCard->add(new Text($vCard, 'EMAIL', $emailAddress, ['TYPE' => 'OTHER'])); + } + if (!empty($cloudId)) { + $vCard->add(new Text($vCard, 'CLOUD', $cloudId)); + } + if ($image) { + $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); + } + $vCard->validate(); + + return $vCard; + } + + /** + * @param VCard $vCard + * @param IUser $user + * @return bool + */ + public function updateCard(VCard $vCard, IUser $user) { + $uid = $user->getUID(); + $displayName = $user->getDisplayName(); + $displayName = empty($displayName ) ? $uid : $displayName; + $emailAddress = $user->getEMailAddress(); + $cloudId = $user->getCloudId(); + $image = $this->getAvatarImage($user); + + $updated = false; + if($this->propertyNeedsUpdate($vCard, 'FN', $displayName)) { + $vCard->FN = new Text($vCard, 'FN', $displayName); + unset($vCard->N); + $vCard->add(new Text($vCard, 'N', $this->splitFullName($displayName))); + $updated = true; + } + if($this->propertyNeedsUpdate($vCard, 'EMAIL', $emailAddress)) { + $vCard->EMAIL = new Text($vCard, 'EMAIL', $emailAddress); + $updated = true; + } + if($this->propertyNeedsUpdate($vCard, 'CLOUD', $cloudId)) { + $vCard->CLOUD = new Text($vCard, 'CLOUD', $cloudId); + $updated = true; + } + + if($this->propertyNeedsUpdate($vCard, 'PHOTO', $image)) { + $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); + $updated = true; + } + + if (empty($emailAddress) && !is_null($vCard->EMAIL)) { + unset($vCard->EMAIL); + $updated = true; + } + if (empty($cloudId) && !is_null($vCard->CLOUD)) { + unset($vCard->CLOUD); + $updated = true; + } + if (empty($image) && !is_null($vCard->PHOTO)) { + unset($vCard->PHOTO); + $updated = true; + } + + return $updated; + } + + /** + * @param VCard $vCard + * @param string $name + * @param string|IImage $newValue + * @return bool + */ + private function propertyNeedsUpdate(VCard $vCard, $name, $newValue) { + if (is_null($newValue)) { + return false; + } + $value = $vCard->__get($name); + if (!is_null($value)) { + $value = $value->getValue(); + $newValue = $newValue instanceof IImage ? $newValue->data() : $newValue; + + return $value !== $newValue; + } + return true; + } + + /** + * @param string $fullName + * @return string[] + */ + public function splitFullName($fullName) { + // Very basic western style parsing. I'm not gonna implement + // https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;) + + $elements = explode(' ', $fullName); + $result = ['', '', '', '', '']; + if (count($elements) > 2) { + $result[0] = implode(' ', array_slice($elements, count($elements)-1)); + $result[1] = $elements[0]; + $result[2] = implode(' ', array_slice($elements, 1, count($elements)-2)); + } elseif (count($elements) === 2) { + $result[0] = $elements[1]; + $result[1] = $elements[0]; + } else { + $result[0] = $elements[0]; + } + + return $result; + } + + /** + * @param IUser $user + * @return null|IImage + */ + private function getAvatarImage(IUser $user) { + try { + $image = $user->getAvatarImage(-1); + return $image; + } catch (\Exception $ex) { + return null; + } + } + +} diff --git a/apps/dav/lib/CardDAV/Plugin.php b/apps/dav/lib/CardDAV/Plugin.php new file mode 100644 index 00000000000..e02cc5686b8 --- /dev/null +++ b/apps/dav/lib/CardDAV/Plugin.php @@ -0,0 +1,79 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CardDAV; + +use OCA\DAV\CardDAV\Xml\Groups; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\HTTP\URLUtil; + +class Plugin extends \Sabre\CardDAV\Plugin { + + function initialize(Server $server) { + $server->on('propFind', [$this, 'propFind']); + parent::initialize($server); + } + + /** + * Returns the addressbook home for a given principal + * + * @param string $principal + * @return string + */ + protected function getAddressbookHomeForPrincipal($principal) { + + if (strrpos($principal, 'principals/users', -strlen($principal)) !== false) { + list(, $principalId) = URLUtil::splitPath($principal); + return self::ADDRESSBOOK_ROOT . '/users/' . $principalId; + } + if (strrpos($principal, 'principals/groups', -strlen($principal)) !== false) { + list(, $principalId) = URLUtil::splitPath($principal); + return self::ADDRESSBOOK_ROOT . '/groups/' . $principalId; + } + if (strrpos($principal, 'principals/system', -strlen($principal)) !== false) { + list(, $principalId) = URLUtil::splitPath($principal); + return self::ADDRESSBOOK_ROOT . '/system/' . $principalId; + } + + throw new \LogicException('This is not supposed to happen'); + } + + /** + * Adds all CardDAV-specific properties + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + + $ns = '{http://owncloud.org/ns}'; + + if ($node instanceof AddressBook) { + + $propFind->handle($ns . 'groups', function () use ($node) { + return new Groups($node->getContactsGroups()); + }); + } + } +} diff --git a/apps/dav/lib/CardDAV/SyncJob.php b/apps/dav/lib/CardDAV/SyncJob.php new file mode 100644 index 00000000000..b6748c05f5c --- /dev/null +++ b/apps/dav/lib/CardDAV/SyncJob.php @@ -0,0 +1,40 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CardDAV; + +use OC\BackgroundJob\TimedJob; +use OCA\DAV\AppInfo\Application; + +class SyncJob extends TimedJob { + + public function __construct() { + // Run once a day + $this->setInterval(24 * 60 * 60); + } + + protected function run($argument) { + $app = new Application(); + /** @var SyncService $ss */ + $ss = $app->getSyncService(); + $ss->syncInstance(); + } +} diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php new file mode 100644 index 00000000000..c4741a01772 --- /dev/null +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -0,0 +1,284 @@ + + * @author Roeland Jago Douma + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\CardDAV; + +use OCP\AppFramework\Http; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserManager; +use Sabre\DAV\Client; +use Sabre\DAV\Xml\Response\MultiStatus; +use Sabre\DAV\Xml\Service; +use Sabre\HTTP\ClientHttpException; +use Sabre\VObject\Reader; + +class SyncService { + + /** @var CardDavBackend */ + private $backend; + + /** @var IUserManager */ + private $userManager; + + /** @var ILogger */ + private $logger; + + /** @var array */ + private $localSystemAddressBook; + + public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger) { + $this->backend = $backend; + $this->userManager = $userManager; + $this->logger = $logger; + } + + /** + * @param string $url + * @param string $userName + * @param string $sharedSecret + * @param string $syncToken + * @param int $targetBookId + * @param string $targetPrincipal + * @param array $targetProperties + * @return string + * @throws \Exception + */ + public function syncRemoteAddressBook($url, $userName, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) { + // 1. create addressbook + $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookId, $targetProperties); + $addressBookId = $book['id']; + + // 2. query changes + try { + $response = $this->requestSyncReport($url, $userName, $sharedSecret, $syncToken); + } catch (ClientHttpException $ex) { + if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) { + // remote server revoked access to the address book, remove it + $this->backend->deleteAddressBook($addressBookId); + $this->logger->info('Authorization failed, remove address book: ' . $url, ['app' => 'dav']); + throw $ex; + } + } + + // 3. apply changes + // TODO: use multi-get for download + foreach ($response['response'] as $resource => $status) { + $cardUri = basename($resource); + if (isset($status[200])) { + $vCard = $this->download($url, $sharedSecret, $resource); + $existingCard = $this->backend->getCard($addressBookId, $cardUri); + if ($existingCard === false) { + $this->backend->createCard($addressBookId, $cardUri, $vCard['body']); + } else { + $this->backend->updateCard($addressBookId, $cardUri, $vCard['body']); + } + } else { + $this->backend->deleteCard($addressBookId, $cardUri); + } + } + + return $response['token']; + } + + /** + * @param string $principal + * @param string $id + * @param array $properties + * @return array|null + * @throws \Sabre\DAV\Exception\BadRequest + */ + public function ensureSystemAddressBookExists($principal, $id, $properties) { + $book = $this->backend->getAddressBooksByUri($principal, $id); + if (!is_null($book)) { + return $book; + } + $this->backend->createAddressBook($principal, $id, $properties); + + return $this->backend->getAddressBooksByUri($principal, $id); + } + + /** + * @param string $url + * @param string $userName + * @param string $sharedSecret + * @param string $syncToken + * @return array + */ + protected function requestSyncReport($url, $userName, $sharedSecret, $syncToken) { + $settings = [ + 'baseUri' => $url . '/', + 'userName' => $userName, + 'password' => $sharedSecret, + ]; + $client = new Client($settings); + $client->setThrowExceptions(true); + + $addressBookUrl = "remote.php/dav/addressbooks/system/system/system"; + $body = $this->buildSyncCollectionRequestBody($syncToken); + + $response = $client->request('REPORT', $addressBookUrl, $body, [ + 'Content-Type' => 'application/xml' + ]); + + $result = $this->parseMultiStatus($response['body']); + + return $result; + } + + /** + * @param string $url + * @param string $sharedSecret + * @param string $resourcePath + * @return array + */ + protected function download($url, $sharedSecret, $resourcePath) { + $settings = [ + 'baseUri' => $url, + 'userName' => 'system', + 'password' => $sharedSecret, + ]; + $client = new Client($settings); + $client->setThrowExceptions(true); + + $response = $client->request('GET', $resourcePath); + return $response; + } + + /** + * @param string|null $syncToken + * @return string + */ + private function buildSyncCollectionRequestBody($syncToken) { + + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $root = $dom->createElementNS('DAV:', 'd:sync-collection'); + $sync = $dom->createElement('d:sync-token', $syncToken); + $prop = $dom->createElement('d:prop'); + $cont = $dom->createElement('d:getcontenttype'); + $etag = $dom->createElement('d:getetag'); + + $prop->appendChild($cont); + $prop->appendChild($etag); + $root->appendChild($sync); + $root->appendChild($prop); + $dom->appendChild($root); + $body = $dom->saveXML(); + + return $body; + } + + /** + * @param string $body + * @return array + * @throws \Sabre\Xml\ParseException + */ + private function parseMultiStatus($body) { + $xml = new Service(); + + /** @var MultiStatus $multiStatus */ + $multiStatus = $xml->expect('{DAV:}multistatus', $body); + + $result = []; + foreach ($multiStatus->getResponses() as $response) { + $result[$response->getHref()] = $response->getResponseProperties(); + } + + return ['response' => $result, 'token' => $multiStatus->getSyncToken()]; + } + + /** + * @param IUser $user + */ + public function updateUser($user) { + $systemAddressBook = $this->getLocalSystemAddressBook(); + $addressBookId = $systemAddressBook['id']; + $converter = new Converter(); + $name = $user->getBackendClassName(); + $userId = $user->getUID(); + + $cardId = "$name:$userId.vcf"; + $card = $this->backend->getCard($addressBookId, $cardId); + if ($card === false) { + $vCard = $converter->createCardFromUser($user); + $this->backend->createCard($addressBookId, $cardId, $vCard->serialize()); + } else { + $vCard = Reader::read($card['carddata']); + if ($converter->updateCard($vCard, $user)) { + $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize()); + } + } + } + + /** + * @param IUser|string $userOrCardId + */ + public function deleteUser($userOrCardId) { + $systemAddressBook = $this->getLocalSystemAddressBook(); + if ($userOrCardId instanceof IUser){ + $name = $userOrCardId->getBackendClassName(); + $userId = $userOrCardId->getUID(); + + $userOrCardId = "$name:$userId.vcf"; + } + $this->backend->deleteCard($systemAddressBook['id'], $userOrCardId); + } + + /** + * @return array|null + */ + public function getLocalSystemAddressBook() { + if (is_null($this->localSystemAddressBook)) { + $systemPrincipal = "principals/system/system"; + $this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [ + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance' + ]); + } + + return $this->localSystemAddressBook; + } + + public function syncInstance(\Closure $progressCallback = null) { + $systemAddressBook = $this->getLocalSystemAddressBook(); + $this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $progressCallback) { + $this->updateUser($user); + if (!is_null($progressCallback)) { + $progressCallback(); + } + }); + + // remove no longer existing + $allCards = $this->backend->getCards($systemAddressBook['id']); + foreach($allCards as $card) { + $vCard = Reader::read($card['carddata']); + $uid = $vCard->UID->getValue(); + // load backend and see if user exists + if (!$this->userManager->userExists($uid)) { + $this->deleteUser($card['uri']); + } + } + } + + +} diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php new file mode 100644 index 00000000000..734e3829e69 --- /dev/null +++ b/apps/dav/lib/CardDAV/UserAddressBooks.php @@ -0,0 +1,67 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CardDAV; + +class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { + + /** + * Returns a list of addressbooks + * + * @return array + */ + function getChildren() { + + $addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); + $objects = []; + foreach($addressBooks as $addressBook) { + $objects[] = new AddressBook($this->carddavBackend, $addressBook); + } + return $objects; + + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + function getACL() { + + $acl = parent::getACL(); + if ($this->principalUri === 'principals/system/system') { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + } + + return $acl; + } + +} diff --git a/apps/dav/lib/CardDAV/Xml/Groups.php b/apps/dav/lib/CardDAV/Xml/Groups.php new file mode 100644 index 00000000000..b39615db033 --- /dev/null +++ b/apps/dav/lib/CardDAV/Xml/Groups.php @@ -0,0 +1,45 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CardDAV\Xml; + +use Sabre\Xml\XmlSerializable; +use Sabre\Xml\Element; +use Sabre\Xml\Writer; + +class Groups implements XmlSerializable { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** @var string[] of TYPE:CHECKSUM */ + private $groups; + + /** + * @param string $groups + */ + public function __construct($groups) { + $this->groups = $groups; + } + + function xmlSerialize(Writer $writer) { + foreach ($this->groups as $group) { + $writer->writeElement('{' . self::NS_OWNCLOUD . '}group', $group); + } + } +} diff --git a/apps/dav/lib/Command/CreateAddressBook.php b/apps/dav/lib/Command/CreateAddressBook.php new file mode 100644 index 00000000000..48302a2b439 --- /dev/null +++ b/apps/dav/lib/Command/CreateAddressBook.php @@ -0,0 +1,77 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Connector\Sabre\Principal; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\ILogger; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class CreateAddressBook extends Command { + + /** @var IUserManager */ + private $userManager; + + /** @var CardDavBackend */ + private $cardDavBackend; + + /** + * @param IUserManager $userManager + * @param CardDavBackend $cardDavBackend + */ + function __construct(IUserManager $userManager, + CardDavBackend $cardDavBackend + ) { + parent::__construct(); + $this->userManager = $userManager; + $this->cardDavBackend = $cardDavBackend; + } + + protected function configure() { + $this + ->setName('dav:create-addressbook') + ->setDescription('Create a dav addressbook') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the addressbook will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the addressbook'); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + + $name = $input->getArgument('name'); + $this->cardDavBackend->createAddressBook("principals/users/$user", $name, []); + } +} diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php new file mode 100644 index 00000000000..d7f82dd0e52 --- /dev/null +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -0,0 +1,81 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Connector\Sabre\Principal; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class CreateCalendar extends Command { + + /** @var IUserManager */ + protected $userManager; + + /** @var IGroupManager $groupManager */ + private $groupManager; + + /** @var \OCP\IDBConnection */ + protected $dbConnection; + + /** + * @param IUserManager $userManager + * @param IDBConnection $dbConnection + */ + function __construct(IUserManager $userManager, IGroupManager $groupManager, IDBConnection $dbConnection) { + parent::__construct(); + $this->userManager = $userManager; + $this->groupManager = $groupManager; + $this->dbConnection = $dbConnection; + } + + protected function configure() { + $this + ->setName('dav:create-calendar') + ->setDescription('Create a dav calendar') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the calendar will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the calendar'); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + $principalBackend = new Principal( + $this->userManager, + $this->groupManager + ); + + $name = $input->getArgument('name'); + $caldav = new CalDavBackend($this->dbConnection, $principalBackend); + $caldav->createCalendar("principals/users/$user", $name, []); + } +} diff --git a/apps/dav/lib/Command/SyncBirthdayCalendar.php b/apps/dav/lib/Command/SyncBirthdayCalendar.php new file mode 100644 index 00000000000..90a73a3eeb3 --- /dev/null +++ b/apps/dav/lib/Command/SyncBirthdayCalendar.php @@ -0,0 +1,85 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\BirthdayService; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SyncBirthdayCalendar extends Command { + + /** @var BirthdayService */ + private $birthdayService; + + /** @var IUserManager */ + private $userManager; + + /** + * @param IUserManager $userManager + * @param BirthdayService $birthdayService + */ + function __construct(IUserManager $userManager, BirthdayService $birthdayService) { + parent::__construct(); + $this->birthdayService = $birthdayService; + $this->userManager = $userManager; + } + + protected function configure() { + $this + ->setName('dav:sync-birthday-calendar') + ->setDescription('Synchronizes the birthday calendar') + ->addArgument('user', + InputArgument::OPTIONAL, + 'User for whom the birthday calendar will be synchronized'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $user = $input->getArgument('user'); + if (!is_null($user)) { + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + $output->writeln("Start birthday calendar sync for $user"); + $this->birthdayService->syncUser($user); + return; + } + $output->writeln("Start birthday calendar sync for all users ..."); + $p = new ProgressBar($output); + $p->start(); + $this->userManager->callForAllUsers(function($user) use ($p) { + $p->advance(); + /** @var IUser $user */ + $this->birthdayService->syncUser($user->getUID()); + }); + + $p->finish(); + $output->writeln(''); + } +} diff --git a/apps/dav/lib/Command/SyncSystemAddressBook.php b/apps/dav/lib/Command/SyncSystemAddressBook.php new file mode 100644 index 00000000000..b62a42d7b90 --- /dev/null +++ b/apps/dav/lib/Command/SyncSystemAddressBook.php @@ -0,0 +1,65 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CardDAV\SyncService; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SyncSystemAddressBook extends Command { + + /** @var SyncService */ + private $syncService; + + /** + * @param SyncService $syncService + */ + function __construct(SyncService $syncService) { + parent::__construct(); + $this->syncService = $syncService; + } + + protected function configure() { + $this + ->setName('dav:sync-system-addressbook') + ->setDescription('Synchronizes users to the system addressbook'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $output->writeln('Syncing users ...'); + $progress = new ProgressBar($output); + $progress->start(); + $this->syncService->syncInstance(function() use ($progress) { + $progress->advance(); + }); + + $progress->finish(); + $output->writeln(''); + } +} diff --git a/apps/dav/lib/Comments/CommentNode.php b/apps/dav/lib/Comments/CommentNode.php new file mode 100644 index 00000000000..339abc6382d --- /dev/null +++ b/apps/dav/lib/Comments/CommentNode.php @@ -0,0 +1,261 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Comments; + + +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; +use OCP\Comments\MessageTooLongException; +use OCP\ILogger; +use OCP\IUserManager; +use OCP\IUserSession; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\PropPatch; + +class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}isUnread'; + const PROPERTY_NAME_MESSAGE = '{http://owncloud.org/ns}message'; + const PROPERTY_NAME_ACTOR_DISPLAYNAME = '{http://owncloud.org/ns}actorDisplayName'; + + /** @var IComment */ + public $comment; + + /** @var ICommentsManager */ + protected $commentsManager; + + /** @var ILogger */ + protected $logger; + + /** @var array list of properties with key being their name and value their setter */ + protected $properties = []; + + /** @var IUserManager */ + protected $userManager; + + /** @var IUserSession */ + protected $userSession; + + /** + * CommentNode constructor. + * + * @param ICommentsManager $commentsManager + * @param IComment $comment + * @param IUserManager $userManager + * @param IUserSession $userSession + * @param ILogger $logger + */ + public function __construct( + ICommentsManager $commentsManager, + IComment $comment, + IUserManager $userManager, + IUserSession $userSession, + ILogger $logger + ) { + $this->commentsManager = $commentsManager; + $this->comment = $comment; + $this->logger = $logger; + + $methods = get_class_methods($this->comment); + $methods = array_filter($methods, function($name){ + return strpos($name, 'get') === 0; + }); + foreach($methods as $getter) { + $name = '{'.self::NS_OWNCLOUD.'}' . lcfirst(substr($getter, 3)); + $this->properties[$name] = $getter; + } + $this->userManager = $userManager; + $this->userSession = $userSession; + } + + /** + * returns a list of all possible property names + * + * @return array + */ + static public function getPropertyNames() { + return [ + '{http://owncloud.org/ns}id', + '{http://owncloud.org/ns}parentId', + '{http://owncloud.org/ns}topmostParentId', + '{http://owncloud.org/ns}childrenCount', + '{http://owncloud.org/ns}verb', + '{http://owncloud.org/ns}actorType', + '{http://owncloud.org/ns}actorId', + '{http://owncloud.org/ns}creationDateTime', + '{http://owncloud.org/ns}latestChildDateTime', + '{http://owncloud.org/ns}objectType', + '{http://owncloud.org/ns}objectId', + // re-used property names are defined as constants + self::PROPERTY_NAME_MESSAGE, + self::PROPERTY_NAME_ACTOR_DISPLAYNAME, + self::PROPERTY_NAME_UNREAD + ]; + } + + protected function checkWriteAccessOnComment() { + $user = $this->userSession->getUser(); + if( $this->comment->getActorType() !== 'users' + || is_null($user) + || $this->comment->getActorId() !== $user->getUID() + ) { + throw new Forbidden('Only authors are allowed to edit their comment.'); + } + } + + /** + * Deleted the current node + * + * @return void + */ + function delete() { + $this->checkWriteAccessOnComment(); + $this->commentsManager->delete($this->comment->getId()); + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + return $this->comment->getId(); + } + + /** + * Renames the node + * + * @param string $name The new name + * @throws MethodNotAllowed + */ + function setName($name) { + throw new MethodNotAllowed(); + } + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int + */ + function getLastModified() { + return null; + } + + /** + * update the comment's message + * + * @param $propertyValue + * @return bool + * @throws BadRequest + * @throws Forbidden + */ + public function updateComment($propertyValue) { + $this->checkWriteAccessOnComment(); + try { + $this->comment->setMessage($propertyValue); + $this->commentsManager->save($this->comment); + return true; + } catch (\Exception $e) { + $this->logger->logException($e, ['app' => 'dav/comments']); + if($e instanceof MessageTooLongException) { + $msg = 'Message exceeds allowed character limit of '; + throw new BadRequest($msg . IComment::MAX_MESSAGE_LENGTH, 0, $e); + } + return false; + } + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + * + * @param PropPatch $propPatch + * @return void + */ + function propPatch(PropPatch $propPatch) { + // other properties than 'message' are read only + $propPatch->handle(self::PROPERTY_NAME_MESSAGE, [$this, 'updateComment']); + } + + /** + * Returns a list of properties for this nodes. + * + * The properties list is a list of propertynames the client requested, + * encoded in clark-notation {xmlnamespace}tagname + * + * If the array is empty, it means 'all properties' were requested. + * + * Note that it's fine to liberally give properties back, instead of + * conforming to the list of requested properties. + * The Server class will filter out the extra. + * + * @param array $properties + * @return array + */ + function getProperties($properties) { + $properties = array_keys($this->properties); + + $result = []; + foreach($properties as $property) { + $getter = $this->properties[$property]; + if(method_exists($this->comment, $getter)) { + $result[$property] = $this->comment->$getter(); + } + } + + if($this->comment->getActorType() === 'users') { + $user = $this->userManager->get($this->comment->getActorId()); + $displayName = is_null($user) ? null : $user->getDisplayName(); + $result[self::PROPERTY_NAME_ACTOR_DISPLAYNAME] = $displayName; + } + + $unread = null; + $user = $this->userSession->getUser(); + if(!is_null($user)) { + $readUntil = $this->commentsManager->getReadMark( + $this->comment->getObjectType(), + $this->comment->getObjectId(), + $user + ); + if(is_null($readUntil)) { + $unread = 'true'; + } else { + $unread = $this->comment->getCreationDateTime() > $readUntil; + // re-format for output + $unread = $unread ? 'true' : 'false'; + } + } + $result[self::PROPERTY_NAME_UNREAD] = $unread; + + return $result; + } +} diff --git a/apps/dav/lib/Comments/CommentsPlugin.php b/apps/dav/lib/Comments/CommentsPlugin.php new file mode 100644 index 00000000000..d4a065649ef --- /dev/null +++ b/apps/dav/lib/Comments/CommentsPlugin.php @@ -0,0 +1,255 @@ + + * @author Joas Schilling + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Comments; + +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; +use OCP\IUserSession; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\ReportNotSupported; +use Sabre\DAV\Exception\UnsupportedMediaType; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Xml\Element\Response; +use Sabre\DAV\Xml\Response\MultiStatus; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\Xml\Writer; + +/** + * Sabre plugin to handle comments: + */ +class CommentsPlugin extends ServerPlugin { + // namespace + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + const REPORT_NAME = '{http://owncloud.org/ns}filter-comments'; + const REPORT_PARAM_LIMIT = '{http://owncloud.org/ns}limit'; + const REPORT_PARAM_OFFSET = '{http://owncloud.org/ns}offset'; + const REPORT_PARAM_TIMESTAMP = '{http://owncloud.org/ns}datetime'; + + /** @var ICommentsManager */ + protected $commentsManager; + + /** @var \Sabre\DAV\Server $server */ + private $server; + + /** @var \OCP\IUserSession */ + protected $userSession; + + /** + * Comments plugin + * + * @param ICommentsManager $commentsManager + * @param IUserSession $userSession + */ + public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) { + $this->commentsManager = $commentsManager; + $this->userSession = $userSession; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + $this->server = $server; + if(strpos($this->server->getRequestUri(), 'comments/') !== 0) { + return; + } + + $this->server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; + + $this->server->xml->classMap['DateTime'] = function(Writer $writer, \DateTime $value) { + $writer->write(\Sabre\HTTP\toDate($value)); + }; + + $this->server->on('report', [$this, 'onReport']); + $this->server->on('method:POST', [$this, 'httpPost']); + } + + /** + * POST operation on Comments collections + * + * @param RequestInterface $request request object + * @param ResponseInterface $response response object + * @return null|false + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) { + $path = $request->getPath(); + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof EntityCollection) { + return null; + } + + $data = $request->getBodyAsString(); + $comment = $this->createComment( + $node->getName(), + $node->getId(), + $data, + $request->getHeader('Content-Type') + ); + + // update read marker for the current user/poster to avoid + // having their own comments marked as unread + $node->setReadMarker(null); + + $url = rtrim($request->getUrl(), '/') . '/' . urlencode($comment->getId()); + + $response->setHeader('Content-Location', $url); + + // created + $response->setStatus(201); + return false; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * + * @param string $uri + * @return array + */ + public function getSupportedReportSet($uri) { + return [self::REPORT_NAME]; + } + + /** + * REPORT operations to look for comments + * + * @param string $reportName + * @param [] $report + * @param string $uri + * @return bool + * @throws NotFound + * @throws ReportNotSupported + */ + public function onReport($reportName, $report, $uri) { + $node = $this->server->tree->getNodeForPath($uri); + if(!$node instanceof EntityCollection || $reportName !== self::REPORT_NAME) { + throw new ReportNotSupported(); + } + $args = ['limit' => 0, 'offset' => 0, 'datetime' => null]; + $acceptableParameters = [ + $this::REPORT_PARAM_LIMIT, + $this::REPORT_PARAM_OFFSET, + $this::REPORT_PARAM_TIMESTAMP + ]; + $ns = '{' . $this::NS_OWNCLOUD . '}'; + foreach($report as $parameter) { + if(!in_array($parameter['name'], $acceptableParameters) || empty($parameter['value'])) { + continue; + } + $args[str_replace($ns, '', $parameter['name'])] = $parameter['value']; + } + + if(!is_null($args['datetime'])) { + $args['datetime'] = new \DateTime($args['datetime']); + } + + $results = $node->findChildren($args['limit'], $args['offset'], $args['datetime']); + + $responses = []; + foreach($results as $node) { + $nodePath = $this->server->getRequestUri() . '/' . $node->comment->getId(); + $resultSet = $this->server->getPropertiesForPath($nodePath, CommentNode::getPropertyNames()); + if(isset($resultSet[0]) && isset($resultSet[0][200])) { + $responses[] = new Response( + $this->server->getBaseUri() . $nodePath, + [200 => $resultSet[0][200]], + 200 + ); + } + + } + + $xml = $this->server->xml->write( + '{DAV:}multistatus', + new MultiStatus($responses) + ); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody($xml); + + return false; + } + + /** + * Creates a new comment + * + * @param string $objectType e.g. "files" + * @param string $objectId e.g. the file id + * @param string $data JSON encoded string containing the properties of the tag to create + * @param string $contentType content type of the data + * @return IComment newly created comment + * + * @throws BadRequest if a field was missing + * @throws UnsupportedMediaType if the content type is not supported + */ + private function createComment($objectType, $objectId, $data, $contentType = 'application/json') { + if (explode(';', $contentType)[0] === 'application/json') { + $data = json_decode($data, true); + } else { + throw new UnsupportedMediaType(); + } + + $actorType = $data['actorType']; + $actorId = null; + if($actorType === 'users') { + $user = $this->userSession->getUser(); + if(!is_null($user)) { + $actorId = $user->getUID(); + } + } + if(is_null($actorId)) { + throw new BadRequest('Invalid actor "' . $actorType .'"'); + } + + try { + $comment = $this->commentsManager->create($actorType, $actorId, $objectType, $objectId); + $comment->setMessage($data['message']); + $comment->setVerb($data['verb']); + $this->commentsManager->save($comment); + return $comment; + } catch (\InvalidArgumentException $e) { + throw new BadRequest('Invalid input values', 0, $e); + } catch (\OCP\Comments\MessageTooLongException $e) { + $msg = 'Message exceeds allowed character limit of '; + throw new BadRequest($msg . \OCP\Comments\IComment::MAX_MESSAGE_LENGTH, 0, $e); + } + } + + + +} diff --git a/apps/dav/lib/Comments/EntityCollection.php b/apps/dav/lib/Comments/EntityCollection.php new file mode 100644 index 00000000000..a55a18c00c0 --- /dev/null +++ b/apps/dav/lib/Comments/EntityCollection.php @@ -0,0 +1,199 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Comments; + +use OCP\Comments\ICommentsManager; +use OCP\Files\Folder; +use OCP\ILogger; +use OCP\IUserManager; +use OCP\IUserSession; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\PropPatch; + +/** + * Class EntityCollection + * + * this represents a specific holder of comments, identified by an entity type + * (class member $name) and an entity id (class member $id). + * + * @package OCA\DAV\Comments + */ +class EntityCollection extends RootCollection implements \Sabre\DAV\IProperties { + const PROPERTY_NAME_READ_MARKER = '{http://owncloud.org/ns}readMarker'; + + /** @var Folder */ + protected $fileRoot; + + /** @var string */ + protected $id; + + /** @var ILogger */ + protected $logger; + + /** + * @param string $id + * @param string $name + * @param ICommentsManager $commentsManager + * @param Folder $fileRoot + * @param IUserManager $userManager + * @param IUserSession $userSession + * @param ILogger $logger + */ + public function __construct( + $id, + $name, + ICommentsManager $commentsManager, + Folder $fileRoot, + IUserManager $userManager, + IUserSession $userSession, + ILogger $logger + ) { + foreach(['id', 'name'] as $property) { + $$property = trim($$property); + if(empty($$property) || !is_string($$property)) { + throw new \InvalidArgumentException('"' . $property . '" parameter must be non-empty string'); + } + } + $this->id = $id; + $this->name = $name; + $this->commentsManager = $commentsManager; + $this->fileRoot = $fileRoot; + $this->logger = $logger; + $this->userManager = $userManager; + $this->userSession = $userSession; + } + + /** + * returns the ID of this entity + * + * @return string + */ + public function getId() { + return $this->id; + } + + /** + * Returns a specific child node, referenced by its name + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * @return \Sabre\DAV\INode + * @throws NotFound + */ + function getChild($name) { + try { + $comment = $this->commentsManager->get($name); + return new CommentNode( + $this->commentsManager, + $comment, + $this->userManager, + $this->userSession, + $this->logger + ); + } catch (\OCP\Comments\NotFoundException $e) { + throw new NotFound(); + } + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + function getChildren() { + return $this->findChildren(); + } + + /** + * Returns an array of comment nodes. Result can be influenced by offset, + * limit and date time parameters. + * + * @param int $limit + * @param int $offset + * @param \DateTime|null $datetime + * @return CommentNode[] + */ + function findChildren($limit = 0, $offset = 0, \DateTime $datetime = null) { + $comments = $this->commentsManager->getForObject($this->name, $this->id, $limit, $offset, $datetime); + $result = []; + foreach($comments as $comment) { + $result[] = new CommentNode( + $this->commentsManager, + $comment, + $this->userManager, + $this->userSession, + $this->logger + ); + } + return $result; + } + + /** + * Checks if a child-node with the specified name exists + * + * @param string $name + * @return bool + */ + function childExists($name) { + try { + $this->commentsManager->get($name); + return true; + } catch (\OCP\Comments\NotFoundException $e) { + return false; + } + } + + /** + * Sets the read marker to the specified date for the logged in user + * + * @param \DateTime $value + * @return bool + */ + public function setReadMarker($value) { + $dateTime = new \DateTime($value); + $user = $this->userSession->getUser(); + $this->commentsManager->setReadMark($this->name, $this->id, $dateTime, $user); + return true; + } + + /** + * @inheritdoc + */ + function propPatch(PropPatch $propPatch) { + $propPatch->handle(self::PROPERTY_NAME_READ_MARKER, [$this, 'setReadMarker']); + } + + /** + * @inheritdoc + */ + function getProperties($properties) { + $marker = null; + $user = $this->userSession->getUser(); + if(!is_null($user)) { + $marker = $this->commentsManager->getReadMark($this->name, $this->id, $user); + } + return [self::PROPERTY_NAME_READ_MARKER => $marker]; + } +} + diff --git a/apps/dav/lib/Comments/EntityTypeCollection.php b/apps/dav/lib/Comments/EntityTypeCollection.php new file mode 100644 index 00000000000..6bc42484207 --- /dev/null +++ b/apps/dav/lib/Comments/EntityTypeCollection.php @@ -0,0 +1,125 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Comments; + +use OCP\Comments\ICommentsManager; +use OCP\Files\Folder; +use OCP\ILogger; +use OCP\IUserManager; +use OCP\IUserSession; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\NotFound; + +/** + * Class EntityTypeCollection + * + * This is collection on the type of things a user can leave comments on, for + * example: 'files'. + * + * Its children are instances of EntityCollection (representing a specific + * object, for example the file by id). + * + * @package OCA\DAV\Comments + */ +class EntityTypeCollection extends RootCollection { + /** @var Folder */ + protected $fileRoot; + + /** @var ILogger */ + protected $logger; + + /** + * @param string $name + * @param ICommentsManager $commentsManager + * @param Folder $fileRoot + * @param IUserManager $userManager + * @param IUserSession $userSession + * @param ILogger $logger + */ + public function __construct( + $name, + ICommentsManager $commentsManager, + Folder $fileRoot, + IUserManager $userManager, + IUserSession $userSession, + ILogger $logger + ) { + $name = trim($name); + if(empty($name) || !is_string($name)) { + throw new \InvalidArgumentException('"name" parameter must be non-empty string'); + } + $this->name = $name; + $this->commentsManager = $commentsManager; + $this->fileRoot = $fileRoot; + $this->logger = $logger; + $this->userManager = $userManager; + $this->userSession = $userSession; + } + + /** + * Returns a specific child node, referenced by its name + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * @return \Sabre\DAV\INode + * @throws NotFound + */ + function getChild($name) { + if(!$this->childExists($name)) { + throw new NotFound('Entity does not exist or is not available'); + } + return new EntityCollection( + $name, + $this->name, + $this->commentsManager, + $this->fileRoot, + $this->userManager, + $this->userSession, + $this->logger + ); + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + * @throws MethodNotAllowed + */ + function getChildren() { + throw new MethodNotAllowed('No permission to list folder contents'); + } + + /** + * Checks if a child-node with the specified name exists + * + * @param string $name + * @return bool + */ + function childExists($name) { + $nodes = $this->fileRoot->getById(intval($name)); + return !empty($nodes); + } + + +} diff --git a/apps/dav/lib/Comments/RootCollection.php b/apps/dav/lib/Comments/RootCollection.php new file mode 100644 index 00000000000..cda666f7162 --- /dev/null +++ b/apps/dav/lib/Comments/RootCollection.php @@ -0,0 +1,205 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Comments; + +use OCP\Comments\ICommentsManager; +use OCP\Files\IRootFolder; +use OCP\ILogger; +use OCP\IUserManager; +use OCP\IUserSession; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; + +class RootCollection implements ICollection { + + /** @var EntityTypeCollection[] */ + private $entityTypeCollections = []; + + /** @var ICommentsManager */ + protected $commentsManager; + + /** @var string */ + protected $name = 'comments'; + + /** @var ILogger */ + protected $logger; + + /** @var IUserManager */ + protected $userManager; + /** + * @var IUserSession + */ + protected $userSession; + /** + * @var IRootFolder + */ + protected $rootFolder; + + /** + * @param ICommentsManager $commentsManager + * @param IUserManager $userManager + * @param IUserSession $userSession + * @param IRootFolder $rootFolder + * @param ILogger $logger + */ + public function __construct( + ICommentsManager $commentsManager, + IUserManager $userManager, + IUserSession $userSession, + IRootFolder $rootFolder, + ILogger $logger) + { + $this->commentsManager = $commentsManager; + $this->logger = $logger; + $this->userManager = $userManager; + $this->userSession = $userSession; + $this->rootFolder = $rootFolder; + } + + /** + * initializes the collection. At this point of time, we need the logged in + * user. Since it is not the case when the instance is created, we cannot + * have this in the constructor. + * + * @throws NotAuthenticated + */ + protected function initCollections() { + if(!empty($this->entityTypeCollections)) { + return; + } + $user = $this->userSession->getUser(); + if(is_null($user)) { + throw new NotAuthenticated(); + } + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $this->entityTypeCollections['files'] = new EntityTypeCollection( + 'files', + $this->commentsManager, + $userFolder, + $this->userManager, + $this->userSession, + $this->logger + ); + } + + /** + * Creates a new file in the directory + * + * @param string $name Name of the file + * @param resource|string $data Initial payload + * @return null|string + * @throws Forbidden + */ + function createFile($name, $data = null) { + throw new Forbidden('Cannot create comments by id'); + } + + /** + * Creates a new subdirectory + * + * @param string $name + * @throws Forbidden + */ + function createDirectory($name) { + throw new Forbidden('Permission denied to create collections'); + } + + /** + * Returns a specific child node, referenced by its name + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * @return \Sabre\DAV\INode + * @throws NotFound + */ + function getChild($name) { + $this->initCollections(); + if(isset($this->entityTypeCollections[$name])) { + return $this->entityTypeCollections[$name]; + } + throw new NotFound('Entity type "' . $name . '" not found."'); + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + function getChildren() { + $this->initCollections(); + return $this->entityTypeCollections; + } + + /** + * Checks if a child-node with the specified name exists + * + * @param string $name + * @return bool + */ + function childExists($name) { + $this->initCollections(); + return isset($this->entityTypeCollections[$name]); + } + + /** + * Deleted the current node + * + * @throws Forbidden + */ + function delete() { + throw new Forbidden('Permission denied to delete this collection'); + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + return $this->name; + } + + /** + * Renames the node + * + * @param string $name The new name + * @throws Forbidden + */ + function setName($name) { + throw new Forbidden('Permission denied to rename this collection'); + } + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int + */ + function getLastModified() { + return null; + } +} diff --git a/apps/dav/lib/Connector/LegacyDAVACL.php b/apps/dav/lib/Connector/LegacyDAVACL.php new file mode 100644 index 00000000000..eb6ca1fd7d5 --- /dev/null +++ b/apps/dav/lib/Connector/LegacyDAVACL.php @@ -0,0 +1,85 @@ + + * @author Lukas Reschke + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector; + +use OCA\DAV\Connector\Sabre\DavAclPlugin; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\HTTP\URLUtil; +use Sabre\DAVACL\Xml\Property\Principal; + +class LegacyDAVACL extends DavAclPlugin { + + /** + * Converts the v1 principal `principal/` to the new v2 + * `principal/users/` which is required for permission checks + * + * @inheritdoc + */ + function getCurrentUserPrincipal() { + $principalV1 = parent::getCurrentUserPrincipal(); + if (is_null($principalV1)) { + return $principalV1; + } + return $this->convertPrincipal($principalV1, true); + } + + + /** + * @inheritdoc + */ + function getCurrentUserPrincipals() { + $principalV2 = $this->getCurrentUserPrincipal(); + + if (is_null($principalV2)) return []; + + $principalV1 = $this->convertPrincipal($principalV2, false); + return array_merge( + [ + $principalV2, + $principalV1 + ], + $this->getPrincipalMembership($principalV1) + ); + } + + private function convertPrincipal($principal, $toV2) { + list(, $name) = URLUtil::splitPath($principal); + if ($toV2) { + return "principals/users/$name"; + } + return "principals/$name"; + } + + function propFind(PropFind $propFind, INode $node) { + /* Overload current-user-principal */ + $propFind->handle('{DAV:}current-user-principal', function () { + if ($url = parent::getCurrentUserPrincipal()) { + return new Principal(Principal::HREF, $url . '/'); + } else { + return new Principal(Principal::UNAUTHENTICATED); + } + }); + parent::propFind($propFind, $node); + } +} diff --git a/apps/dav/lib/Connector/PublicAuth.php b/apps/dav/lib/Connector/PublicAuth.php new file mode 100644 index 00000000000..3aa58cda244 --- /dev/null +++ b/apps/dav/lib/Connector/PublicAuth.php @@ -0,0 +1,122 @@ + + * @author Lukas Reschke + * @author Morris Jobke + * @author Robin Appelman + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector; + +use OCP\IRequest; +use OCP\ISession; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; + +/** + * Class PublicAuth + * + * @package OCA\DAV\Connector + */ +class PublicAuth extends \Sabre\DAV\Auth\Backend\AbstractBasic { + + /** @var \OCP\Share\IShare */ + private $share; + + /** @var IManager */ + private $shareManager; + + /** @var ISession */ + private $session; + + /** @var IRequest */ + private $request; + + /** + * @param IRequest $request + * @param IManager $shareManager + * @param ISession $session + */ + public function __construct(IRequest $request, + IManager $shareManager, + ISession $session) { + $this->request = $request; + $this->shareManager = $shareManager; + $this->session = $session; + } + + /** + * 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 + * @throws \Sabre\DAV\Exception\NotAuthenticated + */ + protected function validateUserPass($username, $password) { + try { + $share = $this->shareManager->getShareByToken($username); + } catch (ShareNotFound $e) { + return false; + } + + $this->share = $share; + + \OC_User::setIncognitoMode(true); + + // check if the share is password protected + if ($share->getPassword() !== null) { + if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) { + if ($this->shareManager->checkPassword($share, $password)) { + return true; + } else if ($this->session->exists('public_link_authenticated') + && $this->session->get('public_link_authenticated') === $share->getId()) { + return true; + } else { + if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) { + // do not re-authenticate over ajax, use dummy auth name to prevent browser popup + http_response_code(401); + header('WWW-Authenticate', 'DummyBasic real="ownCloud"'); + throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); + } + return false; + } + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_REMOTE) { + return true; + } else { + return false; + } + } else { + return true; + } + } + + /** + * @return \OCP\Share\IShare + */ + public function getShare() { + return $this->share; + } +} diff --git a/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php b/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php new file mode 100644 index 00000000000..cb061d6a309 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/AppEnabledPlugin.php @@ -0,0 +1,90 @@ + + * @author Robin Appelman + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCP\App\IAppManager; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\ServerPlugin; + +/** + * Plugin to check if an app is enabled for the current user + */ +class AppEnabledPlugin extends ServerPlugin { + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @var string + */ + private $app; + + /** + * @var \OCP\App\IAppManager + */ + private $appManager; + + /** + * @param string $app + * @param \OCP\App\IAppManager $appManager + */ + public function __construct($app, IAppManager $appManager) { + $this->app = $app; + $this->appManager = $appManager; + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + + $this->server = $server; + $this->server->on('beforeMethod', array($this, 'checkAppEnabled'), 30); + } + + /** + * This method is called before any HTTP after auth and checks if the user has access to the app + * + * @throws \Sabre\DAV\Exception\Forbidden + * @return bool + */ + public function checkAppEnabled() { + if (!$this->appManager->isEnabledForUser($this->app)) { + throw new Forbidden(); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php new file mode 100644 index 00000000000..b8047e779f5 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Auth.php @@ -0,0 +1,229 @@ + + * @author Bart Visscher + * @author Jakob Sack + * @author Lukas Reschke + * @author Markus Goetz + * @author Michael Gapczynski + * @author Morris Jobke + * @author Roeland Jago Douma + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Connector\Sabre; + +use Exception; +use OC\AppFramework\Http\Request; +use OCP\IRequest; +use OCP\ISession; +use OC\User\Session; +use Sabre\DAV\Auth\Backend\AbstractBasic; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAV\Exception\ServiceUnavailable; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class Auth extends AbstractBasic { + const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND'; + + /** @var ISession */ + private $session; + /** @var Session */ + private $userSession; + /** @var IRequest */ + private $request; + /** @var string */ + private $currentUser; + + /** + * @param ISession $session + * @param Session $userSession + * @param IRequest $request + * @param string $principalPrefix + */ + public function __construct(ISession $session, + Session $userSession, + IRequest $request, + $principalPrefix = 'principals/users/') { + $this->session = $session; + $this->userSession = $userSession; + $this->request = $request; + $this->principalPrefix = $principalPrefix; + } + + /** + * Whether the user has initially authenticated via DAV + * + * This is required for WebDAV clients that resent the cookies even when the + * account was changed. + * + * @see https://github.com/owncloud/core/issues/13245 + * + * @param string $username + * @return bool + */ + public function isDavAuthenticated($username) { + return !is_null($this->session->get(self::DAV_AUTHENTICATED)) && + $this->session->get(self::DAV_AUTHENTICATED) === $username; + } + + /** + * Validates a username and password + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $username + * @param string $password + * @return bool + */ + protected function validateUserPass($username, $password) { + if ($this->userSession->isLoggedIn() && + $this->isDavAuthenticated($this->userSession->getUser()->getUID()) + ) { + \OC_Util::setupFS($this->userSession->getUser()->getUID()); + $this->session->close(); + return true; + } else { + \OC_Util::setUpFS(); //login hooks may need early access to the filesystem + if($this->userSession->login($username, $password)) { + $this->userSession->createSessionToken($this->request, $username, $password); + \OC_Util::setUpFS($this->userSession->getUser()->getUID()); + $this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID()); + $this->session->close(); + return true; + } else { + $this->session->close(); + return false; + } + } + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + * @throws NotAuthenticated + * @throws ServiceUnavailable + */ + function check(RequestInterface $request, ResponseInterface $response) { + try { + $result = $this->auth($request, $response); + return $result; + } catch (NotAuthenticated $e) { + throw $e; + } catch (Exception $e) { + $class = get_class($e); + $msg = $e->getMessage(); + throw new ServiceUnavailable("$class: $msg"); + } + } + + /** + * Checks whether a CSRF check is required on the request + * + * @return bool + */ + private function requiresCSRFCheck() { + // GET requires no check at all + if($this->request->getMethod() === 'GET') { + return false; + } + + // Official ownCloud clients require no checks + if($this->request->isUserAgent([ + Request::USER_AGENT_OWNCLOUD_DESKTOP, + Request::USER_AGENT_OWNCLOUD_ANDROID, + Request::USER_AGENT_OWNCLOUD_IOS, + ])) { + return false; + } + + // If not logged-in no check is required + if(!$this->userSession->isLoggedIn()) { + return false; + } + + // POST always requires a check + if($this->request->getMethod() === 'POST') { + return true; + } + + // If logged-in AND DAV authenticated no check is required + if($this->userSession->isLoggedIn() && + $this->isDavAuthenticated($this->userSession->getUser()->getUID())) { + return false; + } + + return true; + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + * @throws NotAuthenticated + */ + private function auth(RequestInterface $request, ResponseInterface $response) { + $forcedLogout = false; + if(!$this->request->passesCSRFCheck() && + $this->requiresCSRFCheck()) { + // In case of a fail with POST we need to recheck the credentials + if($this->request->getMethod() === 'POST') { + $forcedLogout = true; + } else { + $response->setStatus(401); + throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.'); + } + } + + if($forcedLogout) { + $this->userSession->logout(); + } else { + if (\OC_User::handleApacheAuth() || + //Fix for broken webdav clients + ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) || + //Well behaved clients that only send the cookie are allowed + ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null) + ) { + $user = $this->userSession->getUser()->getUID(); + \OC_Util::setupFS($user); + $this->currentUser = $user; + $this->session->close(); + return [true, $this->principalPrefix . $user]; + } + } + + if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With')))) { + // do not re-authenticate over ajax, use dummy auth name to prevent browser popup + $response->addHeader('WWW-Authenticate','DummyBasic realm="' . $this->realm . '"'); + $response->setStatus(401); + throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); + } + + $data = parent::check($request, $response); + if($data[0] === true) { + $startPos = strrpos($data[1], '/') + 1; + $user = $this->userSession->getUser()->getUID(); + $data[1] = substr_replace($data[1], $user, $startPos); + } + return $data; + } +} diff --git a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php new file mode 100644 index 00000000000..70d19cb7f2a --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php @@ -0,0 +1,80 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCP\IConfig; +use Sabre\HTTP\RequestInterface; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Exception; + +/** + * Class BlockLegacyClientPlugin is used to detect old legacy sync clients and + * returns a 403 status to those clients + * + * @package OCA\DAV\Connector\Sabre + */ +class BlockLegacyClientPlugin extends ServerPlugin { + /** @var \Sabre\DAV\Server */ + protected $server; + /** @var IConfig */ + protected $config; + + /** + * @param IConfig $config + */ + public function __construct(IConfig $config) { + $this->config = $config; + } + + /** + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('beforeMethod', [$this, 'beforeHandler'], 200); + } + + /** + * Detects all unsupported clients and throws a \Sabre\DAV\Exception\Forbidden + * exception which will result in a 403 to them. + * @param RequestInterface $request + * @throws \Sabre\DAV\Exception\Forbidden If the client version is not supported + */ + public function beforeHandler(RequestInterface $request) { + $userAgent = $request->getHeader('User-Agent'); + if($userAgent === null) { + return; + } + + $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '1.7.0'); + + // Match on the mirall version which is in scheme "Mozilla/5.0 (%1) mirall/%2" or + // "mirall/%1" for older releases + preg_match("/(?:mirall\\/)([\d.]+)/i", $userAgent, $versionMatches); + if(isset($versionMatches[1]) && + version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) { + throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.'); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/ChecksumList.php b/apps/dav/lib/Connector/Sabre/ChecksumList.php new file mode 100644 index 00000000000..f137222acca --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ChecksumList.php @@ -0,0 +1,71 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Connector\Sabre; + +use Sabre\Xml\XmlSerializable; +use Sabre\Xml\Element; +use Sabre\Xml\Writer; + +/** + * Checksumlist property + * + * This property contains multiple "checksum" elements, each containing a + * checksum name. + */ +class ChecksumList implements XmlSerializable { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** @var string[] of TYPE:CHECKSUM */ + private $checksums; + + /** + * @param string $checksum + */ + public function __construct($checksum) { + $this->checksums = explode(',', $checksum); + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->checksums as $checksum) { + $writer->writeElement('{' . self::NS_OWNCLOUD . '}checksum', $checksum); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php new file mode 100644 index 00000000000..a8d5f771122 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php @@ -0,0 +1,130 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCP\Comments\ICommentsManager; +use OCP\IUserSession; +use Sabre\DAV\PropFind; +use Sabre\DAV\ServerPlugin; + +class CommentPropertiesPlugin extends ServerPlugin { + + const PROPERTY_NAME_HREF = '{http://owncloud.org/ns}comments-href'; + const PROPERTY_NAME_COUNT = '{http://owncloud.org/ns}comments-count'; + const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread'; + + /** @var \Sabre\DAV\Server */ + protected $server; + + /** @var ICommentsManager */ + private $commentsManager; + + /** @var IUserSession */ + private $userSession; + + public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) { + $this->commentsManager = $commentsManager; + $this->userSession = $userSession; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('propFind', array($this, 'handleGetProperties')); + } + + /** + * Adds tags and favorites properties to the response, + * if requested. + * + * @param PropFind $propFind + * @param \Sabre\DAV\INode $node + * @return void + */ + public function handleGetProperties( + PropFind $propFind, + \Sabre\DAV\INode $node + ) { + if (!($node instanceof File) && !($node instanceof Directory)) { + return; + } + + $propFind->handle(self::PROPERTY_NAME_COUNT, function() use ($node) { + return $this->commentsManager->getNumberOfCommentsForObject('files', strval($node->getId())); + }); + + $propFind->handle(self::PROPERTY_NAME_HREF, function() use ($node) { + return $this->getCommentsLink($node); + }); + + $propFind->handle(self::PROPERTY_NAME_UNREAD, function() use ($node) { + return $this->getUnreadCount($node); + }); + } + + /** + * returns a reference to the comments node + * + * @param Node $node + * @return mixed|string + */ + public function getCommentsLink(Node $node) { + $href = $this->server->getBaseUri(); + $entryPoint = strrpos($href, '/webdav/'); + if($entryPoint === false) { + // in case we end up somewhere else, unexpectedly. + return null; + } + $href = substr_replace($href, '/dav/', $entryPoint); + $href .= 'comments/files/' . rawurldecode($node->getId()); + return $href; + } + + /** + * returns the number of unread comments for the currently logged in user + * on the given file or directory node + * + * @param Node $node + * @return Int|null + */ + public function getUnreadCount(Node $node) { + $user = $this->userSession->getUser(); + if(is_null($user)) { + return null; + } + + $lastRead = $this->commentsManager->getReadMark('files', strval($node->getId()), $user); + + return $this->commentsManager->getNumberOfCommentsForObject('files', strval($node->getId()), $lastRead); + } + +} diff --git a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php new file mode 100644 index 00000000000..49b6a7b2de7 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php @@ -0,0 +1,57 @@ + + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use \Sabre\HTTP\RequestInterface; +use \Sabre\HTTP\ResponseInterface; + +/** + * Copies the "Etag" header to "OC-Etag" after any request. + * This is a workaround for setups that automatically strip + * or mangle Etag headers. + */ +class CopyEtagHeaderPlugin extends \Sabre\DAV\ServerPlugin { + /** + * This initializes the plugin. + * + * @param \Sabre\DAV\Server $server Sabre server + * + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + $server->on('afterMethod', array($this, 'afterMethod')); + } + + /** + * After method, copy the "Etag" header to "OC-Etag" header. + * + * @param RequestInterface $request request + * @param ResponseInterface $response response + */ + public function afterMethod(RequestInterface $request, ResponseInterface $response) { + $eTag = $response->getHeader('Etag'); + if (!empty($eTag)) { + $response->setHeader('OC-ETag', $eTag); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/CustomPropertiesBackend.php b/apps/dav/lib/Connector/Sabre/CustomPropertiesBackend.php new file mode 100644 index 00000000000..5946c9910d4 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/CustomPropertiesBackend.php @@ -0,0 +1,355 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCP\IDBConnection; +use OCP\IUser; +use Sabre\DAV\PropertyStorage\Backend\BackendInterface; +use Sabre\DAV\PropFind; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Tree; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\ServiceUnavailable; + +class CustomPropertiesBackend implements BackendInterface { + + /** + * Ignored properties + * + * @var array + */ + private $ignoredProperties = array( + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{DAV:}getetag', + '{DAV:}quota-used-bytes', + '{DAV:}quota-available-bytes', + '{DAV:}quota-available-bytes', + '{http://owncloud.org/ns}permissions', + '{http://owncloud.org/ns}downloadURL', + '{http://owncloud.org/ns}dDC', + '{http://owncloud.org/ns}size', + ); + + /** + * @var Tree + */ + private $tree; + + /** + * @var IDBConnection + */ + private $connection; + + /** + * @var IUser + */ + private $user; + + /** + * Properties cache + * + * @var array + */ + private $cache = []; + + /** + * @param Tree $tree node tree + * @param IDBConnection $connection database connection + * @param IUser $user owner of the tree and properties + */ + public function __construct( + Tree $tree, + IDBConnection $connection, + IUser $user) { + $this->tree = $tree; + $this->connection = $connection; + $this->user = $user->getUID(); + } + + /** + * Fetches properties for a path. + * + * @param string $path + * @param PropFind $propFind + * @return void + */ + public function propFind($path, PropFind $propFind) { + try { + $node = $this->tree->getNodeForPath($path); + if (!($node instanceof Node)) { + return; + } + } catch (ServiceUnavailable $e) { + // might happen for unavailable mount points, skip + return; + } catch (NotFound $e) { + // in some rare (buggy) cases the node might not be found, + // we catch the exception to prevent breaking the whole list with a 404 + // (soft fail) + \OC::$server->getLogger()->warning( + 'Could not get node for path: \"' . $path . '\" : ' . $e->getMessage(), + array('app' => 'files') + ); + return; + } + + $requestedProps = $propFind->get404Properties(); + + // these might appear + $requestedProps = array_diff( + $requestedProps, + $this->ignoredProperties + ); + + if (empty($requestedProps)) { + return; + } + + if ($node instanceof Directory + && $propFind->getDepth() !== 0 + ) { + // note: pre-fetching only supported for depth <= 1 + $this->loadChildrenProperties($node, $requestedProps); + } + + $props = $this->getProperties($node, $requestedProps); + foreach ($props as $propName => $propValue) { + $propFind->set($propName, $propValue); + } + } + + /** + * Updates properties for a path + * + * @param string $path + * @param PropPatch $propPatch + * + * @return void + */ + public function propPatch($path, PropPatch $propPatch) { + $node = $this->tree->getNodeForPath($path); + if (!($node instanceof Node)) { + return; + } + + $propPatch->handleRemaining(function($changedProps) use ($node) { + return $this->updateProperties($node, $changedProps); + }); + } + + /** + * This method is called after a node is deleted. + * + * @param string $path path of node for which to delete properties + */ + public function delete($path) { + $statement = $this->connection->prepare( + 'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?' + ); + $statement->execute(array($this->user, '/' . $path)); + $statement->closeCursor(); + + unset($this->cache[$path]); + } + + /** + * This method is called after a successful MOVE + * + * @param string $source + * @param string $destination + * + * @return void + */ + public function move($source, $destination) { + $statement = $this->connection->prepare( + 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' . + ' WHERE `userid` = ? AND `propertypath` = ?' + ); + $statement->execute(array('/' . $destination, $this->user, '/' . $source)); + $statement->closeCursor(); + } + + /** + * Returns a list of properties for this nodes.; + * @param Node $node + * @param array $requestedProperties requested properties or empty array for "all" + * @return array + * @note 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 + */ + private function getProperties(Node $node, array $requestedProperties) { + $path = $node->getPath(); + if (isset($this->cache[$path])) { + return $this->cache[$path]; + } + + // TODO: chunking if more than 1000 properties + $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'; + + $whereValues = array($this->user, $path); + $whereTypes = array(null, null); + + if (!empty($requestedProperties)) { + // request only a subset + $sql .= ' AND `propertyname` in (?)'; + $whereValues[] = $requestedProperties; + $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY; + } + + $result = $this->connection->executeQuery( + $sql, + $whereValues, + $whereTypes + ); + + $props = []; + while ($row = $result->fetch()) { + $props[$row['propertyname']] = $row['propertyvalue']; + } + + $result->closeCursor(); + + $this->cache[$path] = $props; + return $props; + } + + /** + * Update properties + * + * @param Node $node node for which to update properties + * @param array $properties array of properties to update + * + * @return bool + */ + private function updateProperties($node, $properties) { + $path = $node->getPath(); + + $deleteStatement = 'DELETE FROM `*PREFIX*properties`' . + ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; + + $insertStatement = 'INSERT INTO `*PREFIX*properties`' . + ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)'; + + $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' . + ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; + + // TODO: use "insert or update" strategy ? + $existing = $this->getProperties($node, array()); + $this->connection->beginTransaction(); + foreach ($properties as $propertyName => $propertyValue) { + // If it was null, we need to delete the property + if (is_null($propertyValue)) { + if (array_key_exists($propertyName, $existing)) { + $this->connection->executeUpdate($deleteStatement, + array( + $this->user, + $path, + $propertyName + ) + ); + } + } else { + if (!array_key_exists($propertyName, $existing)) { + $this->connection->executeUpdate($insertStatement, + array( + $this->user, + $path, + $propertyName, + $propertyValue + ) + ); + } else { + $this->connection->executeUpdate($updateStatement, + array( + $propertyValue, + $this->user, + $path, + $propertyName + ) + ); + } + } + } + + $this->connection->commit(); + unset($this->cache[$path]); + + return true; + } + + /** + * Bulk load properties for directory children + * + * @param Directory $node + * @param array $requestedProperties requested properties + * + * @return void + */ + private function loadChildrenProperties(Directory $node, $requestedProperties) { + $path = $node->getPath(); + if (isset($this->cache[$path])) { + // we already loaded them at some point + return; + } + + $childNodes = $node->getChildren(); + // pre-fill cache + foreach ($childNodes as $childNode) { + $this->cache[$childNode->getPath()] = []; + } + + $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` LIKE ?'; + $sql .= ' AND `propertyname` in (?) ORDER BY `propertypath`, `propertyname`'; + + $result = $this->connection->executeQuery( + $sql, + array($this->user, rtrim($path, '/') . '/%', $requestedProperties), + array(null, null, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY) + ); + + $oldPath = null; + $props = []; + while ($row = $result->fetch()) { + $path = $row['propertypath']; + if ($oldPath !== $path) { + // save previously gathered props + $this->cache[$oldPath] = $props; + $oldPath = $path; + // prepare props for next path + $props = []; + } + $props[$row['propertyname']] = $row['propertyvalue']; + } + if (!is_null($oldPath)) { + // save props from last run + $this->cache[$oldPath] = $props; + } + + $result->closeCursor(); + } + +} diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php new file mode 100644 index 00000000000..f5699b469c3 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php @@ -0,0 +1,72 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\IFile; +use Sabre\DAV\INode; +use \Sabre\DAV\PropFind; +use \Sabre\DAV\PropPatch; +use Sabre\DAVACL\Exception\NeedPrivileges; +use \Sabre\HTTP\RequestInterface; +use \Sabre\HTTP\ResponseInterface; +use Sabre\HTTP\URLUtil; + +/** + * Class DavAclPlugin is a wrapper around \Sabre\DAVACL\Plugin that returns 404 + * responses in case the resource to a response has been forbidden instead of + * a 403. This is used to prevent enumeration of valid resources. + * + * @see https://github.com/owncloud/core/issues/22578 + * @package OCA\DAV\Connector\Sabre + */ +class DavAclPlugin extends \Sabre\DAVACL\Plugin { + public function __construct() { + $this->hideNodesFromListings = true; + } + + function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) { + $access = parent::checkPrivileges($uri, $privileges, $recursion, false); + if($access === false && $throwExceptions) { + /** @var INode $node */ + $node = $this->server->tree->getNodeForPath($uri); + + switch(get_class($node)) { + case 'OCA\DAV\CardDAV\AddressBook': + $type = 'Addressbook'; + break; + default: + $type = 'Node'; + break; + } + throw new NotFound( + sprintf( + "%s with name '%s' could not be found", + $type, + $node->getName() + ) + ); + } + + return $access; + } +} diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php new file mode 100644 index 00000000000..daa5f29ce79 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -0,0 +1,310 @@ + + * @author Bart Visscher + * @author Björn Schießle + * @author Jakob Sack + * @author Joas Schilling + * @author Morris Jobke + * @author Robin Appelman + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Connector\Sabre\Exception\FileLocked; +use OCP\Files\ForbiddenException; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use Sabre\DAV\Exception\Locked; + +class Directory extends \OCA\DAV\Connector\Sabre\Node + implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota { + + /** + * Cached directory content + * + * @var \OCP\Files\FileInfo[] + */ + private $dirContent; + + /** + * Cached quota info + * + * @var array + */ + private $quotaInfo; + + /** + * @var ObjectTree|null + */ + private $tree; + + /** + * Sets up the node, expects a full path name + * + * @param \OC\Files\View $view + * @param \OCP\Files\FileInfo $info + * @param ObjectTree|null $tree + * @param \OCP\Share\IManager $shareManager + */ + public function __construct($view, $info, $tree = null, $shareManager = null) { + parent::__construct($view, $info, $shareManager); + $this->tree = $tree; + } + + /** + * 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 + * @throws Exception\EntityTooLarge + * @throws Exception\UnsupportedMediaType + * @throws FileLocked + * @throws InvalidPath + * @throws \Sabre\DAV\Exception + * @throws \Sabre\DAV\Exception\BadRequest + * @throws \Sabre\DAV\Exception\Forbidden + * @throws \Sabre\DAV\Exception\ServiceUnavailable + */ + public function createFile($name, $data = null) { + + try { + // for chunked upload also updating a existing file is a "createFile" + // because we create all the chunks before re-assemble them to the existing file. + if (isset($_SERVER['HTTP_OC_CHUNKED'])) { + + // exit if we can't create a new file and we don't updatable existing file + $info = \OC_FileChunking::decodeName($name); + if (!$this->fileView->isCreatable($this->path) && + !$this->fileView->isUpdatable($this->path . '/' . $info['name']) + ) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + + } else { + // For non-chunked upload it is enough to check if we can create a new file + if (!$this->fileView->isCreatable($this->path)) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + } + + $this->fileView->verifyPath($this->path, $name); + + $path = $this->fileView->getAbsolutePath($this->path) . '/' . $name; + // using a dummy FileInfo is acceptable here since it will be refreshed after the put is complete + $info = new \OC\Files\FileInfo($path, null, null, array(), null); + $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info); + $node->acquireLock(ILockingProvider::LOCK_SHARED); + return $node->put($data); + } catch (\OCP\Files\StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (\OCP\Files\InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Creates a new subdirectory + * + * @param string $name + * @throws FileLocked + * @throws InvalidPath + * @throws \Sabre\DAV\Exception\Forbidden + * @throws \Sabre\DAV\Exception\ServiceUnavailable + */ + public function createDirectory($name) { + try { + if (!$this->info->isCreatable()) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + + $this->fileView->verifyPath($this->path, $name); + $newPath = $this->path . '/' . $name; + if (!$this->fileView->mkdir($newPath)) { + throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath); + } + } catch (\OCP\Files\StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (\OCP\Files\InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns a specific child node, referenced by its name + * + * @param string $name + * @param \OCP\Files\FileInfo $info + * @return \Sabre\DAV\INode + * @throws InvalidPath + * @throws \Sabre\DAV\Exception\NotFound + * @throws \Sabre\DAV\Exception\ServiceUnavailable + */ + public function getChild($name, $info = null) { + $path = $this->path . '/' . $name; + if (is_null($info)) { + try { + $this->fileView->verifyPath($this->path, $name); + $info = $this->fileView->getFileInfo($path); + } catch (\OCP\Files\StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (\OCP\Files\InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } + } + + if (!$info) { + throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located'); + } + + if ($info['mimetype'] == 'httpd/unix-directory') { + $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager); + } else { + $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager); + } + if ($this->tree) { + $this->tree->cacheNode($node); + } + return $node; + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + public function getChildren() { + if (!is_null($this->dirContent)) { + return $this->dirContent; + } + try { + $folderContent = $this->fileView->getDirectoryContent($this->path); + } catch (LockedException $e) { + throw new Locked(); + } + + $nodes = array(); + foreach ($folderContent as $info) { + $node = $this->getChild($info->getName(), $info); + $nodes[] = $node; + } + $this->dirContent = $nodes; + return $this->dirContent; + } + + /** + * Checks if a child exists. + * + * @param string $name + * @return bool + */ + public function childExists($name) { + // note: here we do NOT resolve the chunk file name to the real file name + // to make sure we return false when checking for file existence with a chunk + // file name. + // This is to make sure that "createFile" is still triggered + // (required old code) instead of "updateFile". + // + // TODO: resolve chunk file name here and implement "updateFile" + $path = $this->path . '/' . $name; + return $this->fileView->file_exists($path); + + } + + /** + * Deletes all files in this directory, and then itself + * + * @return void + * @throws FileLocked + * @throws \Sabre\DAV\Exception\Forbidden + */ + public function delete() { + + if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + + try { + if (!$this->fileView->rmdir($this->path)) { + // assume it wasn't possible to remove due to permission issue + throw new \Sabre\DAV\Exception\Forbidden(); + } + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns available diskspace information + * + * @return array + */ + public function getQuotaInfo() { + if ($this->quotaInfo) { + return $this->quotaInfo; + } + try { + $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $this->info); + if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) { + $free = \OCP\Files\FileInfo::SPACE_UNLIMITED; + } else { + $free = $storageInfo['free']; + } + $this->quotaInfo = array( + $storageInfo['used'], + $free + ); + return $this->quotaInfo; + } catch (\OCP\Files\StorageNotAvailableException $e) { + return array(0, 0); + } + } + +} diff --git a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php new file mode 100644 index 00000000000..b10d5aaab36 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php @@ -0,0 +1,70 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; +use Sabre\HTTP\ResponseInterface; +use Sabre\HTTP\RequestInterface; + +/** + * Class DummyGetResponsePlugin is a plugin used to not show a "Not implemented" + * error to clients that rely on verifying the functionality of the ownCloud + * WebDAV backend using a simple GET to /. + * + * This is considered a legacy behaviour and implementers should consider sending + * a PROPFIND request instead to verify whether the WebDAV component is working + * properly. + * + * FIXME: Remove once clients are all compliant. + * + * @package OCA\DAV\Connector\Sabre + */ +class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin { + /** @var \Sabre\DAV\Server */ + protected $server; + + /** + * @param \Sabre\DAV\Server $server + * @return void + */ + function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 200); + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @return false + */ + function httpGet(RequestInterface $request, ResponseInterface $response) { + $string = 'This is the WebDAV interface. It can only be accessed by ' . + 'WebDAV clients such as the ownCloud desktop sync client.'; + $stream = fopen('php://memory','r+'); + fwrite($stream, $string); + rewind($stream); + + $response->setStatus(200); + $response->setBody($stream); + + return false; + } +} diff --git a/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php b/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php new file mode 100644 index 00000000000..d16e93bb637 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php @@ -0,0 +1,44 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Connector\Sabre\Exception; + +/** + * Entity Too Large + * + * This exception is thrown whenever a user tries to upload a file which exceeds hard limitations + * + */ +class EntityTooLarge extends \Sabre\DAV\Exception { + + /** + * Returns the HTTP status code for this exception + * + * @return int + */ + public function getHTTPCode() { + + return 413; + + } + +} diff --git a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php new file mode 100644 index 00000000000..03ced0e81e2 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php @@ -0,0 +1,48 @@ + + * @author Morris Jobke + * @author Owen Winkler + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre\Exception; + +use Exception; + +class FileLocked extends \Sabre\DAV\Exception { + + public function __construct($message = "", $code = 0, Exception $previous = null) { + if($previous instanceof \OCP\Files\LockNotAcquiredException) { + $message = sprintf('Target file %s is locked by another process.', $previous->path); + } + parent::__construct($message, $code, $previous); + } + + /** + * Returns the HTTP status code for this exception + * + * @return int + */ + public function getHTTPCode() { + + return 423; + } +} diff --git a/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php new file mode 100644 index 00000000000..f2467e6b298 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php @@ -0,0 +1,64 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre\Exception; + +class Forbidden extends \Sabre\DAV\Exception\Forbidden { + + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * @var bool + */ + private $retry; + + /** + * @param string $message + * @param bool $retry + * @param \Exception $previous + */ + public function __construct($message, $retry = false, \Exception $previous = null) { + parent::__construct($message, 0, $previous); + $this->retry = $retry; + } + + /** + * This method allows the exception to include additional information + * into the WebDAV error response + * + * @param \Sabre\DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) { + + // set ownCloud namespace + $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD); + + // adding the retry node + $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true)); + $errorNode->appendChild($error); + + // adding the message node + $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage()); + $errorNode->appendChild($error); + } +} diff --git a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php new file mode 100644 index 00000000000..7951a0a89b7 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php @@ -0,0 +1,77 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre\Exception; + +use Sabre\DAV\Exception; + +class InvalidPath extends Exception { + + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * @var bool + */ + private $retry; + + /** + * @param string $message + * @param bool $retry + */ + public function __construct($message, $retry = false) { + parent::__construct($message); + $this->retry = $retry; + } + + /** + * Returns the HTTP status code for this exception + * + * @return int + */ + public function getHTTPCode() { + + return 400; + + } + + /** + * This method allows the exception to include additional information + * into the WebDAV error response + * + * @param \Sabre\DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) { + + // set ownCloud namespace + $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD); + + // adding the retry node + $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true)); + $errorNode->appendChild($error); + + // adding the message node + $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage()); + $errorNode->appendChild($error); + } + +} diff --git a/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php b/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php new file mode 100644 index 00000000000..99e3c222c75 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php @@ -0,0 +1,44 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Connector\Sabre\Exception; + +/** + * Unsupported Media Type + * + * This exception is thrown whenever a user tries to upload a file which holds content which is not allowed + * + */ +class UnsupportedMediaType extends \Sabre\DAV\Exception { + + /** + * Returns the HTTP status code for this exception + * + * @return int + */ + public function getHTTPCode() { + + return 415; + + } + +} diff --git a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php new file mode 100644 index 00000000000..38bddef8769 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php @@ -0,0 +1,111 @@ + + * @author Pierre Jochem + * @author Robin Appelman + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCP\ILogger; +use Sabre\DAV\Exception; +use Sabre\HTTP\Response; + +class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { + protected $nonFatalExceptions = array( + 'Sabre\DAV\Exception\NotAuthenticated' => true, + // the sync client uses this to find out whether files exist, + // so it is not always an error, log it as debug + 'Sabre\DAV\Exception\NotFound' => true, + // this one mostly happens when the same file is uploaded at + // exactly the same time from two clients, only one client + // wins, the second one gets "Precondition failed" + 'Sabre\DAV\Exception\PreconditionFailed' => true, + // forbidden can be expected when trying to upload to + // read-only folders for example + 'Sabre\DAV\Exception\Forbidden' => true, + ); + + /** @var string */ + private $appName; + + /** @var ILogger */ + private $logger; + + /** + * @param string $loggerAppName app name to use when logging + * @param ILogger $logger + */ + public function __construct($loggerAppName, $logger) { + $this->appName = $loggerAppName; + $this->logger = $logger; + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + + $server->on('exception', array($this, 'logException'), 10); + } + + /** + * Log exception + * + */ + public function logException(\Exception $ex) { + $exceptionClass = get_class($ex); + $level = \OCP\Util::FATAL; + if (isset($this->nonFatalExceptions[$exceptionClass])) { + $level = \OCP\Util::DEBUG; + } + + $message = $ex->getMessage(); + if ($ex instanceof Exception) { + if (empty($message)) { + $response = new Response($ex->getHTTPCode()); + $message = $response->getStatusText(); + } + $message = "HTTP/1.1 {$ex->getHTTPCode()} $message"; + } + + $user = \OC_User::getUser(); + + $exception = [ + 'Message' => $message, + 'Exception' => $exceptionClass, + 'Code' => $ex->getCode(), + 'Trace' => $ex->getTraceAsString(), + 'File' => $ex->getFile(), + 'Line' => $ex->getLine(), + 'User' => $user, + ]; + $this->logger->log($level, 'Exception: ' . json_encode($exception), ['app' => $this->appName]); + } +} diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php new file mode 100644 index 00000000000..6db8740bc13 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php @@ -0,0 +1,156 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use Sabre\DAV\Locks\LockInfo; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Xml\Property\LockDiscovery; +use Sabre\DAV\Xml\Property\SupportedLock; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\DAV\PropFind; +use Sabre\DAV\INode; + +/** + * Class FakeLockerPlugin is a plugin only used when connections come in from + * OS X via Finder. The fake locking plugin does emulate Class 2 WebDAV support + * (locking of files) which allows Finder to access the storage in write mode as + * well. + * + * No real locking is performed, instead the plugin just returns always positive + * responses. + * + * @see https://github.com/owncloud/core/issues/17732 + * @package OCA\DAV\Connector\Sabre + */ +class FakeLockerPlugin extends ServerPlugin { + /** @var \Sabre\DAV\Server */ + private $server; + + /** {@inheritDoc} */ + public function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1); + $this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1); + $server->on('propFind', [$this, 'propFind']); + $server->on('validateTokens', [$this, 'validateTokens']); + } + + /** + * Indicate that we support LOCK and UNLOCK + * + * @param string $path + * @return string[] + */ + public function getHTTPMethods($path) { + return [ + 'LOCK', + 'UNLOCK', + ]; + } + + /** + * Indicate that we support locking + * + * @return integer[] + */ + function getFeatures() { + return [2]; + } + + /** + * Return some dummy response for PROPFIND requests with regard to locking + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + $propFind->handle('{DAV:}supportedlock', function() { + return new SupportedLock(true); + }); + $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) { + return new LockDiscovery([]); + }); + } + + /** + * Mark a locking token always as valid + * + * @param RequestInterface $request + * @param array $conditions + */ + public function validateTokens(RequestInterface $request, &$conditions) { + foreach($conditions as &$fileCondition) { + if(isset($fileCondition['tokens'])) { + foreach($fileCondition['tokens'] as &$token) { + if(isset($token['token'])) { + if(substr($token['token'], 0, 16) === 'opaquelocktoken:') { + $token['validToken'] = true; + } + } + } + } + } + } + + /** + * Fakes a successful LOCK + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function fakeLockProvider(RequestInterface $request, + ResponseInterface $response) { + + $lockInfo = new LockInfo(); + $lockInfo->token = md5($request->getPath()); + $lockInfo->uri = $request->getPath(); + $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY; + $lockInfo->timeout = 1800; + + $body = $this->server->xml->write('{DAV:}prop', [ + '{DAV:}lockdiscovery' => + new LockDiscovery([$lockInfo]) + ]); + + $response->setBody($body); + + return false; + } + + /** + * Fakes a successful LOCK + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function fakeUnlockProvider(RequestInterface $request, + ResponseInterface $response) { + $response->setStatus(204); + $response->setHeader('Content-Length', '0'); + return false; + } +} diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php new file mode 100644 index 00000000000..943e9150e74 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -0,0 +1,567 @@ + + * @author Björn Schießle + * @author Jakob Sack + * @author Joas Schilling + * @author Jörn Friedrich Dreyer + * @author Lukas Reschke + * @author Morris Jobke + * @author Owen Winkler + * @author Robin Appelman + * @author Roeland Jago Douma + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OC\Files\Filesystem; +use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge; +use OCA\DAV\Connector\Sabre\Exception\FileLocked; +use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; +use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; +use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\Files\EntityTooLargeException; +use OCP\Files\ForbiddenException; +use OCP\Files\InvalidContentException; +use OCP\Files\InvalidPathException; +use OCP\Files\LockNotAcquiredException; +use OCP\Files\NotPermittedException; +use OCP\Files\StorageNotAvailableException; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use Sabre\DAV\Exception; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotImplemented; +use Sabre\DAV\Exception\ServiceUnavailable; +use Sabre\DAV\IFile; + +class File extends Node implements IFile { + + /** + * Updates the data + * + * The data argument is a readable stream resource. + * + * After a successful put operation, you may choose to return an ETag. The + * etag must always be surrounded by double-quotes. These quotes must + * appear in the actual string you're returning. + * + * Clients may use the ETag from a PUT request to later on make sure that + * when they update the file, the contents haven't changed in the mean + * time. + * + * If you don't plan to store the file byte-by-byte, and you return a + * different object on a subsequent GET you are strongly recommended to not + * return an ETag, and just return null. + * + * @param resource $data + * + * @throws Forbidden + * @throws UnsupportedMediaType + * @throws BadRequest + * @throws Exception + * @throws EntityTooLarge + * @throws ServiceUnavailable + * @throws FileLocked + * @return string|null + */ + public function put($data) { + try { + $exists = $this->fileView->file_exists($this->path); + if ($this->info && $exists && !$this->info->isUpdateable()) { + throw new Forbidden(); + } + } catch (StorageNotAvailableException $e) { + throw new ServiceUnavailable("File is not updatable: " . $e->getMessage()); + } + + // verify path of the target + $this->verifyPath(); + + // chunked handling + if (isset($_SERVER['HTTP_OC_CHUNKED'])) { + try { + return $this->createFileChunked($data); + } catch (\Exception $e) { + $this->convertToSabreException($e); + } + } + + list($partStorage) = $this->fileView->resolvePath($this->path); + $needsPartFile = $this->needsPartFile($partStorage) && (strlen($this->path) > 1); + + if ($needsPartFile) { + // mark file as partial while uploading (ignored by the scanner) + $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part'; + } else { + // upload file directly as the final path + $partFilePath = $this->path; + } + + // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) + /** @var \OC\Files\Storage\Storage $partStorage */ + list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath); + /** @var \OC\Files\Storage\Storage $storage */ + list($storage, $internalPath) = $this->fileView->resolvePath($this->path); + try { + $target = $partStorage->fopen($internalPartPath, 'wb'); + if ($target === false) { + \OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::fopen() failed', \OCP\Util::ERROR); + // because we have no clue about the cause we can only throw back a 500/Internal Server Error + throw new Exception('Could not write file contents'); + } + list($count, $result) = \OC_Helper::streamCopy($data, $target); + fclose($target); + + if ($result === false) { + $expected = -1; + if (isset($_SERVER['CONTENT_LENGTH'])) { + $expected = $_SERVER['CONTENT_LENGTH']; + } + throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )'); + } + + // if content length is sent by client: + // double check if the file was fully received + // compare expected and actual size + if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { + $expected = $_SERVER['CONTENT_LENGTH']; + if ($count != $expected) { + throw new BadRequest('expected filesize ' . $expected . ' got ' . $count); + } + } + + } catch (\Exception $e) { + if ($needsPartFile) { + $partStorage->unlink($internalPartPath); + } + $this->convertToSabreException($e); + } + + try { + $view = \OC\Files\Filesystem::getView(); + if ($view) { + $run = $this->emitPreHooks($exists); + } else { + $run = true; + } + + try { + $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); + } catch (LockedException $e) { + if ($needsPartFile) { + $partStorage->unlink($internalPartPath); + } + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + + if ($needsPartFile) { + // rename to correct path + try { + if ($run) { + $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); + $fileExists = $storage->file_exists($internalPath); + } + if (!$run || $renameOkay === false || $fileExists === false) { + \OCP\Util::writeLog('webdav', 'renaming part file to final file failed', \OCP\Util::ERROR); + throw new Exception('Could not rename part file to final file'); + } + } catch (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); + } catch (\Exception $e) { + $partStorage->unlink($internalPartPath); + $this->convertToSabreException($e); + } + } + + // since we skipped the view we need to scan and emit the hooks ourselves + $storage->getUpdater()->update($internalPath); + + try { + $this->changeLock(ILockingProvider::LOCK_SHARED); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + + if ($view) { + $this->emitPostHooks($exists); + } + + // allow sync clients to send the mtime along in a header + $request = \OC::$server->getRequest(); + if (isset($request->server['HTTP_X_OC_MTIME'])) { + if ($this->fileView->touch($this->path, $request->server['HTTP_X_OC_MTIME'])) { + header('X-OC-MTime: accepted'); + } + } + + $this->refreshInfo(); + + if (isset($request->server['HTTP_OC_CHECKSUM'])) { + $checksum = trim($request->server['HTTP_OC_CHECKSUM']); + $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); + $this->refreshInfo(); + } else if ($this->getChecksum() !== null && $this->getChecksum() !== '') { + $this->fileView->putFileInfo($this->path, ['checksum' => '']); + $this->refreshInfo(); + } + + } catch (StorageNotAvailableException $e) { + throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage()); + } + + return '"' . $this->info->getEtag() . '"'; + } + + private function getPartFileBasePath($path) { + $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true); + if ($partFileInStorage) { + return $path; + } else { + return md5($path); // will place it in the root of the view with a unique name + } + } + + /** + * @param string $path + */ + private function emitPreHooks($exists, $path = null) { + if (is_null($path)) { + $path = $this->path; + } + $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); + $run = true; + + if (!$exists) { + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array( + \OC\Files\Filesystem::signal_param_path => $hookPath, + \OC\Files\Filesystem::signal_param_run => &$run, + )); + } else { + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array( + \OC\Files\Filesystem::signal_param_path => $hookPath, + \OC\Files\Filesystem::signal_param_run => &$run, + )); + } + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array( + \OC\Files\Filesystem::signal_param_path => $hookPath, + \OC\Files\Filesystem::signal_param_run => &$run, + )); + return $run; + } + + /** + * @param string $path + */ + private function emitPostHooks($exists, $path = null) { + if (is_null($path)) { + $path = $this->path; + } + $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); + if (!$exists) { + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array( + \OC\Files\Filesystem::signal_param_path => $hookPath + )); + } else { + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array( + \OC\Files\Filesystem::signal_param_path => $hookPath + )); + } + \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array( + \OC\Files\Filesystem::signal_param_path => $hookPath + )); + } + + /** + * Returns the data + * + * @return resource + * @throws Forbidden + * @throws ServiceUnavailable + */ + public function get() { + //throw exception if encryption is disabled but files are still encrypted + try { + $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb'); + if ($res === false) { + throw new ServiceUnavailable("Could not open file"); + } + return $res; + } catch (GenericEncryptionException $e) { + // returning 503 will allow retry of the operation at a later point in time + throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage()); + } catch (StorageNotAvailableException $e) { + throw new ServiceUnavailable("Failed to open file: " . $e->getMessage()); + } catch (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Delete the current file + * + * @throws Forbidden + * @throws ServiceUnavailable + */ + public function delete() { + if (!$this->info->isDeletable()) { + throw new Forbidden(); + } + + try { + if (!$this->fileView->unlink($this->path)) { + // assume it wasn't possible to delete due to permissions + throw new Forbidden(); + } + } catch (StorageNotAvailableException $e) { + throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage()); + } catch (ForbiddenException $ex) { + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns the mime-type for a file + * + * If null is returned, we'll assume application/octet-stream + * + * @return string + */ + public function getContentType() { + $mimeType = $this->info->getMimetype(); + + // PROPFIND needs to return the correct mime type, for consistency with the web UI + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') { + return $mimeType; + } + return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType); + } + + /** + * @return array|false + */ + public function getDirectDownload() { + if (\OCP\App::isEnabled('encryption')) { + return []; + } + /** @var \OCP\Files\Storage $storage */ + list($storage, $internalPath) = $this->fileView->resolvePath($this->path); + if (is_null($storage)) { + return []; + } + + return $storage->getDirectDownload($internalPath); + } + + /** + * @param resource $data + * @return null|string + * @throws Exception + * @throws BadRequest + * @throws NotImplemented + * @throws ServiceUnavailable + */ + private function createFileChunked($data) { + list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($this->path); + + $info = \OC_FileChunking::decodeName($name); + if (empty($info)) { + throw new NotImplemented('Invalid chunk name'); + } + + $chunk_handler = new \OC_FileChunking($info); + $bytesWritten = $chunk_handler->store($info['index'], $data); + + //detect aborted upload + if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { + if (isset($_SERVER['CONTENT_LENGTH'])) { + $expected = $_SERVER['CONTENT_LENGTH']; + if ($bytesWritten != $expected) { + $chunk_handler->remove($info['index']); + throw new BadRequest( + 'expected filesize ' . $expected . ' got ' . $bytesWritten); + } + } + } + + if ($chunk_handler->isComplete()) { + list($storage,) = $this->fileView->resolvePath($path); + $needsPartFile = $this->needsPartFile($storage); + $partFile = null; + + $targetPath = $path . '/' . $info['name']; + /** @var \OC\Files\Storage\Storage $targetStorage */ + list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); + + $exists = $this->fileView->file_exists($targetPath); + + try { + $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED); + + $this->emitPreHooks($exists, $targetPath); + $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE); + /** @var \OC\Files\Storage\Storage $targetStorage */ + list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); + + if ($needsPartFile) { + // we first assembly the target file as a part file + $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; + /** @var \OC\Files\Storage\Storage $targetStorage */ + list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile); + + + $chunk_handler->file_assemble($partStorage, $partInternalPath); + + // here is the final atomic rename + $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath); + $fileExists = $targetStorage->file_exists($targetInternalPath); + if ($renameOkay === false || $fileExists === false) { + \OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::rename() failed', \OCP\Util::ERROR); + // only delete if an error occurred and the target file was already created + if ($fileExists) { + // set to null to avoid double-deletion when handling exception + // stray part file + $partFile = null; + $targetStorage->unlink($targetInternalPath); + } + $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); + throw new Exception('Could not rename part file assembled from chunks'); + } + } else { + // assemble directly into the final file + $chunk_handler->file_assemble($targetStorage, $targetInternalPath); + } + + // allow sync clients to send the mtime along in a header + $request = \OC::$server->getRequest(); + if (isset($request->server['HTTP_X_OC_MTIME'])) { + if ($targetStorage->touch($targetInternalPath, $request->server['HTTP_X_OC_MTIME'])) { + header('X-OC-MTime: accepted'); + } + } + + // since we skipped the view we need to scan and emit the hooks ourselves + $targetStorage->getUpdater()->update($targetInternalPath); + + $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); + + $this->emitPostHooks($exists, $targetPath); + + // FIXME: should call refreshInfo but can't because $this->path is not the of the final file + $info = $this->fileView->getFileInfo($targetPath); + + if (isset($request->server['HTTP_OC_CHECKSUM'])) { + $checksum = trim($request->server['HTTP_OC_CHECKSUM']); + $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]); + } else if ($info->getChecksum() !== null && $info->getChecksum() !== '') { + $this->fileView->putFileInfo($this->path, ['checksum' => '']); + } + + $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED); + + return $info->getEtag(); + } catch (\Exception $e) { + if ($partFile !== null) { + $targetStorage->unlink($targetInternalPath); + } + $this->convertToSabreException($e); + } + } + + return null; + } + + /** + * Returns whether a part file is needed for the given storage + * or whether the file can be assembled/uploaded directly on the + * target storage. + * + * @param \OCP\Files\Storage $storage + * @return bool true if the storage needs part file handling + */ + private function needsPartFile($storage) { + // TODO: in the future use ChunkHandler provided by storage + // and/or add method on Storage called "needsPartFile()" + return !$storage->instanceOfStorage('OCA\Files_Sharing\External\Storage') && + !$storage->instanceOfStorage('OC\Files\Storage\OwnCloud'); + } + + /** + * Convert the given exception to a SabreException instance + * + * @param \Exception $e + * + * @throws \Sabre\DAV\Exception + */ + private function convertToSabreException(\Exception $e) { + if ($e instanceof \Sabre\DAV\Exception) { + throw $e; + } + if ($e instanceof NotPermittedException) { + // a more general case - due to whatever reason the content could not be written + throw new Forbidden($e->getMessage(), 0, $e); + } + if ($e instanceof ForbiddenException) { + // the path for the file was forbidden + throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); + } + if ($e instanceof EntityTooLargeException) { + // the file is too big to be stored + throw new EntityTooLarge($e->getMessage(), 0, $e); + } + if ($e instanceof InvalidContentException) { + // the file content is not permitted + throw new UnsupportedMediaType($e->getMessage(), 0, $e); + } + if ($e instanceof InvalidPathException) { + // the path for the file was not valid + // TODO: find proper http status code for this case + throw new Forbidden($e->getMessage(), 0, $e); + } + if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) { + // the file is currently being written to by another process + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + if ($e instanceof GenericEncryptionException) { + // returning 503 will allow retry of the operation at a later point in time + throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e); + } + if ($e instanceof StorageNotAvailableException) { + throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e); + } + + throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); + } + + /** + * Get the checksum for this file + * + * @return string + */ + public function getChecksum() { + return $this->info->getChecksum(); + } +} diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php new file mode 100644 index 00000000000..8822deb1661 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -0,0 +1,398 @@ + + * @author Morris Jobke + * @author Robin Appelman + * @author Robin McCorkell + * @author Roeland Jago Douma + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OC\Files\View; +use OCA\DAV\Upload\FutureFile; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\IFile; +use \Sabre\DAV\PropFind; +use \Sabre\DAV\PropPatch; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Tree; +use \Sabre\HTTP\RequestInterface; +use \Sabre\HTTP\ResponseInterface; +use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; + +class FilesPlugin extends ServerPlugin { + + // namespace + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id'; + const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid'; + const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions'; + const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions'; + const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL'; + const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size'; + const GETETAG_PROPERTYNAME = '{DAV:}getetag'; + const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified'; + const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id'; + const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name'; + const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums'; + const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint'; + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @var Tree + */ + private $tree; + + /** + * Whether this is public webdav. + * If true, some returned information will be stripped off. + * + * @var bool + */ + private $isPublic; + + /** + * @var View + */ + private $fileView; + + /** + * @var bool + */ + private $downloadAttachment; + + /** + * @var IConfig + */ + private $config; + + /** + * @param Tree $tree + * @param View $view + * @param bool $isPublic + * @param bool $downloadAttachment + */ + public function __construct(Tree $tree, + View $view, + IConfig $config, + $isPublic = false, + $downloadAttachment = true) { + $this->tree = $tree; + $this->fileView = $view; + $this->config = $config; + $this->isPublic = $isPublic; + $this->downloadAttachment = $downloadAttachment; + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + + $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; + $server->protectedProperties[] = self::FILEID_PROPERTYNAME; + $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME; + $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME; + $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME; + $server->protectedProperties[] = self::SIZE_PROPERTYNAME; + $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME; + $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME; + $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME; + $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME; + $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME; + + // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH + $allowedProperties = ['{DAV:}getetag']; + $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties); + + $this->server = $server; + $this->server->on('propFind', array($this, 'handleGetProperties')); + $this->server->on('propPatch', array($this, 'handleUpdateProperties')); + $this->server->on('afterBind', array($this, 'sendFileIdHeader')); + $this->server->on('afterWriteContent', array($this, 'sendFileIdHeader')); + $this->server->on('afterMethod:GET', [$this,'httpGet']); + $this->server->on('afterMethod:GET', array($this, 'handleDownloadToken')); + $this->server->on('afterResponse', function($request, ResponseInterface $response) { + $body = $response->getBody(); + if (is_resource($body)) { + fclose($body); + } + }); + $this->server->on('beforeMove', [$this, 'checkMove']); + } + + /** + * Plugin that checks if a move can actually be performed. + * + * @param string $source source path + * @param string $destination destination path + * @throws Forbidden + * @throws NotFound + */ + function checkMove($source, $destination) { + $sourceNode = $this->tree->getNodeForPath($source); + if ($sourceNode instanceof FutureFile) { + return; + } + list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($source); + list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination); + + if ($sourceDir !== $destinationDir) { + $sourceFileInfo = $this->fileView->getFileInfo($source); + + if ($sourceFileInfo === false) { + throw new NotFound($source . ' does not exist'); + } + + if (!$sourceFileInfo->isDeletable()) { + throw new Forbidden($source . " cannot be deleted"); + } + } + } + + /** + * This sets a cookie to be able to recognize the start of the download + * the content must not be longer than 32 characters and must only contain + * alphanumeric characters + * + * @param RequestInterface $request + * @param ResponseInterface $response + */ + function handleDownloadToken(RequestInterface $request, ResponseInterface $response) { + $queryParams = $request->getQueryParameters(); + + /** + * this sets a cookie to be able to recognize the start of the download + * the content must not be longer than 32 characters and must only contain + * alphanumeric characters + */ + if (isset($queryParams['downloadStartSecret'])) { + $token = $queryParams['downloadStartSecret']; + if (!isset($token[32]) + && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) { + // FIXME: use $response->setHeader() instead + setcookie('ocDownloadStarted', $token, time() + 20, '/'); + } + } + } + + /** + * Add headers to file download + * + * @param RequestInterface $request + * @param ResponseInterface $response + */ + function httpGet(RequestInterface $request, ResponseInterface $response) { + // Only handle valid files + $node = $this->tree->getNodeForPath($request->getPath()); + if (!($node instanceof IFile)) return; + + // adds a 'Content-Disposition: attachment' header + if ($this->downloadAttachment) { + $response->addHeader('Content-Disposition', 'attachment'); + } + + if ($node instanceof \OCA\DAV\Connector\Sabre\File) { + //Add OC-Checksum header + /** @var $node File */ + $checksum = $node->getChecksum(); + if ($checksum !== null && $checksum !== '') { + $response->addHeader('OC-Checksum', $checksum); + } + } + } + + /** + * Adds all ownCloud-specific properties + * + * @param PropFind $propFind + * @param \Sabre\DAV\INode $node + * @return void + */ + public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) { + + $httpRequest = $this->server->httpRequest; + + if ($node instanceof \OCA\DAV\Connector\Sabre\Node) { + + $propFind->handle(self::FILEID_PROPERTYNAME, function() use ($node) { + return $node->getFileId(); + }); + + $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function() use ($node) { + return $node->getInternalFileId(); + }); + + $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function() use ($node) { + $perms = $node->getDavPermissions(); + if ($this->isPublic) { + // remove mount information + $perms = str_replace(['S', 'M'], '', $perms); + } + return $perms; + }); + + $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function() use ($node, $httpRequest) { + return $node->getSharePermissions( + $httpRequest->getRawServerValue('PHP_AUTH_USER') + ); + }); + + $propFind->handle(self::GETETAG_PROPERTYNAME, function() use ($node) { + return $node->getETag(); + }); + + $propFind->handle(self::OWNER_ID_PROPERTYNAME, function() use ($node) { + $owner = $node->getOwner(); + return $owner->getUID(); + }); + $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function() use ($node) { + $owner = $node->getOwner(); + $displayName = $owner->getDisplayName(); + return $displayName; + }); + + $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) { + if ($node->getPath() === '/') { + return $this->config->getSystemValue('data-fingerprint', ''); + } + }); + } + + if ($node instanceof \OCA\DAV\Files\FilesHome) { + $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) { + return $this->config->getSystemValue('data-fingerprint', ''); + }); + } + + if ($node instanceof \OCA\DAV\Connector\Sabre\File) { + $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function() use ($node) { + /** @var $node \OCA\DAV\Connector\Sabre\File */ + try { + $directDownloadUrl = $node->getDirectDownload(); + if (isset($directDownloadUrl['url'])) { + return $directDownloadUrl['url']; + } + } catch (StorageNotAvailableException $e) { + return false; + } + return false; + }); + + $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function() use ($node) { + $checksum = $node->getChecksum(); + if ($checksum === NULL || $checksum === '') { + return null; + } + + return new ChecksumList($checksum); + }); + + } + + if ($node instanceof \OCA\DAV\Connector\Sabre\Directory) { + $propFind->handle(self::SIZE_PROPERTYNAME, function() use ($node) { + return $node->getSize(); + }); + } + } + + /** + * Update ownCloud-specific properties + * + * @param string $path + * @param PropPatch $propPatch + * + * @return void + */ + public function handleUpdateProperties($path, PropPatch $propPatch) { + $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function($time) use ($path) { + if (empty($time)) { + return false; + } + $node = $this->tree->getNodeForPath($path); + if (is_null($node)) { + return 404; + } + $node->touch($time); + return true; + }); + $propPatch->handle(self::GETETAG_PROPERTYNAME, function($etag) use ($path) { + if (empty($etag)) { + return false; + } + $node = $this->tree->getNodeForPath($path); + if (is_null($node)) { + return 404; + } + if ($node->setEtag($etag) !== -1) { + return true; + } + return false; + }); + } + + /** + * @param string $filePath + * @param \Sabre\DAV\INode $node + * @throws \Sabre\DAV\Exception\BadRequest + */ + public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) { + // chunked upload handling + if (isset($_SERVER['HTTP_OC_CHUNKED'])) { + list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($filePath); + $info = \OC_FileChunking::decodeName($name); + if (!empty($info)) { + $filePath = $path . '/' . $info['name']; + } + } + + // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder + if (!$this->server->tree->nodeExists($filePath)) { + return; + } + $node = $this->server->tree->getNodeForPath($filePath); + if ($node instanceof \OCA\DAV\Connector\Sabre\Node) { + $fileId = $node->getFileId(); + if (!is_null($fileId)) { + $this->server->httpResponse->setHeader('OC-FileId', $fileId); + } + } + } + +} diff --git a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php new file mode 100644 index 00000000000..d4e1cbe3b20 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php @@ -0,0 +1,333 @@ + + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OC\Files\View; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\PreconditionFailed; +use Sabre\DAV\Exception\ReportNotSupported; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Tree; +use Sabre\DAV\Xml\Element\Response; +use Sabre\DAV\Xml\Response\MultiStatus; +use Sabre\DAV\PropFind; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\IUserSession; +use OCP\Files\Folder; +use OCP\IGroupManager; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\TagNotFoundException; + +class FilesReportPlugin extends ServerPlugin { + + // namespace + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + const REPORT_NAME = '{http://owncloud.org/ns}filter-files'; + const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag'; + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @var Tree + */ + private $tree; + + /** + * @var View + */ + private $fileView; + + /** + * @var ISystemTagManager + */ + private $tagManager; + + /** + * @var ISystemTagObjectMapper + */ + private $tagMapper; + + /** + * @var IUserSession + */ + private $userSession; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var Folder + */ + private $userFolder; + + /** + * @param Tree $tree + * @param View $view + */ + public function __construct(Tree $tree, + View $view, + ISystemTagManager $tagManager, + ISystemTagObjectMapper $tagMapper, + IUserSession $userSession, + IGroupManager $groupManager, + Folder $userFolder + ) { + $this->tree = $tree; + $this->fileView = $view; + $this->tagManager = $tagManager; + $this->tagMapper = $tagMapper; + $this->userSession = $userSession; + $this->groupManager = $groupManager; + $this->userFolder = $userFolder; + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + + $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; + + $this->server = $server; + $this->server->on('report', array($this, 'onReport')); + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * + * @param string $uri + * @return array + */ + public function getSupportedReportSet($uri) { + return [self::REPORT_NAME]; + } + + /** + * REPORT operations to look for files + * + * @param string $reportName + * @param [] $report + * @param string $uri + * @return bool + * @throws NotFound + * @throws ReportNotSupported + */ + public function onReport($reportName, $report, $uri) { + $reportTargetNode = $this->server->tree->getNodeForPath($uri); + if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) { + throw new ReportNotSupported(); + } + + $ns = '{' . $this::NS_OWNCLOUD . '}'; + $requestedProps = []; + $filterRules = []; + + // parse report properties and gather filter info + foreach ($report as $reportProps) { + $name = $reportProps['name']; + if ($name === $ns . 'filter-rules') { + $filterRules = $reportProps['value']; + } else if ($name === '{DAV:}prop') { + // propfind properties + foreach ($reportProps['value'] as $propVal) { + $requestedProps[] = $propVal['name']; + } + } + } + + if (empty($filterRules)) { + // an empty filter would return all existing files which would be slow + throw new BadRequest('Missing filter-rule block in request'); + } + + // gather all file ids matching filter + try { + $resultFileIds = $this->processFilterRules($filterRules); + } catch (TagNotFoundException $e) { + throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e); + } + + // find sabre nodes by file id, restricted to the root node path + $results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds); + + $responses = $this->prepareResponses($requestedProps, $results); + + $xml = $this->server->xml->write( + '{DAV:}multistatus', + new MultiStatus($responses) + ); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody($xml); + + return false; + } + + /** + * Find file ids matching the given filter rules + * + * @param array $filterRules + * @return array array of unique file id results + * + * @throws TagNotFoundException whenever a tag was not found + */ + protected function processFilterRules($filterRules) { + $ns = '{' . $this::NS_OWNCLOUD . '}'; + $resultFileIds = null; + $systemTagIds = []; + foreach ($filterRules as $filterRule) { + if ($filterRule['name'] === $ns . 'systemtag') { + $systemTagIds[] = $filterRule['value']; + } + } + + // check user permissions, if applicable + if (!$this->isAdmin()) { + // check visibility/permission + $tags = $this->tagManager->getTagsByIds($systemTagIds); + $unknownTagIds = []; + foreach ($tags as $tag) { + if (!$tag->isUserVisible()) { + $unknownTagIds[] = $tag->getId(); + } + } + + if (!empty($unknownTagIds)) { + throw new TagNotFoundException('Tag with ids ' . implode(', ', $unknownTagIds) . ' not found'); + } + } + + // fetch all file ids and intersect them + foreach ($systemTagIds as $systemTagId) { + $fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files'); + + if (empty($fileIds)) { + // This tag has no files, nothing can ever show up + return []; + } + + // first run ? + if ($resultFileIds === null) { + $resultFileIds = $fileIds; + } else { + $resultFileIds = array_intersect($resultFileIds, $fileIds); + } + + if (empty($resultFileIds)) { + // Empty intersection, nothing can show up anymore + return []; + } + } + return $resultFileIds; + } + + /** + * Prepare propfind response for the given nodes + * + * @param string[] $requestedProps requested properties + * @param Node[] nodes nodes for which to fetch and prepare responses + * @return Response[] + */ + public function prepareResponses($requestedProps, $nodes) { + $responses = []; + foreach ($nodes as $node) { + $propFind = new PropFind($node->getPath(), $requestedProps); + + $this->server->getPropertiesByNode($propFind, $node); + // copied from Sabre Server's getPropertiesForPath + $result = $propFind->getResultForMultiStatus(); + $result['href'] = $propFind->getPath(); + + $resourceType = $this->server->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result['href'] .= '/'; + } + + $responses[] = new Response( + rtrim($this->server->getBaseUri(), '/') . $node->getPath(), + $result, + 200 + ); + } + return $responses; + } + + /** + * Find Sabre nodes by file ids + * + * @param Node $rootNode root node for search + * @param array $fileIds file ids + * @return Node[] array of Sabre nodes + */ + public function findNodesByFileIds($rootNode, $fileIds) { + $folder = $this->userFolder; + if (trim($rootNode->getPath(), '/') !== '') { + $folder = $folder->get($rootNode->getPath()); + } + + $results = []; + foreach ($fileIds as $fileId) { + $entry = $folder->getById($fileId); + if ($entry) { + $entry = current($entry); + if ($entry instanceof \OCP\Files\File) { + $results[] = new File($this->fileView, $entry); + } else if ($entry instanceof \OCP\Files\Folder) { + $results[] = new Directory($this->fileView, $entry); + } + } + } + + return $results; + } + + /** + * Returns whether the currently logged in user is an administrator + */ + private function isAdmin() { + $user = $this->userSession->getUser(); + if ($user !== null) { + return $this->groupManager->isAdmin($user->getUID()); + } + return false; + } +} diff --git a/apps/dav/lib/Connector/Sabre/LockPlugin.php b/apps/dav/lib/Connector/Sabre/LockPlugin.php new file mode 100644 index 00000000000..66da39a57c8 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/LockPlugin.php @@ -0,0 +1,84 @@ + + * @author Roeland Jago Douma + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\Exception\FileLocked; +use OCA\DAV\Connector\Sabre\Node; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; + +class LockPlugin extends ServerPlugin { + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * {@inheritdoc} + */ + public function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('beforeMethod', [$this, 'getLock'], 50); + $this->server->on('afterMethod', [$this, 'releaseLock'], 50); + } + + public function getLock(RequestInterface $request) { + // we can't listen on 'beforeMethod:PUT' due to order of operations with setting up the tree + // so instead we limit ourselves to the PUT method manually + if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) { + return; + } + try { + $node = $this->server->tree->getNodeForPath($request->getPath()); + } catch (NotFound $e) { + return; + } + if ($node instanceof Node) { + try { + $node->acquireLock(ILockingProvider::LOCK_SHARED); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + } + } + + public function releaseLock(RequestInterface $request) { + if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) { + return; + } + try { + $node = $this->server->tree->getNodeForPath($request->getPath()); + } catch (NotFound $e) { + return; + } + if ($node instanceof Node) { + $node->releaseLock(ILockingProvider::LOCK_SHARED); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php new file mode 100644 index 00000000000..6e9a5930b78 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php @@ -0,0 +1,92 @@ + + * @author Joas Schilling + * @author Morris Jobke + * @author Robin Appelman + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCP\IConfig; +use Sabre\DAV\Exception\ServiceUnavailable; +use Sabre\DAV\ServerPlugin; + +class MaintenancePlugin extends ServerPlugin { + + /** @var IConfig */ + private $config; + + /** + * Reference to main server object + * + * @var Server + */ + private $server; + + /** + * @param IConfig $config + */ + public function __construct(IConfig $config = null) { + $this->config = $config; + if (is_null($config)) { + $this->config = \OC::$server->getConfig(); + } + } + + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + $this->server = $server; + $this->server->on('beforeMethod', array($this, 'checkMaintenanceMode'), 1); + } + + /** + * This method is called before any HTTP method and returns http status code 503 + * in case the system is in maintenance mode. + * + * @throws ServiceUnavailable + * @return bool + */ + public function checkMaintenanceMode() { + if ($this->config->getSystemValue('singleuser', false)) { + throw new ServiceUnavailable('System in single user mode.'); + } + if ($this->config->getSystemValue('maintenance', false)) { + throw new ServiceUnavailable('System in maintenance mode.'); + } + if (\OC::checkUpgrade(false)) { + throw new ServiceUnavailable('Upgrade needed'); + } + + return true; + } +} diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php new file mode 100644 index 00000000000..ccc035063cd --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Node.php @@ -0,0 +1,348 @@ + + * @author Bart Visscher + * @author Jakob Sack + * @author Jörn Friedrich Dreyer + * @author Klaas Freitag + * @author Markus Goetz + * @author Morris Jobke + * @author Robin Appelman + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OC\Files\Mount\MoveableMount; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; + + +abstract class Node implements \Sabre\DAV\INode { + + /** + * @var \OC\Files\View + */ + protected $fileView; + + /** + * The path to the current node + * + * @var string + */ + protected $path; + + /** + * node properties cache + * + * @var array + */ + protected $property_cache = null; + + /** + * @var \OCP\Files\FileInfo + */ + protected $info; + + /** + * @var IManager + */ + protected $shareManager; + + /** + * Sets up the node, expects a full path name + * + * @param \OC\Files\View $view + * @param \OCP\Files\FileInfo $info + * @param IManager $shareManager + */ + public function __construct($view, $info, IManager $shareManager = null) { + $this->fileView = $view; + $this->path = $this->fileView->getRelativePath($info->getPath()); + $this->info = $info; + if ($shareManager) { + $this->shareManager = $shareManager; + } else { + $this->shareManager = \OC::$server->getShareManager(); + } + } + + protected function refreshInfo() { + $this->info = $this->fileView->getFileInfo($this->path); + } + + /** + * Returns the name of the node + * + * @return string + */ + public function getName() { + return $this->info->getName(); + } + + /** + * Returns the full path + * + * @return string + */ + public function getPath() { + return $this->path; + } + + /** + * Renames the node + * + * @param string $name The new name + * @throws \Sabre\DAV\Exception\BadRequest + * @throws \Sabre\DAV\Exception\Forbidden + */ + public function setName($name) { + + // rename is only allowed if the update privilege is granted + if (!$this->info->isUpdateable()) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + + list($parentPath,) = \Sabre\HTTP\URLUtil::splitPath($this->path); + list(, $newName) = \Sabre\HTTP\URLUtil::splitPath($name); + + // verify path of the target + $this->verifyPath(); + + $newPath = $parentPath . '/' . $newName; + + $this->fileView->rename($this->path, $newPath); + + $this->path = $newPath; + + $this->refreshInfo(); + } + + public function setPropertyCache($property_cache) { + $this->property_cache = $property_cache; + } + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int timestamp as integer + */ + public function getLastModified() { + $timestamp = $this->info->getMtime(); + if (!empty($timestamp)) { + return (int)$timestamp; + } + return $timestamp; + } + + /** + * sets the last modification time of the file (mtime) to the value given + * in the second parameter or to now if the second param is empty. + * Even if the modification time is set to a custom value the access time is set to now. + */ + public function touch($mtime) { + $this->fileView->touch($this->path, $mtime); + $this->refreshInfo(); + } + + /** + * 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 '"' . $this->info->getEtag() . '"'; + } + + /** + * Sets the ETag + * + * @param string $etag + * + * @return int file id of updated file or -1 on failure + */ + public function setETag($etag) { + return $this->fileView->putFileInfo($this->path, array('etag' => $etag)); + } + + /** + * Returns the size of the node, in bytes + * + * @return integer + */ + public function getSize() { + return $this->info->getSize(); + } + + /** + * Returns the cache's file id + * + * @return int + */ + public function getId() { + return $this->info->getId(); + } + + /** + * @return string|null + */ + public function getFileId() { + if ($this->info->getId()) { + $instanceId = \OC_Util::getInstanceId(); + $id = sprintf('%08d', $this->info->getId()); + return $id . $instanceId; + } + + return null; + } + + /** + * @return integer + */ + public function getInternalFileId() { + return $this->info->getId(); + } + + /** + * @param string $user + * @return int + */ + public function getSharePermissions($user) { + + // check of we access a federated share + if ($user !== null) { + try { + $share = $this->shareManager->getShareByToken($user); + return $share->getPermissions(); + } catch (ShareNotFound $e) { + // ignore + } + } + + $storage = $this->info->getStorage(); + + $path = $this->info->getInternalPath(); + + if ($storage->instanceOfStorage('\OC\Files\Storage\Shared')) { + /** @var \OC\Files\Storage\Shared $storage */ + $permissions = (int)$storage->getShare()->getPermissions(); + } else { + $permissions = $storage->getPermissions($path); + } + + /* + * We can always share non moveable mount points with DELETE and UPDATE + * Eventually we need to do this properly + */ + $mountpoint = $this->info->getMountPoint(); + if (!($mountpoint instanceof MoveableMount)) { + $mountpointpath = $mountpoint->getMountPoint(); + if (substr($mountpointpath, -1) === '/') { + $mountpointpath = substr($mountpointpath, 0, -1); + } + + if ($mountpointpath === $this->info->getPath()) { + $permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE; + } + } + + /* + * Files can't have create or delete permissions + */ + if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) { + $permissions &= ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE); + } + + return $permissions; + } + + /** + * @return string + */ + public function getDavPermissions() { + $p = ''; + if ($this->info->isShared()) { + $p .= 'S'; + } + if ($this->info->isShareable()) { + $p .= 'R'; + } + if ($this->info->isMounted()) { + $p .= 'M'; + } + if ($this->info->isDeletable()) { + $p .= 'D'; + } + if ($this->info->isUpdateable()) { + $p .= 'NV'; // Renameable, Moveable + } + if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) { + if ($this->info->isUpdateable()) { + $p .= 'W'; + } + } else { + if ($this->info->isCreatable()) { + $p .= 'CK'; + } + } + return $p; + } + + public function getOwner() { + return $this->info->getOwner(); + } + + protected function verifyPath() { + try { + $fileName = basename($this->info->getPath()); + $this->fileView->verifyPath($this->path, $fileName); + } catch (\OCP\Files\InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } + } + + /** + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + */ + public function acquireLock($type) { + $this->fileView->lockFile($this->path, $type); + } + + /** + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + */ + public function releaseLock($type) { + $this->fileView->unlockFile($this->path, $type); + } + + /** + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + */ + public function changeLock($type) { + $this->fileView->changeLock($this->path, $type); + } +} diff --git a/apps/dav/lib/Connector/Sabre/ObjectTree.php b/apps/dav/lib/Connector/Sabre/ObjectTree.php new file mode 100644 index 00000000000..f38dfe679c7 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php @@ -0,0 +1,297 @@ + + * @author Joas Schilling + * @author Morris Jobke + * @author Robin Appelman + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Connector\Sabre\Exception\FileLocked; +use OC\Files\FileInfo; +use OC\Files\Mount\MoveableMount; +use OCP\Files\ForbiddenException; +use OCP\Files\StorageInvalidException; +use OCP\Files\StorageNotAvailableException; +use OCP\Lock\LockedException; + +class ObjectTree extends \Sabre\DAV\Tree { + + /** + * @var \OC\Files\View + */ + protected $fileView; + + /** + * @var \OCP\Files\Mount\IMountManager + */ + protected $mountManager; + + /** + * Creates the object + */ + public function __construct() { + } + + /** + * @param \Sabre\DAV\INode $rootNode + * @param \OC\Files\View $view + * @param \OCP\Files\Mount\IMountManager $mountManager + */ + public function init(\Sabre\DAV\INode $rootNode, \OC\Files\View $view, \OCP\Files\Mount\IMountManager $mountManager) { + $this->rootNode = $rootNode; + $this->fileView = $view; + $this->mountManager = $mountManager; + } + + /** + * If the given path is a chunked file name, converts it + * to the real file name. Only applies if the OC-CHUNKED header + * is present. + * + * @param string $path chunk file path to convert + * + * @return string path to real file + */ + private function resolveChunkFile($path) { + if (isset($_SERVER['HTTP_OC_CHUNKED'])) { + // resolve to real file name to find the proper node + list($dir, $name) = \Sabre\HTTP\URLUtil::splitPath($path); + if ($dir == '/' || $dir == '.') { + $dir = ''; + } + + $info = \OC_FileChunking::decodeName($name); + // only replace path if it was really the chunked file + if (isset($info['transferid'])) { + // getNodePath is called for multiple nodes within a chunk + // upload call + $path = $dir . '/' . $info['name']; + $path = ltrim($path, '/'); + } + } + return $path; + } + + public function cacheNode(Node $node) { + $this->cache[trim($node->getPath(), '/')] = $node; + } + + /** + * Returns the INode object for the requested path + * + * @param string $path + * @return \Sabre\DAV\INode + * @throws InvalidPath + * @throws \Sabre\DAV\Exception\Locked + * @throws \Sabre\DAV\Exception\NotFound + * @throws \Sabre\DAV\Exception\ServiceUnavailable + */ + public function getNodeForPath($path) { + if (!$this->fileView) { + throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup'); + } + + $path = trim($path, '/'); + + if (isset($this->cache[$path])) { + return $this->cache[$path]; + } + + if ($path) { + try { + $this->fileView->verifyPath($path, basename($path)); + } catch (\OCP\Files\InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } + } + + // Is it the root node? + if (!strlen($path)) { + return $this->rootNode; + } + + if (pathinfo($path, PATHINFO_EXTENSION) === 'part') { + // read from storage + $absPath = $this->fileView->getAbsolutePath($path); + $mount = $this->fileView->getMount($path); + $storage = $mount->getStorage(); + $internalPath = $mount->getInternalPath($absPath); + if ($storage && $storage->file_exists($internalPath)) { + /** + * @var \OC\Files\Storage\Storage $storage + */ + // get data directly + $data = $storage->getMetaData($internalPath); + $info = new FileInfo($absPath, $storage, $internalPath, $data, $mount); + } else { + $info = null; + } + } else { + // resolve chunk file name to real name, if applicable + $path = $this->resolveChunkFile($path); + + // read from cache + try { + $info = $this->fileView->getFileInfo($path); + } catch (StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable('Storage not available'); + } catch (StorageInvalidException $e) { + throw new \Sabre\DAV\Exception\NotFound('Storage ' . $path . ' is invalid'); + } catch (LockedException $e) { + throw new \Sabre\DAV\Exception\Locked(); + } + } + + if (!$info) { + throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located'); + } + + if ($info->getType() === 'dir') { + $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this); + } else { + $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info); + } + + $this->cache[$path] = $node; + return $node; + + } + + /** + * 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 + * @throws \Sabre\DAV\Exception\BadRequest + * @throws \Sabre\DAV\Exception\ServiceUnavailable + * @throws \Sabre\DAV\Exception\Forbidden + * @return int + */ + public function move($sourcePath, $destinationPath) { + if (!$this->fileView) { + throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup'); + } + + $targetNodeExists = $this->nodeExists($destinationPath); + $sourceNode = $this->getNodeForPath($sourcePath); + if ($sourceNode instanceof \Sabre\DAV\ICollection && $targetNodeExists) { + throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists'); + } + list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($sourcePath); + list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destinationPath); + + $isMovableMount = false; + $sourceMount = $this->mountManager->find($this->fileView->getAbsolutePath($sourcePath)); + $internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath)); + if ($sourceMount instanceof MoveableMount && $internalPath === '') { + $isMovableMount = true; + } + + try { + $sameFolder = ($sourceDir === $destinationDir); + // if we're overwriting or same folder + if ($targetNodeExists || $sameFolder) { + // note that renaming a share mount point is always allowed + if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + } else { + if (!$this->fileView->isCreatable($destinationDir)) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + } + + if (!$sameFolder) { + // moving to a different folder, source will be gone, like a deletion + // note that moving a share mount point is always allowed + if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + } + + $fileName = basename($destinationPath); + try { + $this->fileView->verifyPath($destinationDir, $fileName); + } catch (\OCP\Files\InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } + + $renameOkay = $this->fileView->rename($sourcePath, $destinationPath); + if (!$renameOkay) { + throw new \Sabre\DAV\Exception\Forbidden(''); + } + } catch (StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + + $this->markDirty($sourceDir); + $this->markDirty($destinationDir); + + } + + /** + * Copies a file or directory. + * + * This method must work recursively and delete the destination + * if it exists + * + * @param string $source + * @param string $destination + * @throws \Sabre\DAV\Exception\ServiceUnavailable + * @return void + */ + public function copy($source, $destination) { + if (!$this->fileView) { + throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup'); + } + + // this will trigger existence check + $this->getNodeForPath($source); + + list($destinationDir, $destinationName) = \Sabre\HTTP\URLUtil::splitPath($destination); + try { + $this->fileView->verifyPath($destinationDir, $destinationName); + } catch (\OCP\Files\InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } + + try { + $this->fileView->copy($source, $destination); + } catch (StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + + list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination); + $this->markDirty($destinationDir); + } +} diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php new file mode 100644 index 00000000000..787bcdf469b --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -0,0 +1,234 @@ + + * @author Jakob Sack + * @author Jörn Friedrich Dreyer + * @author Lukas Reschke + * @author Morris Jobke + * @author Thomas Müller + * @author Thomas Tanghus + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use Sabre\DAV\Exception; +use \Sabre\DAV\PropPatch; +use Sabre\DAVACL\PrincipalBackend\BackendInterface; +use Sabre\HTTP\URLUtil; + +class Principal implements BackendInterface { + + /** @var IUserManager */ + private $userManager; + + /** @var IGroupManager */ + private $groupManager; + + /** @var string */ + private $principalPrefix; + + /** @var bool */ + private $hasGroups; + + /** + * @param IUserManager $userManager + * @param IGroupManager $groupManager + * @param string $principalPrefix + */ + public function __construct(IUserManager $userManager, + IGroupManager $groupManager, + $principalPrefix = 'principals/users/') { + $this->userManager = $userManager; + $this->groupManager = $groupManager; + $this->principalPrefix = trim($principalPrefix, '/'); + $this->hasGroups = ($principalPrefix === 'principals/users/'); + } + + /** + * 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 + * + * @param string $prefixPath + * @return string[] + */ + public function getPrincipalsByPrefix($prefixPath) { + $principals = []; + + if ($prefixPath === $this->principalPrefix) { + foreach($this->userManager->search('') as $user) { + $principals[] = $this->userToPrincipal($user); + } + } + + 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) { + list($prefix, $name) = URLUtil::splitPath($path); + + if ($prefix === $this->principalPrefix) { + $user = $this->userManager->get($name); + + if (!is_null($user)) { + return $this->userToPrincipal($user); + } + } + return null; + } + + /** + * Returns the list of members for a group-principal + * + * @param string $principal + * @return string[] + * @throws Exception + */ + public function getGroupMemberSet($principal) { + // TODO: for now the group principal has only one member, the user itself + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new Exception('Principal not found'); + } + + return [$principal['uri']]; + } + + /** + * Returns the list of groups a principal is a member of + * + * @param string $principal + * @param bool $needGroups + * @return array + * @throws Exception + */ + public function getGroupMembership($principal, $needGroups = false) { + list($prefix, $name) = URLUtil::splitPath($principal); + + if ($prefix === $this->principalPrefix) { + $user = $this->userManager->get($name); + if (!$user) { + throw new Exception('Principal not found'); + } + + if ($this->hasGroups || $needGroups) { + $groups = $this->groupManager->getUserGroups($user); + $groups = array_map(function($group) { + /** @var IGroup $group */ + return 'principals/groups/' . $group->getGID(); + }, $groups); + + return $groups; + } + } + return []; + } + + /** + * 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 string[] $members + * @throws Exception + */ + public function setGroupMemberSet($principal, array $members) { + throw new Exception('Setting members of the group is not supported yet'); + } + + /** + * @param string $path + * @param PropPatch $propPatch + * @return int + */ + function updatePrincipal($path, PropPatch $propPatch) { + return 0; + } + + /** + * @param string $prefixPath + * @param array $searchProperties + * @param string $test + * @return array + */ + function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { + return []; + } + + /** + * @param string $uri + * @param string $principalPrefix + * @return string + */ + function findByUri($uri, $principalPrefix) { + if (substr($uri, 0, 7) === 'mailto:') { + $email = substr($uri, 7); + $users = $this->userManager->getByEmail($email); + if (count($users) === 1) { + return $this->principalPrefix . '/' . $users[0]->getUID(); + } + } + + return ''; + } + + /** + * @param IUser $user + * @return array + */ + protected function userToPrincipal($user) { + $userId = $user->getUID(); + $displayName = $user->getDisplayName(); + $principal = [ + 'uri' => $this->principalPrefix . '/' . $userId, + '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName, + ]; + + $email = $user->getEMailAddress(); + if (!empty($email)) { + $principal['{http://sabredav.org/ns}email-address'] = $email; + return $principal; + } + return $principal; + } + + public function getPrincipalPrefix() { + return $this->principalPrefix; + } + +} diff --git a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php new file mode 100644 index 00000000000..a093c52851c --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php @@ -0,0 +1,146 @@ + + * @author Morris Jobke + * @author Robin Appelman + * @author scambra + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Connector\Sabre; + +/** + * This plugin check user quota and deny creating files when they exceeds the quota. + * + * @author Sergio Cambra + * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved. + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class QuotaPlugin extends \Sabre\DAV\ServerPlugin { + + /** + * @var \OC\Files\View + */ + private $view; + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @param \OC\Files\View $view + */ + public function __construct($view) { + $this->view = $view; + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the requires event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + + $this->server = $server; + + $server->on('beforeWriteContent', array($this, 'checkQuota'), 10); + $server->on('beforeCreateFile', array($this, 'checkQuota'), 10); + } + + /** + * This method is called before any HTTP method and validates there is enough free space to store the file + * + * @param string $uri + * @param null $data + * @throws \Sabre\DAV\Exception\InsufficientStorage + * @return bool + */ + public function checkQuota($uri, $data = null) { + $length = $this->getLength(); + if ($length) { + if (substr($uri, 0, 1) !== '/') { + $uri = '/' . $uri; + } + list($parentUri, $newName) = \Sabre\HTTP\URLUtil::splitPath($uri); + if(is_null($parentUri)) { + $parentUri = ''; + } + $req = $this->server->httpRequest; + if ($req->getHeader('OC-Chunked')) { + $info = \OC_FileChunking::decodeName($newName); + $chunkHandler = $this->getFileChunking($info); + // subtract the already uploaded size to see whether + // there is still enough space for the remaining chunks + $length -= $chunkHandler->getCurrentSize(); + // use target file name for free space check in case of shared files + $uri = rtrim($parentUri, '/') . '/' . $info['name']; + } + $freeSpace = $this->getFreeSpace($uri); + if ($freeSpace !== \OCP\Files\FileInfo::SPACE_UNKNOWN && $length > $freeSpace) { + if (isset($chunkHandler)) { + $chunkHandler->cleanup(); + } + throw new \Sabre\DAV\Exception\InsufficientStorage(); + } + } + return true; + } + + public function getFileChunking($info) { + // FIXME: need a factory for better mocking support + return new \OC_FileChunking($info); + } + + public function getLength() { + $req = $this->server->httpRequest; + $length = $req->getHeader('X-Expected-Entity-Length'); + if (!$length) { + $length = $req->getHeader('Content-Length'); + } + + $ocLength = $req->getHeader('OC-Total-Length'); + if ($length && $ocLength) { + return max($length, $ocLength); + } + + return $length; + } + + /** + * @param string $uri + * @return mixed + */ + public function getFreeSpace($uri) { + try { + $freeSpace = $this->view->free_space(ltrim($uri, '/')); + return $freeSpace; + } catch (\OCP\Files\StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/Server.php b/apps/dav/lib/Connector/Sabre/Server.php new file mode 100644 index 00000000000..421fc64422d --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/Server.php @@ -0,0 +1,44 @@ + + * @author scolebrook + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +/** + * Class \OCA\DAV\Connector\Sabre\Server + * + * This class overrides some methods from @see \Sabre\DAV\Server. + * + * @see \Sabre\DAV\Server + */ +class Server extends \Sabre\DAV\Server { + + /** + * @see \Sabre\DAV\Server + */ + public function __construct($treeOrNode = null) { + parent::__construct($treeOrNode); + self::$exposeVersion = false; + $this->enablePropfindDepthInfinity = true; + } +} diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php new file mode 100644 index 00000000000..5853370778d --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -0,0 +1,184 @@ + + * @author Joas Schilling + * @author Lukas Reschke + * @author Robin Appelman + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use OCA\DAV\Files\BrowserErrorPagePlugin; +use OCP\Files\Mount\IMountManager; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ILogger; +use OCP\IRequest; +use OCP\ITagManager; +use OCP\IUserSession; +use Sabre\DAV\Auth\Backend\BackendInterface; + +class ServerFactory { + /** @var IConfig */ + private $config; + /** @var ILogger */ + private $logger; + /** @var IDBConnection */ + private $databaseConnection; + /** @var IUserSession */ + private $userSession; + /** @var IMountManager */ + private $mountManager; + /** @var ITagManager */ + private $tagManager; + /** @var IRequest */ + private $request; + + /** + * @param IConfig $config + * @param ILogger $logger + * @param IDBConnection $databaseConnection + * @param IUserSession $userSession + * @param IMountManager $mountManager + * @param ITagManager $tagManager + * @param IRequest $request + */ + public function __construct( + IConfig $config, + ILogger $logger, + IDBConnection $databaseConnection, + IUserSession $userSession, + IMountManager $mountManager, + ITagManager $tagManager, + IRequest $request + ) { + $this->config = $config; + $this->logger = $logger; + $this->databaseConnection = $databaseConnection; + $this->userSession = $userSession; + $this->mountManager = $mountManager; + $this->tagManager = $tagManager; + $this->request = $request; + } + + /** + * @param string $baseUri + * @param string $requestUri + * @param BackendInterface $authBackend + * @param callable $viewCallBack callback that should return the view for the dav endpoint + * @return Server + */ + public function createServer($baseUri, + $requestUri, + BackendInterface $authBackend, + callable $viewCallBack) { + // Fire up server + $objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree(); + $server = new \OCA\DAV\Connector\Sabre\Server($objectTree); + // Set URL explicitly due to reverse-proxy situations + $server->httpRequest->setUrl($requestUri); + $server->setBaseUri($baseUri); + + // Load plugins + $defaults = new \OC_Defaults(); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config)); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config)); + $server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, $defaults->getName())); + // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / + $server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin()); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger)); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); + // Some WebDAV clients do require Class 2 WebDAV support (locking), since + // we do not provide locking we emulate it using a fake locking plugin. + if($this->request->isUserAgent([ + '/WebDAVFS/', + '/Microsoft Office OneNote 2013/', + '/Microsoft-WebDAV-MiniRedir/', + ])) { + $server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin()); + } + + if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) { + $server->addPlugin(new BrowserErrorPagePlugin()); + } + + // wait with registering these until auth is handled and the filesystem is setup + $server->on('beforeMethod', function () use ($server, $objectTree, $viewCallBack) { + // ensure the skeleton is copied + $userFolder = \OC::$server->getUserFolder(); + + /** @var \OC\Files\View $view */ + $view = $viewCallBack($server); + $rootInfo = $view->getFileInfo(''); + + // Create ownCloud Dir + if ($rootInfo->getType() === 'dir') { + $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree); + } else { + $root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo); + } + $objectTree->init($root, $view, $this->mountManager); + + $server->addPlugin( + new \OCA\DAV\Connector\Sabre\FilesPlugin( + $objectTree, + $view, + $this->config, + false, + !$this->config->getSystemValue('debug', false) + ) + ); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view)); + + if($this->userSession->isLoggedIn()) { + $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager)); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin( + $objectTree, + $this->userSession, + $userFolder, + \OC::$server->getShareManager() + )); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin(\OC::$server->getCommentsManager(), $this->userSession)); + $server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesReportPlugin( + $objectTree, + $view, + \OC::$server->getSystemTagManager(), + \OC::$server->getSystemTagObjectMapper(), + $this->userSession, + \OC::$server->getGroupManager(), + $userFolder + )); + // custom properties plugin must be the last one + $server->addPlugin( + new \Sabre\DAV\PropertyStorage\Plugin( + new \OCA\DAV\Connector\Sabre\CustomPropertiesBackend( + $objectTree, + $this->databaseConnection, + $this->userSession->getUser() + ) + ) + ); + } + $server->addPlugin(new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin()); + }, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request + return $server; + } +} diff --git a/apps/dav/lib/Connector/Sabre/ShareTypeList.php b/apps/dav/lib/Connector/Sabre/ShareTypeList.php new file mode 100644 index 00000000000..763586412ad --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ShareTypeList.php @@ -0,0 +1,87 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use Sabre\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; + +/** + * ShareTypeList property + * + * This property contains multiple "share-type" elements, each containing a share type. + */ +class ShareTypeList implements Element { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * Share types + * + * @var int[] + */ + private $shareTypes; + + /** + * @param int[] $shareTypes + */ + public function __construct($shareTypes) { + $this->shareTypes = $shareTypes; + } + + /** + * Returns the share types + * + * @return int[] + */ + public function getShareTypes() { + return $this->shareTypes; + } + + /** + * The deserialize method is called during xml parsing. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + $shareTypes = []; + + foreach ($reader->parseInnerTree() as $elem) { + if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}share-type') { + $shareTypes[] = (int)$elem['value']; + } + } + return new self($shareTypes); + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + foreach ($this->shareTypes as $shareType) { + $writer->writeElement('{' . self::NS_OWNCLOUD . '}share-type', $shareType); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php new file mode 100644 index 00000000000..c76068969e9 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -0,0 +1,177 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Connector\Sabre; + +use \Sabre\DAV\PropFind; +use \Sabre\DAV\PropPatch; +use OCP\IUserSession; +use OCP\Share\IShare; +use OCA\DAV\Connector\Sabre\ShareTypeList; + +/** + * Sabre Plugin to provide share-related properties + */ +class SharesPlugin extends \Sabre\DAV\ServerPlugin { + + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + const SHARETYPES_PROPERTYNAME = '{http://owncloud.org/ns}share-types'; + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @var \OCP\Share\IManager + */ + private $shareManager; + + /** + * @var \Sabre\DAV\Tree + */ + private $tree; + + /** + * @var string + */ + private $userId; + + /** + * @var \OCP\Files\Folder + */ + private $userFolder; + + /** + * @var IShare[] + */ + private $cachedShareTypes; + + /** + * @param \Sabre\DAV\Tree $tree tree + * @param IUserSession $userSession user session + * @param \OCP\Files\Folder $userFolder user home folder + * @param \OCP\Share\IManager $shareManager share manager + */ + public function __construct( + \Sabre\DAV\Tree $tree, + IUserSession $userSession, + \OCP\Files\Folder $userFolder, + \OCP\Share\IManager $shareManager + ) { + $this->tree = $tree; + $this->shareManager = $shareManager; + $this->userFolder = $userFolder; + $this->userId = $userSession->getUser()->getUID(); + $this->cachedShareTypes = []; + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + */ + public function initialize(\Sabre\DAV\Server $server) { + $server->xml->namespacesMap[self::NS_OWNCLOUD] = 'oc'; + $server->xml->elementMap[self::SHARETYPES_PROPERTYNAME] = 'OCA\\DAV\\Connector\\Sabre\\ShareTypeList'; + $server->protectedProperties[] = self::SHARETYPES_PROPERTYNAME; + + $this->server = $server; + $this->server->on('propFind', array($this, 'handleGetProperties')); + } + + /** + * Return a list of share types for outgoing shares + * + * @param \OCP\Files\Node $node file node + * + * @return int[] array of share types + */ + private function getShareTypes(\OCP\Files\Node $node) { + $shareTypes = []; + $requestedShareTypes = [ + \OCP\Share::SHARE_TYPE_USER, + \OCP\Share::SHARE_TYPE_GROUP, + \OCP\Share::SHARE_TYPE_LINK, + \OCP\Share::SHARE_TYPE_REMOTE + ]; + foreach ($requestedShareTypes as $requestedShareType) { + // one of each type is enough to find out about the types + $shares = $this->shareManager->getSharesBy( + $this->userId, + $requestedShareType, + $node, + false, + 1 + ); + if (!empty($shares)) { + $shareTypes[] = $requestedShareType; + } + } + return $shareTypes; + } + + /** + * Adds shares to propfind response + * + * @param PropFind $propFind propfind object + * @param \Sabre\DAV\INode $sabreNode sabre node + */ + public function handleGetProperties( + PropFind $propFind, + \Sabre\DAV\INode $sabreNode + ) { + if (!($sabreNode instanceof \OCA\DAV\Connector\Sabre\Node)) { + return; + } + + // need prefetch ? + if ($sabreNode instanceof \OCA\DAV\Connector\Sabre\Directory + && $propFind->getDepth() !== 0 + && !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) + ) { + $folderNode = $this->userFolder->get($propFind->getPath()); + $children = $folderNode->getDirectoryListing(); + + $this->cachedShareTypes[$folderNode->getId()] = $this->getShareTypes($folderNode); + foreach ($children as $childNode) { + $this->cachedShareTypes[$childNode->getId()] = $this->getShareTypes($childNode); + } + } + + $propFind->handle(self::SHARETYPES_PROPERTYNAME, function() use ($sabreNode) { + if (isset($this->cachedShareTypes[$sabreNode->getId()])) { + $shareTypes = $this->cachedShareTypes[$sabreNode->getId()]; + } else { + $node = $this->userFolder->get($sabreNode->getPath()); + $shareTypes = $this->getShareTypes($node); + } + + return new ShareTypeList($shareTypes); + }); + } +} diff --git a/apps/dav/lib/Connector/Sabre/TagList.php b/apps/dav/lib/Connector/Sabre/TagList.php new file mode 100644 index 00000000000..5c1cd8b4f1d --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/TagList.php @@ -0,0 +1,120 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use Sabre\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; + +/** + * TagList property + * + * This property contains multiple "tag" elements, each containing a tag name. + */ +class TagList implements Element { + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * tags + * + * @var array + */ + private $tags; + + /** + * @param array $tags + */ + public function __construct(array $tags) { + $this->tags = $tags; + } + + /** + * Returns the tags + * + * @return array + */ + public function getTags() { + + return $this->tags; + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + $tags = []; + + foreach ($reader->parseInnerTree() as $elem) { + if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}tag') { + $tags[] = $elem['value']; + } + } + return new self($tags); + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->tags as $tag) { + $writer->writeElement('{' . self::NS_OWNCLOUD . '}tag', $tag); + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php new file mode 100644 index 00000000000..dfc1a2dd95d --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php @@ -0,0 +1,293 @@ + + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Connector\Sabre; + +/** + * ownCloud + * + * @author Vincent Petry + * @copyright 2014 Vincent Petry + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 library. If not, see . + * + */ + +use \Sabre\DAV\PropFind; +use \Sabre\DAV\PropPatch; + +class TagsPlugin extends \Sabre\DAV\ServerPlugin +{ + + // namespace + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags'; + const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite'; + const TAG_FAVORITE = '_$!!$_'; + + /** + * Reference to main server object + * + * @var \Sabre\DAV\Server + */ + private $server; + + /** + * @var \OCP\ITagManager + */ + private $tagManager; + + /** + * @var \OCP\ITags + */ + private $tagger; + + /** + * Array of file id to tags array + * The null value means the cache wasn't initialized. + * + * @var array + */ + private $cachedTags; + + /** + * @var \Sabre\DAV\Tree + */ + private $tree; + + /** + * @param \Sabre\DAV\Tree $tree tree + * @param \OCP\ITagManager $tagManager tag manager + */ + public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) { + $this->tree = $tree; + $this->tagManager = $tagManager; + $this->tagger = null; + $this->cachedTags = array(); + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + + $server->xml->namespacesMap[self::NS_OWNCLOUD] = 'oc'; + $server->xml->elementMap[self::TAGS_PROPERTYNAME] = 'OCA\\DAV\\Connector\\Sabre\\TagList'; + + $this->server = $server; + $this->server->on('propFind', array($this, 'handleGetProperties')); + $this->server->on('propPatch', array($this, 'handleUpdateProperties')); + } + + /** + * Returns the tagger + * + * @return \OCP\ITags tagger + */ + private function getTagger() { + if (!$this->tagger) { + $this->tagger = $this->tagManager->load('files'); + } + return $this->tagger; + } + + /** + * Returns tags and favorites. + * + * @param integer $fileId file id + * @return array list($tags, $favorite) with $tags as tag array + * and $favorite is a boolean whether the file was favorited + */ + private function getTagsAndFav($fileId) { + $isFav = false; + $tags = $this->getTags($fileId); + if ($tags) { + $favPos = array_search(self::TAG_FAVORITE, $tags); + if ($favPos !== false) { + $isFav = true; + unset($tags[$favPos]); + } + } + return array($tags, $isFav); + } + + /** + * Returns tags for the given file id + * + * @param integer $fileId file id + * @return array list of tags for that file + */ + private function getTags($fileId) { + if (isset($this->cachedTags[$fileId])) { + return $this->cachedTags[$fileId]; + } else { + $tags = $this->getTagger()->getTagsForObjects(array($fileId)); + if ($tags !== false) { + if (empty($tags)) { + return array(); + } + return current($tags); + } + } + return null; + } + + /** + * Updates the tags of the given file id + * + * @param int $fileId + * @param array $tags array of tag strings + */ + private function updateTags($fileId, $tags) { + $tagger = $this->getTagger(); + $currentTags = $this->getTags($fileId); + + $newTags = array_diff($tags, $currentTags); + foreach ($newTags as $tag) { + if ($tag === self::TAG_FAVORITE) { + continue; + } + $tagger->tagAs($fileId, $tag); + } + $deletedTags = array_diff($currentTags, $tags); + foreach ($deletedTags as $tag) { + if ($tag === self::TAG_FAVORITE) { + continue; + } + $tagger->unTag($fileId, $tag); + } + } + + /** + * Adds tags and favorites properties to the response, + * if requested. + * + * @param PropFind $propFind + * @param \Sabre\DAV\INode $node + * @return void + */ + public function handleGetProperties( + PropFind $propFind, + \Sabre\DAV\INode $node + ) { + if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) { + return; + } + + // need prefetch ? + if ($node instanceof \OCA\DAV\Connector\Sabre\Directory + && $propFind->getDepth() !== 0 + && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME)) + || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME)) + )) { + // note: pre-fetching only supported for depth <= 1 + $folderContent = $node->getChildren(); + $fileIds[] = (int)$node->getId(); + foreach ($folderContent as $info) { + $fileIds[] = (int)$info->getId(); + } + $tags = $this->getTagger()->getTagsForObjects($fileIds); + if ($tags === false) { + // the tags API returns false on error... + $tags = array(); + } + + $this->cachedTags = $this->cachedTags + $tags; + $emptyFileIds = array_diff($fileIds, array_keys($tags)); + // also cache the ones that were not found + foreach ($emptyFileIds as $fileId) { + $this->cachedTags[$fileId] = []; + } + } + + $tags = null; + $isFav = null; + + $propFind->handle(self::TAGS_PROPERTYNAME, function() use ($tags, &$isFav, $node) { + list($tags, $isFav) = $this->getTagsAndFav($node->getId()); + return new TagList($tags); + }); + + $propFind->handle(self::FAVORITE_PROPERTYNAME, function() use ($isFav, $node) { + if (is_null($isFav)) { + list(, $isFav) = $this->getTagsAndFav($node->getId()); + } + return $isFav; + }); + } + + /** + * Updates tags and favorites properties, if applicable. + * + * @param string $path + * @param PropPatch $propPatch + * + * @return void + */ + public function handleUpdateProperties($path, PropPatch $propPatch) { + $propPatch->handle(self::TAGS_PROPERTYNAME, function($tagList) use ($path) { + $node = $this->tree->getNodeForPath($path); + if (is_null($node)) { + return 404; + } + $this->updateTags($node->getId(), $tagList->getTags()); + return true; + }); + + $propPatch->handle(self::FAVORITE_PROPERTYNAME, function($favState) use ($path) { + $node = $this->tree->getNodeForPath($path); + if (is_null($node)) { + return 404; + } + if ((int)$favState === 1 || $favState === 'true') { + $this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE); + } else { + $this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE); + } + + if (is_null($favState)) { + // confirm deletion + return 204; + } + + return 200; + }); + } +} diff --git a/apps/dav/lib/DAV/GroupPrincipalBackend.php b/apps/dav/lib/DAV/GroupPrincipalBackend.php new file mode 100644 index 00000000000..e0568639784 --- /dev/null +++ b/apps/dav/lib/DAV/GroupPrincipalBackend.php @@ -0,0 +1,200 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\DAV; + +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use Sabre\DAV\Exception; +use \Sabre\DAV\PropPatch; +use Sabre\DAVACL\PrincipalBackend\BackendInterface; + +class GroupPrincipalBackend implements BackendInterface { + + const PRINCIPAL_PREFIX = 'principals/groups'; + + /** @var IGroupManager */ + private $groupManager; + + /** + * @param IGroupManager $IGroupManager + */ + public function __construct(IGroupManager $IGroupManager) { + $this->groupManager = $IGroupManager; + } + + /** + * 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 + * + * @param string $prefixPath + * @return string[] + */ + public function getPrincipalsByPrefix($prefixPath) { + $principals = []; + + if ($prefixPath === self::PRINCIPAL_PREFIX) { + foreach($this->groupManager->search('') as $user) { + $principals[] = $this->groupToPrincipal($user); + } + } + + 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) { + $elements = explode('/', $path); + if ($elements[0] !== 'principals') { + return null; + } + if ($elements[1] !== 'groups') { + return null; + } + $name = $elements[2]; + $group = $this->groupManager->get($name); + + if (!is_null($group)) { + return $this->groupToPrincipal($group); + } + + return null; + } + + /** + * Returns the list of members for a group-principal + * + * @param string $principal + * @return string[] + * @throws Exception + */ + public function getGroupMemberSet($principal) { + $elements = explode('/', $principal); + if ($elements[0] !== 'principals') { + return []; + } + if ($elements[1] !== 'groups') { + return []; + } + $name = $elements[2]; + $group = $this->groupManager->get($name); + + if (is_null($group)) { + return []; + } + + return array_map(function($user) { + return $this->userToPrincipal($user); + }, $group->getUsers()); + } + + /** + * Returns the list of groups a principal is a member of + * + * @param string $principal + * @return array + * @throws Exception + */ + public function getGroupMembership($principal) { + return []; + } + + /** + * 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 string[] $members + * @throws Exception + */ + public function setGroupMemberSet($principal, array $members) { + throw new Exception('Setting members of the group is not supported yet'); + } + + /** + * @param string $path + * @param PropPatch $propPatch + * @return int + */ + function updatePrincipal($path, PropPatch $propPatch) { + return 0; + } + + /** + * @param string $prefixPath + * @param array $searchProperties + * @param string $test + * @return array + */ + function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { + return []; + } + + /** + * @param string $uri + * @param string $principalPrefix + * @return string + */ + function findByUri($uri, $principalPrefix) { + return ''; + } + + /** + * @param IGroup $group + * @return array + */ + protected function groupToPrincipal($group) { + $groupId = $group->getGID(); + $principal = [ + 'uri' => "principals/groups/$groupId", + '{DAV:}displayname' => $groupId, + ]; + + return $principal; + } + + /** + * @param IUser $user + * @return array + */ + protected function userToPrincipal($user) { + $principal = [ + 'uri' => 'principals/users/' . $user->getUID(), + '{DAV:}displayname' => $user->getDisplayName(), + ]; + + return $principal; + } +} diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php new file mode 100644 index 00000000000..225b773713d --- /dev/null +++ b/apps/dav/lib/DAV/Sharing/Backend.php @@ -0,0 +1,206 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\DAV\Sharing; + +use OCA\DAV\Connector\Sabre\Principal; +use OCP\IDBConnection; + +class Backend { + + /** @var IDBConnection */ + private $db; + /** @var Principal */ + private $principalBackend; + /** @var string */ + private $resourceType; + + const ACCESS_OWNER = 1; + const ACCESS_READ_WRITE = 2; + const ACCESS_READ = 3; + + /** + * @param IDBConnection $db + * @param Principal $principalBackend + * @param string $resourceType + */ + public function __construct(IDBConnection $db, Principal $principalBackend, $resourceType) { + $this->db = $db; + $this->principalBackend = $principalBackend; + $this->resourceType = $resourceType; + } + + /** + * @param IShareable $shareable + * @param string[] $add + * @param string[] $remove + */ + public function updateShares($shareable, $add, $remove) { + foreach($add as $element) { + $this->shareWith($shareable, $element); + } + foreach($remove as $element) { + $this->unshare($shareable, $element); + } + } + + /** + * @param IShareable $shareable + * @param string $element + */ + private function shareWith($shareable, $element) { + $user = $element['href']; + $parts = explode(':', $user, 2); + if ($parts[0] !== 'principal') { + return; + } + + // don't share with owner + if ($shareable->getOwner() === $parts[1]) { + return; + } + + // remove the share if it already exists + $this->unshare($shareable, $element['href']); + $access = self::ACCESS_READ; + if (isset($element['readOnly'])) { + $access = $element['readOnly'] ? self::ACCESS_READ : self::ACCESS_READ_WRITE; + } + + $query = $this->db->getQueryBuilder(); + $query->insert('dav_shares') + ->values([ + 'principaluri' => $query->createNamedParameter($parts[1]), + 'type' => $query->createNamedParameter($this->resourceType), + 'access' => $query->createNamedParameter($access), + 'resourceid' => $query->createNamedParameter($shareable->getResourceId()) + ]); + $query->execute(); + } + + /** + * @param $resourceId + */ + public function deleteAllShares($resourceId) { + $query = $this->db->getQueryBuilder(); + $query->delete('dav_shares') + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId))) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) + ->execute(); + } + + /** + * @param IShareable $shareable + * @param string $element + */ + private function unshare($shareable, $element) { + $parts = explode(':', $element, 2); + if ($parts[0] !== 'principal') { + return; + } + + // don't share with owner + if ($shareable->getOwner() === $parts[1]) { + return; + } + + $query = $this->db->getQueryBuilder(); + $query->delete('dav_shares') + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($shareable->getResourceId()))) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($parts[1]))) + ; + $query->execute(); + } + + /** + * Returns the list of people whom this resource is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @param int $resourceId + * @return array + */ + public function getShares($resourceId) { + $query = $this->db->getQueryBuilder(); + $result = $query->select(['principaluri', 'access']) + ->from('dav_shares') + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId))) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) + ->execute(); + + $shares = []; + while($row = $result->fetch()) { + $p = $this->principalBackend->getPrincipalByPath($row['principaluri']); + $shares[]= [ + 'href' => "principal:${row['principaluri']}", + 'commonName' => isset($p['{DAV:}displayname']) ? $p['{DAV:}displayname'] : '', + 'status' => 1, + 'readOnly' => ($row['access'] == self::ACCESS_READ), + '{http://owncloud.org/ns}principal' => $row['principaluri'], + '{http://owncloud.org/ns}group-share' => is_null($p) + ]; + } + + return $shares; + } + + /** + * For shared resources the sharee is set in the ACL of the resource + * + * @param int $resourceId + * @param array $acl + * @return array + */ + public function applyShareAcl($resourceId, $acl) { + + $shares = $this->getShares($resourceId); + foreach ($shares as $share) { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'], + 'protected' => true, + ]; + if (!$share['readOnly']) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'], + 'protected' => true, + ]; + } else if ($this->resourceType === 'calendar') { + // Allow changing the properties of read only calendars, + // so users can change the visibility. + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'], + 'protected' => true, + ]; + } + } + return $acl; + } +} diff --git a/apps/dav/lib/DAV/Sharing/IShareable.php b/apps/dav/lib/DAV/Sharing/IShareable.php new file mode 100644 index 00000000000..f6b6bfa8862 --- /dev/null +++ b/apps/dav/lib/DAV/Sharing/IShareable.php @@ -0,0 +1,74 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\DAV\Sharing; +use Sabre\DAV\INode; + +/** + * This interface represents a dav resource that can be shared with other users. + * + */ +interface IShareable extends INode { + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * resource. + * + * 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 resource is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares(); + + /** + * @return int + */ + public function getResourceId(); + + /** + * @return string + */ + public function getOwner(); + +} \ No newline at end of file diff --git a/apps/dav/lib/DAV/Sharing/Plugin.php b/apps/dav/lib/DAV/Sharing/Plugin.php new file mode 100644 index 00000000000..3173d1397ba --- /dev/null +++ b/apps/dav/lib/DAV/Sharing/Plugin.php @@ -0,0 +1,200 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\DAV\Sharing; + +use OCA\DAV\Connector\Sabre\Auth; +use OCA\DAV\DAV\Sharing\Xml\Invite; +use OCP\IRequest; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class Plugin extends ServerPlugin { + + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** @var Auth */ + private $auth; + + /** @var IRequest */ + private $request; + + /** + * Plugin constructor. + * + * @param Auth $authBackEnd + * @param IRequest $request + */ + public function __construct(Auth $authBackEnd, IRequest $request) { + $this->auth = $authBackEnd; + $this->request = $request; + } + + /** + * Reference to SabreDAV server object. + * + * @var \Sabre\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 string[] + */ + function getFeatures() { + return ['oc-resource-sharing']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + return 'oc-resource-sharing'; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + $this->server = $server; + $this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}share'] = 'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest'; + $this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}invite'] = 'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite'; + + $this->server->on('method:POST', [$this, 'httpPost']); + $this->server->on('propFind', [$this, 'propFind']); + } + + /** + * We intercept this to handle POST requests on a dav resource. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return null|false + */ + function httpPost(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + + // Only handling xml + $contentType = $request->getHeader('Content-Type'); + if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) + return; + + // Making sure the node exists + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (NotFound $e) { + return; + } + + $requestBody = $request->getBodyAsString(); + + // If this request handler could not deal with this POST request, it + // will return 'null' and other plugins get a chance to handle the + // request. + // + // However, we already requested the full body. This is a problem, + // because a body can only be read once. This is why we preemptively + // re-populated the request body with the existing data. + $request->setBody($requestBody); + + $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); + + switch ($documentType) { + + // Dealing with the 'share' document, which modified invitees on a + // calendar. + case '{' . self::NS_OWNCLOUD . '}share' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof IShareable) { + return; + } + + $this->server->transactionType = 'post-oc-resource-share'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + /** @var \Sabre\DAVACL\Plugin $acl */ + $acl->checkPrivileges($path, '{DAV:}write'); + } + + $node->updateShares($message->set, $message->remove); + + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + } + } + + /** + * This event is triggered when properties are requested for a certain + * node. + * + * This allows us to inject any properties early. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + if ($node instanceof IShareable) { + + $propFind->handle('{' . Plugin::NS_OWNCLOUD . '}invite', function() use ($node) { + return new Invite( + $node->getShares() + ); + }); + + } + } + +} diff --git a/apps/dav/lib/DAV/Sharing/Xml/Invite.php b/apps/dav/lib/DAV/Sharing/Xml/Invite.php new file mode 100644 index 00000000000..ae40ff06701 --- /dev/null +++ b/apps/dav/lib/DAV/Sharing/Xml/Invite.php @@ -0,0 +1,168 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\DAV\Sharing\Xml; + +use OCA\DAV\DAV\Sharing\Plugin; +use Sabre\Xml\Writer; +use Sabre\Xml\XmlSerializable; + +/** + * 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) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Invite implements XmlSerializable { + + /** + * 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 + */ + 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 + */ + function getValue() { + + return $this->users; + + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + $cs = '{' . Plugin::NS_OWNCLOUD . '}'; + + if (!is_null($this->organizer)) { + + $writer->startElement($cs . 'organizer'); + $writer->writeElement('{DAV:}href', $this->organizer['href']); + + if (isset($this->organizer['commonName']) && $this->organizer['commonName']) { + $writer->writeElement($cs . 'common-name', $this->organizer['commonName']); + } + if (isset($this->organizer['firstName']) && $this->organizer['firstName']) { + $writer->writeElement($cs . 'first-name', $this->organizer['firstName']); + } + if (isset($this->organizer['lastName']) && $this->organizer['lastName']) { + $writer->writeElement($cs . 'last-name', $this->organizer['lastName']); + } + $writer->endElement(); // organizer + + } + + foreach ($this->users as $user) { + + $writer->startElement($cs . 'user'); + $writer->writeElement('{DAV:}href', $user['href']); + if (isset($user['commonName']) && $user['commonName']) { + $writer->writeElement($cs . 'common-name', $user['commonName']); + } + $writer->writeElement($cs . 'invite-accepted'); + + $writer->startElement($cs . 'access'); + if ($user['readOnly']) { + $writer->writeElement($cs . 'read'); + } else { + $writer->writeElement($cs . 'read-write'); + } + $writer->endElement(); // access + + if (isset($user['summary']) && $user['summary']) { + $writer->writeElement($cs . 'summary', $user['summary']); + } + + $writer->endElement(); //user + + } + + } +} diff --git a/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php new file mode 100644 index 00000000000..776fb446b6c --- /dev/null +++ b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php @@ -0,0 +1,84 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\DAV\Sharing\Xml; + +use OCA\DAV\DAV\Sharing\Plugin; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; + +class ShareRequest implements XmlDeserializable { + + public $set = []; + + public $remove = []; + + /** + * Constructor + * + * @param array $set + * @param array $remove + */ + function __construct(array $set, array $remove) { + + $this->set = $set; + $this->remove = $remove; + + } + + static function xmlDeserialize(Reader $reader) { + + $elements = $reader->parseInnerTree([ + '{' . Plugin::NS_OWNCLOUD. '}set' => 'Sabre\\Xml\\Element\\KeyValue', + '{' . Plugin::NS_OWNCLOUD . '}remove' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $set = []; + $remove = []; + + foreach ($elements as $elem) { + switch ($elem['name']) { + + case '{' . Plugin::NS_OWNCLOUD . '}set' : + $sharee = $elem['value']; + + $sumElem = '{' . Plugin::NS_OWNCLOUD . '}summary'; + $commonName = '{' . Plugin::NS_OWNCLOUD . '}common-name'; + + $set[] = [ + 'href' => $sharee['{DAV:}href'], + 'commonName' => isset($sharee[$commonName]) ? $sharee[$commonName] : null, + 'summary' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null, + 'readOnly' => !array_key_exists('{' . Plugin::NS_OWNCLOUD . '}read-write', $sharee), + ]; + break; + + case '{' . Plugin::NS_OWNCLOUD . '}remove' : + $remove[] = $elem['value']['{DAV:}href']; + break; + + } + } + + return new self($set, $remove); + + } + +} diff --git a/apps/dav/lib/DAV/SystemPrincipalBackend.php b/apps/dav/lib/DAV/SystemPrincipalBackend.php new file mode 100644 index 00000000000..8001ec4e6c6 --- /dev/null +++ b/apps/dav/lib/DAV/SystemPrincipalBackend.php @@ -0,0 +1,179 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\DAV; + +use Sabre\DAVACL\PrincipalBackend\AbstractBackend; +use Sabre\HTTP\URLUtil; + +class SystemPrincipalBackend extends AbstractBackend { + + /** + * 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) { + $principals = []; + + if ($prefixPath === 'principals/system') { + $principals[] = [ + 'uri' => 'principals/system/system', + '{DAV:}displayname' => 'system', + ]; + } + + return $principals; + } + + /** + * Returns a specific principal, specified by it's path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * @param string $path + * @return array + */ + function getPrincipalByPath($path) { + + if ($path === 'principals/system/system') { + $principal = [ + 'uri' => 'principals/system/system', + '{DAV:}displayname' => 'system', + ]; + return $principal; + } + + return null; + } + + /** + * Updates one ore more webdav properties on a principal. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $path + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) { + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return an array with full principal uri's. + * + * If somebody attempted to search on a property the backend does not + * support, you should simply return 0 results. + * + * You can also just return 0 results if you choose to not support + * searching at all, but keep in mind that this may stop certain features + * from working. + * + * @param string $prefixPath + * @param array $searchProperties + * @param string $test + * @return array + */ + function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { + return []; + } + + /** + * Returns the list of members for a group-principal + * + * @param string $principal + * @return array + */ + function getGroupMemberSet($principal) { + // TODO: for now the group principal has only one member, the user itself + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new \Sabre\DAV\Exception('Principal not found'); + } + + return [$principal['uri']]; + } + + /** + * Returns the list of groups a principal is a member of + * + * @param string $principal + * @return array + */ + function getGroupMembership($principal) { + list($prefix, $name) = URLUtil::splitPath($principal); + + if ($prefix === 'principals/system') { + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new \Sabre\DAV\Exception('Principal not found'); + } + + return []; + } + return []; + } + + /** + * 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) { + throw new \Sabre\DAV\Exception('Setting members of the group is not supported yet'); + } +} diff --git a/apps/dav/lib/Files/BrowserErrorPagePlugin.php b/apps/dav/lib/Files/BrowserErrorPagePlugin.php new file mode 100644 index 00000000000..37a4166efef --- /dev/null +++ b/apps/dav/lib/Files/BrowserErrorPagePlugin.php @@ -0,0 +1,116 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Files; + +use OC\AppFramework\Http\Request; +use OC_Template; +use OCP\IRequest; +use Sabre\DAV\Exception; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; + +class BrowserErrorPagePlugin extends ServerPlugin { + + /** @var Server */ + private $server; + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + $this->server = $server; + $server->on('exception', array($this, 'logException'), 1000); + } + + /** + * @param IRequest $request + * @return bool + */ + public static function isBrowserRequest(IRequest $request) { + if ($request->getMethod() !== 'GET') { + return false; + } + return $request->isUserAgent([ + Request::USER_AGENT_IE, + Request::USER_AGENT_MS_EDGE, + Request::USER_AGENT_CHROME, + Request::USER_AGENT_FIREFOX, + Request::USER_AGENT_SAFARI, + ]); + } + + /** + * @param \Exception $ex + */ + public function logException(\Exception $ex) { + if ($ex instanceof Exception) { + $httpCode = $ex->getHTTPCode(); + $headers = $ex->getHTTPHeaders($this->server); + } else { + $httpCode = 500; + $headers = []; + } + $this->server->httpResponse->addHeaders($headers); + $this->server->httpResponse->setStatus($httpCode); + $body = $this->generateBody($ex); + $this->server->httpResponse->setBody($body); + $this->sendResponse(); + } + + /** + * @codeCoverageIgnore + * @param \Exception $ex + * @param int $httpCode + * @return bool|string + */ + public function generateBody(\Exception $exception) { + $request = \OC::$server->getRequest(); + $content = new OC_Template('dav', 'exception', 'guest'); + $content->assign('title', $this->server->httpResponse->getStatusText()); + $content->assign('message', $exception->getMessage()); + $content->assign('errorClass', get_class($exception)); + $content->assign('errorMsg', $exception->getMessage()); + $content->assign('errorCode', $exception->getCode()); + $content->assign('file', $exception->getFile()); + $content->assign('line', $exception->getLine()); + $content->assign('trace', $exception->getTraceAsString()); + $content->assign('debugMode', \OC::$server->getSystemConfig()->getValue('debug', false)); + $content->assign('remoteAddr', $request->getRemoteAddress()); + $content->assign('requestID', $request->getId()); + return $content->fetchPage(); + } + + /* + * @codeCoverageIgnore + */ + public function sendResponse() { + $this->server->sapi->sendResponse($this->server->httpResponse); + } +} diff --git a/apps/dav/lib/Files/CustomPropertiesBackend.php b/apps/dav/lib/Files/CustomPropertiesBackend.php new file mode 100644 index 00000000000..aa541f88dad --- /dev/null +++ b/apps/dav/lib/Files/CustomPropertiesBackend.php @@ -0,0 +1,267 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Files; + +use OCP\IDBConnection; +use OCP\IUser; +use Sabre\DAV\PropertyStorage\Backend\BackendInterface; +use Sabre\DAV\PropFind; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Tree; + +class CustomPropertiesBackend implements BackendInterface { + + /** + * Ignored properties + * + * @var array + */ + private $ignoredProperties = array( + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{DAV:}getetag', + '{DAV:}quota-used-bytes', + '{DAV:}quota-available-bytes', + '{http://owncloud.org/ns}permissions', + '{http://owncloud.org/ns}downloadURL', + '{http://owncloud.org/ns}dDC', + '{http://owncloud.org/ns}size', + ); + + /** + * @var Tree + */ + private $tree; + + /** + * @var IDBConnection + */ + private $connection; + + /** + * @var IUser + */ + private $user; + + /** + * Properties cache + * + * @var array + */ + private $cache = []; + + /** + * @param Tree $tree node tree + * @param IDBConnection $connection database connection + * @param IUser $user owner of the tree and properties + */ + public function __construct( + Tree $tree, + IDBConnection $connection, + IUser $user) { + $this->tree = $tree; + $this->connection = $connection; + $this->user = $user->getUID(); + } + + /** + * Fetches properties for a path. + * + * @param string $path + * @param PropFind $propFind + * @return void + */ + public function propFind($path, PropFind $propFind) { + + $requestedProps = $propFind->get404Properties(); + + // these might appear + $requestedProps = array_diff( + $requestedProps, + $this->ignoredProperties + ); + + if (empty($requestedProps)) { + return; + } + + $props = $this->getProperties($path, $requestedProps); + foreach ($props as $propName => $propValue) { + $propFind->set($propName, $propValue); + } + } + + /** + * Updates properties for a path + * + * @param string $path + * @param PropPatch $propPatch + * + * @return void + */ + public function propPatch($path, PropPatch $propPatch) { + $propPatch->handleRemaining(function($changedProps) use ($path) { + return $this->updateProperties($path, $changedProps); + }); + } + + /** + * This method is called after a node is deleted. + * + * @param string $path path of node for which to delete properties + */ + public function delete($path) { + $statement = $this->connection->prepare( + 'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?' + ); + $statement->execute(array($this->user, $path)); + $statement->closeCursor(); + + unset($this->cache[$path]); + } + + /** + * This method is called after a successful MOVE + * + * @param string $source + * @param string $destination + * + * @return void + */ + public function move($source, $destination) { + $statement = $this->connection->prepare( + 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' . + ' WHERE `userid` = ? AND `propertypath` = ?' + ); + $statement->execute(array($destination, $this->user, $source)); + $statement->closeCursor(); + } + + /** + * Returns a list of properties for this nodes.; + * @param string $path + * @param array $requestedProperties requested properties or empty array for "all" + * @return array + * @note 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 + */ + private function getProperties($path, array $requestedProperties) { + if (isset($this->cache[$path])) { + return $this->cache[$path]; + } + + // TODO: chunking if more than 1000 properties + $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'; + + $whereValues = array($this->user, $path); + $whereTypes = array(null, null); + + if (!empty($requestedProperties)) { + // request only a subset + $sql .= ' AND `propertyname` in (?)'; + $whereValues[] = $requestedProperties; + $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY; + } + + $result = $this->connection->executeQuery( + $sql, + $whereValues, + $whereTypes + ); + + $props = []; + while ($row = $result->fetch()) { + $props[$row['propertyname']] = $row['propertyvalue']; + } + + $result->closeCursor(); + + $this->cache[$path] = $props; + return $props; + } + + /** + * Update properties + * + * @param string $path node for which to update properties + * @param array $properties array of properties to update + * + * @return bool + */ + private function updateProperties($path, $properties) { + + $deleteStatement = 'DELETE FROM `*PREFIX*properties`' . + ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; + + $insertStatement = 'INSERT INTO `*PREFIX*properties`' . + ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)'; + + $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' . + ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; + + // TODO: use "insert or update" strategy ? + $existing = $this->getProperties($path, array()); + $this->connection->beginTransaction(); + foreach ($properties as $propertyName => $propertyValue) { + // If it was null, we need to delete the property + if (is_null($propertyValue)) { + if (array_key_exists($propertyName, $existing)) { + $this->connection->executeUpdate($deleteStatement, + array( + $this->user, + $path, + $propertyName + ) + ); + } + } else { + if (!array_key_exists($propertyName, $existing)) { + $this->connection->executeUpdate($insertStatement, + array( + $this->user, + $path, + $propertyName, + $propertyValue + ) + ); + } else { + $this->connection->executeUpdate($updateStatement, + array( + $propertyValue, + $this->user, + $path, + $propertyName + ) + ); + } + } + } + + $this->connection->commit(); + unset($this->cache[$path]); + + return true; + } + +} diff --git a/apps/dav/lib/Files/FilesHome.php b/apps/dav/lib/Files/FilesHome.php new file mode 100644 index 00000000000..ef572d6618b --- /dev/null +++ b/apps/dav/lib/Files/FilesHome.php @@ -0,0 +1,103 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Files; + +use OCA\DAV\Connector\Sabre\Directory; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\ICollection; +use Sabre\DAV\SimpleCollection; +use Sabre\HTTP\URLUtil; + +class FilesHome implements ICollection { + + /** + * @var array + */ + private $principalInfo; + + /** + * FilesHome constructor. + * + * @param array $principalInfo + */ + public function __construct($principalInfo) { + $this->principalInfo = $principalInfo; + } + + function createFile($name, $data = null) { + return $this->impl()->createFile($name, $data); + } + + function createDirectory($name) { + $this->impl()->createDirectory($name); + } + + function getChild($name) { + return $this->impl()->getChild($name); + } + + function getChildren() { + return $this->impl()->getChildren(); + } + + function childExists($name) { + return $this->impl()->childExists($name); + } + + function delete() { + $this->impl()->delete(); + } + + function getName() { + list(,$name) = URLUtil::splitPath($this->principalInfo['uri']); + return $name; + } + + function setName($name) { + throw new Forbidden('Permission denied to rename this folder'); + } + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int + */ + function getLastModified() { + return $this->impl()->getLastModified(); + } + + /** + * @return Directory + */ + private function impl() { + // + // TODO: we need to mount filesystem of the give user + // + $user = \OC::$server->getUserSession()->getUser(); + if ($this->getName() !== $user->getUID()) { + return new SimpleCollection($this->getName()); + } + $view = \OC\Files\Filesystem::getView(); + $rootInfo = $view->getFileInfo(''); + $impl = new Directory($view, $rootInfo); + return $impl; + } +} diff --git a/apps/dav/lib/Files/RootCollection.php b/apps/dav/lib/Files/RootCollection.php new file mode 100644 index 00000000000..63328aac8e3 --- /dev/null +++ b/apps/dav/lib/Files/RootCollection.php @@ -0,0 +1,46 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\Files; + +use Sabre\DAVACL\AbstractPrincipalCollection; +use Sabre\DAVACL\IPrincipal; + +class RootCollection 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 $principalInfo + * @return IPrincipal + */ + function getChildForPrincipal(array $principalInfo) { + return new FilesHome($principalInfo); + } + + function getName() { + return 'files'; + } + +} diff --git a/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php b/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php new file mode 100644 index 00000000000..751f63d394e --- /dev/null +++ b/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php @@ -0,0 +1,63 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Files\Sharing; + +use OCP\Files\FileInfo; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * Verify that the public link share is valid + */ +class PublicLinkCheckPlugin extends ServerPlugin { + /** + * @var FileInfo + */ + private $fileInfo; + + /** + * @param FileInfo $fileInfo + */ + public function setFileInfo($fileInfo) { + $this->fileInfo = $fileInfo; + } + + /** + * This initializes the plugin. + * + * @param \Sabre\DAV\Server $server Sabre server + * + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + $server->on('beforeMethod', [$this, 'beforeMethod']); + } + + public function beforeMethod(RequestInterface $request, ResponseInterface $response){ + // verify that the owner didn't have his share permissions revoked + if ($this->fileInfo && !$this->fileInfo->isShareable()) { + throw new NotFound(); + } + } +} diff --git a/apps/dav/lib/HookManager.php b/apps/dav/lib/HookManager.php new file mode 100644 index 00000000000..aa3777088f8 --- /dev/null +++ b/apps/dav/lib/HookManager.php @@ -0,0 +1,126 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\SyncService; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Util; + +class HookManager { + + /** @var IUserManager */ + private $userManager; + + /** @var SyncService */ + private $syncService; + + /** @var IUser[] */ + private $usersToDelete; + + /** @var CalDavBackend */ + private $calDav; + + /** @var CardDavBackend */ + private $cardDav; + + public function __construct(IUserManager $userManager, + SyncService $syncService, + CalDavBackend $calDav, + CardDavBackend $cardDav) { + $this->userManager = $userManager; + $this->syncService = $syncService; + $this->calDav = $calDav; + $this->cardDav = $cardDav; + } + + public function setup() { + Util::connectHook('OC_User', + 'post_createUser', + $this, + 'postCreateUser'); + Util::connectHook('OC_User', + 'pre_deleteUser', + $this, + 'preDeleteUser'); + Util::connectHook('OC_User', + 'post_deleteUser', + $this, + 'postDeleteUser'); + Util::connectHook('OC_User', + 'changeUser', + $this, + 'changeUser'); + Util::connectHook('OC_User', + 'post_login', + $this, + 'postLogin'); + } + + public function postCreateUser($params) { + $user = $this->userManager->get($params['uid']); + $this->syncService->updateUser($user); + } + + public function preDeleteUser($params) { + $this->usersToDelete[$params['uid']] = $this->userManager->get($params['uid']); + } + + public function postDeleteUser($params) { + $uid = $params['uid']; + if (isset($this->usersToDelete[$uid])){ + $this->syncService->deleteUser($this->usersToDelete[$uid]); + } + } + + public function changeUser($params) { + $user = $params['user']; + $this->syncService->updateUser($user); + } + + public function postLogin($params) { + $user = $this->userManager->get($params['uid']); + if (!is_null($user)) { + $principal = 'principals/users/' . $user->getUID(); + $calendars = $this->calDav->getCalendarsForUser($principal); + if (empty($calendars) || (count($calendars) === 1 && $calendars[0]['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI)) { + try { + $this->calDav->createCalendar($principal, 'personal', [ + '{DAV:}displayname' => 'Personal']); + } catch (\Exception $ex) { + \OC::$server->getLogger()->logException($ex); + } + } + $books = $this->cardDav->getAddressBooksForUser($principal); + if (empty($books)) { + try { + $this->cardDav->createAddressBook($principal, 'contacts', [ + '{DAV:}displayname' => 'Contacts']); + } catch (\Exception $ex) { + \OC::$server->getLogger()->logException($ex); + } + } + } + } +} diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php new file mode 100644 index 00000000000..b6e1747e990 --- /dev/null +++ b/apps/dav/lib/RootCollection.php @@ -0,0 +1,114 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\CalendarRoot; +use OCA\DAV\CardDAV\AddressBookRoot; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\GroupPrincipalBackend; +use OCA\DAV\DAV\SystemPrincipalBackend; +use Sabre\CalDAV\Principal\Collection; +use Sabre\DAV\SimpleCollection; + +class RootCollection extends SimpleCollection { + + public function __construct() { + $config = \OC::$server->getConfig(); + $db = \OC::$server->getDatabaseConnection(); + $dispatcher = \OC::$server->getEventDispatcher(); + $userPrincipalBackend = new Principal( + \OC::$server->getUserManager(), + \OC::$server->getGroupManager() + ); + $groupPrincipalBackend = new GroupPrincipalBackend( + \OC::$server->getGroupManager() + ); + // as soon as debug mode is enabled we allow listing of principals + $disableListing = !$config->getSystemValue('debug', false); + + // setup the first level of the dav tree + $userPrincipals = new Collection($userPrincipalBackend, 'principals/users'); + $userPrincipals->disableListing = $disableListing; + $groupPrincipals = new Collection($groupPrincipalBackend, 'principals/groups'); + $groupPrincipals->disableListing = $disableListing; + $systemPrincipals = new Collection(new SystemPrincipalBackend(), 'principals/system'); + $systemPrincipals->disableListing = $disableListing; + $filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users'); + $filesCollection->disableListing = $disableListing; + $caldavBackend = new CalDavBackend($db, $userPrincipalBackend); + $calendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users'); + $calendarRoot->disableListing = $disableListing; + + $systemTagCollection = new SystemTag\SystemTagsByIdCollection( + \OC::$server->getSystemTagManager(), + \OC::$server->getUserSession(), + \OC::$server->getGroupManager() + ); + $systemTagRelationsCollection = new SystemTag\SystemTagsRelationsCollection( + \OC::$server->getSystemTagManager(), + \OC::$server->getSystemTagObjectMapper(), + \OC::$server->getUserSession(), + \OC::$server->getGroupManager(), + \OC::$server->getRootFolder() + ); + $commentsCollection = new Comments\RootCollection( + \OC::$server->getCommentsManager(), + \OC::$server->getUserManager(), + \OC::$server->getUserSession(), + \OC::$server->getRootFolder(), + \OC::$server->getLogger() + ); + + $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $dispatcher); + $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, 'principals/users'); + $usersAddressBookRoot->disableListing = $disableListing; + + $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $dispatcher); + $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system'); + $systemAddressBookRoot->disableListing = $disableListing; + + $uploadCollection = new Upload\RootCollection($userPrincipalBackend, 'principals/users'); + $uploadCollection->disableListing = $disableListing; + + $children = [ + new SimpleCollection('principals', [ + $userPrincipals, + $groupPrincipals, + $systemPrincipals]), + $filesCollection, + $calendarRoot, + new SimpleCollection('addressbooks', [ + $usersAddressBookRoot, + $systemAddressBookRoot]), + $systemTagCollection, + $systemTagRelationsCollection, + $commentsCollection, + $uploadCollection, + ]; + + parent::__construct('root', $children); + } + +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php new file mode 100644 index 00000000000..edaa7ac8552 --- /dev/null +++ b/apps/dav/lib/Server.php @@ -0,0 +1,164 @@ + + * @author Lukas Reschke + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV; + +use OCA\DAV\CalDAV\Schedule\IMipPlugin; +use OCA\DAV\Connector\FedAuth; +use OCA\DAV\Connector\Sabre\Auth; +use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; +use OCA\DAV\Connector\Sabre\DavAclPlugin; +use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Files\BrowserErrorPagePlugin; +use OCA\DAV\Files\CustomPropertiesBackend; +use OCP\IRequest; +use OCP\SabrePluginEvent; +use Sabre\CardDAV\VCFExportPlugin; +use Sabre\DAV\Auth\Plugin; + +class Server { + + /** @var IRequest */ + private $request; + + public function __construct(IRequest $request, $baseUri) { + $this->request = $request; + $this->baseUri = $baseUri; + $logger = \OC::$server->getLogger(); + $mailer = \OC::$server->getMailer(); + $dispatcher = \OC::$server->getEventDispatcher(); + + $root = new RootCollection(); + $this->server = new \OCA\DAV\Connector\Sabre\Server($root); + + // Backends + $authBackend = new Auth( + \OC::$server->getSession(), + \OC::$server->getUserSession(), + \OC::$server->getRequest() + ); + + // Set URL explicitly due to reverse-proxy situations + $this->server->httpRequest->setUrl($this->request->getRequestUri()); + $this->server->setBaseUri($this->baseUri); + + $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig())); + $authPlugin = new Plugin($authBackend, 'ownCloud'); + $this->server->addPlugin($authPlugin); + + // allow setup of additional auth backends + $event = new SabrePluginEvent($this->server); + $dispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event); + + // debugging + if(\OC::$server->getConfig()->getSystemValue('debug', false)) { + $this->server->addPlugin(new \Sabre\DAV\Browser\Plugin()); + } else { + $this->server->addPlugin(new DummyGetResponsePlugin()); + } + + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger)); + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); + $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin()); + + // acl + $acl = new DavAclPlugin(); + $acl->principalCollectionSet = [ + 'principals/users', 'principals/groups' + ]; + $acl->defaultUsernamePath = 'principals/users'; + $this->server->addPlugin($acl); + + // calendar plugins + $this->server->addPlugin(new \Sabre\CalDAV\Plugin()); + $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); + $this->server->addPlugin(new \Sabre\CalDAV\Schedule\Plugin()); + $this->server->addPlugin(new IMipPlugin($mailer, $logger)); + $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); + $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin()); + $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest())); + + // addressbook plugins + $this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin()); + $this->server->addPlugin(new VCFExportPlugin()); + + // system tags plugins + $this->server->addPlugin(new \OCA\DAV\SystemTag\SystemTagPlugin( + \OC::$server->getSystemTagManager(), + \OC::$server->getGroupManager(), + \OC::$server->getUserSession() + )); + + // comments plugin + $this->server->addPlugin(new \OCA\DAV\Comments\CommentsPlugin( + \OC::$server->getCommentsManager(), + \OC::$server->getUserSession() + )); + + // Some WebDAV clients do require Class 2 WebDAV support (locking), since + // we do not provide locking we emulate it using a fake locking plugin. + if($request->isUserAgent([ + '/WebDAVFS/', + '/Microsoft Office OneNote 2013/', + ])) { + $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin()); + } + + if (BrowserErrorPagePlugin::isBrowserRequest($request)) { + $this->server->addPlugin(new BrowserErrorPagePlugin()); + } + + // wait with registering these until auth is handled and the filesystem is setup + $this->server->on('beforeMethod', function () { + // custom properties plugin must be the last one + $user = \OC::$server->getUserSession()->getUser(); + if (!is_null($user)) { + $view = \OC\Files\Filesystem::getView(); + $this->server->addPlugin( + new FilesPlugin( + $this->server->tree, + $view, + \OC::$server->getConfig(), + false, + !\OC::$server->getConfig()->getSystemValue('debug', false) + ) + ); + + $this->server->addPlugin( + new \Sabre\DAV\PropertyStorage\Plugin( + new CustomPropertiesBackend( + $this->server->tree, + \OC::$server->getDatabaseConnection(), + \OC::$server->getUserSession()->getUser() + ) + ) + ); + } + }); + } + + public function exec() { + $this->server->exec(); + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagMappingNode.php b/apps/dav/lib/SystemTag/SystemTagMappingNode.php new file mode 100644 index 00000000000..bb2936c13dc --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagMappingNode.php @@ -0,0 +1,114 @@ + + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\SystemTag; + +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\Forbidden; + +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\TagNotFoundException; + +/** + * Mapping node for system tag to object id + */ +class SystemTagMappingNode extends SystemTagNode { + + /** + * @var ISystemTagObjectMapper + */ + private $tagMapper; + + /** + * @var string + */ + private $objectId; + + /** + * @var string + */ + private $objectType; + + /** + * Sets up the node, expects a full path name + * + * @param ISystemTag $tag system tag + * @param string $objectId + * @param string $objectType + * @param bool $isAdmin whether to allow permissions for admin + * @param ISystemTagManager $tagManager + * @param ISystemTagObjectMapper $tagMapper + */ + public function __construct( + ISystemTag $tag, + $objectId, + $objectType, + $isAdmin, + ISystemTagManager $tagManager, + ISystemTagObjectMapper $tagMapper + ) { + $this->objectId = $objectId; + $this->objectType = $objectType; + $this->tagMapper = $tagMapper; + parent::__construct($tag, $isAdmin, $tagManager); + } + + /** + * Returns the object id of the relationship + * + * @return string object id + */ + public function getObjectId() { + return $this->objectId; + } + + /** + * Returns the object type of the relationship + * + * @return string object type + */ + public function getObjectType() { + return $this->objectType; + } + + /** + * Delete tag to object association + */ + public function delete() { + try { + if (!$this->isAdmin) { + if (!$this->tag->isUserVisible()) { + throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found'); + } + if (!$this->tag->isUserAssignable()) { + throw new Forbidden('No permission to unassign tag ' . $this->tag->getId()); + } + } + $this->tagMapper->unassignTags($this->objectId, $this->objectType, $this->tag->getId()); + } catch (TagNotFoundException $e) { + // can happen if concurrent deletion occurred + throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found', 0, $e); + } + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagNode.php b/apps/dav/lib/SystemTag/SystemTagNode.php new file mode 100644 index 00000000000..500e1a3adea --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagNode.php @@ -0,0 +1,162 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\SystemTag; + +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\Conflict; + +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\TagNotFoundException; +use OCP\SystemTag\TagAlreadyExistsException; + +/** + * DAV node representing a system tag, with the name being the tag id. + */ +class SystemTagNode implements \Sabre\DAV\INode { + + /** + * @var ISystemTag + */ + protected $tag; + + /** + * @var ISystemTagManager + */ + protected $tagManager; + + /** + * Whether to allow permissions for admins + * + * @var bool + */ + protected $isAdmin; + + /** + * Sets up the node, expects a full path name + * + * @param ISystemTag $tag system tag + * @param bool $isAdmin whether to allow operations for admins + * @param ISystemTagManager $tagManager + */ + public function __construct(ISystemTag $tag, $isAdmin, ISystemTagManager $tagManager) { + $this->tag = $tag; + $this->isAdmin = $isAdmin; + $this->tagManager = $tagManager; + } + + /** + * Returns the id of the tag + * + * @return string + */ + public function getName() { + return $this->tag->getId(); + } + + /** + * Returns the system tag represented by this node + * + * @return ISystemTag system tag + */ + public function getSystemTag() { + return $this->tag; + } + + /** + * Renames the node + * + * @param string $name The new name + * + * @throws MethodNotAllowed not allowed to rename node + */ + public function setName($name) { + throw new MethodNotAllowed(); + } + + /** + * Update tag + * + * @param string $name new tag name + * @param bool $userVisible user visible + * @param bool $userAssignable user assignable + * @throws NotFound whenever the given tag id does not exist + * @throws Forbidden whenever there is no permission to update said tag + * @throws Conflict whenever a tag already exists with the given attributes + */ + public function update($name, $userVisible, $userAssignable) { + try { + if (!$this->isAdmin) { + if (!$this->tag->isUserVisible()) { + throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist'); + } + if (!$this->tag->isUserAssignable()) { + throw new Forbidden('No permission to update tag ' . $this->tag->getId()); + } + + // only renaming is allowed for regular users + if ($userVisible !== $this->tag->isUserVisible() + || $userAssignable !== $this->tag->isUserAssignable() + ) { + throw new Forbidden('No permission to update permissions for tag ' . $this->tag->getId()); + } + } + $this->tagManager->updateTag($this->tag->getId(), $name, $userVisible, $userAssignable); + } catch (TagNotFoundException $e) { + throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist'); + } catch (TagAlreadyExistsException $e) { + throw new Conflict( + 'Tag with the properties "' . $name . '", ' . + $userVisible . ', ' . $userAssignable . ' already exists' + ); + } + } + + /** + * Returns null, not supported + * + */ + public function getLastModified() { + return null; + } + + public function delete() { + try { + if (!$this->isAdmin) { + if (!$this->tag->isUserVisible()) { + throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found'); + } + if (!$this->tag->isUserAssignable()) { + throw new Forbidden('No permission to delete tag ' . $this->tag->getId()); + } + } + $this->tagManager->deleteTags($this->tag->getId()); + } catch (TagNotFoundException $e) { + // can happen if concurrent deletion occurred + throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found', 0, $e); + } + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php new file mode 100644 index 00000000000..a266b4a1214 --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php @@ -0,0 +1,271 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\SystemTag; + +use OCP\IGroupManager; +use OCP\IUserSession; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\PropFind; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\UnsupportedMediaType; +use Sabre\DAV\Exception\Conflict; + +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\TagAlreadyExistsException; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * Sabre plugin to handle system tags: + * + * - makes it possible to create new tags with POST operation + * - get/set Webdav properties for tags + * + */ +class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { + + // namespace + const NS_OWNCLOUD = 'http://owncloud.org/ns'; + const ID_PROPERTYNAME = '{http://owncloud.org/ns}id'; + const DISPLAYNAME_PROPERTYNAME = '{http://owncloud.org/ns}display-name'; + const USERVISIBLE_PROPERTYNAME = '{http://owncloud.org/ns}user-visible'; + const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable'; + + /** + * @var \Sabre\DAV\Server $server + */ + private $server; + + /** + * @var ISystemTagManager + */ + protected $tagManager; + + /** + * @var IUserSession + */ + protected $userSession; + + /** + * @var IGroupManager + */ + protected $groupManager; + + /** + * @param ISystemTagManager $tagManager tag manager + * @param IGroupManager $groupManager + * @param IUserSession $userSession + */ + public function __construct(ISystemTagManager $tagManager, + IGroupManager $groupManager, + IUserSession $userSession) { + $this->tagManager = $tagManager; + $this->userSession = $userSession; + $this->groupManager = $groupManager; + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param \Sabre\DAV\Server $server + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + + $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; + + $server->protectedProperties[] = self::ID_PROPERTYNAME; + + $server->on('propFind', array($this, 'handleGetProperties')); + $server->on('propPatch', array($this, 'handleUpdateProperties')); + $server->on('method:POST', [$this, 'httpPost']); + + $this->server = $server; + } + + /** + * POST operation on system tag collections + * + * @param RequestInterface $request request object + * @param ResponseInterface $response response object + * @return null|false + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) { + $path = $request->getPath(); + + // Making sure the node exists + $node = $this->server->tree->getNodeForPath($path); + if ($node instanceof SystemTagsByIdCollection || $node instanceof SystemTagsObjectMappingCollection) { + $data = $request->getBodyAsString(); + + $tag = $this->createTag($data, $request->getHeader('Content-Type')); + + if ($node instanceof SystemTagsObjectMappingCollection) { + // also add to collection + $node->createFile($tag->getId()); + $url = $request->getBaseUrl() . 'systemtags/'; + } else { + $url = $request->getUrl(); + } + + if ($url[strlen($url) - 1] !== '/') { + $url .= '/'; + } + + $response->setHeader('Content-Location', $url . $tag->getId()); + + // created + $response->setStatus(201); + return false; + } + } + + /** + * Creates a new tag + * + * @param string $data JSON encoded string containing the properties of the tag to create + * @param string $contentType content type of the data + * @return ISystemTag newly created system tag + * + * @throws BadRequest if a field was missing + * @throws Conflict if a tag with the same properties already exists + * @throws UnsupportedMediaType if the content type is not supported + */ + private function createTag($data, $contentType = 'application/json') { + if (explode(';', $contentType)[0] === 'application/json') { + $data = json_decode($data, true); + } else { + throw new UnsupportedMediaType(); + } + + if (!isset($data['name'])) { + throw new BadRequest('Missing "name" attribute'); + } + + $tagName = $data['name']; + $userVisible = true; + $userAssignable = true; + + if (isset($data['userVisible'])) { + $userVisible = (bool)$data['userVisible']; + } + + if (isset($data['userAssignable'])) { + $userAssignable = (bool)$data['userAssignable']; + } + + if($userVisible === false || $userAssignable === false) { + if(!$this->userSession->isLoggedIn() || !$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) { + throw new BadRequest('Not sufficient permissions'); + } + } + + try { + return $this->tagManager->createTag($tagName, $userVisible, $userAssignable); + } catch (TagAlreadyExistsException $e) { + throw new Conflict('Tag already exists', 0, $e); + } + } + + + /** + * Retrieves system tag properties + * + * @param PropFind $propFind + * @param \Sabre\DAV\INode $node + */ + public function handleGetProperties( + PropFind $propFind, + \Sabre\DAV\INode $node + ) { + if (!($node instanceof SystemTagNode)) { + return; + } + + $propFind->handle(self::ID_PROPERTYNAME, function() use ($node) { + return $node->getSystemTag()->getId(); + }); + + $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function() use ($node) { + return $node->getSystemTag()->getName(); + }); + + $propFind->handle(self::USERVISIBLE_PROPERTYNAME, function() use ($node) { + return $node->getSystemTag()->isUserVisible() ? 'true' : 'false'; + }); + + $propFind->handle(self::USERASSIGNABLE_PROPERTYNAME, function() use ($node) { + return $node->getSystemTag()->isUserAssignable() ? 'true' : 'false'; + }); + } + + /** + * Updates tag attributes + * + * @param string $path + * @param PropPatch $propPatch + * + * @return void + */ + public function handleUpdateProperties($path, PropPatch $propPatch) { + $propPatch->handle([ + self::DISPLAYNAME_PROPERTYNAME, + self::USERVISIBLE_PROPERTYNAME, + self::USERASSIGNABLE_PROPERTYNAME, + ], function($props) use ($path) { + $node = $this->server->tree->getNodeForPath($path); + if (!($node instanceof SystemTagNode)) { + return; + } + + $tag = $node->getSystemTag(); + $name = $tag->getName(); + $userVisible = $tag->isUserVisible(); + $userAssignable = $tag->isUserAssignable(); + + if (isset($props[self::DISPLAYNAME_PROPERTYNAME])) { + $name = $props[self::DISPLAYNAME_PROPERTYNAME]; + } + + if (isset($props[self::USERVISIBLE_PROPERTYNAME])) { + $propValue = $props[self::USERVISIBLE_PROPERTYNAME]; + $userVisible = ($propValue !== 'false' && $propValue !== '0'); + } + + if (isset($props[self::USERASSIGNABLE_PROPERTYNAME])) { + $propValue = $props[self::USERASSIGNABLE_PROPERTYNAME]; + $userAssignable = ($propValue !== 'false' && $propValue !== '0'); + } + + $node->update($name, $userVisible, $userAssignable); + return true; + }); + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php b/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php new file mode 100644 index 00000000000..298902501ab --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php @@ -0,0 +1,176 @@ + + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\SystemTag; + +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\ICollection; + +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\TagNotFoundException; +use OCP\IGroupManager; +use OCP\IUserSession; + +class SystemTagsByIdCollection implements ICollection { + + /** + * @var ISystemTagManager + */ + private $tagManager; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + /** + * SystemTagsByIdCollection constructor. + * + * @param ISystemTagManager $tagManager + * @param IUserSession $userSession + * @param IGroupManager $groupManager + */ + public function __construct( + ISystemTagManager $tagManager, + IUserSession $userSession, + IGroupManager $groupManager + ) { + $this->tagManager = $tagManager; + $this->userSession = $userSession; + $this->groupManager = $groupManager; + } + + /** + * Returns whether the currently logged in user is an administrator + */ + private function isAdmin() { + $user = $this->userSession->getUser(); + if ($user !== null) { + return $this->groupManager->isAdmin($user->getUID()); + } + return false; + } + + /** + * @param string $name + * @param resource|string $data Initial payload + * @throws Forbidden + */ + function createFile($name, $data = null) { + throw new Forbidden('Cannot create tags by id'); + } + + /** + * @param string $name + */ + function createDirectory($name) { + throw new Forbidden('Permission denied to create collections'); + } + + /** + * @param string $name + */ + function getChild($name) { + try { + $tag = $this->tagManager->getTagsByIds([$name]); + $tag = current($tag); + if (!$this->isAdmin() && !$tag->isUserVisible()) { + throw new NotFound('Tag with id ' . $name . ' not found'); + } + return $this->makeNode($tag); + } catch (\InvalidArgumentException $e) { + throw new BadRequest('Invalid tag id', 0, $e); + } catch (TagNotFoundException $e) { + throw new NotFound('Tag with id ' . $name . ' not found', 0, $e); + } + } + + function getChildren() { + $visibilityFilter = true; + if ($this->isAdmin()) { + $visibilityFilter = null; + } + + $tags = $this->tagManager->getAllTags($visibilityFilter); + return array_map(function($tag) { + return $this->makeNode($tag); + }, $tags); + } + + /** + * @param string $name + */ + function childExists($name) { + try { + $tag = $this->tagManager->getTagsByIds([$name]); + $tag = current($tag); + if (!$this->isAdmin() && !$tag->isUserVisible()) { + return false; + } + return true; + } catch (\InvalidArgumentException $e) { + throw new BadRequest('Invalid tag id', 0, $e); + } catch (TagNotFoundException $e) { + return false; + } + } + + function delete() { + throw new Forbidden('Permission denied to delete this collection'); + } + + function getName() { + return 'systemtags'; + } + + function setName($name) { + throw new Forbidden('Permission denied to rename this collection'); + } + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int + */ + function getLastModified() { + return null; + } + + /** + * Create a sabre node for the given system tag + * + * @param ISystemTag $tag + * + * @return SystemTagNode + */ + private function makeNode(ISystemTag $tag) { + return new SystemTagNode($tag, $this->isAdmin(), $this->tagManager); + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php new file mode 100644 index 00000000000..eb75ed06393 --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php @@ -0,0 +1,201 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\SystemTag; + +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\PreconditionFailed; +use Sabre\DAV\ICollection; + +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\TagNotFoundException; + +/** + * Collection containing tags by object id + */ +class SystemTagsObjectMappingCollection implements ICollection { + + /** + * @var string + */ + private $objectId; + + /** + * @var string + */ + private $objectType; + + /** + * @var ISystemTagManager + */ + private $tagManager; + + /** + * @var ISystemTagObjectMapper + */ + private $tagMapper; + + /** + * Whether to return results only visible for admins + * + * @var bool + */ + private $isAdmin; + + + /** + * Constructor + * + * @param string $objectId object id + * @param string $objectType object type + * @param bool $isAdmin whether to return results visible only for admins + * @param ISystemTagManager $tagManager + * @param ISystemTagObjectMapper $tagMapper + */ + public function __construct($objectId, $objectType, $isAdmin, $tagManager, $tagMapper) { + $this->tagManager = $tagManager; + $this->tagMapper = $tagMapper; + $this->objectId = $objectId; + $this->objectType = $objectType; + $this->isAdmin = $isAdmin; + } + + function createFile($tagId, $data = null) { + try { + if (!$this->isAdmin) { + $tag = $this->tagManager->getTagsByIds($tagId); + $tag = current($tag); + if (!$tag->isUserVisible()) { + throw new PreconditionFailed('Tag with id ' . $tagId . ' does not exist, cannot assign'); + } + if (!$tag->isUserAssignable()) { + throw new Forbidden('No permission to assign tag ' . $tag->getId()); + } + } + $this->tagMapper->assignTags($this->objectId, $this->objectType, $tagId); + } catch (TagNotFoundException $e) { + throw new PreconditionFailed('Tag with id ' . $tagId . ' does not exist, cannot assign'); + } + } + + function createDirectory($name) { + throw new Forbidden('Permission denied to create collections'); + } + + function getChild($tagId) { + try { + if ($this->tagMapper->haveTag([$this->objectId], $this->objectType, $tagId, true)) { + $tag = $this->tagManager->getTagsByIds([$tagId]); + $tag = current($tag); + if ($this->isAdmin || $tag->isUserVisible()) { + return $this->makeNode($tag); + } + } + throw new NotFound('Tag with id ' . $tagId . ' not present for object ' . $this->objectId); + } catch (\InvalidArgumentException $e) { + throw new BadRequest('Invalid tag id', 0, $e); + } catch (TagNotFoundException $e) { + throw new NotFound('Tag with id ' . $tagId . ' not found', 0, $e); + } + } + + function getChildren() { + $tagIds = current($this->tagMapper->getTagIdsForObjects([$this->objectId], $this->objectType)); + if (empty($tagIds)) { + return []; + } + $tags = $this->tagManager->getTagsByIds($tagIds); + if (!$this->isAdmin) { + // filter out non-visible tags + $tags = array_filter($tags, function($tag) { + return $tag->isUserVisible(); + }); + } + return array_values(array_map(function($tag) { + return $this->makeNode($tag); + }, $tags)); + } + + function childExists($tagId) { + try { + $result = ($this->tagMapper->haveTag([$this->objectId], $this->objectType, $tagId, true)); + if ($this->isAdmin || !$result) { + return $result; + } + + // verify if user is allowed to see this tag + $tag = $this->tagManager->getTagsByIds($tagId); + $tag = current($tag); + if (!$tag->isUserVisible()) { + return false; + } + return true; + } catch (\InvalidArgumentException $e) { + throw new BadRequest('Invalid tag id', 0, $e); + } catch (TagNotFoundException $e) { + return false; + } + } + + function delete() { + throw new Forbidden('Permission denied to delete this collection'); + } + + function getName() { + return $this->objectId; + } + + function setName($name) { + throw new Forbidden('Permission denied to rename this collection'); + } + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int + */ + function getLastModified() { + return null; + } + + /** + * Create a sabre node for the mapping of the + * given system tag to the collection's object + * + * @param ISystemTag $tag + * + * @return SystemTagNode + */ + private function makeNode(ISystemTag $tag) { + return new SystemTagMappingNode( + $tag, + $this->objectId, + $this->objectType, + $this->isAdmin, + $this->tagManager, + $this->tagMapper + ); + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php new file mode 100644 index 00000000000..bdbc73c4e32 --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php @@ -0,0 +1,183 @@ + + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\SystemTag; + +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; + +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\IUserSession; +use OCP\IGroupManager; +use OCP\Files\IRootFolder; + +/** + * Collection containing object ids by object type + */ +class SystemTagsObjectTypeCollection implements ICollection { + + /** + * @var string + */ + private $objectType; + + /** + * @var ISystemTagManager + */ + private $tagManager; + + /** + * @var ISystemTagObjectMapper + */ + private $tagMapper; + + /** + * @var IGroupManager + */ + private $groupManager; + + /** + * @var IUserSession + */ + private $userSession; + + /** + * @var IRootFolder + **/ + protected $fileRoot; + + /** + * Constructor + * + * @param string $objectType object type + * @param ISystemTagManager $tagManager + * @param ISystemTagObjectMapper $tagMapper + * @param IUserSession $userSession + * @param IGroupManager $groupManager + * @param IRootFolder $fileRoot + */ + public function __construct( + $objectType, + ISystemTagManager $tagManager, + ISystemTagObjectMapper $tagMapper, + IUserSession $userSession, + IGroupManager $groupManager, + IRootFolder $fileRoot + ) { + $this->tagManager = $tagManager; + $this->tagMapper = $tagMapper; + $this->objectType = $objectType; + $this->userSession = $userSession; + $this->groupManager = $groupManager; + $this->fileRoot = $fileRoot; + } + + /** + * Returns whether the currently logged in user is an administrator + */ + private function isAdmin() { + $user = $this->userSession->getUser(); + if ($user !== null) { + return $this->groupManager->isAdmin($user->getUID()); + } + return false; + } + + /** + * @param string $name + * @param resource|string $data Initial payload + * @throws Forbidden + */ + function createFile($name, $data = null) { + throw new Forbidden('Permission denied to create nodes'); + } + + /** + * @param string $name + */ + function createDirectory($name) { + throw new Forbidden('Permission denied to create collections'); + } + + /** + * @param string $objectId + */ + function getChild($objectId) { + // make sure the object exists and is reachable + if(!$this->childExists($objectId)) { + throw new NotFound('Entity does not exist or is not available'); + } + return new SystemTagsObjectMappingCollection( + $objectId, + $this->objectType, + $this->isAdmin(), + $this->tagManager, + $this->tagMapper + ); + } + + function getChildren() { + // do not list object ids + throw new MethodNotAllowed(); + } + + /** + * @param string $name + */ + function childExists($name) { + // TODO: make this more abstract + if ($this->objectType === 'files') { + // make sure the object is reachable for the current user + $userId = $this->userSession->getUser()->getUID(); + $nodes = $this->fileRoot->getUserFolder($userId)->getById(intval($name)); + return !empty($nodes); + } + return true; + } + + function delete() { + throw new Forbidden('Permission denied to delete this collection'); + } + + function getName() { + return $this->objectType; + } + + /** + * @param string $name + */ + function setName($name) { + throw new Forbidden('Permission denied to rename this collection'); + } + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int + */ + function getLastModified() { + return null; + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php new file mode 100644 index 00000000000..19db39d3f3a --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php @@ -0,0 +1,74 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\SystemTag; + +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\SimpleCollection; +use OCP\IUserSession; +use OCP\IGroupManager; +use OCP\Files\IRootFolder; + +class SystemTagsRelationsCollection extends SimpleCollection { + + /** + * SystemTagsRelationsCollection constructor. + * + * @param ISystemTagManager $tagManager + * @param ISystemTagObjectMapper $tagMapper + * @param IUserSession $userSession + * @param IGroupManager $groupManager + * @param IRootFolder $fileRoot + */ + public function __construct( + ISystemTagManager $tagManager, + ISystemTagObjectMapper $tagMapper, + IUserSession $userSession, + IGroupManager $groupManager, + IRootFolder $fileRoot + ) { + $children = [ + new SystemTagsObjectTypeCollection( + 'files', + $tagManager, + $tagMapper, + $userSession, + $groupManager, + $fileRoot + ), + ]; + + parent::__construct('root', $children); + } + + function getName() { + return 'systemtags-relations'; + } + + function setName($name) { + throw new Forbidden('Permission denied to rename this collection'); + } + +} diff --git a/apps/dav/lib/Upload/AssemblyStream.php b/apps/dav/lib/Upload/AssemblyStream.php new file mode 100644 index 00000000000..4b80a591ce4 --- /dev/null +++ b/apps/dav/lib/Upload/AssemblyStream.php @@ -0,0 +1,234 @@ +loadContext('assembly'); + + // sort the nodes + $nodes = $this->nodes; + // http://stackoverflow.com/a/10985500 + @usort($nodes, function(IFile $a, IFile $b) { + return strcmp($a->getName(), $b->getName()); + }); + $this->nodes = $nodes; + + // build additional information + $this->sortedNodes = []; + $start = 0; + foreach($this->nodes as $node) { + $size = $node->getSize(); + $name = $node->getName(); + $this->sortedNodes[$name] = ['node' => $node, 'start' => $start, 'end' => $start + $size]; + $start += $size; + $this->size = $start; + } + return true; + } + + /** + * @param string $offset + * @param int $whence + * @return bool + */ + public function stream_seek($offset, $whence = SEEK_SET) { + return false; + } + + /** + * @return int + */ + public function stream_tell() { + return $this->pos; + } + + /** + * @param int $count + * @return string + */ + public function stream_read($count) { + + list($node, $posInNode) = $this->getNodeForPosition($this->pos); + if (is_null($node)) { + return null; + } + $stream = $this->getStream($node); + + fseek($stream, $posInNode); + $data = fread($stream, $count); + $read = strlen($data); + + // update position + $this->pos += $read; + return $data; + } + + /** + * @param string $data + * @return int + */ + public function stream_write($data) { + return false; + } + + /** + * @param int $option + * @param int $arg1 + * @param int $arg2 + * @return bool + */ + public function stream_set_option($option, $arg1, $arg2) { + return false; + } + + /** + * @param int $size + * @return bool + */ + public function stream_truncate($size) { + return false; + } + + /** + * @return array + */ + public function stream_stat() { + return []; + } + + /** + * @param int $operation + * @return bool + */ + public function stream_lock($operation) { + return false; + } + + /** + * @return bool + */ + public function stream_flush() { + return false; + } + + /** + * @return bool + */ + public function stream_eof() { + return $this->pos >= $this->size; + } + + /** + * @return bool + */ + public function stream_close() { + return true; + } + + + /** + * Load the source from the stream context and return the context options + * + * @param string $name + * @return array + * @throws \Exception + */ + protected function loadContext($name) { + $context = stream_context_get_options($this->context); + if (isset($context[$name])) { + $context = $context[$name]; + } else { + throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); + } + if (isset($context['nodes']) and is_array($context['nodes'])) { + $this->nodes = $context['nodes']; + } else { + throw new \BadMethodCallException('Invalid context, nodes not set'); + } + return $context; + } + + /** + * @param IFile[] $nodes + * @return resource + * + * @throws \BadMethodCallException + */ + public static function wrap(array $nodes) { + $context = stream_context_create([ + 'assembly' => [ + 'nodes' => $nodes] + ]); + stream_wrapper_register('assembly', '\OCA\DAV\Upload\AssemblyStream'); + try { + $wrapped = fopen('assembly://', 'r', null, $context); + } catch (\BadMethodCallException $e) { + stream_wrapper_unregister('assembly'); + throw $e; + } + stream_wrapper_unregister('assembly'); + return $wrapped; + } + + /** + * @param $pos + * @return IFile | null + */ + private function getNodeForPosition($pos) { + foreach($this->sortedNodes as $node) { + if ($pos >= $node['start'] && $pos < $node['end']) { + return [$node['node'], $pos - $node['start']]; + } + } + return null; + } + + /** + * @param IFile $node + * @return resource + */ + private function getStream(IFile $node) { + $data = $node->get(); + if (is_resource($data)) { + return $data; + } + + return fopen('data://text/plain,' . $data,'r'); + } + +} diff --git a/apps/dav/lib/Upload/FutureFile.php b/apps/dav/lib/Upload/FutureFile.php new file mode 100644 index 00000000000..aca81afc055 --- /dev/null +++ b/apps/dav/lib/Upload/FutureFile.php @@ -0,0 +1,103 @@ +root = $root; + $this->name = $name; + } + + /** + * @inheritdoc + */ + function put($data) { + throw new Forbidden('Permission denied to put into this file'); + } + + /** + * @inheritdoc + */ + function get() { + $nodes = $this->root->getChildren(); + return AssemblyStream::wrap($nodes); + } + + /** + * @inheritdoc + */ + function getContentType() { + return 'application/octet-stream'; + } + + /** + * @inheritdoc + */ + function getETag() { + return $this->root->getETag(); + } + + /** + * @inheritdoc + */ + function getSize() { + $children = $this->root->getChildren(); + $sizes = array_map(function($node) { + /** @var IFile $node */ + return $node->getSize(); + }, $children); + + return array_sum($sizes); + } + + /** + * @inheritdoc + */ + function delete() { + $this->root->delete(); + } + + /** + * @inheritdoc + */ + function getName() { + return $this->name; + } + + /** + * @inheritdoc + */ + function setName($name) { + throw new Forbidden('Permission denied to rename this file'); + } + + /** + * @inheritdoc + */ + function getLastModified() { + return $this->root->getLastModified(); + } +} diff --git a/apps/dav/lib/Upload/RootCollection.php b/apps/dav/lib/Upload/RootCollection.php new file mode 100644 index 00000000000..673a3734318 --- /dev/null +++ b/apps/dav/lib/Upload/RootCollection.php @@ -0,0 +1,23 @@ +node = $node; + } + + function createFile($name, $data = null) { + // TODO: verify name - should be a simple number + $this->node->createFile($name, $data); + } + + function createDirectory($name) { + throw new Forbidden('Permission denied to create file (filename ' . $name . ')'); + } + + function getChild($name) { + if ($name === '.file') { + return new FutureFile($this->node, '.file'); + } + return $this->node->getChild($name); + } + + function getChildren() { + $children = $this->node->getChildren(); + $children[] = new FutureFile($this->node, '.file'); + return $children; + } + + function childExists($name) { + if ($name === '.file') { + return true; + } + return $this->node->childExists($name); + } + + function delete() { + $this->node->delete(); + } + + function getName() { + return $this->node->getName(); + } + + function setName($name) { + throw new Forbidden('Permission denied to rename this folder'); + } + + function getLastModified() { + return $this->node->getLastModified(); + } +} diff --git a/apps/dav/lib/Upload/UploadHome.php b/apps/dav/lib/Upload/UploadHome.php new file mode 100644 index 00000000000..ae4dcfa4931 --- /dev/null +++ b/apps/dav/lib/Upload/UploadHome.php @@ -0,0 +1,74 @@ +principalInfo = $principalInfo; + } + + function createFile($name, $data = null) { + throw new Forbidden('Permission denied to create file (filename ' . $name . ')'); + } + + function createDirectory($name) { + $this->impl()->createDirectory($name); + } + + function getChild($name) { + return new UploadFolder($this->impl()->getChild($name)); + } + + function getChildren() { + return array_map(function($node) { + return new UploadFolder($node); + }, $this->impl()->getChildren()); + } + + function childExists($name) { + return !is_null($this->getChild($name)); + } + + function delete() { + $this->impl()->delete(); + } + + function getName() { + return 'uploads'; + } + + function setName($name) { + throw new Forbidden('Permission denied to rename this folder'); + } + + function getLastModified() { + return $this->impl()->getLastModified(); + } + + /** + * @return Directory + */ + private function impl() { + $rootView = new View(); + $user = \OC::$server->getUserSession()->getUser(); + Filesystem::initMountPoints($user->getUID()); + if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) { + $rootView->mkdir('/' . $user->getUID() . '/uploads'); + } + $view = new View('/' . $user->getUID() . '/uploads'); + $rootInfo = $view->getFileInfo(''); + $impl = new Directory($view, $rootInfo); + return $impl; + } +} diff --git a/apps/dav/lib/caldav/birthdayservice.php b/apps/dav/lib/caldav/birthdayservice.php deleted file mode 100644 index b74116f4083..00000000000 --- a/apps/dav/lib/caldav/birthdayservice.php +++ /dev/null @@ -1,226 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CalDAV; - -use Exception; -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\DAV\GroupPrincipalBackend; -use Sabre\VObject\Component\VCalendar; -use Sabre\VObject\Reader; - -class BirthdayService { - - const BIRTHDAY_CALENDAR_URI = 'contact_birthdays'; - - /** @var GroupPrincipalBackend */ - private $principalBackend; - - /** - * BirthdayService constructor. - * - * @param CalDavBackend $calDavBackEnd - * @param CardDavBackend $cardDavBackEnd - * @param GroupPrincipalBackend $principalBackend - */ - public function __construct($calDavBackEnd, $cardDavBackEnd, $principalBackend) { - $this->calDavBackEnd = $calDavBackEnd; - $this->cardDavBackEnd = $cardDavBackEnd; - $this->principalBackend = $principalBackend; - } - - /** - * @param int $addressBookId - * @param string $cardUri - * @param string $cardData - */ - public function onCardChanged($addressBookId, $cardUri, $cardData) { - - $targetPrincipals = $this->getAllAffectedPrincipals($addressBookId); - - $book = $this->cardDavBackEnd->getAddressBookById($addressBookId); - $targetPrincipals[] = $book['principaluri']; - foreach ($targetPrincipals as $principalUri) { - $calendar = $this->ensureCalendarExists($principalUri); - $objectUri = $book['uri'] . '-' . $cardUri. '.ics'; - $calendarData = $this->buildBirthdayFromContact($cardData); - $existing = $this->calDavBackEnd->getCalendarObject($calendar['id'], $objectUri); - if (is_null($calendarData)) { - if (!is_null($existing)) { - $this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri); - } - } else { - if (is_null($existing)) { - $this->calDavBackEnd->createCalendarObject($calendar['id'], $objectUri, $calendarData->serialize()); - } else { - if ($this->birthdayEvenChanged($existing['calendardata'], $calendarData)) { - $this->calDavBackEnd->updateCalendarObject($calendar['id'], $objectUri, $calendarData->serialize()); - } - } - } - } - } - - /** - * @param int $addressBookId - * @param string $cardUri - */ - public function onCardDeleted($addressBookId, $cardUri) { - $targetPrincipals = $this->getAllAffectedPrincipals($addressBookId); - $book = $this->cardDavBackEnd->getAddressBookById($addressBookId); - $targetPrincipals[] = $book['principaluri']; - foreach ($targetPrincipals as $principalUri) { - $calendar = $this->ensureCalendarExists($principalUri); - $objectUri = $book['uri'] . '-' . $cardUri . '.ics'; - $this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri); - } - } - - /** - * @param string $principal - * @return array|null - * @throws \Sabre\DAV\Exception\BadRequest - */ - public function ensureCalendarExists($principal) { - $book = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI); - if (!is_null($book)) { - return $book; - } - $this->calDavBackEnd->createCalendar($principal, self::BIRTHDAY_CALENDAR_URI, [ - '{DAV:}displayname' => 'Contact birthdays', - '{http://apple.com/ns/ical/}calendar-color' => '#FFFFCA', - ]); - - return $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI); - } - - /** - * @param string $cardData - * @return null|VCalendar - */ - public function buildBirthdayFromContact($cardData) { - if (empty($cardData)) { - return null; - } - try { - $doc = Reader::read($cardData); - } catch (Exception $e) { - return null; - } - - if (!isset($doc->BDAY)) { - return null; - } - $birthday = $doc->BDAY; - if (!(string)$birthday) { - return null; - } - $title = str_replace('{name}', - strtr((string)$doc->FN, array('\,' => ',', '\;' => ';')), - '{name}' - ); - try { - $date = new \DateTime($birthday); - } catch (Exception $e) { - return null; - } - $vCal = new VCalendar(); - $vCal->VERSION = '2.0'; - $vEvent = $vCal->createComponent('VEVENT'); - $vEvent->add('DTSTART'); - $vEvent->DTSTART->setDateTime( - $date - ); - $vEvent->DTSTART['VALUE'] = 'DATE'; - $vEvent->add('DTEND'); - $date->add(new \DateInterval('P1D')); - $vEvent->DTEND->setDateTime( - $date - ); - $vEvent->DTEND['VALUE'] = 'DATE'; - $vEvent->{'UID'} = $doc->UID; - $vEvent->{'RRULE'} = 'FREQ=YEARLY'; - $vEvent->{'SUMMARY'} = $title . ' (*' . $date->format('Y') . ')'; - $vEvent->{'TRANSP'} = 'TRANSPARENT'; - $alarm = $vCal->createComponent('VALARM'); - $alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION'])); - $alarm->add($vCal->createProperty('ACTION', 'DISPLAY')); - $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'})); - $vEvent->add($alarm); - $vCal->add($vEvent); - return $vCal; - } - - /** - * @param string $user - */ - public function syncUser($user) { - $principal = 'principals/users/'.$user; - $this->ensureCalendarExists($principal); - $books = $this->cardDavBackEnd->getAddressBooksForUser($principal); - foreach($books as $book) { - $cards = $this->cardDavBackEnd->getCards($book['id']); - foreach($cards as $card) { - $this->onCardChanged($book['id'], $card['uri'], $card['carddata']); - } - } - } - - /** - * @param string $existingCalendarData - * @param VCalendar $newCalendarData - * @return bool - */ - public function birthdayEvenChanged($existingCalendarData, $newCalendarData) { - try { - $existingBirthday = Reader::read($existingCalendarData); - } catch (Exception $ex) { - return true; - } - if ($newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() || - $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue() - ) { - return true; - } - return false; - } - - /** - * @param integer $addressBookId - * @return mixed - */ - protected function getAllAffectedPrincipals($addressBookId) { - $targetPrincipals = []; - $shares = $this->cardDavBackEnd->getShares($addressBookId); - foreach ($shares as $share) { - if ($share['{http://owncloud.org/ns}group-share']) { - $users = $this->principalBackend->getGroupMemberSet($share['{http://owncloud.org/ns}principal']); - foreach ($users as $user) { - $targetPrincipals[] = $user['uri']; - } - } else { - $targetPrincipals[] = $share['{http://owncloud.org/ns}principal']; - } - } - return array_values(array_unique($targetPrincipals, SORT_STRING)); - } - -} diff --git a/apps/dav/lib/caldav/caldavbackend.php b/apps/dav/lib/caldav/caldavbackend.php deleted file mode 100644 index f0f236de3ff..00000000000 --- a/apps/dav/lib/caldav/caldavbackend.php +++ /dev/null @@ -1,1390 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CalDAV; - -use OCA\DAV\DAV\Sharing\IShareable; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCA\DAV\Connector\Sabre\Principal; -use OCA\DAV\DAV\Sharing\Backend; -use OCP\IDBConnection; -use Sabre\CalDAV\Backend\AbstractBackend; -use Sabre\CalDAV\Backend\SchedulingSupport; -use Sabre\CalDAV\Backend\SubscriptionSupport; -use Sabre\CalDAV\Backend\SyncSupport; -use Sabre\CalDAV\Plugin; -use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; -use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; -use Sabre\DAV; -use Sabre\DAV\Exception\Forbidden; -use Sabre\HTTP\URLUtil; -use Sabre\VObject\DateTimeParser; -use Sabre\VObject\Reader; -use Sabre\VObject\RecurrenceIterator; - -/** - * Class CalDavBackend - * - * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php - * - * @package OCA\DAV\CalDAV - */ -class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport { - - /** - * 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'; - - /** - * List of CalDAV properties, and how they map to database field names - * Add your own properties by simply adding on to this array. - * - * Note that only string-based properties are supported here. - * - * @var array - */ - public $propertyMap = [ - '{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', - ]; - - /** - * List of subscription properties, and how they map to database field names. - * - * @var array - */ - public $subscriptionPropertyMap = [ - '{DAV:}displayname' => 'displayname', - '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', - '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', - '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', - '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', - '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', - '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', - ]; - - /** @var IDBConnection */ - private $db; - - /** @var Backend */ - private $sharingBackend; - - /** @var Principal */ - private $principalBackend; - - /** - * CalDavBackend constructor. - * - * @param IDBConnection $db - * @param Principal $principalBackend - */ - public function __construct(IDBConnection $db, Principal $principalBackend) { - $this->db = $db; - $this->principalBackend = $principalBackend; - $this->sharingBackend = new Backend($this->db, $principalBackend, 'calendar'); - } - - /** - * 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'. - * - * Many clients also require: - * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set - * For this property, you can just return an instance of - * Sabre\CalDAV\Property\SupportedCalendarComponentSet. - * - * If you return {http://sabredav.org/ns}read-only and set the value to 1, - * ACL will automatically be put in read-only mode. - * - * @param string $principalUri - * @return array - */ - function getCalendarsForUser($principalUri) { - $principalUriOriginal = $principalUri; - $principalUri = $this->convertPrincipal($principalUri, true); - $fields = array_values($this->propertyMap); - $fields[] = 'id'; - $fields[] = 'uri'; - $fields[] = 'synctoken'; - $fields[] = 'components'; - $fields[] = 'principaluri'; - $fields[] = 'transparent'; - - // Making fields a comma-delimited list - $query = $this->db->getQueryBuilder(); - $query->select($fields)->from('calendars') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->orderBy('calendarorder', 'ASC'); - $stmt = $query->execute(); - - $calendars = []; - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - $components = []; - if ($row['components']) { - $components = explode(',',$row['components']); - } - - $calendar = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $this->convertPrincipal($row['principaluri'], false), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), - ]; - - foreach($this->propertyMap as $xmlName=>$dbName) { - $calendar[$xmlName] = $row[$dbName]; - } - - if (!isset($calendars[$calendar['id']])) { - $calendars[$calendar['id']] = $calendar; - } - } - - $stmt->closeCursor(); - - // query for shared calendars - $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); - $principals[]= $principalUri; - - $fields = array_values($this->propertyMap); - $fields[] = 'a.id'; - $fields[] = 'a.uri'; - $fields[] = 'a.synctoken'; - $fields[] = 'a.components'; - $fields[] = 'a.principaluri'; - $fields[] = 'a.transparent'; - $fields[] = 's.access'; - $query = $this->db->getQueryBuilder(); - $result = $query->select($fields) - ->from('dav_shares', 's') - ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) - ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) - ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) - ->setParameter('type', 'calendar') - ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY) - ->execute(); - - while($row = $result->fetch()) { - list(, $name) = URLUtil::splitPath($row['principaluri']); - $uri = $row['uri'] . '_shared_by_' . $name; - $row['displayname'] = $row['displayname'] . "($name)"; - $components = []; - if ($row['components']) { - $components = explode(',',$row['components']); - } - $calendar = [ - 'id' => $row['id'], - 'uri' => $uri, - 'principaluri' => $principalUri, - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, - ]; - - foreach($this->propertyMap as $xmlName=>$dbName) { - $calendar[$xmlName] = $row[$dbName]; - } - - if (!isset($calendars[$calendar['id']])) { - $calendars[$calendar['id']] = $calendar; - } - } - $result->closeCursor(); - - return array_values($calendars); - } - - /** - * @param string $principal - * @param string $uri - * @return array|null - */ - public function getCalendarByUri($principal, $uri) { - $fields = array_values($this->propertyMap); - $fields[] = 'id'; - $fields[] = 'uri'; - $fields[] = 'synctoken'; - $fields[] = 'components'; - $fields[] = 'principaluri'; - $fields[] = 'transparent'; - - // Making fields a comma-delimited list - $query = $this->db->getQueryBuilder(); - $query->select($fields)->from('calendars') - ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) - ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) - ->setMaxResults(1); - $stmt = $query->execute(); - - $row = $stmt->fetch(\PDO::FETCH_ASSOC); - $stmt->closeCursor(); - if ($row === false) { - return null; - } - - $components = []; - if ($row['components']) { - $components = explode(',',$row['components']); - } - - $calendar = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $row['principaluri'], - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), - ]; - - foreach($this->propertyMap as $xmlName=>$dbName) { - $calendar[$xmlName] = $row[$dbName]; - } - - return $calendar; - } - - public function getCalendarById($calendarId) { - $fields = array_values($this->propertyMap); - $fields[] = 'id'; - $fields[] = 'uri'; - $fields[] = 'synctoken'; - $fields[] = 'components'; - $fields[] = 'principaluri'; - $fields[] = 'transparent'; - - // Making fields a comma-delimited list - $query = $this->db->getQueryBuilder(); - $query->select($fields)->from('calendars') - ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))) - ->setMaxResults(1); - $stmt = $query->execute(); - - $row = $stmt->fetch(\PDO::FETCH_ASSOC); - $stmt->closeCursor(); - if ($row === false) { - return null; - } - - $components = []; - if ($row['components']) { - $components = explode(',',$row['components']); - } - - $calendar = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $row['principaluri'], - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), - ]; - - foreach($this->propertyMap as $xmlName=>$dbName) { - $calendar[$xmlName] = $row[$dbName]; - } - - return $calendar; - } - - /** - * 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 int - */ - function createCalendar($principalUri, $calendarUri, array $properties) { - $values = [ - 'principaluri' => $principalUri, - 'uri' => $calendarUri, - 'synctoken' => 1, - 'transparent' => 0, - 'components' => 'VEVENT,VTODO', - 'displayname' => $calendarUri - ]; - - // Default value - $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; - if (isset($properties[$sccs])) { - if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) { - throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet'); - } - $values['components'] = implode(',',$properties[$sccs]->getValue()); - } - $transp = '{' . 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]; - } - } - - $query = $this->db->getQueryBuilder(); - $query->insert('calendars'); - foreach($values as $column => $value) { - $query->setValue($column, $query->createNamedParameter($value)); - } - $query->execute(); - return $query->getLastInsertId(); - } - - /** - * Updates properties for a calendar. - * - * The list of mutations is stored in a Sabre\DAV\PropPatch object. - * To do the actual updates, you must tell this object which properties - * you're going to process with the handle() method. - * - * Calling the handle method is like telling the PropPatch object "I - * promise I can handle updating this property". - * - * Read the PropPatch documentation for more info and examples. - * - * @param \Sabre\DAV\PropPatch $propPatch - * @return void - */ - function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { - $supportedProperties = array_keys($this->propertyMap); - $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; - - $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) { - $newValues = []; - foreach ($mutations as $propertyName => $propertyValue) { - - switch ($propertyName) { - case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' : - $fieldName = 'transparent'; - $newValues[$fieldName] = $propertyValue->getValue() === 'transparent'; - break; - default : - $fieldName = $this->propertyMap[$propertyName]; - $newValues[$fieldName] = $propertyValue; - break; - } - - } - $query = $this->db->getQueryBuilder(); - $query->update('calendars'); - foreach ($newValues as $fieldName => $value) { - $query->set($fieldName, $query->createNamedParameter($value)); - } - $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); - $query->execute(); - - $this->addChange($calendarId, "", 2); - - return true; - }); - } - - /** - * Delete a calendar and all it's objects - * - * @param mixed $calendarId - * @return void - */ - function deleteCalendar($calendarId) { - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?'); - $stmt->execute([$calendarId]); - - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?'); - $stmt->execute([$calendarId]); - - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?'); - $stmt->execute([$calendarId]); - - $this->sharingBackend->deleteAllShares($calendarId); - } - - /** - * Returns all calendar objects within a calendar. - * - * Every item contains an array with the following keys: - * * calendardata - The iCalendar-compatible calendar data - * * uri - a unique key which will be used to construct the uri. This can - * be any arbitrary string, but making sure it ends with '.ics' is a - * good idea. This is only the basename, or filename, not the full - * path. - * * lastmodified - a timestamp of the last modification time - * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: - * '"abcdef"') - * * size - The size of the calendar objects, in bytes. - * * component - optional, a string containing the type of object, such - * as 'vevent' or 'vtodo'. If specified, this will be used to populate - * the Content-Type header. - * - * Note that the etag is optional, but it's highly encouraged to return for - * speed reasons. - * - * The calendardata is also optional. If it's not returned - * 'getCalendarObject' will be called later, which *is* expected to return - * calendardata. - * - * If neither etag or size are specified, the calendardata will be - * used/fetched to determine these numbers. If both are specified the - * amount of times this is needed is reduced by a great degree. - * - * @param mixed $calendarId - * @return array - */ - function getCalendarObjects($calendarId) { - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype']) - ->from('calendarobjects') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); - $stmt = $query->execute(); - - $result = []; - foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { - $result[] = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'lastmodified' => $row['lastmodified'], - 'etag' => '"' . $row['etag'] . '"', - 'calendarid' => $row['calendarid'], - 'size' => (int)$row['size'], - 'component' => strtolower($row['componenttype']), - ]; - } - - return $result; - } - - /** - * Returns information from a single calendar object, based on it's object - * uri. - * - * The object uri is only the basename, or filename and not a full path. - * - * The returned array must have the same keys as getCalendarObjects. The - * 'calendardata' object is required here though, while it's not required - * for getCalendarObjects. - * - * This method must return null if the object did not exist. - * - * @param mixed $calendarId - * @param string $objectUri - * @return array|null - */ - function getCalendarObject($calendarId, $objectUri) { - - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype']) - ->from('calendarobjects') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))); - $stmt = $query->execute(); - $row = $stmt->fetch(\PDO::FETCH_ASSOC); - - if(!$row) return null; - - return [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'lastmodified' => $row['lastmodified'], - 'etag' => '"' . $row['etag'] . '"', - 'calendarid' => $row['calendarid'], - 'size' => (int)$row['size'], - 'calendardata' => $this->readBlob($row['calendardata']), - 'component' => strtolower($row['componenttype']), - ]; - } - - /** - * Returns a list of calendar objects. - * - * This method should work identical to getCalendarObject, but instead - * return all the calendar objects in the list as an array. - * - * If the backend supports this, it may allow for some speed-ups. - * - * @param mixed $calendarId - * @param string[] $uris - * @return array - */ - function getMultipleCalendarObjects($calendarId, array $uris) { - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype']) - ->from('calendarobjects') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) - ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) - ->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); - - $stmt = $query->execute(); - - $result = []; - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - $result[] = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'lastmodified' => $row['lastmodified'], - 'etag' => '"' . $row['etag'] . '"', - 'calendarid' => $row['calendarid'], - 'size' => (int)$row['size'], - 'calendardata' => $this->readBlob($row['calendardata']), - 'component' => strtolower($row['componenttype']), - ]; - - } - return $result; - } - - /** - * Creates a new calendar object. - * - * The object uri is only the basename, or filename and not a full path. - * - * It is possible return an etag from this function, which will be used in - * the response to this PUT request. Note that the ETag must be surrounded - * by double-quotes. - * - * However, you should only really return this ETag if you don't mangle the - * calendar-data. If the result of a subsequent GET to this object is not - * the exact same as this request body, you should omit the ETag. - * - * @param mixed $calendarId - * @param string $objectUri - * @param string $calendarData - * @return string - */ - function createCalendarObject($calendarId, $objectUri, $calendarData) { - $extraData = $this->getDenormalizedData($calendarData); - - $query = $this->db->getQueryBuilder(); - $query->insert('calendarobjects') - ->values([ - 'calendarid' => $query->createNamedParameter($calendarId), - 'uri' => $query->createNamedParameter($objectUri), - 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB), - 'lastmodified' => $query->createNamedParameter(time()), - 'etag' => $query->createNamedParameter($extraData['etag']), - 'size' => $query->createNamedParameter($extraData['size']), - 'componenttype' => $query->createNamedParameter($extraData['componentType']), - 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']), - 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']), - 'uid' => $query->createNamedParameter($extraData['uid']), - ]) - ->execute(); - - $this->addChange($calendarId, $objectUri, 1); - - return '"' . $extraData['etag'] . '"'; - } - - /** - * Updates an existing calendarobject, based on it's uri. - * - * The object uri is only the basename, or filename and not a full path. - * - * It is possible return an etag from this function, which will be used in - * the response to this PUT request. Note that the ETag must be surrounded - * by double-quotes. - * - * However, you should only really return this ETag if you don't mangle the - * calendar-data. If the result of a subsequent GET to this object is not - * the exact same as this request body, you should omit the ETag. - * - * @param mixed $calendarId - * @param string $objectUri - * @param string $calendarData - * @return string - */ - function updateCalendarObject($calendarId, $objectUri, $calendarData) { - $extraData = $this->getDenormalizedData($calendarData); - - $query = $this->db->getQueryBuilder(); - $query->update('calendarobjects') - ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) - ->set('lastmodified', $query->createNamedParameter(time())) - ->set('etag', $query->createNamedParameter($extraData['etag'])) - ->set('size', $query->createNamedParameter($extraData['size'])) - ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) - ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) - ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) - ->set('uid', $query->createNamedParameter($extraData['uid'])) - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) - ->execute(); - - $this->addChange($calendarId, $objectUri, 2); - - return '"' . $extraData['etag'] . '"'; - } - - /** - * Deletes an existing calendar object. - * - * The object uri is only the basename, or filename and not a full path. - * - * @param mixed $calendarId - * @param string $objectUri - * @return void - */ - function deleteCalendarObject($calendarId, $objectUri) { - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?'); - $stmt->execute([$calendarId, $objectUri]); - - $this->addChange($calendarId, $objectUri, 3); - } - - /** - * Performs a calendar-query on the contents of this calendar. - * - * The calendar-query is defined in RFC4791 : CalDAV. Using the - * calendar-query it is possible for a client to request a specific set of - * object, based on contents of iCalendar properties, date-ranges and - * iCalendar component types (VTODO, VEVENT). - * - * This method should just return a list of (relative) urls that match this - * query. - * - * The list of filters are specified as an array. The exact array is - * documented by Sabre\CalDAV\CalendarQueryParser. - * - * Note that it is extremely likely that getCalendarObject for every path - * returned from this method will be called almost immediately after. You - * may want to anticipate this to speed up these requests. - * - * This method provides a default implementation, which parses *all* the - * iCalendar objects in the specified calendar. - * - * This default may well be good enough for personal use, and calendars - * that aren't very large. But if you anticipate high usage, big calendars - * or high loads, you are strongly advised to optimize certain paths. - * - * The best way to do so is override this method and to optimize - * specifically for 'common filters'. - * - * Requests that are extremely common are: - * * requests for just VEVENTS - * * requests for just VTODO - * * requests with a time-range-filter on either VEVENT or VTODO. - * - * ..and combinations of these requests. It may not be worth it to try to - * handle every possible situation and just rely on the (relatively - * easy to use) CalendarQueryValidator to handle the rest. - * - * Note that especially time-range-filters may be difficult to parse. A - * time-range filter specified on a VEVENT must for instance also handle - * recurrence rules correctly. - * A good example of how to interprete all these filters can also simply - * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct - * as possible, so it gives you a good idea on what type of stuff you need - * to think of. - * - * @param mixed $calendarId - * @param array $filters - * @return array - */ - function calendarQuery($calendarId, array $filters) { - $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; - } - } - - } - $columns = ['uri']; - if ($requirePostFilter) { - $columns = ['uri', 'calendardata']; - } - $query = $this->db->getQueryBuilder(); - $query->select($columns) - ->from('calendarobjects') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); - - if ($componentType) { - $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType))); - } - - if ($timeRange && $timeRange['start']) { - $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp()))); - } - if ($timeRange && $timeRange['end']) { - $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp()))); - } - - $stmt = $query->execute(); - - $result = []; - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - if ($requirePostFilter) { - if (!$this->validateFilterForObject($row, $filters)) { - continue; - } - } - $result[] = $row['uri']; - } - - return $result; - } - - /** - * Searches through all of a users calendars and calendar objects to find - * an object with a specific UID. - * - * This method should return the path to this object, relative to the - * calendar home, so this path usually only contains two parts: - * - * calendarpath/objectpath.ics - * - * If the uid is not found, return null. - * - * This method should only consider * objects that the principal owns, so - * any calendars owned by other principals that also appear in this - * collection should be ignored. - * - * @param string $principalUri - * @param string $uid - * @return string|null - */ - function getCalendarObjectByUID($principalUri, $uid) { - - $query = $this->db->getQueryBuilder(); - $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi') - ->from('calendarobjects', 'co') - ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id')) - ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri))) - ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid))); - - $stmt = $query->execute(); - - if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - return $row['calendaruri'] . '/' . $row['objecturi']; - } - - return null; - } - - /** - * The getChanges method returns all the changes that have happened, since - * the specified syncToken in the specified calendar. - * - * This function should return an array, such as the following: - * - * [ - * 'syncToken' => 'The current synctoken', - * 'added' => [ - * 'new.txt', - * ], - * 'modified' => [ - * 'modified.txt', - * ], - * 'deleted' => [ - * 'foo.php.bak', - * 'old.txt' - * ] - * ); - * - * The returned syncToken property should reflect the *current* syncToken - * of the calendar, as reported in the {http://sabredav.org/ns}sync-token - * property This is * needed here too, to ensure the operation is atomic. - * - * If the $syncToken argument is specified as null, this is an initial - * sync, and all members should be reported. - * - * The modified property is an array of nodenames that have changed since - * the last token. - * - * The deleted property is an array with nodenames, that have been deleted - * from collection. - * - * The $syncLevel argument is basically the 'depth' of the report. If it's - * 1, you only have to report changes that happened only directly in - * immediate descendants. If it's 2, it should also include changes from - * the nodes below the child collections. (grandchildren) - * - * The $limit argument allows a client to specify how many results should - * be returned at most. If the limit is not specified, it should be treated - * as infinite. - * - * If the limit (infinite or not) is higher than you're willing to return, - * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. - * - * If the syncToken is expired (due to data cleanup) or unknown, you must - * return null. - * - * The limit is 'suggestive'. You are free to ignore it. - * - * @param string $calendarId - * @param string $syncToken - * @param int $syncLevel - * @param int $limit - * @return array - */ - function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) { - // Current synctoken - $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?'); - $stmt->execute([ $calendarId ]); - $currentToken = $stmt->fetchColumn(0); - - if (is_null($currentToken)) { - return null; - } - - $result = [ - 'syncToken' => $currentToken, - 'added' => [], - 'modified' => [], - 'deleted' => [], - ]; - - if ($syncToken) { - - $query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`"; - if ($limit>0) { - $query.= " `LIMIT` " . (int)$limit; - } - - // Fetching all changes - $stmt = $this->db->prepare($query); - $stmt->execute([$syncToken, $currentToken, $calendarId]); - - $changes = []; - - // This loop ensures that any duplicates are overwritten, only the - // last change on a node is relevant. - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - $changes[$row['uri']] = $row['operation']; - - } - - foreach($changes as $uri => $operation) { - - switch($operation) { - case 1 : - $result['added'][] = $uri; - break; - case 2 : - $result['modified'][] = $uri; - break; - case 3 : - $result['deleted'][] = $uri; - break; - } - - } - } else { - // No synctoken supplied, this is the initial sync. - $query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?"; - $stmt = $this->db->prepare($query); - $stmt->execute([$calendarId]); - - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); - } - return $result; - - } - - /** - * Returns a list of subscriptions for a principal. - * - * Every subscription is an array with the following keys: - * * id, a unique id that will be used by other functions to modify the - * subscription. This can be the same as the uri or a database key. - * * uri. This is just the 'base uri' or 'filename' of the subscription. - * * principaluri. The owner of the subscription. Almost always the same as - * principalUri passed to this method. - * - * Furthermore, all the subscription info must be returned too: - * - * 1. {DAV:}displayname - * 2. {http://apple.com/ns/ical/}refreshrate - * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos - * should not be stripped). - * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms - * should not be stripped). - * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if - * attachments should not be stripped). - * 6. {http://calendarserver.org/ns/}source (Must be a - * Sabre\DAV\Property\Href). - * 7. {http://apple.com/ns/ical/}calendar-color - * 8. {http://apple.com/ns/ical/}calendar-order - * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set - * (should just be an instance of - * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of - * default components). - * - * @param string $principalUri - * @return array - */ - function getSubscriptionsForUser($principalUri) { - $fields = array_values($this->subscriptionPropertyMap); - $fields[] = 'id'; - $fields[] = 'uri'; - $fields[] = 'source'; - $fields[] = 'principaluri'; - $fields[] = 'lastmodified'; - - $query = $this->db->getQueryBuilder(); - $query->select($fields) - ->from('calendarsubscriptions') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->orderBy('calendarorder', 'asc'); - $stmt =$query->execute(); - - $subscriptions = []; - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - $subscription = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $row['principaluri'], - 'source' => $row['source'], - 'lastmodified' => $row['lastmodified'], - - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), - ]; - - foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) { - if (!is_null($row[$dbName])) { - $subscription[$xmlName] = $row[$dbName]; - } - } - - $subscriptions[] = $subscription; - - } - - return $subscriptions; - } - - /** - * Creates a new subscription for a principal. - * - * If the creation was a success, an id must be returned that can be used to reference - * this subscription in other methods, such as updateSubscription. - * - * @param string $principalUri - * @param string $uri - * @param array $properties - * @return mixed - */ - function createSubscription($principalUri, $uri, array $properties) { - - if (!isset($properties['{http://calendarserver.org/ns/}source'])) { - throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); - } - - $values = [ - 'principaluri' => $principalUri, - 'uri' => $uri, - 'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), - 'lastmodified' => time(), - ]; - - foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) { - if (isset($properties[$xmlName])) { - - $values[$dbName] = $properties[$xmlName]; - $fieldNames[] = $dbName; - } - } - - $query = $this->db->getQueryBuilder(); - $query->insert('calendarsubscriptions') - ->values([ - 'principaluri' => $query->createNamedParameter($values['principaluri']), - 'uri' => $query->createNamedParameter($values['uri']), - 'source' => $query->createNamedParameter($values['source']), - 'lastmodified' => $query->createNamedParameter($values['lastmodified']), - ]) - ->execute(); - - return $this->db->lastInsertId('*PREFIX*calendarsubscriptions'); - } - - /** - * Updates a subscription - * - * The list of mutations is stored in a Sabre\DAV\PropPatch object. - * To do the actual updates, you must tell this object which properties - * you're going to process with the handle() method. - * - * Calling the handle method is like telling the PropPatch object "I - * promise I can handle updating this property". - * - * Read the PropPatch documentation for more info and examples. - * - * @param mixed $subscriptionId - * @param \Sabre\DAV\PropPatch $propPatch - * @return void - */ - function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) { - $supportedProperties = array_keys($this->subscriptionPropertyMap); - $supportedProperties[] = '{http://calendarserver.org/ns/}source'; - - $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) { - - $newValues = []; - - foreach($mutations as $propertyName=>$propertyValue) { - if ($propertyName === '{http://calendarserver.org/ns/}source') { - $newValues['source'] = $propertyValue->getHref(); - } else { - $fieldName = $this->subscriptionPropertyMap[$propertyName]; - $newValues[$fieldName] = $propertyValue; - } - } - - $query = $this->db->getQueryBuilder(); - $query->update('calendarsubscriptions') - ->set('lastmodified', $query->createNamedParameter(time())); - foreach($newValues as $fieldName=>$value) { - $query->set($fieldName, $query->createNamedParameter($value)); - } - $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) - ->execute(); - - return true; - - }); - } - - /** - * Deletes a subscription. - * - * @param mixed $subscriptionId - * @return void - */ - function deleteSubscription($subscriptionId) { - $query = $this->db->getQueryBuilder(); - $query->delete('calendarsubscriptions') - ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) - ->execute(); - } - - /** - * Returns a single scheduling object for the inbox collection. - * - * The returned array should contain the following elements: - * * uri - A unique basename for the object. This will be used to - * construct a full uri. - * * calendardata - The iCalendar object - * * lastmodified - The last modification date. Can be an int for a unix - * timestamp, or a PHP DateTime object. - * * etag - A unique token that must change if the object changed. - * * size - The size of the object, in bytes. - * - * @param string $principalUri - * @param string $objectUri - * @return array - */ - function getSchedulingObject($principalUri, $objectUri) { - $query = $this->db->getQueryBuilder(); - $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) - ->from('schedulingobjects') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) - ->execute(); - - $row = $stmt->fetch(\PDO::FETCH_ASSOC); - - if(!$row) { - return null; - } - - return [ - 'uri' => $row['uri'], - 'calendardata' => $row['calendardata'], - 'lastmodified' => $row['lastmodified'], - 'etag' => '"' . $row['etag'] . '"', - 'size' => (int)$row['size'], - ]; - } - - /** - * Returns all scheduling objects for the inbox collection. - * - * These objects should be returned as an array. Every item in the array - * should follow the same structure as returned from getSchedulingObject. - * - * The main difference is that 'calendardata' is optional. - * - * @param string $principalUri - * @return array - */ - function getSchedulingObjects($principalUri) { - $query = $this->db->getQueryBuilder(); - $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) - ->from('schedulingobjects') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->execute(); - - $result = []; - foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { - $result[] = [ - 'calendardata' => $row['calendardata'], - 'uri' => $row['uri'], - 'lastmodified' => $row['lastmodified'], - 'etag' => '"' . $row['etag'] . '"', - 'size' => (int)$row['size'], - ]; - } - - return $result; - } - - /** - * Deletes a scheduling object from the inbox collection. - * - * @param string $principalUri - * @param string $objectUri - * @return void - */ - function deleteSchedulingObject($principalUri, $objectUri) { - $query = $this->db->getQueryBuilder(); - $query->delete('schedulingobjects') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) - ->execute(); - } - - /** - * Creates a new scheduling object. This should land in a users' inbox. - * - * @param string $principalUri - * @param string $objectUri - * @param string $objectData - * @return void - */ - function createSchedulingObject($principalUri, $objectUri, $objectData) { - $query = $this->db->getQueryBuilder(); - $query->insert('schedulingobjects') - ->values([ - 'principaluri' => $query->createNamedParameter($principalUri), - 'calendardata' => $query->createNamedParameter($objectData), - 'uri' => $query->createNamedParameter($objectUri), - 'lastmodified' => $query->createNamedParameter(time()), - 'etag' => $query->createNamedParameter(md5($objectData)), - 'size' => $query->createNamedParameter(strlen($objectData)) - ]) - ->execute(); - } - - /** - * Adds a change record to the calendarchanges table. - * - * @param mixed $calendarId - * @param string $objectUri - * @param int $operation 1 = add, 2 = modify, 3 = delete. - * @return void - */ - protected function addChange($calendarId, $objectUri, $operation) { - - $stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?'); - $stmt->execute([ - $objectUri, - $calendarId, - $operation, - $calendarId - ]); - $stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); - $stmt->execute([ - $calendarId - ]); - - } - - /** - * Parses some information from calendar objects, used for optimized - * calendar-queries. - * - * Returns an array with the following keys: - * * etag - An md5 checksum of the object without the quotes. - * * size - Size of the object in bytes - * * componentType - VEVENT, VTODO or VJOURNAL - * * firstOccurence - * * lastOccurence - * * uid - value of the UID property - * - * @param string $calendarData - * @return array - */ - protected function getDenormalizedData($calendarData) { - - $vObject = Reader::read($calendarData); - $componentType = null; - $component = null; - $firstOccurence = null; - $lastOccurence = null; - $uid = null; - foreach($vObject->getComponents() as $component) { - if ($component->name!=='VTIMEZONE') { - $componentType = $component->name; - $uid = (string)$component->UID; - break; - } - } - if (!$componentType) { - throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); - } - if ($componentType === 'VEVENT' && $component->DTSTART) { - $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); - // Finding the last occurrence 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(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 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 [ - 'etag' => md5($calendarData), - 'size' => strlen($calendarData), - 'componentType' => $componentType, - 'firstOccurence' => is_null($firstOccurence) ? null : max(0, $firstOccurence), - 'lastOccurence' => $lastOccurence, - 'uid' => $uid, - ]; - - } - - private function readBlob($cardData) { - if (is_resource($cardData)) { - return stream_get_contents($cardData); - } - - return $cardData; - } - - /** - * @param IShareable $shareable - * @param array $add - * @param array $remove - */ - public function updateShares($shareable, $add, $remove) { - $this->sharingBackend->updateShares($shareable, $add, $remove); - } - - /** - * @param int $resourceId - * @return array - */ - public function getShares($resourceId) { - return $this->sharingBackend->getShares($resourceId); - } - - /** - * @param int $resourceId - * @param array $acl - * @return array - */ - public function applyShareAcl($resourceId, $acl) { - return $this->sharingBackend->applyShareAcl($resourceId, $acl); - } - - private function convertPrincipal($principalUri, $toV2) { - if ($this->principalBackend->getPrincipalPrefix() === 'principals') { - list(, $name) = URLUtil::splitPath($principalUri); - if ($toV2 === true) { - return "principals/users/$name"; - } - return "principals/$name"; - } - return $principalUri; - } -} diff --git a/apps/dav/lib/caldav/calendar.php b/apps/dav/lib/caldav/calendar.php deleted file mode 100644 index f3637692e43..00000000000 --- a/apps/dav/lib/caldav/calendar.php +++ /dev/null @@ -1,171 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\CalDAV; - -use OCA\DAV\DAV\Sharing\IShareable; -use OCP\IL10N; -use Sabre\CalDAV\Backend\BackendInterface; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\PropPatch; - -class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { - - public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n) { - parent::__construct($caldavBackend, $calendarInfo); - - if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) { - $this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays'); - } - } - - /** - * Updates the list of shares. - * - * The first array is a list of people that are to be added to the - * resource. - * - * 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) { - /** @var CalDavBackend $calDavBackend */ - $calDavBackend = $this->caldavBackend; - $calDavBackend->updateShares($this, $add, $remove); - } - - /** - * Returns the list of people whom this resource is shared with. - * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share - * - * @return array - */ - function getShares() { - /** @var CalDavBackend $calDavBackend */ - $calDavBackend = $this->caldavBackend; - return $calDavBackend->getShares($this->getResourceId()); - } - - /** - * @return int - */ - public function getResourceId() { - return $this->calendarInfo['id']; - } - - function getACL() { - $acl = [ - [ - 'privilege' => '{DAV:}read', - 'principal' => $this->getOwner(), - 'protected' => true, - ]]; - if ($this->getName() !== BirthdayService::BIRTHDAY_CALENDAR_URI) { - $acl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => $this->getOwner(), - 'protected' => true, - ]; - } - if ($this->getOwner() !== parent::getOwner()) { - $acl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => parent::getOwner(), - 'protected' => true, - ]; - if ($this->canWrite()) { - $acl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => parent::getOwner(), - 'protected' => true, - ]; - } - } - - /** @var CalDavBackend $calDavBackend */ - $calDavBackend = $this->caldavBackend; - return $calDavBackend->applyShareAcl($this->getResourceId(), $acl); - } - - function getChildACL() { - return $this->getACL(); - } - - function getOwner() { - if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { - return $this->calendarInfo['{http://owncloud.org/ns}owner-principal']; - } - return parent::getOwner(); - } - - function delete() { - if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { - $principal = 'principal:' . parent::getOwner(); - $shares = $this->getShares(); - $shares = array_filter($shares, function($share) use ($principal){ - return $share['href'] === $principal; - }); - if (empty($shares)) { - throw new Forbidden(); - } - - /** @var CalDavBackend $calDavBackend */ - $calDavBackend = $this->caldavBackend; - $calDavBackend->updateShares($this, [], [ - 'href' => $principal - ]); - return; - } - parent::delete(); - } - - function propPatch(PropPatch $propPatch) { - $mutations = $propPatch->getMutations(); - // If this is a shared calendar, the user can only change the enabled property, to hide it. - if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) && (sizeof($mutations) !== 1 || !isset($mutations['{http://owncloud.org/ns}calendar-enabled']))) { - throw new Forbidden(); - } - parent::propPatch($propPatch); - } - - private function canWrite() { - if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) { - return !$this->calendarInfo['{http://owncloud.org/ns}read-only']; - } - return true; - } - -} diff --git a/apps/dav/lib/caldav/calendarhome.php b/apps/dav/lib/caldav/calendarhome.php deleted file mode 100644 index a4c485a8983..00000000000 --- a/apps/dav/lib/caldav/calendarhome.php +++ /dev/null @@ -1,106 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\CalDAV; - -use Sabre\CalDAV\Backend\BackendInterface; -use Sabre\CalDAV\Backend\NotificationSupport; -use Sabre\CalDAV\Backend\SchedulingSupport; -use Sabre\CalDAV\Backend\SubscriptionSupport; -use Sabre\CalDAV\Schedule\Inbox; -use Sabre\CalDAV\Schedule\Outbox; -use Sabre\CalDAV\Subscriptions\Subscription; -use Sabre\DAV\Exception\NotFound; - -class CalendarHome extends \Sabre\CalDAV\CalendarHome { - - /** @var \OCP\IL10N */ - private $l10n; - - public function __construct(BackendInterface $caldavBackend, $principalInfo) { - parent::__construct($caldavBackend, $principalInfo); - $this->l10n = \OC::$server->getL10N('dav'); - } - - /** - * @inheritdoc - */ - function getChildren() { - $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); - $objects = []; - foreach ($calendars as $calendar) { - $objects[] = new Calendar($this->caldavBackend, $calendar, $this->l10n); - } - - if ($this->caldavBackend instanceof SchedulingSupport) { - $objects[] = new Inbox($this->caldavBackend, $this->principalInfo['uri']); - $objects[] = new Outbox($this->principalInfo['uri']); - } - - // We're adding a notifications node, if it's supported by the backend. - if ($this->caldavBackend instanceof NotificationSupport) { - $objects[] = new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); - } - - // If the backend supports subscriptions, we'll add those as well, - if ($this->caldavBackend instanceof SubscriptionSupport) { - foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { - $objects[] = new Subscription($this->caldavBackend, $subscription); - } - } - - return $objects; - } - - /** - * @inheritdoc - */ - function getChild($name) { - // Special nodes - if ($name === 'inbox' && $this->caldavBackend instanceof SchedulingSupport) { - return new Inbox($this->caldavBackend, $this->principalInfo['uri']); - } - if ($name === 'outbox' && $this->caldavBackend instanceof SchedulingSupport) { - return new Outbox($this->principalInfo['uri']); - } - if ($name === 'notifications' && $this->caldavBackend instanceof NotificationSupport) { - return new \Sabre\CalDAv\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); - } - - // Calendars - foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) { - if ($calendar['uri'] === $name) { - return new Calendar($this->caldavBackend, $calendar, $this->l10n); - } - } - - if ($this->caldavBackend instanceof SubscriptionSupport) { - foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { - if ($subscription['uri'] === $name) { - return new Subscription($this->caldavBackend, $subscription); - } - } - - } - - throw new NotFound('Node with name \'' . $name . '\' could not be found'); - } -} diff --git a/apps/dav/lib/caldav/calendarroot.php b/apps/dav/lib/caldav/calendarroot.php deleted file mode 100644 index d302da3d861..00000000000 --- a/apps/dav/lib/caldav/calendarroot.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\CalDAV; - -class CalendarRoot extends \Sabre\CalDAV\CalendarRoot { - - function getChildForPrincipal(array $principal) { - return new CalendarHome($this->caldavBackend, $principal); - } -} \ No newline at end of file diff --git a/apps/dav/lib/caldav/schedule/imipplugin.php b/apps/dav/lib/caldav/schedule/imipplugin.php deleted file mode 100644 index d9f2ec674f0..00000000000 --- a/apps/dav/lib/caldav/schedule/imipplugin.php +++ /dev/null @@ -1,128 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\CalDAV\Schedule; - -use OCP\ILogger; -use OCP\Mail\IMailer; -use Sabre\DAV; -use Sabre\VObject\ITip; -use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin; -/** - * 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 IMipPlugin extends SabreIMipPlugin { - - /** @var IMailer */ - private $mailer; - - /** @var ILogger */ - private $logger; - - /** - * Creates the email handler. - * - * @param IMailer $mailer - */ - function __construct(IMailer $mailer, ILogger $logger) { - parent::__construct(''); - $this->mailer = $mailer; - $this->logger = $logger; - } - - /** - * Event handler for the 'schedule' event. - * - * @param ITip\Message $iTipMessage - * @return void - */ - function schedule(ITip\Message $iTipMessage) { - - // Not sending any emails if the system considers the update - // insignificant. - if (!$iTipMessage->significantChange) { - if (!$iTipMessage->scheduleStatus) { - $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; - } - return; - } - - $summary = $iTipMessage->message->VEVENT->SUMMARY; - - if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') { - return; - } - - if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') { - return; - } - - $sender = substr($iTipMessage->sender, 7); - $recipient = substr($iTipMessage->recipient, 7); - - $senderName = ($iTipMessage->senderName) ? $iTipMessage->senderName : null; - $recipientName = ($iTipMessage->recipientName) ? $iTipMessage->recipientName : null; - - $subject = 'SabreDAV iTIP message'; - switch (strtoupper($iTipMessage->method)) { - case 'REPLY' : - $subject = 'Re: ' . $summary; - break; - case 'REQUEST' : - $subject = $summary; - break; - case 'CANCEL' : - $subject = 'Cancelled: ' . $summary; - break; - } - - $contentType = 'text/calendar; charset=UTF-8; method=' . $iTipMessage->method; - - $message = $this->mailer->createMessage(); - - $message->setReplyTo([$sender => $senderName]) - ->setTo([$recipient => $recipientName]) - ->setSubject($subject) - ->setBody($iTipMessage->message->serialize(), $contentType); - try { - $failed = $this->mailer->send($message); - if ($failed) { - $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); - $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; - } - $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; - } catch(\Exception $ex) { - $this->logger->logException($ex, ['app' => 'dav']); - $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; - } - } - -} diff --git a/apps/dav/lib/carddav/addressbook.php b/apps/dav/lib/carddav/addressbook.php deleted file mode 100644 index 8b1b600ec3d..00000000000 --- a/apps/dav/lib/carddav/addressbook.php +++ /dev/null @@ -1,182 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\CardDAV; - -use OCA\DAV\DAV\Sharing\IShareable; -use Sabre\CardDAV\Card; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\PropPatch; - -class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable { - - /** - * Updates the list of shares. - * - * The first array is a list of people that are to be added to the - * addressbook. - * - * 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) { - /** @var CardDavBackend $carddavBackend */ - $carddavBackend = $this->carddavBackend; - $carddavBackend->updateShares($this, $add, $remove); - } - - /** - * Returns the list of people whom this addressbook is shared with. - * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share - * - * @return array - */ - function getShares() { - /** @var CardDavBackend $carddavBackend */ - $carddavBackend = $this->carddavBackend; - return $carddavBackend->getShares($this->getResourceId()); - } - - function getACL() { - $acl = [ - [ - 'privilege' => '{DAV:}read', - 'principal' => $this->getOwner(), - 'protected' => true, - ]]; - $acl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => $this->getOwner(), - 'protected' => true, - ]; - if ($this->getOwner() !== parent::getOwner()) { - $acl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => parent::getOwner(), - 'protected' => true, - ]; - if ($this->canWrite()) { - $acl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => parent::getOwner(), - 'protected' => true, - ]; - } - } - if ($this->getOwner() === 'principals/system/system') { - $acl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => '{DAV:}authenticated', - 'protected' => true, - ]; - } - - /** @var CardDavBackend $carddavBackend */ - $carddavBackend = $this->carddavBackend; - return $carddavBackend->applyShareAcl($this->getResourceId(), $acl); - } - - function getChildACL() { - return $this->getACL(); - } - - function getChild($name) { - - $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name); - if (!$obj) { - throw new NotFound('Card not found'); - } - $obj['acl'] = $this->getChildACL(); - return new Card($this->carddavBackend, $this->addressBookInfo, $obj); - - } - - /** - * @return int - */ - public function getResourceId() { - return $this->addressBookInfo['id']; - } - - function getOwner() { - if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) { - return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal']; - } - return parent::getOwner(); - } - - function delete() { - if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) { - $principal = 'principal:' . parent::getOwner(); - $shares = $this->getShares(); - $shares = array_filter($shares, function($share) use ($principal){ - return $share['href'] === $principal; - }); - if (empty($shares)) { - throw new Forbidden(); - } - - /** @var CardDavBackend $cardDavBackend */ - $cardDavBackend = $this->carddavBackend; - $cardDavBackend->updateShares($this, [], [ - 'href' => $principal - ]); - return; - } - parent::delete(); - } - - function propPatch(PropPatch $propPatch) { - if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) { - throw new Forbidden(); - } - parent::propPatch($propPatch); - } - - public function getContactsGroups() { - /** @var CardDavBackend $cardDavBackend */ - $cardDavBackend = $this->carddavBackend; - - return $cardDavBackend->collectCardProperties($this->getResourceId(), 'CATEGORIES'); - } - - private function canWrite() { - if (isset($this->addressBookInfo['{http://owncloud.org/ns}read-only'])) { - return !$this->addressBookInfo['{http://owncloud.org/ns}read-only']; - } - return true; - } -} diff --git a/apps/dav/lib/carddav/addressbookimpl.php b/apps/dav/lib/carddav/addressbookimpl.php deleted file mode 100644 index 8b29d6d5c0c..00000000000 --- a/apps/dav/lib/carddav/addressbookimpl.php +++ /dev/null @@ -1,224 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CardDAV; - -use OCP\Constants; -use OCP\IAddressBook; -use Sabre\VObject\Component\VCard; -use Sabre\VObject\Property\Text; -use Sabre\VObject\Reader; -use Sabre\VObject\UUIDUtil; - -class AddressBookImpl implements IAddressBook { - - /** @var CardDavBackend */ - private $backend; - - /** @var array */ - private $addressBookInfo; - - /** @var AddressBook */ - private $addressBook; - - /** - * AddressBookImpl constructor. - * - * @param AddressBook $addressBook - * @param array $addressBookInfo - * @param CardDavBackend $backend - */ - public function __construct( - AddressBook $addressBook, - array $addressBookInfo, - CardDavBackend $backend) { - - $this->addressBook = $addressBook; - $this->addressBookInfo = $addressBookInfo; - $this->backend = $backend; - } - - /** - * @return string defining the technical unique key - * @since 5.0.0 - */ - public function getKey() { - return $this->addressBookInfo['id']; - } - - /** - * In comparison to getKey() this function returns a human readable (maybe translated) name - * - * @return mixed - * @since 5.0.0 - */ - public function getDisplayName() { - return $this->addressBookInfo['{DAV:}displayname']; - } - - /** - * @param string $pattern which should match within the $searchProperties - * @param array $searchProperties defines the properties within the query pattern should match - * @param array $options - for future use. One should always have options! - * @return array an array of contacts which are arrays of key-value-pairs - * @since 5.0.0 - */ - public function search($pattern, $searchProperties, $options) { - $result = $this->backend->search($this->getKey(), $pattern, $searchProperties); - - $vCards = []; - foreach ($result as $cardData) { - $vCards[] = $this->vCard2Array($this->readCard($cardData)); - } - - return $vCards; - } - - /** - * @param array $properties this array if key-value-pairs defines a contact - * @return array an array representing the contact just created or updated - * @since 5.0.0 - */ - public function createOrUpdate($properties) { - $update = false; - if (!isset($properties['UID'])) { // create a new contact - $uid = $this->createUid(); - $uri = $uid . '.vcf'; - $vCard = $this->createEmptyVCard($uid); - } else { // update existing contact - $uid = $properties['UID']; - $uri = $uid . '.vcf'; - $vCardData = $this->backend->getCard($this->getKey(), $uri); - $vCard = $this->readCard($vCardData['carddata']); - $update = true; - } - - foreach ($properties as $key => $value) { - $vCard->$key = $vCard->createProperty($key, $value); - } - - if ($update) { - $this->backend->updateCard($this->getKey(), $uri, $vCard->serialize()); - } else { - $this->backend->createCard($this->getKey(), $uri, $vCard->serialize()); - } - - return $this->vCard2Array($vCard); - - } - - /** - * @return mixed - * @since 5.0.0 - */ - public function getPermissions() { - $permissions = $this->addressBook->getACL(); - $result = 0; - foreach ($permissions as $permission) { - switch($permission['privilege']) { - case '{DAV:}read': - $result |= Constants::PERMISSION_READ; - break; - case '{DAV:}write': - $result |= Constants::PERMISSION_CREATE; - $result |= Constants::PERMISSION_UPDATE; - break; - case '{DAV:}all': - $result |= Constants::PERMISSION_ALL; - break; - } - } - - return $result; - } - - /** - * @param object $id the unique identifier to a contact - * @return bool successful or not - * @since 5.0.0 - */ - public function delete($id) { - $uri = $this->backend->getCardUri($id); - return $this->backend->deleteCard($this->addressBookInfo['id'], $uri); - } - - /** - * read vCard data into a vCard object - * - * @param string $cardData - * @return VCard - */ - protected function readCard($cardData) { - return Reader::read($cardData); - } - - /** - * create UID for contact - * - * @return string - */ - protected function createUid() { - do { - $uid = $this->getUid(); - $contact = $this->backend->getContact($this->getKey(), $uid . '.vcf'); - } while (!empty($contact)); - - return $uid; - } - - /** - * getUid is only there for testing, use createUid instead - */ - protected function getUid() { - return UUIDUtil::getUUID(); - } - - /** - * create empty vcard - * - * @param string $uid - * @return VCard - */ - protected function createEmptyVCard($uid) { - $vCard = new VCard(); - $vCard->add(new Text($vCard, 'UID', $uid)); - return $vCard; - } - - /** - * create array with all vCard properties - * - * @param VCard $vCard - * @return array - */ - protected function vCard2Array(VCard $vCard) { - $result = []; - foreach ($vCard->children as $property) { - $result[$property->name] = $property->getValue(); - } - if ($this->addressBookInfo['principaluri'] === 'principals/system/system' && - $this->addressBookInfo['uri'] === 'system') { - $result['isLocalSystemBook'] = true; - } - return $result; - } -} diff --git a/apps/dav/lib/carddav/addressbookroot.php b/apps/dav/lib/carddav/addressbookroot.php deleted file mode 100644 index 99c36c2e767..00000000000 --- a/apps/dav/lib/carddav/addressbookroot.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\CardDAV; - -class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { - - /** - * This method returns a node for a principal. - * - * The passed array contains principal information, and is guaranteed to - * at least contain a uri item. Other properties may or may not be - * supplied by the authentication backend. - * - * @param array $principal - * @return \Sabre\DAV\INode - */ - function getChildForPrincipal(array $principal) { - - return new UserAddressBooks($this->carddavBackend, $principal['uri']); - - } - - function getName() { - - if ($this->principalPrefix === 'principals') { - return parent::getName(); - } - // Grabbing all the components of the principal path. - $parts = explode('/', $this->principalPrefix); - - // We are only interested in the second part. - return $parts[1]; - - } - -} diff --git a/apps/dav/lib/carddav/carddavbackend.php b/apps/dav/lib/carddav/carddavbackend.php deleted file mode 100644 index 28d5ed1ae99..00000000000 --- a/apps/dav/lib/carddav/carddavbackend.php +++ /dev/null @@ -1,987 +0,0 @@ - - * @author Björn Schießle - * @author Joas Schilling - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CardDAV; - -use OCA\DAV\Connector\Sabre\Principal; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCA\DAV\DAV\Sharing\Backend; -use OCA\DAV\DAV\Sharing\IShareable; -use OCP\IDBConnection; -use PDO; -use Sabre\CardDAV\Backend\BackendInterface; -use Sabre\CardDAV\Backend\SyncSupport; -use Sabre\CardDAV\Plugin; -use Sabre\DAV\Exception\BadRequest; -use Sabre\HTTP\URLUtil; -use Sabre\VObject\Component\VCard; -use Sabre\VObject\Reader; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; - -class CardDavBackend implements BackendInterface, SyncSupport { - - /** @var Principal */ - private $principalBackend; - - /** @var string */ - private $dbCardsTable = 'cards'; - - /** @var string */ - private $dbCardsPropertiesTable = 'cards_properties'; - - /** @var IDBConnection */ - private $db; - - /** @var Backend */ - private $sharingBackend; - - /** @var array properties to index */ - public static $indexProperties = array( - 'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME', - 'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD'); - - /** @var EventDispatcherInterface */ - private $dispatcher; - - /** - * CardDavBackend constructor. - * - * @param IDBConnection $db - * @param Principal $principalBackend - * @param EventDispatcherInterface $dispatcher - */ - public function __construct(IDBConnection $db, - Principal $principalBackend, - EventDispatcherInterface $dispatcher = null) { - $this->db = $db; - $this->principalBackend = $principalBackend; - $this->dispatcher = $dispatcher; - $this->sharingBackend = new Backend($this->db, $principalBackend, 'addressbook'); - } - - /** - * Returns the list of address books 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 - */ - function getAddressBooksForUser($principalUri) { - $principalUriOriginal = $principalUri; - $principalUri = $this->convertPrincipal($principalUri, true); - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) - ->from('addressbooks') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); - - $addressBooks = []; - - $result = $query->execute(); - while($row = $result->fetch()) { - $addressBooks[$row['id']] = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $this->convertPrincipal($row['principaluri'], false), - '{DAV:}displayname' => $row['displayname'], - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', - ]; - } - $result->closeCursor(); - - // query for shared calendars - $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); - $principals[]= $principalUri; - - $query = $this->db->getQueryBuilder(); - $result = $query->select(['a.id', 'a.uri', 'a.displayname', 'a.principaluri', 'a.description', 'a.synctoken', 's.access']) - ->from('dav_shares', 's') - ->join('s', 'addressbooks', 'a', $query->expr()->eq('s.resourceid', 'a.id')) - ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) - ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) - ->setParameter('type', 'addressbook') - ->setParameter('principaluri', $principals, IQueryBuilder::PARAM_STR_ARRAY) - ->execute(); - - while($row = $result->fetch()) { - list(, $name) = URLUtil::splitPath($row['principaluri']); - $uri = $row['uri'] . '_shared_by_' . $name; - $displayName = $row['displayname'] . "($name)"; - if (!isset($addressBooks[$row['id']])) { - $addressBooks[$row['id']] = [ - 'id' => $row['id'], - 'uri' => $uri, - 'principaluri' => $principalUri, - '{DAV:}displayname' => $displayName, - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, - ]; - } - } - $result->closeCursor(); - - return array_values($addressBooks); - } - - /** - * @param int $addressBookId - */ - public function getAddressBookById($addressBookId) { - $query = $this->db->getQueryBuilder(); - $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) - ->from('addressbooks') - ->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) - ->execute(); - - $row = $result->fetch(); - $result->closeCursor(); - if ($row === false) { - return null; - } - - return [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $row['principaluri'], - '{DAV:}displayname' => $row['displayname'], - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', - ]; - } - - /** - * @param $addressBookUri - * @return array|null - */ - public function getAddressBooksByUri($principal, $addressBookUri) { - $query = $this->db->getQueryBuilder(); - $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) - ->from('addressbooks') - ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri))) - ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) - ->setMaxResults(1) - ->execute(); - - $row = $result->fetch(); - $result->closeCursor(); - if ($row === false) { - return null; - } - - return [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $row['principaluri'], - '{DAV:}displayname' => $row['displayname'], - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'], - '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', - ]; - } - - /** - * Updates properties for an address book. - * - * The list of mutations is stored in a Sabre\DAV\PropPatch object. - * To do the actual updates, you must tell this object which properties - * you're going to process with the handle() method. - * - * Calling the handle method is like telling the PropPatch object "I - * promise I can handle updating this property". - * - * Read the PropPatch documentation for more info and examples. - * - * @param string $addressBookId - * @param \Sabre\DAV\PropPatch $propPatch - * @return void - */ - function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { - $supportedProperties = [ - '{DAV:}displayname', - '{' . Plugin::NS_CARDDAV . '}addressbook-description', - ]; - - $propPatch->handle($supportedProperties, function($mutations) use ($addressBookId) { - - $updates = []; - foreach($mutations as $property=>$newValue) { - - switch($property) { - case '{DAV:}displayname' : - $updates['displayname'] = $newValue; - break; - case '{' . Plugin::NS_CARDDAV . '}addressbook-description' : - $updates['description'] = $newValue; - break; - } - } - $query = $this->db->getQueryBuilder(); - $query->update('addressbooks'); - - foreach($updates as $key=>$value) { - $query->set($key, $query->createNamedParameter($value)); - } - $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) - ->execute(); - - $this->addChange($addressBookId, "", 2); - - return true; - - }); - } - - /** - * Creates a new address book - * - * @param string $principalUri - * @param string $url Just the 'basename' of the url. - * @param array $properties - * @return int - * @throws BadRequest - */ - function createAddressBook($principalUri, $url, array $properties) { - $values = [ - 'displayname' => null, - 'description' => null, - 'principaluri' => $principalUri, - 'uri' => $url, - 'synctoken' => 1 - ]; - - foreach($properties as $property=>$newValue) { - - switch($property) { - case '{DAV:}displayname' : - $values['displayname'] = $newValue; - break; - case '{' . Plugin::NS_CARDDAV . '}addressbook-description' : - $values['description'] = $newValue; - break; - default : - throw new BadRequest('Unknown property: ' . $property); - } - - } - - // Fallback to make sure the displayname is set. Some clients may refuse - // to work with addressbooks not having a displayname. - if(is_null($values['displayname'])) { - $values['displayname'] = $url; - } - - $query = $this->db->getQueryBuilder(); - $query->insert('addressbooks') - ->values([ - 'uri' => $query->createParameter('uri'), - 'displayname' => $query->createParameter('displayname'), - 'description' => $query->createParameter('description'), - 'principaluri' => $query->createParameter('principaluri'), - 'synctoken' => $query->createParameter('synctoken'), - ]) - ->setParameters($values) - ->execute(); - - return $query->getLastInsertId(); - } - - /** - * Deletes an entire addressbook and all its contents - * - * @param mixed $addressBookId - * @return void - */ - function deleteAddressBook($addressBookId) { - $query = $this->db->getQueryBuilder(); - $query->delete('cards') - ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) - ->setParameter('addressbookid', $addressBookId) - ->execute(); - - $query->delete('addressbookchanges') - ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) - ->setParameter('addressbookid', $addressBookId) - ->execute(); - - $query->delete('addressbooks') - ->where($query->expr()->eq('id', $query->createParameter('id'))) - ->setParameter('id', $addressBookId) - ->execute(); - - $this->sharingBackend->deleteAllShares($addressBookId); - - $query->delete($this->dbCardsPropertiesTable) - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->execute(); - - } - - /** - * Returns all cards for a specific addressbook id. - * - * This method should return the following properties for each card: - * * carddata - raw vcard data - * * uri - Some unique url - * * lastmodified - A unix timestamp - * - * It's recommended to also return the following properties: - * * etag - A unique etag. This must change every time the card changes. - * * size - The size of the card in bytes. - * - * If these last two properties are provided, less time will be spent - * calculating them. If they are specified, you can also ommit carddata. - * This may speed up certain requests, especially with large cards. - * - * @param mixed $addressBookId - * @return array - */ - function getCards($addressBookId) { - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) - ->from('cards') - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); - - $cards = []; - - $result = $query->execute(); - while($row = $result->fetch()) { - $row['etag'] = '"' . $row['etag'] . '"'; - $row['carddata'] = $this->readBlob($row['carddata']); - $cards[] = $row; - } - $result->closeCursor(); - - return $cards; - } - - /** - * Returns a specific card. - * - * The same set of properties must be returned as with getCards. The only - * exception is that 'carddata' is absolutely required. - * - * If the card does not exist, you must return false. - * - * @param mixed $addressBookId - * @param string $cardUri - * @return array - */ - function getCard($addressBookId, $cardUri) { - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) - ->from('cards') - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) - ->setMaxResults(1); - - $result = $query->execute(); - $row = $result->fetch(); - if (!$row) { - return false; - } - $row['etag'] = '"' . $row['etag'] . '"'; - $row['carddata'] = $this->readBlob($row['carddata']); - - return $row; - } - - /** - * Returns a list of cards. - * - * This method should work identical to getCard, but instead return all the - * cards in the list as an array. - * - * If the backend supports this, it may allow for some speed-ups. - * - * @param mixed $addressBookId - * @param string[] $uris - * @return array - */ - function getMultipleCards($addressBookId, array $uris) { - $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata']) - ->from('cards') - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) - ->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); - - $cards = []; - - $result = $query->execute(); - while($row = $result->fetch()) { - $row['etag'] = '"' . $row['etag'] . '"'; - $row['carddata'] = $this->readBlob($row['carddata']); - $cards[] = $row; - } - $result->closeCursor(); - - return $cards; - } - - /** - * 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 - */ - function createCard($addressBookId, $cardUri, $cardData) { - $etag = md5($cardData); - - $query = $this->db->getQueryBuilder(); - $query->insert('cards') - ->values([ - 'carddata' => $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB), - 'uri' => $query->createNamedParameter($cardUri), - 'lastmodified' => $query->createNamedParameter(time()), - 'addressbookid' => $query->createNamedParameter($addressBookId), - 'size' => $query->createNamedParameter(strlen($cardData)), - 'etag' => $query->createNamedParameter($etag), - ]) - ->execute(); - - $this->addChange($addressBookId, $cardUri, 1); - $this->updateProperties($addressBookId, $cardUri, $cardData); - - if (!is_null($this->dispatcher)) { - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri, - 'cardData' => $cardData])); - } - - return '"' . $etag . '"'; - } - - /** - * Updates a card. - * - * The addressbook id will be passed as the first argument. This is the - * same id as it is returned from the getAddressBooksForUser method. - * - * The cardUri is a base uri, and doesn't include the full path. The - * cardData argument is the vcard body, and is passed as a string. - * - * It is possible to return an ETag from this method. This ETag should - * match that of the updated resource, and must be enclosed with double - * quotes (that is: the string itself must contain the actual quotes). - * - * You should only return the ETag if you store the carddata as-is. If a - * subsequent GET request on the same card does not have the same body, - * byte-by-byte and you did return an ETag here, clients tend to get - * confused. - * - * If you don't return an ETag, you can just return null. - * - * @param mixed $addressBookId - * @param string $cardUri - * @param string $cardData - * @return string - */ - function updateCard($addressBookId, $cardUri, $cardData) { - - $etag = md5($cardData); - $query = $this->db->getQueryBuilder(); - $query->update('cards') - ->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB)) - ->set('lastmodified', $query->createNamedParameter(time())) - ->set('size', $query->createNamedParameter(strlen($cardData))) - ->set('etag', $query->createNamedParameter($etag)) - ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->execute(); - - $this->addChange($addressBookId, $cardUri, 2); - $this->updateProperties($addressBookId, $cardUri, $cardData); - - if (!is_null($this->dispatcher)) { - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri, - 'cardData' => $cardData])); - } - - return '"' . $etag . '"'; - } - - /** - * Deletes a card - * - * @param mixed $addressBookId - * @param string $cardUri - * @return bool - */ - function deleteCard($addressBookId, $cardUri) { - try { - $cardId = $this->getCardId($addressBookId, $cardUri); - } catch (\InvalidArgumentException $e) { - $cardId = null; - } - $query = $this->db->getQueryBuilder(); - $ret = $query->delete('cards') - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) - ->execute(); - - $this->addChange($addressBookId, $cardUri, 3); - - if (!is_null($this->dispatcher)) { - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri])); - } - - if ($ret === 1) { - if ($cardId !== null) { - $this->purgeProperties($addressBookId, $cardId); - } - return true; - } - - return false; - } - - /** - * The getChanges method returns all the changes that have happened, since - * the specified syncToken in the specified address book. - * - * This function should return an array, such as the following: - * - * [ - * 'syncToken' => 'The current synctoken', - * 'added' => [ - * 'new.txt', - * ], - * 'modified' => [ - * 'modified.txt', - * ], - * 'deleted' => [ - * 'foo.php.bak', - * 'old.txt' - * ] - * ]; - * - * The returned syncToken property should reflect the *current* syncToken - * of the calendar, as reported in the {http://sabredav.org/ns}sync-token - * property. This is needed here too, to ensure the operation is atomic. - * - * If the $syncToken argument is specified as null, this is an initial - * sync, and all members should be reported. - * - * The modified property is an array of nodenames that have changed since - * the last token. - * - * The deleted property is an array with nodenames, that have been deleted - * from collection. - * - * The $syncLevel argument is basically the 'depth' of the report. If it's - * 1, you only have to report changes that happened only directly in - * immediate descendants. If it's 2, it should also include changes from - * the nodes below the child collections. (grandchildren) - * - * The $limit argument allows a client to specify how many results should - * be returned at most. If the limit is not specified, it should be treated - * as infinite. - * - * If the limit (infinite or not) is higher than you're willing to return, - * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. - * - * If the syncToken is expired (due to data cleanup) or unknown, you must - * return null. - * - * The limit is 'suggestive'. You are free to ignore it. - * - * @param string $addressBookId - * @param string $syncToken - * @param int $syncLevel - * @param int $limit - * @return array - */ - function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { - // Current synctoken - $stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*addressbooks` WHERE `id` = ?'); - $stmt->execute([ $addressBookId ]); - $currentToken = $stmt->fetchColumn(0); - - if (is_null($currentToken)) return null; - - $result = [ - 'syncToken' => $currentToken, - 'added' => [], - 'modified' => [], - 'deleted' => [], - ]; - - if ($syncToken) { - - $query = "SELECT `uri`, `operation` FROM `*PREFIX*addressbookchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `addressbookid` = ? ORDER BY `synctoken`"; - if ($limit>0) { - $query .= " `LIMIT` " . (int)$limit; - } - - // Fetching all changes - $stmt = $this->db->prepare($query); - $stmt->execute([$syncToken, $currentToken, $addressBookId]); - - $changes = []; - - // This loop ensures that any duplicates are overwritten, only the - // last change on a node is relevant. - while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - $changes[$row['uri']] = $row['operation']; - - } - - foreach($changes as $uri => $operation) { - - switch($operation) { - case 1: - $result['added'][] = $uri; - break; - case 2: - $result['modified'][] = $uri; - break; - case 3: - $result['deleted'][] = $uri; - break; - } - - } - } else { - // No synctoken supplied, this is the initial sync. - $query = "SELECT `uri` FROM `*PREFIX*cards` WHERE `addressbookid` = ?"; - $stmt = $this->db->prepare($query); - $stmt->execute([$addressBookId]); - - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); - } - return $result; - } - - /** - * Adds a change record to the addressbookchanges table. - * - * @param mixed $addressBookId - * @param string $objectUri - * @param int $operation 1 = add, 2 = modify, 3 = delete - * @return void - */ - protected function addChange($addressBookId, $objectUri, $operation) { - $sql = 'INSERT INTO `*PREFIX*addressbookchanges`(`uri`, `synctoken`, `addressbookid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*addressbooks` WHERE `id` = ?'; - $stmt = $this->db->prepare($sql); - $stmt->execute([ - $objectUri, - $addressBookId, - $operation, - $addressBookId - ]); - $stmt = $this->db->prepare('UPDATE `*PREFIX*addressbooks` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?'); - $stmt->execute([ - $addressBookId - ]); - } - - private function readBlob($cardData) { - if (is_resource($cardData)) { - return stream_get_contents($cardData); - } - - return $cardData; - } - - /** - * @param IShareable $shareable - * @param string[] $add - * @param string[] $remove - */ - public function updateShares(IShareable $shareable, $add, $remove) { - $this->sharingBackend->updateShares($shareable, $add, $remove); - } - - /** - * search contact - * - * @param int $addressBookId - * @param string $pattern which should match within the $searchProperties - * @param array $searchProperties defines the properties within the query pattern should match - * @return array an array of contacts which are arrays of key-value-pairs - */ - public function search($addressBookId, $pattern, $searchProperties) { - $query = $this->db->getQueryBuilder(); - $query2 = $this->db->getQueryBuilder(); - $query2->selectDistinct('cp.cardid')->from($this->dbCardsPropertiesTable, 'cp'); - foreach ($searchProperties as $property) { - $query2->orWhere( - $query2->expr()->andX( - $query2->expr()->eq('cp.name', $query->createNamedParameter($property)), - $query2->expr()->ilike('cp.value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')) - ) - ); - } - $query2->andWhere($query2->expr()->eq('cp.addressbookid', $query->createNamedParameter($addressBookId))); - - $query->select('c.carddata')->from($this->dbCardsTable, 'c') - ->where($query->expr()->in('c.id', $query->createFunction($query2->getSQL()))); - - $result = $query->execute(); - $cards = $result->fetchAll(); - - $result->closeCursor(); - - return array_map(function($array) {return $this->readBlob($array['carddata']);}, $cards); - - } - - /** - * @param int $bookId - * @param string $name - * @return array - */ - public function collectCardProperties($bookId, $name) { - $query = $this->db->getQueryBuilder(); - $result = $query->selectDistinct('value') - ->from($this->dbCardsPropertiesTable) - ->where($query->expr()->eq('name', $query->createNamedParameter($name))) - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId))) - ->execute(); - - $all = $result->fetchAll(PDO::FETCH_COLUMN); - $result->closeCursor(); - - return $all; - } - - /** - * get URI from a given contact - * - * @param int $id - * @return string - */ - public function getCardUri($id) { - $query = $this->db->getQueryBuilder(); - $query->select('uri')->from($this->dbCardsTable) - ->where($query->expr()->eq('id', $query->createParameter('id'))) - ->setParameter('id', $id); - - $result = $query->execute(); - $uri = $result->fetch(); - $result->closeCursor(); - - if (!isset($uri['uri'])) { - throw new \InvalidArgumentException('Card does not exists: ' . $id); - } - - return $uri['uri']; - } - - /** - * return contact with the given URI - * - * @param int $addressBookId - * @param string $uri - * @returns array - */ - public function getContact($addressBookId, $uri) { - $result = []; - $query = $this->db->getQueryBuilder(); - $query->select('*')->from($this->dbCardsTable) - ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); - $queryResult = $query->execute(); - $contact = $queryResult->fetch(); - $queryResult->closeCursor(); - - if (is_array($contact)) { - $result = $contact; - } - - return $result; - } - - /** - * Returns the list of people whom this address book is shared with. - * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share - * - * @return array - */ - public function getShares($addressBookId) { - return $this->sharingBackend->getShares($addressBookId); - } - - /** - * update properties table - * - * @param int $addressBookId - * @param string $cardUri - * @param string $vCardSerialized - */ - protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) { - $cardId = $this->getCardId($addressBookId, $cardUri); - $vCard = $this->readCard($vCardSerialized); - - $this->purgeProperties($addressBookId, $cardId); - - $query = $this->db->getQueryBuilder(); - $query->insert($this->dbCardsPropertiesTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter($addressBookId), - 'cardid' => $query->createNamedParameter($cardId), - 'name' => $query->createParameter('name'), - 'value' => $query->createParameter('value'), - 'preferred' => $query->createParameter('preferred') - ] - ); - - foreach ($vCard->children as $property) { - if(!in_array($property->name, self::$indexProperties)) { - continue; - } - $preferred = 0; - foreach($property->parameters as $parameter) { - if ($parameter->name == 'TYPE' && strtoupper($parameter->getValue()) == 'PREF') { - $preferred = 1; - break; - } - } - $query->setParameter('name', $property->name); - $query->setParameter('value', substr($property->getValue(), 0, 254)); - $query->setParameter('preferred', $preferred); - $query->execute(); - } - } - - /** - * read vCard data into a vCard object - * - * @param string $cardData - * @return VCard - */ - protected function readCard($cardData) { - return Reader::read($cardData); - } - - /** - * delete all properties from a given card - * - * @param int $addressBookId - * @param int $cardId - */ - protected function purgeProperties($addressBookId, $cardId) { - $query = $this->db->getQueryBuilder(); - $query->delete($this->dbCardsPropertiesTable) - ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId))) - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); - $query->execute(); - } - - /** - * get ID from a given contact - * - * @param int $addressBookId - * @param string $uri - * @return int - */ - protected function getCardId($addressBookId, $uri) { - $query = $this->db->getQueryBuilder(); - $query->select('id')->from($this->dbCardsTable) - ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) - ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); - - $result = $query->execute(); - $cardIds = $result->fetch(); - $result->closeCursor(); - - if (!isset($cardIds['id'])) { - throw new \InvalidArgumentException('Card does not exists: ' . $uri); - } - - return (int)$cardIds['id']; - } - - /** - * For shared address books the sharee is set in the ACL of the address book - * @param $addressBookId - * @param $acl - * @return array - */ - public function applyShareAcl($addressBookId, $acl) { - return $this->sharingBackend->applyShareAcl($addressBookId, $acl); - } - - private function convertPrincipal($principalUri, $toV2) { - if ($this->principalBackend->getPrincipalPrefix() === 'principals') { - list(, $name) = URLUtil::splitPath($principalUri); - if ($toV2 === true) { - return "principals/users/$name"; - } - return "principals/$name"; - } - return $principalUri; - } -} diff --git a/apps/dav/lib/carddav/contactsmanager.php b/apps/dav/lib/carddav/contactsmanager.php deleted file mode 100644 index 7900c6ccae0..00000000000 --- a/apps/dav/lib/carddav/contactsmanager.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CardDAV; - -use OCP\Contacts\IManager; - -class ContactsManager { - - /** - * ContactsManager constructor. - * - * @param CardDavBackend $backend - */ - public function __construct(CardDavBackend $backend) { - $this->backend = $backend; - } - - /** - * @param IManager $cm - * @param string $userId - */ - public function setupContactsProvider(IManager $cm, $userId) { - $addressBooks = $this->backend->getAddressBooksForUser("principals/users/$userId"); - $this->register($cm, $addressBooks); - $addressBooks = $this->backend->getAddressBooksForUser("principals/system/system"); - $this->register($cm, $addressBooks); - } - - /** - * @param IManager $cm - * @param $addressBooks - */ - private function register(IManager $cm, $addressBooks) { - foreach ($addressBooks as $addressBookInfo) { - $addressBook = new \OCA\DAV\CardDAV\AddressBook($this->backend, $addressBookInfo); - $cm->registerAddressBook( - new AddressBookImpl( - $addressBook, - $addressBookInfo, - $this->backend - ) - ); - } - } - -} diff --git a/apps/dav/lib/carddav/converter.php b/apps/dav/lib/carddav/converter.php deleted file mode 100644 index c8d9b94c267..00000000000 --- a/apps/dav/lib/carddav/converter.php +++ /dev/null @@ -1,171 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CardDAV; - -use OCP\IImage; -use OCP\IUser; -use Sabre\VObject\Component\VCard; -use Sabre\VObject\Property\Text; - -class Converter { - - /** - * @param IUser $user - * @return VCard - */ - public function createCardFromUser(IUser $user) { - - $uid = $user->getUID(); - $displayName = $user->getDisplayName(); - $displayName = empty($displayName ) ? $uid : $displayName; - $emailAddress = $user->getEMailAddress(); - $cloudId = $user->getCloudId(); - $image = $this->getAvatarImage($user); - - $vCard = new VCard(); - $vCard->add(new Text($vCard, 'UID', $uid)); - if (!empty($displayName)) { - $vCard->add(new Text($vCard, 'FN', $displayName)); - $vCard->add(new Text($vCard, 'N', $this->splitFullName($displayName))); - } - if (!empty($emailAddress)) { - $vCard->add(new Text($vCard, 'EMAIL', $emailAddress, ['TYPE' => 'OTHER'])); - } - if (!empty($cloudId)) { - $vCard->add(new Text($vCard, 'CLOUD', $cloudId)); - } - if ($image) { - $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); - } - $vCard->validate(); - - return $vCard; - } - - /** - * @param VCard $vCard - * @param IUser $user - * @return bool - */ - public function updateCard(VCard $vCard, IUser $user) { - $uid = $user->getUID(); - $displayName = $user->getDisplayName(); - $displayName = empty($displayName ) ? $uid : $displayName; - $emailAddress = $user->getEMailAddress(); - $cloudId = $user->getCloudId(); - $image = $this->getAvatarImage($user); - - $updated = false; - if($this->propertyNeedsUpdate($vCard, 'FN', $displayName)) { - $vCard->FN = new Text($vCard, 'FN', $displayName); - unset($vCard->N); - $vCard->add(new Text($vCard, 'N', $this->splitFullName($displayName))); - $updated = true; - } - if($this->propertyNeedsUpdate($vCard, 'EMAIL', $emailAddress)) { - $vCard->EMAIL = new Text($vCard, 'EMAIL', $emailAddress); - $updated = true; - } - if($this->propertyNeedsUpdate($vCard, 'CLOUD', $cloudId)) { - $vCard->CLOUD = new Text($vCard, 'CLOUD', $cloudId); - $updated = true; - } - - if($this->propertyNeedsUpdate($vCard, 'PHOTO', $image)) { - $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); - $updated = true; - } - - if (empty($emailAddress) && !is_null($vCard->EMAIL)) { - unset($vCard->EMAIL); - $updated = true; - } - if (empty($cloudId) && !is_null($vCard->CLOUD)) { - unset($vCard->CLOUD); - $updated = true; - } - if (empty($image) && !is_null($vCard->PHOTO)) { - unset($vCard->PHOTO); - $updated = true; - } - - return $updated; - } - - /** - * @param VCard $vCard - * @param string $name - * @param string|IImage $newValue - * @return bool - */ - private function propertyNeedsUpdate(VCard $vCard, $name, $newValue) { - if (is_null($newValue)) { - return false; - } - $value = $vCard->__get($name); - if (!is_null($value)) { - $value = $value->getValue(); - $newValue = $newValue instanceof IImage ? $newValue->data() : $newValue; - - return $value !== $newValue; - } - return true; - } - - /** - * @param string $fullName - * @return string[] - */ - public function splitFullName($fullName) { - // Very basic western style parsing. I'm not gonna implement - // https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;) - - $elements = explode(' ', $fullName); - $result = ['', '', '', '', '']; - if (count($elements) > 2) { - $result[0] = implode(' ', array_slice($elements, count($elements)-1)); - $result[1] = $elements[0]; - $result[2] = implode(' ', array_slice($elements, 1, count($elements)-2)); - } elseif (count($elements) === 2) { - $result[0] = $elements[1]; - $result[1] = $elements[0]; - } else { - $result[0] = $elements[0]; - } - - return $result; - } - - /** - * @param IUser $user - * @return null|IImage - */ - private function getAvatarImage(IUser $user) { - try { - $image = $user->getAvatarImage(-1); - return $image; - } catch (\Exception $ex) { - return null; - } - } - -} diff --git a/apps/dav/lib/carddav/plugin.php b/apps/dav/lib/carddav/plugin.php deleted file mode 100644 index e02cc5686b8..00000000000 --- a/apps/dav/lib/carddav/plugin.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CardDAV; - -use OCA\DAV\CardDAV\Xml\Groups; -use Sabre\DAV\INode; -use Sabre\DAV\PropFind; -use Sabre\DAV\Server; -use Sabre\HTTP\URLUtil; - -class Plugin extends \Sabre\CardDAV\Plugin { - - function initialize(Server $server) { - $server->on('propFind', [$this, 'propFind']); - parent::initialize($server); - } - - /** - * Returns the addressbook home for a given principal - * - * @param string $principal - * @return string - */ - protected function getAddressbookHomeForPrincipal($principal) { - - if (strrpos($principal, 'principals/users', -strlen($principal)) !== false) { - list(, $principalId) = URLUtil::splitPath($principal); - return self::ADDRESSBOOK_ROOT . '/users/' . $principalId; - } - if (strrpos($principal, 'principals/groups', -strlen($principal)) !== false) { - list(, $principalId) = URLUtil::splitPath($principal); - return self::ADDRESSBOOK_ROOT . '/groups/' . $principalId; - } - if (strrpos($principal, 'principals/system', -strlen($principal)) !== false) { - list(, $principalId) = URLUtil::splitPath($principal); - return self::ADDRESSBOOK_ROOT . '/system/' . $principalId; - } - - throw new \LogicException('This is not supposed to happen'); - } - - /** - * Adds all CardDAV-specific properties - * - * @param PropFind $propFind - * @param INode $node - * @return void - */ - function propFind(PropFind $propFind, INode $node) { - - $ns = '{http://owncloud.org/ns}'; - - if ($node instanceof AddressBook) { - - $propFind->handle($ns . 'groups', function () use ($node) { - return new Groups($node->getContactsGroups()); - }); - } - } -} diff --git a/apps/dav/lib/carddav/syncjob.php b/apps/dav/lib/carddav/syncjob.php deleted file mode 100644 index 0554af6fbf1..00000000000 --- a/apps/dav/lib/carddav/syncjob.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CardDAV; - -use OC\BackgroundJob\TimedJob; -use OCA\Dav\AppInfo\Application; - -class SyncJob extends TimedJob { - - public function __construct() { - // Run once a day - $this->setInterval(24 * 60 * 60); - } - - protected function run($argument) { - $app = new Application(); - /** @var SyncService $ss */ - $ss = $app->getSyncService(); - $ss->syncInstance(); - } -} diff --git a/apps/dav/lib/carddav/syncservice.php b/apps/dav/lib/carddav/syncservice.php deleted file mode 100644 index c4741a01772..00000000000 --- a/apps/dav/lib/carddav/syncservice.php +++ /dev/null @@ -1,284 +0,0 @@ - - * @author Roeland Jago Douma - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\CardDAV; - -use OCP\AppFramework\Http; -use OCP\ILogger; -use OCP\IUser; -use OCP\IUserManager; -use Sabre\DAV\Client; -use Sabre\DAV\Xml\Response\MultiStatus; -use Sabre\DAV\Xml\Service; -use Sabre\HTTP\ClientHttpException; -use Sabre\VObject\Reader; - -class SyncService { - - /** @var CardDavBackend */ - private $backend; - - /** @var IUserManager */ - private $userManager; - - /** @var ILogger */ - private $logger; - - /** @var array */ - private $localSystemAddressBook; - - public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger) { - $this->backend = $backend; - $this->userManager = $userManager; - $this->logger = $logger; - } - - /** - * @param string $url - * @param string $userName - * @param string $sharedSecret - * @param string $syncToken - * @param int $targetBookId - * @param string $targetPrincipal - * @param array $targetProperties - * @return string - * @throws \Exception - */ - public function syncRemoteAddressBook($url, $userName, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) { - // 1. create addressbook - $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookId, $targetProperties); - $addressBookId = $book['id']; - - // 2. query changes - try { - $response = $this->requestSyncReport($url, $userName, $sharedSecret, $syncToken); - } catch (ClientHttpException $ex) { - if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) { - // remote server revoked access to the address book, remove it - $this->backend->deleteAddressBook($addressBookId); - $this->logger->info('Authorization failed, remove address book: ' . $url, ['app' => 'dav']); - throw $ex; - } - } - - // 3. apply changes - // TODO: use multi-get for download - foreach ($response['response'] as $resource => $status) { - $cardUri = basename($resource); - if (isset($status[200])) { - $vCard = $this->download($url, $sharedSecret, $resource); - $existingCard = $this->backend->getCard($addressBookId, $cardUri); - if ($existingCard === false) { - $this->backend->createCard($addressBookId, $cardUri, $vCard['body']); - } else { - $this->backend->updateCard($addressBookId, $cardUri, $vCard['body']); - } - } else { - $this->backend->deleteCard($addressBookId, $cardUri); - } - } - - return $response['token']; - } - - /** - * @param string $principal - * @param string $id - * @param array $properties - * @return array|null - * @throws \Sabre\DAV\Exception\BadRequest - */ - public function ensureSystemAddressBookExists($principal, $id, $properties) { - $book = $this->backend->getAddressBooksByUri($principal, $id); - if (!is_null($book)) { - return $book; - } - $this->backend->createAddressBook($principal, $id, $properties); - - return $this->backend->getAddressBooksByUri($principal, $id); - } - - /** - * @param string $url - * @param string $userName - * @param string $sharedSecret - * @param string $syncToken - * @return array - */ - protected function requestSyncReport($url, $userName, $sharedSecret, $syncToken) { - $settings = [ - 'baseUri' => $url . '/', - 'userName' => $userName, - 'password' => $sharedSecret, - ]; - $client = new Client($settings); - $client->setThrowExceptions(true); - - $addressBookUrl = "remote.php/dav/addressbooks/system/system/system"; - $body = $this->buildSyncCollectionRequestBody($syncToken); - - $response = $client->request('REPORT', $addressBookUrl, $body, [ - 'Content-Type' => 'application/xml' - ]); - - $result = $this->parseMultiStatus($response['body']); - - return $result; - } - - /** - * @param string $url - * @param string $sharedSecret - * @param string $resourcePath - * @return array - */ - protected function download($url, $sharedSecret, $resourcePath) { - $settings = [ - 'baseUri' => $url, - 'userName' => 'system', - 'password' => $sharedSecret, - ]; - $client = new Client($settings); - $client->setThrowExceptions(true); - - $response = $client->request('GET', $resourcePath); - return $response; - } - - /** - * @param string|null $syncToken - * @return string - */ - private function buildSyncCollectionRequestBody($syncToken) { - - $dom = new \DOMDocument('1.0', 'UTF-8'); - $dom->formatOutput = true; - $root = $dom->createElementNS('DAV:', 'd:sync-collection'); - $sync = $dom->createElement('d:sync-token', $syncToken); - $prop = $dom->createElement('d:prop'); - $cont = $dom->createElement('d:getcontenttype'); - $etag = $dom->createElement('d:getetag'); - - $prop->appendChild($cont); - $prop->appendChild($etag); - $root->appendChild($sync); - $root->appendChild($prop); - $dom->appendChild($root); - $body = $dom->saveXML(); - - return $body; - } - - /** - * @param string $body - * @return array - * @throws \Sabre\Xml\ParseException - */ - private function parseMultiStatus($body) { - $xml = new Service(); - - /** @var MultiStatus $multiStatus */ - $multiStatus = $xml->expect('{DAV:}multistatus', $body); - - $result = []; - foreach ($multiStatus->getResponses() as $response) { - $result[$response->getHref()] = $response->getResponseProperties(); - } - - return ['response' => $result, 'token' => $multiStatus->getSyncToken()]; - } - - /** - * @param IUser $user - */ - public function updateUser($user) { - $systemAddressBook = $this->getLocalSystemAddressBook(); - $addressBookId = $systemAddressBook['id']; - $converter = new Converter(); - $name = $user->getBackendClassName(); - $userId = $user->getUID(); - - $cardId = "$name:$userId.vcf"; - $card = $this->backend->getCard($addressBookId, $cardId); - if ($card === false) { - $vCard = $converter->createCardFromUser($user); - $this->backend->createCard($addressBookId, $cardId, $vCard->serialize()); - } else { - $vCard = Reader::read($card['carddata']); - if ($converter->updateCard($vCard, $user)) { - $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize()); - } - } - } - - /** - * @param IUser|string $userOrCardId - */ - public function deleteUser($userOrCardId) { - $systemAddressBook = $this->getLocalSystemAddressBook(); - if ($userOrCardId instanceof IUser){ - $name = $userOrCardId->getBackendClassName(); - $userId = $userOrCardId->getUID(); - - $userOrCardId = "$name:$userId.vcf"; - } - $this->backend->deleteCard($systemAddressBook['id'], $userOrCardId); - } - - /** - * @return array|null - */ - public function getLocalSystemAddressBook() { - if (is_null($this->localSystemAddressBook)) { - $systemPrincipal = "principals/system/system"; - $this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [ - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance' - ]); - } - - return $this->localSystemAddressBook; - } - - public function syncInstance(\Closure $progressCallback = null) { - $systemAddressBook = $this->getLocalSystemAddressBook(); - $this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $progressCallback) { - $this->updateUser($user); - if (!is_null($progressCallback)) { - $progressCallback(); - } - }); - - // remove no longer existing - $allCards = $this->backend->getCards($systemAddressBook['id']); - foreach($allCards as $card) { - $vCard = Reader::read($card['carddata']); - $uid = $vCard->UID->getValue(); - // load backend and see if user exists - if (!$this->userManager->userExists($uid)) { - $this->deleteUser($card['uri']); - } - } - } - - -} diff --git a/apps/dav/lib/carddav/useraddressbooks.php b/apps/dav/lib/carddav/useraddressbooks.php deleted file mode 100644 index 734e3829e69..00000000000 --- a/apps/dav/lib/carddav/useraddressbooks.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\CardDAV; - -class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { - - /** - * Returns a list of addressbooks - * - * @return array - */ - function getChildren() { - - $addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); - $objects = []; - foreach($addressBooks as $addressBook) { - $objects[] = new AddressBook($this->carddavBackend, $addressBook); - } - return $objects; - - } - - /** - * Returns a list of ACE's for this node. - * - * Each ACE has the following properties: - * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are - * currently the only supported privileges - * * 'principal', a url to the principal who owns the node - * * 'protected' (optional), indicating that this ACE is not allowed to - * be updated. - * - * @return array - */ - function getACL() { - - $acl = parent::getACL(); - if ($this->principalUri === 'principals/system/system') { - $acl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => '{DAV:}authenticated', - 'protected' => true, - ]; - } - - return $acl; - } - -} diff --git a/apps/dav/lib/carddav/xml/groups.php b/apps/dav/lib/carddav/xml/groups.php deleted file mode 100644 index b39615db033..00000000000 --- a/apps/dav/lib/carddav/xml/groups.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\CardDAV\Xml; - -use Sabre\Xml\XmlSerializable; -use Sabre\Xml\Element; -use Sabre\Xml\Writer; - -class Groups implements XmlSerializable { - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - /** @var string[] of TYPE:CHECKSUM */ - private $groups; - - /** - * @param string $groups - */ - public function __construct($groups) { - $this->groups = $groups; - } - - function xmlSerialize(Writer $writer) { - foreach ($this->groups as $group) { - $writer->writeElement('{' . self::NS_OWNCLOUD . '}group', $group); - } - } -} diff --git a/apps/dav/lib/comments/commentnode.php b/apps/dav/lib/comments/commentnode.php deleted file mode 100644 index 339abc6382d..00000000000 --- a/apps/dav/lib/comments/commentnode.php +++ /dev/null @@ -1,261 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Comments; - - -use OCP\Comments\IComment; -use OCP\Comments\ICommentsManager; -use OCP\Comments\MessageTooLongException; -use OCP\ILogger; -use OCP\IUserManager; -use OCP\IUserSession; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\MethodNotAllowed; -use Sabre\DAV\PropPatch; - -class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties { - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}isUnread'; - const PROPERTY_NAME_MESSAGE = '{http://owncloud.org/ns}message'; - const PROPERTY_NAME_ACTOR_DISPLAYNAME = '{http://owncloud.org/ns}actorDisplayName'; - - /** @var IComment */ - public $comment; - - /** @var ICommentsManager */ - protected $commentsManager; - - /** @var ILogger */ - protected $logger; - - /** @var array list of properties with key being their name and value their setter */ - protected $properties = []; - - /** @var IUserManager */ - protected $userManager; - - /** @var IUserSession */ - protected $userSession; - - /** - * CommentNode constructor. - * - * @param ICommentsManager $commentsManager - * @param IComment $comment - * @param IUserManager $userManager - * @param IUserSession $userSession - * @param ILogger $logger - */ - public function __construct( - ICommentsManager $commentsManager, - IComment $comment, - IUserManager $userManager, - IUserSession $userSession, - ILogger $logger - ) { - $this->commentsManager = $commentsManager; - $this->comment = $comment; - $this->logger = $logger; - - $methods = get_class_methods($this->comment); - $methods = array_filter($methods, function($name){ - return strpos($name, 'get') === 0; - }); - foreach($methods as $getter) { - $name = '{'.self::NS_OWNCLOUD.'}' . lcfirst(substr($getter, 3)); - $this->properties[$name] = $getter; - } - $this->userManager = $userManager; - $this->userSession = $userSession; - } - - /** - * returns a list of all possible property names - * - * @return array - */ - static public function getPropertyNames() { - return [ - '{http://owncloud.org/ns}id', - '{http://owncloud.org/ns}parentId', - '{http://owncloud.org/ns}topmostParentId', - '{http://owncloud.org/ns}childrenCount', - '{http://owncloud.org/ns}verb', - '{http://owncloud.org/ns}actorType', - '{http://owncloud.org/ns}actorId', - '{http://owncloud.org/ns}creationDateTime', - '{http://owncloud.org/ns}latestChildDateTime', - '{http://owncloud.org/ns}objectType', - '{http://owncloud.org/ns}objectId', - // re-used property names are defined as constants - self::PROPERTY_NAME_MESSAGE, - self::PROPERTY_NAME_ACTOR_DISPLAYNAME, - self::PROPERTY_NAME_UNREAD - ]; - } - - protected function checkWriteAccessOnComment() { - $user = $this->userSession->getUser(); - if( $this->comment->getActorType() !== 'users' - || is_null($user) - || $this->comment->getActorId() !== $user->getUID() - ) { - throw new Forbidden('Only authors are allowed to edit their comment.'); - } - } - - /** - * Deleted the current node - * - * @return void - */ - function delete() { - $this->checkWriteAccessOnComment(); - $this->commentsManager->delete($this->comment->getId()); - } - - /** - * Returns the name of the node. - * - * This is used to generate the url. - * - * @return string - */ - function getName() { - return $this->comment->getId(); - } - - /** - * Renames the node - * - * @param string $name The new name - * @throws MethodNotAllowed - */ - function setName($name) { - throw new MethodNotAllowed(); - } - - /** - * Returns the last modification time, as a unix timestamp - * - * @return int - */ - function getLastModified() { - return null; - } - - /** - * update the comment's message - * - * @param $propertyValue - * @return bool - * @throws BadRequest - * @throws Forbidden - */ - public function updateComment($propertyValue) { - $this->checkWriteAccessOnComment(); - try { - $this->comment->setMessage($propertyValue); - $this->commentsManager->save($this->comment); - return true; - } catch (\Exception $e) { - $this->logger->logException($e, ['app' => 'dav/comments']); - if($e instanceof MessageTooLongException) { - $msg = 'Message exceeds allowed character limit of '; - throw new BadRequest($msg . IComment::MAX_MESSAGE_LENGTH, 0, $e); - } - return false; - } - } - - /** - * Updates properties on this node. - * - * This method received a PropPatch object, which contains all the - * information about the update. - * - * To update specific properties, call the 'handle' method on this object. - * Read the PropPatch documentation for more information. - * - * @param PropPatch $propPatch - * @return void - */ - function propPatch(PropPatch $propPatch) { - // other properties than 'message' are read only - $propPatch->handle(self::PROPERTY_NAME_MESSAGE, [$this, 'updateComment']); - } - - /** - * Returns a list of properties for this nodes. - * - * The properties list is a list of propertynames the client requested, - * encoded in clark-notation {xmlnamespace}tagname - * - * If the array is empty, it means 'all properties' were requested. - * - * Note that it's fine to liberally give properties back, instead of - * conforming to the list of requested properties. - * The Server class will filter out the extra. - * - * @param array $properties - * @return array - */ - function getProperties($properties) { - $properties = array_keys($this->properties); - - $result = []; - foreach($properties as $property) { - $getter = $this->properties[$property]; - if(method_exists($this->comment, $getter)) { - $result[$property] = $this->comment->$getter(); - } - } - - if($this->comment->getActorType() === 'users') { - $user = $this->userManager->get($this->comment->getActorId()); - $displayName = is_null($user) ? null : $user->getDisplayName(); - $result[self::PROPERTY_NAME_ACTOR_DISPLAYNAME] = $displayName; - } - - $unread = null; - $user = $this->userSession->getUser(); - if(!is_null($user)) { - $readUntil = $this->commentsManager->getReadMark( - $this->comment->getObjectType(), - $this->comment->getObjectId(), - $user - ); - if(is_null($readUntil)) { - $unread = 'true'; - } else { - $unread = $this->comment->getCreationDateTime() > $readUntil; - // re-format for output - $unread = $unread ? 'true' : 'false'; - } - } - $result[self::PROPERTY_NAME_UNREAD] = $unread; - - return $result; - } -} diff --git a/apps/dav/lib/comments/commentsplugin.php b/apps/dav/lib/comments/commentsplugin.php deleted file mode 100644 index d4a065649ef..00000000000 --- a/apps/dav/lib/comments/commentsplugin.php +++ /dev/null @@ -1,255 +0,0 @@ - - * @author Joas Schilling - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Comments; - -use OCP\Comments\IComment; -use OCP\Comments\ICommentsManager; -use OCP\IUserSession; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\Exception\ReportNotSupported; -use Sabre\DAV\Exception\UnsupportedMediaType; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Server; -use Sabre\DAV\ServerPlugin; -use Sabre\DAV\Xml\Element\Response; -use Sabre\DAV\Xml\Response\MultiStatus; -use Sabre\HTTP\RequestInterface; -use Sabre\HTTP\ResponseInterface; -use Sabre\Xml\Writer; - -/** - * Sabre plugin to handle comments: - */ -class CommentsPlugin extends ServerPlugin { - // namespace - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - const REPORT_NAME = '{http://owncloud.org/ns}filter-comments'; - const REPORT_PARAM_LIMIT = '{http://owncloud.org/ns}limit'; - const REPORT_PARAM_OFFSET = '{http://owncloud.org/ns}offset'; - const REPORT_PARAM_TIMESTAMP = '{http://owncloud.org/ns}datetime'; - - /** @var ICommentsManager */ - protected $commentsManager; - - /** @var \Sabre\DAV\Server $server */ - private $server; - - /** @var \OCP\IUserSession */ - protected $userSession; - - /** - * Comments plugin - * - * @param ICommentsManager $commentsManager - * @param IUserSession $userSession - */ - public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) { - $this->commentsManager = $commentsManager; - $this->userSession = $userSession; - } - - /** - * This initializes the plugin. - * - * This function is called by Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param Server $server - * @return void - */ - function initialize(Server $server) { - $this->server = $server; - if(strpos($this->server->getRequestUri(), 'comments/') !== 0) { - return; - } - - $this->server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; - - $this->server->xml->classMap['DateTime'] = function(Writer $writer, \DateTime $value) { - $writer->write(\Sabre\HTTP\toDate($value)); - }; - - $this->server->on('report', [$this, 'onReport']); - $this->server->on('method:POST', [$this, 'httpPost']); - } - - /** - * POST operation on Comments collections - * - * @param RequestInterface $request request object - * @param ResponseInterface $response response object - * @return null|false - */ - public function httpPost(RequestInterface $request, ResponseInterface $response) { - $path = $request->getPath(); - $node = $this->server->tree->getNodeForPath($path); - if (!$node instanceof EntityCollection) { - return null; - } - - $data = $request->getBodyAsString(); - $comment = $this->createComment( - $node->getName(), - $node->getId(), - $data, - $request->getHeader('Content-Type') - ); - - // update read marker for the current user/poster to avoid - // having their own comments marked as unread - $node->setReadMarker(null); - - $url = rtrim($request->getUrl(), '/') . '/' . urlencode($comment->getId()); - - $response->setHeader('Content-Location', $url); - - // created - $response->setStatus(201); - return false; - } - - /** - * Returns a list of reports this plugin supports. - * - * This will be used in the {DAV:}supported-report-set property. - * - * @param string $uri - * @return array - */ - public function getSupportedReportSet($uri) { - return [self::REPORT_NAME]; - } - - /** - * REPORT operations to look for comments - * - * @param string $reportName - * @param [] $report - * @param string $uri - * @return bool - * @throws NotFound - * @throws ReportNotSupported - */ - public function onReport($reportName, $report, $uri) { - $node = $this->server->tree->getNodeForPath($uri); - if(!$node instanceof EntityCollection || $reportName !== self::REPORT_NAME) { - throw new ReportNotSupported(); - } - $args = ['limit' => 0, 'offset' => 0, 'datetime' => null]; - $acceptableParameters = [ - $this::REPORT_PARAM_LIMIT, - $this::REPORT_PARAM_OFFSET, - $this::REPORT_PARAM_TIMESTAMP - ]; - $ns = '{' . $this::NS_OWNCLOUD . '}'; - foreach($report as $parameter) { - if(!in_array($parameter['name'], $acceptableParameters) || empty($parameter['value'])) { - continue; - } - $args[str_replace($ns, '', $parameter['name'])] = $parameter['value']; - } - - if(!is_null($args['datetime'])) { - $args['datetime'] = new \DateTime($args['datetime']); - } - - $results = $node->findChildren($args['limit'], $args['offset'], $args['datetime']); - - $responses = []; - foreach($results as $node) { - $nodePath = $this->server->getRequestUri() . '/' . $node->comment->getId(); - $resultSet = $this->server->getPropertiesForPath($nodePath, CommentNode::getPropertyNames()); - if(isset($resultSet[0]) && isset($resultSet[0][200])) { - $responses[] = new Response( - $this->server->getBaseUri() . $nodePath, - [200 => $resultSet[0][200]], - 200 - ); - } - - } - - $xml = $this->server->xml->write( - '{DAV:}multistatus', - new MultiStatus($responses) - ); - - $this->server->httpResponse->setStatus(207); - $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); - $this->server->httpResponse->setBody($xml); - - return false; - } - - /** - * Creates a new comment - * - * @param string $objectType e.g. "files" - * @param string $objectId e.g. the file id - * @param string $data JSON encoded string containing the properties of the tag to create - * @param string $contentType content type of the data - * @return IComment newly created comment - * - * @throws BadRequest if a field was missing - * @throws UnsupportedMediaType if the content type is not supported - */ - private function createComment($objectType, $objectId, $data, $contentType = 'application/json') { - if (explode(';', $contentType)[0] === 'application/json') { - $data = json_decode($data, true); - } else { - throw new UnsupportedMediaType(); - } - - $actorType = $data['actorType']; - $actorId = null; - if($actorType === 'users') { - $user = $this->userSession->getUser(); - if(!is_null($user)) { - $actorId = $user->getUID(); - } - } - if(is_null($actorId)) { - throw new BadRequest('Invalid actor "' . $actorType .'"'); - } - - try { - $comment = $this->commentsManager->create($actorType, $actorId, $objectType, $objectId); - $comment->setMessage($data['message']); - $comment->setVerb($data['verb']); - $this->commentsManager->save($comment); - return $comment; - } catch (\InvalidArgumentException $e) { - throw new BadRequest('Invalid input values', 0, $e); - } catch (\OCP\Comments\MessageTooLongException $e) { - $msg = 'Message exceeds allowed character limit of '; - throw new BadRequest($msg . \OCP\Comments\IComment::MAX_MESSAGE_LENGTH, 0, $e); - } - } - - - -} diff --git a/apps/dav/lib/comments/entitycollection.php b/apps/dav/lib/comments/entitycollection.php deleted file mode 100644 index a55a18c00c0..00000000000 --- a/apps/dav/lib/comments/entitycollection.php +++ /dev/null @@ -1,199 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Comments; - -use OCP\Comments\ICommentsManager; -use OCP\Files\Folder; -use OCP\ILogger; -use OCP\IUserManager; -use OCP\IUserSession; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\PropPatch; - -/** - * Class EntityCollection - * - * this represents a specific holder of comments, identified by an entity type - * (class member $name) and an entity id (class member $id). - * - * @package OCA\DAV\Comments - */ -class EntityCollection extends RootCollection implements \Sabre\DAV\IProperties { - const PROPERTY_NAME_READ_MARKER = '{http://owncloud.org/ns}readMarker'; - - /** @var Folder */ - protected $fileRoot; - - /** @var string */ - protected $id; - - /** @var ILogger */ - protected $logger; - - /** - * @param string $id - * @param string $name - * @param ICommentsManager $commentsManager - * @param Folder $fileRoot - * @param IUserManager $userManager - * @param IUserSession $userSession - * @param ILogger $logger - */ - public function __construct( - $id, - $name, - ICommentsManager $commentsManager, - Folder $fileRoot, - IUserManager $userManager, - IUserSession $userSession, - ILogger $logger - ) { - foreach(['id', 'name'] as $property) { - $$property = trim($$property); - if(empty($$property) || !is_string($$property)) { - throw new \InvalidArgumentException('"' . $property . '" parameter must be non-empty string'); - } - } - $this->id = $id; - $this->name = $name; - $this->commentsManager = $commentsManager; - $this->fileRoot = $fileRoot; - $this->logger = $logger; - $this->userManager = $userManager; - $this->userSession = $userSession; - } - - /** - * returns the ID of this entity - * - * @return string - */ - public function getId() { - return $this->id; - } - - /** - * Returns a specific child node, referenced by its name - * - * This method must throw Sabre\DAV\Exception\NotFound if the node does not - * exist. - * - * @param string $name - * @return \Sabre\DAV\INode - * @throws NotFound - */ - function getChild($name) { - try { - $comment = $this->commentsManager->get($name); - return new CommentNode( - $this->commentsManager, - $comment, - $this->userManager, - $this->userSession, - $this->logger - ); - } catch (\OCP\Comments\NotFoundException $e) { - throw new NotFound(); - } - } - - /** - * Returns an array with all the child nodes - * - * @return \Sabre\DAV\INode[] - */ - function getChildren() { - return $this->findChildren(); - } - - /** - * Returns an array of comment nodes. Result can be influenced by offset, - * limit and date time parameters. - * - * @param int $limit - * @param int $offset - * @param \DateTime|null $datetime - * @return CommentNode[] - */ - function findChildren($limit = 0, $offset = 0, \DateTime $datetime = null) { - $comments = $this->commentsManager->getForObject($this->name, $this->id, $limit, $offset, $datetime); - $result = []; - foreach($comments as $comment) { - $result[] = new CommentNode( - $this->commentsManager, - $comment, - $this->userManager, - $this->userSession, - $this->logger - ); - } - return $result; - } - - /** - * Checks if a child-node with the specified name exists - * - * @param string $name - * @return bool - */ - function childExists($name) { - try { - $this->commentsManager->get($name); - return true; - } catch (\OCP\Comments\NotFoundException $e) { - return false; - } - } - - /** - * Sets the read marker to the specified date for the logged in user - * - * @param \DateTime $value - * @return bool - */ - public function setReadMarker($value) { - $dateTime = new \DateTime($value); - $user = $this->userSession->getUser(); - $this->commentsManager->setReadMark($this->name, $this->id, $dateTime, $user); - return true; - } - - /** - * @inheritdoc - */ - function propPatch(PropPatch $propPatch) { - $propPatch->handle(self::PROPERTY_NAME_READ_MARKER, [$this, 'setReadMarker']); - } - - /** - * @inheritdoc - */ - function getProperties($properties) { - $marker = null; - $user = $this->userSession->getUser(); - if(!is_null($user)) { - $marker = $this->commentsManager->getReadMark($this->name, $this->id, $user); - } - return [self::PROPERTY_NAME_READ_MARKER => $marker]; - } -} - diff --git a/apps/dav/lib/comments/entitytypecollection.php b/apps/dav/lib/comments/entitytypecollection.php deleted file mode 100644 index 6bc42484207..00000000000 --- a/apps/dav/lib/comments/entitytypecollection.php +++ /dev/null @@ -1,125 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Comments; - -use OCP\Comments\ICommentsManager; -use OCP\Files\Folder; -use OCP\ILogger; -use OCP\IUserManager; -use OCP\IUserSession; -use Sabre\DAV\Exception\MethodNotAllowed; -use Sabre\DAV\Exception\NotFound; - -/** - * Class EntityTypeCollection - * - * This is collection on the type of things a user can leave comments on, for - * example: 'files'. - * - * Its children are instances of EntityCollection (representing a specific - * object, for example the file by id). - * - * @package OCA\DAV\Comments - */ -class EntityTypeCollection extends RootCollection { - /** @var Folder */ - protected $fileRoot; - - /** @var ILogger */ - protected $logger; - - /** - * @param string $name - * @param ICommentsManager $commentsManager - * @param Folder $fileRoot - * @param IUserManager $userManager - * @param IUserSession $userSession - * @param ILogger $logger - */ - public function __construct( - $name, - ICommentsManager $commentsManager, - Folder $fileRoot, - IUserManager $userManager, - IUserSession $userSession, - ILogger $logger - ) { - $name = trim($name); - if(empty($name) || !is_string($name)) { - throw new \InvalidArgumentException('"name" parameter must be non-empty string'); - } - $this->name = $name; - $this->commentsManager = $commentsManager; - $this->fileRoot = $fileRoot; - $this->logger = $logger; - $this->userManager = $userManager; - $this->userSession = $userSession; - } - - /** - * Returns a specific child node, referenced by its name - * - * This method must throw Sabre\DAV\Exception\NotFound if the node does not - * exist. - * - * @param string $name - * @return \Sabre\DAV\INode - * @throws NotFound - */ - function getChild($name) { - if(!$this->childExists($name)) { - throw new NotFound('Entity does not exist or is not available'); - } - return new EntityCollection( - $name, - $this->name, - $this->commentsManager, - $this->fileRoot, - $this->userManager, - $this->userSession, - $this->logger - ); - } - - /** - * Returns an array with all the child nodes - * - * @return \Sabre\DAV\INode[] - * @throws MethodNotAllowed - */ - function getChildren() { - throw new MethodNotAllowed('No permission to list folder contents'); - } - - /** - * Checks if a child-node with the specified name exists - * - * @param string $name - * @return bool - */ - function childExists($name) { - $nodes = $this->fileRoot->getById(intval($name)); - return !empty($nodes); - } - - -} diff --git a/apps/dav/lib/comments/rootcollection.php b/apps/dav/lib/comments/rootcollection.php deleted file mode 100644 index cda666f7162..00000000000 --- a/apps/dav/lib/comments/rootcollection.php +++ /dev/null @@ -1,205 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Comments; - -use OCP\Comments\ICommentsManager; -use OCP\Files\IRootFolder; -use OCP\ILogger; -use OCP\IUserManager; -use OCP\IUserSession; -use Sabre\DAV\Exception\NotAuthenticated; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\ICollection; - -class RootCollection implements ICollection { - - /** @var EntityTypeCollection[] */ - private $entityTypeCollections = []; - - /** @var ICommentsManager */ - protected $commentsManager; - - /** @var string */ - protected $name = 'comments'; - - /** @var ILogger */ - protected $logger; - - /** @var IUserManager */ - protected $userManager; - /** - * @var IUserSession - */ - protected $userSession; - /** - * @var IRootFolder - */ - protected $rootFolder; - - /** - * @param ICommentsManager $commentsManager - * @param IUserManager $userManager - * @param IUserSession $userSession - * @param IRootFolder $rootFolder - * @param ILogger $logger - */ - public function __construct( - ICommentsManager $commentsManager, - IUserManager $userManager, - IUserSession $userSession, - IRootFolder $rootFolder, - ILogger $logger) - { - $this->commentsManager = $commentsManager; - $this->logger = $logger; - $this->userManager = $userManager; - $this->userSession = $userSession; - $this->rootFolder = $rootFolder; - } - - /** - * initializes the collection. At this point of time, we need the logged in - * user. Since it is not the case when the instance is created, we cannot - * have this in the constructor. - * - * @throws NotAuthenticated - */ - protected function initCollections() { - if(!empty($this->entityTypeCollections)) { - return; - } - $user = $this->userSession->getUser(); - if(is_null($user)) { - throw new NotAuthenticated(); - } - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - $this->entityTypeCollections['files'] = new EntityTypeCollection( - 'files', - $this->commentsManager, - $userFolder, - $this->userManager, - $this->userSession, - $this->logger - ); - } - - /** - * Creates a new file in the directory - * - * @param string $name Name of the file - * @param resource|string $data Initial payload - * @return null|string - * @throws Forbidden - */ - function createFile($name, $data = null) { - throw new Forbidden('Cannot create comments by id'); - } - - /** - * Creates a new subdirectory - * - * @param string $name - * @throws Forbidden - */ - function createDirectory($name) { - throw new Forbidden('Permission denied to create collections'); - } - - /** - * Returns a specific child node, referenced by its name - * - * This method must throw Sabre\DAV\Exception\NotFound if the node does not - * exist. - * - * @param string $name - * @return \Sabre\DAV\INode - * @throws NotFound - */ - function getChild($name) { - $this->initCollections(); - if(isset($this->entityTypeCollections[$name])) { - return $this->entityTypeCollections[$name]; - } - throw new NotFound('Entity type "' . $name . '" not found."'); - } - - /** - * Returns an array with all the child nodes - * - * @return \Sabre\DAV\INode[] - */ - function getChildren() { - $this->initCollections(); - return $this->entityTypeCollections; - } - - /** - * Checks if a child-node with the specified name exists - * - * @param string $name - * @return bool - */ - function childExists($name) { - $this->initCollections(); - return isset($this->entityTypeCollections[$name]); - } - - /** - * Deleted the current node - * - * @throws Forbidden - */ - function delete() { - throw new Forbidden('Permission denied to delete this collection'); - } - - /** - * Returns the name of the node. - * - * This is used to generate the url. - * - * @return string - */ - function getName() { - return $this->name; - } - - /** - * Renames the node - * - * @param string $name The new name - * @throws Forbidden - */ - function setName($name) { - throw new Forbidden('Permission denied to rename this collection'); - } - - /** - * Returns the last modification time, as a unix timestamp - * - * @return int - */ - function getLastModified() { - return null; - } -} diff --git a/apps/dav/lib/connector/legacydavacl.php b/apps/dav/lib/connector/legacydavacl.php deleted file mode 100644 index eb6ca1fd7d5..00000000000 --- a/apps/dav/lib/connector/legacydavacl.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @author Lukas Reschke - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector; - -use OCA\DAV\Connector\Sabre\DavAclPlugin; -use Sabre\DAV\INode; -use Sabre\DAV\PropFind; -use Sabre\HTTP\URLUtil; -use Sabre\DAVACL\Xml\Property\Principal; - -class LegacyDAVACL extends DavAclPlugin { - - /** - * Converts the v1 principal `principal/` to the new v2 - * `principal/users/` which is required for permission checks - * - * @inheritdoc - */ - function getCurrentUserPrincipal() { - $principalV1 = parent::getCurrentUserPrincipal(); - if (is_null($principalV1)) { - return $principalV1; - } - return $this->convertPrincipal($principalV1, true); - } - - - /** - * @inheritdoc - */ - function getCurrentUserPrincipals() { - $principalV2 = $this->getCurrentUserPrincipal(); - - if (is_null($principalV2)) return []; - - $principalV1 = $this->convertPrincipal($principalV2, false); - return array_merge( - [ - $principalV2, - $principalV1 - ], - $this->getPrincipalMembership($principalV1) - ); - } - - private function convertPrincipal($principal, $toV2) { - list(, $name) = URLUtil::splitPath($principal); - if ($toV2) { - return "principals/users/$name"; - } - return "principals/$name"; - } - - function propFind(PropFind $propFind, INode $node) { - /* Overload current-user-principal */ - $propFind->handle('{DAV:}current-user-principal', function () { - if ($url = parent::getCurrentUserPrincipal()) { - return new Principal(Principal::HREF, $url . '/'); - } else { - return new Principal(Principal::UNAUTHENTICATED); - } - }); - parent::propFind($propFind, $node); - } -} diff --git a/apps/dav/lib/connector/publicauth.php b/apps/dav/lib/connector/publicauth.php deleted file mode 100644 index 3aa58cda244..00000000000 --- a/apps/dav/lib/connector/publicauth.php +++ /dev/null @@ -1,122 +0,0 @@ - - * @author Lukas Reschke - * @author Morris Jobke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector; - -use OCP\IRequest; -use OCP\ISession; -use OCP\Share\Exceptions\ShareNotFound; -use OCP\Share\IManager; - -/** - * Class PublicAuth - * - * @package OCA\DAV\Connector - */ -class PublicAuth extends \Sabre\DAV\Auth\Backend\AbstractBasic { - - /** @var \OCP\Share\IShare */ - private $share; - - /** @var IManager */ - private $shareManager; - - /** @var ISession */ - private $session; - - /** @var IRequest */ - private $request; - - /** - * @param IRequest $request - * @param IManager $shareManager - * @param ISession $session - */ - public function __construct(IRequest $request, - IManager $shareManager, - ISession $session) { - $this->request = $request; - $this->shareManager = $shareManager; - $this->session = $session; - } - - /** - * 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 - * @throws \Sabre\DAV\Exception\NotAuthenticated - */ - protected function validateUserPass($username, $password) { - try { - $share = $this->shareManager->getShareByToken($username); - } catch (ShareNotFound $e) { - return false; - } - - $this->share = $share; - - \OC_User::setIncognitoMode(true); - - // check if the share is password protected - if ($share->getPassword() !== null) { - if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) { - if ($this->shareManager->checkPassword($share, $password)) { - return true; - } else if ($this->session->exists('public_link_authenticated') - && $this->session->get('public_link_authenticated') === $share->getId()) { - return true; - } else { - if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) { - // do not re-authenticate over ajax, use dummy auth name to prevent browser popup - http_response_code(401); - header('WWW-Authenticate', 'DummyBasic real="ownCloud"'); - throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); - } - return false; - } - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_REMOTE) { - return true; - } else { - return false; - } - } else { - return true; - } - } - - /** - * @return \OCP\Share\IShare - */ - public function getShare() { - return $this->share; - } -} diff --git a/apps/dav/lib/connector/sabre/appenabledplugin.php b/apps/dav/lib/connector/sabre/appenabledplugin.php deleted file mode 100644 index cb061d6a309..00000000000 --- a/apps/dav/lib/connector/sabre/appenabledplugin.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCP\App\IAppManager; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\ServerPlugin; - -/** - * Plugin to check if an app is enabled for the current user - */ -class AppEnabledPlugin extends ServerPlugin { - - /** - * Reference to main server object - * - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var string - */ - private $app; - - /** - * @var \OCP\App\IAppManager - */ - private $appManager; - - /** - * @param string $app - * @param \OCP\App\IAppManager $appManager - */ - public function __construct($app, IAppManager $appManager) { - $this->app = $app; - $this->appManager = $appManager; - } - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - - $this->server = $server; - $this->server->on('beforeMethod', array($this, 'checkAppEnabled'), 30); - } - - /** - * This method is called before any HTTP after auth and checks if the user has access to the app - * - * @throws \Sabre\DAV\Exception\Forbidden - * @return bool - */ - public function checkAppEnabled() { - if (!$this->appManager->isEnabledForUser($this->app)) { - throw new Forbidden(); - } - } -} diff --git a/apps/dav/lib/connector/sabre/auth.php b/apps/dav/lib/connector/sabre/auth.php deleted file mode 100644 index b8047e779f5..00000000000 --- a/apps/dav/lib/connector/sabre/auth.php +++ /dev/null @@ -1,229 +0,0 @@ - - * @author Bart Visscher - * @author Jakob Sack - * @author Lukas Reschke - * @author Markus Goetz - * @author Michael Gapczynski - * @author Morris Jobke - * @author Roeland Jago Douma - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Connector\Sabre; - -use Exception; -use OC\AppFramework\Http\Request; -use OCP\IRequest; -use OCP\ISession; -use OC\User\Session; -use Sabre\DAV\Auth\Backend\AbstractBasic; -use Sabre\DAV\Exception\NotAuthenticated; -use Sabre\DAV\Exception\ServiceUnavailable; -use Sabre\HTTP\RequestInterface; -use Sabre\HTTP\ResponseInterface; - -class Auth extends AbstractBasic { - const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND'; - - /** @var ISession */ - private $session; - /** @var Session */ - private $userSession; - /** @var IRequest */ - private $request; - /** @var string */ - private $currentUser; - - /** - * @param ISession $session - * @param Session $userSession - * @param IRequest $request - * @param string $principalPrefix - */ - public function __construct(ISession $session, - Session $userSession, - IRequest $request, - $principalPrefix = 'principals/users/') { - $this->session = $session; - $this->userSession = $userSession; - $this->request = $request; - $this->principalPrefix = $principalPrefix; - } - - /** - * Whether the user has initially authenticated via DAV - * - * This is required for WebDAV clients that resent the cookies even when the - * account was changed. - * - * @see https://github.com/owncloud/core/issues/13245 - * - * @param string $username - * @return bool - */ - public function isDavAuthenticated($username) { - return !is_null($this->session->get(self::DAV_AUTHENTICATED)) && - $this->session->get(self::DAV_AUTHENTICATED) === $username; - } - - /** - * Validates a username and password - * - * This method should return true or false depending on if login - * succeeded. - * - * @param string $username - * @param string $password - * @return bool - */ - protected function validateUserPass($username, $password) { - if ($this->userSession->isLoggedIn() && - $this->isDavAuthenticated($this->userSession->getUser()->getUID()) - ) { - \OC_Util::setupFS($this->userSession->getUser()->getUID()); - $this->session->close(); - return true; - } else { - \OC_Util::setUpFS(); //login hooks may need early access to the filesystem - if($this->userSession->login($username, $password)) { - $this->userSession->createSessionToken($this->request, $username, $password); - \OC_Util::setUpFS($this->userSession->getUser()->getUID()); - $this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID()); - $this->session->close(); - return true; - } else { - $this->session->close(); - return false; - } - } - } - - /** - * @param RequestInterface $request - * @param ResponseInterface $response - * @return array - * @throws NotAuthenticated - * @throws ServiceUnavailable - */ - function check(RequestInterface $request, ResponseInterface $response) { - try { - $result = $this->auth($request, $response); - return $result; - } catch (NotAuthenticated $e) { - throw $e; - } catch (Exception $e) { - $class = get_class($e); - $msg = $e->getMessage(); - throw new ServiceUnavailable("$class: $msg"); - } - } - - /** - * Checks whether a CSRF check is required on the request - * - * @return bool - */ - private function requiresCSRFCheck() { - // GET requires no check at all - if($this->request->getMethod() === 'GET') { - return false; - } - - // Official ownCloud clients require no checks - if($this->request->isUserAgent([ - Request::USER_AGENT_OWNCLOUD_DESKTOP, - Request::USER_AGENT_OWNCLOUD_ANDROID, - Request::USER_AGENT_OWNCLOUD_IOS, - ])) { - return false; - } - - // If not logged-in no check is required - if(!$this->userSession->isLoggedIn()) { - return false; - } - - // POST always requires a check - if($this->request->getMethod() === 'POST') { - return true; - } - - // If logged-in AND DAV authenticated no check is required - if($this->userSession->isLoggedIn() && - $this->isDavAuthenticated($this->userSession->getUser()->getUID())) { - return false; - } - - return true; - } - - /** - * @param RequestInterface $request - * @param ResponseInterface $response - * @return array - * @throws NotAuthenticated - */ - private function auth(RequestInterface $request, ResponseInterface $response) { - $forcedLogout = false; - if(!$this->request->passesCSRFCheck() && - $this->requiresCSRFCheck()) { - // In case of a fail with POST we need to recheck the credentials - if($this->request->getMethod() === 'POST') { - $forcedLogout = true; - } else { - $response->setStatus(401); - throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.'); - } - } - - if($forcedLogout) { - $this->userSession->logout(); - } else { - if (\OC_User::handleApacheAuth() || - //Fix for broken webdav clients - ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) || - //Well behaved clients that only send the cookie are allowed - ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null) - ) { - $user = $this->userSession->getUser()->getUID(); - \OC_Util::setupFS($user); - $this->currentUser = $user; - $this->session->close(); - return [true, $this->principalPrefix . $user]; - } - } - - if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With')))) { - // do not re-authenticate over ajax, use dummy auth name to prevent browser popup - $response->addHeader('WWW-Authenticate','DummyBasic realm="' . $this->realm . '"'); - $response->setStatus(401); - throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); - } - - $data = parent::check($request, $response); - if($data[0] === true) { - $startPos = strrpos($data[1], '/') + 1; - $user = $this->userSession->getUser()->getUID(); - $data[1] = substr_replace($data[1], $user, $startPos); - } - return $data; - } -} diff --git a/apps/dav/lib/connector/sabre/blocklegacyclientplugin.php b/apps/dav/lib/connector/sabre/blocklegacyclientplugin.php deleted file mode 100644 index 70d19cb7f2a..00000000000 --- a/apps/dav/lib/connector/sabre/blocklegacyclientplugin.php +++ /dev/null @@ -1,80 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCP\IConfig; -use Sabre\HTTP\RequestInterface; -use Sabre\DAV\ServerPlugin; -use Sabre\DAV\Exception; - -/** - * Class BlockLegacyClientPlugin is used to detect old legacy sync clients and - * returns a 403 status to those clients - * - * @package OCA\DAV\Connector\Sabre - */ -class BlockLegacyClientPlugin extends ServerPlugin { - /** @var \Sabre\DAV\Server */ - protected $server; - /** @var IConfig */ - protected $config; - - /** - * @param IConfig $config - */ - public function __construct(IConfig $config) { - $this->config = $config; - } - - /** - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - $this->server = $server; - $this->server->on('beforeMethod', [$this, 'beforeHandler'], 200); - } - - /** - * Detects all unsupported clients and throws a \Sabre\DAV\Exception\Forbidden - * exception which will result in a 403 to them. - * @param RequestInterface $request - * @throws \Sabre\DAV\Exception\Forbidden If the client version is not supported - */ - public function beforeHandler(RequestInterface $request) { - $userAgent = $request->getHeader('User-Agent'); - if($userAgent === null) { - return; - } - - $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '1.7.0'); - - // Match on the mirall version which is in scheme "Mozilla/5.0 (%1) mirall/%2" or - // "mirall/%1" for older releases - preg_match("/(?:mirall\\/)([\d.]+)/i", $userAgent, $versionMatches); - if(isset($versionMatches[1]) && - version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) { - throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.'); - } - } -} diff --git a/apps/dav/lib/connector/sabre/checksumlist.php b/apps/dav/lib/connector/sabre/checksumlist.php deleted file mode 100644 index f137222acca..00000000000 --- a/apps/dav/lib/connector/sabre/checksumlist.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Connector\Sabre; - -use Sabre\Xml\XmlSerializable; -use Sabre\Xml\Element; -use Sabre\Xml\Writer; - -/** - * Checksumlist property - * - * This property contains multiple "checksum" elements, each containing a - * checksum name. - */ -class ChecksumList implements XmlSerializable { - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - /** @var string[] of TYPE:CHECKSUM */ - private $checksums; - - /** - * @param string $checksum - */ - public function __construct($checksum) { - $this->checksums = explode(',', $checksum); - } - - /** - * The xmlSerialize metod is called during xml writing. - * - * Use the $writer argument to write its own xml serialization. - * - * An important note: do _not_ create a parent element. Any element - * implementing XmlSerializble should only ever write what's considered - * its 'inner xml'. - * - * The parent of the current element is responsible for writing a - * containing element. - * - * This allows serializers to be re-used for different element names. - * - * If you are opening new elements, you must also close them again. - * - * @param Writer $writer - * @return void - */ - function xmlSerialize(Writer $writer) { - - foreach ($this->checksums as $checksum) { - $writer->writeElement('{' . self::NS_OWNCLOUD . '}checksum', $checksum); - } - } -} diff --git a/apps/dav/lib/connector/sabre/commentpropertiesplugin.php b/apps/dav/lib/connector/sabre/commentpropertiesplugin.php deleted file mode 100644 index a8d5f771122..00000000000 --- a/apps/dav/lib/connector/sabre/commentpropertiesplugin.php +++ /dev/null @@ -1,130 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCP\Comments\ICommentsManager; -use OCP\IUserSession; -use Sabre\DAV\PropFind; -use Sabre\DAV\ServerPlugin; - -class CommentPropertiesPlugin extends ServerPlugin { - - const PROPERTY_NAME_HREF = '{http://owncloud.org/ns}comments-href'; - const PROPERTY_NAME_COUNT = '{http://owncloud.org/ns}comments-count'; - const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread'; - - /** @var \Sabre\DAV\Server */ - protected $server; - - /** @var ICommentsManager */ - private $commentsManager; - - /** @var IUserSession */ - private $userSession; - - public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) { - $this->commentsManager = $commentsManager; - $this->userSession = $userSession; - } - - /** - * This initializes the plugin. - * - * This function is called by Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - function initialize(\Sabre\DAV\Server $server) { - $this->server = $server; - $this->server->on('propFind', array($this, 'handleGetProperties')); - } - - /** - * Adds tags and favorites properties to the response, - * if requested. - * - * @param PropFind $propFind - * @param \Sabre\DAV\INode $node - * @return void - */ - public function handleGetProperties( - PropFind $propFind, - \Sabre\DAV\INode $node - ) { - if (!($node instanceof File) && !($node instanceof Directory)) { - return; - } - - $propFind->handle(self::PROPERTY_NAME_COUNT, function() use ($node) { - return $this->commentsManager->getNumberOfCommentsForObject('files', strval($node->getId())); - }); - - $propFind->handle(self::PROPERTY_NAME_HREF, function() use ($node) { - return $this->getCommentsLink($node); - }); - - $propFind->handle(self::PROPERTY_NAME_UNREAD, function() use ($node) { - return $this->getUnreadCount($node); - }); - } - - /** - * returns a reference to the comments node - * - * @param Node $node - * @return mixed|string - */ - public function getCommentsLink(Node $node) { - $href = $this->server->getBaseUri(); - $entryPoint = strrpos($href, '/webdav/'); - if($entryPoint === false) { - // in case we end up somewhere else, unexpectedly. - return null; - } - $href = substr_replace($href, '/dav/', $entryPoint); - $href .= 'comments/files/' . rawurldecode($node->getId()); - return $href; - } - - /** - * returns the number of unread comments for the currently logged in user - * on the given file or directory node - * - * @param Node $node - * @return Int|null - */ - public function getUnreadCount(Node $node) { - $user = $this->userSession->getUser(); - if(is_null($user)) { - return null; - } - - $lastRead = $this->commentsManager->getReadMark('files', strval($node->getId()), $user); - - return $this->commentsManager->getNumberOfCommentsForObject('files', strval($node->getId()), $lastRead); - } - -} diff --git a/apps/dav/lib/connector/sabre/copyetagheaderplugin.php b/apps/dav/lib/connector/sabre/copyetagheaderplugin.php deleted file mode 100644 index 49b6a7b2de7..00000000000 --- a/apps/dav/lib/connector/sabre/copyetagheaderplugin.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use \Sabre\HTTP\RequestInterface; -use \Sabre\HTTP\ResponseInterface; - -/** - * Copies the "Etag" header to "OC-Etag" after any request. - * This is a workaround for setups that automatically strip - * or mangle Etag headers. - */ -class CopyEtagHeaderPlugin extends \Sabre\DAV\ServerPlugin { - /** - * This initializes the plugin. - * - * @param \Sabre\DAV\Server $server Sabre server - * - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - $server->on('afterMethod', array($this, 'afterMethod')); - } - - /** - * After method, copy the "Etag" header to "OC-Etag" header. - * - * @param RequestInterface $request request - * @param ResponseInterface $response response - */ - public function afterMethod(RequestInterface $request, ResponseInterface $response) { - $eTag = $response->getHeader('Etag'); - if (!empty($eTag)) { - $response->setHeader('OC-ETag', $eTag); - } - } -} diff --git a/apps/dav/lib/connector/sabre/custompropertiesbackend.php b/apps/dav/lib/connector/sabre/custompropertiesbackend.php deleted file mode 100644 index 5946c9910d4..00000000000 --- a/apps/dav/lib/connector/sabre/custompropertiesbackend.php +++ /dev/null @@ -1,355 +0,0 @@ - - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCP\IDBConnection; -use OCP\IUser; -use Sabre\DAV\PropertyStorage\Backend\BackendInterface; -use Sabre\DAV\PropFind; -use Sabre\DAV\PropPatch; -use Sabre\DAV\Tree; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Exception\ServiceUnavailable; - -class CustomPropertiesBackend implements BackendInterface { - - /** - * Ignored properties - * - * @var array - */ - private $ignoredProperties = array( - '{DAV:}getcontentlength', - '{DAV:}getcontenttype', - '{DAV:}getetag', - '{DAV:}quota-used-bytes', - '{DAV:}quota-available-bytes', - '{DAV:}quota-available-bytes', - '{http://owncloud.org/ns}permissions', - '{http://owncloud.org/ns}downloadURL', - '{http://owncloud.org/ns}dDC', - '{http://owncloud.org/ns}size', - ); - - /** - * @var Tree - */ - private $tree; - - /** - * @var IDBConnection - */ - private $connection; - - /** - * @var IUser - */ - private $user; - - /** - * Properties cache - * - * @var array - */ - private $cache = []; - - /** - * @param Tree $tree node tree - * @param IDBConnection $connection database connection - * @param IUser $user owner of the tree and properties - */ - public function __construct( - Tree $tree, - IDBConnection $connection, - IUser $user) { - $this->tree = $tree; - $this->connection = $connection; - $this->user = $user->getUID(); - } - - /** - * Fetches properties for a path. - * - * @param string $path - * @param PropFind $propFind - * @return void - */ - public function propFind($path, PropFind $propFind) { - try { - $node = $this->tree->getNodeForPath($path); - if (!($node instanceof Node)) { - return; - } - } catch (ServiceUnavailable $e) { - // might happen for unavailable mount points, skip - return; - } catch (NotFound $e) { - // in some rare (buggy) cases the node might not be found, - // we catch the exception to prevent breaking the whole list with a 404 - // (soft fail) - \OC::$server->getLogger()->warning( - 'Could not get node for path: \"' . $path . '\" : ' . $e->getMessage(), - array('app' => 'files') - ); - return; - } - - $requestedProps = $propFind->get404Properties(); - - // these might appear - $requestedProps = array_diff( - $requestedProps, - $this->ignoredProperties - ); - - if (empty($requestedProps)) { - return; - } - - if ($node instanceof Directory - && $propFind->getDepth() !== 0 - ) { - // note: pre-fetching only supported for depth <= 1 - $this->loadChildrenProperties($node, $requestedProps); - } - - $props = $this->getProperties($node, $requestedProps); - foreach ($props as $propName => $propValue) { - $propFind->set($propName, $propValue); - } - } - - /** - * Updates properties for a path - * - * @param string $path - * @param PropPatch $propPatch - * - * @return void - */ - public function propPatch($path, PropPatch $propPatch) { - $node = $this->tree->getNodeForPath($path); - if (!($node instanceof Node)) { - return; - } - - $propPatch->handleRemaining(function($changedProps) use ($node) { - return $this->updateProperties($node, $changedProps); - }); - } - - /** - * This method is called after a node is deleted. - * - * @param string $path path of node for which to delete properties - */ - public function delete($path) { - $statement = $this->connection->prepare( - 'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?' - ); - $statement->execute(array($this->user, '/' . $path)); - $statement->closeCursor(); - - unset($this->cache[$path]); - } - - /** - * This method is called after a successful MOVE - * - * @param string $source - * @param string $destination - * - * @return void - */ - public function move($source, $destination) { - $statement = $this->connection->prepare( - 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' . - ' WHERE `userid` = ? AND `propertypath` = ?' - ); - $statement->execute(array('/' . $destination, $this->user, '/' . $source)); - $statement->closeCursor(); - } - - /** - * Returns a list of properties for this nodes.; - * @param Node $node - * @param array $requestedProperties requested properties or empty array for "all" - * @return array - * @note 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 - */ - private function getProperties(Node $node, array $requestedProperties) { - $path = $node->getPath(); - if (isset($this->cache[$path])) { - return $this->cache[$path]; - } - - // TODO: chunking if more than 1000 properties - $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'; - - $whereValues = array($this->user, $path); - $whereTypes = array(null, null); - - if (!empty($requestedProperties)) { - // request only a subset - $sql .= ' AND `propertyname` in (?)'; - $whereValues[] = $requestedProperties; - $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY; - } - - $result = $this->connection->executeQuery( - $sql, - $whereValues, - $whereTypes - ); - - $props = []; - while ($row = $result->fetch()) { - $props[$row['propertyname']] = $row['propertyvalue']; - } - - $result->closeCursor(); - - $this->cache[$path] = $props; - return $props; - } - - /** - * Update properties - * - * @param Node $node node for which to update properties - * @param array $properties array of properties to update - * - * @return bool - */ - private function updateProperties($node, $properties) { - $path = $node->getPath(); - - $deleteStatement = 'DELETE FROM `*PREFIX*properties`' . - ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; - - $insertStatement = 'INSERT INTO `*PREFIX*properties`' . - ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)'; - - $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' . - ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; - - // TODO: use "insert or update" strategy ? - $existing = $this->getProperties($node, array()); - $this->connection->beginTransaction(); - foreach ($properties as $propertyName => $propertyValue) { - // If it was null, we need to delete the property - if (is_null($propertyValue)) { - if (array_key_exists($propertyName, $existing)) { - $this->connection->executeUpdate($deleteStatement, - array( - $this->user, - $path, - $propertyName - ) - ); - } - } else { - if (!array_key_exists($propertyName, $existing)) { - $this->connection->executeUpdate($insertStatement, - array( - $this->user, - $path, - $propertyName, - $propertyValue - ) - ); - } else { - $this->connection->executeUpdate($updateStatement, - array( - $propertyValue, - $this->user, - $path, - $propertyName - ) - ); - } - } - } - - $this->connection->commit(); - unset($this->cache[$path]); - - return true; - } - - /** - * Bulk load properties for directory children - * - * @param Directory $node - * @param array $requestedProperties requested properties - * - * @return void - */ - private function loadChildrenProperties(Directory $node, $requestedProperties) { - $path = $node->getPath(); - if (isset($this->cache[$path])) { - // we already loaded them at some point - return; - } - - $childNodes = $node->getChildren(); - // pre-fill cache - foreach ($childNodes as $childNode) { - $this->cache[$childNode->getPath()] = []; - } - - $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` LIKE ?'; - $sql .= ' AND `propertyname` in (?) ORDER BY `propertypath`, `propertyname`'; - - $result = $this->connection->executeQuery( - $sql, - array($this->user, rtrim($path, '/') . '/%', $requestedProperties), - array(null, null, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY) - ); - - $oldPath = null; - $props = []; - while ($row = $result->fetch()) { - $path = $row['propertypath']; - if ($oldPath !== $path) { - // save previously gathered props - $this->cache[$oldPath] = $props; - $oldPath = $path; - // prepare props for next path - $props = []; - } - $props[$row['propertyname']] = $row['propertyvalue']; - } - if (!is_null($oldPath)) { - // save props from last run - $this->cache[$oldPath] = $props; - } - - $result->closeCursor(); - } - -} diff --git a/apps/dav/lib/connector/sabre/davaclplugin.php b/apps/dav/lib/connector/sabre/davaclplugin.php deleted file mode 100644 index f5699b469c3..00000000000 --- a/apps/dav/lib/connector/sabre/davaclplugin.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\IFile; -use Sabre\DAV\INode; -use \Sabre\DAV\PropFind; -use \Sabre\DAV\PropPatch; -use Sabre\DAVACL\Exception\NeedPrivileges; -use \Sabre\HTTP\RequestInterface; -use \Sabre\HTTP\ResponseInterface; -use Sabre\HTTP\URLUtil; - -/** - * Class DavAclPlugin is a wrapper around \Sabre\DAVACL\Plugin that returns 404 - * responses in case the resource to a response has been forbidden instead of - * a 403. This is used to prevent enumeration of valid resources. - * - * @see https://github.com/owncloud/core/issues/22578 - * @package OCA\DAV\Connector\Sabre - */ -class DavAclPlugin extends \Sabre\DAVACL\Plugin { - public function __construct() { - $this->hideNodesFromListings = true; - } - - function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) { - $access = parent::checkPrivileges($uri, $privileges, $recursion, false); - if($access === false && $throwExceptions) { - /** @var INode $node */ - $node = $this->server->tree->getNodeForPath($uri); - - switch(get_class($node)) { - case 'OCA\DAV\CardDAV\AddressBook': - $type = 'Addressbook'; - break; - default: - $type = 'Node'; - break; - } - throw new NotFound( - sprintf( - "%s with name '%s' could not be found", - $type, - $node->getName() - ) - ); - } - - return $access; - } -} diff --git a/apps/dav/lib/connector/sabre/directory.php b/apps/dav/lib/connector/sabre/directory.php deleted file mode 100644 index daa5f29ce79..00000000000 --- a/apps/dav/lib/connector/sabre/directory.php +++ /dev/null @@ -1,310 +0,0 @@ - - * @author Bart Visscher - * @author Björn Schießle - * @author Jakob Sack - * @author Joas Schilling - * @author Morris Jobke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\Exception\Forbidden; -use OCA\DAV\Connector\Sabre\Exception\InvalidPath; -use OCA\DAV\Connector\Sabre\Exception\FileLocked; -use OCP\Files\ForbiddenException; -use OCP\Lock\ILockingProvider; -use OCP\Lock\LockedException; -use Sabre\DAV\Exception\Locked; - -class Directory extends \OCA\DAV\Connector\Sabre\Node - implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota { - - /** - * Cached directory content - * - * @var \OCP\Files\FileInfo[] - */ - private $dirContent; - - /** - * Cached quota info - * - * @var array - */ - private $quotaInfo; - - /** - * @var ObjectTree|null - */ - private $tree; - - /** - * Sets up the node, expects a full path name - * - * @param \OC\Files\View $view - * @param \OCP\Files\FileInfo $info - * @param ObjectTree|null $tree - * @param \OCP\Share\IManager $shareManager - */ - public function __construct($view, $info, $tree = null, $shareManager = null) { - parent::__construct($view, $info, $shareManager); - $this->tree = $tree; - } - - /** - * 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 - * @throws Exception\EntityTooLarge - * @throws Exception\UnsupportedMediaType - * @throws FileLocked - * @throws InvalidPath - * @throws \Sabre\DAV\Exception - * @throws \Sabre\DAV\Exception\BadRequest - * @throws \Sabre\DAV\Exception\Forbidden - * @throws \Sabre\DAV\Exception\ServiceUnavailable - */ - public function createFile($name, $data = null) { - - try { - // for chunked upload also updating a existing file is a "createFile" - // because we create all the chunks before re-assemble them to the existing file. - if (isset($_SERVER['HTTP_OC_CHUNKED'])) { - - // exit if we can't create a new file and we don't updatable existing file - $info = \OC_FileChunking::decodeName($name); - if (!$this->fileView->isCreatable($this->path) && - !$this->fileView->isUpdatable($this->path . '/' . $info['name']) - ) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - - } else { - // For non-chunked upload it is enough to check if we can create a new file - if (!$this->fileView->isCreatable($this->path)) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - } - - $this->fileView->verifyPath($this->path, $name); - - $path = $this->fileView->getAbsolutePath($this->path) . '/' . $name; - // using a dummy FileInfo is acceptable here since it will be refreshed after the put is complete - $info = new \OC\Files\FileInfo($path, null, null, array(), null); - $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info); - $node->acquireLock(ILockingProvider::LOCK_SHARED); - return $node->put($data); - } catch (\OCP\Files\StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); - } catch (\OCP\Files\InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); - } catch (ForbiddenException $ex) { - throw new Forbidden($ex->getMessage(), $ex->getRetry()); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Creates a new subdirectory - * - * @param string $name - * @throws FileLocked - * @throws InvalidPath - * @throws \Sabre\DAV\Exception\Forbidden - * @throws \Sabre\DAV\Exception\ServiceUnavailable - */ - public function createDirectory($name) { - try { - if (!$this->info->isCreatable()) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - - $this->fileView->verifyPath($this->path, $name); - $newPath = $this->path . '/' . $name; - if (!$this->fileView->mkdir($newPath)) { - throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath); - } - } catch (\OCP\Files\StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); - } catch (\OCP\Files\InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); - } catch (ForbiddenException $ex) { - throw new Forbidden($ex->getMessage(), $ex->getRetry()); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Returns a specific child node, referenced by its name - * - * @param string $name - * @param \OCP\Files\FileInfo $info - * @return \Sabre\DAV\INode - * @throws InvalidPath - * @throws \Sabre\DAV\Exception\NotFound - * @throws \Sabre\DAV\Exception\ServiceUnavailable - */ - public function getChild($name, $info = null) { - $path = $this->path . '/' . $name; - if (is_null($info)) { - try { - $this->fileView->verifyPath($this->path, $name); - $info = $this->fileView->getFileInfo($path); - } catch (\OCP\Files\StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); - } catch (\OCP\Files\InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); - } - } - - if (!$info) { - throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located'); - } - - if ($info['mimetype'] == 'httpd/unix-directory') { - $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager); - } else { - $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager); - } - if ($this->tree) { - $this->tree->cacheNode($node); - } - return $node; - } - - /** - * Returns an array with all the child nodes - * - * @return \Sabre\DAV\INode[] - */ - public function getChildren() { - if (!is_null($this->dirContent)) { - return $this->dirContent; - } - try { - $folderContent = $this->fileView->getDirectoryContent($this->path); - } catch (LockedException $e) { - throw new Locked(); - } - - $nodes = array(); - foreach ($folderContent as $info) { - $node = $this->getChild($info->getName(), $info); - $nodes[] = $node; - } - $this->dirContent = $nodes; - return $this->dirContent; - } - - /** - * Checks if a child exists. - * - * @param string $name - * @return bool - */ - public function childExists($name) { - // note: here we do NOT resolve the chunk file name to the real file name - // to make sure we return false when checking for file existence with a chunk - // file name. - // This is to make sure that "createFile" is still triggered - // (required old code) instead of "updateFile". - // - // TODO: resolve chunk file name here and implement "updateFile" - $path = $this->path . '/' . $name; - return $this->fileView->file_exists($path); - - } - - /** - * Deletes all files in this directory, and then itself - * - * @return void - * @throws FileLocked - * @throws \Sabre\DAV\Exception\Forbidden - */ - public function delete() { - - if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - - try { - if (!$this->fileView->rmdir($this->path)) { - // assume it wasn't possible to remove due to permission issue - throw new \Sabre\DAV\Exception\Forbidden(); - } - } catch (ForbiddenException $ex) { - throw new Forbidden($ex->getMessage(), $ex->getRetry()); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Returns available diskspace information - * - * @return array - */ - public function getQuotaInfo() { - if ($this->quotaInfo) { - return $this->quotaInfo; - } - try { - $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $this->info); - if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) { - $free = \OCP\Files\FileInfo::SPACE_UNLIMITED; - } else { - $free = $storageInfo['free']; - } - $this->quotaInfo = array( - $storageInfo['used'], - $free - ); - return $this->quotaInfo; - } catch (\OCP\Files\StorageNotAvailableException $e) { - return array(0, 0); - } - } - -} diff --git a/apps/dav/lib/connector/sabre/dummygetresponseplugin.php b/apps/dav/lib/connector/sabre/dummygetresponseplugin.php deleted file mode 100644 index b10d5aaab36..00000000000 --- a/apps/dav/lib/connector/sabre/dummygetresponseplugin.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; -use Sabre\HTTP\ResponseInterface; -use Sabre\HTTP\RequestInterface; - -/** - * Class DummyGetResponsePlugin is a plugin used to not show a "Not implemented" - * error to clients that rely on verifying the functionality of the ownCloud - * WebDAV backend using a simple GET to /. - * - * This is considered a legacy behaviour and implementers should consider sending - * a PROPFIND request instead to verify whether the WebDAV component is working - * properly. - * - * FIXME: Remove once clients are all compliant. - * - * @package OCA\DAV\Connector\Sabre - */ -class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin { - /** @var \Sabre\DAV\Server */ - protected $server; - - /** - * @param \Sabre\DAV\Server $server - * @return void - */ - function initialize(\Sabre\DAV\Server $server) { - $this->server = $server; - $this->server->on('method:GET', [$this, 'httpGet'], 200); - } - - /** - * @param RequestInterface $request - * @param ResponseInterface $response - * @return false - */ - function httpGet(RequestInterface $request, ResponseInterface $response) { - $string = 'This is the WebDAV interface. It can only be accessed by ' . - 'WebDAV clients such as the ownCloud desktop sync client.'; - $stream = fopen('php://memory','r+'); - fwrite($stream, $string); - rewind($stream); - - $response->setStatus(200); - $response->setBody($stream); - - return false; - } -} diff --git a/apps/dav/lib/connector/sabre/exception/entitytoolarge.php b/apps/dav/lib/connector/sabre/exception/entitytoolarge.php deleted file mode 100644 index d16e93bb637..00000000000 --- a/apps/dav/lib/connector/sabre/exception/entitytoolarge.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Connector\Sabre\Exception; - -/** - * Entity Too Large - * - * This exception is thrown whenever a user tries to upload a file which exceeds hard limitations - * - */ -class EntityTooLarge extends \Sabre\DAV\Exception { - - /** - * Returns the HTTP status code for this exception - * - * @return int - */ - public function getHTTPCode() { - - return 413; - - } - -} diff --git a/apps/dav/lib/connector/sabre/exception/filelocked.php b/apps/dav/lib/connector/sabre/exception/filelocked.php deleted file mode 100644 index 03ced0e81e2..00000000000 --- a/apps/dav/lib/connector/sabre/exception/filelocked.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @author Morris Jobke - * @author Owen Winkler - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre\Exception; - -use Exception; - -class FileLocked extends \Sabre\DAV\Exception { - - public function __construct($message = "", $code = 0, Exception $previous = null) { - if($previous instanceof \OCP\Files\LockNotAcquiredException) { - $message = sprintf('Target file %s is locked by another process.', $previous->path); - } - parent::__construct($message, $code, $previous); - } - - /** - * Returns the HTTP status code for this exception - * - * @return int - */ - public function getHTTPCode() { - - return 423; - } -} diff --git a/apps/dav/lib/connector/sabre/exception/forbidden.php b/apps/dav/lib/connector/sabre/exception/forbidden.php deleted file mode 100644 index f2467e6b298..00000000000 --- a/apps/dav/lib/connector/sabre/exception/forbidden.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre\Exception; - -class Forbidden extends \Sabre\DAV\Exception\Forbidden { - - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - /** - * @var bool - */ - private $retry; - - /** - * @param string $message - * @param bool $retry - * @param \Exception $previous - */ - public function __construct($message, $retry = false, \Exception $previous = null) { - parent::__construct($message, 0, $previous); - $this->retry = $retry; - } - - /** - * This method allows the exception to include additional information - * into the WebDAV error response - * - * @param \Sabre\DAV\Server $server - * @param \DOMElement $errorNode - * @return void - */ - public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) { - - // set ownCloud namespace - $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD); - - // adding the retry node - $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true)); - $errorNode->appendChild($error); - - // adding the message node - $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage()); - $errorNode->appendChild($error); - } -} diff --git a/apps/dav/lib/connector/sabre/exception/invalidpath.php b/apps/dav/lib/connector/sabre/exception/invalidpath.php deleted file mode 100644 index 7951a0a89b7..00000000000 --- a/apps/dav/lib/connector/sabre/exception/invalidpath.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre\Exception; - -use Sabre\DAV\Exception; - -class InvalidPath extends Exception { - - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - /** - * @var bool - */ - private $retry; - - /** - * @param string $message - * @param bool $retry - */ - public function __construct($message, $retry = false) { - parent::__construct($message); - $this->retry = $retry; - } - - /** - * Returns the HTTP status code for this exception - * - * @return int - */ - public function getHTTPCode() { - - return 400; - - } - - /** - * This method allows the exception to include additional information - * into the WebDAV error response - * - * @param \Sabre\DAV\Server $server - * @param \DOMElement $errorNode - * @return void - */ - public function serialize(\Sabre\DAV\Server $server,\DOMElement $errorNode) { - - // set ownCloud namespace - $errorNode->setAttribute('xmlns:o', self::NS_OWNCLOUD); - - // adding the retry node - $error = $errorNode->ownerDocument->createElementNS('o:','o:retry', var_export($this->retry, true)); - $errorNode->appendChild($error); - - // adding the message node - $error = $errorNode->ownerDocument->createElementNS('o:','o:reason', $this->getMessage()); - $errorNode->appendChild($error); - } - -} diff --git a/apps/dav/lib/connector/sabre/exception/unsupportedmediatype.php b/apps/dav/lib/connector/sabre/exception/unsupportedmediatype.php deleted file mode 100644 index 99e3c222c75..00000000000 --- a/apps/dav/lib/connector/sabre/exception/unsupportedmediatype.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Connector\Sabre\Exception; - -/** - * Unsupported Media Type - * - * This exception is thrown whenever a user tries to upload a file which holds content which is not allowed - * - */ -class UnsupportedMediaType extends \Sabre\DAV\Exception { - - /** - * Returns the HTTP status code for this exception - * - * @return int - */ - public function getHTTPCode() { - - return 415; - - } - -} diff --git a/apps/dav/lib/connector/sabre/exceptionloggerplugin.php b/apps/dav/lib/connector/sabre/exceptionloggerplugin.php deleted file mode 100644 index 38bddef8769..00000000000 --- a/apps/dav/lib/connector/sabre/exceptionloggerplugin.php +++ /dev/null @@ -1,111 +0,0 @@ - - * @author Pierre Jochem - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCP\ILogger; -use Sabre\DAV\Exception; -use Sabre\HTTP\Response; - -class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { - protected $nonFatalExceptions = array( - 'Sabre\DAV\Exception\NotAuthenticated' => true, - // the sync client uses this to find out whether files exist, - // so it is not always an error, log it as debug - 'Sabre\DAV\Exception\NotFound' => true, - // this one mostly happens when the same file is uploaded at - // exactly the same time from two clients, only one client - // wins, the second one gets "Precondition failed" - 'Sabre\DAV\Exception\PreconditionFailed' => true, - // forbidden can be expected when trying to upload to - // read-only folders for example - 'Sabre\DAV\Exception\Forbidden' => true, - ); - - /** @var string */ - private $appName; - - /** @var ILogger */ - private $logger; - - /** - * @param string $loggerAppName app name to use when logging - * @param ILogger $logger - */ - public function __construct($loggerAppName, $logger) { - $this->appName = $loggerAppName; - $this->logger = $logger; - } - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - - $server->on('exception', array($this, 'logException'), 10); - } - - /** - * Log exception - * - */ - public function logException(\Exception $ex) { - $exceptionClass = get_class($ex); - $level = \OCP\Util::FATAL; - if (isset($this->nonFatalExceptions[$exceptionClass])) { - $level = \OCP\Util::DEBUG; - } - - $message = $ex->getMessage(); - if ($ex instanceof Exception) { - if (empty($message)) { - $response = new Response($ex->getHTTPCode()); - $message = $response->getStatusText(); - } - $message = "HTTP/1.1 {$ex->getHTTPCode()} $message"; - } - - $user = \OC_User::getUser(); - - $exception = [ - 'Message' => $message, - 'Exception' => $exceptionClass, - 'Code' => $ex->getCode(), - 'Trace' => $ex->getTraceAsString(), - 'File' => $ex->getFile(), - 'Line' => $ex->getLine(), - 'User' => $user, - ]; - $this->logger->log($level, 'Exception: ' . json_encode($exception), ['app' => $this->appName]); - } -} diff --git a/apps/dav/lib/connector/sabre/fakelockerplugin.php b/apps/dav/lib/connector/sabre/fakelockerplugin.php deleted file mode 100644 index 6db8740bc13..00000000000 --- a/apps/dav/lib/connector/sabre/fakelockerplugin.php +++ /dev/null @@ -1,156 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use Sabre\DAV\Locks\LockInfo; -use Sabre\DAV\ServerPlugin; -use Sabre\DAV\Xml\Property\LockDiscovery; -use Sabre\DAV\Xml\Property\SupportedLock; -use Sabre\HTTP\RequestInterface; -use Sabre\HTTP\ResponseInterface; -use Sabre\DAV\PropFind; -use Sabre\DAV\INode; - -/** - * Class FakeLockerPlugin is a plugin only used when connections come in from - * OS X via Finder. The fake locking plugin does emulate Class 2 WebDAV support - * (locking of files) which allows Finder to access the storage in write mode as - * well. - * - * No real locking is performed, instead the plugin just returns always positive - * responses. - * - * @see https://github.com/owncloud/core/issues/17732 - * @package OCA\DAV\Connector\Sabre - */ -class FakeLockerPlugin extends ServerPlugin { - /** @var \Sabre\DAV\Server */ - private $server; - - /** {@inheritDoc} */ - public function initialize(\Sabre\DAV\Server $server) { - $this->server = $server; - $this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1); - $this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1); - $server->on('propFind', [$this, 'propFind']); - $server->on('validateTokens', [$this, 'validateTokens']); - } - - /** - * Indicate that we support LOCK and UNLOCK - * - * @param string $path - * @return string[] - */ - public function getHTTPMethods($path) { - return [ - 'LOCK', - 'UNLOCK', - ]; - } - - /** - * Indicate that we support locking - * - * @return integer[] - */ - function getFeatures() { - return [2]; - } - - /** - * Return some dummy response for PROPFIND requests with regard to locking - * - * @param PropFind $propFind - * @param INode $node - * @return void - */ - function propFind(PropFind $propFind, INode $node) { - $propFind->handle('{DAV:}supportedlock', function() { - return new SupportedLock(true); - }); - $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) { - return new LockDiscovery([]); - }); - } - - /** - * Mark a locking token always as valid - * - * @param RequestInterface $request - * @param array $conditions - */ - public function validateTokens(RequestInterface $request, &$conditions) { - foreach($conditions as &$fileCondition) { - if(isset($fileCondition['tokens'])) { - foreach($fileCondition['tokens'] as &$token) { - if(isset($token['token'])) { - if(substr($token['token'], 0, 16) === 'opaquelocktoken:') { - $token['validToken'] = true; - } - } - } - } - } - } - - /** - * Fakes a successful LOCK - * - * @param RequestInterface $request - * @param ResponseInterface $response - * @return bool - */ - public function fakeLockProvider(RequestInterface $request, - ResponseInterface $response) { - - $lockInfo = new LockInfo(); - $lockInfo->token = md5($request->getPath()); - $lockInfo->uri = $request->getPath(); - $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY; - $lockInfo->timeout = 1800; - - $body = $this->server->xml->write('{DAV:}prop', [ - '{DAV:}lockdiscovery' => - new LockDiscovery([$lockInfo]) - ]); - - $response->setBody($body); - - return false; - } - - /** - * Fakes a successful LOCK - * - * @param RequestInterface $request - * @param ResponseInterface $response - * @return bool - */ - public function fakeUnlockProvider(RequestInterface $request, - ResponseInterface $response) { - $response->setStatus(204); - $response->setHeader('Content-Length', '0'); - return false; - } -} diff --git a/apps/dav/lib/connector/sabre/file.php b/apps/dav/lib/connector/sabre/file.php deleted file mode 100644 index 943e9150e74..00000000000 --- a/apps/dav/lib/connector/sabre/file.php +++ /dev/null @@ -1,567 +0,0 @@ - - * @author Björn Schießle - * @author Jakob Sack - * @author Joas Schilling - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Morris Jobke - * @author Owen Winkler - * @author Robin Appelman - * @author Roeland Jago Douma - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OC\Files\Filesystem; -use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge; -use OCA\DAV\Connector\Sabre\Exception\FileLocked; -use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; -use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; -use OCP\Encryption\Exceptions\GenericEncryptionException; -use OCP\Files\EntityTooLargeException; -use OCP\Files\ForbiddenException; -use OCP\Files\InvalidContentException; -use OCP\Files\InvalidPathException; -use OCP\Files\LockNotAcquiredException; -use OCP\Files\NotPermittedException; -use OCP\Files\StorageNotAvailableException; -use OCP\Lock\ILockingProvider; -use OCP\Lock\LockedException; -use Sabre\DAV\Exception; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotImplemented; -use Sabre\DAV\Exception\ServiceUnavailable; -use Sabre\DAV\IFile; - -class File extends Node implements IFile { - - /** - * Updates the data - * - * The data argument is a readable stream resource. - * - * After a successful put operation, you may choose to return an ETag. The - * etag must always be surrounded by double-quotes. These quotes must - * appear in the actual string you're returning. - * - * Clients may use the ETag from a PUT request to later on make sure that - * when they update the file, the contents haven't changed in the mean - * time. - * - * If you don't plan to store the file byte-by-byte, and you return a - * different object on a subsequent GET you are strongly recommended to not - * return an ETag, and just return null. - * - * @param resource $data - * - * @throws Forbidden - * @throws UnsupportedMediaType - * @throws BadRequest - * @throws Exception - * @throws EntityTooLarge - * @throws ServiceUnavailable - * @throws FileLocked - * @return string|null - */ - public function put($data) { - try { - $exists = $this->fileView->file_exists($this->path); - if ($this->info && $exists && !$this->info->isUpdateable()) { - throw new Forbidden(); - } - } catch (StorageNotAvailableException $e) { - throw new ServiceUnavailable("File is not updatable: " . $e->getMessage()); - } - - // verify path of the target - $this->verifyPath(); - - // chunked handling - if (isset($_SERVER['HTTP_OC_CHUNKED'])) { - try { - return $this->createFileChunked($data); - } catch (\Exception $e) { - $this->convertToSabreException($e); - } - } - - list($partStorage) = $this->fileView->resolvePath($this->path); - $needsPartFile = $this->needsPartFile($partStorage) && (strlen($this->path) > 1); - - if ($needsPartFile) { - // mark file as partial while uploading (ignored by the scanner) - $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part'; - } else { - // upload file directly as the final path - $partFilePath = $this->path; - } - - // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) - /** @var \OC\Files\Storage\Storage $partStorage */ - list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath); - /** @var \OC\Files\Storage\Storage $storage */ - list($storage, $internalPath) = $this->fileView->resolvePath($this->path); - try { - $target = $partStorage->fopen($internalPartPath, 'wb'); - if ($target === false) { - \OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::fopen() failed', \OCP\Util::ERROR); - // because we have no clue about the cause we can only throw back a 500/Internal Server Error - throw new Exception('Could not write file contents'); - } - list($count, $result) = \OC_Helper::streamCopy($data, $target); - fclose($target); - - if ($result === false) { - $expected = -1; - if (isset($_SERVER['CONTENT_LENGTH'])) { - $expected = $_SERVER['CONTENT_LENGTH']; - } - throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )'); - } - - // if content length is sent by client: - // double check if the file was fully received - // compare expected and actual size - if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { - $expected = $_SERVER['CONTENT_LENGTH']; - if ($count != $expected) { - throw new BadRequest('expected filesize ' . $expected . ' got ' . $count); - } - } - - } catch (\Exception $e) { - if ($needsPartFile) { - $partStorage->unlink($internalPartPath); - } - $this->convertToSabreException($e); - } - - try { - $view = \OC\Files\Filesystem::getView(); - if ($view) { - $run = $this->emitPreHooks($exists); - } else { - $run = true; - } - - try { - $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); - } catch (LockedException $e) { - if ($needsPartFile) { - $partStorage->unlink($internalPartPath); - } - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - - if ($needsPartFile) { - // rename to correct path - try { - if ($run) { - $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); - $fileExists = $storage->file_exists($internalPath); - } - if (!$run || $renameOkay === false || $fileExists === false) { - \OCP\Util::writeLog('webdav', 'renaming part file to final file failed', \OCP\Util::ERROR); - throw new Exception('Could not rename part file to final file'); - } - } catch (ForbiddenException $ex) { - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); - } catch (\Exception $e) { - $partStorage->unlink($internalPartPath); - $this->convertToSabreException($e); - } - } - - // since we skipped the view we need to scan and emit the hooks ourselves - $storage->getUpdater()->update($internalPath); - - try { - $this->changeLock(ILockingProvider::LOCK_SHARED); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - - if ($view) { - $this->emitPostHooks($exists); - } - - // allow sync clients to send the mtime along in a header - $request = \OC::$server->getRequest(); - if (isset($request->server['HTTP_X_OC_MTIME'])) { - if ($this->fileView->touch($this->path, $request->server['HTTP_X_OC_MTIME'])) { - header('X-OC-MTime: accepted'); - } - } - - $this->refreshInfo(); - - if (isset($request->server['HTTP_OC_CHECKSUM'])) { - $checksum = trim($request->server['HTTP_OC_CHECKSUM']); - $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); - $this->refreshInfo(); - } else if ($this->getChecksum() !== null && $this->getChecksum() !== '') { - $this->fileView->putFileInfo($this->path, ['checksum' => '']); - $this->refreshInfo(); - } - - } catch (StorageNotAvailableException $e) { - throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage()); - } - - return '"' . $this->info->getEtag() . '"'; - } - - private function getPartFileBasePath($path) { - $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true); - if ($partFileInStorage) { - return $path; - } else { - return md5($path); // will place it in the root of the view with a unique name - } - } - - /** - * @param string $path - */ - private function emitPreHooks($exists, $path = null) { - if (is_null($path)) { - $path = $this->path; - } - $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); - $run = true; - - if (!$exists) { - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array( - \OC\Files\Filesystem::signal_param_path => $hookPath, - \OC\Files\Filesystem::signal_param_run => &$run, - )); - } else { - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array( - \OC\Files\Filesystem::signal_param_path => $hookPath, - \OC\Files\Filesystem::signal_param_run => &$run, - )); - } - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array( - \OC\Files\Filesystem::signal_param_path => $hookPath, - \OC\Files\Filesystem::signal_param_run => &$run, - )); - return $run; - } - - /** - * @param string $path - */ - private function emitPostHooks($exists, $path = null) { - if (is_null($path)) { - $path = $this->path; - } - $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); - if (!$exists) { - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array( - \OC\Files\Filesystem::signal_param_path => $hookPath - )); - } else { - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array( - \OC\Files\Filesystem::signal_param_path => $hookPath - )); - } - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array( - \OC\Files\Filesystem::signal_param_path => $hookPath - )); - } - - /** - * Returns the data - * - * @return resource - * @throws Forbidden - * @throws ServiceUnavailable - */ - public function get() { - //throw exception if encryption is disabled but files are still encrypted - try { - $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb'); - if ($res === false) { - throw new ServiceUnavailable("Could not open file"); - } - return $res; - } catch (GenericEncryptionException $e) { - // returning 503 will allow retry of the operation at a later point in time - throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage()); - } catch (StorageNotAvailableException $e) { - throw new ServiceUnavailable("Failed to open file: " . $e->getMessage()); - } catch (ForbiddenException $ex) { - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Delete the current file - * - * @throws Forbidden - * @throws ServiceUnavailable - */ - public function delete() { - if (!$this->info->isDeletable()) { - throw new Forbidden(); - } - - try { - if (!$this->fileView->unlink($this->path)) { - // assume it wasn't possible to delete due to permissions - throw new Forbidden(); - } - } catch (StorageNotAvailableException $e) { - throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage()); - } catch (ForbiddenException $ex) { - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Returns the mime-type for a file - * - * If null is returned, we'll assume application/octet-stream - * - * @return string - */ - public function getContentType() { - $mimeType = $this->info->getMimetype(); - - // PROPFIND needs to return the correct mime type, for consistency with the web UI - if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') { - return $mimeType; - } - return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType); - } - - /** - * @return array|false - */ - public function getDirectDownload() { - if (\OCP\App::isEnabled('encryption')) { - return []; - } - /** @var \OCP\Files\Storage $storage */ - list($storage, $internalPath) = $this->fileView->resolvePath($this->path); - if (is_null($storage)) { - return []; - } - - return $storage->getDirectDownload($internalPath); - } - - /** - * @param resource $data - * @return null|string - * @throws Exception - * @throws BadRequest - * @throws NotImplemented - * @throws ServiceUnavailable - */ - private function createFileChunked($data) { - list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($this->path); - - $info = \OC_FileChunking::decodeName($name); - if (empty($info)) { - throw new NotImplemented('Invalid chunk name'); - } - - $chunk_handler = new \OC_FileChunking($info); - $bytesWritten = $chunk_handler->store($info['index'], $data); - - //detect aborted upload - if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { - if (isset($_SERVER['CONTENT_LENGTH'])) { - $expected = $_SERVER['CONTENT_LENGTH']; - if ($bytesWritten != $expected) { - $chunk_handler->remove($info['index']); - throw new BadRequest( - 'expected filesize ' . $expected . ' got ' . $bytesWritten); - } - } - } - - if ($chunk_handler->isComplete()) { - list($storage,) = $this->fileView->resolvePath($path); - $needsPartFile = $this->needsPartFile($storage); - $partFile = null; - - $targetPath = $path . '/' . $info['name']; - /** @var \OC\Files\Storage\Storage $targetStorage */ - list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); - - $exists = $this->fileView->file_exists($targetPath); - - try { - $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED); - - $this->emitPreHooks($exists, $targetPath); - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE); - /** @var \OC\Files\Storage\Storage $targetStorage */ - list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); - - if ($needsPartFile) { - // we first assembly the target file as a part file - $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; - /** @var \OC\Files\Storage\Storage $targetStorage */ - list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile); - - - $chunk_handler->file_assemble($partStorage, $partInternalPath); - - // here is the final atomic rename - $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath); - $fileExists = $targetStorage->file_exists($targetInternalPath); - if ($renameOkay === false || $fileExists === false) { - \OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::rename() failed', \OCP\Util::ERROR); - // only delete if an error occurred and the target file was already created - if ($fileExists) { - // set to null to avoid double-deletion when handling exception - // stray part file - $partFile = null; - $targetStorage->unlink($targetInternalPath); - } - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); - throw new Exception('Could not rename part file assembled from chunks'); - } - } else { - // assemble directly into the final file - $chunk_handler->file_assemble($targetStorage, $targetInternalPath); - } - - // allow sync clients to send the mtime along in a header - $request = \OC::$server->getRequest(); - if (isset($request->server['HTTP_X_OC_MTIME'])) { - if ($targetStorage->touch($targetInternalPath, $request->server['HTTP_X_OC_MTIME'])) { - header('X-OC-MTime: accepted'); - } - } - - // since we skipped the view we need to scan and emit the hooks ourselves - $targetStorage->getUpdater()->update($targetInternalPath); - - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); - - $this->emitPostHooks($exists, $targetPath); - - // FIXME: should call refreshInfo but can't because $this->path is not the of the final file - $info = $this->fileView->getFileInfo($targetPath); - - if (isset($request->server['HTTP_OC_CHECKSUM'])) { - $checksum = trim($request->server['HTTP_OC_CHECKSUM']); - $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]); - } else if ($info->getChecksum() !== null && $info->getChecksum() !== '') { - $this->fileView->putFileInfo($this->path, ['checksum' => '']); - } - - $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED); - - return $info->getEtag(); - } catch (\Exception $e) { - if ($partFile !== null) { - $targetStorage->unlink($targetInternalPath); - } - $this->convertToSabreException($e); - } - } - - return null; - } - - /** - * Returns whether a part file is needed for the given storage - * or whether the file can be assembled/uploaded directly on the - * target storage. - * - * @param \OCP\Files\Storage $storage - * @return bool true if the storage needs part file handling - */ - private function needsPartFile($storage) { - // TODO: in the future use ChunkHandler provided by storage - // and/or add method on Storage called "needsPartFile()" - return !$storage->instanceOfStorage('OCA\Files_Sharing\External\Storage') && - !$storage->instanceOfStorage('OC\Files\Storage\OwnCloud'); - } - - /** - * Convert the given exception to a SabreException instance - * - * @param \Exception $e - * - * @throws \Sabre\DAV\Exception - */ - private function convertToSabreException(\Exception $e) { - if ($e instanceof \Sabre\DAV\Exception) { - throw $e; - } - if ($e instanceof NotPermittedException) { - // a more general case - due to whatever reason the content could not be written - throw new Forbidden($e->getMessage(), 0, $e); - } - if ($e instanceof ForbiddenException) { - // the path for the file was forbidden - throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); - } - if ($e instanceof EntityTooLargeException) { - // the file is too big to be stored - throw new EntityTooLarge($e->getMessage(), 0, $e); - } - if ($e instanceof InvalidContentException) { - // the file content is not permitted - throw new UnsupportedMediaType($e->getMessage(), 0, $e); - } - if ($e instanceof InvalidPathException) { - // the path for the file was not valid - // TODO: find proper http status code for this case - throw new Forbidden($e->getMessage(), 0, $e); - } - if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) { - // the file is currently being written to by another process - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - if ($e instanceof GenericEncryptionException) { - // returning 503 will allow retry of the operation at a later point in time - throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e); - } - if ($e instanceof StorageNotAvailableException) { - throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e); - } - - throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); - } - - /** - * Get the checksum for this file - * - * @return string - */ - public function getChecksum() { - return $this->info->getChecksum(); - } -} diff --git a/apps/dav/lib/connector/sabre/filesplugin.php b/apps/dav/lib/connector/sabre/filesplugin.php deleted file mode 100644 index 8822deb1661..00000000000 --- a/apps/dav/lib/connector/sabre/filesplugin.php +++ /dev/null @@ -1,398 +0,0 @@ - - * @author Morris Jobke - * @author Robin Appelman - * @author Robin McCorkell - * @author Roeland Jago Douma - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OC\Files\View; -use OCA\DAV\Upload\FutureFile; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\IFile; -use \Sabre\DAV\PropFind; -use \Sabre\DAV\PropPatch; -use Sabre\DAV\ServerPlugin; -use Sabre\DAV\Tree; -use \Sabre\HTTP\RequestInterface; -use \Sabre\HTTP\ResponseInterface; -use OCP\Files\StorageNotAvailableException; -use OCP\IConfig; - -class FilesPlugin extends ServerPlugin { - - // namespace - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id'; - const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid'; - const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions'; - const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions'; - const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL'; - const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size'; - const GETETAG_PROPERTYNAME = '{DAV:}getetag'; - const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified'; - const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id'; - const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name'; - const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums'; - const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint'; - - /** - * Reference to main server object - * - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var Tree - */ - private $tree; - - /** - * Whether this is public webdav. - * If true, some returned information will be stripped off. - * - * @var bool - */ - private $isPublic; - - /** - * @var View - */ - private $fileView; - - /** - * @var bool - */ - private $downloadAttachment; - - /** - * @var IConfig - */ - private $config; - - /** - * @param Tree $tree - * @param View $view - * @param bool $isPublic - * @param bool $downloadAttachment - */ - public function __construct(Tree $tree, - View $view, - IConfig $config, - $isPublic = false, - $downloadAttachment = true) { - $this->tree = $tree; - $this->fileView = $view; - $this->config = $config; - $this->isPublic = $isPublic; - $this->downloadAttachment = $downloadAttachment; - } - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - - $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; - $server->protectedProperties[] = self::FILEID_PROPERTYNAME; - $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME; - $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME; - $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME; - $server->protectedProperties[] = self::SIZE_PROPERTYNAME; - $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME; - $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME; - $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME; - $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME; - $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME; - - // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH - $allowedProperties = ['{DAV:}getetag']; - $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties); - - $this->server = $server; - $this->server->on('propFind', array($this, 'handleGetProperties')); - $this->server->on('propPatch', array($this, 'handleUpdateProperties')); - $this->server->on('afterBind', array($this, 'sendFileIdHeader')); - $this->server->on('afterWriteContent', array($this, 'sendFileIdHeader')); - $this->server->on('afterMethod:GET', [$this,'httpGet']); - $this->server->on('afterMethod:GET', array($this, 'handleDownloadToken')); - $this->server->on('afterResponse', function($request, ResponseInterface $response) { - $body = $response->getBody(); - if (is_resource($body)) { - fclose($body); - } - }); - $this->server->on('beforeMove', [$this, 'checkMove']); - } - - /** - * Plugin that checks if a move can actually be performed. - * - * @param string $source source path - * @param string $destination destination path - * @throws Forbidden - * @throws NotFound - */ - function checkMove($source, $destination) { - $sourceNode = $this->tree->getNodeForPath($source); - if ($sourceNode instanceof FutureFile) { - return; - } - list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($source); - list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination); - - if ($sourceDir !== $destinationDir) { - $sourceFileInfo = $this->fileView->getFileInfo($source); - - if ($sourceFileInfo === false) { - throw new NotFound($source . ' does not exist'); - } - - if (!$sourceFileInfo->isDeletable()) { - throw new Forbidden($source . " cannot be deleted"); - } - } - } - - /** - * This sets a cookie to be able to recognize the start of the download - * the content must not be longer than 32 characters and must only contain - * alphanumeric characters - * - * @param RequestInterface $request - * @param ResponseInterface $response - */ - function handleDownloadToken(RequestInterface $request, ResponseInterface $response) { - $queryParams = $request->getQueryParameters(); - - /** - * this sets a cookie to be able to recognize the start of the download - * the content must not be longer than 32 characters and must only contain - * alphanumeric characters - */ - if (isset($queryParams['downloadStartSecret'])) { - $token = $queryParams['downloadStartSecret']; - if (!isset($token[32]) - && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) { - // FIXME: use $response->setHeader() instead - setcookie('ocDownloadStarted', $token, time() + 20, '/'); - } - } - } - - /** - * Add headers to file download - * - * @param RequestInterface $request - * @param ResponseInterface $response - */ - function httpGet(RequestInterface $request, ResponseInterface $response) { - // Only handle valid files - $node = $this->tree->getNodeForPath($request->getPath()); - if (!($node instanceof IFile)) return; - - // adds a 'Content-Disposition: attachment' header - if ($this->downloadAttachment) { - $response->addHeader('Content-Disposition', 'attachment'); - } - - if ($node instanceof \OCA\DAV\Connector\Sabre\File) { - //Add OC-Checksum header - /** @var $node File */ - $checksum = $node->getChecksum(); - if ($checksum !== null && $checksum !== '') { - $response->addHeader('OC-Checksum', $checksum); - } - } - } - - /** - * Adds all ownCloud-specific properties - * - * @param PropFind $propFind - * @param \Sabre\DAV\INode $node - * @return void - */ - public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) { - - $httpRequest = $this->server->httpRequest; - - if ($node instanceof \OCA\DAV\Connector\Sabre\Node) { - - $propFind->handle(self::FILEID_PROPERTYNAME, function() use ($node) { - return $node->getFileId(); - }); - - $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function() use ($node) { - return $node->getInternalFileId(); - }); - - $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function() use ($node) { - $perms = $node->getDavPermissions(); - if ($this->isPublic) { - // remove mount information - $perms = str_replace(['S', 'M'], '', $perms); - } - return $perms; - }); - - $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function() use ($node, $httpRequest) { - return $node->getSharePermissions( - $httpRequest->getRawServerValue('PHP_AUTH_USER') - ); - }); - - $propFind->handle(self::GETETAG_PROPERTYNAME, function() use ($node) { - return $node->getETag(); - }); - - $propFind->handle(self::OWNER_ID_PROPERTYNAME, function() use ($node) { - $owner = $node->getOwner(); - return $owner->getUID(); - }); - $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function() use ($node) { - $owner = $node->getOwner(); - $displayName = $owner->getDisplayName(); - return $displayName; - }); - - $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) { - if ($node->getPath() === '/') { - return $this->config->getSystemValue('data-fingerprint', ''); - } - }); - } - - if ($node instanceof \OCA\DAV\Files\FilesHome) { - $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) { - return $this->config->getSystemValue('data-fingerprint', ''); - }); - } - - if ($node instanceof \OCA\DAV\Connector\Sabre\File) { - $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function() use ($node) { - /** @var $node \OCA\DAV\Connector\Sabre\File */ - try { - $directDownloadUrl = $node->getDirectDownload(); - if (isset($directDownloadUrl['url'])) { - return $directDownloadUrl['url']; - } - } catch (StorageNotAvailableException $e) { - return false; - } - return false; - }); - - $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function() use ($node) { - $checksum = $node->getChecksum(); - if ($checksum === NULL || $checksum === '') { - return null; - } - - return new ChecksumList($checksum); - }); - - } - - if ($node instanceof \OCA\DAV\Connector\Sabre\Directory) { - $propFind->handle(self::SIZE_PROPERTYNAME, function() use ($node) { - return $node->getSize(); - }); - } - } - - /** - * Update ownCloud-specific properties - * - * @param string $path - * @param PropPatch $propPatch - * - * @return void - */ - public function handleUpdateProperties($path, PropPatch $propPatch) { - $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function($time) use ($path) { - if (empty($time)) { - return false; - } - $node = $this->tree->getNodeForPath($path); - if (is_null($node)) { - return 404; - } - $node->touch($time); - return true; - }); - $propPatch->handle(self::GETETAG_PROPERTYNAME, function($etag) use ($path) { - if (empty($etag)) { - return false; - } - $node = $this->tree->getNodeForPath($path); - if (is_null($node)) { - return 404; - } - if ($node->setEtag($etag) !== -1) { - return true; - } - return false; - }); - } - - /** - * @param string $filePath - * @param \Sabre\DAV\INode $node - * @throws \Sabre\DAV\Exception\BadRequest - */ - public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) { - // chunked upload handling - if (isset($_SERVER['HTTP_OC_CHUNKED'])) { - list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($filePath); - $info = \OC_FileChunking::decodeName($name); - if (!empty($info)) { - $filePath = $path . '/' . $info['name']; - } - } - - // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder - if (!$this->server->tree->nodeExists($filePath)) { - return; - } - $node = $this->server->tree->getNodeForPath($filePath); - if ($node instanceof \OCA\DAV\Connector\Sabre\Node) { - $fileId = $node->getFileId(); - if (!is_null($fileId)) { - $this->server->httpResponse->setHeader('OC-FileId', $fileId); - } - } - } - -} diff --git a/apps/dav/lib/connector/sabre/filesreportplugin.php b/apps/dav/lib/connector/sabre/filesreportplugin.php deleted file mode 100644 index d4e1cbe3b20..00000000000 --- a/apps/dav/lib/connector/sabre/filesreportplugin.php +++ /dev/null @@ -1,333 +0,0 @@ - - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OC\Files\View; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Exception\PreconditionFailed; -use Sabre\DAV\Exception\ReportNotSupported; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\ServerPlugin; -use Sabre\DAV\Tree; -use Sabre\DAV\Xml\Element\Response; -use Sabre\DAV\Xml\Response\MultiStatus; -use Sabre\DAV\PropFind; -use OCP\SystemTag\ISystemTagObjectMapper; -use OCP\IUserSession; -use OCP\Files\Folder; -use OCP\IGroupManager; -use OCP\SystemTag\ISystemTagManager; -use OCP\SystemTag\TagNotFoundException; - -class FilesReportPlugin extends ServerPlugin { - - // namespace - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - const REPORT_NAME = '{http://owncloud.org/ns}filter-files'; - const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag'; - - /** - * Reference to main server object - * - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var Tree - */ - private $tree; - - /** - * @var View - */ - private $fileView; - - /** - * @var ISystemTagManager - */ - private $tagManager; - - /** - * @var ISystemTagObjectMapper - */ - private $tagMapper; - - /** - * @var IUserSession - */ - private $userSession; - - /** - * @var IGroupManager - */ - private $groupManager; - - /** - * @var Folder - */ - private $userFolder; - - /** - * @param Tree $tree - * @param View $view - */ - public function __construct(Tree $tree, - View $view, - ISystemTagManager $tagManager, - ISystemTagObjectMapper $tagMapper, - IUserSession $userSession, - IGroupManager $groupManager, - Folder $userFolder - ) { - $this->tree = $tree; - $this->fileView = $view; - $this->tagManager = $tagManager; - $this->tagMapper = $tagMapper; - $this->userSession = $userSession; - $this->groupManager = $groupManager; - $this->userFolder = $userFolder; - } - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - - $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; - - $this->server = $server; - $this->server->on('report', array($this, 'onReport')); - } - - /** - * Returns a list of reports this plugin supports. - * - * This will be used in the {DAV:}supported-report-set property. - * - * @param string $uri - * @return array - */ - public function getSupportedReportSet($uri) { - return [self::REPORT_NAME]; - } - - /** - * REPORT operations to look for files - * - * @param string $reportName - * @param [] $report - * @param string $uri - * @return bool - * @throws NotFound - * @throws ReportNotSupported - */ - public function onReport($reportName, $report, $uri) { - $reportTargetNode = $this->server->tree->getNodeForPath($uri); - if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) { - throw new ReportNotSupported(); - } - - $ns = '{' . $this::NS_OWNCLOUD . '}'; - $requestedProps = []; - $filterRules = []; - - // parse report properties and gather filter info - foreach ($report as $reportProps) { - $name = $reportProps['name']; - if ($name === $ns . 'filter-rules') { - $filterRules = $reportProps['value']; - } else if ($name === '{DAV:}prop') { - // propfind properties - foreach ($reportProps['value'] as $propVal) { - $requestedProps[] = $propVal['name']; - } - } - } - - if (empty($filterRules)) { - // an empty filter would return all existing files which would be slow - throw new BadRequest('Missing filter-rule block in request'); - } - - // gather all file ids matching filter - try { - $resultFileIds = $this->processFilterRules($filterRules); - } catch (TagNotFoundException $e) { - throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e); - } - - // find sabre nodes by file id, restricted to the root node path - $results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds); - - $responses = $this->prepareResponses($requestedProps, $results); - - $xml = $this->server->xml->write( - '{DAV:}multistatus', - new MultiStatus($responses) - ); - - $this->server->httpResponse->setStatus(207); - $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); - $this->server->httpResponse->setBody($xml); - - return false; - } - - /** - * Find file ids matching the given filter rules - * - * @param array $filterRules - * @return array array of unique file id results - * - * @throws TagNotFoundException whenever a tag was not found - */ - protected function processFilterRules($filterRules) { - $ns = '{' . $this::NS_OWNCLOUD . '}'; - $resultFileIds = null; - $systemTagIds = []; - foreach ($filterRules as $filterRule) { - if ($filterRule['name'] === $ns . 'systemtag') { - $systemTagIds[] = $filterRule['value']; - } - } - - // check user permissions, if applicable - if (!$this->isAdmin()) { - // check visibility/permission - $tags = $this->tagManager->getTagsByIds($systemTagIds); - $unknownTagIds = []; - foreach ($tags as $tag) { - if (!$tag->isUserVisible()) { - $unknownTagIds[] = $tag->getId(); - } - } - - if (!empty($unknownTagIds)) { - throw new TagNotFoundException('Tag with ids ' . implode(', ', $unknownTagIds) . ' not found'); - } - } - - // fetch all file ids and intersect them - foreach ($systemTagIds as $systemTagId) { - $fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files'); - - if (empty($fileIds)) { - // This tag has no files, nothing can ever show up - return []; - } - - // first run ? - if ($resultFileIds === null) { - $resultFileIds = $fileIds; - } else { - $resultFileIds = array_intersect($resultFileIds, $fileIds); - } - - if (empty($resultFileIds)) { - // Empty intersection, nothing can show up anymore - return []; - } - } - return $resultFileIds; - } - - /** - * Prepare propfind response for the given nodes - * - * @param string[] $requestedProps requested properties - * @param Node[] nodes nodes for which to fetch and prepare responses - * @return Response[] - */ - public function prepareResponses($requestedProps, $nodes) { - $responses = []; - foreach ($nodes as $node) { - $propFind = new PropFind($node->getPath(), $requestedProps); - - $this->server->getPropertiesByNode($propFind, $node); - // copied from Sabre Server's getPropertiesForPath - $result = $propFind->getResultForMultiStatus(); - $result['href'] = $propFind->getPath(); - - $resourceType = $this->server->getResourceTypeForNode($node); - if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { - $result['href'] .= '/'; - } - - $responses[] = new Response( - rtrim($this->server->getBaseUri(), '/') . $node->getPath(), - $result, - 200 - ); - } - return $responses; - } - - /** - * Find Sabre nodes by file ids - * - * @param Node $rootNode root node for search - * @param array $fileIds file ids - * @return Node[] array of Sabre nodes - */ - public function findNodesByFileIds($rootNode, $fileIds) { - $folder = $this->userFolder; - if (trim($rootNode->getPath(), '/') !== '') { - $folder = $folder->get($rootNode->getPath()); - } - - $results = []; - foreach ($fileIds as $fileId) { - $entry = $folder->getById($fileId); - if ($entry) { - $entry = current($entry); - if ($entry instanceof \OCP\Files\File) { - $results[] = new File($this->fileView, $entry); - } else if ($entry instanceof \OCP\Files\Folder) { - $results[] = new Directory($this->fileView, $entry); - } - } - } - - return $results; - } - - /** - * Returns whether the currently logged in user is an administrator - */ - private function isAdmin() { - $user = $this->userSession->getUser(); - if ($user !== null) { - return $this->groupManager->isAdmin($user->getUID()); - } - return false; - } -} diff --git a/apps/dav/lib/connector/sabre/lockplugin.php b/apps/dav/lib/connector/sabre/lockplugin.php deleted file mode 100644 index 66da39a57c8..00000000000 --- a/apps/dav/lib/connector/sabre/lockplugin.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @author Roeland Jago Douma - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\Exception\FileLocked; -use OCA\DAV\Connector\Sabre\Node; -use OCP\Lock\ILockingProvider; -use OCP\Lock\LockedException; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\ServerPlugin; -use Sabre\HTTP\RequestInterface; - -class LockPlugin extends ServerPlugin { - /** - * Reference to main server object - * - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * {@inheritdoc} - */ - public function initialize(\Sabre\DAV\Server $server) { - $this->server = $server; - $this->server->on('beforeMethod', [$this, 'getLock'], 50); - $this->server->on('afterMethod', [$this, 'releaseLock'], 50); - } - - public function getLock(RequestInterface $request) { - // we can't listen on 'beforeMethod:PUT' due to order of operations with setting up the tree - // so instead we limit ourselves to the PUT method manually - if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) { - return; - } - try { - $node = $this->server->tree->getNodeForPath($request->getPath()); - } catch (NotFound $e) { - return; - } - if ($node instanceof Node) { - try { - $node->acquireLock(ILockingProvider::LOCK_SHARED); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - } - } - - public function releaseLock(RequestInterface $request) { - if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) { - return; - } - try { - $node = $this->server->tree->getNodeForPath($request->getPath()); - } catch (NotFound $e) { - return; - } - if ($node instanceof Node) { - $node->releaseLock(ILockingProvider::LOCK_SHARED); - } - } -} diff --git a/apps/dav/lib/connector/sabre/maintenanceplugin.php b/apps/dav/lib/connector/sabre/maintenanceplugin.php deleted file mode 100644 index 6e9a5930b78..00000000000 --- a/apps/dav/lib/connector/sabre/maintenanceplugin.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @author Joas Schilling - * @author Morris Jobke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCP\IConfig; -use Sabre\DAV\Exception\ServiceUnavailable; -use Sabre\DAV\ServerPlugin; - -class MaintenancePlugin extends ServerPlugin { - - /** @var IConfig */ - private $config; - - /** - * Reference to main server object - * - * @var Server - */ - private $server; - - /** - * @param IConfig $config - */ - public function __construct(IConfig $config = null) { - $this->config = $config; - if (is_null($config)) { - $this->config = \OC::$server->getConfig(); - } - } - - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - $this->server = $server; - $this->server->on('beforeMethod', array($this, 'checkMaintenanceMode'), 1); - } - - /** - * This method is called before any HTTP method and returns http status code 503 - * in case the system is in maintenance mode. - * - * @throws ServiceUnavailable - * @return bool - */ - public function checkMaintenanceMode() { - if ($this->config->getSystemValue('singleuser', false)) { - throw new ServiceUnavailable('System in single user mode.'); - } - if ($this->config->getSystemValue('maintenance', false)) { - throw new ServiceUnavailable('System in maintenance mode.'); - } - if (\OC::checkUpgrade(false)) { - throw new ServiceUnavailable('Upgrade needed'); - } - - return true; - } -} diff --git a/apps/dav/lib/connector/sabre/node.php b/apps/dav/lib/connector/sabre/node.php deleted file mode 100644 index ccc035063cd..00000000000 --- a/apps/dav/lib/connector/sabre/node.php +++ /dev/null @@ -1,348 +0,0 @@ - - * @author Bart Visscher - * @author Jakob Sack - * @author Jörn Friedrich Dreyer - * @author Klaas Freitag - * @author Markus Goetz - * @author Morris Jobke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OC\Files\Mount\MoveableMount; -use OCA\DAV\Connector\Sabre\Exception\InvalidPath; -use OCP\Share\Exceptions\ShareNotFound; -use OCP\Share\IManager; - - -abstract class Node implements \Sabre\DAV\INode { - - /** - * @var \OC\Files\View - */ - protected $fileView; - - /** - * The path to the current node - * - * @var string - */ - protected $path; - - /** - * node properties cache - * - * @var array - */ - protected $property_cache = null; - - /** - * @var \OCP\Files\FileInfo - */ - protected $info; - - /** - * @var IManager - */ - protected $shareManager; - - /** - * Sets up the node, expects a full path name - * - * @param \OC\Files\View $view - * @param \OCP\Files\FileInfo $info - * @param IManager $shareManager - */ - public function __construct($view, $info, IManager $shareManager = null) { - $this->fileView = $view; - $this->path = $this->fileView->getRelativePath($info->getPath()); - $this->info = $info; - if ($shareManager) { - $this->shareManager = $shareManager; - } else { - $this->shareManager = \OC::$server->getShareManager(); - } - } - - protected function refreshInfo() { - $this->info = $this->fileView->getFileInfo($this->path); - } - - /** - * Returns the name of the node - * - * @return string - */ - public function getName() { - return $this->info->getName(); - } - - /** - * Returns the full path - * - * @return string - */ - public function getPath() { - return $this->path; - } - - /** - * Renames the node - * - * @param string $name The new name - * @throws \Sabre\DAV\Exception\BadRequest - * @throws \Sabre\DAV\Exception\Forbidden - */ - public function setName($name) { - - // rename is only allowed if the update privilege is granted - if (!$this->info->isUpdateable()) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - - list($parentPath,) = \Sabre\HTTP\URLUtil::splitPath($this->path); - list(, $newName) = \Sabre\HTTP\URLUtil::splitPath($name); - - // verify path of the target - $this->verifyPath(); - - $newPath = $parentPath . '/' . $newName; - - $this->fileView->rename($this->path, $newPath); - - $this->path = $newPath; - - $this->refreshInfo(); - } - - public function setPropertyCache($property_cache) { - $this->property_cache = $property_cache; - } - - /** - * Returns the last modification time, as a unix timestamp - * - * @return int timestamp as integer - */ - public function getLastModified() { - $timestamp = $this->info->getMtime(); - if (!empty($timestamp)) { - return (int)$timestamp; - } - return $timestamp; - } - - /** - * sets the last modification time of the file (mtime) to the value given - * in the second parameter or to now if the second param is empty. - * Even if the modification time is set to a custom value the access time is set to now. - */ - public function touch($mtime) { - $this->fileView->touch($this->path, $mtime); - $this->refreshInfo(); - } - - /** - * 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 '"' . $this->info->getEtag() . '"'; - } - - /** - * Sets the ETag - * - * @param string $etag - * - * @return int file id of updated file or -1 on failure - */ - public function setETag($etag) { - return $this->fileView->putFileInfo($this->path, array('etag' => $etag)); - } - - /** - * Returns the size of the node, in bytes - * - * @return integer - */ - public function getSize() { - return $this->info->getSize(); - } - - /** - * Returns the cache's file id - * - * @return int - */ - public function getId() { - return $this->info->getId(); - } - - /** - * @return string|null - */ - public function getFileId() { - if ($this->info->getId()) { - $instanceId = \OC_Util::getInstanceId(); - $id = sprintf('%08d', $this->info->getId()); - return $id . $instanceId; - } - - return null; - } - - /** - * @return integer - */ - public function getInternalFileId() { - return $this->info->getId(); - } - - /** - * @param string $user - * @return int - */ - public function getSharePermissions($user) { - - // check of we access a federated share - if ($user !== null) { - try { - $share = $this->shareManager->getShareByToken($user); - return $share->getPermissions(); - } catch (ShareNotFound $e) { - // ignore - } - } - - $storage = $this->info->getStorage(); - - $path = $this->info->getInternalPath(); - - if ($storage->instanceOfStorage('\OC\Files\Storage\Shared')) { - /** @var \OC\Files\Storage\Shared $storage */ - $permissions = (int)$storage->getShare()->getPermissions(); - } else { - $permissions = $storage->getPermissions($path); - } - - /* - * We can always share non moveable mount points with DELETE and UPDATE - * Eventually we need to do this properly - */ - $mountpoint = $this->info->getMountPoint(); - if (!($mountpoint instanceof MoveableMount)) { - $mountpointpath = $mountpoint->getMountPoint(); - if (substr($mountpointpath, -1) === '/') { - $mountpointpath = substr($mountpointpath, 0, -1); - } - - if ($mountpointpath === $this->info->getPath()) { - $permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE; - } - } - - /* - * Files can't have create or delete permissions - */ - if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) { - $permissions &= ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE); - } - - return $permissions; - } - - /** - * @return string - */ - public function getDavPermissions() { - $p = ''; - if ($this->info->isShared()) { - $p .= 'S'; - } - if ($this->info->isShareable()) { - $p .= 'R'; - } - if ($this->info->isMounted()) { - $p .= 'M'; - } - if ($this->info->isDeletable()) { - $p .= 'D'; - } - if ($this->info->isUpdateable()) { - $p .= 'NV'; // Renameable, Moveable - } - if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) { - if ($this->info->isUpdateable()) { - $p .= 'W'; - } - } else { - if ($this->info->isCreatable()) { - $p .= 'CK'; - } - } - return $p; - } - - public function getOwner() { - return $this->info->getOwner(); - } - - protected function verifyPath() { - try { - $fileName = basename($this->info->getPath()); - $this->fileView->verifyPath($this->path, $fileName); - } catch (\OCP\Files\InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); - } - } - - /** - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - */ - public function acquireLock($type) { - $this->fileView->lockFile($this->path, $type); - } - - /** - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - */ - public function releaseLock($type) { - $this->fileView->unlockFile($this->path, $type); - } - - /** - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - */ - public function changeLock($type) { - $this->fileView->changeLock($this->path, $type); - } -} diff --git a/apps/dav/lib/connector/sabre/objecttree.php b/apps/dav/lib/connector/sabre/objecttree.php deleted file mode 100644 index f38dfe679c7..00000000000 --- a/apps/dav/lib/connector/sabre/objecttree.php +++ /dev/null @@ -1,297 +0,0 @@ - - * @author Joas Schilling - * @author Morris Jobke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\Exception\Forbidden; -use OCA\DAV\Connector\Sabre\Exception\InvalidPath; -use OCA\DAV\Connector\Sabre\Exception\FileLocked; -use OC\Files\FileInfo; -use OC\Files\Mount\MoveableMount; -use OCP\Files\ForbiddenException; -use OCP\Files\StorageInvalidException; -use OCP\Files\StorageNotAvailableException; -use OCP\Lock\LockedException; - -class ObjectTree extends \Sabre\DAV\Tree { - - /** - * @var \OC\Files\View - */ - protected $fileView; - - /** - * @var \OCP\Files\Mount\IMountManager - */ - protected $mountManager; - - /** - * Creates the object - */ - public function __construct() { - } - - /** - * @param \Sabre\DAV\INode $rootNode - * @param \OC\Files\View $view - * @param \OCP\Files\Mount\IMountManager $mountManager - */ - public function init(\Sabre\DAV\INode $rootNode, \OC\Files\View $view, \OCP\Files\Mount\IMountManager $mountManager) { - $this->rootNode = $rootNode; - $this->fileView = $view; - $this->mountManager = $mountManager; - } - - /** - * If the given path is a chunked file name, converts it - * to the real file name. Only applies if the OC-CHUNKED header - * is present. - * - * @param string $path chunk file path to convert - * - * @return string path to real file - */ - private function resolveChunkFile($path) { - if (isset($_SERVER['HTTP_OC_CHUNKED'])) { - // resolve to real file name to find the proper node - list($dir, $name) = \Sabre\HTTP\URLUtil::splitPath($path); - if ($dir == '/' || $dir == '.') { - $dir = ''; - } - - $info = \OC_FileChunking::decodeName($name); - // only replace path if it was really the chunked file - if (isset($info['transferid'])) { - // getNodePath is called for multiple nodes within a chunk - // upload call - $path = $dir . '/' . $info['name']; - $path = ltrim($path, '/'); - } - } - return $path; - } - - public function cacheNode(Node $node) { - $this->cache[trim($node->getPath(), '/')] = $node; - } - - /** - * Returns the INode object for the requested path - * - * @param string $path - * @return \Sabre\DAV\INode - * @throws InvalidPath - * @throws \Sabre\DAV\Exception\Locked - * @throws \Sabre\DAV\Exception\NotFound - * @throws \Sabre\DAV\Exception\ServiceUnavailable - */ - public function getNodeForPath($path) { - if (!$this->fileView) { - throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup'); - } - - $path = trim($path, '/'); - - if (isset($this->cache[$path])) { - return $this->cache[$path]; - } - - if ($path) { - try { - $this->fileView->verifyPath($path, basename($path)); - } catch (\OCP\Files\InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); - } - } - - // Is it the root node? - if (!strlen($path)) { - return $this->rootNode; - } - - if (pathinfo($path, PATHINFO_EXTENSION) === 'part') { - // read from storage - $absPath = $this->fileView->getAbsolutePath($path); - $mount = $this->fileView->getMount($path); - $storage = $mount->getStorage(); - $internalPath = $mount->getInternalPath($absPath); - if ($storage && $storage->file_exists($internalPath)) { - /** - * @var \OC\Files\Storage\Storage $storage - */ - // get data directly - $data = $storage->getMetaData($internalPath); - $info = new FileInfo($absPath, $storage, $internalPath, $data, $mount); - } else { - $info = null; - } - } else { - // resolve chunk file name to real name, if applicable - $path = $this->resolveChunkFile($path); - - // read from cache - try { - $info = $this->fileView->getFileInfo($path); - } catch (StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable('Storage not available'); - } catch (StorageInvalidException $e) { - throw new \Sabre\DAV\Exception\NotFound('Storage ' . $path . ' is invalid'); - } catch (LockedException $e) { - throw new \Sabre\DAV\Exception\Locked(); - } - } - - if (!$info) { - throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located'); - } - - if ($info->getType() === 'dir') { - $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this); - } else { - $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info); - } - - $this->cache[$path] = $node; - return $node; - - } - - /** - * 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 - * @throws \Sabre\DAV\Exception\BadRequest - * @throws \Sabre\DAV\Exception\ServiceUnavailable - * @throws \Sabre\DAV\Exception\Forbidden - * @return int - */ - public function move($sourcePath, $destinationPath) { - if (!$this->fileView) { - throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup'); - } - - $targetNodeExists = $this->nodeExists($destinationPath); - $sourceNode = $this->getNodeForPath($sourcePath); - if ($sourceNode instanceof \Sabre\DAV\ICollection && $targetNodeExists) { - throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists'); - } - list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($sourcePath); - list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destinationPath); - - $isMovableMount = false; - $sourceMount = $this->mountManager->find($this->fileView->getAbsolutePath($sourcePath)); - $internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath)); - if ($sourceMount instanceof MoveableMount && $internalPath === '') { - $isMovableMount = true; - } - - try { - $sameFolder = ($sourceDir === $destinationDir); - // if we're overwriting or same folder - if ($targetNodeExists || $sameFolder) { - // note that renaming a share mount point is always allowed - if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - } else { - if (!$this->fileView->isCreatable($destinationDir)) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - } - - if (!$sameFolder) { - // moving to a different folder, source will be gone, like a deletion - // note that moving a share mount point is always allowed - if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - } - - $fileName = basename($destinationPath); - try { - $this->fileView->verifyPath($destinationDir, $fileName); - } catch (\OCP\Files\InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); - } - - $renameOkay = $this->fileView->rename($sourcePath, $destinationPath); - if (!$renameOkay) { - throw new \Sabre\DAV\Exception\Forbidden(''); - } - } catch (StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); - } catch (ForbiddenException $ex) { - throw new Forbidden($ex->getMessage(), $ex->getRetry()); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - - $this->markDirty($sourceDir); - $this->markDirty($destinationDir); - - } - - /** - * Copies a file or directory. - * - * This method must work recursively and delete the destination - * if it exists - * - * @param string $source - * @param string $destination - * @throws \Sabre\DAV\Exception\ServiceUnavailable - * @return void - */ - public function copy($source, $destination) { - if (!$this->fileView) { - throw new \Sabre\DAV\Exception\ServiceUnavailable('filesystem not setup'); - } - - // this will trigger existence check - $this->getNodeForPath($source); - - list($destinationDir, $destinationName) = \Sabre\HTTP\URLUtil::splitPath($destination); - try { - $this->fileView->verifyPath($destinationDir, $destinationName); - } catch (\OCP\Files\InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); - } - - try { - $this->fileView->copy($source, $destination); - } catch (StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); - } catch (ForbiddenException $ex) { - throw new Forbidden($ex->getMessage(), $ex->getRetry()); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - - list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination); - $this->markDirty($destinationDir); - } -} diff --git a/apps/dav/lib/connector/sabre/principal.php b/apps/dav/lib/connector/sabre/principal.php deleted file mode 100644 index 787bcdf469b..00000000000 --- a/apps/dav/lib/connector/sabre/principal.php +++ /dev/null @@ -1,234 +0,0 @@ - - * @author Jakob Sack - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Morris Jobke - * @author Thomas Müller - * @author Thomas Tanghus - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCP\IGroup; -use OCP\IGroupManager; -use OCP\IUser; -use OCP\IUserManager; -use Sabre\DAV\Exception; -use \Sabre\DAV\PropPatch; -use Sabre\DAVACL\PrincipalBackend\BackendInterface; -use Sabre\HTTP\URLUtil; - -class Principal implements BackendInterface { - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var string */ - private $principalPrefix; - - /** @var bool */ - private $hasGroups; - - /** - * @param IUserManager $userManager - * @param IGroupManager $groupManager - * @param string $principalPrefix - */ - public function __construct(IUserManager $userManager, - IGroupManager $groupManager, - $principalPrefix = 'principals/users/') { - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->principalPrefix = trim($principalPrefix, '/'); - $this->hasGroups = ($principalPrefix === 'principals/users/'); - } - - /** - * 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 - * - * @param string $prefixPath - * @return string[] - */ - public function getPrincipalsByPrefix($prefixPath) { - $principals = []; - - if ($prefixPath === $this->principalPrefix) { - foreach($this->userManager->search('') as $user) { - $principals[] = $this->userToPrincipal($user); - } - } - - 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) { - list($prefix, $name) = URLUtil::splitPath($path); - - if ($prefix === $this->principalPrefix) { - $user = $this->userManager->get($name); - - if (!is_null($user)) { - return $this->userToPrincipal($user); - } - } - return null; - } - - /** - * Returns the list of members for a group-principal - * - * @param string $principal - * @return string[] - * @throws Exception - */ - public function getGroupMemberSet($principal) { - // TODO: for now the group principal has only one member, the user itself - $principal = $this->getPrincipalByPath($principal); - if (!$principal) { - throw new Exception('Principal not found'); - } - - return [$principal['uri']]; - } - - /** - * Returns the list of groups a principal is a member of - * - * @param string $principal - * @param bool $needGroups - * @return array - * @throws Exception - */ - public function getGroupMembership($principal, $needGroups = false) { - list($prefix, $name) = URLUtil::splitPath($principal); - - if ($prefix === $this->principalPrefix) { - $user = $this->userManager->get($name); - if (!$user) { - throw new Exception('Principal not found'); - } - - if ($this->hasGroups || $needGroups) { - $groups = $this->groupManager->getUserGroups($user); - $groups = array_map(function($group) { - /** @var IGroup $group */ - return 'principals/groups/' . $group->getGID(); - }, $groups); - - return $groups; - } - } - return []; - } - - /** - * 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 string[] $members - * @throws Exception - */ - public function setGroupMemberSet($principal, array $members) { - throw new Exception('Setting members of the group is not supported yet'); - } - - /** - * @param string $path - * @param PropPatch $propPatch - * @return int - */ - function updatePrincipal($path, PropPatch $propPatch) { - return 0; - } - - /** - * @param string $prefixPath - * @param array $searchProperties - * @param string $test - * @return array - */ - function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { - return []; - } - - /** - * @param string $uri - * @param string $principalPrefix - * @return string - */ - function findByUri($uri, $principalPrefix) { - if (substr($uri, 0, 7) === 'mailto:') { - $email = substr($uri, 7); - $users = $this->userManager->getByEmail($email); - if (count($users) === 1) { - return $this->principalPrefix . '/' . $users[0]->getUID(); - } - } - - return ''; - } - - /** - * @param IUser $user - * @return array - */ - protected function userToPrincipal($user) { - $userId = $user->getUID(); - $displayName = $user->getDisplayName(); - $principal = [ - 'uri' => $this->principalPrefix . '/' . $userId, - '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName, - ]; - - $email = $user->getEMailAddress(); - if (!empty($email)) { - $principal['{http://sabredav.org/ns}email-address'] = $email; - return $principal; - } - return $principal; - } - - public function getPrincipalPrefix() { - return $this->principalPrefix; - } - -} diff --git a/apps/dav/lib/connector/sabre/quotaplugin.php b/apps/dav/lib/connector/sabre/quotaplugin.php deleted file mode 100644 index a093c52851c..00000000000 --- a/apps/dav/lib/connector/sabre/quotaplugin.php +++ /dev/null @@ -1,146 +0,0 @@ - - * @author Morris Jobke - * @author Robin Appelman - * @author scambra - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Connector\Sabre; - -/** - * This plugin check user quota and deny creating files when they exceeds the quota. - * - * @author Sergio Cambra - * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved. - * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License - */ -class QuotaPlugin extends \Sabre\DAV\ServerPlugin { - - /** - * @var \OC\Files\View - */ - private $view; - - /** - * Reference to main server object - * - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @param \OC\Files\View $view - */ - public function __construct($view) { - $this->view = $view; - } - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the requires event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - - $this->server = $server; - - $server->on('beforeWriteContent', array($this, 'checkQuota'), 10); - $server->on('beforeCreateFile', array($this, 'checkQuota'), 10); - } - - /** - * This method is called before any HTTP method and validates there is enough free space to store the file - * - * @param string $uri - * @param null $data - * @throws \Sabre\DAV\Exception\InsufficientStorage - * @return bool - */ - public function checkQuota($uri, $data = null) { - $length = $this->getLength(); - if ($length) { - if (substr($uri, 0, 1) !== '/') { - $uri = '/' . $uri; - } - list($parentUri, $newName) = \Sabre\HTTP\URLUtil::splitPath($uri); - if(is_null($parentUri)) { - $parentUri = ''; - } - $req = $this->server->httpRequest; - if ($req->getHeader('OC-Chunked')) { - $info = \OC_FileChunking::decodeName($newName); - $chunkHandler = $this->getFileChunking($info); - // subtract the already uploaded size to see whether - // there is still enough space for the remaining chunks - $length -= $chunkHandler->getCurrentSize(); - // use target file name for free space check in case of shared files - $uri = rtrim($parentUri, '/') . '/' . $info['name']; - } - $freeSpace = $this->getFreeSpace($uri); - if ($freeSpace !== \OCP\Files\FileInfo::SPACE_UNKNOWN && $length > $freeSpace) { - if (isset($chunkHandler)) { - $chunkHandler->cleanup(); - } - throw new \Sabre\DAV\Exception\InsufficientStorage(); - } - } - return true; - } - - public function getFileChunking($info) { - // FIXME: need a factory for better mocking support - return new \OC_FileChunking($info); - } - - public function getLength() { - $req = $this->server->httpRequest; - $length = $req->getHeader('X-Expected-Entity-Length'); - if (!$length) { - $length = $req->getHeader('Content-Length'); - } - - $ocLength = $req->getHeader('OC-Total-Length'); - if ($length && $ocLength) { - return max($length, $ocLength); - } - - return $length; - } - - /** - * @param string $uri - * @return mixed - */ - public function getFreeSpace($uri) { - try { - $freeSpace = $this->view->free_space(ltrim($uri, '/')); - return $freeSpace; - } catch (\OCP\Files\StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); - } - } -} diff --git a/apps/dav/lib/connector/sabre/server.php b/apps/dav/lib/connector/sabre/server.php deleted file mode 100644 index 421fc64422d..00000000000 --- a/apps/dav/lib/connector/sabre/server.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @author scolebrook - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -/** - * Class \OCA\DAV\Connector\Sabre\Server - * - * This class overrides some methods from @see \Sabre\DAV\Server. - * - * @see \Sabre\DAV\Server - */ -class Server extends \Sabre\DAV\Server { - - /** - * @see \Sabre\DAV\Server - */ - public function __construct($treeOrNode = null) { - parent::__construct($treeOrNode); - self::$exposeVersion = false; - $this->enablePropfindDepthInfinity = true; - } -} diff --git a/apps/dav/lib/connector/sabre/serverfactory.php b/apps/dav/lib/connector/sabre/serverfactory.php deleted file mode 100644 index 5853370778d..00000000000 --- a/apps/dav/lib/connector/sabre/serverfactory.php +++ /dev/null @@ -1,184 +0,0 @@ - - * @author Joas Schilling - * @author Lukas Reschke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use OCA\DAV\Files\BrowserErrorPagePlugin; -use OCP\Files\Mount\IMountManager; -use OCP\IConfig; -use OCP\IDBConnection; -use OCP\ILogger; -use OCP\IRequest; -use OCP\ITagManager; -use OCP\IUserSession; -use Sabre\DAV\Auth\Backend\BackendInterface; - -class ServerFactory { - /** @var IConfig */ - private $config; - /** @var ILogger */ - private $logger; - /** @var IDBConnection */ - private $databaseConnection; - /** @var IUserSession */ - private $userSession; - /** @var IMountManager */ - private $mountManager; - /** @var ITagManager */ - private $tagManager; - /** @var IRequest */ - private $request; - - /** - * @param IConfig $config - * @param ILogger $logger - * @param IDBConnection $databaseConnection - * @param IUserSession $userSession - * @param IMountManager $mountManager - * @param ITagManager $tagManager - * @param IRequest $request - */ - public function __construct( - IConfig $config, - ILogger $logger, - IDBConnection $databaseConnection, - IUserSession $userSession, - IMountManager $mountManager, - ITagManager $tagManager, - IRequest $request - ) { - $this->config = $config; - $this->logger = $logger; - $this->databaseConnection = $databaseConnection; - $this->userSession = $userSession; - $this->mountManager = $mountManager; - $this->tagManager = $tagManager; - $this->request = $request; - } - - /** - * @param string $baseUri - * @param string $requestUri - * @param BackendInterface $authBackend - * @param callable $viewCallBack callback that should return the view for the dav endpoint - * @return Server - */ - public function createServer($baseUri, - $requestUri, - BackendInterface $authBackend, - callable $viewCallBack) { - // Fire up server - $objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree(); - $server = new \OCA\DAV\Connector\Sabre\Server($objectTree); - // Set URL explicitly due to reverse-proxy situations - $server->httpRequest->setUrl($requestUri); - $server->setBaseUri($baseUri); - - // Load plugins - $defaults = new \OC_Defaults(); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config)); - $server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, $defaults->getName())); - // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / - $server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin()); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); - // Some WebDAV clients do require Class 2 WebDAV support (locking), since - // we do not provide locking we emulate it using a fake locking plugin. - if($this->request->isUserAgent([ - '/WebDAVFS/', - '/Microsoft Office OneNote 2013/', - '/Microsoft-WebDAV-MiniRedir/', - ])) { - $server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin()); - } - - if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) { - $server->addPlugin(new BrowserErrorPagePlugin()); - } - - // wait with registering these until auth is handled and the filesystem is setup - $server->on('beforeMethod', function () use ($server, $objectTree, $viewCallBack) { - // ensure the skeleton is copied - $userFolder = \OC::$server->getUserFolder(); - - /** @var \OC\Files\View $view */ - $view = $viewCallBack($server); - $rootInfo = $view->getFileInfo(''); - - // Create ownCloud Dir - if ($rootInfo->getType() === 'dir') { - $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree); - } else { - $root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo); - } - $objectTree->init($root, $view, $this->mountManager); - - $server->addPlugin( - new \OCA\DAV\Connector\Sabre\FilesPlugin( - $objectTree, - $view, - $this->config, - false, - !$this->config->getSystemValue('debug', false) - ) - ); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view)); - - if($this->userSession->isLoggedIn()) { - $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin( - $objectTree, - $this->userSession, - $userFolder, - \OC::$server->getShareManager() - )); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin(\OC::$server->getCommentsManager(), $this->userSession)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesReportPlugin( - $objectTree, - $view, - \OC::$server->getSystemTagManager(), - \OC::$server->getSystemTagObjectMapper(), - $this->userSession, - \OC::$server->getGroupManager(), - $userFolder - )); - // custom properties plugin must be the last one - $server->addPlugin( - new \Sabre\DAV\PropertyStorage\Plugin( - new \OCA\DAV\Connector\Sabre\CustomPropertiesBackend( - $objectTree, - $this->databaseConnection, - $this->userSession->getUser() - ) - ) - ); - } - $server->addPlugin(new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin()); - }, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request - return $server; - } -} diff --git a/apps/dav/lib/connector/sabre/sharesplugin.php b/apps/dav/lib/connector/sabre/sharesplugin.php deleted file mode 100644 index c76068969e9..00000000000 --- a/apps/dav/lib/connector/sabre/sharesplugin.php +++ /dev/null @@ -1,177 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Connector\Sabre; - -use \Sabre\DAV\PropFind; -use \Sabre\DAV\PropPatch; -use OCP\IUserSession; -use OCP\Share\IShare; -use OCA\DAV\Connector\Sabre\ShareTypeList; - -/** - * Sabre Plugin to provide share-related properties - */ -class SharesPlugin extends \Sabre\DAV\ServerPlugin { - - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - const SHARETYPES_PROPERTYNAME = '{http://owncloud.org/ns}share-types'; - - /** - * Reference to main server object - * - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var \OCP\Share\IManager - */ - private $shareManager; - - /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** - * @var string - */ - private $userId; - - /** - * @var \OCP\Files\Folder - */ - private $userFolder; - - /** - * @var IShare[] - */ - private $cachedShareTypes; - - /** - * @param \Sabre\DAV\Tree $tree tree - * @param IUserSession $userSession user session - * @param \OCP\Files\Folder $userFolder user home folder - * @param \OCP\Share\IManager $shareManager share manager - */ - public function __construct( - \Sabre\DAV\Tree $tree, - IUserSession $userSession, - \OCP\Files\Folder $userFolder, - \OCP\Share\IManager $shareManager - ) { - $this->tree = $tree; - $this->shareManager = $shareManager; - $this->userFolder = $userFolder; - $this->userId = $userSession->getUser()->getUID(); - $this->cachedShareTypes = []; - } - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - */ - public function initialize(\Sabre\DAV\Server $server) { - $server->xml->namespacesMap[self::NS_OWNCLOUD] = 'oc'; - $server->xml->elementMap[self::SHARETYPES_PROPERTYNAME] = 'OCA\\DAV\\Connector\\Sabre\\ShareTypeList'; - $server->protectedProperties[] = self::SHARETYPES_PROPERTYNAME; - - $this->server = $server; - $this->server->on('propFind', array($this, 'handleGetProperties')); - } - - /** - * Return a list of share types for outgoing shares - * - * @param \OCP\Files\Node $node file node - * - * @return int[] array of share types - */ - private function getShareTypes(\OCP\Files\Node $node) { - $shareTypes = []; - $requestedShareTypes = [ - \OCP\Share::SHARE_TYPE_USER, - \OCP\Share::SHARE_TYPE_GROUP, - \OCP\Share::SHARE_TYPE_LINK, - \OCP\Share::SHARE_TYPE_REMOTE - ]; - foreach ($requestedShareTypes as $requestedShareType) { - // one of each type is enough to find out about the types - $shares = $this->shareManager->getSharesBy( - $this->userId, - $requestedShareType, - $node, - false, - 1 - ); - if (!empty($shares)) { - $shareTypes[] = $requestedShareType; - } - } - return $shareTypes; - } - - /** - * Adds shares to propfind response - * - * @param PropFind $propFind propfind object - * @param \Sabre\DAV\INode $sabreNode sabre node - */ - public function handleGetProperties( - PropFind $propFind, - \Sabre\DAV\INode $sabreNode - ) { - if (!($sabreNode instanceof \OCA\DAV\Connector\Sabre\Node)) { - return; - } - - // need prefetch ? - if ($sabreNode instanceof \OCA\DAV\Connector\Sabre\Directory - && $propFind->getDepth() !== 0 - && !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) - ) { - $folderNode = $this->userFolder->get($propFind->getPath()); - $children = $folderNode->getDirectoryListing(); - - $this->cachedShareTypes[$folderNode->getId()] = $this->getShareTypes($folderNode); - foreach ($children as $childNode) { - $this->cachedShareTypes[$childNode->getId()] = $this->getShareTypes($childNode); - } - } - - $propFind->handle(self::SHARETYPES_PROPERTYNAME, function() use ($sabreNode) { - if (isset($this->cachedShareTypes[$sabreNode->getId()])) { - $shareTypes = $this->cachedShareTypes[$sabreNode->getId()]; - } else { - $node = $this->userFolder->get($sabreNode->getPath()); - $shareTypes = $this->getShareTypes($node); - } - - return new ShareTypeList($shareTypes); - }); - } -} diff --git a/apps/dav/lib/connector/sabre/sharetypelist.php b/apps/dav/lib/connector/sabre/sharetypelist.php deleted file mode 100644 index 763586412ad..00000000000 --- a/apps/dav/lib/connector/sabre/sharetypelist.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use Sabre\Xml\Element; -use Sabre\Xml\Reader; -use Sabre\Xml\Writer; - -/** - * ShareTypeList property - * - * This property contains multiple "share-type" elements, each containing a share type. - */ -class ShareTypeList implements Element { - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - /** - * Share types - * - * @var int[] - */ - private $shareTypes; - - /** - * @param int[] $shareTypes - */ - public function __construct($shareTypes) { - $this->shareTypes = $shareTypes; - } - - /** - * Returns the share types - * - * @return int[] - */ - public function getShareTypes() { - return $this->shareTypes; - } - - /** - * The deserialize method is called during xml parsing. - * - * @param Reader $reader - * @return mixed - */ - static function xmlDeserialize(Reader $reader) { - $shareTypes = []; - - foreach ($reader->parseInnerTree() as $elem) { - if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}share-type') { - $shareTypes[] = (int)$elem['value']; - } - } - return new self($shareTypes); - } - - /** - * The xmlSerialize metod is called during xml writing. - * - * @param Writer $writer - * @return void - */ - function xmlSerialize(Writer $writer) { - foreach ($this->shareTypes as $shareType) { - $writer->writeElement('{' . self::NS_OWNCLOUD . '}share-type', $shareType); - } - } -} diff --git a/apps/dav/lib/connector/sabre/taglist.php b/apps/dav/lib/connector/sabre/taglist.php deleted file mode 100644 index 5c1cd8b4f1d..00000000000 --- a/apps/dav/lib/connector/sabre/taglist.php +++ /dev/null @@ -1,120 +0,0 @@ - - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Connector\Sabre; - -use Sabre\Xml\Element; -use Sabre\Xml\Reader; -use Sabre\Xml\Writer; - -/** - * TagList property - * - * This property contains multiple "tag" elements, each containing a tag name. - */ -class TagList implements Element { - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - /** - * tags - * - * @var array - */ - private $tags; - - /** - * @param array $tags - */ - public function __construct(array $tags) { - $this->tags = $tags; - } - - /** - * Returns the tags - * - * @return array - */ - public function getTags() { - - return $this->tags; - - } - - /** - * The deserialize method is called during xml parsing. - * - * This method is called statictly, this is because in theory this method - * may be used as a type of constructor, or factory method. - * - * Often you want to return an instance of the current class, but you are - * free to return other data as well. - * - * You are responsible for advancing the reader to the next element. Not - * doing anything will result in a never-ending loop. - * - * If you just want to skip parsing for this element altogether, you can - * just call $reader->next(); - * - * $reader->parseInnerTree() will parse the entire sub-tree, and advance to - * the next element. - * - * @param Reader $reader - * @return mixed - */ - static function xmlDeserialize(Reader $reader) { - $tags = []; - - foreach ($reader->parseInnerTree() as $elem) { - if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}tag') { - $tags[] = $elem['value']; - } - } - return new self($tags); - } - - /** - * The xmlSerialize metod is called during xml writing. - * - * Use the $writer argument to write its own xml serialization. - * - * An important note: do _not_ create a parent element. Any element - * implementing XmlSerializble should only ever write what's considered - * its 'inner xml'. - * - * The parent of the current element is responsible for writing a - * containing element. - * - * This allows serializers to be re-used for different element names. - * - * If you are opening new elements, you must also close them again. - * - * @param Writer $writer - * @return void - */ - function xmlSerialize(Writer $writer) { - - foreach ($this->tags as $tag) { - $writer->writeElement('{' . self::NS_OWNCLOUD . '}tag', $tag); - } - } -} diff --git a/apps/dav/lib/connector/sabre/tagsplugin.php b/apps/dav/lib/connector/sabre/tagsplugin.php deleted file mode 100644 index dfc1a2dd95d..00000000000 --- a/apps/dav/lib/connector/sabre/tagsplugin.php +++ /dev/null @@ -1,293 +0,0 @@ - - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Connector\Sabre; - -/** - * ownCloud - * - * @author Vincent Petry - * @copyright 2014 Vincent Petry - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 library. If not, see . - * - */ - -use \Sabre\DAV\PropFind; -use \Sabre\DAV\PropPatch; - -class TagsPlugin extends \Sabre\DAV\ServerPlugin -{ - - // namespace - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags'; - const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite'; - const TAG_FAVORITE = '_$!!$_'; - - /** - * Reference to main server object - * - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var \OCP\ITagManager - */ - private $tagManager; - - /** - * @var \OCP\ITags - */ - private $tagger; - - /** - * Array of file id to tags array - * The null value means the cache wasn't initialized. - * - * @var array - */ - private $cachedTags; - - /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** - * @param \Sabre\DAV\Tree $tree tree - * @param \OCP\ITagManager $tagManager tag manager - */ - public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) { - $this->tree = $tree; - $this->tagManager = $tagManager; - $this->tagger = null; - $this->cachedTags = array(); - } - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - - $server->xml->namespacesMap[self::NS_OWNCLOUD] = 'oc'; - $server->xml->elementMap[self::TAGS_PROPERTYNAME] = 'OCA\\DAV\\Connector\\Sabre\\TagList'; - - $this->server = $server; - $this->server->on('propFind', array($this, 'handleGetProperties')); - $this->server->on('propPatch', array($this, 'handleUpdateProperties')); - } - - /** - * Returns the tagger - * - * @return \OCP\ITags tagger - */ - private function getTagger() { - if (!$this->tagger) { - $this->tagger = $this->tagManager->load('files'); - } - return $this->tagger; - } - - /** - * Returns tags and favorites. - * - * @param integer $fileId file id - * @return array list($tags, $favorite) with $tags as tag array - * and $favorite is a boolean whether the file was favorited - */ - private function getTagsAndFav($fileId) { - $isFav = false; - $tags = $this->getTags($fileId); - if ($tags) { - $favPos = array_search(self::TAG_FAVORITE, $tags); - if ($favPos !== false) { - $isFav = true; - unset($tags[$favPos]); - } - } - return array($tags, $isFav); - } - - /** - * Returns tags for the given file id - * - * @param integer $fileId file id - * @return array list of tags for that file - */ - private function getTags($fileId) { - if (isset($this->cachedTags[$fileId])) { - return $this->cachedTags[$fileId]; - } else { - $tags = $this->getTagger()->getTagsForObjects(array($fileId)); - if ($tags !== false) { - if (empty($tags)) { - return array(); - } - return current($tags); - } - } - return null; - } - - /** - * Updates the tags of the given file id - * - * @param int $fileId - * @param array $tags array of tag strings - */ - private function updateTags($fileId, $tags) { - $tagger = $this->getTagger(); - $currentTags = $this->getTags($fileId); - - $newTags = array_diff($tags, $currentTags); - foreach ($newTags as $tag) { - if ($tag === self::TAG_FAVORITE) { - continue; - } - $tagger->tagAs($fileId, $tag); - } - $deletedTags = array_diff($currentTags, $tags); - foreach ($deletedTags as $tag) { - if ($tag === self::TAG_FAVORITE) { - continue; - } - $tagger->unTag($fileId, $tag); - } - } - - /** - * Adds tags and favorites properties to the response, - * if requested. - * - * @param PropFind $propFind - * @param \Sabre\DAV\INode $node - * @return void - */ - public function handleGetProperties( - PropFind $propFind, - \Sabre\DAV\INode $node - ) { - if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) { - return; - } - - // need prefetch ? - if ($node instanceof \OCA\DAV\Connector\Sabre\Directory - && $propFind->getDepth() !== 0 - && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME)) - || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME)) - )) { - // note: pre-fetching only supported for depth <= 1 - $folderContent = $node->getChildren(); - $fileIds[] = (int)$node->getId(); - foreach ($folderContent as $info) { - $fileIds[] = (int)$info->getId(); - } - $tags = $this->getTagger()->getTagsForObjects($fileIds); - if ($tags === false) { - // the tags API returns false on error... - $tags = array(); - } - - $this->cachedTags = $this->cachedTags + $tags; - $emptyFileIds = array_diff($fileIds, array_keys($tags)); - // also cache the ones that were not found - foreach ($emptyFileIds as $fileId) { - $this->cachedTags[$fileId] = []; - } - } - - $tags = null; - $isFav = null; - - $propFind->handle(self::TAGS_PROPERTYNAME, function() use ($tags, &$isFav, $node) { - list($tags, $isFav) = $this->getTagsAndFav($node->getId()); - return new TagList($tags); - }); - - $propFind->handle(self::FAVORITE_PROPERTYNAME, function() use ($isFav, $node) { - if (is_null($isFav)) { - list(, $isFav) = $this->getTagsAndFav($node->getId()); - } - return $isFav; - }); - } - - /** - * Updates tags and favorites properties, if applicable. - * - * @param string $path - * @param PropPatch $propPatch - * - * @return void - */ - public function handleUpdateProperties($path, PropPatch $propPatch) { - $propPatch->handle(self::TAGS_PROPERTYNAME, function($tagList) use ($path) { - $node = $this->tree->getNodeForPath($path); - if (is_null($node)) { - return 404; - } - $this->updateTags($node->getId(), $tagList->getTags()); - return true; - }); - - $propPatch->handle(self::FAVORITE_PROPERTYNAME, function($favState) use ($path) { - $node = $this->tree->getNodeForPath($path); - if (is_null($node)) { - return 404; - } - if ((int)$favState === 1 || $favState === 'true') { - $this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE); - } else { - $this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE); - } - - if (is_null($favState)) { - // confirm deletion - return 204; - } - - return 200; - }); - } -} diff --git a/apps/dav/lib/dav/groupprincipalbackend.php b/apps/dav/lib/dav/groupprincipalbackend.php deleted file mode 100644 index e0568639784..00000000000 --- a/apps/dav/lib/dav/groupprincipalbackend.php +++ /dev/null @@ -1,200 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\DAV; - -use OCP\IGroup; -use OCP\IGroupManager; -use OCP\IUser; -use Sabre\DAV\Exception; -use \Sabre\DAV\PropPatch; -use Sabre\DAVACL\PrincipalBackend\BackendInterface; - -class GroupPrincipalBackend implements BackendInterface { - - const PRINCIPAL_PREFIX = 'principals/groups'; - - /** @var IGroupManager */ - private $groupManager; - - /** - * @param IGroupManager $IGroupManager - */ - public function __construct(IGroupManager $IGroupManager) { - $this->groupManager = $IGroupManager; - } - - /** - * 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 - * - * @param string $prefixPath - * @return string[] - */ - public function getPrincipalsByPrefix($prefixPath) { - $principals = []; - - if ($prefixPath === self::PRINCIPAL_PREFIX) { - foreach($this->groupManager->search('') as $user) { - $principals[] = $this->groupToPrincipal($user); - } - } - - 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) { - $elements = explode('/', $path); - if ($elements[0] !== 'principals') { - return null; - } - if ($elements[1] !== 'groups') { - return null; - } - $name = $elements[2]; - $group = $this->groupManager->get($name); - - if (!is_null($group)) { - return $this->groupToPrincipal($group); - } - - return null; - } - - /** - * Returns the list of members for a group-principal - * - * @param string $principal - * @return string[] - * @throws Exception - */ - public function getGroupMemberSet($principal) { - $elements = explode('/', $principal); - if ($elements[0] !== 'principals') { - return []; - } - if ($elements[1] !== 'groups') { - return []; - } - $name = $elements[2]; - $group = $this->groupManager->get($name); - - if (is_null($group)) { - return []; - } - - return array_map(function($user) { - return $this->userToPrincipal($user); - }, $group->getUsers()); - } - - /** - * Returns the list of groups a principal is a member of - * - * @param string $principal - * @return array - * @throws Exception - */ - public function getGroupMembership($principal) { - return []; - } - - /** - * 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 string[] $members - * @throws Exception - */ - public function setGroupMemberSet($principal, array $members) { - throw new Exception('Setting members of the group is not supported yet'); - } - - /** - * @param string $path - * @param PropPatch $propPatch - * @return int - */ - function updatePrincipal($path, PropPatch $propPatch) { - return 0; - } - - /** - * @param string $prefixPath - * @param array $searchProperties - * @param string $test - * @return array - */ - function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { - return []; - } - - /** - * @param string $uri - * @param string $principalPrefix - * @return string - */ - function findByUri($uri, $principalPrefix) { - return ''; - } - - /** - * @param IGroup $group - * @return array - */ - protected function groupToPrincipal($group) { - $groupId = $group->getGID(); - $principal = [ - 'uri' => "principals/groups/$groupId", - '{DAV:}displayname' => $groupId, - ]; - - return $principal; - } - - /** - * @param IUser $user - * @return array - */ - protected function userToPrincipal($user) { - $principal = [ - 'uri' => 'principals/users/' . $user->getUID(), - '{DAV:}displayname' => $user->getDisplayName(), - ]; - - return $principal; - } -} diff --git a/apps/dav/lib/dav/sharing/backend.php b/apps/dav/lib/dav/sharing/backend.php deleted file mode 100644 index 225b773713d..00000000000 --- a/apps/dav/lib/dav/sharing/backend.php +++ /dev/null @@ -1,206 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\DAV\Sharing; - -use OCA\DAV\Connector\Sabre\Principal; -use OCP\IDBConnection; - -class Backend { - - /** @var IDBConnection */ - private $db; - /** @var Principal */ - private $principalBackend; - /** @var string */ - private $resourceType; - - const ACCESS_OWNER = 1; - const ACCESS_READ_WRITE = 2; - const ACCESS_READ = 3; - - /** - * @param IDBConnection $db - * @param Principal $principalBackend - * @param string $resourceType - */ - public function __construct(IDBConnection $db, Principal $principalBackend, $resourceType) { - $this->db = $db; - $this->principalBackend = $principalBackend; - $this->resourceType = $resourceType; - } - - /** - * @param IShareable $shareable - * @param string[] $add - * @param string[] $remove - */ - public function updateShares($shareable, $add, $remove) { - foreach($add as $element) { - $this->shareWith($shareable, $element); - } - foreach($remove as $element) { - $this->unshare($shareable, $element); - } - } - - /** - * @param IShareable $shareable - * @param string $element - */ - private function shareWith($shareable, $element) { - $user = $element['href']; - $parts = explode(':', $user, 2); - if ($parts[0] !== 'principal') { - return; - } - - // don't share with owner - if ($shareable->getOwner() === $parts[1]) { - return; - } - - // remove the share if it already exists - $this->unshare($shareable, $element['href']); - $access = self::ACCESS_READ; - if (isset($element['readOnly'])) { - $access = $element['readOnly'] ? self::ACCESS_READ : self::ACCESS_READ_WRITE; - } - - $query = $this->db->getQueryBuilder(); - $query->insert('dav_shares') - ->values([ - 'principaluri' => $query->createNamedParameter($parts[1]), - 'type' => $query->createNamedParameter($this->resourceType), - 'access' => $query->createNamedParameter($access), - 'resourceid' => $query->createNamedParameter($shareable->getResourceId()) - ]); - $query->execute(); - } - - /** - * @param $resourceId - */ - public function deleteAllShares($resourceId) { - $query = $this->db->getQueryBuilder(); - $query->delete('dav_shares') - ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId))) - ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) - ->execute(); - } - - /** - * @param IShareable $shareable - * @param string $element - */ - private function unshare($shareable, $element) { - $parts = explode(':', $element, 2); - if ($parts[0] !== 'principal') { - return; - } - - // don't share with owner - if ($shareable->getOwner() === $parts[1]) { - return; - } - - $query = $this->db->getQueryBuilder(); - $query->delete('dav_shares') - ->where($query->expr()->eq('resourceid', $query->createNamedParameter($shareable->getResourceId()))) - ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) - ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($parts[1]))) - ; - $query->execute(); - } - - /** - * Returns the list of people whom this resource is shared with. - * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share - * - * @param int $resourceId - * @return array - */ - public function getShares($resourceId) { - $query = $this->db->getQueryBuilder(); - $result = $query->select(['principaluri', 'access']) - ->from('dav_shares') - ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId))) - ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) - ->execute(); - - $shares = []; - while($row = $result->fetch()) { - $p = $this->principalBackend->getPrincipalByPath($row['principaluri']); - $shares[]= [ - 'href' => "principal:${row['principaluri']}", - 'commonName' => isset($p['{DAV:}displayname']) ? $p['{DAV:}displayname'] : '', - 'status' => 1, - 'readOnly' => ($row['access'] == self::ACCESS_READ), - '{http://owncloud.org/ns}principal' => $row['principaluri'], - '{http://owncloud.org/ns}group-share' => is_null($p) - ]; - } - - return $shares; - } - - /** - * For shared resources the sharee is set in the ACL of the resource - * - * @param int $resourceId - * @param array $acl - * @return array - */ - public function applyShareAcl($resourceId, $acl) { - - $shares = $this->getShares($resourceId); - foreach ($shares as $share) { - $acl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'], - 'protected' => true, - ]; - if (!$share['readOnly']) { - $acl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'], - 'protected' => true, - ]; - } else if ($this->resourceType === 'calendar') { - // Allow changing the properties of read only calendars, - // so users can change the visibility. - $acl[] = [ - 'privilege' => '{DAV:}write-properties', - 'principal' => $share['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}principal'], - 'protected' => true, - ]; - } - } - return $acl; - } -} diff --git a/apps/dav/lib/dav/sharing/ishareable.php b/apps/dav/lib/dav/sharing/ishareable.php deleted file mode 100644 index f6b6bfa8862..00000000000 --- a/apps/dav/lib/dav/sharing/ishareable.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\DAV\Sharing; -use Sabre\DAV\INode; - -/** - * This interface represents a dav resource that can be shared with other users. - * - */ -interface IShareable extends INode { - - /** - * Updates the list of shares. - * - * The first array is a list of people that are to be added to the - * resource. - * - * 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 resource is shared with. - * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share - * - * @return array - */ - function getShares(); - - /** - * @return int - */ - public function getResourceId(); - - /** - * @return string - */ - public function getOwner(); - -} \ No newline at end of file diff --git a/apps/dav/lib/dav/sharing/plugin.php b/apps/dav/lib/dav/sharing/plugin.php deleted file mode 100644 index 3173d1397ba..00000000000 --- a/apps/dav/lib/dav/sharing/plugin.php +++ /dev/null @@ -1,200 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\DAV\Sharing; - -use OCA\DAV\Connector\Sabre\Auth; -use OCA\DAV\DAV\Sharing\Xml\Invite; -use OCP\IRequest; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\INode; -use Sabre\DAV\PropFind; -use Sabre\DAV\Server; -use Sabre\DAV\ServerPlugin; -use Sabre\HTTP\RequestInterface; -use Sabre\HTTP\ResponseInterface; - -class Plugin extends ServerPlugin { - - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - - /** @var Auth */ - private $auth; - - /** @var IRequest */ - private $request; - - /** - * Plugin constructor. - * - * @param Auth $authBackEnd - * @param IRequest $request - */ - public function __construct(Auth $authBackEnd, IRequest $request) { - $this->auth = $authBackEnd; - $this->request = $request; - } - - /** - * Reference to SabreDAV server object. - * - * @var \Sabre\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 string[] - */ - function getFeatures() { - return ['oc-resource-sharing']; - } - - /** - * Returns a plugin name. - * - * Using this name other plugins will be able to access other plugins - * using Sabre\DAV\Server::getPlugin - * - * @return string - */ - function getPluginName() { - return 'oc-resource-sharing'; - } - - /** - * This initializes the plugin. - * - * This function is called by Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param Server $server - * @return void - */ - function initialize(Server $server) { - $this->server = $server; - $this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}share'] = 'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest'; - $this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}invite'] = 'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite'; - - $this->server->on('method:POST', [$this, 'httpPost']); - $this->server->on('propFind', [$this, 'propFind']); - } - - /** - * We intercept this to handle POST requests on a dav resource. - * - * @param RequestInterface $request - * @param ResponseInterface $response - * @return null|false - */ - function httpPost(RequestInterface $request, ResponseInterface $response) { - - $path = $request->getPath(); - - // Only handling xml - $contentType = $request->getHeader('Content-Type'); - if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) - return; - - // Making sure the node exists - try { - $node = $this->server->tree->getNodeForPath($path); - } catch (NotFound $e) { - return; - } - - $requestBody = $request->getBodyAsString(); - - // If this request handler could not deal with this POST request, it - // will return 'null' and other plugins get a chance to handle the - // request. - // - // However, we already requested the full body. This is a problem, - // because a body can only be read once. This is why we preemptively - // re-populated the request body with the existing data. - $request->setBody($requestBody); - - $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); - - switch ($documentType) { - - // Dealing with the 'share' document, which modified invitees on a - // calendar. - case '{' . self::NS_OWNCLOUD . '}share' : - - // We can only deal with IShareableCalendar objects - if (!$node instanceof IShareable) { - return; - } - - $this->server->transactionType = 'post-oc-resource-share'; - - // Getting ACL info - $acl = $this->server->getPlugin('acl'); - - // If there's no ACL support, we allow everything - if ($acl) { - /** @var \Sabre\DAVACL\Plugin $acl */ - $acl->checkPrivileges($path, '{DAV:}write'); - } - - $node->updateShares($message->set, $message->remove); - - $response->setStatus(200); - // Adding this because sending a response body may cause issues, - // and I wanted some type of indicator the response was handled. - $response->setHeader('X-Sabre-Status', 'everything-went-well'); - - // Breaking the event chain - return false; - } - } - - /** - * This event is triggered when properties are requested for a certain - * node. - * - * This allows us to inject any properties early. - * - * @param PropFind $propFind - * @param INode $node - * @return void - */ - function propFind(PropFind $propFind, INode $node) { - if ($node instanceof IShareable) { - - $propFind->handle('{' . Plugin::NS_OWNCLOUD . '}invite', function() use ($node) { - return new Invite( - $node->getShares() - ); - }); - - } - } - -} diff --git a/apps/dav/lib/dav/sharing/xml/invite.php b/apps/dav/lib/dav/sharing/xml/invite.php deleted file mode 100644 index ae40ff06701..00000000000 --- a/apps/dav/lib/dav/sharing/xml/invite.php +++ /dev/null @@ -1,168 +0,0 @@ - - * @author Thomas Müller - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\DAV\Sharing\Xml; - -use OCA\DAV\DAV\Sharing\Plugin; -use Sabre\Xml\Writer; -use Sabre\Xml\XmlSerializable; - -/** - * 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) fruux GmbH (https://fruux.com/) - * @author Evert Pot (http://evertpot.com/) - * @license http://sabre.io/license/ Modified BSD License - */ -class Invite implements XmlSerializable { - - /** - * 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 - */ - 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 - */ - function getValue() { - - return $this->users; - - } - - /** - * The xmlSerialize metod is called during xml writing. - * - * Use the $writer argument to write its own xml serialization. - * - * An important note: do _not_ create a parent element. Any element - * implementing XmlSerializble should only ever write what's considered - * its 'inner xml'. - * - * The parent of the current element is responsible for writing a - * containing element. - * - * This allows serializers to be re-used for different element names. - * - * If you are opening new elements, you must also close them again. - * - * @param Writer $writer - * @return void - */ - function xmlSerialize(Writer $writer) { - - $cs = '{' . Plugin::NS_OWNCLOUD . '}'; - - if (!is_null($this->organizer)) { - - $writer->startElement($cs . 'organizer'); - $writer->writeElement('{DAV:}href', $this->organizer['href']); - - if (isset($this->organizer['commonName']) && $this->organizer['commonName']) { - $writer->writeElement($cs . 'common-name', $this->organizer['commonName']); - } - if (isset($this->organizer['firstName']) && $this->organizer['firstName']) { - $writer->writeElement($cs . 'first-name', $this->organizer['firstName']); - } - if (isset($this->organizer['lastName']) && $this->organizer['lastName']) { - $writer->writeElement($cs . 'last-name', $this->organizer['lastName']); - } - $writer->endElement(); // organizer - - } - - foreach ($this->users as $user) { - - $writer->startElement($cs . 'user'); - $writer->writeElement('{DAV:}href', $user['href']); - if (isset($user['commonName']) && $user['commonName']) { - $writer->writeElement($cs . 'common-name', $user['commonName']); - } - $writer->writeElement($cs . 'invite-accepted'); - - $writer->startElement($cs . 'access'); - if ($user['readOnly']) { - $writer->writeElement($cs . 'read'); - } else { - $writer->writeElement($cs . 'read-write'); - } - $writer->endElement(); // access - - if (isset($user['summary']) && $user['summary']) { - $writer->writeElement($cs . 'summary', $user['summary']); - } - - $writer->endElement(); //user - - } - - } -} diff --git a/apps/dav/lib/dav/sharing/xml/sharerequest.php b/apps/dav/lib/dav/sharing/xml/sharerequest.php deleted file mode 100644 index 776fb446b6c..00000000000 --- a/apps/dav/lib/dav/sharing/xml/sharerequest.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\DAV\Sharing\Xml; - -use OCA\DAV\DAV\Sharing\Plugin; -use Sabre\Xml\Reader; -use Sabre\Xml\XmlDeserializable; - -class ShareRequest implements XmlDeserializable { - - public $set = []; - - public $remove = []; - - /** - * Constructor - * - * @param array $set - * @param array $remove - */ - function __construct(array $set, array $remove) { - - $this->set = $set; - $this->remove = $remove; - - } - - static function xmlDeserialize(Reader $reader) { - - $elements = $reader->parseInnerTree([ - '{' . Plugin::NS_OWNCLOUD. '}set' => 'Sabre\\Xml\\Element\\KeyValue', - '{' . Plugin::NS_OWNCLOUD . '}remove' => 'Sabre\\Xml\\Element\\KeyValue', - ]); - - $set = []; - $remove = []; - - foreach ($elements as $elem) { - switch ($elem['name']) { - - case '{' . Plugin::NS_OWNCLOUD . '}set' : - $sharee = $elem['value']; - - $sumElem = '{' . Plugin::NS_OWNCLOUD . '}summary'; - $commonName = '{' . Plugin::NS_OWNCLOUD . '}common-name'; - - $set[] = [ - 'href' => $sharee['{DAV:}href'], - 'commonName' => isset($sharee[$commonName]) ? $sharee[$commonName] : null, - 'summary' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null, - 'readOnly' => !array_key_exists('{' . Plugin::NS_OWNCLOUD . '}read-write', $sharee), - ]; - break; - - case '{' . Plugin::NS_OWNCLOUD . '}remove' : - $remove[] = $elem['value']['{DAV:}href']; - break; - - } - } - - return new self($set, $remove); - - } - -} diff --git a/apps/dav/lib/dav/systemprincipalbackend.php b/apps/dav/lib/dav/systemprincipalbackend.php deleted file mode 100644 index 8001ec4e6c6..00000000000 --- a/apps/dav/lib/dav/systemprincipalbackend.php +++ /dev/null @@ -1,179 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\DAV; - -use Sabre\DAVACL\PrincipalBackend\AbstractBackend; -use Sabre\HTTP\URLUtil; - -class SystemPrincipalBackend extends AbstractBackend { - - /** - * 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) { - $principals = []; - - if ($prefixPath === 'principals/system') { - $principals[] = [ - 'uri' => 'principals/system/system', - '{DAV:}displayname' => 'system', - ]; - } - - return $principals; - } - - /** - * Returns a specific principal, specified by it's path. - * The returned structure should be the exact same as from - * getPrincipalsByPrefix. - * - * @param string $path - * @return array - */ - function getPrincipalByPath($path) { - - if ($path === 'principals/system/system') { - $principal = [ - 'uri' => 'principals/system/system', - '{DAV:}displayname' => 'system', - ]; - return $principal; - } - - return null; - } - - /** - * Updates one ore more webdav properties on a principal. - * - * The list of mutations is stored in a Sabre\DAV\PropPatch object. - * To do the actual updates, you must tell this object which properties - * you're going to process with the handle() method. - * - * Calling the handle method is like telling the PropPatch object "I - * promise I can handle updating this property". - * - * Read the PropPatch documentation for more info and examples. - * - * @param string $path - * @param \Sabre\DAV\PropPatch $propPatch - * @return void - */ - function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) { - } - - /** - * This method is used to search for principals matching a set of - * properties. - * - * This search is specifically used by RFC3744's principal-property-search - * REPORT. - * - * The actual search should be a unicode-non-case-sensitive search. The - * keys in searchProperties are the WebDAV property names, while the values - * are the property values to search on. - * - * By default, if multiple properties are submitted to this method, the - * various properties should be combined with 'AND'. If $test is set to - * 'anyof', it should be combined using 'OR'. - * - * This method should simply return an array with full principal uri's. - * - * If somebody attempted to search on a property the backend does not - * support, you should simply return 0 results. - * - * You can also just return 0 results if you choose to not support - * searching at all, but keep in mind that this may stop certain features - * from working. - * - * @param string $prefixPath - * @param array $searchProperties - * @param string $test - * @return array - */ - function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { - return []; - } - - /** - * Returns the list of members for a group-principal - * - * @param string $principal - * @return array - */ - function getGroupMemberSet($principal) { - // TODO: for now the group principal has only one member, the user itself - $principal = $this->getPrincipalByPath($principal); - if (!$principal) { - throw new \Sabre\DAV\Exception('Principal not found'); - } - - return [$principal['uri']]; - } - - /** - * Returns the list of groups a principal is a member of - * - * @param string $principal - * @return array - */ - function getGroupMembership($principal) { - list($prefix, $name) = URLUtil::splitPath($principal); - - if ($prefix === 'principals/system') { - $principal = $this->getPrincipalByPath($principal); - if (!$principal) { - throw new \Sabre\DAV\Exception('Principal not found'); - } - - return []; - } - return []; - } - - /** - * 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) { - throw new \Sabre\DAV\Exception('Setting members of the group is not supported yet'); - } -} diff --git a/apps/dav/lib/files/browsererrorpageplugin.php b/apps/dav/lib/files/browsererrorpageplugin.php deleted file mode 100644 index 37a4166efef..00000000000 --- a/apps/dav/lib/files/browsererrorpageplugin.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Files; - -use OC\AppFramework\Http\Request; -use OC_Template; -use OCP\IRequest; -use Sabre\DAV\Exception; -use Sabre\DAV\Server; -use Sabre\DAV\ServerPlugin; - -class BrowserErrorPagePlugin extends ServerPlugin { - - /** @var Server */ - private $server; - - /** - * This initializes the plugin. - * - * This function is called by Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param Server $server - * @return void - */ - function initialize(Server $server) { - $this->server = $server; - $server->on('exception', array($this, 'logException'), 1000); - } - - /** - * @param IRequest $request - * @return bool - */ - public static function isBrowserRequest(IRequest $request) { - if ($request->getMethod() !== 'GET') { - return false; - } - return $request->isUserAgent([ - Request::USER_AGENT_IE, - Request::USER_AGENT_MS_EDGE, - Request::USER_AGENT_CHROME, - Request::USER_AGENT_FIREFOX, - Request::USER_AGENT_SAFARI, - ]); - } - - /** - * @param \Exception $ex - */ - public function logException(\Exception $ex) { - if ($ex instanceof Exception) { - $httpCode = $ex->getHTTPCode(); - $headers = $ex->getHTTPHeaders($this->server); - } else { - $httpCode = 500; - $headers = []; - } - $this->server->httpResponse->addHeaders($headers); - $this->server->httpResponse->setStatus($httpCode); - $body = $this->generateBody($ex); - $this->server->httpResponse->setBody($body); - $this->sendResponse(); - } - - /** - * @codeCoverageIgnore - * @param \Exception $ex - * @param int $httpCode - * @return bool|string - */ - public function generateBody(\Exception $exception) { - $request = \OC::$server->getRequest(); - $content = new OC_Template('dav', 'exception', 'guest'); - $content->assign('title', $this->server->httpResponse->getStatusText()); - $content->assign('message', $exception->getMessage()); - $content->assign('errorClass', get_class($exception)); - $content->assign('errorMsg', $exception->getMessage()); - $content->assign('errorCode', $exception->getCode()); - $content->assign('file', $exception->getFile()); - $content->assign('line', $exception->getLine()); - $content->assign('trace', $exception->getTraceAsString()); - $content->assign('debugMode', \OC::$server->getSystemConfig()->getValue('debug', false)); - $content->assign('remoteAddr', $request->getRemoteAddress()); - $content->assign('requestID', $request->getId()); - return $content->fetchPage(); - } - - /* - * @codeCoverageIgnore - */ - public function sendResponse() { - $this->server->sapi->sendResponse($this->server->httpResponse); - } -} diff --git a/apps/dav/lib/files/custompropertiesbackend.php b/apps/dav/lib/files/custompropertiesbackend.php deleted file mode 100644 index aa541f88dad..00000000000 --- a/apps/dav/lib/files/custompropertiesbackend.php +++ /dev/null @@ -1,267 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Files; - -use OCP\IDBConnection; -use OCP\IUser; -use Sabre\DAV\PropertyStorage\Backend\BackendInterface; -use Sabre\DAV\PropFind; -use Sabre\DAV\PropPatch; -use Sabre\DAV\Tree; - -class CustomPropertiesBackend implements BackendInterface { - - /** - * Ignored properties - * - * @var array - */ - private $ignoredProperties = array( - '{DAV:}getcontentlength', - '{DAV:}getcontenttype', - '{DAV:}getetag', - '{DAV:}quota-used-bytes', - '{DAV:}quota-available-bytes', - '{http://owncloud.org/ns}permissions', - '{http://owncloud.org/ns}downloadURL', - '{http://owncloud.org/ns}dDC', - '{http://owncloud.org/ns}size', - ); - - /** - * @var Tree - */ - private $tree; - - /** - * @var IDBConnection - */ - private $connection; - - /** - * @var IUser - */ - private $user; - - /** - * Properties cache - * - * @var array - */ - private $cache = []; - - /** - * @param Tree $tree node tree - * @param IDBConnection $connection database connection - * @param IUser $user owner of the tree and properties - */ - public function __construct( - Tree $tree, - IDBConnection $connection, - IUser $user) { - $this->tree = $tree; - $this->connection = $connection; - $this->user = $user->getUID(); - } - - /** - * Fetches properties for a path. - * - * @param string $path - * @param PropFind $propFind - * @return void - */ - public function propFind($path, PropFind $propFind) { - - $requestedProps = $propFind->get404Properties(); - - // these might appear - $requestedProps = array_diff( - $requestedProps, - $this->ignoredProperties - ); - - if (empty($requestedProps)) { - return; - } - - $props = $this->getProperties($path, $requestedProps); - foreach ($props as $propName => $propValue) { - $propFind->set($propName, $propValue); - } - } - - /** - * Updates properties for a path - * - * @param string $path - * @param PropPatch $propPatch - * - * @return void - */ - public function propPatch($path, PropPatch $propPatch) { - $propPatch->handleRemaining(function($changedProps) use ($path) { - return $this->updateProperties($path, $changedProps); - }); - } - - /** - * This method is called after a node is deleted. - * - * @param string $path path of node for which to delete properties - */ - public function delete($path) { - $statement = $this->connection->prepare( - 'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?' - ); - $statement->execute(array($this->user, $path)); - $statement->closeCursor(); - - unset($this->cache[$path]); - } - - /** - * This method is called after a successful MOVE - * - * @param string $source - * @param string $destination - * - * @return void - */ - public function move($source, $destination) { - $statement = $this->connection->prepare( - 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' . - ' WHERE `userid` = ? AND `propertypath` = ?' - ); - $statement->execute(array($destination, $this->user, $source)); - $statement->closeCursor(); - } - - /** - * Returns a list of properties for this nodes.; - * @param string $path - * @param array $requestedProperties requested properties or empty array for "all" - * @return array - * @note 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 - */ - private function getProperties($path, array $requestedProperties) { - if (isset($this->cache[$path])) { - return $this->cache[$path]; - } - - // TODO: chunking if more than 1000 properties - $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'; - - $whereValues = array($this->user, $path); - $whereTypes = array(null, null); - - if (!empty($requestedProperties)) { - // request only a subset - $sql .= ' AND `propertyname` in (?)'; - $whereValues[] = $requestedProperties; - $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY; - } - - $result = $this->connection->executeQuery( - $sql, - $whereValues, - $whereTypes - ); - - $props = []; - while ($row = $result->fetch()) { - $props[$row['propertyname']] = $row['propertyvalue']; - } - - $result->closeCursor(); - - $this->cache[$path] = $props; - return $props; - } - - /** - * Update properties - * - * @param string $path node for which to update properties - * @param array $properties array of properties to update - * - * @return bool - */ - private function updateProperties($path, $properties) { - - $deleteStatement = 'DELETE FROM `*PREFIX*properties`' . - ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; - - $insertStatement = 'INSERT INTO `*PREFIX*properties`' . - ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)'; - - $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' . - ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; - - // TODO: use "insert or update" strategy ? - $existing = $this->getProperties($path, array()); - $this->connection->beginTransaction(); - foreach ($properties as $propertyName => $propertyValue) { - // If it was null, we need to delete the property - if (is_null($propertyValue)) { - if (array_key_exists($propertyName, $existing)) { - $this->connection->executeUpdate($deleteStatement, - array( - $this->user, - $path, - $propertyName - ) - ); - } - } else { - if (!array_key_exists($propertyName, $existing)) { - $this->connection->executeUpdate($insertStatement, - array( - $this->user, - $path, - $propertyName, - $propertyValue - ) - ); - } else { - $this->connection->executeUpdate($updateStatement, - array( - $propertyValue, - $this->user, - $path, - $propertyName - ) - ); - } - } - } - - $this->connection->commit(); - unset($this->cache[$path]); - - return true; - } - -} diff --git a/apps/dav/lib/files/fileshome.php b/apps/dav/lib/files/fileshome.php deleted file mode 100644 index ef572d6618b..00000000000 --- a/apps/dav/lib/files/fileshome.php +++ /dev/null @@ -1,103 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Files; - -use OCA\DAV\Connector\Sabre\Directory; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\ICollection; -use Sabre\DAV\SimpleCollection; -use Sabre\HTTP\URLUtil; - -class FilesHome implements ICollection { - - /** - * @var array - */ - private $principalInfo; - - /** - * FilesHome constructor. - * - * @param array $principalInfo - */ - public function __construct($principalInfo) { - $this->principalInfo = $principalInfo; - } - - function createFile($name, $data = null) { - return $this->impl()->createFile($name, $data); - } - - function createDirectory($name) { - $this->impl()->createDirectory($name); - } - - function getChild($name) { - return $this->impl()->getChild($name); - } - - function getChildren() { - return $this->impl()->getChildren(); - } - - function childExists($name) { - return $this->impl()->childExists($name); - } - - function delete() { - $this->impl()->delete(); - } - - function getName() { - list(,$name) = URLUtil::splitPath($this->principalInfo['uri']); - return $name; - } - - function setName($name) { - throw new Forbidden('Permission denied to rename this folder'); - } - - /** - * Returns the last modification time, as a unix timestamp - * - * @return int - */ - function getLastModified() { - return $this->impl()->getLastModified(); - } - - /** - * @return Directory - */ - private function impl() { - // - // TODO: we need to mount filesystem of the give user - // - $user = \OC::$server->getUserSession()->getUser(); - if ($this->getName() !== $user->getUID()) { - return new SimpleCollection($this->getName()); - } - $view = \OC\Files\Filesystem::getView(); - $rootInfo = $view->getFileInfo(''); - $impl = new Directory($view, $rootInfo); - return $impl; - } -} diff --git a/apps/dav/lib/files/rootcollection.php b/apps/dav/lib/files/rootcollection.php deleted file mode 100644 index 63328aac8e3..00000000000 --- a/apps/dav/lib/files/rootcollection.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\Files; - -use Sabre\DAVACL\AbstractPrincipalCollection; -use Sabre\DAVACL\IPrincipal; - -class RootCollection 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 $principalInfo - * @return IPrincipal - */ - function getChildForPrincipal(array $principalInfo) { - return new FilesHome($principalInfo); - } - - function getName() { - return 'files'; - } - -} diff --git a/apps/dav/lib/files/sharing/publiclinkcheckplugin.php b/apps/dav/lib/files/sharing/publiclinkcheckplugin.php deleted file mode 100644 index 751f63d394e..00000000000 --- a/apps/dav/lib/files/sharing/publiclinkcheckplugin.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\Files\Sharing; - -use OCP\Files\FileInfo; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\ServerPlugin; -use Sabre\HTTP\RequestInterface; -use Sabre\HTTP\ResponseInterface; - -/** - * Verify that the public link share is valid - */ -class PublicLinkCheckPlugin extends ServerPlugin { - /** - * @var FileInfo - */ - private $fileInfo; - - /** - * @param FileInfo $fileInfo - */ - public function setFileInfo($fileInfo) { - $this->fileInfo = $fileInfo; - } - - /** - * This initializes the plugin. - * - * @param \Sabre\DAV\Server $server Sabre server - * - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - $server->on('beforeMethod', [$this, 'beforeMethod']); - } - - public function beforeMethod(RequestInterface $request, ResponseInterface $response){ - // verify that the owner didn't have his share permissions revoked - if ($this->fileInfo && !$this->fileInfo->isShareable()) { - throw new NotFound(); - } - } -} diff --git a/apps/dav/lib/hookmanager.php b/apps/dav/lib/hookmanager.php deleted file mode 100644 index aa3777088f8..00000000000 --- a/apps/dav/lib/hookmanager.php +++ /dev/null @@ -1,126 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV; - -use OCA\DAV\CalDAV\BirthdayService; -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\CardDAV\SyncService; -use OCP\IUser; -use OCP\IUserManager; -use OCP\Util; - -class HookManager { - - /** @var IUserManager */ - private $userManager; - - /** @var SyncService */ - private $syncService; - - /** @var IUser[] */ - private $usersToDelete; - - /** @var CalDavBackend */ - private $calDav; - - /** @var CardDavBackend */ - private $cardDav; - - public function __construct(IUserManager $userManager, - SyncService $syncService, - CalDavBackend $calDav, - CardDavBackend $cardDav) { - $this->userManager = $userManager; - $this->syncService = $syncService; - $this->calDav = $calDav; - $this->cardDav = $cardDav; - } - - public function setup() { - Util::connectHook('OC_User', - 'post_createUser', - $this, - 'postCreateUser'); - Util::connectHook('OC_User', - 'pre_deleteUser', - $this, - 'preDeleteUser'); - Util::connectHook('OC_User', - 'post_deleteUser', - $this, - 'postDeleteUser'); - Util::connectHook('OC_User', - 'changeUser', - $this, - 'changeUser'); - Util::connectHook('OC_User', - 'post_login', - $this, - 'postLogin'); - } - - public function postCreateUser($params) { - $user = $this->userManager->get($params['uid']); - $this->syncService->updateUser($user); - } - - public function preDeleteUser($params) { - $this->usersToDelete[$params['uid']] = $this->userManager->get($params['uid']); - } - - public function postDeleteUser($params) { - $uid = $params['uid']; - if (isset($this->usersToDelete[$uid])){ - $this->syncService->deleteUser($this->usersToDelete[$uid]); - } - } - - public function changeUser($params) { - $user = $params['user']; - $this->syncService->updateUser($user); - } - - public function postLogin($params) { - $user = $this->userManager->get($params['uid']); - if (!is_null($user)) { - $principal = 'principals/users/' . $user->getUID(); - $calendars = $this->calDav->getCalendarsForUser($principal); - if (empty($calendars) || (count($calendars) === 1 && $calendars[0]['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI)) { - try { - $this->calDav->createCalendar($principal, 'personal', [ - '{DAV:}displayname' => 'Personal']); - } catch (\Exception $ex) { - \OC::$server->getLogger()->logException($ex); - } - } - $books = $this->cardDav->getAddressBooksForUser($principal); - if (empty($books)) { - try { - $this->cardDav->createAddressBook($principal, 'contacts', [ - '{DAV:}displayname' => 'Contacts']); - } catch (\Exception $ex) { - \OC::$server->getLogger()->logException($ex); - } - } - } - } -} diff --git a/apps/dav/lib/rootcollection.php b/apps/dav/lib/rootcollection.php deleted file mode 100644 index b6e1747e990..00000000000 --- a/apps/dav/lib/rootcollection.php +++ /dev/null @@ -1,114 +0,0 @@ - - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV; - -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CalDAV\CalendarRoot; -use OCA\DAV\CardDAV\AddressBookRoot; -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\Connector\Sabre\Principal; -use OCA\DAV\DAV\GroupPrincipalBackend; -use OCA\DAV\DAV\SystemPrincipalBackend; -use Sabre\CalDAV\Principal\Collection; -use Sabre\DAV\SimpleCollection; - -class RootCollection extends SimpleCollection { - - public function __construct() { - $config = \OC::$server->getConfig(); - $db = \OC::$server->getDatabaseConnection(); - $dispatcher = \OC::$server->getEventDispatcher(); - $userPrincipalBackend = new Principal( - \OC::$server->getUserManager(), - \OC::$server->getGroupManager() - ); - $groupPrincipalBackend = new GroupPrincipalBackend( - \OC::$server->getGroupManager() - ); - // as soon as debug mode is enabled we allow listing of principals - $disableListing = !$config->getSystemValue('debug', false); - - // setup the first level of the dav tree - $userPrincipals = new Collection($userPrincipalBackend, 'principals/users'); - $userPrincipals->disableListing = $disableListing; - $groupPrincipals = new Collection($groupPrincipalBackend, 'principals/groups'); - $groupPrincipals->disableListing = $disableListing; - $systemPrincipals = new Collection(new SystemPrincipalBackend(), 'principals/system'); - $systemPrincipals->disableListing = $disableListing; - $filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users'); - $filesCollection->disableListing = $disableListing; - $caldavBackend = new CalDavBackend($db, $userPrincipalBackend); - $calendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users'); - $calendarRoot->disableListing = $disableListing; - - $systemTagCollection = new SystemTag\SystemTagsByIdCollection( - \OC::$server->getSystemTagManager(), - \OC::$server->getUserSession(), - \OC::$server->getGroupManager() - ); - $systemTagRelationsCollection = new SystemTag\SystemTagsRelationsCollection( - \OC::$server->getSystemTagManager(), - \OC::$server->getSystemTagObjectMapper(), - \OC::$server->getUserSession(), - \OC::$server->getGroupManager(), - \OC::$server->getRootFolder() - ); - $commentsCollection = new Comments\RootCollection( - \OC::$server->getCommentsManager(), - \OC::$server->getUserManager(), - \OC::$server->getUserSession(), - \OC::$server->getRootFolder(), - \OC::$server->getLogger() - ); - - $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $dispatcher); - $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, 'principals/users'); - $usersAddressBookRoot->disableListing = $disableListing; - - $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $dispatcher); - $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system'); - $systemAddressBookRoot->disableListing = $disableListing; - - $uploadCollection = new Upload\RootCollection($userPrincipalBackend, 'principals/users'); - $uploadCollection->disableListing = $disableListing; - - $children = [ - new SimpleCollection('principals', [ - $userPrincipals, - $groupPrincipals, - $systemPrincipals]), - $filesCollection, - $calendarRoot, - new SimpleCollection('addressbooks', [ - $usersAddressBookRoot, - $systemAddressBookRoot]), - $systemTagCollection, - $systemTagRelationsCollection, - $commentsCollection, - $uploadCollection, - ]; - - parent::__construct('root', $children); - } - -} diff --git a/apps/dav/lib/server.php b/apps/dav/lib/server.php deleted file mode 100644 index edaa7ac8552..00000000000 --- a/apps/dav/lib/server.php +++ /dev/null @@ -1,164 +0,0 @@ - - * @author Lukas Reschke - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV; - -use OCA\DAV\CalDAV\Schedule\IMipPlugin; -use OCA\DAV\Connector\FedAuth; -use OCA\DAV\Connector\Sabre\Auth; -use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; -use OCA\DAV\Connector\Sabre\DavAclPlugin; -use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin; -use OCA\DAV\Connector\Sabre\FilesPlugin; -use OCA\DAV\Files\BrowserErrorPagePlugin; -use OCA\DAV\Files\CustomPropertiesBackend; -use OCP\IRequest; -use OCP\SabrePluginEvent; -use Sabre\CardDAV\VCFExportPlugin; -use Sabre\DAV\Auth\Plugin; - -class Server { - - /** @var IRequest */ - private $request; - - public function __construct(IRequest $request, $baseUri) { - $this->request = $request; - $this->baseUri = $baseUri; - $logger = \OC::$server->getLogger(); - $mailer = \OC::$server->getMailer(); - $dispatcher = \OC::$server->getEventDispatcher(); - - $root = new RootCollection(); - $this->server = new \OCA\DAV\Connector\Sabre\Server($root); - - // Backends - $authBackend = new Auth( - \OC::$server->getSession(), - \OC::$server->getUserSession(), - \OC::$server->getRequest() - ); - - // Set URL explicitly due to reverse-proxy situations - $this->server->httpRequest->setUrl($this->request->getRequestUri()); - $this->server->setBaseUri($this->baseUri); - - $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig())); - $authPlugin = new Plugin($authBackend, 'ownCloud'); - $this->server->addPlugin($authPlugin); - - // allow setup of additional auth backends - $event = new SabrePluginEvent($this->server); - $dispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event); - - // debugging - if(\OC::$server->getConfig()->getSystemValue('debug', false)) { - $this->server->addPlugin(new \Sabre\DAV\Browser\Plugin()); - } else { - $this->server->addPlugin(new DummyGetResponsePlugin()); - } - - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger)); - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); - $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin()); - - // acl - $acl = new DavAclPlugin(); - $acl->principalCollectionSet = [ - 'principals/users', 'principals/groups' - ]; - $acl->defaultUsernamePath = 'principals/users'; - $this->server->addPlugin($acl); - - // calendar plugins - $this->server->addPlugin(new \Sabre\CalDAV\Plugin()); - $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); - $this->server->addPlugin(new \Sabre\CalDAV\Schedule\Plugin()); - $this->server->addPlugin(new IMipPlugin($mailer, $logger)); - $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); - $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin()); - $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest())); - - // addressbook plugins - $this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin()); - $this->server->addPlugin(new VCFExportPlugin()); - - // system tags plugins - $this->server->addPlugin(new \OCA\DAV\SystemTag\SystemTagPlugin( - \OC::$server->getSystemTagManager(), - \OC::$server->getGroupManager(), - \OC::$server->getUserSession() - )); - - // comments plugin - $this->server->addPlugin(new \OCA\DAV\Comments\CommentsPlugin( - \OC::$server->getCommentsManager(), - \OC::$server->getUserSession() - )); - - // Some WebDAV clients do require Class 2 WebDAV support (locking), since - // we do not provide locking we emulate it using a fake locking plugin. - if($request->isUserAgent([ - '/WebDAVFS/', - '/Microsoft Office OneNote 2013/', - ])) { - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin()); - } - - if (BrowserErrorPagePlugin::isBrowserRequest($request)) { - $this->server->addPlugin(new BrowserErrorPagePlugin()); - } - - // wait with registering these until auth is handled and the filesystem is setup - $this->server->on('beforeMethod', function () { - // custom properties plugin must be the last one - $user = \OC::$server->getUserSession()->getUser(); - if (!is_null($user)) { - $view = \OC\Files\Filesystem::getView(); - $this->server->addPlugin( - new FilesPlugin( - $this->server->tree, - $view, - \OC::$server->getConfig(), - false, - !\OC::$server->getConfig()->getSystemValue('debug', false) - ) - ); - - $this->server->addPlugin( - new \Sabre\DAV\PropertyStorage\Plugin( - new CustomPropertiesBackend( - $this->server->tree, - \OC::$server->getDatabaseConnection(), - \OC::$server->getUserSession()->getUser() - ) - ) - ); - } - }); - } - - public function exec() { - $this->server->exec(); - } -} diff --git a/apps/dav/lib/systemtag/systemtagmappingnode.php b/apps/dav/lib/systemtag/systemtagmappingnode.php deleted file mode 100644 index bb2936c13dc..00000000000 --- a/apps/dav/lib/systemtag/systemtagmappingnode.php +++ /dev/null @@ -1,114 +0,0 @@ - - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\SystemTag; - -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Exception\Forbidden; - -use OCP\SystemTag\ISystemTag; -use OCP\SystemTag\ISystemTagManager; -use OCP\SystemTag\ISystemTagObjectMapper; -use OCP\SystemTag\TagNotFoundException; - -/** - * Mapping node for system tag to object id - */ -class SystemTagMappingNode extends SystemTagNode { - - /** - * @var ISystemTagObjectMapper - */ - private $tagMapper; - - /** - * @var string - */ - private $objectId; - - /** - * @var string - */ - private $objectType; - - /** - * Sets up the node, expects a full path name - * - * @param ISystemTag $tag system tag - * @param string $objectId - * @param string $objectType - * @param bool $isAdmin whether to allow permissions for admin - * @param ISystemTagManager $tagManager - * @param ISystemTagObjectMapper $tagMapper - */ - public function __construct( - ISystemTag $tag, - $objectId, - $objectType, - $isAdmin, - ISystemTagManager $tagManager, - ISystemTagObjectMapper $tagMapper - ) { - $this->objectId = $objectId; - $this->objectType = $objectType; - $this->tagMapper = $tagMapper; - parent::__construct($tag, $isAdmin, $tagManager); - } - - /** - * Returns the object id of the relationship - * - * @return string object id - */ - public function getObjectId() { - return $this->objectId; - } - - /** - * Returns the object type of the relationship - * - * @return string object type - */ - public function getObjectType() { - return $this->objectType; - } - - /** - * Delete tag to object association - */ - public function delete() { - try { - if (!$this->isAdmin) { - if (!$this->tag->isUserVisible()) { - throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found'); - } - if (!$this->tag->isUserAssignable()) { - throw new Forbidden('No permission to unassign tag ' . $this->tag->getId()); - } - } - $this->tagMapper->unassignTags($this->objectId, $this->objectType, $this->tag->getId()); - } catch (TagNotFoundException $e) { - // can happen if concurrent deletion occurred - throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found', 0, $e); - } - } -} diff --git a/apps/dav/lib/systemtag/systemtagnode.php b/apps/dav/lib/systemtag/systemtagnode.php deleted file mode 100644 index 500e1a3adea..00000000000 --- a/apps/dav/lib/systemtag/systemtagnode.php +++ /dev/null @@ -1,162 +0,0 @@ - - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\SystemTag; - -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Exception\MethodNotAllowed; -use Sabre\DAV\Exception\Conflict; - -use OCP\SystemTag\ISystemTag; -use OCP\SystemTag\ISystemTagManager; -use OCP\SystemTag\TagNotFoundException; -use OCP\SystemTag\TagAlreadyExistsException; - -/** - * DAV node representing a system tag, with the name being the tag id. - */ -class SystemTagNode implements \Sabre\DAV\INode { - - /** - * @var ISystemTag - */ - protected $tag; - - /** - * @var ISystemTagManager - */ - protected $tagManager; - - /** - * Whether to allow permissions for admins - * - * @var bool - */ - protected $isAdmin; - - /** - * Sets up the node, expects a full path name - * - * @param ISystemTag $tag system tag - * @param bool $isAdmin whether to allow operations for admins - * @param ISystemTagManager $tagManager - */ - public function __construct(ISystemTag $tag, $isAdmin, ISystemTagManager $tagManager) { - $this->tag = $tag; - $this->isAdmin = $isAdmin; - $this->tagManager = $tagManager; - } - - /** - * Returns the id of the tag - * - * @return string - */ - public function getName() { - return $this->tag->getId(); - } - - /** - * Returns the system tag represented by this node - * - * @return ISystemTag system tag - */ - public function getSystemTag() { - return $this->tag; - } - - /** - * Renames the node - * - * @param string $name The new name - * - * @throws MethodNotAllowed not allowed to rename node - */ - public function setName($name) { - throw new MethodNotAllowed(); - } - - /** - * Update tag - * - * @param string $name new tag name - * @param bool $userVisible user visible - * @param bool $userAssignable user assignable - * @throws NotFound whenever the given tag id does not exist - * @throws Forbidden whenever there is no permission to update said tag - * @throws Conflict whenever a tag already exists with the given attributes - */ - public function update($name, $userVisible, $userAssignable) { - try { - if (!$this->isAdmin) { - if (!$this->tag->isUserVisible()) { - throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist'); - } - if (!$this->tag->isUserAssignable()) { - throw new Forbidden('No permission to update tag ' . $this->tag->getId()); - } - - // only renaming is allowed for regular users - if ($userVisible !== $this->tag->isUserVisible() - || $userAssignable !== $this->tag->isUserAssignable() - ) { - throw new Forbidden('No permission to update permissions for tag ' . $this->tag->getId()); - } - } - $this->tagManager->updateTag($this->tag->getId(), $name, $userVisible, $userAssignable); - } catch (TagNotFoundException $e) { - throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist'); - } catch (TagAlreadyExistsException $e) { - throw new Conflict( - 'Tag with the properties "' . $name . '", ' . - $userVisible . ', ' . $userAssignable . ' already exists' - ); - } - } - - /** - * Returns null, not supported - * - */ - public function getLastModified() { - return null; - } - - public function delete() { - try { - if (!$this->isAdmin) { - if (!$this->tag->isUserVisible()) { - throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found'); - } - if (!$this->tag->isUserAssignable()) { - throw new Forbidden('No permission to delete tag ' . $this->tag->getId()); - } - } - $this->tagManager->deleteTags($this->tag->getId()); - } catch (TagNotFoundException $e) { - // can happen if concurrent deletion occurred - throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found', 0, $e); - } - } -} diff --git a/apps/dav/lib/systemtag/systemtagplugin.php b/apps/dav/lib/systemtag/systemtagplugin.php deleted file mode 100644 index a266b4a1214..00000000000 --- a/apps/dav/lib/systemtag/systemtagplugin.php +++ /dev/null @@ -1,271 +0,0 @@ - - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ -namespace OCA\DAV\SystemTag; - -use OCP\IGroupManager; -use OCP\IUserSession; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\PropFind; -use Sabre\DAV\PropPatch; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\Exception\UnsupportedMediaType; -use Sabre\DAV\Exception\Conflict; - -use OCP\SystemTag\ISystemTag; -use OCP\SystemTag\ISystemTagManager; -use OCP\SystemTag\TagAlreadyExistsException; -use Sabre\HTTP\RequestInterface; -use Sabre\HTTP\ResponseInterface; - -/** - * Sabre plugin to handle system tags: - * - * - makes it possible to create new tags with POST operation - * - get/set Webdav properties for tags - * - */ -class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { - - // namespace - const NS_OWNCLOUD = 'http://owncloud.org/ns'; - const ID_PROPERTYNAME = '{http://owncloud.org/ns}id'; - const DISPLAYNAME_PROPERTYNAME = '{http://owncloud.org/ns}display-name'; - const USERVISIBLE_PROPERTYNAME = '{http://owncloud.org/ns}user-visible'; - const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable'; - - /** - * @var \Sabre\DAV\Server $server - */ - private $server; - - /** - * @var ISystemTagManager - */ - protected $tagManager; - - /** - * @var IUserSession - */ - protected $userSession; - - /** - * @var IGroupManager - */ - protected $groupManager; - - /** - * @param ISystemTagManager $tagManager tag manager - * @param IGroupManager $groupManager - * @param IUserSession $userSession - */ - public function __construct(ISystemTagManager $tagManager, - IGroupManager $groupManager, - IUserSession $userSession) { - $this->tagManager = $tagManager; - $this->userSession = $userSession; - $this->groupManager = $groupManager; - } - - /** - * This initializes the plugin. - * - * This function is called by \Sabre\DAV\Server, after - * addPlugin is called. - * - * This method should set up the required event subscriptions. - * - * @param \Sabre\DAV\Server $server - * @return void - */ - public function initialize(\Sabre\DAV\Server $server) { - - $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; - - $server->protectedProperties[] = self::ID_PROPERTYNAME; - - $server->on('propFind', array($this, 'handleGetProperties')); - $server->on('propPatch', array($this, 'handleUpdateProperties')); - $server->on('method:POST', [$this, 'httpPost']); - - $this->server = $server; - } - - /** - * POST operation on system tag collections - * - * @param RequestInterface $request request object - * @param ResponseInterface $response response object - * @return null|false - */ - public function httpPost(RequestInterface $request, ResponseInterface $response) { - $path = $request->getPath(); - - // Making sure the node exists - $node = $this->server->tree->getNodeForPath($path); - if ($node instanceof SystemTagsByIdCollection || $node instanceof SystemTagsObjectMappingCollection) { - $data = $request->getBodyAsString(); - - $tag = $this->createTag($data, $request->getHeader('Content-Type')); - - if ($node instanceof SystemTagsObjectMappingCollection) { - // also add to collection - $node->createFile($tag->getId()); - $url = $request->getBaseUrl() . 'systemtags/'; - } else { - $url = $request->getUrl(); - } - - if ($url[strlen($url) - 1] !== '/') { - $url .= '/'; - } - - $response->setHeader('Content-Location', $url . $tag->getId()); - - // created - $response->setStatus(201); - return false; - } - } - - /** - * Creates a new tag - * - * @param string $data JSON encoded string containing the properties of the tag to create - * @param string $contentType content type of the data - * @return ISystemTag newly created system tag - * - * @throws BadRequest if a field was missing - * @throws Conflict if a tag with the same properties already exists - * @throws UnsupportedMediaType if the content type is not supported - */ - private function createTag($data, $contentType = 'application/json') { - if (explode(';', $contentType)[0] === 'application/json') { - $data = json_decode($data, true); - } else { - throw new UnsupportedMediaType(); - } - - if (!isset($data['name'])) { - throw new BadRequest('Missing "name" attribute'); - } - - $tagName = $data['name']; - $userVisible = true; - $userAssignable = true; - - if (isset($data['userVisible'])) { - $userVisible = (bool)$data['userVisible']; - } - - if (isset($data['userAssignable'])) { - $userAssignable = (bool)$data['userAssignable']; - } - - if($userVisible === false || $userAssignable === false) { - if(!$this->userSession->isLoggedIn() || !$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) { - throw new BadRequest('Not sufficient permissions'); - } - } - - try { - return $this->tagManager->createTag($tagName, $userVisible, $userAssignable); - } catch (TagAlreadyExistsException $e) { - throw new Conflict('Tag already exists', 0, $e); - } - } - - - /** - * Retrieves system tag properties - * - * @param PropFind $propFind - * @param \Sabre\DAV\INode $node - */ - public function handleGetProperties( - PropFind $propFind, - \Sabre\DAV\INode $node - ) { - if (!($node instanceof SystemTagNode)) { - return; - } - - $propFind->handle(self::ID_PROPERTYNAME, function() use ($node) { - return $node->getSystemTag()->getId(); - }); - - $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function() use ($node) { - return $node->getSystemTag()->getName(); - }); - - $propFind->handle(self::USERVISIBLE_PROPERTYNAME, function() use ($node) { - return $node->getSystemTag()->isUserVisible() ? 'true' : 'false'; - }); - - $propFind->handle(self::USERASSIGNABLE_PROPERTYNAME, function() use ($node) { - return $node->getSystemTag()->isUserAssignable() ? 'true' : 'false'; - }); - } - - /** - * Updates tag attributes - * - * @param string $path - * @param PropPatch $propPatch - * - * @return void - */ - public function handleUpdateProperties($path, PropPatch $propPatch) { - $propPatch->handle([ - self::DISPLAYNAME_PROPERTYNAME, - self::USERVISIBLE_PROPERTYNAME, - self::USERASSIGNABLE_PROPERTYNAME, - ], function($props) use ($path) { - $node = $this->server->tree->getNodeForPath($path); - if (!($node instanceof SystemTagNode)) { - return; - } - - $tag = $node->getSystemTag(); - $name = $tag->getName(); - $userVisible = $tag->isUserVisible(); - $userAssignable = $tag->isUserAssignable(); - - if (isset($props[self::DISPLAYNAME_PROPERTYNAME])) { - $name = $props[self::DISPLAYNAME_PROPERTYNAME]; - } - - if (isset($props[self::USERVISIBLE_PROPERTYNAME])) { - $propValue = $props[self::USERVISIBLE_PROPERTYNAME]; - $userVisible = ($propValue !== 'false' && $propValue !== '0'); - } - - if (isset($props[self::USERASSIGNABLE_PROPERTYNAME])) { - $propValue = $props[self::USERASSIGNABLE_PROPERTYNAME]; - $userAssignable = ($propValue !== 'false' && $propValue !== '0'); - } - - $node->update($name, $userVisible, $userAssignable); - return true; - }); - } -} diff --git a/apps/dav/lib/systemtag/systemtagsbyidcollection.php b/apps/dav/lib/systemtag/systemtagsbyidcollection.php deleted file mode 100644 index 298902501ab..00000000000 --- a/apps/dav/lib/systemtag/systemtagsbyidcollection.php +++ /dev/null @@ -1,176 +0,0 @@ - - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\SystemTag; - -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\ICollection; - -use OCP\SystemTag\ISystemTagManager; -use OCP\SystemTag\ISystemTag; -use OCP\SystemTag\TagNotFoundException; -use OCP\IGroupManager; -use OCP\IUserSession; - -class SystemTagsByIdCollection implements ICollection { - - /** - * @var ISystemTagManager - */ - private $tagManager; - - /** - * @var IGroupManager - */ - private $groupManager; - - /** - * @var IUserSession - */ - private $userSession; - - /** - * SystemTagsByIdCollection constructor. - * - * @param ISystemTagManager $tagManager - * @param IUserSession $userSession - * @param IGroupManager $groupManager - */ - public function __construct( - ISystemTagManager $tagManager, - IUserSession $userSession, - IGroupManager $groupManager - ) { - $this->tagManager = $tagManager; - $this->userSession = $userSession; - $this->groupManager = $groupManager; - } - - /** - * Returns whether the currently logged in user is an administrator - */ - private function isAdmin() { - $user = $this->userSession->getUser(); - if ($user !== null) { - return $this->groupManager->isAdmin($user->getUID()); - } - return false; - } - - /** - * @param string $name - * @param resource|string $data Initial payload - * @throws Forbidden - */ - function createFile($name, $data = null) { - throw new Forbidden('Cannot create tags by id'); - } - - /** - * @param string $name - */ - function createDirectory($name) { - throw new Forbidden('Permission denied to create collections'); - } - - /** - * @param string $name - */ - function getChild($name) { - try { - $tag = $this->tagManager->getTagsByIds([$name]); - $tag = current($tag); - if (!$this->isAdmin() && !$tag->isUserVisible()) { - throw new NotFound('Tag with id ' . $name . ' not found'); - } - return $this->makeNode($tag); - } catch (\InvalidArgumentException $e) { - throw new BadRequest('Invalid tag id', 0, $e); - } catch (TagNotFoundException $e) { - throw new NotFound('Tag with id ' . $name . ' not found', 0, $e); - } - } - - function getChildren() { - $visibilityFilter = true; - if ($this->isAdmin()) { - $visibilityFilter = null; - } - - $tags = $this->tagManager->getAllTags($visibilityFilter); - return array_map(function($tag) { - return $this->makeNode($tag); - }, $tags); - } - - /** - * @param string $name - */ - function childExists($name) { - try { - $tag = $this->tagManager->getTagsByIds([$name]); - $tag = current($tag); - if (!$this->isAdmin() && !$tag->isUserVisible()) { - return false; - } - return true; - } catch (\InvalidArgumentException $e) { - throw new BadRequest('Invalid tag id', 0, $e); - } catch (TagNotFoundException $e) { - return false; - } - } - - function delete() { - throw new Forbidden('Permission denied to delete this collection'); - } - - function getName() { - return 'systemtags'; - } - - function setName($name) { - throw new Forbidden('Permission denied to rename this collection'); - } - - /** - * Returns the last modification time, as a unix timestamp - * - * @return int - */ - function getLastModified() { - return null; - } - - /** - * Create a sabre node for the given system tag - * - * @param ISystemTag $tag - * - * @return SystemTagNode - */ - private function makeNode(ISystemTag $tag) { - return new SystemTagNode($tag, $this->isAdmin(), $this->tagManager); - } -} diff --git a/apps/dav/lib/systemtag/systemtagsobjectmappingcollection.php b/apps/dav/lib/systemtag/systemtagsobjectmappingcollection.php deleted file mode 100644 index eb75ed06393..00000000000 --- a/apps/dav/lib/systemtag/systemtagsobjectmappingcollection.php +++ /dev/null @@ -1,201 +0,0 @@ - - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\SystemTag; - -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\Exception\PreconditionFailed; -use Sabre\DAV\ICollection; - -use OCP\SystemTag\ISystemTagManager; -use OCP\SystemTag\ISystemTagObjectMapper; -use OCP\SystemTag\ISystemTag; -use OCP\SystemTag\TagNotFoundException; - -/** - * Collection containing tags by object id - */ -class SystemTagsObjectMappingCollection implements ICollection { - - /** - * @var string - */ - private $objectId; - - /** - * @var string - */ - private $objectType; - - /** - * @var ISystemTagManager - */ - private $tagManager; - - /** - * @var ISystemTagObjectMapper - */ - private $tagMapper; - - /** - * Whether to return results only visible for admins - * - * @var bool - */ - private $isAdmin; - - - /** - * Constructor - * - * @param string $objectId object id - * @param string $objectType object type - * @param bool $isAdmin whether to return results visible only for admins - * @param ISystemTagManager $tagManager - * @param ISystemTagObjectMapper $tagMapper - */ - public function __construct($objectId, $objectType, $isAdmin, $tagManager, $tagMapper) { - $this->tagManager = $tagManager; - $this->tagMapper = $tagMapper; - $this->objectId = $objectId; - $this->objectType = $objectType; - $this->isAdmin = $isAdmin; - } - - function createFile($tagId, $data = null) { - try { - if (!$this->isAdmin) { - $tag = $this->tagManager->getTagsByIds($tagId); - $tag = current($tag); - if (!$tag->isUserVisible()) { - throw new PreconditionFailed('Tag with id ' . $tagId . ' does not exist, cannot assign'); - } - if (!$tag->isUserAssignable()) { - throw new Forbidden('No permission to assign tag ' . $tag->getId()); - } - } - $this->tagMapper->assignTags($this->objectId, $this->objectType, $tagId); - } catch (TagNotFoundException $e) { - throw new PreconditionFailed('Tag with id ' . $tagId . ' does not exist, cannot assign'); - } - } - - function createDirectory($name) { - throw new Forbidden('Permission denied to create collections'); - } - - function getChild($tagId) { - try { - if ($this->tagMapper->haveTag([$this->objectId], $this->objectType, $tagId, true)) { - $tag = $this->tagManager->getTagsByIds([$tagId]); - $tag = current($tag); - if ($this->isAdmin || $tag->isUserVisible()) { - return $this->makeNode($tag); - } - } - throw new NotFound('Tag with id ' . $tagId . ' not present for object ' . $this->objectId); - } catch (\InvalidArgumentException $e) { - throw new BadRequest('Invalid tag id', 0, $e); - } catch (TagNotFoundException $e) { - throw new NotFound('Tag with id ' . $tagId . ' not found', 0, $e); - } - } - - function getChildren() { - $tagIds = current($this->tagMapper->getTagIdsForObjects([$this->objectId], $this->objectType)); - if (empty($tagIds)) { - return []; - } - $tags = $this->tagManager->getTagsByIds($tagIds); - if (!$this->isAdmin) { - // filter out non-visible tags - $tags = array_filter($tags, function($tag) { - return $tag->isUserVisible(); - }); - } - return array_values(array_map(function($tag) { - return $this->makeNode($tag); - }, $tags)); - } - - function childExists($tagId) { - try { - $result = ($this->tagMapper->haveTag([$this->objectId], $this->objectType, $tagId, true)); - if ($this->isAdmin || !$result) { - return $result; - } - - // verify if user is allowed to see this tag - $tag = $this->tagManager->getTagsByIds($tagId); - $tag = current($tag); - if (!$tag->isUserVisible()) { - return false; - } - return true; - } catch (\InvalidArgumentException $e) { - throw new BadRequest('Invalid tag id', 0, $e); - } catch (TagNotFoundException $e) { - return false; - } - } - - function delete() { - throw new Forbidden('Permission denied to delete this collection'); - } - - function getName() { - return $this->objectId; - } - - function setName($name) { - throw new Forbidden('Permission denied to rename this collection'); - } - - /** - * Returns the last modification time, as a unix timestamp - * - * @return int - */ - function getLastModified() { - return null; - } - - /** - * Create a sabre node for the mapping of the - * given system tag to the collection's object - * - * @param ISystemTag $tag - * - * @return SystemTagNode - */ - private function makeNode(ISystemTag $tag) { - return new SystemTagMappingNode( - $tag, - $this->objectId, - $this->objectType, - $this->isAdmin, - $this->tagManager, - $this->tagMapper - ); - } -} diff --git a/apps/dav/lib/systemtag/systemtagsobjecttypecollection.php b/apps/dav/lib/systemtag/systemtagsobjecttypecollection.php deleted file mode 100644 index bdbc73c4e32..00000000000 --- a/apps/dav/lib/systemtag/systemtagsobjecttypecollection.php +++ /dev/null @@ -1,183 +0,0 @@ - - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\SystemTag; - -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\Exception\MethodNotAllowed; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\ICollection; - -use OCP\SystemTag\ISystemTagManager; -use OCP\SystemTag\ISystemTagObjectMapper; -use OCP\IUserSession; -use OCP\IGroupManager; -use OCP\Files\IRootFolder; - -/** - * Collection containing object ids by object type - */ -class SystemTagsObjectTypeCollection implements ICollection { - - /** - * @var string - */ - private $objectType; - - /** - * @var ISystemTagManager - */ - private $tagManager; - - /** - * @var ISystemTagObjectMapper - */ - private $tagMapper; - - /** - * @var IGroupManager - */ - private $groupManager; - - /** - * @var IUserSession - */ - private $userSession; - - /** - * @var IRootFolder - **/ - protected $fileRoot; - - /** - * Constructor - * - * @param string $objectType object type - * @param ISystemTagManager $tagManager - * @param ISystemTagObjectMapper $tagMapper - * @param IUserSession $userSession - * @param IGroupManager $groupManager - * @param IRootFolder $fileRoot - */ - public function __construct( - $objectType, - ISystemTagManager $tagManager, - ISystemTagObjectMapper $tagMapper, - IUserSession $userSession, - IGroupManager $groupManager, - IRootFolder $fileRoot - ) { - $this->tagManager = $tagManager; - $this->tagMapper = $tagMapper; - $this->objectType = $objectType; - $this->userSession = $userSession; - $this->groupManager = $groupManager; - $this->fileRoot = $fileRoot; - } - - /** - * Returns whether the currently logged in user is an administrator - */ - private function isAdmin() { - $user = $this->userSession->getUser(); - if ($user !== null) { - return $this->groupManager->isAdmin($user->getUID()); - } - return false; - } - - /** - * @param string $name - * @param resource|string $data Initial payload - * @throws Forbidden - */ - function createFile($name, $data = null) { - throw new Forbidden('Permission denied to create nodes'); - } - - /** - * @param string $name - */ - function createDirectory($name) { - throw new Forbidden('Permission denied to create collections'); - } - - /** - * @param string $objectId - */ - function getChild($objectId) { - // make sure the object exists and is reachable - if(!$this->childExists($objectId)) { - throw new NotFound('Entity does not exist or is not available'); - } - return new SystemTagsObjectMappingCollection( - $objectId, - $this->objectType, - $this->isAdmin(), - $this->tagManager, - $this->tagMapper - ); - } - - function getChildren() { - // do not list object ids - throw new MethodNotAllowed(); - } - - /** - * @param string $name - */ - function childExists($name) { - // TODO: make this more abstract - if ($this->objectType === 'files') { - // make sure the object is reachable for the current user - $userId = $this->userSession->getUser()->getUID(); - $nodes = $this->fileRoot->getUserFolder($userId)->getById(intval($name)); - return !empty($nodes); - } - return true; - } - - function delete() { - throw new Forbidden('Permission denied to delete this collection'); - } - - function getName() { - return $this->objectType; - } - - /** - * @param string $name - */ - function setName($name) { - throw new Forbidden('Permission denied to rename this collection'); - } - - /** - * Returns the last modification time, as a unix timestamp - * - * @return int - */ - function getLastModified() { - return null; - } -} diff --git a/apps/dav/lib/systemtag/systemtagsrelationscollection.php b/apps/dav/lib/systemtag/systemtagsrelationscollection.php deleted file mode 100644 index 19db39d3f3a..00000000000 --- a/apps/dav/lib/systemtag/systemtagsrelationscollection.php +++ /dev/null @@ -1,74 +0,0 @@ - - * @author Thomas Müller - * @author Vincent Petry - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\DAV\SystemTag; - -use OCP\SystemTag\ISystemTagManager; -use OCP\SystemTag\ISystemTagObjectMapper; -use Sabre\DAV\Exception\Forbidden; -use Sabre\DAV\SimpleCollection; -use OCP\IUserSession; -use OCP\IGroupManager; -use OCP\Files\IRootFolder; - -class SystemTagsRelationsCollection extends SimpleCollection { - - /** - * SystemTagsRelationsCollection constructor. - * - * @param ISystemTagManager $tagManager - * @param ISystemTagObjectMapper $tagMapper - * @param IUserSession $userSession - * @param IGroupManager $groupManager - * @param IRootFolder $fileRoot - */ - public function __construct( - ISystemTagManager $tagManager, - ISystemTagObjectMapper $tagMapper, - IUserSession $userSession, - IGroupManager $groupManager, - IRootFolder $fileRoot - ) { - $children = [ - new SystemTagsObjectTypeCollection( - 'files', - $tagManager, - $tagMapper, - $userSession, - $groupManager, - $fileRoot - ), - ]; - - parent::__construct('root', $children); - } - - function getName() { - return 'systemtags-relations'; - } - - function setName($name) { - throw new Forbidden('Permission denied to rename this collection'); - } - -} diff --git a/apps/dav/lib/upload/assemblystream.php b/apps/dav/lib/upload/assemblystream.php deleted file mode 100644 index 4b80a591ce4..00000000000 --- a/apps/dav/lib/upload/assemblystream.php +++ /dev/null @@ -1,234 +0,0 @@ -loadContext('assembly'); - - // sort the nodes - $nodes = $this->nodes; - // http://stackoverflow.com/a/10985500 - @usort($nodes, function(IFile $a, IFile $b) { - return strcmp($a->getName(), $b->getName()); - }); - $this->nodes = $nodes; - - // build additional information - $this->sortedNodes = []; - $start = 0; - foreach($this->nodes as $node) { - $size = $node->getSize(); - $name = $node->getName(); - $this->sortedNodes[$name] = ['node' => $node, 'start' => $start, 'end' => $start + $size]; - $start += $size; - $this->size = $start; - } - return true; - } - - /** - * @param string $offset - * @param int $whence - * @return bool - */ - public function stream_seek($offset, $whence = SEEK_SET) { - return false; - } - - /** - * @return int - */ - public function stream_tell() { - return $this->pos; - } - - /** - * @param int $count - * @return string - */ - public function stream_read($count) { - - list($node, $posInNode) = $this->getNodeForPosition($this->pos); - if (is_null($node)) { - return null; - } - $stream = $this->getStream($node); - - fseek($stream, $posInNode); - $data = fread($stream, $count); - $read = strlen($data); - - // update position - $this->pos += $read; - return $data; - } - - /** - * @param string $data - * @return int - */ - public function stream_write($data) { - return false; - } - - /** - * @param int $option - * @param int $arg1 - * @param int $arg2 - * @return bool - */ - public function stream_set_option($option, $arg1, $arg2) { - return false; - } - - /** - * @param int $size - * @return bool - */ - public function stream_truncate($size) { - return false; - } - - /** - * @return array - */ - public function stream_stat() { - return []; - } - - /** - * @param int $operation - * @return bool - */ - public function stream_lock($operation) { - return false; - } - - /** - * @return bool - */ - public function stream_flush() { - return false; - } - - /** - * @return bool - */ - public function stream_eof() { - return $this->pos >= $this->size; - } - - /** - * @return bool - */ - public function stream_close() { - return true; - } - - - /** - * Load the source from the stream context and return the context options - * - * @param string $name - * @return array - * @throws \Exception - */ - protected function loadContext($name) { - $context = stream_context_get_options($this->context); - if (isset($context[$name])) { - $context = $context[$name]; - } else { - throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); - } - if (isset($context['nodes']) and is_array($context['nodes'])) { - $this->nodes = $context['nodes']; - } else { - throw new \BadMethodCallException('Invalid context, nodes not set'); - } - return $context; - } - - /** - * @param IFile[] $nodes - * @return resource - * - * @throws \BadMethodCallException - */ - public static function wrap(array $nodes) { - $context = stream_context_create([ - 'assembly' => [ - 'nodes' => $nodes] - ]); - stream_wrapper_register('assembly', '\OCA\DAV\Upload\AssemblyStream'); - try { - $wrapped = fopen('assembly://', 'r', null, $context); - } catch (\BadMethodCallException $e) { - stream_wrapper_unregister('assembly'); - throw $e; - } - stream_wrapper_unregister('assembly'); - return $wrapped; - } - - /** - * @param $pos - * @return IFile | null - */ - private function getNodeForPosition($pos) { - foreach($this->sortedNodes as $node) { - if ($pos >= $node['start'] && $pos < $node['end']) { - return [$node['node'], $pos - $node['start']]; - } - } - return null; - } - - /** - * @param IFile $node - * @return resource - */ - private function getStream(IFile $node) { - $data = $node->get(); - if (is_resource($data)) { - return $data; - } - - return fopen('data://text/plain,' . $data,'r'); - } - -} diff --git a/apps/dav/lib/upload/futurefile.php b/apps/dav/lib/upload/futurefile.php deleted file mode 100644 index aca81afc055..00000000000 --- a/apps/dav/lib/upload/futurefile.php +++ /dev/null @@ -1,103 +0,0 @@ -root = $root; - $this->name = $name; - } - - /** - * @inheritdoc - */ - function put($data) { - throw new Forbidden('Permission denied to put into this file'); - } - - /** - * @inheritdoc - */ - function get() { - $nodes = $this->root->getChildren(); - return AssemblyStream::wrap($nodes); - } - - /** - * @inheritdoc - */ - function getContentType() { - return 'application/octet-stream'; - } - - /** - * @inheritdoc - */ - function getETag() { - return $this->root->getETag(); - } - - /** - * @inheritdoc - */ - function getSize() { - $children = $this->root->getChildren(); - $sizes = array_map(function($node) { - /** @var IFile $node */ - return $node->getSize(); - }, $children); - - return array_sum($sizes); - } - - /** - * @inheritdoc - */ - function delete() { - $this->root->delete(); - } - - /** - * @inheritdoc - */ - function getName() { - return $this->name; - } - - /** - * @inheritdoc - */ - function setName($name) { - throw new Forbidden('Permission denied to rename this file'); - } - - /** - * @inheritdoc - */ - function getLastModified() { - return $this->root->getLastModified(); - } -} diff --git a/apps/dav/lib/upload/rootcollection.php b/apps/dav/lib/upload/rootcollection.php deleted file mode 100644 index 673a3734318..00000000000 --- a/apps/dav/lib/upload/rootcollection.php +++ /dev/null @@ -1,23 +0,0 @@ -node = $node; - } - - function createFile($name, $data = null) { - // TODO: verify name - should be a simple number - $this->node->createFile($name, $data); - } - - function createDirectory($name) { - throw new Forbidden('Permission denied to create file (filename ' . $name . ')'); - } - - function getChild($name) { - if ($name === '.file') { - return new FutureFile($this->node, '.file'); - } - return $this->node->getChild($name); - } - - function getChildren() { - $children = $this->node->getChildren(); - $children[] = new FutureFile($this->node, '.file'); - return $children; - } - - function childExists($name) { - if ($name === '.file') { - return true; - } - return $this->node->childExists($name); - } - - function delete() { - $this->node->delete(); - } - - function getName() { - return $this->node->getName(); - } - - function setName($name) { - throw new Forbidden('Permission denied to rename this folder'); - } - - function getLastModified() { - return $this->node->getLastModified(); - } -} diff --git a/apps/dav/lib/upload/uploadhome.php b/apps/dav/lib/upload/uploadhome.php deleted file mode 100644 index ae4dcfa4931..00000000000 --- a/apps/dav/lib/upload/uploadhome.php +++ /dev/null @@ -1,74 +0,0 @@ -principalInfo = $principalInfo; - } - - function createFile($name, $data = null) { - throw new Forbidden('Permission denied to create file (filename ' . $name . ')'); - } - - function createDirectory($name) { - $this->impl()->createDirectory($name); - } - - function getChild($name) { - return new UploadFolder($this->impl()->getChild($name)); - } - - function getChildren() { - return array_map(function($node) { - return new UploadFolder($node); - }, $this->impl()->getChildren()); - } - - function childExists($name) { - return !is_null($this->getChild($name)); - } - - function delete() { - $this->impl()->delete(); - } - - function getName() { - return 'uploads'; - } - - function setName($name) { - throw new Forbidden('Permission denied to rename this folder'); - } - - function getLastModified() { - return $this->impl()->getLastModified(); - } - - /** - * @return Directory - */ - private function impl() { - $rootView = new View(); - $user = \OC::$server->getUserSession()->getUser(); - Filesystem::initMountPoints($user->getUID()); - if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) { - $rootView->mkdir('/' . $user->getUID() . '/uploads'); - } - $view = new View('/' . $user->getUID() . '/uploads'); - $rootInfo = $view->getFileInfo(''); - $impl = new Directory($view, $rootInfo); - return $impl; - } -} diff --git a/apps/dav/tests/unit/appinfo/applicationtest.php b/apps/dav/tests/unit/appinfo/applicationtest.php index 7f533a185df..05dd75b2c68 100644 --- a/apps/dav/tests/unit/appinfo/applicationtest.php +++ b/apps/dav/tests/unit/appinfo/applicationtest.php @@ -21,7 +21,7 @@ namespace OCA\DAV\Tests\Unit\AppInfo; -use OCA\Dav\AppInfo\Application; +use OCA\DAV\AppInfo\Application; use OCP\Contacts\IManager; use Test\TestCase; -- cgit v1.2.3