diff options
Diffstat (limited to 'apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php')
-rw-r--r-- | apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php | 380 |
1 files changed, 175 insertions, 205 deletions
diff --git a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php index 8af1c803e18..afba45cac4a 100644 --- a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php +++ b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php @@ -2,23 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Louis Chemineau <louis@chmn.me> - * - * @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\Files_Sharing; @@ -34,11 +19,12 @@ use OCP\AppFramework\Http\Template\LinkMenuAction; 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; -use OCP\Files\FileInfo; -use OCP\Files\Folder; +use OCP\Files\File; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; use OCP\IPreview; @@ -48,43 +34,24 @@ use OCP\IUser; use OCP\IUserManager; use OCP\Share\IPublicShareTemplateProvider; use OCP\Share\IShare; -use OCP\Template; 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, + private IAppConfig $appConfig, ) { - $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 { @@ -93,105 +60,149 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider public function renderPage(IShare $share, string $token, string $path): TemplateResponse { $shareNode = $share->getNode(); + $ownerName = ''; + $ownerId = ''; - $shareTmpl = []; - $shareTmpl['owner'] = ''; - $shareTmpl['shareOwner'] = ''; - + // Only make the share owner public if they allowed to show their name $owner = $this->userManager->get($share->getShareOwner()); if ($owner instanceof IUser) { $ownerAccount = $this->accountManager->getAccount($owner); - $ownerName = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); - if ($ownerName->getScope() === IAccountManager::SCOPE_PUBLISHED) { - $shareTmpl['owner'] = $owner->getUID(); - $shareTmpl['shareOwner'] = $owner->getDisplayName(); + $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); + if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) { + $ownerId = $owner->getUID(); + $ownerName = $owner->getDisplayName(); + $this->initialState->provideInitialState('owner', $ownerId); + $this->initialState->provideInitialState('ownerDisplayName', $ownerName); } } - $shareTmpl['filename'] = $shareNode->getName(); - $shareTmpl['directory_path'] = $share->getTarget(); - $shareTmpl['note'] = $share->getNote(); - $shareTmpl['mimetype'] = $shareNode->getMimetype(); - $shareTmpl['previewSupported'] = $this->previewManager->isMimeSupported($shareNode->getMimetype()); - $shareTmpl['dirToken'] = $token; - $shareTmpl['sharingToken'] = $token; - $shareTmpl['server2serversharing'] = $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); - $shareTmpl['protected'] = $share->getPassword() !== null ? 'true' : 'false'; - $shareTmpl['dir'] = ''; - $shareTmpl['nonHumanFileSize'] = $shareNode->getSize(); - $shareTmpl['fileSize'] = Util::humanFileSize($shareNode->getSize()); - $shareTmpl['hideDownload'] = $share->getHideDownload(); - - $hideFileList = false; + $view = 'public-share'; + if ($shareNode instanceof File) { + $view = 'public-file-share'; + $this->initialState->provideInitialState('fileId', $shareNode->getId()); + } elseif (($share->getPermissions() & Constants::PERMISSION_CREATE) + && !($share->getPermissions() & Constants::PERMISSION_READ) + ) { + // share is a folder with create but no read permissions -> file drop only + $view = 'public-file-drop'; + // Only needed for file drops + $this->initialState->provideInitialState( + 'disclaimer', + $this->appConfig->getValueString('core', 'shareapi_public_link_disclaimertext'), + ); + // file drops do not request the root folder so we need to provide label and note if available + $this->initialState->provideInitialState('label', $share->getLabel()); + $this->initialState->provideInitialState('note', $share->getNote()); + } + // Set up initial state + $this->initialState->provideInitialState('isPublic', true); + $this->initialState->provideInitialState('sharingToken', $token); + $this->initialState->provideInitialState('sharePermissions', $share->getPermissions()); + $this->initialState->provideInitialState('filename', $shareNode->getName()); + $this->initialState->provideInitialState('view', $view); + + // Load scripts and styles for UI + Util::addInitScript('files', 'init'); + Util::addInitScript(Application::APP_ID, 'init'); + Util::addInitScript(Application::APP_ID, 'init-public'); + Util::addScript('files', 'main'); + Util::addScript(Application::APP_ID, 'public-nickname-handler'); + + // Add file-request script if needed + $attributes = $share->getAttributes(); + $isFileRequest = $attributes?->getAttribute('fileRequest', 'enabled') === true; + $this->initialState->provideInitialState('isFileRequest', $isFileRequest); - if ($shareNode instanceof Folder) { - $shareIsFolder = true; + // Load Viewer scripts + if (class_exists(LoadViewer::class)) { + $this->eventDispatcher->dispatchTyped(new LoadViewer()); + } - $folderNode = $shareNode->get($path); - $shareTmpl['dir'] = $shareNode->getRelativePath($folderNode->getPath()); + // Allow external apps to register their scripts + $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($share)); - /* - * The OC_Util methods require a view. This just uses the node API - */ - $freeSpace = $share->getNode()->getStorage()->free_space($share->getNode()->getInternalPath()); - if ($freeSpace < FileInfo::SPACE_UNLIMITED) { - $freeSpace = (int)max($freeSpace, 0); - } else { - $freeSpace = (INF > 0) ? INF: PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188 - } + $this->addMetaHeaders($share); - $hideFileList = !($share->getPermissions() & Constants::PERMISSION_READ); - $maxUploadFilesize = $freeSpace; + // CSP to allow office + $csp = new ContentSecurityPolicy(); + $csp->addAllowedFrameDomain('\'self\''); - $folder = new Template('files', 'list', ''); + $response = new PublicTemplateResponse( + 'files', + 'index', + ); + $response->setContentSecurityPolicy($csp); - $folder->assign('dir', $shareNode->getRelativePath($folderNode->getPath())); - $folder->assign('dirToken', $token); - $folder->assign('permissions', Constants::PERMISSION_READ); - $folder->assign('isPublic', true); - $folder->assign('hideFileList', $hideFileList); - $folder->assign('publicUploadEnabled', 'no'); - // default to list view - $folder->assign('showgridview', false); - $folder->assign('uploadMaxFilesize', $maxUploadFilesize); - $folder->assign('uploadMaxHumanFilesize', Util::humanFileSize($maxUploadFilesize)); - $folder->assign('freeSpace', $freeSpace); - $folder->assign('usedSpacePercent', 0); - $folder->assign('trash', false); - $shareTmpl['folder'] = $folder->fetchPage(); + // If the share has a label, use it as the title + if ($share->getLabel() !== '') { + $response->setHeaderTitle($share->getLabel()); + $response->setParams(['pageTitle' => $share->getLabel()]); } else { - $shareIsFolder = false; + $response->setHeaderTitle($shareNode->getName()); + $response->setParams(['pageTitle' => $shareNode->getName()]); } - // default to list view - $shareTmpl['showgridview'] = false; + if ($ownerName !== '') { + $response->setHeaderDetails($this->l10n->t('shared by %s', [$ownerName])); + } - $shareTmpl['hideFileList'] = $hideFileList; - $shareTmpl['downloadURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadShare', [ - 'token' => $token, - 'filename' => $shareIsFolder ? null : $shareNode->getName() - ]); - $shareTmpl['shareUrl'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]); - $shareTmpl['maxSizeAnimateGif'] = $this->config->getSystemValue('max_filesize_animated_gifs_public_sharing', 10); - $shareTmpl['previewEnabled'] = $this->config->getSystemValue('enable_previews', true); - $shareTmpl['previewMaxX'] = $this->config->getSystemValue('preview_max_x', 1024); - $shareTmpl['previewMaxY'] = $this->config->getSystemValue('preview_max_y', 1024); - $shareTmpl['disclaimer'] = $this->config->getAppValue('core', 'shareapi_public_link_disclaimertext', ''); - $shareTmpl['previewURL'] = $shareTmpl['downloadURL']; + // Create the header action menu + $headerActions = []; + if ($view !== 'public-file-drop' && !$share->getHideDownload()) { + // The download URL is used for the "download" header action as well as in some cases for the direct link + $downloadUrl = $this->urlGenerator->getAbsoluteURL('/public.php/dav/files/' . $token . '/?accept=zip'); - if ($shareTmpl['previewSupported']) { - $shareTmpl['previewImage'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.PublicPreview.getPreview', - ['x' => 200, 'y' => 200, 'file' => $shareTmpl['directory_path'], 'token' => $shareTmpl['dirToken']]); - $ogPreview = $shareTmpl['previewImage']; + // If not a file drop, then add the download header action + $headerActions[] = new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', $downloadUrl, 0, (string)$shareNode->getSize()); - // We just have direct previews for image files - if ($shareNode->getMimePart() === 'image') { - $shareTmpl['previewURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $token]); + // If remote sharing is enabled also add the remote share action to the menu + if ($this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { + $headerActions[] = new ExternalShareMenuAction( + // TRANSLATORS The placeholder refers to the software product name as in 'Add to your Nextcloud' + $this->l10n->t('Add to your %s', [$this->defaults->getProductName()]), + 'icon-external', + $ownerId, + $ownerName, + $shareNode->getName(), + ); + } + } - $ogPreview = $shareTmpl['previewURL']; + $shareUrl = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]); + // By default use the share link as the direct link + $directLink = $shareUrl; + // Add the direct link header actions + if ($shareNode->getMimePart() === 'image') { + // If this is a file and especially an image directly point to the image preview + $directLink = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $token]); + } elseif (($share->getPermissions() & Constants::PERMISSION_READ) && !$share->getHideDownload()) { + // Can read and no download restriction, so just download it + $directLink = $downloadUrl ?? $shareUrl; + } + $headerActions[] = new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', $directLink); + $response->setHeaderActions($headerActions); - //Whatapp is kind of picky about their size requirements + return $response; + } + + /** + * Add OpenGraph headers to response for preview + * @param IShare $share The share for which to add the headers + */ + protected function addMetaHeaders(IShare $share): void { + $shareNode = $share->getNode(); + $token = $share->getToken(); + $shareUrl = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]); + + // Handle preview generation for OpenGraph + $hasImagePreview = false; + if ($this->previewManager->isMimeSupported($shareNode->getMimetype())) { + // For images we can use direct links + if ($shareNode->getMimePart() === 'image') { + $hasImagePreview = true; + $ogPreview = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $token]); + // Whatsapp is kind of picky about their size requirements if ($this->request->isUserAgent(['/^WhatsApp/'])) { $ogPreview = $this->urlGenerator->linkToRouteAbsolute('files_sharing.PublicPreview.getPreview', [ 'token' => $token, @@ -200,92 +211,51 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider 'a' => true, ]); } + } else { + // For normal files use preview API + $ogPreview = $this->urlGenerator->linkToRouteAbsolute( + 'files_sharing.PublicPreview.getPreview', + [ + 'x' => 256, + 'y' => 256, + 'file' => $share->getTarget(), + 'token' => $token, + ], + ); } } else { - $shareTmpl['previewImage'] = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-fb.png')); - $ogPreview = $shareTmpl['previewImage']; + // No preview supported, so we just add the favicon + $ogPreview = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-fb.png')); } - // Load files we need - Util::addScript('files', 'semaphore'); - Util::addScript('files', 'file-upload'); - Util::addStyle('files_sharing', 'publicView'); - Util::addScript('files_sharing', 'public'); - Util::addScript('files_sharing', 'templates'); - Util::addScript('files', 'fileactions'); - Util::addScript('files', 'fileactionsmenu'); - Util::addScript('files', 'jquery.fileupload'); - Util::addScript('files_sharing', 'files_drop'); - - if (isset($shareTmpl['folder'])) { - // JS required for folders - Util::addStyle('files', 'merged'); - Util::addScript('files', 'filesummary'); - Util::addScript('files', 'templates'); - Util::addScript('files', 'breadcrumb'); - Util::addScript('files', 'fileinfomodel'); - Util::addScript('files', 'newfilemenu'); - Util::addScript('files', 'files'); - Util::addScript('files', 'filemultiselectmenu'); - Util::addScript('files', 'filelist'); - Util::addScript('files', 'keyboardshortcuts'); - Util::addScript('files', 'operationprogressbar'); - } + $title = $shareNode->getName(); + $siteName = $this->defaults->getName(); + $description = $siteName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''); - // Load Viewer scripts - if (class_exists(LoadViewer::class)) { - $this->eventDispatcher->dispatchTyped(new LoadViewer()); - } // OpenGraph Support: http://ogp.me/ - Util::addHeader('meta', ['property' => "og:title", 'content' => $shareTmpl['filename']]); - Util::addHeader('meta', ['property' => "og:description", 'content' => $this->defaults->getName() . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : '')]); - Util::addHeader('meta', ['property' => "og:site_name", 'content' => $this->defaults->getName()]); - Util::addHeader('meta', ['property' => "og:url", 'content' => $shareTmpl['shareUrl']]); - Util::addHeader('meta', ['property' => "og:type", 'content' => "object"]); - Util::addHeader('meta', ['property' => "og:image", 'content' => $ogPreview]); - - $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($share)); - - $csp = new ContentSecurityPolicy(); - $csp->addAllowedFrameDomain('\'self\''); - - $response = new PublicTemplateResponse(Application::APP_ID, 'public', $shareTmpl); - $response->setHeaderTitle($shareTmpl['filename']); - if ($shareTmpl['shareOwner'] !== '') { - $response->setHeaderDetails($this->l10n->t('shared by %s', [$shareTmpl['shareOwner']])); + Util::addHeader('meta', ['property' => 'og:title', 'content' => $title]); + Util::addHeader('meta', ['property' => 'og:description', 'content' => $description]); + Util::addHeader('meta', ['property' => 'og:site_name', 'content' => $siteName]); + Util::addHeader('meta', ['property' => 'og:url', 'content' => $shareUrl]); + Util::addHeader('meta', ['property' => 'og:type', 'content' => 'website']); + Util::addHeader('meta', ['property' => 'og:image', 'content' => $ogPreview]); // recommended to always have the image + if ($shareNode->getMimePart() === 'image') { + Util::addHeader('meta', ['property' => 'og:image:type', 'content' => $shareNode->getMimeType()]); + } elseif ($shareNode->getMimePart() === 'audio') { + $audio = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadshare', ['token' => $token]); + Util::addHeader('meta', ['property' => 'og:audio', 'content' => $audio]); + Util::addHeader('meta', ['property' => 'og:audio:type', 'content' => $shareNode->getMimeType()]); + } elseif ($shareNode->getMimePart() === 'video') { + $video = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadshare', ['token' => $token]); + Util::addHeader('meta', ['property' => 'og:video', 'content' => $video]); + Util::addHeader('meta', ['property' => 'og:video:type', 'content' => $shareNode->getMimeType()]); } - $isNoneFileDropFolder = $shareIsFolder === false || $share->getPermissions() !== Constants::PERMISSION_CREATE; - - if ($isNoneFileDropFolder && !$share->getHideDownload()) { - Util::addScript('files_sharing', 'public_note'); - - $downloadWhite = new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download-white', $shareTmpl['downloadURL'], 0); - $downloadAllWhite = new SimpleMenuAction('download', $this->l10n->t('Download all files'), 'icon-download-white', $shareTmpl['downloadURL'], 0); - $download = new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', $shareTmpl['downloadURL'], 10, $shareTmpl['fileSize']); - $downloadAll = new SimpleMenuAction('download', $this->l10n->t('Download all files'), 'icon-download', $shareTmpl['downloadURL'], 10, $shareTmpl['fileSize']); - $directLink = new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', $shareTmpl['previewURL']); - // TRANSLATORS The placeholder refers to the software product name as in 'Add to your Nextcloud' - $externalShare = new ExternalShareMenuAction($this->l10n->t('Add to your %s', [$this->defaults->getProductName()]), 'icon-external', $shareTmpl['owner'], $shareTmpl['shareOwner'], $shareTmpl['filename']); - - $responseComposer = []; - if ($shareIsFolder) { - $responseComposer[] = $downloadAllWhite; - $responseComposer[] = $downloadAll; - } else { - $responseComposer[] = $downloadWhite; - $responseComposer[] = $download; - } - $responseComposer[] = $directLink; - if ($this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { - $responseComposer[] = $externalShare; - } - - $response->setHeaderActions($responseComposer); - } - - $response->setContentSecurityPolicy($csp); - return $response; + // Twitter Support: https://developer.x.com/en/docs/x-for-websites/cards/overview/markup + Util::addHeader('meta', ['property' => 'twitter:title', 'content' => $title]); + Util::addHeader('meta', ['property' => 'twitter:description', 'content' => $description]); + Util::addHeader('meta', ['property' => 'twitter:card', 'content' => $hasImagePreview ? 'summary_large_image' : 'summary']); + Util::addHeader('meta', ['property' => 'twitter:image', 'content' => $ogPreview]); } } |