diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index 6c94490b0855a..ca556c62a5734 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -183,6 +183,12 @@ 'url' => '/api/v1/openlocaleditor/{token}', 'verb' => 'POST', ], + [ + /** @see DownloadController::index() */ + 'name' => 'Download#index', + 'url' => '/api/v1/download', + 'verb' => 'GET', + ], ], ] ); diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 2327cf4413812..62047cc60de1c 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -36,6 +36,7 @@ 'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php', 'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php', 'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php', + 'OCA\\Files\\Controller\\DownloadController' => $baseDir . '/../lib/Controller/DownloadController.php', 'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php', 'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php', 'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php', @@ -54,6 +55,7 @@ 'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php', 'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php', 'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', + 'OCA\\Files\\Provider\\FileDownloadProvider' => $baseDir . '/../lib/Provider/FileDownloadProvider.php', 'OCA\\Files\\Search\\FilesSearchProvider' => $baseDir . '/../lib/Search/FilesSearchProvider.php', 'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php', 'OCA\\Files\\Service\\OwnershipTransferService' => $baseDir . '/../lib/Service/OwnershipTransferService.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index fe23d4ed7b0e1..fdcb54447f595 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -51,6 +51,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php', 'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php', 'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php', + 'OCA\\Files\\Controller\\DownloadController' => __DIR__ . '/..' . '/../lib/Controller/DownloadController.php', 'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php', 'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php', 'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php', @@ -69,6 +70,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php', 'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php', 'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', + 'OCA\\Files\\Provider\\FileDownloadProvider' => __DIR__ . '/..' . '/../lib/Provider/FileDownloadProvider.php', 'OCA\\Files\\Search\\FilesSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FilesSearchProvider.php', 'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php', 'OCA\\Files\\Service\\OwnershipTransferService' => __DIR__ . '/..' . '/../lib/Service/OwnershipTransferService.php', diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 2662f2d6e9b48..4f659977a33d8 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -45,6 +45,7 @@ use OCA\Files\Listener\LegacyLoadAdditionalScriptsAdapter; use OCA\Files\Listener\LoadSidebarListener; use OCA\Files\Notification\Notifier; +use OCA\Files\Provider\FileDownloadProvider; use OCA\Files\Search\FilesSearchProvider; use OCA\Files\Service\TagService; use OCP\Activity\IManager as IActivityManager; @@ -120,6 +121,8 @@ public function register(IRegistrationContext $context): void { $context->registerSearchProvider(FilesSearchProvider::class); $context->registerNotifierService(Notifier::class); + + $context->registerFileDownloadProvider(FileDownloadProvider::class); } public function boot(IBootContext $context): void { diff --git a/apps/files/lib/Controller/DownloadController.php b/apps/files/lib/Controller/DownloadController.php new file mode 100644 index 0000000000000..29d46ba826abe --- /dev/null +++ b/apps/files/lib/Controller/DownloadController.php @@ -0,0 +1,154 @@ + + * + * @author Louis Chmn + * + * @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 . + * + */ + +namespace OCA\Files\Controller; + +use OC\AppFramework\Bootstrap\Coordinator; +use OCP\AppFramework\Http\ZipResponse; +use OCP\AppFramework\Controller; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IFileDownloadProvider; +use OCP\Files\Node; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +class DownloadController extends Controller { + private Coordinator $coordinator; + private LoggerInterface $logger; + + public function __construct( + string $appName, + IRequest $request, + Coordinator $coordinator, + LoggerInterface $logger + ) { + parent::__construct($appName, $request); + + $this->request = $request; + $this->coordinator = $coordinator; + $this->logger = $logger; + } + + /** + * @NoCSRFRequired + * @PublicPage + * @UserRateThrottle(limit=5, period=100) + * @AnonRateThrottle(limit=1, period=100) + * @BruteForceProtection(action='download_files') + */ + public function index(string $files): ZipResponse { + $response = new ZipResponse($this->request, 'download'); + + /** @var string[] */ + $files = json_decode($files); + + if (count($files) === 0) { + return $response; + } + + [$firstPrefix,] = \Sabre\Uri\split($files[0]); + $commonPrefix = $firstPrefix; + foreach ($files as $filePath) { + $commonPrefix = $this->getCommonPrefix($filePath, $commonPrefix); + } + + foreach ($files as $filePath) { + $node = null; + + foreach ($this->getProviders() as $provider) { + try { + $node = $provider->getNode($filePath); + if ($node !== null) { + break; + } + } catch (\Throwable $ex) { + $providerClass = $provider::class; + $this->logger->warning("Error while getting file content from $providerClass", ['exception' => $ex]); + } + } + + if ($node === null) { + continue; + } + + $this->addNode($response, $node, substr($filePath, strlen($commonPrefix))); + } + + return $response; + } + + private function getCommonPrefix(string $str1, string $str2): string { + $mbStr1 = mb_str_split($str1); + $mbStr2 = mb_str_split($str2); + + for ($i = 0; $i < count($mbStr1); $i++) { + if ($mbStr1[$i] !== $mbStr2[$i]) { + $i--; + break; + } + } + + if ($i < 0) { + return ''; + } else { + return join(array_slice($mbStr1, 0, $i)); + } + } + + private function addNode(ZipResponse $response, Node $node, string $path): void { + if ($node instanceof File) { + $response->addResource($node->fopen('r'), $path, $node->getSize()); + } + + if ($node instanceof Folder) { + foreach ($node->getDirectoryListing() as $subnode) { + $this->addNode($response, $subnode, $path.'/'.$subnode->getName()); + } + } + } + + /** + * @return IFileDownloadProvider[] + */ + private function getProviders() { + /** @var IFileDownloadProvider[] */ + $providers = []; + + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + throw new \Exception("Can't get download providers"); + } + + $providerRegistrations = $context->getFileDownloadProviders(); + + foreach ($providerRegistrations as $registration) { + $providers[] = \OCP\Server::get($registration->getService()); + } + + return $providers; + } +} diff --git a/apps/files/lib/Provider/FileDownloadProvider.php b/apps/files/lib/Provider/FileDownloadProvider.php new file mode 100644 index 0000000000000..3cd6546cc3664 --- /dev/null +++ b/apps/files/lib/Provider/FileDownloadProvider.php @@ -0,0 +1,60 @@ + + * + * @author Louis Chmn + * + * @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 . + * + */ +namespace OCA\Files\Provider; + +use OCP\Files\Folder; +use OCP\Files\Node; +use OCP\Files\IFileDownloadProvider; + +class FileDownloadProvider implements IFileDownloadProvider { + private ?Folder $userFolder; + + public function __construct( + ?Folder $userFolder + ) { + $this->userFolder = $userFolder; + } + + public function getNode(string $davPath): ?Node { + if (!str_starts_with($davPath, "files/")) { + return null; + } + + if ($this->userFolder === null) { + return null; + } + + /** @var ?string */ + $userId = explode('/', $davPath, 3)[1] ?? null; + if (is_null($userId) || $userId !== $this->userFolder->getOwner()->getUID()) { + return null; + } + + $filePath = substr($davPath, strlen("files/$userId")); + + return $this->userFolder->get($filePath); + } +} diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 3ade98e334f4d..1fceeffe2ac30 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -47,6 +47,7 @@ use OCP\Dashboard\IManager; use OCP\Dashboard\IWidget; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\IFileDownloadProvider; use OCP\Files\Template\ICustomTemplateProvider; use OCP\Http\WellKnown\IHandler; use OCP\Notification\INotifier; @@ -58,7 +59,6 @@ use Throwable; class RegistrationContext { - /** @var ServiceRegistration[] */ private $capabilities = []; @@ -128,6 +128,9 @@ class RegistrationContext { /** @var ParameterRegistration[] */ private $sensitiveMethods = []; + /** @var ServiceRegistration[] */ + private array $fileDownloadProviders = []; + /** @var LoggerInterface */ private $logger; @@ -326,6 +329,13 @@ public function registerSensitiveMethods(string $class, array $methods): void { $methods ); } + + public function registerFileDownloadProvider(string $class): void { + $this->context->registerFileDownloadProvider( + $this->appId, + $class + ); + } }; } @@ -461,6 +471,10 @@ public function registerSensitiveMethods(string $appId, string $class, array $me $this->sensitiveMethods[] = new ParameterRegistration($appId, $class, $methods); } + public function registerFileDownloadProvider(string $appId, string $class): void { + $this->fileDownloadProviders[] = new ServiceRegistration($appId, $class); + } + /** * @param App[] $apps */ @@ -757,4 +771,11 @@ public function getUserMigrators(): array { public function getSensitiveMethods(): array { return $this->sensitiveMethods; } + + /** + * @return ServiceRegistration[] + */ + public function getFileDownloadProviders(): array { + return $this->fileDownloadProviders; + } } diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 0f398c13979bb..c7500f328438a 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -47,7 +47,6 @@ * @see IBootstrap::register() */ interface IRegistrationContext { - /** * @param string $capability * @psalm-param class-string $capability @@ -327,4 +326,14 @@ public function registerUserMigrator(string $migratorClass): void; * @since 25.0.0 */ public function registerSensitiveMethods(string $class, array $methods): void; + + /** + * Register a backend to provide file based on a dav path. + * + * @param string $class + * @param string[] $methods + * @return void + * @since 26.0.0 + */ + public function registerFileDownloadProvider(string $class): void; } diff --git a/lib/public/Files/IFileDownloadProvider.php b/lib/public/Files/IFileDownloadProvider.php new file mode 100644 index 0000000000000..ed007bcb421c2 --- /dev/null +++ b/lib/public/Files/IFileDownloadProvider.php @@ -0,0 +1,38 @@ + + * + * @author Louis Chmn + * + * @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 . + * + */ +namespace OCP\Files; + +/** + * This interface defines how to provide a file given a dav path. + * + * @since 26.0.0 + */ +interface IFileDownloadProvider { + /** + * @since 26.0.0 + */ + public function getNode(string $davPath): ?Node; +}