diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php index a47242f825..b8886c0d15 100644 --- a/apps/dav/appinfo/v1/carddav.php +++ b/apps/dav/appinfo/v1/carddav.php @@ -27,6 +27,7 @@ */ // Backends +use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CardDAV\AddressBookRoot; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\Connector\LegacyDAVACL; @@ -34,6 +35,7 @@ use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin; use OCA\DAV\Connector\Sabre\Principal; +use OCP\App\IAppManager; use Sabre\CardDAV\Plugin; $authBackend = new Auth( @@ -63,7 +65,8 @@ $debugging = \OC::$server->getConfig()->getSystemValue('debug', false); $principalCollection = new \Sabre\CalDAV\Principal\Collection($principalBackend); $principalCollection->disableListing = !$debugging; // Disable listing -$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend); +$pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class)); +$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend, $pluginManager); $addressBookRoot->disableListing = !$debugging; // Disable listing $nodes = array( diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 8a26b80916..ecf51164e8 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -91,6 +91,8 @@ return array( 'OCA\\DAV\\CardDAV\\Converter' => $baseDir . '/../lib/CardDAV/Converter.php', 'OCA\\DAV\\CardDAV\\HasPhotoPlugin' => $baseDir . '/../lib/CardDAV/HasPhotoPlugin.php', 'OCA\\DAV\\CardDAV\\ImageExportPlugin' => $baseDir . '/../lib/CardDAV/ImageExportPlugin.php', + 'OCA\\DAV\\CardDAV\\Integration\\ExternalAddressBook' => $baseDir . '/../lib/CardDAV/Integration/ExternalAddressBook.php', + 'OCA\\DAV\\CardDAV\\Integration\\IAddressBookProvider' => $baseDir . '/../lib/CardDAV/Integration/IAddressBookProvider.php', 'OCA\\DAV\\CardDAV\\MultiGetExportPlugin' => $baseDir . '/../lib/CardDAV/MultiGetExportPlugin.php', 'OCA\\DAV\\CardDAV\\PhotoCache' => $baseDir . '/../lib/CardDAV/PhotoCache.php', 'OCA\\DAV\\CardDAV\\Plugin' => $baseDir . '/../lib/CardDAV/Plugin.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 001297ffd2..4df92c174e 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -106,6 +106,8 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CardDAV\\Converter' => __DIR__ . '/..' . '/../lib/CardDAV/Converter.php', 'OCA\\DAV\\CardDAV\\HasPhotoPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/HasPhotoPlugin.php', 'OCA\\DAV\\CardDAV\\ImageExportPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/ImageExportPlugin.php', + 'OCA\\DAV\\CardDAV\\Integration\\ExternalAddressBook' => __DIR__ . '/..' . '/../lib/CardDAV/Integration/ExternalAddressBook.php', + 'OCA\\DAV\\CardDAV\\Integration\\IAddressBookProvider' => __DIR__ . '/..' . '/../lib/CardDAV/Integration/IAddressBookProvider.php', 'OCA\\DAV\\CardDAV\\MultiGetExportPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/MultiGetExportPlugin.php', 'OCA\\DAV\\CardDAV\\PhotoCache' => __DIR__ . '/..' . '/../lib/CardDAV/PhotoCache.php', 'OCA\\DAV\\CardDAV\\Plugin' => __DIR__ . '/..' . '/../lib/CardDAV/Plugin.php', diff --git a/apps/dav/lib/AppInfo/PluginManager.php b/apps/dav/lib/AppInfo/PluginManager.php index 6a44332ddb..f123648cd3 100644 --- a/apps/dav/lib/AppInfo/PluginManager.php +++ b/apps/dav/lib/AppInfo/PluginManager.php @@ -1,4 +1,7 @@ collections; } + /** + * @return IAddressBookProvider[] + */ + public function getAddressBookPlugins(): array { + if ($this->addressBookPlugins === null) { + $this->populate(); + } + return $this->addressBookPlugins; + } + /** * Returns an array of app-registered calendar plugins * @@ -118,6 +142,7 @@ class PluginManager { */ private function populate() { $this->plugins = []; + $this->addressBookPlugins = []; $this->calendarPlugins = []; $this->collections = []; foreach ($this->appManager->getInstalledApps() as $app) { @@ -128,6 +153,7 @@ class PluginManager { } $this->loadSabrePluginsFromInfoXml($this->extractPluginList($info)); $this->loadSabreCollectionsFromInfoXml($this->extractCollectionList($info)); + $this->loadSabreAddressBookPluginsFromInfoXml($this->extractAddressBookPluginList($info)); $this->loadSabreCalendarPluginsFromInfoXml($this->extractCalendarPluginList($info)); } } @@ -162,6 +188,29 @@ class PluginManager { return []; } + /** + * @param array $array + * + * @return string[] + */ + private function extractAddressBookPluginList(array $array): array { + if (!isset($array['sabre']) || !is_array($array['sabre'])) { + return []; + } + if (!isset($array['sabre']['address-book-plugins']) || !is_array($array['sabre']['address-book-plugins'])) { + return []; + } + if (!isset($array['sabre']['address-book-plugins']['plugin'])) { + return []; + } + + $items = $array['sabre']['address-book-plugins']['plugin']; + if (!is_array($items)) { + $items = [$items]; + } + return $items; + } + private function extractCalendarPluginList(array $array):array { if (isset($array['sabre']) && is_array($array['sabre'])) { if (isset($array['sabre']['calendar-plugins']) && is_array($array['sabre']['calendar-plugins'])) { @@ -205,6 +254,34 @@ class PluginManager { } } + private function createPluginInstance(string $className) { + try { + return $this->container->query($className); + } catch (QueryException $e) { + if (class_exists($className)) { + return new $className(); + } + } + + throw new \Exception("Sabre plugin class '$className' is unknown and could not be loaded"); + } + + /** + * @param string[] $plugin + */ + private function loadSabreAddressBookPluginsFromInfoXml(array $plugins): void { + $providers = array_map(function(string $className): IAddressBookProvider { + $instance = $this->createPluginInstance($className); + if (!($instance instanceof IAddressBookProvider)) { + throw new \Exception("Sabre address book plugin class '$className' does not implement the \OCA\DAV\CardDAV\Integration\IAddressBookProvider interface"); + } + return $instance; + }, $plugins); + foreach ($providers as $provider) { + $this->addressBookPlugins[] = $provider; + } + } + private function loadSabreCalendarPluginsFromInfoXml(array $calendarPlugins):void { foreach ($calendarPlugins as $calendarPlugin) { try { diff --git a/apps/dav/lib/CardDAV/AddressBookRoot.php b/apps/dav/lib/CardDAV/AddressBookRoot.php index 4b83661679..4436775653 100644 --- a/apps/dav/lib/CardDAV/AddressBookRoot.php +++ b/apps/dav/lib/CardDAV/AddressBookRoot.php @@ -24,21 +24,24 @@ namespace OCA\DAV\CardDAV; -use OCP\IL10N; +use OCA\DAV\AppInfo\PluginManager; class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { - /** @var IL10N */ - protected $l10n; + /** @var PluginManager */ + private $pluginManager; /** * @param \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend * @param \Sabre\CardDAV\Backend\BackendInterface $carddavBackend * @param string $principalPrefix */ - public function __construct(\Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend, \Sabre\CardDAV\Backend\BackendInterface $carddavBackend, $principalPrefix = 'principals') { + public function __construct(\Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend, + \Sabre\CardDAV\Backend\BackendInterface $carddavBackend, + PluginManager $pluginManager, + $principalPrefix = 'principals') { parent::__construct($principalBackend, $carddavBackend, $principalPrefix); - $this->l10n = \OC::$server->getL10N('dav'); + $this->pluginManager = $pluginManager; } /** @@ -49,12 +52,11 @@ class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { * supplied by the authentication backend. * * @param array $principal + * * @return \Sabre\DAV\INode */ function getChildForPrincipal(array $principal) { - - return new UserAddressBooks($this->carddavBackend, $principal['uri'], $this->l10n); - + return new UserAddressBooks($this->carddavBackend, $principal['uri'], $this->pluginManager); } function getName() { diff --git a/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php new file mode 100644 index 0000000000..6ac36fea44 --- /dev/null +++ b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php @@ -0,0 +1,113 @@ + must contain only lowercase ASCII characters and underscore + * - explode has a limit of three, so even if the app-generated + * URI has double dashes, it won't be split + */ + private const DELIMITER = '--'; + + /** @var string */ + private $appId; + + /** @var string */ + private $uri; + + /** + * @param string $appId + * @param string $uri + */ + public function __construct(string $appId, string $uri) { + $this->appId = $appId; + $this->uri = $uri; + } + + /** + * @inheritDoc + */ + final public function getName() { + return implode(self::DELIMITER, [ + self::PREFIX, + $this->appId, + $this->uri, + ]); + } + + /** + * @inheritDoc + */ + final public function setName($name) { + throw new DAV\Exception\MethodNotAllowed('Renaming address books is not yet supported'); + } + + /** + * @inheritDoc + */ + final public function createDirectory($name) { + throw new DAV\Exception\MethodNotAllowed('Creating collections in address book objects is not allowed'); + + } + + /** + * Checks whether the address book uri is app-generated + * + * @param string $uri + * + * @return bool + */ + public static function isAppGeneratedAddressBook(string $uri): bool { + return strpos($uri, self::PREFIX) === 0 && substr_count($uri, self::DELIMITER) >= 2; + } + + /** + * Splits an app-generated uri into appId and uri + * + * @param string $uri + * + * @return array + */ + public static function splitAppGeneratedAddressBookUri(string $uri): array { + $array = array_slice(explode(self::DELIMITER, $uri, 3), 1); + // Check the array has expected amount of elements + // and none of them is an empty string + if (\count($array) !== 2 || \in_array('', $array, true)) { + throw new \InvalidArgumentException('Provided address book uri was not app-generated'); + } + + return $array; + } + + /** + * Checks whether a address book name the user wants to create violates + * the reserved name for URIs + * + * @param string $uri + * + * @return bool + */ + public static function doesViolateReservedName(string $uri): bool { + return strpos($uri, self::PREFIX) === 0; + } + +} diff --git a/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php b/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php new file mode 100644 index 0000000000..4410a7486b --- /dev/null +++ b/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php @@ -0,0 +1,53 @@ +pluginManager = $pluginManager; + } + /** - * Returns a list of addressbooks + * Returns a list of address books * * @return array */ @@ -49,18 +67,28 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { } $addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); - $objects = []; - foreach($addressBooks as $addressBook) { + $objects = array_map(function(array $addressBook) { if ($addressBook['principaluri'] === 'principals/system/system') { - $objects[] = new SystemAddressbook($this->carddavBackend, $addressBook, $this->l10n, $this->config); - } else { - $objects[] = new AddressBook($this->carddavBackend, $addressBook, $this->l10n); + return new SystemAddressbook($this->carddavBackend, $addressBook, $this->l10n, $this->config); } + + return new AddressBook($this->carddavBackend, $addressBook, $this->l10n); + }, $addressBooks); + foreach ($this->pluginManager->getAddressBookPlugins() as $plugin) { + $plugin->fetchAllForAddressBookHome($this->principalUri); } return $objects; } + public function createExtendedCollection($name, MkCol $mkCol) { + if (ExternalAddressBook::doesViolateReservedName($name)) { + throw new MethodNotAllowed('The resource you tried to create has a reserved name'); + } + + parent::createExtendedCollection($name, $mkCol); + } + /** * Returns a list of ACE's for this node. * @@ -78,9 +106,9 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { $acl = parent::getACL(); if ($this->principalUri === 'principals/system/system') { $acl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => '{DAV:}authenticated', - 'protected' => true, + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, ]; } diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index becbbb3847..6f8e74692c 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -27,6 +27,7 @@ namespace OCA\DAV; +use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalendarRoot; use OCA\DAV\CalDAV\Principal\Collection; @@ -41,6 +42,7 @@ use OCA\DAV\DAV\GroupPrincipalBackend; use OCA\DAV\DAV\SystemPrincipalBackend; use OCA\DAV\Provisioning\Apple\AppleProvisioningNode; use OCA\DAV\Upload\CleanupService; +use OCP\App\IAppManager; use OCP\AppFramework\Utility\ITimeFactory; use Sabre\DAV\SimpleCollection; @@ -123,12 +125,13 @@ class RootCollection extends SimpleCollection { \OC::$server->getLogger() ); + $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class)); $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher); - $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, 'principals/users'); + $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher); - $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system'); + $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; $uploadCollection = new Upload\RootCollection(