diff options
Diffstat (limited to 'apps/cloud_federation_api')
35 files changed, 1136 insertions, 391 deletions
diff --git a/apps/cloud_federation_api/README.md b/apps/cloud_federation_api/README.md index d10071c8758..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 across Nextcloud instances diff --git a/apps/cloud_federation_api/appinfo/info.xml b/apps/cloud_federation_api/appinfo/info.xml index 12fb73ff12c..81343cb49bf 100644 --- a/apps/cloud_federation_api/appinfo/info.xml +++ b/apps/cloud_federation_api/appinfo/info.xml @@ -1,11 +1,15 @@ <?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.13.0</version> + <version>1.16.0</version> <licence>agpl</licence> <author>Bjoern Schiessle</author> <namespace>CloudFederationAPI</namespace> @@ -15,6 +19,6 @@ <category>integration</category> <bugs>https://github.com/nextcloud/server/issues</bugs> <dependencies> - <nextcloud min-version="30" max-version="30"/> + <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 ad508e13277..6467005e21b 100644 --- a/apps/cloud_federation_api/appinfo/routes.php +++ b/apps/cloud_federation_api/appinfo/routes.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Maxence Lange <maxence@artificial-owl.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' => [ @@ -38,11 +20,11 @@ return [ 'verb' => 'POST', 'root' => '/ocm', ], - // [ - // 'name' => 'RequestHandler#inviteAccepted', - // 'url' => '/invite-accepted', - // 'verb' => 'POST', - // 'root' => '/ocm', - // ] + [ + 'name' => 'RequestHandler#inviteAccepted', + 'url' => '/invite-accepted', + 'verb' => 'POST', + 'root' => '/ocm', + ] ], ]; diff --git a/apps/cloud_federation_api/composer/composer/autoload_classmap.php b/apps/cloud_federation_api/composer/composer/autoload_classmap.php index dd096ebf563..3cadc540c88 100644 --- a/apps/cloud_federation_api/composer/composer/autoload_classmap.php +++ b/apps/cloud_federation_api/composer/composer/autoload_classmap.php @@ -11,5 +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_static.php b/apps/cloud_federation_api/composer/composer/autoload_static.php index 75557a20126..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,10 @@ 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', ); diff --git a/apps/cloud_federation_api/l10n/da.js b/apps/cloud_federation_api/l10n/da.js index 941a8a761e8..a3a8d6a2303 100644 --- a/apps/cloud_federation_api/l10n/da.js +++ b/apps/cloud_federation_api/l10n/da.js @@ -1,8 +1,8 @@ OC.L10N.register( "cloud_federation_api", { - "Cloud Federation API" : "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 Federation API gør det muligt for forskellige Nextcloud-instanser 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 index b3fee15a43a..fc26d6c54d6 100644 --- a/apps/cloud_federation_api/l10n/da.json +++ b/apps/cloud_federation_api/l10n/da.json @@ -1,6 +1,6 @@ { "translations": { - "Cloud Federation API" : "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 Federation API gør det muligt for forskellige Nextcloud-instanser 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/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/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/pt_BR.js b/apps/cloud_federation_api/l10n/pt_BR.js index ca2e6ccfcf7..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=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 766cdaaf8cc..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=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/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/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/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/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 509e8322ba5..e34b2f2dc3d 100644 --- a/apps/cloud_federation_api/lib/AppInfo/Application.php +++ b/apps/cloud_federation_api/lib/AppInfo/Application.php @@ -3,26 +3,8 @@ 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; diff --git a/apps/cloud_federation_api/lib/Capabilities.php b/apps/cloud_federation_api/lib/Capabilities.php index ad7e0889ba5..599733123b3 100644 --- a/apps/cloud_federation_api/lib/Capabilities.php +++ b/apps/cloud_federation_api/lib/Capabilities.php @@ -3,82 +3,76 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Kate Döen <kate.doeen@nextcloud.com> - * @author Maxence Lange <maxence@artificial-owl.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: 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\IOCMProvider; +use OCP\OCM\ICapabilityAwareOCMProvider; +use Psr\Log\LoggerInterface; -class Capabilities implements ICapability { - public const API_VERSION = '1.0-proposal1'; +class Capabilities implements ICapability, IInitialStateExcludedCapability { + public const API_VERSION = '1.1.0'; public function __construct( private IURLGenerator $urlGenerator, - private IOCMProvider $provider, + private IAppConfig $appConfig, + private ICapabilityAwareOCMProvider $provider, + private readonly OCMSignatoryManager $ocmSignatoryManager, + private readonly LoggerInterface $logger, ) { } /** * Function an app uses to return the capabilities * - * @return array{ - * ocm: array{ - * enabled: bool, - * apiVersion: string, - * endPoint: string, - * resourceTypes: array{ - * name: string, - * shareTypes: string[], - * protocols: array<string, string> - * }[], - * }, - * } + * @return array<string, array<string, mixed>> * @throws OCMArgumentException */ public function getCapabilities() { $url = $this->urlGenerator->linkToRouteAbsolute('cloud_federation_api.requesthandlercontroller.addShare'); - - $this->provider->setEnabled(true); - $this->provider->setApiVersion(self::API_VERSION); - $pos = strrpos($url, '/'); if ($pos === false) { - throw new OCMArgumentException('generated route should contains a slash character'); + 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/']); + ->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 ['ocm' => $this->provider->jsonSerialize()]; } } diff --git a/apps/cloud_federation_api/lib/Config.php b/apps/cloud_federation_api/lib/Config.php index 8611b7cef9c..23788c26dc9 100644 --- a/apps/cloud_federation_api/lib/Config.php +++ b/apps/cloud_federation_api/lib/Config.php @@ -1,29 +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 @@ -34,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, + ) { } /** @@ -52,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 0255e011ea4..a76b1884a0b 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -1,37 +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 Maxence Lange <maxence@artificial-owl.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.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: 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; @@ -40,11 +39,13 @@ 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\IRequest; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Util; use Psr\Log\LoggerInterface; /** @@ -67,8 +68,15 @@ class RequestHandlerController extends Controller { 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 ICloudIdManager $cloudIdManager, + private readonly ISignatureManager $signatureManager, + private readonly OCMSignatoryManager $signatoryManager, + private ITimeFactory $timeFactory, ) { parent::__construct($appName, $request); } @@ -76,10 +84,6 @@ class RequestHandlerController extends Controller { /** * 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 @@ -88,28 +92,42 @@ class RequestHandlerController extends Controller { * @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: string[], options: array<string, mixed>} $protocol e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]] + * @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',... * * @return JSONResponse<Http::STATUS_CREATED, CloudFederationAPIAddShare, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST, CloudFederationAPIValidationError, array{}>|JSONResponse<Http::STATUS_NOT_IMPLEMENTED, CloudFederationAPIError, array{}> + * * 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( [ @@ -192,23 +210,117 @@ class RequestHandlerController extends Controller { ); } - $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); } /** - * Send a notification about an 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 + * + * @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 * - * @NoCSRFRequired - * @PublicPage - * @BruteForceProtection(action=receiveFederatedShareNotification) + * @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 + */ + #[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,... @@ -216,17 +328,22 @@ class RequestHandlerController extends Controller { * @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( [ @@ -238,6 +355,16 @@ class RequestHandlerController extends Controller { } 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) { @@ -270,6 +397,7 @@ class RequestHandlerController extends Controller { $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(), @@ -291,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] @@ -300,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 index 69090e6196f..b17f5aadc1d 100644 --- a/apps/cloud_federation_api/lib/ResponseDefinitions.php +++ b/apps/cloud_federation_api/lib/ResponseDefinitions.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Kate Döen <kate.doeen@nextcloud.com> - * - * @author Kate Döen <kate.doeen@nextcloud.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: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\CloudFederationAPI; @@ -29,6 +12,7 @@ namespace OCA\CloudFederationAPI; /** * @psalm-type CloudFederationAPIAddShare = array{ * recipientDisplayName: string, + * recipientUserId?: string, * } * * @psalm-type CloudFederationAPIError = array{ @@ -36,10 +20,10 @@ namespace OCA\CloudFederationAPI; * } * * @psalm-type CloudFederationAPIValidationError = CloudFederationAPIError&array{ - * validationErrors: 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 index ca20c31d6d9..9c92a152bf8 100644 --- a/apps/cloud_federation_api/openapi.json +++ b/apps/cloud_federation_api/openapi.json @@ -28,62 +28,18 @@ "properties": { "recipientDisplayName": { "type": "string" + }, + "recipientUserId": { + "type": "string" } } }, "Capabilities": { "type": "object", - "required": [ - "ocm" - ], - "properties": { - "ocm": { - "type": "object", - "required": [ - "enabled", - "apiVersion", - "endPoint", - "resourceTypes" - ], - "properties": { - "enabled": { - "type": "boolean" - }, - "apiVersion": { - "type": "string" - }, - "endPoint": { - "type": "string" - }, - "resourceTypes": { - "type": "array", - "items": { - "type": "object", - "required": [ - "name", - "shareTypes", - "protocols" - ], - "properties": { - "name": { - "type": "string" - }, - "shareTypes": { - "type": "array", - "items": { - "type": "string" - } - }, - "protocols": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - } + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object" } } }, @@ -151,107 +107,93 @@ "basic_auth": [] } ], - "parameters": [ - { - "name": "shareWith", - "in": "query", - "description": "The user who the share will be shared with", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "name", - "in": "query", - "description": "The resource name (e.g. document.odt)", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "description", - "in": "query", - "description": "Share description", - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "providerId", - "in": "query", - "description": "Resource UID on the provider side", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "owner", - "in": "query", - "description": "Provider specific UID of the user who owns the resource", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "ownerDisplayName", - "in": "query", - "description": "Display name of the user who shared the item", - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "sharedBy", - "in": "query", - "description": "Provider specific UID of the user who shared the resource", - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "sharedByDisplayName", - "in": "query", - "description": "Display name of the user who shared the resource", - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "protocol", - "in": "query", - "description": "e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "shareType", - "in": "query", - "description": "'group' or 'user' share", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "resourceType", - "in": "query", - "description": "'file', 'calendar',...", - "required": true, - "schema": { - "type": "string" + "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", @@ -302,44 +244,43 @@ "basic_auth": [] } ], - "parameters": [ - { - "name": "notificationType", - "in": "query", - "description": "Notification type, e.g. SHARE_ACCEPTED", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "resourceType", - "in": "query", - "description": "calendar, file, contact,...", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "providerId", - "in": "query", - "description": "ID of the share", - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "notification", - "in": "query", - "description": "The actual payload of the notification", - "schema": { - "type": "string", - "nullable": true + "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", @@ -386,6 +327,167 @@ } } } + }, + "/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": [ @@ -394,4 +496,4 @@ "description": "Open-Cloud-Mesh-API" } ] -}
\ No newline at end of file +} 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)); + } +} |