diff options
Diffstat (limited to 'apps/cloud_federation_api')
98 files changed, 2342 insertions, 359 deletions
diff --git a/apps/cloud_federation_api/README.md b/apps/cloud_federation_api/README.md index 34b5045eaf9..4597269da43 100644 --- a/apps/cloud_federation_api/README.md +++ b/apps/cloud_federation_api/README.md @@ -1,2 +1,6 @@ +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> # cloud_federation_api -The cloud federation API allows to share information like files, contacts, calendars, incoming calls, etc accross Nextcloud instances +The cloud federation API allows to share information like files, contacts, calendars, incoming calls, etc across Nextcloud instances diff --git a/apps/cloud_federation_api/appinfo/info.xml b/apps/cloud_federation_api/appinfo/info.xml index f747d971cc7..81343cb49bf 100644 --- a/apps/cloud_federation_api/appinfo/info.xml +++ b/apps/cloud_federation_api/appinfo/info.xml @@ -1,20 +1,24 @@ <?xml version="1.0"?> +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd"> <id>cloud_federation_api</id> <name>Cloud Federation API</name> <summary>Enable clouds to communicate with each other and exchange data</summary> <description>The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data.</description> - <version>1.4.0</version> + <version>1.16.0</version> <licence>agpl</licence> <author>Bjoern Schiessle</author> <namespace>CloudFederationAPI</namespace> <types> <filesystem/> </types> - <category>files</category> - <bugs>https://github.com/nextcloud/cloud_federation/issues</bugs> + <category>integration</category> + <bugs>https://github.com/nextcloud/server/issues</bugs> <dependencies> - <nextcloud min-version="22" max-version="22"/> + <nextcloud min-version="32" max-version="32"/> </dependencies> </info> diff --git a/apps/cloud_federation_api/appinfo/routes.php b/apps/cloud_federation_api/appinfo/routes.php index cedcacb5bf6..6467005e21b 100644 --- a/apps/cloud_federation_api/appinfo/routes.php +++ b/apps/cloud_federation_api/appinfo/routes.php @@ -3,40 +3,28 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - return [ 'routes' => [ [ 'name' => 'RequestHandler#addShare', - 'url' => '/ocm/shares', + 'url' => '/shares', 'verb' => 'POST', - 'root' => '', + 'root' => '/ocm', ], [ 'name' => 'RequestHandler#receiveNotification', - 'url' => '/ocm/notifications', + 'url' => '/notifications', 'verb' => 'POST', - 'root' => '', + 'root' => '/ocm', ], + [ + 'name' => 'RequestHandler#inviteAccepted', + 'url' => '/invite-accepted', + 'verb' => 'POST', + 'root' => '/ocm', + ] ], ]; diff --git a/apps/cloud_federation_api/composer/autoload.php b/apps/cloud_federation_api/composer/autoload.php index fcf0f062846..0c261b9233d 100644 --- a/apps/cloud_federation_api/composer/autoload.php +++ b/apps/cloud_federation_api/composer/autoload.php @@ -2,6 +2,24 @@ // autoload.php @generated by Composer +if (PHP_VERSION_ID < 50600) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, $err); + } elseif (!headers_sent()) { + echo $err; + } + } + trigger_error( + $err, + E_USER_ERROR + ); +} + require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInitCloudFederationAPI::getLoader(); diff --git a/apps/cloud_federation_api/composer/composer.lock b/apps/cloud_federation_api/composer/composer.lock new file mode 100644 index 00000000000..fd0bcbcb753 --- /dev/null +++ b/apps/cloud_federation_api/composer/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/apps/cloud_federation_api/composer/composer/ClassLoader.php b/apps/cloud_federation_api/composer/composer/ClassLoader.php index 4d989a212c9..7824d8f7eaf 100644 --- a/apps/cloud_federation_api/composer/composer/ClassLoader.php +++ b/apps/cloud_federation_api/composer/composer/ClassLoader.php @@ -42,30 +42,76 @@ namespace Composer\Autoload; */ class ClassLoader { + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ private $vendorDir; // PSR-4 + /** + * @var array<string, array<string, int>> + */ private $prefixLengthsPsr4 = array(); + /** + * @var array<string, list<string>> + */ private $prefixDirsPsr4 = array(); + /** + * @var list<string> + */ private $fallbackDirsPsr4 = array(); // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array<string, array<string, list<string>>> + */ private $prefixesPsr0 = array(); + /** + * @var list<string> + */ private $fallbackDirsPsr0 = array(); + /** @var bool */ private $useIncludePath = false; + + /** + * @var array<string, string> + */ private $classMap = array(); + + /** @var bool */ private $classMapAuthoritative = false; + + /** + * @var array<string, bool> + */ private $missingClasses = array(); + + /** @var string|null */ private $apcuPrefix; + /** + * @var array<string, self> + */ private static $registeredLoaders = array(); + /** + * @param string|null $vendorDir + */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); } + /** + * @return array<string, list<string>> + */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { @@ -75,28 +121,42 @@ class ClassLoader return array(); } + /** + * @return array<string, list<string>> + */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } + /** + * @return list<string> + */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } + /** + * @return list<string> + */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } + /** + * @return array<string, string> Array of classname => path + */ public function getClassMap() { return $this->classMap; } /** - * @param array $classMap Class to filename map + * @param array<string, string> $classMap Class to filename map + * + * @return void */ public function addClassMap(array $classMap) { @@ -111,22 +171,25 @@ class ClassLoader * 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 + * @param string $prefix The prefix + * @param list<string>|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void */ public function add($prefix, $paths, $prepend = false) { + $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - (array) $paths, + $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - (array) $paths + $paths ); } @@ -135,19 +198,19 @@ class ClassLoader $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = (array) $paths; + $this->prefixesPsr0[$first][$prefix] = $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, + $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - (array) $paths + $paths ); } } @@ -156,25 +219,28 @@ class ClassLoader * 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 + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list<string>|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException + * + * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { + $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - (array) $paths, + $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - (array) $paths + $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -184,18 +250,18 @@ class ClassLoader 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; + $this->prefixDirsPsr4[$prefix] = $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, + $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - (array) $paths + $paths ); } } @@ -204,8 +270,10 @@ class ClassLoader * 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 + * @param string $prefix The prefix + * @param list<string>|string $paths The PSR-0 base directories + * + * @return void */ public function set($prefix, $paths) { @@ -220,10 +288,12 @@ class ClassLoader * 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 + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list<string>|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException + * + * @return void */ public function setPsr4($prefix, $paths) { @@ -243,6 +313,8 @@ class ClassLoader * Turns on searching the include path for class files. * * @param bool $useIncludePath + * + * @return void */ public function setUseIncludePath($useIncludePath) { @@ -265,6 +337,8 @@ class ClassLoader * that have not been registered with the class map. * * @param bool $classMapAuthoritative + * + * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { @@ -285,6 +359,8 @@ class ClassLoader * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix + * + * @return void */ public function setApcuPrefix($apcuPrefix) { @@ -305,14 +381,18 @@ class ClassLoader * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { - //no-op - } elseif ($prepend) { + return; + } + + if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); @@ -322,6 +402,8 @@ class ClassLoader /** * Unregisters this instance as an autoloader. + * + * @return void */ public function unregister() { @@ -336,15 +418,18 @@ class ClassLoader * Loads the given class or interface. * * @param string $class The name of the class - * @return bool|null True if loaded, null otherwise + * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { - includeFile($file); + $includeFile = self::$includeFile; + $includeFile($file); return true; } + + return null; } /** @@ -390,15 +475,20 @@ class ClassLoader } /** - * Returns the currently registered loaders indexed by their corresponding vendor directories. + * Returns the currently registered loaders keyed by their corresponding vendor directories. * - * @return self[] + * @return array<string, self> */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } + /** + * @param string $class + * @param string $ext + * @return string|false + */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup @@ -464,14 +554,26 @@ class ClassLoader return false; } -} -/** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - */ -function includeFile($file) -{ - include $file; + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } } diff --git a/apps/cloud_federation_api/composer/composer/InstalledVersions.php b/apps/cloud_federation_api/composer/composer/InstalledVersions.php new file mode 100644 index 00000000000..51e734a774b --- /dev/null +++ b/apps/cloud_federation_api/composer/composer/InstalledVersions.php @@ -0,0 +1,359 @@ +<?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; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/apps/cloud_federation_api/composer/composer/autoload_classmap.php b/apps/cloud_federation_api/composer/composer/autoload_classmap.php index d5c197f1d4b..3cadc540c88 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_classmap.php +++ b/apps/cloud_federation_api/composer/composer/autoload_classmap.php @@ -2,7 +2,7 @@ // autoload_classmap.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = $vendorDir; return array( @@ -11,4 +11,9 @@ return array( 'OCA\\CloudFederationAPI\\Capabilities' => $baseDir . '/../lib/Capabilities.php', 'OCA\\CloudFederationAPI\\Config' => $baseDir . '/../lib/Config.php', 'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => $baseDir . '/../lib/Controller/RequestHandlerController.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => $baseDir . '/../lib/Db/FederatedInvite.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => $baseDir . '/../lib/Db/FederatedInviteMapper.php', + 'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => $baseDir . '/../lib/Events/FederatedInviteAcceptedEvent.php', + 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => $baseDir . '/../lib/Migration/Version1016Date202502262004.php', + 'OCA\\CloudFederationAPI\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', ); diff --git a/apps/cloud_federation_api/composer/composer/autoload_namespaces.php b/apps/cloud_federation_api/composer/composer/autoload_namespaces.php index 71c9e91858d..3f5c9296251 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_namespaces.php +++ b/apps/cloud_federation_api/composer/composer/autoload_namespaces.php @@ -2,7 +2,7 @@ // autoload_namespaces.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = $vendorDir; return array( diff --git a/apps/cloud_federation_api/composer/composer/autoload_psr4.php b/apps/cloud_federation_api/composer/composer/autoload_psr4.php index a24ce444a67..de1b8cee1e9 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_psr4.php +++ b/apps/cloud_federation_api/composer/composer/autoload_psr4.php @@ -2,7 +2,7 @@ // autoload_psr4.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = $vendorDir; return array( diff --git a/apps/cloud_federation_api/composer/composer/autoload_real.php b/apps/cloud_federation_api/composer/composer/autoload_real.php index f0ee7acb591..53093c129e3 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_real.php +++ b/apps/cloud_federation_api/composer/composer/autoload_real.php @@ -23,20 +23,11 @@ class ComposerAutoloaderInitCloudFederationAPI } spl_autoload_register(array('ComposerAutoloaderInitCloudFederationAPI', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInitCloudFederationAPI', 'loadClassLoader')); - $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); - if ($useStaticLoader) { - require __DIR__ . '/autoload_static.php'; - - call_user_func(\Composer\Autoload\ComposerStaticInitCloudFederationAPI::getInitializer($loader)); - } else { - $classMap = require __DIR__ . '/autoload_classmap.php'; - if ($classMap) { - $loader->addClassMap($classMap); - } - } + require __DIR__ . '/autoload_static.php'; + call_user_func(\Composer\Autoload\ComposerStaticInitCloudFederationAPI::getInitializer($loader)); $loader->setClassMapAuthoritative(true); $loader->register(true); diff --git a/apps/cloud_federation_api/composer/composer/autoload_static.php b/apps/cloud_federation_api/composer/composer/autoload_static.php index a1dfe33706c..849b755cd2f 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_static.php +++ b/apps/cloud_federation_api/composer/composer/autoload_static.php @@ -26,6 +26,11 @@ class ComposerStaticInitCloudFederationAPI 'OCA\\CloudFederationAPI\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', 'OCA\\CloudFederationAPI\\Config' => __DIR__ . '/..' . '/../lib/Config.php', 'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => __DIR__ . '/..' . '/../lib/Controller/RequestHandlerController.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => __DIR__ . '/..' . '/../lib/Db/FederatedInvite.php', + 'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => __DIR__ . '/..' . '/../lib/Db/FederatedInviteMapper.php', + 'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/FederatedInviteAcceptedEvent.php', + 'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date202502262004.php', + 'OCA\\CloudFederationAPI\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/cloud_federation_api/composer/composer/installed.json b/apps/cloud_federation_api/composer/composer/installed.json new file mode 100644 index 00000000000..f20a6c47c6d --- /dev/null +++ b/apps/cloud_federation_api/composer/composer/installed.json @@ -0,0 +1,5 @@ +{ + "packages": [], + "dev": false, + "dev-package-names": [] +} diff --git a/apps/cloud_federation_api/composer/composer/installed.php b/apps/cloud_federation_api/composer/composer/installed.php new file mode 100644 index 00000000000..1a66c7f2416 --- /dev/null +++ b/apps/cloud_federation_api/composer/composer/installed.php @@ -0,0 +1,23 @@ +<?php return array( + 'root' => array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev' => false, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/apps/cloud_federation_api/l10n/ar.js b/apps/cloud_federation_api/l10n/ar.js new file mode 100644 index 00000000000..45df0372089 --- /dev/null +++ b/apps/cloud_federation_api/l10n/ar.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "واجهة برمجة التطبيقات API للاتحاد السحابي Cloud Federation", + "Enable clouds to communicate with each other and exchange data" : "يسمح للسحابات أن تتراسل فيما بينها و تتبادل البيانات", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "واجهة برمجة التطبيقات API للاتحاد السحابي Cloud Federation تسمح لخوادم نكست كلاود بالاتصال ببعضها البعض و تبادل البيانات." +}, +"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;"); diff --git a/apps/cloud_federation_api/l10n/ar.json b/apps/cloud_federation_api/l10n/ar.json new file mode 100644 index 00000000000..29aaedaca6d --- /dev/null +++ b/apps/cloud_federation_api/l10n/ar.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "واجهة برمجة التطبيقات API للاتحاد السحابي Cloud Federation", + "Enable clouds to communicate with each other and exchange data" : "يسمح للسحابات أن تتراسل فيما بينها و تتبادل البيانات", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "واجهة برمجة التطبيقات API للاتحاد السحابي Cloud Federation تسمح لخوادم نكست كلاود بالاتصال ببعضها البعض و تبادل البيانات." +},"pluralForm" :"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/ast.js b/apps/cloud_federation_api/l10n/ast.js new file mode 100644 index 00000000000..a8de145df5a --- /dev/null +++ b/apps/cloud_federation_api/l10n/ast.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API de la federación na nube", + "Enable clouds to communicate with each other and exchange data" : "Permite que les nubes se comuniquen ente elles ya intercambien datos", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API de la federación na nube permite que delles instancies de Nextcloud se comuniquen ente elles ya intercambien datos." +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/ast.json b/apps/cloud_federation_api/l10n/ast.json new file mode 100644 index 00000000000..a5d8651f720 --- /dev/null +++ b/apps/cloud_federation_api/l10n/ast.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API de la federación na nube", + "Enable clouds to communicate with each other and exchange data" : "Permite que les nubes se comuniquen ente elles ya intercambien datos", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API de la federación na nube permite que delles instancies de Nextcloud se comuniquen ente elles ya intercambien datos." +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/bg.js b/apps/cloud_federation_api/l10n/bg.js new file mode 100644 index 00000000000..0c431162e51 --- /dev/null +++ b/apps/cloud_federation_api/l10n/bg.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API на Cloud Federation/федериране на облак/", + "Enable clouds to communicate with each other and exchange data" : "Активиране на облаците да комуникират помежду си и да обменят данни", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API на Cloud Federation/федериране на облак/, позволява на различни екземпляри на Nextcloud да комуникират помежду си и да обменят данни. " +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/bg.json b/apps/cloud_federation_api/l10n/bg.json new file mode 100644 index 00000000000..57d73cd0ab2 --- /dev/null +++ b/apps/cloud_federation_api/l10n/bg.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API на Cloud Federation/федериране на облак/", + "Enable clouds to communicate with each other and exchange data" : "Активиране на облаците да комуникират помежду си и да обменят данни", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API на Cloud Federation/федериране на облак/, позволява на различни екземпляри на Nextcloud да комуникират помежду си и да обменят данни. " +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/ca.js b/apps/cloud_federation_api/l10n/ca.js index 389da07af89..80abccba99e 100644 --- a/apps/cloud_federation_api/l10n/ca.js +++ b/apps/cloud_federation_api/l10n/ca.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "API de la Federació cloud", - "Enable clouds to communicate with each other and exchange data" : "Permetre que els núvols es comuniquin entre si i intercanviar dades", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API de la Federació cloud permet que diverses instàncies Nextcloud es comuniquin entre si i intercanviin dades." + "Cloud Federation API" : "API de federació de núvols", + "Enable clouds to communicate with each other and exchange data" : "Permeteu que els núvols es comuniquin entre si i intercanviïn dades", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API de federació de núvols permet que diverses instàncies del Nextcloud es comuniquin entre si i intercanviïn dades." }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/ca.json b/apps/cloud_federation_api/l10n/ca.json index fa2cad5e80f..fae66aea62a 100644 --- a/apps/cloud_federation_api/l10n/ca.json +++ b/apps/cloud_federation_api/l10n/ca.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "API de la Federació cloud", - "Enable clouds to communicate with each other and exchange data" : "Permetre que els núvols es comuniquin entre si i intercanviar dades", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API de la Federació cloud permet que diverses instàncies Nextcloud es comuniquin entre si i intercanviin dades." + "Cloud Federation API" : "API de federació de núvols", + "Enable clouds to communicate with each other and exchange data" : "Permeteu que els núvols es comuniquin entre si i intercanviïn dades", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API de federació de núvols permet que diverses instàncies del Nextcloud es comuniquin entre si i intercanviïn dades." },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/cs.js b/apps/cloud_federation_api/l10n/cs.js index 567c481f960..f6176d5447d 100644 --- a/apps/cloud_federation_api/l10n/cs.js +++ b/apps/cloud_federation_api/l10n/cs.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "API pro federování cloudu", - "Enable clouds to communicate with each other and exchange data" : "Umožňuje cloudům navzájem komunikovat a vyměňovat si data", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API pro federování cloudů umožňuje různým instancím Nextcloud vzájemně komunikovat a vyměňovat si data." + "Cloud Federation API" : "API pro federovaný cloud", + "Enable clouds to communicate with each other and exchange data" : "Umožňuje cloudům vzájemně komunikovat a vyměňovat si data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API pro federovaný cloud umožňuje různým instancím Nextcloud vzájemně komunikovat a vyměňovat si data." }, "nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;"); diff --git a/apps/cloud_federation_api/l10n/cs.json b/apps/cloud_federation_api/l10n/cs.json index 5f454a38e0f..f7a47c5f27f 100644 --- a/apps/cloud_federation_api/l10n/cs.json +++ b/apps/cloud_federation_api/l10n/cs.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "API pro federování cloudu", - "Enable clouds to communicate with each other and exchange data" : "Umožňuje cloudům navzájem komunikovat a vyměňovat si data", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API pro federování cloudů umožňuje různým instancím Nextcloud vzájemně komunikovat a vyměňovat si data." + "Cloud Federation API" : "API pro federovaný cloud", + "Enable clouds to communicate with each other and exchange data" : "Umožňuje cloudům vzájemně komunikovat a vyměňovat si data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API pro federovaný cloud umožňuje různým instancím Nextcloud vzájemně komunikovat a vyměňovat si data." },"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/da.js b/apps/cloud_federation_api/l10n/da.js new file mode 100644 index 00000000000..a3a8d6a2303 --- /dev/null +++ b/apps/cloud_federation_api/l10n/da.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Cloud sammenkoblings API", + "Enable clouds to communicate with each other and exchange data" : "Gør det muligt for skyer at kommunikere med hinanden og udveksle data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud sammenkoblings API gør det muligt for forskellige Nextcloud-instanser at kommunikere med hinanden og udveksle data." +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/da.json b/apps/cloud_federation_api/l10n/da.json new file mode 100644 index 00000000000..fc26d6c54d6 --- /dev/null +++ b/apps/cloud_federation_api/l10n/da.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Cloud sammenkoblings API", + "Enable clouds to communicate with each other and exchange data" : "Gør det muligt for skyer at kommunikere med hinanden og udveksle data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud sammenkoblings API gør det muligt for forskellige Nextcloud-instanser at kommunikere med hinanden og udveksle data." +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/de_DE.js b/apps/cloud_federation_api/l10n/de_DE.js index 62f81ba77ac..7f0df974ac4 100644 --- a/apps/cloud_federation_api/l10n/de_DE.js +++ b/apps/cloud_federation_api/l10n/de_DE.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "Cloud Federation API", + "Cloud Federation API" : "Cloud-Federation-API", "Enable clouds to communicate with each other and exchange data" : "Erlaubt es, dass Server miteinander kommunizieren und Daten austauschen", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Die Cloud Federation API ermöglicht es Nextcloud-Instanzen miteinander zu kommunizieren und Daten auszutauschen." + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Die Cloud-Federation-API ermöglicht es Nextcloud-Instanzen miteinander zu kommunizieren und Daten auszutauschen." }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/de_DE.json b/apps/cloud_federation_api/l10n/de_DE.json index 08c4ad7ae0e..98d772c755a 100644 --- a/apps/cloud_federation_api/l10n/de_DE.json +++ b/apps/cloud_federation_api/l10n/de_DE.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "Cloud Federation API", + "Cloud Federation API" : "Cloud-Federation-API", "Enable clouds to communicate with each other and exchange data" : "Erlaubt es, dass Server miteinander kommunizieren und Daten austauschen", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Die Cloud Federation API ermöglicht es Nextcloud-Instanzen miteinander zu kommunizieren und Daten auszutauschen." + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Die Cloud-Federation-API ermöglicht es Nextcloud-Instanzen miteinander zu kommunizieren und Daten auszutauschen." },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/eo.js b/apps/cloud_federation_api/l10n/eo.js new file mode 100644 index 00000000000..d01810bba74 --- /dev/null +++ b/apps/cloud_federation_api/l10n/eo.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API de Nuba Federacio", + "Enable clouds to communicate with each other and exchange data" : "Ebligi interkomunikadon inter diversaj nuboj kaj la interŝanĝon de datumoj", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de Nuba Federacio ebligas diversajn instancojn de Nextcloud interkomuniki kaj interŝanĝi datumojn inter si." +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/eo.json b/apps/cloud_federation_api/l10n/eo.json new file mode 100644 index 00000000000..17dd80538d8 --- /dev/null +++ b/apps/cloud_federation_api/l10n/eo.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API de Nuba Federacio", + "Enable clouds to communicate with each other and exchange data" : "Ebligi interkomunikadon inter diversaj nuboj kaj la interŝanĝon de datumoj", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de Nuba Federacio ebligas diversajn instancojn de Nextcloud interkomuniki kaj interŝanĝi datumojn inter si." +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/es.js b/apps/cloud_federation_api/l10n/es.js index 6a9c9151651..26b6df89cea 100644 --- a/apps/cloud_federation_api/l10n/es.js +++ b/apps/cloud_federation_api/l10n/es.js @@ -5,4 +5,4 @@ OC.L10N.register( "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre ellas e intercambien datos", "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API Cloud Federation permite que varias instancias de Nextcloud se comuniquen entre ellas e intercambien datos." }, -"nplurals=2; plural=(n != 1);"); +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/es.json b/apps/cloud_federation_api/l10n/es.json index aaeaabcc9db..8bf650e7c41 100644 --- a/apps/cloud_federation_api/l10n/es.json +++ b/apps/cloud_federation_api/l10n/es.json @@ -2,5 +2,5 @@ "Cloud Federation API" : "Cloud Federation API", "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre ellas e intercambien datos", "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API Cloud Federation permite que varias instancias de Nextcloud se comuniquen entre ellas e intercambien datos." -},"pluralForm" :"nplurals=2; plural=(n != 1);" +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/es_CL.js b/apps/cloud_federation_api/l10n/es_CL.js new file mode 100644 index 00000000000..5f223cbf59c --- /dev/null +++ b/apps/cloud_federation_api/l10n/es_CL.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API de Federación en la Nube", + "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre sí e intercambien datos", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de Federación de Nubes permite que varias instancias de Nextcloud se comuniquen entre sí y intercambien datos." +}, +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/es_CL.json b/apps/cloud_federation_api/l10n/es_CL.json new file mode 100644 index 00000000000..32f7e283911 --- /dev/null +++ b/apps/cloud_federation_api/l10n/es_CL.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API de Federación en la Nube", + "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre sí e intercambien datos", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de Federación de Nubes permite que varias instancias de Nextcloud se comuniquen entre sí y intercambien datos." +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/es_EC.js b/apps/cloud_federation_api/l10n/es_EC.js new file mode 100644 index 00000000000..5f223cbf59c --- /dev/null +++ b/apps/cloud_federation_api/l10n/es_EC.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API de Federación en la Nube", + "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre sí e intercambien datos", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de Federación de Nubes permite que varias instancias de Nextcloud se comuniquen entre sí y intercambien datos." +}, +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/es_EC.json b/apps/cloud_federation_api/l10n/es_EC.json new file mode 100644 index 00000000000..32f7e283911 --- /dev/null +++ b/apps/cloud_federation_api/l10n/es_EC.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API de Federación en la Nube", + "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre sí e intercambien datos", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de Federación de Nubes permite que varias instancias de Nextcloud se comuniquen entre sí y intercambien datos." +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/es_MX.js b/apps/cloud_federation_api/l10n/es_MX.js new file mode 100644 index 00000000000..9022bd89dfb --- /dev/null +++ b/apps/cloud_federation_api/l10n/es_MX.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API de la federación en la nube", + "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre sí e intercambien datos", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de la federación en la nube permite que varias instancias de Nextcloud se comuniquen entre sí e intercambien datos." +}, +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/es_MX.json b/apps/cloud_federation_api/l10n/es_MX.json new file mode 100644 index 00000000000..8726b94be1c --- /dev/null +++ b/apps/cloud_federation_api/l10n/es_MX.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API de la federación en la nube", + "Enable clouds to communicate with each other and exchange data" : "Permitir que las nubes se comuniquen entre sí e intercambien datos", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "La API de la federación en la nube permite que varias instancias de Nextcloud se comuniquen entre sí e intercambien datos." +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/et_EE.js b/apps/cloud_federation_api/l10n/et_EE.js new file mode 100644 index 00000000000..67ae7128738 --- /dev/null +++ b/apps/cloud_federation_api/l10n/et_EE.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Pilve Liit API", + "Enable clouds to communicate with each other and exchange data" : "Luba pilvedel suhelda omavahel ja vahetada andmeid", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Pilve Liit API lubab erinevad Nextcloud'i osadel suhelda omavahel ja vahetada andmeid" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/et_EE.json b/apps/cloud_federation_api/l10n/et_EE.json new file mode 100644 index 00000000000..5fbc024b957 --- /dev/null +++ b/apps/cloud_federation_api/l10n/et_EE.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Pilve Liit API", + "Enable clouds to communicate with each other and exchange data" : "Luba pilvedel suhelda omavahel ja vahetada andmeid", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Pilve Liit API lubab erinevad Nextcloud'i osadel suhelda omavahel ja vahetada andmeid" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/eu.js b/apps/cloud_federation_api/l10n/eu.js index 105abbf19e5..ea05a6a6c89 100644 --- a/apps/cloud_federation_api/l10n/eu.js +++ b/apps/cloud_federation_api/l10n/eu.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "Cloud Federation API", - "Enable clouds to communicate with each other and exchange data" : "Aukera ematen du beste hodeirekin komunikatzeko eta datuak trukatzeko.", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation APIk aukera ematen du hainbat Nextcloud instantzien artean elkarri komunikatzeko eta datuak trukatzeko. " + "Cloud Federation API" : "Cloud Federation APIa", + "Enable clouds to communicate with each other and exchange data" : "Aukera ematen du beste hodeiekin komunikatzeko eta datuak trukatzeko.", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation APIak aukera ematen du hainbat Nextcloud instantziaren artean komunikatzeko eta datuak trukatzeko. " }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/eu.json b/apps/cloud_federation_api/l10n/eu.json index 358a36596a3..2452339c097 100644 --- a/apps/cloud_federation_api/l10n/eu.json +++ b/apps/cloud_federation_api/l10n/eu.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "Cloud Federation API", - "Enable clouds to communicate with each other and exchange data" : "Aukera ematen du beste hodeirekin komunikatzeko eta datuak trukatzeko.", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation APIk aukera ematen du hainbat Nextcloud instantzien artean elkarri komunikatzeko eta datuak trukatzeko. " + "Cloud Federation API" : "Cloud Federation APIa", + "Enable clouds to communicate with each other and exchange data" : "Aukera ematen du beste hodeiekin komunikatzeko eta datuak trukatzeko.", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation APIak aukera ematen du hainbat Nextcloud instantziaren artean komunikatzeko eta datuak trukatzeko. " },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/fa.js b/apps/cloud_federation_api/l10n/fa.js new file mode 100644 index 00000000000..2ef5db15303 --- /dev/null +++ b/apps/cloud_federation_api/l10n/fa.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API فدراسیون ابری", + "Enable clouds to communicate with each other and exchange data" : "ابرها را فعال کنید تا با یکدیگر ارتباط برقرار کنند و داده ها را مبادله کنند", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API نمونه های مختلف Nextcloud را قادر می سازد تا با یکدیگر ارتباط برقرار کرده و داده ها را مبادله کنند." +}, +"nplurals=2; plural=(n > 1);"); diff --git a/apps/cloud_federation_api/l10n/fa.json b/apps/cloud_federation_api/l10n/fa.json new file mode 100644 index 00000000000..c34928e1d85 --- /dev/null +++ b/apps/cloud_federation_api/l10n/fa.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API فدراسیون ابری", + "Enable clouds to communicate with each other and exchange data" : "ابرها را فعال کنید تا با یکدیگر ارتباط برقرار کنند و داده ها را مبادله کنند", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API نمونه های مختلف Nextcloud را قادر می سازد تا با یکدیگر ارتباط برقرار کرده و داده ها را مبادله کنند." +},"pluralForm" :"nplurals=2; plural=(n > 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/fi.js b/apps/cloud_federation_api/l10n/fi.js new file mode 100644 index 00000000000..036c4c9a5a3 --- /dev/null +++ b/apps/cloud_federation_api/l10n/fi.js @@ -0,0 +1,7 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Pilvifederaation rajapinta", + "Enable clouds to communicate with each other and exchange data" : "Mahdollistaa pilvien viestiä ja vaihtaa dataa toistensa kanssa" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/fi.json b/apps/cloud_federation_api/l10n/fi.json new file mode 100644 index 00000000000..81dd7adc596 --- /dev/null +++ b/apps/cloud_federation_api/l10n/fi.json @@ -0,0 +1,5 @@ +{ "translations": { + "Cloud Federation API" : "Pilvifederaation rajapinta", + "Enable clouds to communicate with each other and exchange data" : "Mahdollistaa pilvien viestiä ja vaihtaa dataa toistensa kanssa" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/fr.js b/apps/cloud_federation_api/l10n/fr.js index b4aaa45f0e0..05421436df0 100644 --- a/apps/cloud_federation_api/l10n/fr.js +++ b/apps/cloud_federation_api/l10n/fr.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "API Cloud Federation", + "Cloud Federation API" : "API de fédération Cloud", "Enable clouds to communicate with each other and exchange data" : "Permettre aux clouds de communiquer entre eux et d'échanger des données", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API Cloud Federation permet à diverses instances Nextcloud de communiquer entre elles et d'échanger des données." + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L’API de fédération cloud permet à différentes instances de Nextcloud de communiquer entre elles et d’échanger des données." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/fr.json b/apps/cloud_federation_api/l10n/fr.json index 95816791116..5a2a2a07617 100644 --- a/apps/cloud_federation_api/l10n/fr.json +++ b/apps/cloud_federation_api/l10n/fr.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "API Cloud Federation", + "Cloud Federation API" : "API de fédération Cloud", "Enable clouds to communicate with each other and exchange data" : "Permettre aux clouds de communiquer entre eux et d'échanger des données", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API Cloud Federation permet à diverses instances Nextcloud de communiquer entre elles et d'échanger des données." -},"pluralForm" :"nplurals=2; plural=(n > 1);" + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L’API de fédération cloud permet à différentes instances de Nextcloud de communiquer entre elles et d’échanger des données." +},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/ga.js b/apps/cloud_federation_api/l10n/ga.js new file mode 100644 index 00000000000..a6e46e561c9 --- /dev/null +++ b/apps/cloud_federation_api/l10n/ga.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Ligeann API Comhdhéanta na Scamaill", + "Enable clouds to communicate with each other and exchange data" : "Cumasaigh scamaill cumarsáid a dhéanamh lena chéile agus sonraí a mhalartú", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cuireann API Cloud Federation ar chumas cásanna éagsúla Nextcloud cumarsáid a dhéanamh lena chéile agus sonraí a mhalartú." +}, +"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"); diff --git a/apps/cloud_federation_api/l10n/ga.json b/apps/cloud_federation_api/l10n/ga.json new file mode 100644 index 00000000000..5c3d361aef4 --- /dev/null +++ b/apps/cloud_federation_api/l10n/ga.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Ligeann API Comhdhéanta na Scamaill", + "Enable clouds to communicate with each other and exchange data" : "Cumasaigh scamaill cumarsáid a dhéanamh lena chéile agus sonraí a mhalartú", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cuireann API Cloud Federation ar chumas cásanna éagsúla Nextcloud cumarsáid a dhéanamh lena chéile agus sonraí a mhalartú." +},"pluralForm" :"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/gl.js b/apps/cloud_federation_api/l10n/gl.js index 4c885ae3b93..06b753a1f0b 100644 --- a/apps/cloud_federation_api/l10n/gl.js +++ b/apps/cloud_federation_api/l10n/gl.js @@ -3,6 +3,6 @@ OC.L10N.register( { "Cloud Federation API" : "API da Nube federada", "Enable clouds to communicate with each other and exchange data" : "Permite que as nubes se comuniquen entre elas e intercambien datos", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "A API de Nube federada permite que varias instancias do Nextcloud se comuniquen entre elas e intercambien datos." + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "A API de Nube federada permite que varias instancias de Nextcloud se comuniquen entre elas e intercambien datos." }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/gl.json b/apps/cloud_federation_api/l10n/gl.json index 5d468a800b8..17c51a3d7b2 100644 --- a/apps/cloud_federation_api/l10n/gl.json +++ b/apps/cloud_federation_api/l10n/gl.json @@ -1,6 +1,6 @@ { "translations": { "Cloud Federation API" : "API da Nube federada", "Enable clouds to communicate with each other and exchange data" : "Permite que as nubes se comuniquen entre elas e intercambien datos", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "A API de Nube federada permite que varias instancias do Nextcloud se comuniquen entre elas e intercambien datos." + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "A API de Nube federada permite que varias instancias de Nextcloud se comuniquen entre elas e intercambien datos." },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/hu.js b/apps/cloud_federation_api/l10n/hu.js new file mode 100644 index 00000000000..a0176bd8b3c --- /dev/null +++ b/apps/cloud_federation_api/l10n/hu.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Felhő föderációs API", + "Enable clouds to communicate with each other and exchange data" : "A felhőszolgáltatások egymás közti kommunikációjának és adatcseréjének lehetővé tétele", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "A felhő föderációs API segítségével a különféle Nextcloud példányok képesek lesznek egymás között kommunikálni és adatokat cserélni." +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/hu.json b/apps/cloud_federation_api/l10n/hu.json new file mode 100644 index 00000000000..717ea7337ac --- /dev/null +++ b/apps/cloud_federation_api/l10n/hu.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Felhő föderációs API", + "Enable clouds to communicate with each other and exchange data" : "A felhőszolgáltatások egymás közti kommunikációjának és adatcseréjének lehetővé tétele", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "A felhő föderációs API segítségével a különféle Nextcloud példányok képesek lesznek egymás között kommunikálni és adatokat cserélni." +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/it.js b/apps/cloud_federation_api/l10n/it.js index 795f0579529..a8db86e9ee9 100644 --- a/apps/cloud_federation_api/l10n/it.js +++ b/apps/cloud_federation_api/l10n/it.js @@ -5,4 +5,4 @@ OC.L10N.register( "Enable clouds to communicate with each other and exchange data" : "Consenti ai cloud di comunicare tra loro e di scambiare dati", "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API Federazione Cloud consente a varie istanze di Nextcloud di comunicare tra loro e scambiare dati." }, -"nplurals=2; plural=(n != 1);"); +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/it.json b/apps/cloud_federation_api/l10n/it.json index ef52752308b..f55b725d7ee 100644 --- a/apps/cloud_federation_api/l10n/it.json +++ b/apps/cloud_federation_api/l10n/it.json @@ -2,5 +2,5 @@ "Cloud Federation API" : "API Federazione Cloud", "Enable clouds to communicate with each other and exchange data" : "Consenti ai cloud di comunicare tra loro e di scambiare dati", "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "L'API Federazione Cloud consente a varie istanze di Nextcloud di comunicare tra loro e scambiare dati." -},"pluralForm" :"nplurals=2; plural=(n != 1);" +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/ja.js b/apps/cloud_federation_api/l10n/ja.js new file mode 100644 index 00000000000..19a8d910c47 --- /dev/null +++ b/apps/cloud_federation_api/l10n/ja.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "クラウドフェデレーションAPI", + "Enable clouds to communicate with each other and exchange data" : "クラウドが相互に通信し、データを交換できるようにします", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation APIにより、さまざまなNextcloudインスタンスが相互に通信し、データを交換することができるようになります。" +}, +"nplurals=1; plural=0;"); diff --git a/apps/cloud_federation_api/l10n/ja.json b/apps/cloud_federation_api/l10n/ja.json new file mode 100644 index 00000000000..5d522b23fe3 --- /dev/null +++ b/apps/cloud_federation_api/l10n/ja.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "クラウドフェデレーションAPI", + "Enable clouds to communicate with each other and exchange data" : "クラウドが相互に通信し、データを交換できるようにします", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation APIにより、さまざまなNextcloudインスタンスが相互に通信し、データを交換することができるようになります。" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/ko.js b/apps/cloud_federation_api/l10n/ko.js new file mode 100644 index 00000000000..09af58b662e --- /dev/null +++ b/apps/cloud_federation_api/l10n/ko.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Cloud Federation API", + "Enable clouds to communicate with each other and exchange data" : "클라우드 간 소통과 데이터 교환을 가능케 합니다.", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API는 다양한 Nextcloud 인스턴스가 서로 소통하거나 데이터를 교환할 수 있도록 합니다." +}, +"nplurals=1; plural=0;"); diff --git a/apps/cloud_federation_api/l10n/ko.json b/apps/cloud_federation_api/l10n/ko.json new file mode 100644 index 00000000000..aa9e0642e29 --- /dev/null +++ b/apps/cloud_federation_api/l10n/ko.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Cloud Federation API", + "Enable clouds to communicate with each other and exchange data" : "클라우드 간 소통과 데이터 교환을 가능케 합니다.", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API는 다양한 Nextcloud 인스턴스가 서로 소통하거나 데이터를 교환할 수 있도록 합니다." +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/lt_LT.js b/apps/cloud_federation_api/l10n/lt_LT.js new file mode 100644 index 00000000000..991f0c3335a --- /dev/null +++ b/apps/cloud_federation_api/l10n/lt_LT.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Cloud Federation įskiepis", + "Enable clouds to communicate with each other and exchange data" : "Leisti debesų technologijoms jungtis ir keistis duomenimis tarpusavyje", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation įskiepis leidžia atskiriems Nextcloud serveriams jungtis ir keistis duomenimis tarpusavyje." +}, +"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"); diff --git a/apps/cloud_federation_api/l10n/lt_LT.json b/apps/cloud_federation_api/l10n/lt_LT.json new file mode 100644 index 00000000000..37bbcccdc91 --- /dev/null +++ b/apps/cloud_federation_api/l10n/lt_LT.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Cloud Federation įskiepis", + "Enable clouds to communicate with each other and exchange data" : "Leisti debesų technologijoms jungtis ir keistis duomenimis tarpusavyje", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation įskiepis leidžia atskiriems Nextcloud serveriams jungtis ir keistis duomenimis tarpusavyje." +},"pluralForm" :"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/my.js b/apps/cloud_federation_api/l10n/my.js new file mode 100644 index 00000000000..e07f0f71a4e --- /dev/null +++ b/apps/cloud_federation_api/l10n/my.js @@ -0,0 +1,7 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Cloud ဖွဲ့စည်းမှု API ", + "Enable clouds to communicate with each other and exchange data" : "Cloud များကို တစ်ခုခုနှင့်တစ်ခု ဆက်သွယ်ခြင်း၊ သတင်းအချက်အလက်ဖလှယ်ခြင်းကို ပြုလုပ်နိုင်ရန် ဖွင့်ပေးပါ။" +}, +"nplurals=1; plural=0;"); diff --git a/apps/cloud_federation_api/l10n/my.json b/apps/cloud_federation_api/l10n/my.json new file mode 100644 index 00000000000..8c1425e659f --- /dev/null +++ b/apps/cloud_federation_api/l10n/my.json @@ -0,0 +1,5 @@ +{ "translations": { + "Cloud Federation API" : "Cloud ဖွဲ့စည်းမှု API ", + "Enable clouds to communicate with each other and exchange data" : "Cloud များကို တစ်ခုခုနှင့်တစ်ခု ဆက်သွယ်ခြင်း၊ သတင်းအချက်အလက်ဖလှယ်ခြင်းကို ပြုလုပ်နိုင်ရန် ဖွင့်ပေးပါ။" +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/pt_BR.js b/apps/cloud_federation_api/l10n/pt_BR.js index f853f359ee7..210c2d16cbd 100644 --- a/apps/cloud_federation_api/l10n/pt_BR.js +++ b/apps/cloud_federation_api/l10n/pt_BR.js @@ -2,7 +2,7 @@ OC.L10N.register( "cloud_federation_api", { "Cloud Federation API" : "API de Nuvem Federada", - "Enable clouds to communicate with each other and exchange data" : "Permite que diferentes nuvens se comuniquem entre si e troquem dados", + "Enable clouds to communicate with each other and exchange data" : "Permitir que as nuvens se comuniquem entre si e troquem dados", "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "A API de Nuvem Federada permite que várias instâncias do Nextcloud se comuniquem entre si e troquem dados." }, -"nplurals=2; plural=(n > 1);"); +"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/pt_BR.json b/apps/cloud_federation_api/l10n/pt_BR.json index dd61f1a81fe..56091c2b9d6 100644 --- a/apps/cloud_federation_api/l10n/pt_BR.json +++ b/apps/cloud_federation_api/l10n/pt_BR.json @@ -1,6 +1,6 @@ { "translations": { "Cloud Federation API" : "API de Nuvem Federada", - "Enable clouds to communicate with each other and exchange data" : "Permite que diferentes nuvens se comuniquem entre si e troquem dados", + "Enable clouds to communicate with each other and exchange data" : "Permitir que as nuvens se comuniquem entre si e troquem dados", "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "A API de Nuvem Federada permite que várias instâncias do Nextcloud se comuniquem entre si e troquem dados." -},"pluralForm" :"nplurals=2; plural=(n > 1);" +},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/pt_PT.js b/apps/cloud_federation_api/l10n/pt_PT.js new file mode 100644 index 00000000000..db0fadb64f8 --- /dev/null +++ b/apps/cloud_federation_api/l10n/pt_PT.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API de Federação Cloud", + "Enable clouds to communicate with each other and exchange data" : "Enable clouds to communicate with each other and exchange data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." +}, +"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/cloud_federation_api/l10n/pt_PT.json b/apps/cloud_federation_api/l10n/pt_PT.json new file mode 100644 index 00000000000..78fdf12910a --- /dev/null +++ b/apps/cloud_federation_api/l10n/pt_PT.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API de Federação Cloud", + "Enable clouds to communicate with each other and exchange data" : "Enable clouds to communicate with each other and exchange data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." +},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/sv.js b/apps/cloud_federation_api/l10n/sv.js new file mode 100644 index 00000000000..e0bef8449b6 --- /dev/null +++ b/apps/cloud_federation_api/l10n/sv.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Moln-federations API", + "Enable clouds to communicate with each other and exchange data" : "Tillåter olika moln att kommunicera med varandra samt utbyta data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Moln-federations API:et tillåter olika Nextcloud-instanser att kommunicera med varandra samt utbyta data." +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/sv.json b/apps/cloud_federation_api/l10n/sv.json new file mode 100644 index 00000000000..c2e408362f4 --- /dev/null +++ b/apps/cloud_federation_api/l10n/sv.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Moln-federations API", + "Enable clouds to communicate with each other and exchange data" : "Tillåter olika moln att kommunicera med varandra samt utbyta data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Moln-federations API:et tillåter olika Nextcloud-instanser att kommunicera med varandra samt utbyta data." +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/sw.js b/apps/cloud_federation_api/l10n/sw.js new file mode 100644 index 00000000000..c58c7d2b6f8 --- /dev/null +++ b/apps/cloud_federation_api/l10n/sw.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "API ya Shirikisho la Cloud", + "Enable clouds to communicate with each other and exchange data" : "Washa clouds kuwasiliana na kubadilishana data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API ya Shirikisho la Cloud huwezesha matukio mbalimbali ya Nextcloud kuwasiliana na kubadilishana data." +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/sw.json b/apps/cloud_federation_api/l10n/sw.json new file mode 100644 index 00000000000..622453561cb --- /dev/null +++ b/apps/cloud_federation_api/l10n/sw.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "API ya Shirikisho la Cloud", + "Enable clouds to communicate with each other and exchange data" : "Washa clouds kuwasiliana na kubadilishana data", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API ya Shirikisho la Cloud huwezesha matukio mbalimbali ya Nextcloud kuwasiliana na kubadilishana data." +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/tr.js b/apps/cloud_federation_api/l10n/tr.js index 98c5ff91d60..1ab230f68e1 100644 --- a/apps/cloud_federation_api/l10n/tr.js +++ b/apps/cloud_federation_api/l10n/tr.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "Bulut Birleşim API", + "Cloud Federation API" : "Birleşik Bulut API", "Enable clouds to communicate with each other and exchange data" : "Karşılıklı iletişim kurmak için bulut hizmetlerini kullanın", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Bulut Birleşim API farklı Nextcloud kopyalarının birbiri ile iletişim kurarak karşılıklı veri aktarmasını sağlar." + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Birleşik Bulut API, farklı Nextcloud kopyalarının birbiri ile iletişim kurarak karşılıklı veri aktarmasını sağlar." }, "nplurals=2; plural=(n > 1);"); diff --git a/apps/cloud_federation_api/l10n/tr.json b/apps/cloud_federation_api/l10n/tr.json index c77f170f3b9..17d28152fc6 100644 --- a/apps/cloud_federation_api/l10n/tr.json +++ b/apps/cloud_federation_api/l10n/tr.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "Bulut Birleşim API", + "Cloud Federation API" : "Birleşik Bulut API", "Enable clouds to communicate with each other and exchange data" : "Karşılıklı iletişim kurmak için bulut hizmetlerini kullanın", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Bulut Birleşim API farklı Nextcloud kopyalarının birbiri ile iletişim kurarak karşılıklı veri aktarmasını sağlar." + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Birleşik Bulut API, farklı Nextcloud kopyalarının birbiri ile iletişim kurarak karşılıklı veri aktarmasını sağlar." },"pluralForm" :"nplurals=2; plural=(n > 1);" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/ug.js b/apps/cloud_federation_api/l10n/ug.js new file mode 100644 index 00000000000..7428c9eb203 --- /dev/null +++ b/apps/cloud_federation_api/l10n/ug.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "بۇلۇت فېدېراتسىيەسى API", + "Enable clouds to communicate with each other and exchange data" : "بۇلۇتلارنىڭ ئۆز-ئارا ئالاقە قىلىشى ۋە سانلىق مەلۇمات ئالماشتۇرۇشىنى قوزغىتىڭ", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "بۇلۇت فېدېراتسىيەسى API ھەر خىل Nextcloud مىساللىرىنى ئۆز-ئارا ئالاقە قىلىش ۋە سانلىق مەلۇمات ئالماشتۇرۇش ئىمكانىيىتىگە ئىگە قىلىدۇ." +}, +"nplurals=2; plural=(n != 1);"); diff --git a/apps/cloud_federation_api/l10n/ug.json b/apps/cloud_federation_api/l10n/ug.json new file mode 100644 index 00000000000..d7c588468fa --- /dev/null +++ b/apps/cloud_federation_api/l10n/ug.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "بۇلۇت فېدېراتسىيەسى API", + "Enable clouds to communicate with each other and exchange data" : "بۇلۇتلارنىڭ ئۆز-ئارا ئالاقە قىلىشى ۋە سانلىق مەلۇمات ئالماشتۇرۇشىنى قوزغىتىڭ", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "بۇلۇت فېدېراتسىيەسى API ھەر خىل Nextcloud مىساللىرىنى ئۆز-ئارا ئالاقە قىلىش ۋە سانلىق مەلۇمات ئالماشتۇرۇش ئىمكانىيىتىگە ئىگە قىلىدۇ." +},"pluralForm" :"nplurals=2; plural=(n != 1);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/uk.js b/apps/cloud_federation_api/l10n/uk.js new file mode 100644 index 00000000000..83a05116dca --- /dev/null +++ b/apps/cloud_federation_api/l10n/uk.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Cloud Federation API", + "Enable clouds to communicate with each other and exchange data" : "Увімкніть хмари аби спілкуватися один з одним і обмінюватися даними", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API дозволяє різним примірникам серверу хмари Nextcloud спілкуватися між собою та обмінюватися даними." +}, +"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"); diff --git a/apps/cloud_federation_api/l10n/uk.json b/apps/cloud_federation_api/l10n/uk.json new file mode 100644 index 00000000000..fdea9b33c38 --- /dev/null +++ b/apps/cloud_federation_api/l10n/uk.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Cloud Federation API", + "Enable clouds to communicate with each other and exchange data" : "Увімкніть хмари аби спілкуватися один з одним і обмінюватися даними", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API дозволяє різним примірникам серверу хмари Nextcloud спілкуватися між собою та обмінюватися даними." +},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/uz.js b/apps/cloud_federation_api/l10n/uz.js new file mode 100644 index 00000000000..cec1c75f757 --- /dev/null +++ b/apps/cloud_federation_api/l10n/uz.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Asl faylni o'chirishda kutilmagan xatolik yuz berdi.", + "Enable clouds to communicate with each other and exchange data" : "Bulutlar bir-biri bilan aloqa qilish va ma'lumot almashish imkonini beradi", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API turli xil Nextcloud misollariga bir-biri bilan muloqot qilish va ma'lumotlarni almashish imkonini beradi." +}, +"nplurals=1; plural=0;"); diff --git a/apps/cloud_federation_api/l10n/uz.json b/apps/cloud_federation_api/l10n/uz.json new file mode 100644 index 00000000000..fdab2842db9 --- /dev/null +++ b/apps/cloud_federation_api/l10n/uz.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Asl faylni o'chirishda kutilmagan xatolik yuz berdi.", + "Enable clouds to communicate with each other and exchange data" : "Bulutlar bir-biri bilan aloqa qilish va ma'lumot almashish imkonini beradi", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API turli xil Nextcloud misollariga bir-biri bilan muloqot qilish va ma'lumotlarni almashish imkonini beradi." +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/vi.js b/apps/cloud_federation_api/l10n/vi.js new file mode 100644 index 00000000000..50b76cf0e0e --- /dev/null +++ b/apps/cloud_federation_api/l10n/vi.js @@ -0,0 +1,8 @@ +OC.L10N.register( + "cloud_federation_api", + { + "Cloud Federation API" : "Cloud Federation API", + "Enable clouds to communicate with each other and exchange data" : "Cho phép các đám mây giao tiếp với nhau và trao đổi dữ liệu", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API Cloud Federation cho phép các phiên bản Nextcloud khác nhau giao tiếp với nhau và trao đổi dữ liệu." +}, +"nplurals=1; plural=0;"); diff --git a/apps/cloud_federation_api/l10n/vi.json b/apps/cloud_federation_api/l10n/vi.json new file mode 100644 index 00000000000..c7244873d68 --- /dev/null +++ b/apps/cloud_federation_api/l10n/vi.json @@ -0,0 +1,6 @@ +{ "translations": { + "Cloud Federation API" : "Cloud Federation API", + "Enable clouds to communicate with each other and exchange data" : "Cho phép các đám mây giao tiếp với nhau và trao đổi dữ liệu", + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API Cloud Federation cho phép các phiên bản Nextcloud khác nhau giao tiếp với nhau và trao đổi dữ liệu." +},"pluralForm" :"nplurals=1; plural=0;" +}
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/zh_CN.js b/apps/cloud_federation_api/l10n/zh_CN.js index 5bea9e26c1e..90684ddca9d 100644 --- a/apps/cloud_federation_api/l10n/zh_CN.js +++ b/apps/cloud_federation_api/l10n/zh_CN.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "联合云 API", + "Cloud Federation API" : "联合云API", "Enable clouds to communicate with each other and exchange data" : "使云能够相互通信并交换数据", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "联合云 API 使各种 Nextcloud 实例可以相互通信并交换数据。" + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "联合云API使各种 Nextcloud 实例可以相互通信并交换数据。" }, "nplurals=1; plural=0;"); diff --git a/apps/cloud_federation_api/l10n/zh_CN.json b/apps/cloud_federation_api/l10n/zh_CN.json index f0b021fa6a6..491d0494c0e 100644 --- a/apps/cloud_federation_api/l10n/zh_CN.json +++ b/apps/cloud_federation_api/l10n/zh_CN.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "联合云 API", + "Cloud Federation API" : "联合云API", "Enable clouds to communicate with each other and exchange data" : "使云能够相互通信并交换数据", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "联合云 API 使各种 Nextcloud 实例可以相互通信并交换数据。" + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "联合云API使各种 Nextcloud 实例可以相互通信并交换数据。" },"pluralForm" :"nplurals=1; plural=0;" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/l10n/zh_TW.js b/apps/cloud_federation_api/l10n/zh_TW.js index 99b56dc3587..8f997c9aff0 100644 --- a/apps/cloud_federation_api/l10n/zh_TW.js +++ b/apps/cloud_federation_api/l10n/zh_TW.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "雲端聯盟 API", + "Cloud Federation API" : "雲端聯邦 API", "Enable clouds to communicate with each other and exchange data" : "讓雲端可互相通訊並交換資料", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "雲端聯盟 API 讓多個 Nextcloud 站台可以互相通訊並交換資料。" + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "雲端聯邦 API 能讓多個 Nextcloud 實體之間,可以互相通訊並交換資料。" }, "nplurals=1; plural=0;"); diff --git a/apps/cloud_federation_api/l10n/zh_TW.json b/apps/cloud_federation_api/l10n/zh_TW.json index 5850a7f8f91..785e7515484 100644 --- a/apps/cloud_federation_api/l10n/zh_TW.json +++ b/apps/cloud_federation_api/l10n/zh_TW.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "雲端聯盟 API", + "Cloud Federation API" : "雲端聯邦 API", "Enable clouds to communicate with each other and exchange data" : "讓雲端可互相通訊並交換資料", - "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "雲端聯盟 API 讓多個 Nextcloud 站台可以互相通訊並交換資料。" + "The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "雲端聯邦 API 能讓多個 Nextcloud 實體之間,可以互相通訊並交換資料。" },"pluralForm" :"nplurals=1; plural=0;" }
\ No newline at end of file diff --git a/apps/cloud_federation_api/lib/AppInfo/Application.php b/apps/cloud_federation_api/lib/AppInfo/Application.php index 83bf310f993..e34b2f2dc3d 100644 --- a/apps/cloud_federation_api/lib/AppInfo/Application.php +++ b/apps/cloud_federation_api/lib/AppInfo/Application.php @@ -3,28 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\CloudFederationAPI\AppInfo; use OCA\CloudFederationAPI\Capabilities; diff --git a/apps/cloud_federation_api/lib/Capabilities.php b/apps/cloud_federation_api/lib/Capabilities.php index cd1ce5dd359..599733123b3 100644 --- a/apps/cloud_federation_api/lib/Capabilities.php +++ b/apps/cloud_federation_api/lib/Capabilities.php @@ -1,65 +1,78 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\CloudFederationAPI; +use NCU\Security\Signature\Exceptions\IdentityNotFoundException; +use NCU\Security\Signature\Exceptions\SignatoryException; +use OC\OCM\OCMSignatoryManager; use OCP\Capabilities\ICapability; +use OCP\Capabilities\IInitialStateExcludedCapability; +use OCP\IAppConfig; use OCP\IURLGenerator; +use OCP\OCM\Exceptions\OCMArgumentException; +use OCP\OCM\ICapabilityAwareOCMProvider; +use Psr\Log\LoggerInterface; -class Capabilities implements ICapability { +class Capabilities implements ICapability, IInitialStateExcludedCapability { + public const API_VERSION = '1.1.0'; - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IURLGenerator $urlGenerator) { - $this->urlGenerator = $urlGenerator; + public function __construct( + private IURLGenerator $urlGenerator, + private IAppConfig $appConfig, + private ICapabilityAwareOCMProvider $provider, + private readonly OCMSignatoryManager $ocmSignatoryManager, + private readonly LoggerInterface $logger, + ) { } /** * Function an app uses to return the capabilities * - * @return array Array containing the apps capabilities - * @since 8.2.0 + * @return array<string, array<string, mixed>> + * @throws OCMArgumentException */ public function getCapabilities() { $url = $this->urlGenerator->linkToRouteAbsolute('cloud_federation_api.requesthandlercontroller.addShare'); - $capabilities = ['ocm' => - [ - 'enabled' => true, - 'apiVersion' => '1.0-proposal1', - 'endPoint' => substr($url, 0, strrpos($url, '/')), - 'resourceTypes' => [ - [ - 'name' => 'file', - 'shareTypes' => ['user', 'group'], - 'protocols' => [ - 'webdav' => '/public.php/webdav/', - ] - ], - ] - ] - ]; + $pos = strrpos($url, '/'); + if ($pos === false) { + throw new OCMArgumentException('generated route should contain a slash character'); + } + + $this->provider->setEnabled(true); + $this->provider->setApiVersion(self::API_VERSION); + $this->provider->setCapabilities(['/invite-accepted', '/notifications', '/shares']); + + $this->provider->setEndPoint(substr($url, 0, $pos)); + + $resource = $this->provider->createNewResourceType(); + $resource->setName('file') + ->setShareTypes(['user', 'group']) + ->setProtocols(['webdav' => '/public.php/webdav/']); + + $this->provider->addResourceType($resource); + + // Adding a public key to the ocm discovery + try { + if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { + /** + * @experimental 31.0.0 + * @psalm-suppress UndefinedInterfaceMethod + */ + $this->provider->setSignatory($this->ocmSignatoryManager->getLocalSignatory()); + } else { + $this->logger->debug('ocm public key feature disabled'); + } + } catch (SignatoryException|IdentityNotFoundException $e) { + $this->logger->warning('cannot generate local signatory', ['exception' => $e]); + } - return $capabilities; + return ['ocm' => $this->provider->jsonSerialize()]; } } diff --git a/apps/cloud_federation_api/lib/Config.php b/apps/cloud_federation_api/lib/Config.php index a55dfabb527..23788c26dc9 100644 --- a/apps/cloud_federation_api/lib/Config.php +++ b/apps/cloud_federation_api/lib/Config.php @@ -1,30 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2018 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author 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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\CloudFederationAPI; use OCP\Federation\ICloudFederationProviderManager; +use Psr\Log\LoggerInterface; /** * Class config @@ -35,11 +18,10 @@ use OCP\Federation\ICloudFederationProviderManager; */ class Config { - /** @var ICloudFederationProviderManager */ - private $cloudFederationProviderManager; - - public function __construct(ICloudFederationProviderManager $cloudFederationProviderManager) { - $this->cloudFederationProviderManager = $cloudFederationProviderManager; + public function __construct( + private ICloudFederationProviderManager $cloudFederationProviderManager, + private LoggerInterface $logger, + ) { } /** @@ -53,6 +35,7 @@ class Config { $provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType); return $provider->getSupportedShareTypes(); } catch (\Exception $e) { + $this->logger->error('Failed to create federation provider', ['exception' => $e]); return []; } } diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index 17d2bea323f..a76b1884a0b 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -1,34 +1,36 @@ <?php + /** - * @copyright Copyright (c) 2018 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\CloudFederationAPI\Controller; +use NCU\Federation\ISignedCloudFederationProvider; +use NCU\Security\Signature\Exceptions\IdentityNotFoundException; +use NCU\Security\Signature\Exceptions\IncomingRequestException; +use NCU\Security\Signature\Exceptions\SignatoryNotFoundException; +use NCU\Security\Signature\Exceptions\SignatureException; +use NCU\Security\Signature\Exceptions\SignatureNotFoundException; +use NCU\Security\Signature\IIncomingSignedRequest; +use NCU\Security\Signature\ISignatureManager; +use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; +use OCA\CloudFederationAPI\Db\FederatedInviteMapper; +use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent; +use OCA\CloudFederationAPI\ResponseDefinitions; +use OCA\FederatedFileSharing\AddressHandler; use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\Exceptions\ActionNotSupportedException; use OCP\Federation\Exceptions\AuthenticationFailedException; use OCP\Federation\Exceptions\BadRequestException; @@ -37,108 +39,101 @@ use OCP\Federation\Exceptions\ProviderDoesNotExistsException; use OCP\Federation\ICloudFederationFactory; use OCP\Federation\ICloudFederationProviderManager; use OCP\Federation\ICloudIdManager; +use OCP\IAppConfig; use OCP\IGroupManager; -use OCP\ILogger; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Util; +use Psr\Log\LoggerInterface; /** - * Class RequestHandlerController - * - * handle API between different Cloud instances + * Open-Cloud-Mesh-API * * @package OCA\CloudFederationAPI\Controller + * + * @psalm-import-type CloudFederationAPIAddShare from ResponseDefinitions + * @psalm-import-type CloudFederationAPIValidationError from ResponseDefinitions + * @psalm-import-type CloudFederationAPIError from ResponseDefinitions */ +#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] class RequestHandlerController extends Controller { - - /** @var ILogger */ - private $logger; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var ICloudFederationProviderManager */ - private $cloudFederationProviderManager; - - /** @var Config */ - private $config; - - /** @var ICloudFederationFactory */ - private $factory; - - /** @var ICloudIdManager */ - private $cloudIdManager; - - public function __construct($appName, - IRequest $request, - ILogger $logger, - IUserManager $userManager, - IGroupManager $groupManager, - IURLGenerator $urlGenerator, - ICloudFederationProviderManager $cloudFederationProviderManager, - Config $config, - ICloudFederationFactory $factory, - ICloudIdManager $cloudIdManager + public function __construct( + string $appName, + IRequest $request, + private LoggerInterface $logger, + private IUserManager $userManager, + private IGroupManager $groupManager, + private IURLGenerator $urlGenerator, + private ICloudFederationProviderManager $cloudFederationProviderManager, + private Config $config, + private IEventDispatcher $dispatcher, + private FederatedInviteMapper $federatedInviteMapper, + private readonly AddressHandler $addressHandler, + private readonly IAppConfig $appConfig, + private ICloudFederationFactory $factory, + private ICloudIdManager $cloudIdManager, + private readonly ISignatureManager $signatureManager, + private readonly OCMSignatoryManager $signatoryManager, + private ITimeFactory $timeFactory, ) { parent::__construct($appName, $request); - - $this->logger = $logger; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->urlGenerator = $urlGenerator; - $this->cloudFederationProviderManager = $cloudFederationProviderManager; - $this->config = $config; - $this->factory = $factory; - $this->cloudIdManager = $cloudIdManager; } /** - * add share + * Add share * - * @NoCSRFRequired - * @PublicPage - * @BruteForceProtection(action=receiveFederatedShare) + * @param string $shareWith The user who the share will be shared with + * @param string $name The resource name (e.g. document.odt) + * @param string|null $description Share description + * @param string $providerId Resource UID on the provider side + * @param string $owner Provider specific UID of the user who owns the resource + * @param string|null $ownerDisplayName Display name of the user who shared the item + * @param string|null $sharedBy Provider specific UID of the user who shared the resource + * @param string|null $sharedByDisplayName Display name of the user who shared the resource + * @param array{name: list<string>, options: array<string, mixed>} $protocol e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]] + * @param string $shareType 'group' or 'user' share + * @param string $resourceType 'file', 'calendar',... * - * @param string $shareWith - * @param string $name resource name (e.g. document.odt) - * @param string $description share description (optional) - * @param string $providerId resource UID on the provider side - * @param string $owner provider specific UID of the user who owns the resource - * @param string $ownerDisplayName display name of the user who shared the item - * @param string $sharedBy provider specific UID of the user who shared the resource - * @param string $sharedByDisplayName display name of the user who shared the resource - * @param array $protocol (e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]) - * @param string $shareType ('group' or 'user' share) - * @param $resourceType ('file', 'calendar',...) - * @return Http\DataResponse|JSONResponse + * @return JSONResponse<Http::STATUS_CREATED, CloudFederationAPIAddShare, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST, CloudFederationAPIValidationError, array{}>|JSONResponse<Http::STATUS_NOT_IMPLEMENTED, CloudFederationAPIError, array{}> * - * Example: curl -H "Content-Type: application/json" -X POST -d '{"shareWith":"admin1@serve1","name":"welcome server2.txt","description":"desc","providerId":"2","owner":"admin2@http://localhost/server2","ownerDisplayName":"admin2 display","shareType":"user","resourceType":"file","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' http://localhost/server/index.php/ocm/shares + * 201: The notification was successfully received. The display name of the recipient might be returned in the body + * 400: Bad request due to invalid parameters, e.g. when `shareWith` is not found or required properties are missing + * 501: Share type or the resource type is not supported */ + #[PublicPage] + #[NoCSRFRequired] + #[BruteForceProtection(action: 'receiveFederatedShare')] public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { + try { + // if request is signed and well signed, no exception are thrown + // if request is not signed and host is known for not supporting signed request, no exception are thrown + $signedRequest = $this->getSignedRequest(); + $this->confirmSignedOrigin($signedRequest, 'owner', $owner); + } catch (IncomingRequestException $e) { + $this->logger->warning('incoming request exception', ['exception' => $e]); + return new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST); + } // check if all required parameters are set - if ($shareWith === null || - $name === null || - $providerId === null || - $owner === null || - $resourceType === null || - $shareType === null || - !is_array($protocol) || - !isset($protocol['name']) || - !isset($protocol['options']) || - !is_array($protocol['options']) || - !isset($protocol['options']['sharedSecret']) + if ( + $shareWith === null + || $name === null + || $providerId === null + || $resourceType === null + || $shareType === null + || !is_array($protocol) + || !isset($protocol['name']) + || !isset($protocol['options']) + || !is_array($protocol['options']) + || !isset($protocol['options']['sharedSecret']) ) { return new JSONResponse( - ['message' => 'Missing arguments'], + [ + 'message' => 'Missing arguments', + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } @@ -158,19 +153,29 @@ class RequestHandlerController extends Controller { $shareWith = $this->mapUid($shareWith); if (!$this->userManager->userExists($shareWith)) { - return new JSONResponse( - ['message' => 'User "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], + $response = new JSONResponse( + [ + 'message' => 'User "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); + $response->throttle(); + return $response; } } if ($shareType === 'group') { if (!$this->groupManager->groupExists($shareWith)) { - return new JSONResponse( - ['message' => 'Group "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], + $response = new JSONResponse( + [ + 'message' => 'Group "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); + $response->throttle(); + return $response; } } @@ -189,74 +194,197 @@ class RequestHandlerController extends Controller { $share = $this->factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType); $share->setProtocol($protocol); $provider->shareReceived($share); - } catch (ProviderDoesNotExistsException $e) { + } catch (ProviderDoesNotExistsException|ProviderCouldNotAddShareException $e) { return new JSONResponse( ['message' => $e->getMessage()], Http::STATUS_NOT_IMPLEMENTED ); - } catch (ProviderCouldNotAddShareException $e) { - return new JSONResponse( - ['message' => $e->getMessage()], - $e->getCode() - ); } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); return new JSONResponse( - ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], + [ + 'message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } - $user = $this->userManager->get($shareWith); - $recipientDisplayName = ''; - if ($user) { - $recipientDisplayName = $user->getDisplayName(); + $responseData = ['recipientDisplayName' => '']; + if ($shareType === 'user') { + $user = $this->userManager->get($shareWith); + if ($user) { + $responseData = [ + 'recipientDisplayName' => $user->getDisplayName(), + 'recipientUserId' => $user->getUID(), + ]; + } } - return new JSONResponse( - ['recipientDisplayName' => $recipientDisplayName], - Http::STATUS_CREATED); + return new JSONResponse($responseData, Http::STATUS_CREATED); } /** - * receive notification about existing share + * Inform the sender that an invitation was accepted to start sharing + * + * Inform about an accepted invitation so the user on the sender provider's side + * can initiate the OCM share creation. To protect the identity of the parties, + * for shares created following an OCM invitation, the user id MAY be hashed, + * and recipients implementing the OCM invitation workflow MAY refuse to process + * shares coming from unknown parties. + * @link https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post * - * @NoCSRFRequired - * @PublicPage - * @BruteForceProtection(action=receiveFederatedShareNotification) + * @param string $recipientProvider The address of the recipent's provider + * @param string $token The token used for the invitation + * @param string $userId The userId of the recipient at the recipient's provider + * @param string $email The email address of the recipient + * @param string $name The display name of the recipient * - * @param string $notificationType (notification type, e.g. SHARE_ACCEPTED) - * @param string $resourceType (calendar, file, contact,...) - * @param string $providerId id of the share - * @param array $notification the actual payload of the notification - * @return JSONResponse + * @return JSONResponse<Http::STATUS_OK, array{userID: string, email: string, name: string}, array{}>|JSONResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_CONFLICT, array{message: string, error: true}, array{}> + * + * Note: Not implementing 404 Invitation token does not exist, instead using 400 + * 200: Invitation accepted + * 400: Invalid token + * 403: Invitation token does not exist + * 409: User is already known by the OCM provider */ - public function receiveNotification($notificationType, $resourceType, $providerId, array $notification) { + #[PublicPage] + #[NoCSRFRequired] + #[BruteForceProtection(action: 'inviteAccepted')] + public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse { + $this->logger->debug('Processing share invitation for ' . $userId . ' with token ' . $token . ' and email ' . $email . ' and name ' . $name); + + $updated = $this->timeFactory->getTime(); + + if ($token === '') { + $response = new JSONResponse(['message' => 'Invalid or non existing token', 'error' => true], Http::STATUS_BAD_REQUEST); + $response->throttle(); + return $response; + } + + try { + $invitation = $this->federatedInviteMapper->findByToken($token); + } catch (DoesNotExistException) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + + if ($invitation->isAccepted() === true) { + $response = ['message' => 'Invite already accepted', 'error' => true]; + $status = Http::STATUS_CONFLICT; + return new JSONResponse($response, $status); + } + + if ($invitation->getExpiredAt() !== null && $updated > $invitation->getExpiredAt()) { + $response = ['message' => 'Invitation expired', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + return new JSONResponse($response, $status); + } + $localUser = $this->userManager->get($invitation->getUserId()); + if ($localUser === null) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + + $sharedFromEmail = $localUser->getEMailAddress(); + if ($sharedFromEmail === null) { + $response = ['message' => 'Invalid or non existing token', 'error' => true]; + $status = Http::STATUS_BAD_REQUEST; + $response = new JSONResponse($response, $status); + $response->throttle(); + return $response; + } + $sharedFromDisplayName = $localUser->getDisplayName(); + $response = ['userID' => $localUser->getUID(), 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName]; + $status = Http::STATUS_OK; + + $invitation->setAccepted(true); + $invitation->setRecipientEmail($email); + $invitation->setRecipientName($name); + $invitation->setRecipientProvider($recipientProvider); + $invitation->setRecipientUserId($userId); + $invitation->setAcceptedAt($updated); + $invitation = $this->federatedInviteMapper->update($invitation); + + $event = new FederatedInviteAcceptedEvent($invitation); + $this->dispatcher->dispatchTyped($event); + + return new JSONResponse($response, $status); + } + + /** + * Send a notification about an existing share + * + * @param string $notificationType Notification type, e.g. SHARE_ACCEPTED + * @param string $resourceType calendar, file, contact,... + * @param string|null $providerId ID of the share + * @param array<string, mixed>|null $notification The actual payload of the notification + * + * @return JSONResponse<Http::STATUS_CREATED, array<string, mixed>, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST, CloudFederationAPIValidationError, array{}>|JSONResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_IMPLEMENTED, CloudFederationAPIError, array{}> + * + * 201: The notification was successfully received + * 400: Bad request due to invalid parameters, e.g. when `type` is invalid or missing + * 403: Getting resource is not allowed + * 501: The resource type is not supported + */ + #[NoCSRFRequired] + #[PublicPage] + #[BruteForceProtection(action: 'receiveFederatedShareNotification')] + public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) { // check if all required parameters are set - if ($notificationType === null || - $resourceType === null || - $providerId === null || - !is_array($notification) + if ( + $notificationType === null + || $resourceType === null + || $providerId === null + || !is_array($notification) ) { return new JSONResponse( - ['message' => 'Missing arguments'], + [ + 'message' => 'Missing arguments', + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } try { + // if request is signed and well signed, no exception are thrown + // if request is not signed and host is known for not supporting signed request, no exception are thrown + $signedRequest = $this->getSignedRequest(); + $this->confirmNotificationIdentity($signedRequest, $resourceType, $notification); + } catch (IncomingRequestException $e) { + $this->logger->warning('incoming request exception', ['exception' => $e]); + return new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST); + } + + try { $provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType); $result = $provider->notificationReceived($notificationType, $providerId, $notification); } catch (ProviderDoesNotExistsException $e) { return new JSONResponse( - ['message' => $e->getMessage()], + [ + 'message' => $e->getMessage(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } catch (ShareNotFound $e) { - return new JSONResponse( - ['message' => $e->getMessage()], + $response = new JSONResponse( + [ + 'message' => $e->getMessage(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); + $response->throttle(); + return $response; } catch (ActionNotSupportedException $e) { return new JSONResponse( ['message' => $e->getMessage()], @@ -265,15 +393,21 @@ class RequestHandlerController extends Controller { } catch (BadRequestException $e) { return new JSONResponse($e->getReturnMessage(), Http::STATUS_BAD_REQUEST); } catch (AuthenticationFailedException $e) { - return new JSONResponse(["message" => "RESOURCE_NOT_FOUND"], Http::STATUS_FORBIDDEN); + $response = new JSONResponse(['message' => 'RESOURCE_NOT_FOUND'], Http::STATUS_FORBIDDEN); + $response->throttle(); + return $response; } catch (\Exception $e) { + $this->logger->warning('incoming notification exception', ['exception' => $e]); return new JSONResponse( - ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], + [ + 'message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl(), + 'validationErrors' => [], + ], Http::STATUS_BAD_REQUEST ); } - return new JSONResponse($result,Http::STATUS_CREATED); + return new JSONResponse($result, Http::STATUS_CREATED); } /** @@ -285,7 +419,7 @@ class RequestHandlerController extends Controller { private function mapUid($uid) { // FIXME this should be a method in the user management instead $this->logger->debug('shareWith before, ' . $uid, ['app' => $this->appName]); - \OCP\Util::emitHook( + Util::emitHook( '\OCA\Files_Sharing\API\Server2Server', 'preLoginNameUsedAsUserName', ['uid' => &$uid] @@ -294,4 +428,147 @@ class RequestHandlerController extends Controller { return $uid; } + + + /** + * returns signed request if available. + * throw an exception: + * - if request is signed, but wrongly signed + * - if request is not signed but instance is configured to only accept signed ocm request + * + * @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request + * @throws IncomingRequestException + */ + private function getSignedRequest(): ?IIncomingSignedRequest { + try { + $signedRequest = $this->signatureManager->getIncomingSignedRequest($this->signatoryManager); + $this->logger->debug('signed request available', ['signedRequest' => $signedRequest]); + return $signedRequest; + } catch (SignatureNotFoundException|SignatoryNotFoundException $e) { + $this->logger->debug('remote does not support signed request', ['exception' => $e]); + // remote does not support signed request. + // currently we still accept unsigned request until lazy appconfig + // core.enforce_signed_ocm_request is set to true (default: false) + if ($this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, lazy: true)) { + $this->logger->notice('ignored unsigned request', ['exception' => $e]); + throw new IncomingRequestException('Unsigned request'); + } + } catch (SignatureException $e) { + $this->logger->warning('wrongly signed request', ['exception' => $e]); + throw new IncomingRequestException('Invalid signature'); + } + return null; + } + + + /** + * confirm that the value related to $key entry from the payload is in format userid@hostname + * and compare hostname with the origin of the signed request. + * + * If request is not signed, we still verify that the hostname from the extracted value does, + * actually, not support signed request + * + * @param IIncomingSignedRequest|null $signedRequest + * @param string $key entry from data available in data + * @param string $value value itself used in case request is not signed + * + * @throws IncomingRequestException + */ + private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void { + if ($signedRequest === null) { + $instance = $this->getHostFromFederationId($value); + try { + $this->signatureManager->getSignatory($instance); + throw new IncomingRequestException('instance is supposed to sign its request'); + } catch (SignatoryNotFoundException) { + return; + } + } + + $body = json_decode($signedRequest->getBody(), true) ?? []; + $entry = trim($body[$key] ?? '', '@'); + if ($this->getHostFromFederationId($entry) !== $signedRequest->getOrigin()) { + throw new IncomingRequestException('share initiation (' . $signedRequest->getOrigin() . ') from different instance (' . $entry . ') [key=' . $key . ']'); + } + } + + /** + * confirm identity of the remote instance on notification, based on the share token. + * + * If request is not signed, we still verify that the hostname from the extracted value does, + * actually, not support signed request + * + * @param IIncomingSignedRequest|null $signedRequest + * @param string $resourceType + * @param string $sharedSecret + * + * @throws IncomingRequestException + * @throws BadRequestException + */ + private function confirmNotificationIdentity( + ?IIncomingSignedRequest $signedRequest, + string $resourceType, + array $notification, + ): void { + $sharedSecret = $notification['sharedSecret'] ?? ''; + if ($sharedSecret === '') { + throw new BadRequestException(['sharedSecret']); + } + + try { + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType); + if ($provider instanceof ISignedCloudFederationProvider) { + $identity = $provider->getFederationIdFromSharedSecret($sharedSecret, $notification); + } else { + $this->logger->debug('cloud federation provider {provider} does not implements ISignedCloudFederationProvider', ['provider' => $provider::class]); + return; + } + } catch (\Exception $e) { + throw new IncomingRequestException($e->getMessage()); + } + + $this->confirmNotificationEntry($signedRequest, $identity); + } + + + /** + * @param IIncomingSignedRequest|null $signedRequest + * @param string $entry + * + * @return void + * @throws IncomingRequestException + */ + private function confirmNotificationEntry(?IIncomingSignedRequest $signedRequest, string $entry): void { + $instance = $this->getHostFromFederationId($entry); + if ($signedRequest === null) { + try { + $this->signatureManager->getSignatory($instance); + throw new IncomingRequestException('instance is supposed to sign its request'); + } catch (SignatoryNotFoundException) { + return; + } + } elseif ($instance !== $signedRequest->getOrigin()) { + throw new IncomingRequestException('remote instance ' . $instance . ' not linked to origin ' . $signedRequest->getOrigin()); + } + } + + /** + * @param string $entry + * @return string + * @throws IncomingRequestException + */ + private function getHostFromFederationId(string $entry): string { + if (!str_contains($entry, '@')) { + throw new IncomingRequestException('entry ' . $entry . ' does not contain @'); + } + $rightPart = substr($entry, strrpos($entry, '@') + 1); + + // in case the full scheme is sent; getting rid of it + $rightPart = $this->addressHandler->removeProtocolFromUrl($rightPart); + try { + return $this->signatureManager->extractIdentityFromUri('https://' . $rightPart); + } catch (IdentityNotFoundException) { + throw new IncomingRequestException('invalid host within federation id: ' . $entry); + } + } } diff --git a/apps/cloud_federation_api/lib/Db/FederatedInvite.php b/apps/cloud_federation_api/lib/Db/FederatedInvite.php new file mode 100644 index 00000000000..b2447ff4e23 --- /dev/null +++ b/apps/cloud_federation_api/lib/Db/FederatedInvite.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\CloudFederationAPI\Db; + +use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; + +/** + * @method bool isAccepted() + * @method void setAccepted(bool $accepted) + * @method int|null getAcceptedAt() + * @method void setAcceptedAt(int $acceptedAt) + * @method int|null getCreatedAt() + * @method void setCreatedAt(int $createdAt) + * @method int|null getExpiredAt() + * @method void setExpiredAt(int $expiredAt) + * @method string|null getRecipientEmail() + * @method void setRecipientEmail(string $recipientEmail) + * @method string|null getRecipientName() + * @method void setRecipientName(string $recipientName) + * @method string|null getRecipientProvider() + * @method void setRecipientProvider(string $recipientProvider) + * @method string|null getRecipientUserId() + * @method void setRecipientUserId(string $recipientUserId) + * @method string getToken() + * @method void setToken(string $token) + * @method string|null getUserId() + * @method void setUserId(string $userId) + */ + +class FederatedInvite extends Entity { + protected bool $accepted = false; + protected ?int $acceptedAt = 0; + protected int $createdAt = 0; + protected ?int $expiredAt = 0; + protected ?string $recipientEmail = null; + protected ?string $recipientName = null; + protected ?string $recipientProvider = null; + protected ?string $recipientUserId = null; + protected string $token = ''; + protected string $userId = ''; + + public function __construct() { + $this->addType('accepted', Types::BOOLEAN); + $this->addType('acceptedAt', Types::BIGINT); + $this->addType('createdAt', Types::BIGINT); + $this->addType('expiredAt', Types::BIGINT); + $this->addType('recipientEmail', Types::STRING); + $this->addType('recipientName', Types::STRING); + $this->addType('recipientProvider', Types::STRING); + $this->addType('recipientUserId', Types::STRING); + $this->addType('token', Types::STRING); + $this->addType('userId', Types::STRING); + } +} diff --git a/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php b/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php new file mode 100644 index 00000000000..5feb08b2c7f --- /dev/null +++ b/apps/cloud_federation_api/lib/Db/FederatedInviteMapper.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\CloudFederationAPI\Db; + +use OCP\AppFramework\Db\QBMapper; +use OCP\IDBConnection; + +/** + * @template-extends QBMapper<FederatedInvite> + */ +class FederatedInviteMapper extends QBMapper { + public const TABLE_NAME = 'federated_invites'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, self::TABLE_NAME); + } + + public function findByToken(string $token): FederatedInvite { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('federated_invites') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); + return $this->findEntity($qb); + } + +} diff --git a/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php b/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php new file mode 100644 index 00000000000..c4d079d083e --- /dev/null +++ b/apps/cloud_federation_api/lib/Events/FederatedInviteAcceptedEvent.php @@ -0,0 +1,24 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\CloudFederationAPI\Events; + +use OCA\CloudFederationAPI\Db\FederatedInvite; +use OCP\EventDispatcher\Event; + +class FederatedInviteAcceptedEvent extends Event { + public function __construct( + private FederatedInvite $invitation, + ) { + parent::__construct(); + } + + public function getInvitation(): FederatedInvite { + return $this->invitation; + } +} diff --git a/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php b/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php new file mode 100644 index 00000000000..a3523d45e38 --- /dev/null +++ b/apps/cloud_federation_api/lib/Migration/Version1016Date202502262004.php @@ -0,0 +1,89 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\CloudFederationAPI\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1016Date202502262004 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + $table_name = 'federated_invites'; + + if (!$schema->hasTable($table_name)) { + $table = $schema->createTable($table_name); + $table->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ]); + $table->addColumn('user_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + + ]); + // https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/#maximum-url-length-in-different-browsers + // We use the least common denominator, the minimum length supported by browsers + $table->addColumn('recipient_provider', Types::STRING, [ + 'notnull' => false, + 'length' => 2083, + ]); + $table->addColumn('recipient_user_id', Types::STRING, [ + 'notnull' => false, + 'length' => 1024, + ]); + $table->addColumn('recipient_name', Types::STRING, [ + 'notnull' => false, + 'length' => 1024, + ]); + // https://www.directedignorance.com/blog/maximum-length-of-email-address + $table->addColumn('recipient_email', Types::STRING, [ + 'notnull' => false, + 'length' => 320, + ]); + $table->addColumn('token', Types::STRING, [ + 'notnull' => true, + 'length' => 60, + ]); + $table->addColumn('accepted', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false + ]); + $table->addColumn('created_at', Types::BIGINT, [ + 'notnull' => true, + ]); + + $table->addColumn('expired_at', Types::BIGINT, [ + 'notnull' => false, + ]); + + $table->addColumn('accepted_at', Types::BIGINT, [ + 'notnull' => false, + ]); + + $table->addUniqueConstraint(['token']); + $table->setPrimaryKey(['id']); + return $schema; + } + + return null; + } +} diff --git a/apps/cloud_federation_api/lib/ResponseDefinitions.php b/apps/cloud_federation_api/lib/ResponseDefinitions.php new file mode 100644 index 00000000000..b17f5aadc1d --- /dev/null +++ b/apps/cloud_federation_api/lib/ResponseDefinitions.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\CloudFederationAPI; + +/** + * @psalm-type CloudFederationAPIAddShare = array{ + * recipientDisplayName: string, + * recipientUserId?: string, + * } + * + * @psalm-type CloudFederationAPIError = array{ + * message: string, + * } + * + * @psalm-type CloudFederationAPIValidationError = CloudFederationAPIError&array{ + * validationErrors: list<array{ + * name: string, + * message: string|null, + * }>, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/cloud_federation_api/openapi.json b/apps/cloud_federation_api/openapi.json new file mode 100644 index 00000000000..9c92a152bf8 --- /dev/null +++ b/apps/cloud_federation_api/openapi.json @@ -0,0 +1,499 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "cloud_federation_api", + "version": "0.0.1", + "description": "Enable clouds to communicate with each other and exchange data", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "AddShare": { + "type": "object", + "required": [ + "recipientDisplayName" + ], + "properties": { + "recipientDisplayName": { + "type": "string" + }, + "recipientUserId": { + "type": "string" + } + } + }, + "Capabilities": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + }, + "Error": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + }, + "ValidationError": { + "allOf": [ + { + "$ref": "#/components/schemas/Error" + }, + { + "type": "object", + "required": [ + "validationErrors" + ], + "properties": { + "validationErrors": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "message" + ], + "properties": { + "name": { + "type": "string" + }, + "message": { + "type": "string", + "nullable": true + } + } + } + } + } + } + ] + } + } + }, + "paths": { + "/index.php/ocm/shares": { + "post": { + "operationId": "request_handler-add-share", + "summary": "Add share", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "shareWith", + "name", + "providerId", + "owner", + "protocol", + "shareType", + "resourceType" + ], + "properties": { + "shareWith": { + "type": "string", + "description": "The user who the share will be shared with" + }, + "name": { + "type": "string", + "description": "The resource name (e.g. document.odt)" + }, + "description": { + "type": "string", + "nullable": true, + "description": "Share description" + }, + "providerId": { + "type": "string", + "description": "Resource UID on the provider side" + }, + "owner": { + "type": "string", + "description": "Provider specific UID of the user who owns the resource" + }, + "ownerDisplayName": { + "type": "string", + "nullable": true, + "description": "Display name of the user who shared the item" + }, + "sharedBy": { + "type": "string", + "nullable": true, + "description": "Provider specific UID of the user who shared the resource" + }, + "sharedByDisplayName": { + "type": "string", + "nullable": true, + "description": "Display name of the user who shared the resource" + }, + "protocol": { + "type": "object", + "description": "e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]", + "required": [ + "name", + "options" + ], + "properties": { + "name": { + "type": "array", + "items": { + "type": "string" + } + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + }, + "shareType": { + "type": "string", + "description": "'group' or 'user' share" + }, + "resourceType": { + "type": "string", + "description": "'file', 'calendar',..." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The notification was successfully received. The display name of the recipient might be returned in the body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddShare" + } + } + } + }, + "400": { + "description": "Bad request due to invalid parameters, e.g. when `shareWith` is not found or required properties are missing", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + }, + "501": { + "description": "Share type or the resource type is not supported", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/index.php/ocm/notifications": { + "post": { + "operationId": "request_handler-receive-notification", + "summary": "Send a notification about an existing share", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "notificationType", + "resourceType" + ], + "properties": { + "notificationType": { + "type": "string", + "description": "Notification type, e.g. SHARE_ACCEPTED" + }, + "resourceType": { + "type": "string", + "description": "calendar, file, contact,..." + }, + "providerId": { + "type": "string", + "nullable": true, + "description": "ID of the share" + }, + "notification": { + "type": "object", + "nullable": true, + "description": "The actual payload of the notification", + "additionalProperties": { + "type": "object" + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The notification was successfully received", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + } + }, + "400": { + "description": "Bad request due to invalid parameters, e.g. when `type` is invalid or missing", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + }, + "403": { + "description": "Getting resource is not allowed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "501": { + "description": "The resource type is not supported", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/index.php/ocm/invite-accepted": { + "post": { + "operationId": "request_handler-invite-accepted", + "summary": "Inform the sender that an invitation was accepted to start sharing", + "description": "Inform about an accepted invitation so the user on the sender provider's side can initiate the OCM share creation. To protect the identity of the parties, for shares created following an OCM invitation, the user id MAY be hashed, and recipients implementing the OCM invitation workflow MAY refuse to process shares coming from unknown parties.\nhttps://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post\nNote: Not implementing 404 Invitation token does not exist, instead using 400", + "tags": [ + "request_handler" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "recipientProvider", + "token", + "userId", + "email", + "name" + ], + "properties": { + "recipientProvider": { + "type": "string", + "description": "The address of the recipent's provider" + }, + "token": { + "type": "string", + "description": "The token used for the invitation" + }, + "userId": { + "type": "string", + "description": "The userId of the recipient at the recipient's provider" + }, + "email": { + "type": "string", + "description": "The email address of the recipient" + }, + "name": { + "type": "string", + "description": "The display name of the recipient" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Invitation accepted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "userID", + "email", + "name" + ], + "properties": { + "userID": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "403": { + "description": "Invitation token does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message", + "error" + ], + "properties": { + "message": { + "type": "string" + }, + "error": { + "type": "boolean", + "enum": [ + true + ] + } + } + } + } + } + }, + "400": { + "description": "Invalid token", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message", + "error" + ], + "properties": { + "message": { + "type": "string" + }, + "error": { + "type": "boolean", + "enum": [ + true + ] + } + } + } + } + } + }, + "409": { + "description": "User is already known by the OCM provider", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message", + "error" + ], + "properties": { + "message": { + "type": "string" + }, + "error": { + "type": "boolean", + "enum": [ + true + ] + } + } + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "request_handler", + "description": "Open-Cloud-Mesh-API" + } + ] +} diff --git a/apps/cloud_federation_api/openapi.json.license b/apps/cloud_federation_api/openapi.json.license new file mode 100644 index 00000000000..83559daa9dc --- /dev/null +++ b/apps/cloud_federation_api/openapi.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later
\ No newline at end of file diff --git a/apps/cloud_federation_api/tests/RequestHandlerControllerTest.php b/apps/cloud_federation_api/tests/RequestHandlerControllerTest.php new file mode 100644 index 00000000000..769e0a2dbff --- /dev/null +++ b/apps/cloud_federation_api/tests/RequestHandlerControllerTest.php @@ -0,0 +1,136 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\CloudFederationApi\Tests; + +use NCU\Security\Signature\ISignatureManager; +use OC\OCM\OCMSignatoryManager; +use OCA\CloudFederationAPI\Config; +use OCA\CloudFederationAPI\Controller\RequestHandlerController; +use OCA\CloudFederationAPI\Db\FederatedInvite; +use OCA\CloudFederationAPI\Db\FederatedInviteMapper; +use OCA\FederatedFileSharing\AddressHandler; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\ICloudFederationFactory; +use OCP\Federation\ICloudFederationProviderManager; +use OCP\Federation\ICloudIdManager; +use OCP\IAppConfig; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class RequestHandlerControllerTest extends TestCase { + private IRequest&MockObject $request; + private LoggerInterface&MockObject $logger; + private IUserManager&MockObject $userManager; + private IGroupManager&MockObject $groupManager; + private IURLGenerator&MockObject $urlGenerator; + private ICloudFederationProviderManager&MockObject $cloudFederationProviderManager; + private Config&MockObject $config; + private IEventDispatcher&MockObject $eventDispatcher; + private FederatedInviteMapper&MockObject $federatedInviteMapper; + private AddressHandler&MockObject $addressHandler; + private IAppConfig&MockObject $appConfig; + private ICloudFederationFactory&MockObject $cloudFederationFactory; + private ICloudIdManager&MockObject $cloudIdManager; + private ISignatureManager&MockObject $signatureManager; + private OCMSignatoryManager&MockObject $signatoryManager; + private ITimeFactory&MockObject $timeFactory; + + private RequestHandlerController $requestHandlerController; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->cloudFederationProviderManager = $this->createMock(ICloudFederationProviderManager::class); + $this->config = $this->createMock(Config::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->federatedInviteMapper = $this->createMock(FederatedInviteMapper::class); + $this->addressHandler = $this->createMock(AddressHandler::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->cloudFederationFactory = $this->createMock(ICloudFederationFactory::class); + $this->cloudIdManager = $this->createMock(ICloudIdManager::class); + $this->signatureManager = $this->createMock(ISignatureManager::class); + $this->signatoryManager = $this->createMock(OCMSignatoryManager::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->requestHandlerController = new RequestHandlerController( + 'cloud_federation_api', + $this->request, + $this->logger, + $this->userManager, + $this->groupManager, + $this->urlGenerator, + $this->cloudFederationProviderManager, + $this->config, + $this->eventDispatcher, + $this->federatedInviteMapper, + $this->addressHandler, + $this->appConfig, + $this->cloudFederationFactory, + $this->cloudIdManager, + $this->signatureManager, + $this->signatoryManager, + $this->timeFactory, + ); + } + + public function testInviteAccepted(): void { + $token = 'token'; + $userId = 'userId'; + $invite = new FederatedInvite(); + $invite->setCreatedAt(1); + $invite->setUserId($userId); + $invite->setToken($token); + + $this->federatedInviteMapper->expects(self::once()) + ->method('findByToken') + ->with($token) + ->willReturn($invite); + + $this->federatedInviteMapper->expects(self::once()) + ->method('update') + ->willReturnArgument(0); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn($userId); + $user->method('getEMailAddress') + ->willReturn('email'); + $user->method('getDisplayName') + ->willReturn('displayName'); + + $this->userManager->expects(self::once()) + ->method('get') + ->with($userId) + ->willReturn($user); + + $recipientProvider = 'http://127.0.0.1'; + $recipientId = 'remote'; + $recipientEmail = 'remote@example.org'; + $recipientName = 'Remote Remoteson'; + $response = ['userID' => $userId, 'email' => 'email', 'name' => 'displayName']; + $json = new JSONResponse($response, Http::STATUS_OK); + + $this->assertEquals($json, $this->requestHandlerController->inviteAccepted($recipientProvider, $token, $recipientId, $recipientEmail, $recipientName)); + } +} |