diff options
author | Christoph Wurst <ChristophWurst@users.noreply.github.com> | 2020-03-25 18:39:37 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-25 18:39:37 +0100 |
commit | 3cf321fdfc4235a87015a9af2f59c63220016c65 (patch) | |
tree | a868d20ff4dc0b629a3063cc5f46bc3d4a32feab | |
parent | 902adbe1592ed2828365f765c21acad654204c3b (diff) | |
parent | 37786c4d74618bcedc9f6087a8994aa224a349ff (diff) | |
download | nextcloud-server-3cf321fdfc4235a87015a9af2f59c63220016c65.tar.gz nextcloud-server-3cf321fdfc4235a87015a9af2f59c63220016c65.zip |
Merge pull request #19075 from nextcloud/feature/contact-interaction-event
Collect recent contact interactions
32 files changed, 1968 insertions, 7 deletions
diff --git a/.gitignore b/.gitignore index 4e6967c19d1..2030c73a9ea 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ !/apps/accessibility !/apps/cloud_federation_api !/apps/comments +!/apps/contactsinteraction !/apps/dav !/apps/files !/apps/federation diff --git a/apps/contactsinteraction/appinfo/app.php b/apps/contactsinteraction/appinfo/app.php new file mode 100644 index 00000000000..7bc55c958dd --- /dev/null +++ b/apps/contactsinteraction/appinfo/app.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +\OC::$server->query(\OCA\ContactsInteraction\AppInfo\Application::class); diff --git a/apps/contactsinteraction/appinfo/info.xml b/apps/contactsinteraction/appinfo/info.xml new file mode 100644 index 00000000000..f4e18611150 --- /dev/null +++ b/apps/contactsinteraction/appinfo/info.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd"> + <id>contactsinteraction</id> + <name>Contacts Interaction</name> + <summary>Manages interaction between users and contacts</summary> + <description>Collect data about user and contacts interactions and provide an address book for the data</description> + <version>1.0.0</version> + <licence>agpl</licence> + <author>Christoph Wurst</author> + <namespace>ContactsInteraction</namespace> + <types> + <dav/> + </types> + <default_enable/> + <category>integration</category> + <category>social</category> + <bugs>https://github.com/nextcloud/server/issues</bugs> + <dependencies> + <nextcloud min-version="19" max-version="19"/> + </dependencies> + <background-jobs> + <job>OCA\ContactsInteraction\BackgroundJob\CleanupJob</job> + </background-jobs> + <sabre> + <address-book-plugins> + <plugin>OCA\ContactsInteraction\AddressBookProvider</plugin> + </address-book-plugins> + </sabre> +</info> diff --git a/apps/contactsinteraction/composer/autoload.php b/apps/contactsinteraction/composer/autoload.php new file mode 100644 index 00000000000..7bf597cd9cd --- /dev/null +++ b/apps/contactsinteraction/composer/autoload.php @@ -0,0 +1,7 @@ +<?php + +// autoload.php @generated by Composer + +require_once __DIR__ . '/composer/autoload_real.php'; + +return ComposerAutoloaderInitContactsInteraction::getLoader(); diff --git a/apps/contactsinteraction/composer/composer.json b/apps/contactsinteraction/composer/composer.json new file mode 100644 index 00000000000..232fef13e81 --- /dev/null +++ b/apps/contactsinteraction/composer/composer.json @@ -0,0 +1,13 @@ +{ + "config" : { + "vendor-dir": ".", + "optimize-autoloader": true, + "classmap-authoritative": true, + "autoloader-suffix": "ContactsInteraction" + }, + "autoload" : { + "psr-4": { + "OCA\\ContactsInteraction\\": "../lib/" + } + } +} diff --git a/apps/contactsinteraction/composer/composer/ClassLoader.php b/apps/contactsinteraction/composer/composer/ClassLoader.php new file mode 100644 index 00000000000..fce8549f078 --- /dev/null +++ b/apps/contactsinteraction/composer/composer/ClassLoader.php @@ -0,0 +1,445 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Jordi Boggiano <j.boggiano@seld.be> + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/apps/contactsinteraction/composer/composer/LICENSE b/apps/contactsinteraction/composer/composer/LICENSE new file mode 100644 index 00000000000..f27399a042d --- /dev/null +++ b/apps/contactsinteraction/composer/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/apps/contactsinteraction/composer/composer/autoload_classmap.php b/apps/contactsinteraction/composer/composer/autoload_classmap.php new file mode 100644 index 00000000000..d66c04af714 --- /dev/null +++ b/apps/contactsinteraction/composer/composer/autoload_classmap.php @@ -0,0 +1,19 @@ +<?php + +// autoload_classmap.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = $vendorDir; + +return array( + 'OCA\\ContactsInteraction\\AddressBook' => $baseDir . '/../lib/AddressBook.php', + 'OCA\\ContactsInteraction\\AddressBookProvider' => $baseDir . '/../lib/AddressBookProvider.php', + 'OCA\\ContactsInteraction\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\ContactsInteraction\\BackgroundJob\\CleanupJob' => $baseDir . '/../lib/BackgroundJob/CleanupJob.php', + 'OCA\\ContactsInteraction\\Card' => $baseDir . '/../lib/Card.php', + 'OCA\\ContactsInteraction\\Db\\CardSearchDao' => $baseDir . '/../lib/Db/CardSearchDao.php', + 'OCA\\ContactsInteraction\\Db\\RecentContact' => $baseDir . '/../lib/Db/RecentContact.php', + 'OCA\\ContactsInteraction\\Db\\RecentContactMapper' => $baseDir . '/../lib/Db/RecentContactMapper.php', + 'OCA\\ContactsInteraction\\Listeners\\ContactInteractionListener' => $baseDir . '/../lib/Listeners/ContactInteractionListener.php', + 'OCA\\ContactsInteraction\\Migration\\Version010000Date20200304152605' => $baseDir . '/../lib/Migration/Version010000Date20200304152605.php', +); diff --git a/apps/contactsinteraction/composer/composer/autoload_namespaces.php b/apps/contactsinteraction/composer/composer/autoload_namespaces.php new file mode 100644 index 00000000000..71c9e91858d --- /dev/null +++ b/apps/contactsinteraction/composer/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ +<?php + +// autoload_namespaces.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = $vendorDir; + +return array( +); diff --git a/apps/contactsinteraction/composer/composer/autoload_psr4.php b/apps/contactsinteraction/composer/composer/autoload_psr4.php new file mode 100644 index 00000000000..945013a79f5 --- /dev/null +++ b/apps/contactsinteraction/composer/composer/autoload_psr4.php @@ -0,0 +1,10 @@ +<?php + +// autoload_psr4.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = $vendorDir; + +return array( + 'OCA\\ContactsInteraction\\' => array($baseDir . '/../lib'), +); diff --git a/apps/contactsinteraction/composer/composer/autoload_real.php b/apps/contactsinteraction/composer/composer/autoload_real.php new file mode 100644 index 00000000000..efb953248d5 --- /dev/null +++ b/apps/contactsinteraction/composer/composer/autoload_real.php @@ -0,0 +1,46 @@ +<?php + +// autoload_real.php @generated by Composer + +class ComposerAutoloaderInitContactsInteraction +{ + private static $loader; + + public static function loadClassLoader($class) + { + if ('Composer\Autoload\ClassLoader' === $class) { + require __DIR__ . '/ClassLoader.php'; + } + } + + /** + * @return \Composer\Autoload\ClassLoader + */ + public static function getLoader() + { + if (null !== self::$loader) { + return self::$loader; + } + + spl_autoload_register(array('ComposerAutoloaderInitContactsInteraction', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(); + spl_autoload_unregister(array('ComposerAutoloaderInitContactsInteraction', 'loadClassLoader')); + + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitContactsInteraction::getInitializer($loader)); + } else { + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->setClassMapAuthoritative(true); + $loader->register(true); + + return $loader; + } +} diff --git a/apps/contactsinteraction/composer/composer/autoload_static.php b/apps/contactsinteraction/composer/composer/autoload_static.php new file mode 100644 index 00000000000..892ed8d92f2 --- /dev/null +++ b/apps/contactsinteraction/composer/composer/autoload_static.php @@ -0,0 +1,45 @@ +<?php + +// autoload_static.php @generated by Composer + +namespace Composer\Autoload; + +class ComposerStaticInitContactsInteraction +{ + public static $prefixLengthsPsr4 = array ( + 'O' => + array ( + 'OCA\\ContactsInteraction\\' => 24, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'OCA\\ContactsInteraction\\' => + array ( + 0 => __DIR__ . '/..' . '/../lib', + ), + ); + + public static $classMap = array ( + 'OCA\\ContactsInteraction\\AddressBook' => __DIR__ . '/..' . '/../lib/AddressBook.php', + 'OCA\\ContactsInteraction\\AddressBookProvider' => __DIR__ . '/..' . '/../lib/AddressBookProvider.php', + 'OCA\\ContactsInteraction\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\ContactsInteraction\\BackgroundJob\\CleanupJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupJob.php', + 'OCA\\ContactsInteraction\\Card' => __DIR__ . '/..' . '/../lib/Card.php', + 'OCA\\ContactsInteraction\\Db\\CardSearchDao' => __DIR__ . '/..' . '/../lib/Db/CardSearchDao.php', + 'OCA\\ContactsInteraction\\Db\\RecentContact' => __DIR__ . '/..' . '/../lib/Db/RecentContact.php', + 'OCA\\ContactsInteraction\\Db\\RecentContactMapper' => __DIR__ . '/..' . '/../lib/Db/RecentContactMapper.php', + 'OCA\\ContactsInteraction\\Listeners\\ContactInteractionListener' => __DIR__ . '/..' . '/../lib/Listeners/ContactInteractionListener.php', + 'OCA\\ContactsInteraction\\Migration\\Version010000Date20200304152605' => __DIR__ . '/..' . '/../lib/Migration/Version010000Date20200304152605.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitContactsInteraction::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitContactsInteraction::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitContactsInteraction::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/apps/contactsinteraction/lib/AddressBook.php b/apps/contactsinteraction/lib/AddressBook.php new file mode 100644 index 00000000000..6e015780378 --- /dev/null +++ b/apps/contactsinteraction/lib/AddressBook.php @@ -0,0 +1,178 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction; + +use Exception; +use OCA\ContactsInteraction\AppInfo\Application; +use OCA\ContactsInteraction\Db\RecentContact; +use OCA\ContactsInteraction\Db\RecentContactMapper; +use OCA\DAV\CardDAV\Integration\ExternalAddressBook; +use OCA\DAV\DAV\Sharing\Plugin; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IL10N; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\NotImplemented; +use Sabre\DAV\PropPatch; +use Sabre\DAVACL\ACLTrait; +use Sabre\DAVACL\IACL; + +class AddressBook extends ExternalAddressBook implements IACL { + + public const URI = 'recent'; + + use ACLTrait; + + /** @var RecentContactMapper */ + private $mapper; + + /** @var IL10N */ + private $l10n; + + /** @var string */ + private $principalUri; + + public function __construct(RecentContactMapper $mapper, + IL10N $l10n, + string $principalUri) { + parent::__construct(Application::APP_ID, self::URI); + + $this->mapper = $mapper; + $this->l10n = $l10n; + $this->principalUri = $principalUri; + } + + /** + * @inheritDoc + */ + public function delete(): void { + throw new Exception("This addressbook is immutable"); + } + + /** + * @inheritDoc + */ + function createFile($name, $data = null) { + throw new Exception("This addressbook is immutable"); + } + + /** + * @inheritDoc + * @throws NotFound + */ + public function getChild($name) { + try { + return new Card( + $this->mapper->find( + $this->getUid(), + (int)$name + ), + $this->principalUri, + $this->getACL() + ); + } catch (DoesNotExistException $ex) { + throw new NotFound("Contact does not exist: " . $ex->getMessage(), 0, $ex); + } + } + + /** + * @inheritDoc + */ + public function getChildren(): array { + return array_map( + function (RecentContact $contact) { + return new Card( + $contact, + $this->principalUri, + $this->getACL() + ); + }, + $this->mapper->findAll($this->getUid()) + ); + } + + /** + * @inheritDoc + */ + public function childExists($name) { + try { + $this->mapper->find( + $this->getUid(), + (int)$name + ); + return true; + } catch (DoesNotExistException $e) { + return false; + } + } + + /** + * @inheritDoc + */ + public function getLastModified() { + throw new NotImplemented(); + } + + /** + * @inheritDoc + */ + public function propPatch(PropPatch $propPatch) { + throw new Exception("This addressbook is immutable"); + } + + /** + * @inheritDoc + */ + public function getProperties($properties) { + return [ + 'principaluri' => $this->principalUri, + '{DAV:}displayname' => $this->l10n->t('Recently contacted'), + '{' . Plugin::NS_OWNCLOUD . '}read-only' => true, + ]; + } + + public function getOwner(): string { + return $this->principalUri; + } + + /** + * @inheritDoc + */ + public function getACL() { + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + ]; + } + + private function getUid(): string { + list(, $uid) = \Sabre\Uri\split($this->principalUri); + return $uid; + } + +} diff --git a/apps/contactsinteraction/lib/AddressBookProvider.php b/apps/contactsinteraction/lib/AddressBookProvider.php new file mode 100644 index 00000000000..6d16d1da0a5 --- /dev/null +++ b/apps/contactsinteraction/lib/AddressBookProvider.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction; + +use OCA\ContactsInteraction\AppInfo\Application; +use OCA\ContactsInteraction\Db\RecentContactMapper; +use OCA\DAV\CardDAV\Integration\ExternalAddressBook; +use OCA\DAV\CardDAV\Integration\IAddressBookProvider; +use OCP\IL10N; + +class AddressBookProvider implements IAddressBookProvider { + + /** @var RecentContactMapper */ + private $mapper; + + /** @var IL10N */ + private $l10n; + + public function __construct(RecentContactMapper $mapper, IL10N $l10n) { + $this->mapper = $mapper; + $this->l10n = $l10n; + } + + /** + * @inheritDoc + */ + public function getAppId(): string { + return Application::APP_ID; + } + + /** + * @inheritDoc + */ + public function fetchAllForAddressBookHome(string $principalUri): array { + return [ + new AddressBook($this->mapper, $this->l10n, $principalUri) + ]; + } + + /** + * @inheritDoc + */ + public function hasAddressBookInAddressBookHome(string $principalUri, string $uri): bool { + return $uri === AddressBook::URI; + } + + /** + * @inheritDoc + */ + public function getAddressBookInAddressBookHome(string $principalUri, string $uri): ?ExternalAddressBook { + if ($uri === AddressBook::URI) { + return new AddressBook($this->mapper, $this->l10n, $principalUri); + } + + return null; + } + +} diff --git a/apps/contactsinteraction/lib/AppInfo/Application.php b/apps/contactsinteraction/lib/AppInfo/Application.php new file mode 100644 index 00000000000..674034cbe48 --- /dev/null +++ b/apps/contactsinteraction/lib/AppInfo/Application.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction\AppInfo; + +use OCA\ContactsInteraction\AddressBook; +use OCA\ContactsInteraction\Listeners\ContactInteractionListener; +use OCA\ContactsInteraction\Store; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; +use OCP\Contacts\Events\ContactInteractedWithEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\EventDispatcher\IEventListener; +use OCP\IL10N; + +class Application extends App { + + public const APP_ID = 'contactsinteraction'; + + public function __construct() { + parent::__construct(self::APP_ID); + + $this->registerListeners($this->getContainer()->query(IEventDispatcher::class)); + } + + private function registerListeners(IEventDispatcher $dispatcher): void { + $dispatcher->addServiceListener(ContactInteractedWithEvent::class, ContactInteractionListener::class); + } + +} diff --git a/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php b/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php new file mode 100644 index 00000000000..0efc9d54e81 --- /dev/null +++ b/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction\BackgroundJob; + +use OCA\ContactsInteraction\Db\RecentContactMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; + +class CleanupJob extends TimedJob { + + /** @var RecentContactMapper */ + private $mapper; + + public function __construct(ITimeFactory $time, + RecentContactMapper $mapper) { + parent::__construct($time); + + $this->setInterval(12 * 60 * 60); + + $this->mapper = $mapper; + } + + protected function run($argument) { + $time = $this->time->getDateTime(); + $time->modify('-7days'); + $this->mapper->cleanUp($time->getTimestamp()); + } + +} diff --git a/apps/contactsinteraction/lib/Card.php b/apps/contactsinteraction/lib/Card.php new file mode 100644 index 00000000000..264f0ebe96f --- /dev/null +++ b/apps/contactsinteraction/lib/Card.php @@ -0,0 +1,137 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction; + +use OCA\ContactsInteraction\Db\RecentContact; +use Sabre\CardDAV\ICard; +use Sabre\DAV\Exception\NotImplemented; +use Sabre\DAVACL\ACLTrait; +use Sabre\DAVACL\IACL; + +class Card implements ICard, IACL { + + use ACLTrait; + + /** @var RecentContact */ + private $contact; + + /** @var string */ + private $principal; + + /** @var array */ + private $acls; + + public function __construct(RecentContact $contact, string $principal, array $acls) { + $this->contact = $contact; + $this->principal = $principal; + $this->acls = $acls; + } + + /** + * @inheritDoc + */ + function getOwner(): ?string { + $this->principal; + } + + /** + * @inheritDoc + */ + function getACL(): array { + return $this->acls; + } + + /** + * @inheritDoc + */ + function setAcls(array $acls): void { + throw new NotImplemented(); + } + + /** + * @inheritDoc + */ + function put($data): ?string { + throw new NotImplemented(); + } + + /** + * @inheritDoc + */ + function get() { + return $this->contact->getCard(); + } + + /** + * @inheritDoc + */ + function getContentType(): ?string { + return 'text/vcard; charset=utf-8'; + } + + /** + * @inheritDoc + */ + function getETag(): ?string { + return null; + } + + /** + * @inheritDoc + */ + function getSize(): int { + throw new NotImplemented(); + } + + /** + * @inheritDoc + */ + function delete(): void { + throw new NotImplemented(); + } + + /** + * @inheritDoc + */ + function getName(): string { + return (string) $this->contact->getId(); + } + + /** + * @inheritDoc + */ + function setName($name): void { + throw new NotImplemented(); + } + + /** + * @inheritDoc + */ + function getLastModified(): ?int { + return $this->contact->getLastContact(); + } + +} diff --git a/apps/contactsinteraction/lib/Db/CardSearchDao.php b/apps/contactsinteraction/lib/Db/CardSearchDao.php new file mode 100644 index 00000000000..8370203bb9e --- /dev/null +++ b/apps/contactsinteraction/lib/Db/CardSearchDao.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction\Db; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUser; + +class CardSearchDao { + + /** @var IDBConnection */ + private $db; + + public function __construct(IDBConnection $db) { + $this->db = $db; + } + + public function findExisting(IUser $user, + ?string $uid, + ?string $email, + ?string $cloudId): ?string { + $addressbooksQuery = $this->db->getQueryBuilder(); + $cardQuery = $this->db->getQueryBuilder(); + $propQuery = $this->db->getQueryBuilder(); + + $propOr = $propQuery->expr()->orX(); + if ($uid !== null) { + $propOr->add($propQuery->expr()->andX( + $propQuery->expr()->eq('name', $cardQuery->createNamedParameter('UID')), + $propQuery->expr()->eq('value', $cardQuery->createNamedParameter($uid)) + )); + } + if ($email !== null) { + $propOr->add($propQuery->expr()->andX( + $propQuery->expr()->eq('name', $cardQuery->createNamedParameter('EMAIL')), + $propQuery->expr()->eq('value', $cardQuery->createNamedParameter($email)) + )); + } + if ($cloudId !== null) { + $propOr->add($propQuery->expr()->andX( + $propQuery->expr()->eq('name', $cardQuery->createNamedParameter('CLOUD')), + $propQuery->expr()->eq('value', $cardQuery->createNamedParameter($cloudId)) + )); + } + $addressbooksQuery->selectDistinct('id') + ->from('addressbooks') + ->where($addressbooksQuery->expr()->eq('principaluri', $cardQuery->createNamedParameter("principals/users/" . $user->getUID()))); + $propQuery->selectDistinct('cardid') + ->from('cards_properties') + ->where($propQuery->expr()->in('addressbookid', $propQuery->createFunction($addressbooksQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY)) + ->andWhere($propOr) + ->groupBy('cardid'); + $cardQuery->select('carddata') + ->from('cards') + ->where($cardQuery->expr()->in('id', $cardQuery->createFunction($propQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY)) + ->andWhere($cardQuery->expr()->in('addressbookid', $cardQuery->createFunction($addressbooksQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY)) + ->setMaxResults(1); + $result = $cardQuery->execute(); + /** @var string|false $card */ + $card = $result->fetchColumn(0); + + if ($card === false) { + return null; + } + + return $card; + } + +} diff --git a/apps/contactsinteraction/lib/Db/RecentContact.php b/apps/contactsinteraction/lib/Db/RecentContact.php new file mode 100644 index 00000000000..71b58353efb --- /dev/null +++ b/apps/contactsinteraction/lib/Db/RecentContact.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction\Db; + +use OCP\AppFramework\Db\Entity; + +/** + * @method void setActorUid(string $uid) + * @method string|null getActorUid() + * @method void setUid(string $uid) + * @method string|null getUid() + * @method void setEmail(string $email) + * @method string|null getEmail() + * @method void setFederatedCloudId(string $federatedCloudId) + * @method string|null getFederatedCloudId() + * @method void setCard(string $card) + * @method string getCard() + * @method void setLastContact(int $lastContact) + * @method int getLastContact() + */ +class RecentContact extends Entity { + + /** @var string */ + protected $actorUid; + + /** @var string|null */ + protected $uid; + + /** @var string|null */ + protected $email; + + /** @var string|null */ + protected $federatedCloudId; + + /** @var string */ + protected $card; + + /** @var int */ + protected $lastContact; + + public function __construct() { + $this->addType('actorUid', 'string'); + $this->addType('uid', 'string'); + $this->addType('email', 'string'); + $this->addType('federatedCloudId', 'string'); + $this->addType('card', 'string'); + $this->addType('lastContact', 'int'); + } + +} diff --git a/apps/contactsinteraction/lib/Db/RecentContactMapper.php b/apps/contactsinteraction/lib/Db/RecentContactMapper.php new file mode 100644 index 00000000000..7fe98e6e4ec --- /dev/null +++ b/apps/contactsinteraction/lib/Db/RecentContactMapper.php @@ -0,0 +1,118 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\IDBConnection; +use OCP\IUser; + +class RecentContactMapper extends QBMapper { + + public const TABLE_NAME = 'recent_contact'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, self::TABLE_NAME); + } + + /** + * @return RecentContact[] + */ + public function findAll(string $uid): array { + $qb = $this->db->getQueryBuilder(); + + $select = $qb + ->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('actor_uid', $qb->createNamedParameter($uid))); + + return $this->findEntities($select); + } + + /** + * @param string $uid + * @param int $id + * + * @return RecentContact + * @throws DoesNotExistException + */ + public function find(string $uid, int $id): RecentContact { + $qb = $this->db->getQueryBuilder(); + + $select = $qb + ->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, $qb::PARAM_INT))) + ->andWhere($qb->expr()->eq('actor_uid', $qb->createNamedParameter($uid))); + + return $this->findEntity($select); + } + + /** + * @param IUser $user + * @param string|null $uid + * @param string|null $email + * @param string|null $cloudId + * + * @return RecentContact[] + */ + public function findMatch(IUser $user, + ?string $uid, + ?string $email, + ?string $cloudId): array { + $qb = $this->db->getQueryBuilder(); + + $or = $qb->expr()->orX(); + if ($uid !== null) { + $or->add($qb->expr()->eq('uid', $qb->createNamedParameter($uid))); + } + if ($email !== null) { + $or->add($qb->expr()->eq('email', $qb->createNamedParameter($email))); + } + if ($cloudId !== null) { + $or->add($qb->expr()->eq('federated_cloud_id', $qb->createNamedParameter($cloudId))); + } + + $select = $qb + ->select('*') + ->from($this->getTableName()) + ->where($or) + ->andWhere($qb->expr()->eq('actor_uid', $qb->createNamedParameter($user->getUID()))); + + return $this->findEntities($select); + } + + public function cleanUp(int $olderThan): void { + $qb = $this->db->getQueryBuilder(); + + $delete = $qb + ->delete($this->getTableName()) + ->where($qb->expr()->lt('last_contact', $qb->createNamedParameter($olderThan))); + + $delete->execute(); + } + +} diff --git a/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php b/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php new file mode 100644 index 00000000000..8e801f2e76e --- /dev/null +++ b/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php @@ -0,0 +1,171 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction\Listeners; + +use OCA\ContactsInteraction\Db\CardSearchDao; +use OCA\ContactsInteraction\Db\RecentContact; +use OCA\ContactsInteraction\Db\RecentContactMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Contacts\Events\ContactInteractedWithEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IUserManager; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Reader; +use Sabre\VObject\UUIDUtil; +use Throwable; + +class ContactInteractionListener implements IEventListener { + + /** @var RecentContactMapper */ + private $mapper; + + /** @var CardSearchDao */ + private $cardSearchDao; + + /** @var IUserManager */ + private $userManager; + + /** @var ITimeFactory */ + private $timeFactory; + + /** @var IL10N */ + private $l10n; + + /** @var ILogger */ + private $logger; + + public function __construct(RecentContactMapper $mapper, + CardSearchDao $cardSearchDao, + IUserManager $userManager, + ITimeFactory $timeFactory, + IL10N $l10nFactory, + ILogger $logger) { + $this->mapper = $mapper; + $this->cardSearchDao = $cardSearchDao; + $this->userManager = $userManager; + $this->timeFactory = $timeFactory; + $this->l10n = $l10nFactory; + $this->logger = $logger; + } + + public function handle(Event $event): void { + if (!($event instanceof ContactInteractedWithEvent)) { + return; + } + + if ($event->getUid() === null && $event->getEmail() === null && $event->getFederatedCloudId() === null) { + $this->logger->warning("Contact interaction event has no user identifier set"); + return; + } + + $existing = $this->mapper->findMatch( + $event->getActor(), + $event->getUid(), + $event->getEmail(), + $event->getFederatedCloudId() + ); + if (!empty($existing)) { + $now = $this->timeFactory->getTime(); + foreach ($existing as $c) { + $c->setLastContact($now); + $this->mapper->update($c); + } + + return; + } + + $contact = new RecentContact(); + $contact->setActorUid($event->getActor()->getUID()); + if ($event->getUid() !== null) { + $contact->setUid($event->getUid()); + } + if ($event->getEmail() !== null) { + $contact->setEmail($event->getEmail()); + } + if ($event->getFederatedCloudId() !== null) { + $contact->setFederatedCloudId($event->getFederatedCloudId()); + } + $contact->setLastContact($this->timeFactory->getTime()); + + $copy = $this->cardSearchDao->findExisting( + $event->getActor(), + $event->getUid(), + $event->getEmail(), + $event->getFederatedCloudId() + ); + if ($copy !== null) { + try { + $parsed = Reader::read($copy, Reader::OPTION_FORGIVING); + $parsed->CATEGORIES = $this->l10n->t('Recently contacted'); + $contact->setCard($parsed->serialize()); + } catch (Throwable $e) { + $this->logger->logException($e, [ + 'message' => 'Could not parse card to add recent category: ' . $e->getMessage(), + 'level' => ILogger::WARN, + ]); + $contact->setCard($copy); + } + } else { + $contact->setCard($this->generateCard($contact)); + } + $this->mapper->insert($contact); + } + + private function getDisplayName(?string $uid): ?string { + if ($uid === null) { + return null; + } + if (($user = $this->userManager->get($uid)) === null) { + return null; + } + + return $user->getDisplayName(); + } + + private function generateCard(RecentContact $contact): string { + $props = [ + 'URI' => UUIDUtil::getUUID(), + 'FN' => $this->getDisplayName($contact->getUid()) ?? $contact->getEmail() ?? $contact->getFederatedCloudId(), + 'CATEGORIES' => $this->l10n->t('Recently contacted'), + ]; + + if ($contact->getUid() !== null) { + $props['X-NEXTCLOUD-UID'] = $contact->getUid(); + } + if ($contact->getEmail() !== null) { + $props['EMAIL'] = $contact->getEmail(); + } + if ($contact->getFederatedCloudId() !== null) { + $props['CLOUD'] = $contact->getFederatedCloudId(); + } + + return (new VCard($props))->serialize(); + } + +} diff --git a/apps/contactsinteraction/lib/Migration/Version010000Date20200304152605.php b/apps/contactsinteraction/lib/Migration/Version010000Date20200304152605.php new file mode 100644 index 00000000000..fea763106ae --- /dev/null +++ b/apps/contactsinteraction/lib/Migration/Version010000Date20200304152605.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\ContactsInteraction\Migration; + +use Closure; +use OCA\ContactsInteraction\Db\RecentContactMapper; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version010000Date20200304152605 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * + * @return ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->createTable(RecentContactMapper::TABLE_NAME); + $table->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + ]); + $table->addColumn('actor_uid', 'string', [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('uid', 'string', [ + 'notnull' => false, + 'length' => 64, + ]); + $table->addColumn('email', 'string', [ + 'notnull' => false, + 'length' => 255, + ]); + $table->addColumn('federated_cloud_id', 'string', [ + 'notnull' => false, + 'length' => 255, + ]); + $table->addColumn('card', 'blob', [ + 'notnull' => true, + ]); + $table->addColumn('last_contact', 'integer', [ + 'notnull' => true, + 'length' => 4, + ]); + $table->setPrimaryKey(['id']); + // To find all recent entries + $table->addIndex(['actor_uid'], RecentContactMapper::TABLE_NAME . '_actor_uid'); + // To find a specific entry + $table->addIndex(['id', 'actor_uid'], RecentContactMapper::TABLE_NAME . '_id_uid'); + // To find all recent entries with a given UID + $table->addIndex(['uid'], RecentContactMapper::TABLE_NAME . '_uid'); + // To find all recent entries with a given email address + $table->addIndex(['email'], RecentContactMapper::TABLE_NAME . '_email'); + // To find all recent entries with a give federated cloud id + $table->addIndex(['federated_cloud_id'], RecentContactMapper::TABLE_NAME . '_fed_id'); + // For the cleanup + $table->addIndex(['last_contact'], RecentContactMapper::TABLE_NAME . '_last_contact'); + + return $schema; + } + +} diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php index 8b9e22db5ac..625eb2c0b80 100644 --- a/apps/dav/lib/CardDAV/UserAddressBooks.php +++ b/apps/dav/lib/CardDAV/UserAddressBooks.php @@ -28,11 +28,14 @@ declare(strict_types=1); namespace OCA\DAV\CardDAV; use OCA\DAV\AppInfo\PluginManager; +use OCA\DAV\CardDAV\Integration\IAddressBookProvider; use OCA\DAV\CardDAV\Integration\ExternalAddressBook; use OCP\IConfig; use OCP\IL10N; use Sabre\CardDAV\Backend; use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\CardDAV\IAddressBook; +use function array_map; use Sabre\DAV\MkCol; class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { @@ -56,7 +59,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { /** * Returns a list of address books * - * @return array + * @return IAddressBook[] */ function getChildren() { if ($this->l10n === null) { @@ -67,6 +70,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { } $addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); + /** @var IAddressBook[] $objects */ $objects = array_map(function(array $addressBook) { if ($addressBook['principaluri'] === 'principals/system/system') { return new SystemAddressbook($this->carddavBackend, $addressBook, $this->l10n, $this->config); @@ -74,11 +78,12 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { return new AddressBook($this->carddavBackend, $addressBook, $this->l10n); }, $addressBooks); - foreach ($this->pluginManager->getAddressBookPlugins() as $plugin) { - $plugin->fetchAllForAddressBookHome($this->principalUri); - } - return $objects; + /** @var IAddressBook[][] $objectsFromPlugins */ + $objectsFromPlugins = array_map(function(IAddressBookProvider $plugin): array { + return $plugin->fetchAllForAddressBookHome($this->principalUri); + }, $this->pluginManager->getAddressBookPlugins()); + return array_merge($objects, ...$objectsFromPlugins); } public function createExtendedCollection($name, MkCol $mkCol) { diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index da355a48ce6..046f626d04f 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -50,6 +50,7 @@ return array( 'OCA\\Files_Sharing\\ISharedStorage' => $baseDir . '/../lib/ISharedStorage.php', 'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php', 'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php', + 'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php', 'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php', 'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => $baseDir . '/../lib/Listener/UserShareAcceptanceListener.php', 'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => $baseDir . '/../lib/Middleware/OCSShareAPIMiddleware.php', diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index 08e7a502f79..c1e88744582 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -65,6 +65,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\ISharedStorage' => __DIR__ . '/..' . '/../lib/ISharedStorage.php', 'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php', 'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php', + 'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php', 'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php', 'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => __DIR__ . '/..' . '/../lib/Listener/UserShareAcceptanceListener.php', 'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/OCSShareAPIMiddleware.php', diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 40c024eea45..51f7ab75a11 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -36,9 +36,9 @@ use OCA\Files_Sharing\Capabilities; use OCA\Files_Sharing\Controller\ExternalSharesController; use OCA\Files_Sharing\Controller\ShareController; use OCA\Files_Sharing\External\Manager; -use OCA\Files_Sharing\Listener\GlobalShareAcceptanceListener; use OCA\Files_Sharing\Listener\LoadAdditionalListener; use OCA\Files_Sharing\Listener\LoadSidebarListener; +use OCA\Files_Sharing\Listener\ShareInteractionListener; use OCA\Files_Sharing\Listener\UserAddedToGroupListener; use OCA\Files_Sharing\Listener\UserShareAcceptanceListener; use OCA\Files_Sharing\Middleware\OCSShareAPIMiddleware; @@ -213,6 +213,7 @@ class Application extends App { // sidebar and files scripts $dispatcher->addServiceListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); $dispatcher->addServiceListener(LoadSidebar::class, LoadSidebarListener::class); + $dispatcher->addServiceListener(ShareCreatedEvent::class, ShareInteractionListener::class); $dispatcher->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function() { \OCP\Util::addScript('files_sharing', 'dist/collaboration'); }); diff --git a/apps/files_sharing/lib/Listener/ShareInteractionListener.php b/apps/files_sharing/lib/Listener/ShareInteractionListener.php new file mode 100644 index 00000000000..de4753d3da8 --- /dev/null +++ b/apps/files_sharing/lib/Listener/ShareInteractionListener.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Files_Sharing\Listener; + +use OCP\Contacts\Events\ContactInteractedWithEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\EventDispatcher\IEventListener; +use OCP\ILogger; +use OCP\IUserManager; +use OCP\Share\Events\ShareCreatedEvent; +use OCP\Share\IShare; +use function in_array; + +class ShareInteractionListener implements IEventListener { + + private const SUPPORTED_SHARE_TYPES = [ + IShare::TYPE_USER, + IShare::TYPE_EMAIL, + IShare::TYPE_REMOTE, + ]; + + /** @var IEventDispatcher */ + private $dispatcher; + + /** @var IUserManager */ + private $userManager; + + /** @var ILogger */ + private $logger; + + public function __construct(IEventDispatcher $dispatcher, + IUserManager $userManager, + ILogger $logger) { + $this->dispatcher = $dispatcher; + $this->userManager = $userManager; + $this->logger = $logger; + } + + public function handle(Event $event): void { + if (!($event instanceof ShareCreatedEvent)) { + // Unrelated + return; + } + + $share = $event->getShare(); + if (!in_array($share->getShareType(), self::SUPPORTED_SHARE_TYPES, true)) { + $this->logger->debug('Share type does not allow to emit interaction event'); + return; + } + $actor = $this->userManager->get($share->getSharedBy()); + if ($actor === null) { + $this->logger->warning('Share was not created by a user, can\'t emit interaction event'); + return; + } + $interactionEvent = new ContactInteractedWithEvent($actor); + switch ($share->getShareType()) { + case IShare::TYPE_USER: + $interactionEvent->setUid($share->getSharedWith()); + break; + case IShare::TYPE_EMAIL: + $interactionEvent->setEmail($share->getSharedWith()); + break; + case IShare::TYPE_REMOTE: + $interactionEvent->setFederatedCloudId($share->getSharedWith()); + break; + } + + $this->dispatcher->dispatchTyped($interactionEvent); + } + +} diff --git a/core/Command/App/ListApps.php b/core/Command/App/ListApps.php index 5ee575f60d9..6c3d4bb743d 100644 --- a/core/Command/App/ListApps.php +++ b/core/Command/App/ListApps.php @@ -66,7 +66,7 @@ class ListApps extends Base { } else { $shippedFilter = null; } - + $apps = \OC_App::getAllApps(); $enabledApps = $disabledApps = []; $versions = \OC_App::getAppVersions(); diff --git a/core/shipped.json b/core/shipped.json index da408d5a347..b700b3fab37 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -5,6 +5,7 @@ "admin_audit", "cloud_federation_api", "comments", + "contactsinteraction", "dav", "encryption", "federatedfilesharing", diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 94addec59c6..388c7906eb8 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -147,6 +147,7 @@ return array( 'OCP\\Contacts\\ContactsMenu\\IEntry' => $baseDir . '/lib/public/Contacts/ContactsMenu/IEntry.php', 'OCP\\Contacts\\ContactsMenu\\ILinkAction' => $baseDir . '/lib/public/Contacts/ContactsMenu/ILinkAction.php', 'OCP\\Contacts\\ContactsMenu\\IProvider' => $baseDir . '/lib/public/Contacts/ContactsMenu/IProvider.php', + 'OCP\\Contacts\\Events\\ContactInteractedWithEvent' => $baseDir . '/lib/public/Contacts/Events/ContactInteractedWithEvent.php', 'OCP\\Contacts\\IManager' => $baseDir . '/lib/public/Contacts/IManager.php', 'OCP\\DB\\ISchemaWrapper' => $baseDir . '/lib/public/DB/ISchemaWrapper.php', 'OCP\\DB\\QueryBuilder\\ICompositeExpression' => $baseDir . '/lib/public/DB/QueryBuilder/ICompositeExpression.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d8f68ccd5ea..cfc6d9842df 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -176,6 +176,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Contacts\\ContactsMenu\\IEntry' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IEntry.php', 'OCP\\Contacts\\ContactsMenu\\ILinkAction' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/ILinkAction.php', 'OCP\\Contacts\\ContactsMenu\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IProvider.php', + 'OCP\\Contacts\\Events\\ContactInteractedWithEvent' => __DIR__ . '/../../..' . '/lib/public/Contacts/Events/ContactInteractedWithEvent.php', 'OCP\\Contacts\\IManager' => __DIR__ . '/../../..' . '/lib/public/Contacts/IManager.php', 'OCP\\DB\\ISchemaWrapper' => __DIR__ . '/../../..' . '/lib/public/DB/ISchemaWrapper.php', 'OCP\\DB\\QueryBuilder\\ICompositeExpression' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/ICompositeExpression.php', diff --git a/lib/public/Contacts/Events/ContactInteractedWithEvent.php b/lib/public/Contacts/Events/ContactInteractedWithEvent.php new file mode 100644 index 00000000000..a21e17124c8 --- /dev/null +++ b/lib/public/Contacts/Events/ContactInteractedWithEvent.php @@ -0,0 +1,136 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\Contacts\Events; + +use OCP\EventDispatcher\Event; +use OCP\IUser; + +/** + * An event that allows apps to notify other components about an interaction + * between two users. This can be used to build better recommendations and + * suggestions in user interfaces. + * + * Emitters should add at least one identifier (uid, email, federated cloud ID) + * of the recipient of the interaction. + * + * @since 19.0.0 + */ +class ContactInteractedWithEvent extends Event { + + /** @var IUser */ + private $actor; + + /** @var string|null */ + private $uid; + + /** @var string|null */ + private $email; + + /** @var string|null */ + private $federatedCloudId; + + /** + * @param IUser $actor the user who started the interaction + * + * @since 19.0.0 + */ + public function __construct(IUser $actor) { + parent::__construct(); + $this->actor = $actor; + } + + /** + * @return IUser + * @since 19.0.0 + */ + public function getActor(): IUser { + return $this->actor; + } + + /** + * @return string|null + * @since 19.0.0 + */ + public function getUid(): ?string { + return $this->uid; + } + + /** + * Set the uid of the person interacted with, if known + * + * @param string $uid + * + * @return self + * @since 19.0.0 + */ + public function setUid(string $uid): self { + $this->uid = $uid; + return $this; + } + + /** + * @return string|null + * @since 19.0.0 + */ + public function getEmail(): ?string { + return $this->email; + } + + /** + * Set the email of the person interacted with, if known + * + * @param string $email + * + * @return self + * @since 19.0.0 + */ + public function setEmail(string $email): self { + $this->email = $email; + return $this; + } + + /** + * @return string|null + * @since 19.0.0 + */ + public function getFederatedCloudId(): ?string { + return $this->federatedCloudId; + } + + /** + * Set the federated cloud of the person interacted with, if known + * + * @param string $federatedCloudId + * + * @return self + * @since 19.0.0 + */ + public function setFederatedCloudId(string $federatedCloudId): self { + $this->federatedCloudId = $federatedCloudId; + return $this; + } + +} |