diff --git a/apps/dav/appinfo/v1/publicwebdav.php b/apps/dav/appinfo/v1/publicwebdav.php index 5ef383e1dd5d7..3875337415004 100644 --- a/apps/dav/appinfo/v1/publicwebdav.php +++ b/apps/dav/appinfo/v1/publicwebdav.php @@ -87,6 +87,7 @@ $view = new \OC\Files\View($node->getPath()); $filesDropPlugin->setView($view); + $filesDropPlugin->setShare($share); return $view; }); diff --git a/apps/dav/appinfo/v2/publicremote.php b/apps/dav/appinfo/v2/publicremote.php index bdc4169dd4ebe..44cf4214505da 100644 --- a/apps/dav/appinfo/v2/publicremote.php +++ b/apps/dav/appinfo/v2/publicremote.php @@ -116,6 +116,7 @@ $view = new View($node->getPath()); $filesDropPlugin->setView($view); + $filesDropPlugin->setShare($share); return $view; }); diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php index 2f4bacf69d4b7..69328d42272c6 100644 --- a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php +++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php @@ -6,6 +6,7 @@ namespace OCA\DAV\Files\Sharing; use OC\Files\View; +use OCP\Share\IShare; use Sabre\DAV\Exception\MethodNotAllowed; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; @@ -16,20 +17,19 @@ */ class FilesDropPlugin extends ServerPlugin { - /** @var View */ - private $view; + private ?View $view = null; + private ?IShare $share = null; + private bool $enabled = false; - /** @var bool */ - private $enabled = false; - - /** - * @param View $view - */ - public function setView($view) { + public function setView(View $view): void { $this->view = $view; } - public function enable() { + public function setShare(IShare $share): void { + $this->share = $share; + } + + public function enable(): void { $this->enabled = true; } @@ -42,25 +42,51 @@ 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) { - if (!$this->enabled) { + public function beforeMethod(RequestInterface $request, ResponseInterface $response): void { + if (!$this->enabled || $this->share === null || $this->view === null) { return; } + // Only allow file drop if ($request->getMethod() !== 'PUT') { throw new MethodNotAllowed('Only PUT is allowed on files drop'); } + // Always upload at the root level $path = explode('/', $request->getPath()); $path = array_pop($path); + // Extract the attributes for the file request + $isFileRequest = false; + $attributes = $this->share->getAttributes(); + $nickName = $request->getHeader('X-NC-Nickname'); + if ($attributes !== null) { + $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true; + } + + // 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); + } + // Put all files in the subfolder + $path = $nickName . '/' . $path; + } + $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view); $url = $request->getBaseUrl() . $newName; $request->setUrl($url); } + } diff --git a/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php b/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php index 9a077e35076aa..7264119f8c688 100644 --- a/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php +++ b/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php @@ -7,6 +7,8 @@ use OC\Files\View; use OCA\DAV\Files\Sharing\FilesDropPlugin; +use OCP\Share\IAttributes; +use OCP\Share\IShare; use Sabre\DAV\Exception\MethodNotAllowed; use Sabre\DAV\Server; use Sabre\HTTP\RequestInterface; @@ -18,6 +20,9 @@ class FilesDropPluginTest extends TestCase { /** @var View|\PHPUnit\Framework\MockObject\MockObject */ private $view; + /** @var IShare|\PHPUnit\Framework\MockObject\MockObject */ + private $share; + /** @var Server|\PHPUnit\Framework\MockObject\MockObject */ private $server; @@ -34,6 +39,7 @@ protected function setUp(): void { parent::setUp(); $this->view = $this->createMock(View::class); + $this->share = $this->createMock(IShare::class); $this->server = $this->createMock(Server::class); $this->plugin = new FilesDropPlugin(); @@ -42,6 +48,11 @@ protected function setUp(): void { $this->response->expects($this->never()) ->method($this->anything()); + + $attributes = $this->createMock(IAttributes::class); + $this->share->expects($this->any()) + ->method('getAttributes') + ->willReturn($attributes); } public function testInitialize(): void { @@ -69,6 +80,7 @@ public function testNotEnabled(): void { public function testValid(): void { $this->plugin->enable(); $this->plugin->setView($this->view); + $this->plugin->setShare($this->share); $this->request->method('getMethod') ->willReturn('PUT'); @@ -93,6 +105,7 @@ public function testValid(): void { public function testFileAlreadyExistsValid(): void { $this->plugin->enable(); $this->plugin->setView($this->view); + $this->plugin->setShare($this->share); $this->request->method('getMethod') ->willReturn('PUT'); @@ -122,6 +135,7 @@ public function testFileAlreadyExistsValid(): void { public function testNoMKCOL(): void { $this->plugin->enable(); $this->plugin->setView($this->view); + $this->plugin->setShare($this->share); $this->request->method('getMethod') ->willReturn('MKCOL'); @@ -134,6 +148,7 @@ public function testNoMKCOL(): void { public function testNoSubdirPut(): void { $this->plugin->enable(); $this->plugin->setView($this->view); + $this->plugin->setShare($this->share); $this->request->method('getMethod') ->willReturn('PUT'); 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..450af078af269 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 🤷‍♀️ @@ -47,6 +47,10 @@ data.headers = {}; } + if (localStorage.getItem('nick') !== null) { + data.headers['X-NC-Nickname'] = localStorage.getItem('nick') + } + $('#drop-upload-done-indicator').addClass('hidden'); $('#drop-upload-progress-indicator').removeClass('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/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index f2ace7b4d704b..1e6750a5bceb2 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -596,8 +596,10 @@ public function createShare( throw new OCSNotFoundException($this->l->t('Invalid permissions')); } - // Shares always require read permissions - $permissions |= Constants::PERMISSION_READ; + // Shares always require read permissions OR create permissions + if (($permissions & Constants::PERMISSION_READ) === 0 && ($permissions & Constants::PERMISSION_CREATE) === 0) { + $permissions |= Constants::PERMISSION_READ; + } if ($node instanceof \OCP\Files\File) { // Single file shares should never have delete or create permissions diff --git a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php index 8f5ea9fd0beb5..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,11 +73,19 @@ 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(); $shareTmpl['note'] = $share->getNote(); $shareTmpl['mimetype'] = $shareNode->getMimetype(); $shareTmpl['previewSupported'] = $this->previewManager->isMimeSupported($shareNode->getMimetype()); @@ -240,6 +230,11 @@ public function renderPage(IShare $share, string $token, string $path): Template $response->setHeaderDetails($this->l10n->t('shared by %s', [$shareTmpl['shareOwner']])); } + // If the share has a label, use it as the title + if ($shareTmpl['label'] !== '') { + $response->setHeaderTitle($shareTmpl['label']); + } + $isNoneFileDropFolder = $shareIsFolder === false || $share->getPermissions() !== Constants::PERMISSION_CREATE; if ($isNoneFileDropFolder && !$share->getHideDownload()) { diff --git a/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php b/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php new file mode 100644 index 0000000000000..f1e054c7ee5e1 --- /dev/null +++ b/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php @@ -0,0 +1,59 @@ + */ +class LoadPublicFileRequestAuthListener implements IEventListener { + public function __construct( + private IManager $shareManager, + ) { + } + + 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; + } + + $token = $event->getResponse()->getParams()['sharingToken'] ?? null; + if ($token === null || $token === '') { + return; + } + + // Check if the share is a file request + $isFileRequest = false; + try { + $share = $this->shareManager->getShareByToken($token); + $attributes = $share->getAttributes(); + if ($attributes === null) { + return; + } + + $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true; + } catch (\Exception $e) { + // Ignore, this is not a file request or the share does not exist + } + + if ($isFileRequest) { + // Add the script to the public page + Util::addScript(Application::APP_ID, 'public-file-request'); + } + } +} diff --git a/apps/files_sharing/src/actions/openInFilesAction.ts b/apps/files_sharing/src/actions/openInFilesAction.ts index 51b9ec84a1d94..82b66927c9e4e 100644 --- a/apps/files_sharing/src/actions/openInFilesAction.ts +++ b/apps/files_sharing/src/actions/openInFilesAction.ts @@ -11,7 +11,7 @@ import { sharesViewId, sharedWithYouViewId, sharedWithOthersViewId, sharingByLin export const action = new FileAction({ id: 'open-in-files', - displayName: () => t('files', 'Open in Files'), + displayName: () => t('files_sharing', 'Open in Files'), iconSvgInline: () => '', enabled: (nodes, view) => [ diff --git a/apps/files_sharing/src/components/NewFileRequestDialog.vue b/apps/files_sharing/src/components/NewFileRequestDialog.vue index 35cd4395290b9..6b0bb5a1fc221 100644 --- a/apps/files_sharing/src/components/NewFileRequestDialog.vue +++ b/apps/files_sharing/src/components/NewFileRequestDialog.vue @@ -50,8 +50,22 @@