From 85a5c0de0f3fcacd2e664c300f45a50cf8883553 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Wed, 17 Jul 2024 16:48:47 +0200 Subject: [PATCH] feat(files_sharing): add public name prompt for files requests Signed-off-by: skjnldsv --- .../dav/lib/Files/Sharing/FilesDropPlugin.php | 73 ++++++++-- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/files_sharing/js/files_drop.js | 6 +- .../files_sharing/lib/AppInfo/Application.php | 7 +- .../DefaultPublicShareTemplateProvider.php | 49 +++---- .../LoadPublicFileRequestAuthListener.php | 30 ++++ apps/files_sharing/src/public-file-request.ts | 23 +++ .../src/views/PublicAuthPrompt.vue | 136 ++++++++++++++++++ apps/files_sharing/templates/public.php | 10 +- .../tests/Controller/ShareControllerTest.php | 14 +- webpack.modules.js | 1 + 12 files changed, 299 insertions(+), 52 deletions(-) create mode 100644 apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php create mode 100644 apps/files_sharing/src/public-file-request.ts create mode 100644 apps/files_sharing/src/views/PublicAuthPrompt.vue diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php index c4d1957c67e4c..01d3f81ec2550 100644 --- a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php +++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php @@ -6,7 +6,10 @@ namespace OCA\DAV\Files\Sharing; use OC\Files\View; +use OCP\Share\IManager; use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; @@ -16,17 +19,14 @@ */ class FilesDropPlugin extends ServerPlugin { - /** @var View */ - private $view; + private ?View $view = null; + private bool $enabled = false; - /** @var bool */ - private $enabled = false; - - public function setView(View $view) { + public function setView(View $view): void { $this->view = $view; } - public function enable() { + public function enable(): void { $this->enabled = true; } @@ -39,25 +39,80 @@ public function enable() { * @return void * @throws MethodNotAllowed */ - public function initialize(\Sabre\DAV\Server $server) { + public function initialize(\Sabre\DAV\Server $server): void { $server->on('beforeMethod:*', [$this, 'beforeMethod'], 999); $this->enabled = false; } - public function beforeMethod(RequestInterface $request, ResponseInterface $response) { + public function beforeMethod(RequestInterface $request, ResponseInterface $response): void { if (!$this->enabled) { return; } + $paths = explode('/', $request->getPath()); + + // Only allow file drop or folder creation if ($request->getMethod() !== 'PUT' && $request->getMethod() !== 'MKCOL') { throw new MethodNotAllowed('Only PUT is allowed on files drop'); } + // Only allow folder creation at the root of the files drop + if ($request->getMethod() === 'MKCOL' && count($paths) > 1) { + throw new MethodNotAllowed('Cannot create deep folders inside the files drop'); + } + + // Always upload at the root level $path = explode('/', $request->getPath()); $path = array_pop($path); + $isFileRequest = false; + $nickName = $request->getHeader('X-NC-Nickname'); + try { + $shareManager = \OCP\Server::get(IManager::class); + $token = $this->getToken($request); + $share = $shareManager->getShareByToken($token); + $attributes = $share->getAttributes(); + $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true; + } catch (\Exception $e) { + // Continue + } + + // We need a valid nickname for file requests + if ($isFileRequest && ($nickName == null || trim($nickName) === '')) { + throw new MethodNotAllowed('Nickname is required for file requests'); + } + + // If this is a file request we need to create a folder for the user + if ($isFileRequest) { + // Check if the folder already exists + if (!$this->view->file_exists($nickName) === true) { + $this->view->mkdir($nickName); + } + $path = $nickName . '/' . $path; + } + $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view); $url = $request->getBaseUrl() . $newName; $request->setUrl($url); } + + + + /** + * Extract token from request url + * @param RequestInterface $request + * @return string + * @throws NotFound + */ + private function getToken(RequestInterface $request): string { + $path = $request->getBaseUrl() ?: ''; + // ['', 'public.php', 'dav', 'files', 'token'] + $splittedPath = explode('/', $path); + + if (count($splittedPath) < 5 || $splittedPath[4] === '') { + throw new NotFound(); + } + + return $splittedPath[4]; + } } diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index e1abddb3a6450..b7a931a622895 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -59,6 +59,7 @@ 'OCA\\Files_Sharing\\Listener\\BeforeDirectFileDownloadListener' => $baseDir . '/../lib/Listener/BeforeDirectFileDownloadListener.php', 'OCA\\Files_Sharing\\Listener\\BeforeZipCreatedListener' => $baseDir . '/../lib/Listener/BeforeZipCreatedListener.php', 'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php', + 'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => $baseDir . '/../lib/Listener/LoadPublicFileRequestAuthListener.php', 'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php', 'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php', 'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php', diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index 5d2fb3bac2a47..70dc7be7cdf69 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -74,6 +74,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\Listener\\BeforeDirectFileDownloadListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeDirectFileDownloadListener.php', 'OCA\\Files_Sharing\\Listener\\BeforeZipCreatedListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeZipCreatedListener.php', 'OCA\\Files_Sharing\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php', + 'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => __DIR__ . '/..' . '/../lib/Listener/LoadPublicFileRequestAuthListener.php', 'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php', 'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php', 'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php', diff --git a/apps/files_sharing/js/files_drop.js b/apps/files_sharing/js/files_drop.js index fd9b796ee2c04..6f4a016513e30 100644 --- a/apps/files_sharing/js/files_drop.js +++ b/apps/files_sharing/js/files_drop.js @@ -22,7 +22,7 @@ // note: password not be required, the endpoint // will recognize previous validation from the session root: OC.getRootPath() + '/public.php/dav/files/' + $('#sharingToken').val() + '/', - useHTTPS: OC.getProtocol() === 'https' + useHTTPS: OC.getProtocol() === 'https', }); // We only process one file at a time 🤷‍♀️ @@ -44,7 +44,9 @@ data.multipart = false; if (!data.headers) { - data.headers = {}; + data.headers = { + 'X-NC-Nickname': localStorage.getItem('nick'), + }; } $('#drop-upload-done-indicator').addClass('hidden'); diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 82a5981febf85..98c2d280856e0 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -18,6 +18,7 @@ use OCA\Files_Sharing\Listener\BeforeDirectFileDownloadListener; use OCA\Files_Sharing\Listener\BeforeZipCreatedListener; use OCA\Files_Sharing\Listener\LoadAdditionalListener; +use OCA\Files_Sharing\Listener\LoadPublicFileRequestAuthListener; use OCA\Files_Sharing\Listener\LoadSidebarListener; use OCA\Files_Sharing\Listener\ShareInteractionListener; use OCA\Files_Sharing\Listener\UserAddedToGroupListener; @@ -34,6 +35,7 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent; use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\ICloudIdManager; @@ -85,7 +87,7 @@ function () use ($c) { $context->registerEventListener(GroupChangedEvent::class, GroupDisplayNameCache::class); $context->registerEventListener(GroupDeletedEvent::class, GroupDisplayNameCache::class); - // sidebar and files scripts + // Sidebar and files scripts $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class); $context->registerEventListener(ShareCreatedEvent::class, ShareInteractionListener::class); @@ -95,6 +97,9 @@ function () use ($c) { // Handle download events for view only checks $context->registerEventListener(BeforeZipCreatedEvent::class, BeforeZipCreatedListener::class); $context->registerEventListener(BeforeDirectFileDownloadEvent::class, BeforeDirectFileDownloadListener::class); + + // File request auth + $context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadPublicFileRequestAuthListener::class); } public function boot(IBootContext $context): void { diff --git a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php index e125100eb6864..477bc9f82ce7d 100644 --- a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php +++ b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php @@ -19,6 +19,7 @@ use OCP\AppFramework\Http\Template\PublicTemplateResponse; use OCP\AppFramework\Http\Template\SimpleMenuAction; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; use OCP\Constants; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; @@ -37,39 +38,20 @@ use OCP\Util; class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider { - private IUserManager $userManager; - private IAccountManager $accountManager; - private IPreview $previewManager; - protected FederatedShareProvider $federatedShareProvider; - private IURLGenerator $urlGenerator; - private IEventDispatcher $eventDispatcher; - private IL10N $l10n; - private Defaults $defaults; - private IConfig $config; - private IRequest $request; public function __construct( - IUserManager $userManager, - IAccountManager $accountManager, - IPreview $previewManager, - FederatedShareProvider $federatedShareProvider, - IUrlGenerator $urlGenerator, - IEventDispatcher $eventDispatcher, - IL10N $l10n, - Defaults $defaults, - IConfig $config, - IRequest $request + private IUserManager $userManager, + private IAccountManager $accountManager, + private IPreview $previewManager, + protected FederatedShareProvider $federatedShareProvider, + private IUrlGenerator $urlGenerator, + private IEventDispatcher $eventDispatcher, + private IL10N $l10n, + private Defaults $defaults, + private IConfig $config, + private IRequest $request, + private IInitialState $initialState, ) { - $this->userManager = $userManager; - $this->accountManager = $accountManager; - $this->previewManager = $previewManager; - $this->federatedShareProvider = $federatedShareProvider; - $this->urlGenerator = $urlGenerator; - $this->eventDispatcher = $eventDispatcher; - $this->l10n = $l10n; - $this->defaults = $defaults; - $this->config = $config; - $this->request = $request; } public function shouldRespond(IShare $share): bool { @@ -91,9 +73,16 @@ public function renderPage(IShare $share, string $token, string $path): Template if ($ownerName->getScope() === IAccountManager::SCOPE_PUBLISHED) { $shareTmpl['owner'] = $owner->getUID(); $shareTmpl['shareOwner'] = $owner->getDisplayName(); + $this->initialState->provideInitialState('owner', $shareTmpl['owner']); + $this->initialState->provideInitialState('ownerDisplayName', $shareTmpl['shareOwner']); } } + // Provide initial state + $this->initialState->provideInitialState('label', $share->getLabel()); + $this->initialState->provideInitialState('note', $share->getNote()); + $this->initialState->provideInitialState('filename', $shareNode->getName()); + $shareTmpl['filename'] = $shareNode->getName(); $shareTmpl['directory_path'] = $share->getTarget(); $shareTmpl['label'] = $share->getLabel(); diff --git a/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php b/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php new file mode 100644 index 0000000000000..099188248e0a7 --- /dev/null +++ b/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php @@ -0,0 +1,30 @@ + */ +class LoadPublicFileRequestAuthListener implements IEventListener { + public function handle(Event $event): void { + if (!$event instanceof BeforeTemplateRenderedEvent) { + return; + } + + // Make sure we are on a public page rendering + if ($event->getResponse()->getRenderAs() !== TemplateResponse::RENDER_AS_PUBLIC) { + return; + } + Util::addScript(Application::APP_ID, Application::APP_ID . '-public'); + } +} diff --git a/apps/files_sharing/src/public-file-request.ts b/apps/files_sharing/src/public-file-request.ts new file mode 100644 index 0000000000000..763c4f606240c --- /dev/null +++ b/apps/files_sharing/src/public-file-request.ts @@ -0,0 +1,23 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { spawnDialog } from '@nextcloud/dialogs' +import { defineAsyncComponent } from 'vue' +import logger from './services/logger' + +const nick = localStorage.getItem('nick') +const publicAuthPromptShown = localStorage.getItem('publicAuthPromptShown') + +// If we don't have a nickname or the public auth prompt hasn't been shown yet, show it +// We still show the prompt if the user has a nickname to double check +if (!nick || !publicAuthPromptShown) { + spawnDialog( + defineAsyncComponent(() => import('./views/PublicAuthPrompt.vue')), + {}, + () => localStorage.setItem('publicAuthPromptShown', 'true'), + ) +} else { + logger.debug(`Public auth prompt already shown. Current nickname is '${nick}'`) +} diff --git a/apps/files_sharing/src/views/PublicAuthPrompt.vue b/apps/files_sharing/src/views/PublicAuthPrompt.vue new file mode 100644 index 0000000000000..a929afffefbd7 --- /dev/null +++ b/apps/files_sharing/src/views/PublicAuthPrompt.vue @@ -0,0 +1,136 @@ + + + + + + diff --git a/apps/files_sharing/templates/public.php b/apps/files_sharing/templates/public.php index 109eaf2e9da15..43b13b269462e 100644 --- a/apps/files_sharing/templates/public.php +++ b/apps/files_sharing/templates/public.php @@ -27,6 +27,7 @@ + get(\bantu\IniGetWrapper\IniGetWrapper::class)->getBytes('upload_max_filesize'); $post_max_size = OC::$server->get(\bantu\IniGetWrapper\IniGetWrapper::class)->getBytes('post_max_size'); @@ -102,14 +103,11 @@ class="emptycontent has-note">
-

t('Upload files to %s', [$_['shareOwner']])) ?>

-

- -

t('Upload files to %s', [$_['label']])) ?>

- +

t('%s shared a folder with you.', [$_['shareOwner']])) ?>

+
-

t('Upload files to %s', [$_['filename']])) ?>

+

t('Upload files to %s', [$_['label'] ?? $_['filename']])) ?>

diff --git a/apps/files_sharing/tests/Controller/ShareControllerTest.php b/apps/files_sharing/tests/Controller/ShareControllerTest.php index 493ac10a24bea..79b90d8a15682 100644 --- a/apps/files_sharing/tests/Controller/ShareControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareControllerTest.php @@ -22,6 +22,7 @@ use OCP\AppFramework\Http\Template\LinkMenuAction; use OCP\AppFramework\Http\Template\PublicTemplateResponse; use OCP\AppFramework\Http\Template\SimpleMenuAction; +use OCP\AppFramework\Services\IInitialState; use OCP\Constants; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; @@ -121,6 +122,7 @@ protected function setUp(): void { $this->defaults, $this->config, $this->createMock(IRequest::class), + $this->createMock(IInitialState::class) ) ); @@ -350,7 +352,8 @@ public function testShowShare() { 'previewURL' => 'downloadURL', 'note' => $note, 'hideDownload' => false, - 'showgridview' => false + 'showgridview' => false, + 'label' => '' ]; $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); @@ -511,7 +514,8 @@ public function testShowShareWithPrivateName() { 'previewURL' => 'downloadURL', 'note' => $note, 'hideDownload' => false, - 'showgridview' => false + 'showgridview' => false, + 'label' => '' ]; $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); @@ -672,7 +676,8 @@ public function testShowShareHideDownload() { 'previewURL' => 'downloadURL', 'note' => $note, 'hideDownload' => true, - 'showgridview' => false + 'showgridview' => false, + 'label' => '' ]; $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); @@ -798,7 +803,8 @@ public function testShareFileDrop() { 'previewURL' => '', 'note' => '', 'hideDownload' => false, - 'showgridview' => false + 'showgridview' => false, + 'label' => '' ]; $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); diff --git a/webpack.modules.js b/webpack.modules.js index d13ad284bab3e..887d8dc75a089 100644 --- a/webpack.modules.js +++ b/webpack.modules.js @@ -54,6 +54,7 @@ module.exports = { init: path.join(__dirname, 'apps/files_sharing/src', 'init.ts'), main: path.join(__dirname, 'apps/files_sharing/src', 'main.ts'), 'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'), + 'public-file-request': path.join(__dirname, 'apps/files_sharing/src', 'public-file-request.ts'), }, files_trashbin: { init: path.join(__dirname, 'apps/files_trashbin/src', 'files-init.ts'),