diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-06-04 01:25:28 +0200 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-09-06 03:38:42 +0200 |
commit | 96c827558611033ac35f6095b77ea04dca8044dd (patch) | |
tree | 9657a6d191dba82ae025c23838b2b35716c415cf /apps/files_sharing | |
parent | e4fa9967014e99a70fcf0f45e84b35f610cedeb6 (diff) | |
download | nextcloud-server-96c827558611033ac35f6095b77ea04dca8044dd.tar.gz nextcloud-server-96c827558611033ac35f6095b77ea04dca8044dd.zip |
feat(files_sharing): Migrate public share to use Vue files list
Co-authored-by: Ferdinand Thiessen <opensource@fthiessen.de>
Co-authored-by: Côme Chilliet <91878298+come-nc@users.noreply.github.com>
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/files_sharing')
-rw-r--r-- | apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php | 303 | ||||
-rw-r--r-- | apps/files_sharing/src/init-public.ts | 28 | ||||
-rw-r--r-- | apps/files_sharing/src/router/index.ts | 54 | ||||
-rw-r--r-- | apps/files_sharing/src/services/SharingService.spec.ts | 4 | ||||
-rw-r--r-- | apps/files_sharing/src/services/SharingService.ts | 13 | ||||
-rw-r--r-- | apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue | 67 | ||||
-rw-r--r-- | apps/files_sharing/src/views/publicFileDrop.ts | 59 | ||||
-rw-r--r-- | apps/files_sharing/src/views/publicFileShare.ts | 67 | ||||
-rw-r--r-- | apps/files_sharing/src/views/publicShare.ts | 28 | ||||
-rw-r--r-- | apps/files_sharing/tests/Controller/ShareControllerTest.php | 498 |
10 files changed, 591 insertions, 530 deletions
diff --git a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php index 8792c385a18..4feaac82dc0 100644 --- a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php +++ b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php @@ -20,11 +20,10 @@ 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; @@ -34,7 +33,6 @@ use OCP\IUser; use OCP\IUserManager; use OCP\Share\IPublicShareTemplateProvider; use OCP\Share\IShare; -use OCP\Template; use OCP\Util; class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider { @@ -51,6 +49,7 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider private IConfig $config, private IRequest $request, private IInitialState $initialState, + private IAppConfig $appConfig, ) { } @@ -60,113 +59,142 @@ 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(); - $this->initialState->provideInitialState('owner', $shareTmpl['owner']); - $this->initialState->provideInitialState('ownerDisplayName', $shareTmpl['shareOwner']); + $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); + if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) { + $ownerName = $owner->getDisplayName(); + $ownerId = $owner->getUID(); } } - // Provide initial state - $this->initialState->provideInitialState('label', $share->getLabel()); - $this->initialState->provideInitialState('note', $share->getNote()); + $view = 'public-share'; + if ($shareNode instanceof File) { + $view = 'public-file-share'; + } elseif (($share->getPermissions() & \OCP\Constants::PERMISSION_CREATE) + && !($share->getPermissions() & \OCP\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'), + ); + } + // Set up initial state + $this->initialState->provideInitialState('isPublic', true); + $this->initialState->provideInitialState('sharingToken', $token); $this->initialState->provideInitialState('filename', $shareNode->getName()); + $this->initialState->provideInitialState('view', $view); + + // Load scripts and styles for UI + \OCP\Util::addInitScript('files', 'init'); + \OCP\Util::addInitScript(Application::APP_ID, 'init'); + \OCP\Util::addInitScript(Application::APP_ID, 'init-public'); + \OCP\Util::addScript('files', 'main'); + \OCP\Util::addStyle('files', 'merged'); + + // Add file-request script if needed + $attributes = $share->getAttributes(); + $isFileRequest = $attributes?->getAttribute('fileRequest', 'enabled') === true; + if ($isFileRequest) { + Util::addScript(Application::APP_ID, 'public-file-request'); + } - $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()); - $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; - - if ($shareNode instanceof Folder) { - $shareIsFolder = true; - - $folderNode = $shareNode->get($path); - $shareTmpl['dir'] = $shareNode->getRelativePath($folderNode->getPath()); + // Load Viewer scripts + if (class_exists(LoadViewer::class)) { + $this->eventDispatcher->dispatchTyped(new LoadViewer()); + } - /* - * 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 - } + // Allow external apps to register their scripts + $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($share)); - $hideFileList = !($share->getPermissions() & Constants::PERMISSION_READ); - $maxUploadFilesize = $freeSpace; + // OpenGraph Support: http://ogp.me/ + $this->addOpenGraphHeaders($share); - $folder = new Template('files', 'list', ''); + // CSP to allow office + $csp = new ContentSecurityPolicy(); + $csp->addAllowedFrameDomain('\'self\''); - $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(); + $response = new PublicTemplateResponse( + 'files', + 'index', + ); + $response->setContentSecurityPolicy($csp); + // If the share has a label, use it as the title + if ($share->getLabel() !== '') { + $response->setHeaderTitle($share->getLabel()); } else { - $shareIsFolder = false; + $response->setHeaderTitle($shareNode->getName()); + } + if ($ownerName !== '') { + $response->setHeaderDetails($this->l10n->t('shared by %s', [$ownerName])); } - // default to list view - $shareTmpl['showgridview'] = false; - - $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') { + // The download URL is used for the "download" header action as well as in some cases for the direct link + $downloadUrl = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadShare', [ + 'token' => $token, + 'filename' => ($shareNode instanceof File) ? $shareNode->getName() : null, + ]); + + // 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()); + + // If remote sharing is enabled also add the remote share action to the menu + if ($this->federatedShareProvider->isOutgoingServer2serverShareEnabled() && !$share->getHideDownload()) { + $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(), + ); + } + } - 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']; + $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() & \OCP\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); - // We just have direct previews for image files - if ($shareNode->getMimePart() === 'image') { - $shareTmpl['previewURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $token]); + return $response; + } - $ogPreview = $shareTmpl['previewURL']; + /** + * Add OpenGraph headers to response for preview + * @param IShare $share The share for which to add the headers + */ + protected function addOpenGraphHeaders(IShare $share): void { + $shareNode = $share->getNode(); + $token = $share->getToken(); + $shareUrl = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]); - //Whatapp is kind of picky about their size requirements + // Handle preview generation for OpenGraph + if ($this->previewManager->isMimeSupported($shareNode->getMimetype())) { + // For images we can use direct links + if ($shareNode->getMimePart() === 'image') { + $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, @@ -175,93 +203,28 @@ 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'); - } - - // 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:title', 'content' => $shareNode->getName()]); 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:url', 'content' => $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']])); - } - - // 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()) { - Util::addScript('files_sharing', 'public_note'); - - $download = new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', $shareTmpl['downloadURL'], 0, $shareTmpl['fileSize']); - $downloadAll = new SimpleMenuAction('download', $this->l10n->t('Download all files'), 'icon-download', $shareTmpl['downloadURL'], 0, $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[] = $downloadAll; - } else { - $responseComposer[] = $download; - } - $responseComposer[] = $directLink; - if ($this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { - $responseComposer[] = $externalShare; - } - - $response->setHeaderActions($responseComposer); - } - - $response->setContentSecurityPolicy($csp); - return $response; } } diff --git a/apps/files_sharing/src/init-public.ts b/apps/files_sharing/src/init-public.ts new file mode 100644 index 00000000000..400ee73d2a1 --- /dev/null +++ b/apps/files_sharing/src/init-public.ts @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { getNavigation, registerDavProperty } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' +import registerFileDropView from './views/publicFileDrop.ts' +import registerPublicShareView from './views/publicShare.ts' +import registerPublicFileShareView from './views/publicFileShare.ts' +import RouterService from '../../files/src/services/RouterService' +import router from './router' + +registerFileDropView() +registerPublicShareView() +registerPublicFileShareView() + +registerDavProperty('nc:share-attributes', { nc: 'http://nextcloud.org/ns' }) +registerDavProperty('oc:share-types', { oc: 'http://owncloud.org/ns' }) +registerDavProperty('ocs:share-permissions', { ocs: 'http://open-collaboration-services.org/ns' }) + +// Get the current view from state and set it active +const view = loadState<string>('files_sharing', 'view') +const navigation = getNavigation() +navigation.setActive(navigation.views.find(({ id }) => id === view) ?? null) + +// Force our own router +window.OCP.Files = window.OCP.Files ?? {} +window.OCP.Files.Router = new RouterService(router) diff --git a/apps/files_sharing/src/router/index.ts b/apps/files_sharing/src/router/index.ts new file mode 100644 index 00000000000..6a417975e32 --- /dev/null +++ b/apps/files_sharing/src/router/index.ts @@ -0,0 +1,54 @@ +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { RawLocation, Route } from 'vue-router' +import type { ErrorHandler } from 'vue-router/types/router.d.ts' + +import { loadState } from '@nextcloud/initial-state' +import { generateUrl } from '@nextcloud/router' +import queryString from 'query-string' +import Router from 'vue-router' +import Vue from 'vue' + +const view = loadState<string>('files_sharing', 'view') +const sharingToken = loadState<string>('files_sharing', 'sharingToken') + +Vue.use(Router) + +// Prevent router from throwing errors when we're already on the page we're trying to go to +const originalPush = Router.prototype.push as (to, onComplete?, onAbort?) => Promise<Route> +Router.prototype.push = function push(to: RawLocation, onComplete?: ((route: Route) => void) | undefined, onAbort?: ErrorHandler | undefined): Promise<Route> { + if (onComplete || onAbort) return originalPush.call(this, to, onComplete, onAbort) + return originalPush.call(this, to).catch(err => err) +} + +const router = new Router({ + mode: 'history', + + // if index.php is in the url AND we got this far, then it's working: + // let's keep using index.php in the url + base: generateUrl('/s'), + linkActiveClass: 'active', + + routes: [ + { + path: '/', + // Pretending we're using the default view + redirect: { name: 'filelist', params: { view, token: sharingToken } }, + }, + { + path: '/:token', + name: 'filelist', + props: true, + }, + ], + + // Custom stringifyQuery to prevent encoding of slashes in the url + stringifyQuery(query) { + const result = queryString.stringify(query).replace(/%2F/gmi, '/') + return result ? ('?' + result) : '' + }, +}) + +export default router diff --git a/apps/files_sharing/src/services/SharingService.spec.ts b/apps/files_sharing/src/services/SharingService.spec.ts index ab0a5163618..daba81bd4f2 100644 --- a/apps/files_sharing/src/services/SharingService.spec.ts +++ b/apps/files_sharing/src/services/SharingService.spec.ts @@ -18,14 +18,12 @@ const axios = vi.hoisted(() => ({ get: vi.fn() })) vi.mock('@nextcloud/auth') vi.mock('@nextcloud/axios', () => ({ default: axios })) -// Mock web root variable +// Mock TAG beforeAll(() => { window.OC = { ...window.OC, TAG_FAVORITE, } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(window as any)._oc_webroot = '' }) describe('SharingService methods definitions', () => { diff --git a/apps/files_sharing/src/services/SharingService.ts b/apps/files_sharing/src/services/SharingService.ts index e168f202fba..2f8144e216e 100644 --- a/apps/files_sharing/src/services/SharingService.ts +++ b/apps/files_sharing/src/services/SharingService.ts @@ -6,18 +6,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { AxiosPromise } from '@nextcloud/axios' +import type { ContentsWithRoot } from '@nextcloud/files' import type { OCSResponse } from '@nextcloud/typings/ocs' import type { ShareAttribute } from '../sharing' -import { Folder, File, type ContentsWithRoot, Permission } from '@nextcloud/files' -import { generateOcsUrl, generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' +import { Folder, File, Permission, davRemoteURL, davRootPath } from '@nextcloud/files' +import { generateOcsUrl } from '@nextcloud/router' import axios from '@nextcloud/axios' import logger from './logger' -export const rootPath = `/files/${getCurrentUser()?.uid}` - const headers = { 'Content-Type': 'application/json', } @@ -57,7 +56,7 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu // Generate path and strip double slashes const path = ocsEntry.path || ocsEntry.file_target || ocsEntry.name - const source = generateRemoteUrl(`dav/${rootPath}/${path}`.replaceAll(/\/\//gm, '/')) + const source = `${davRemoteURL}${davRootPath}/${path.replace(/^\/+/, '')}` let mtime = ocsEntry.item_mtime ? new Date((ocsEntry.item_mtime) * 1000) : undefined // Prefer share time if more recent than item mtime @@ -73,7 +72,7 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu mtime, size: ocsEntry?.item_size, permissions: ocsEntry?.item_permissions || ocsEntry?.permissions, - root: rootPath, + root: davRootPath, attributes: { ...ocsEntry, 'has-preview': hasPreview, @@ -217,7 +216,7 @@ export const getContents = async (sharedWithYou = true, sharedWithOthers = true, return { folder: new Folder({ id: 0, - source: generateRemoteUrl('dav' + rootPath), + source: `${davRemoteURL}${davRootPath}`, owner: getCurrentUser()?.uid || null, }), contents, diff --git a/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue new file mode 100644 index 00000000000..538927623ed --- /dev/null +++ b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue @@ -0,0 +1,67 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcEmptyContent class="file-drop-empty-content" + data-cy-files-sharing-file-drop + :name="t('files_sharing', 'File drop')"> + <template #icon> + <NcIconSvgWrapper :svg="svgCloudUpload" /> + </template> + <template #description> + {{ t('files_sharing', 'Upload files to {foldername}.', { foldername }) }} + {{ disclaimer === '' ? '' : t('files_sharing', 'By uploading files, you agree to the terms of service.') }} + </template> + <template #action> + <template v-if="disclaimer"> + <!-- Terms of service if enabled --> + <NcButton type="primary" @click="showDialog = true"> + {{ t('files_sharing', 'View terms of service') }} + </NcButton> + <NcDialog close-on-click-outside + content-classes="terms-of-service-dialog" + :open.sync="showDialog" + :name="t('files_sharing', 'Terms of service')" + :message="disclaimer" /> + </template> + <UploadPicker allow-folders + :content="() => []" + no-menu + :destination="uploadDestination" + multiple /> + </template> + </NcEmptyContent> +</template> + +<script setup lang="ts"> +import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' +import { getUploader, UploadPicker } from '@nextcloud/upload' +import { ref } from 'vue' + +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' +import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' +import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' +import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw' + +defineProps<{ + foldername: string +}>() + +const disclaimer = loadState<string>('files_sharing', 'disclaimer', '') +const showDialog = ref(false) +const uploadDestination = getUploader().destination +</script> + +<style scoped> +:deep(.terms-of-service-dialog) { + min-height: min(100px, 20vh); +} +/* TODO fix in library */ +.file-drop-empty-content :deep(.empty-content__action) { + display: flex; + gap: var(--default-grid-baseline); +} +</style> diff --git a/apps/files_sharing/src/views/publicFileDrop.ts b/apps/files_sharing/src/views/publicFileDrop.ts new file mode 100644 index 00000000000..0d782d48fc7 --- /dev/null +++ b/apps/files_sharing/src/views/publicFileDrop.ts @@ -0,0 +1,59 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { VueConstructor } from 'vue' + +import { Folder, Permission, View, davRemoteURL, davRootPath, getNavigation } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' +import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw' +import Vue from 'vue' + +export default () => { + const foldername = loadState<string>('files_sharing', 'filename') + + let FilesViewFileDropEmptyContent: VueConstructor + let fileDropEmptyContentInstance: Vue + + const view = new View({ + id: 'public-file-drop', + name: t('files_sharing', 'File drop'), + caption: t('files_sharing', 'Upload files to {foldername}', { foldername }), + icon: svgCloudUpload, + order: 1, + + emptyView: async (div: HTMLDivElement) => { + if (FilesViewFileDropEmptyContent === undefined) { + const { default: component } = await import('../views/FilesViewFileDropEmptyContent.vue') + FilesViewFileDropEmptyContent = Vue.extend(component) + } + if (fileDropEmptyContentInstance) { + fileDropEmptyContentInstance.$destroy() + } + fileDropEmptyContentInstance = new FilesViewFileDropEmptyContent({ + propsData: { + foldername, + }, + }) + fileDropEmptyContentInstance.$mount(div) + }, + + getContents: async () => { + return { + contents: [], + // Fake a writeonly folder as root + folder: new Folder({ + id: 0, + source: `${davRemoteURL}${davRootPath}`, + root: davRootPath, + owner: null, + permissions: Permission.CREATE, + }), + } + }, + }) + + const Navigation = getNavigation() + Navigation.register(view) +} diff --git a/apps/files_sharing/src/views/publicFileShare.ts b/apps/files_sharing/src/views/publicFileShare.ts new file mode 100644 index 00000000000..b2b9de9ea5f --- /dev/null +++ b/apps/files_sharing/src/views/publicFileShare.ts @@ -0,0 +1,67 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { FileStat, ResponseDataDetailed } from 'webdav' +import { Folder, Permission, View, davGetDefaultPropfind, davRemoteURL, davRootPath, getNavigation } from '@nextcloud/files' +import { translate as t } from '@nextcloud/l10n' +import { CancelablePromise } from 'cancelable-promise' +import LinkSvg from '@mdi/svg/svg/link.svg?raw' + +import { resultToNode } from '../../../files/src/services/Files' +import { client } from '../../../files/src/services/WebdavClient' +import logger from '../services/logger' + +export default () => { + const view = new View({ + id: 'public-file-share', + name: t('files_sharing', 'Public file share'), + caption: t('files_sharing', 'Public shared file.'), + + emptyTitle: t('files_sharing', 'No file'), + emptyCaption: t('files_sharing', 'The file shared with you will show up here'), + + icon: LinkSvg, + order: 1, + + getContents: () => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + const abort = new AbortController() + onCancel(() => abort.abort()) + try { + const node = await client.stat( + davRootPath, + { + data: davGetDefaultPropfind(), + details: true, + signal: abort.signal, + }, + ) as ResponseDataDetailed<FileStat> + + resolve({ + // We only have one file as the content + contents: [resultToNode(node.data)], + // Fake a readonly folder as root + folder: new Folder({ + id: 0, + source: `${davRemoteURL}${davRootPath}`, + root: davRootPath, + owner: null, + permissions: Permission.READ, + attributes: { + // Ensure the share note is set on the root + note: node.data.props?.note, + }, + }), + }) + } catch (e) { + logger.error(e as Error) + reject(e as Error) + } + }) + }, + }) + + const Navigation = getNavigation() + Navigation.register(view) +} diff --git a/apps/files_sharing/src/views/publicShare.ts b/apps/files_sharing/src/views/publicShare.ts new file mode 100644 index 00000000000..118973f54f5 --- /dev/null +++ b/apps/files_sharing/src/views/publicShare.ts @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { translate as t } from '@nextcloud/l10n' +import { View, getNavigation } from '@nextcloud/files' +import LinkSvg from '@mdi/svg/svg/link.svg?raw' + +import { getContents } from '../../../files/src/services/Files' + +export default () => { + const view = new View({ + id: 'public-share', + name: t('files_sharing', 'Public share'), + caption: t('files_sharing', 'Public shared files.'), + + emptyTitle: t('files_sharing', 'No files'), + emptyCaption: t('files_sharing', 'Files and folders shared with you will show up here'), + + icon: LinkSvg, + order: 1, + + getContents, + }) + + const Navigation = getNavigation() + Navigation.register(view) +} diff --git a/apps/files_sharing/tests/Controller/ShareControllerTest.php b/apps/files_sharing/tests/Controller/ShareControllerTest.php index 6ce92b6fd43..09b02be5f66 100644 --- a/apps/files_sharing/tests/Controller/ShareControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareControllerTest.php @@ -29,7 +29,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; -use OCP\Files\Storage; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; use OCP\IPreview; @@ -50,41 +50,27 @@ use PHPUnit\Framework\MockObject\MockObject; * @package OCA\Files_Sharing\Controllers */ class ShareControllerTest extends \Test\TestCase { - /** @var string */ - private $user; - /** @var string */ - private $oldUser; - - /** @var string */ - private $appName = 'files_sharing'; - /** @var ShareController */ - private $shareController; - /** @var IURLGenerator|MockObject */ - private $urlGenerator; - /** @var ISession|MockObject */ - private $session; - /** @var \OCP\IPreview|MockObject */ - private $previewManager; - /** @var \OCP\IConfig|MockObject */ - private $config; - /** @var \OC\Share20\Manager|MockObject */ - private $shareManager; - /** @var IUserManager|MockObject */ - private $userManager; - /** @var FederatedShareProvider|MockObject */ - private $federatedShareProvider; - /** @var IAccountManager|MockObject */ - private $accountManager; - /** @var IEventDispatcher|MockObject */ - private $eventDispatcher; - /** @var IL10N */ - private $l10n; - /** @var ISecureRandom */ - private $secureRandom; - /** @var Defaults|MockObject */ - private $defaults; - /** @var IPublicShareTemplateFactory|MockObject */ - private $publicShareTemplateFactory; + + private string $user; + private string $oldUser; + private string $appName = 'files_sharing'; + private ShareController $shareController; + + private IL10N&MockObject $l10n; + private IConfig&MockObject $config; + private ISession&MockObject $session; + private Defaults&MockObject $defaults; + private IAppConfig&MockObject $appConfig; + private Manager&MockObject $shareManager; + private IPreview&MockObject $previewManager; + private IUserManager&MockObject $userManager; + private IInitialState&MockObject $initialState; + private IURLGenerator&MockObject $urlGenerator; + private ISecureRandom&MockObject $secureRandom; + private IAccountManager&MockObject $accountManager; + private IEventDispatcher&MockObject $eventDispatcher; + private FederatedShareProvider&MockObject $federatedShareProvider; + private IPublicShareTemplateFactory&MockObject $publicShareTemplateFactory; protected function setUp(): void { parent::setUp(); @@ -95,7 +81,9 @@ class ShareControllerTest extends \Test\TestCase { $this->session = $this->createMock(ISession::class); $this->previewManager = $this->createMock(IPreview::class); $this->config = $this->createMock(IConfig::class); + $this->appConfig = $this->createMock(IAppConfig::class); $this->userManager = $this->createMock(IUserManager::class); + $this->initialState = $this->createMock(IInitialState::class); $this->federatedShareProvider = $this->createMock(FederatedShareProvider::class); $this->federatedShareProvider->expects($this->any()) ->method('isOutgoingServer2serverShareEnabled')->willReturn(true); @@ -122,7 +110,8 @@ class ShareControllerTest extends \Test\TestCase { $this->defaults, $this->config, $this->createMock(IRequest::class), - $this->createMock(IInitialState::class) + $this->initialState, + $this->appConfig, ) ); @@ -246,29 +235,32 @@ class ShareControllerTest extends \Test\TestCase { ->with($owner) ->willReturn($account); - $share = \OC::$server->getShareManager()->newShare(); - $share->setId(42); - $share->setPassword('password') + /** @var Manager */ + $manager = \OCP\Server::get(Manager::class); + $share = $manager->newShare(); + $share->setId(42) + ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE) + ->setPassword('password') ->setShareOwner('ownerUID') ->setSharedBy('initiatorUID') ->setNode($file) ->setNote($note) - ->setTarget("/$filename"); + ->setTarget("/$filename") + ->setToken('token'); $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); $this->session->method('get')->with('public_link_authenticated')->willReturn('42'); - $this->urlGenerator->expects($this->exactly(3)) + $this->urlGenerator->expects(self::atLeast(2)) ->method('linkToRouteAbsolute') - ->withConsecutive( - ['files_sharing.sharecontroller.downloadShare', ['token' => 'token', 'filename' => $filename]], - ['files_sharing.sharecontroller.showShare', ['token' => 'token']], - ['files_sharing.PublicPreview.getPreview', ['token' => 'token', 'x' => 200, 'y' => 200, 'file' => '/'.$filename]], - )->willReturnOnConsecutiveCalls( - 'downloadURL', - 'shareUrl', - 'previewImage', - ); + ->willReturnMap([ + // every file has the show show share url in the opengraph url prop + ['files_sharing.sharecontroller.showShare', ['token' => 'token'], 'shareUrl'], + // this share is not an image to the default preview is used + ['files_sharing.PublicPreview.getPreview', ['x' => 256, 'y' => 256, 'file' => $share->getTarget(), 'token' => 'token'], 'previewUrl'], + // for the direct link + ['files_sharing.sharecontroller.downloadShare', ['token' => 'token', 'filename' => $filename ], 'downloadUrl'], + ]); $this->previewManager->method('isMimeSupported')->with('text/plain')->willReturn(true); @@ -281,19 +273,12 @@ class ShareControllerTest extends \Test\TestCase { ['preview_max_y', 1024, 1024], ] ); - $shareTmpl['maxSizeAnimateGif'] = $this->config->getSystemValue('max_filesize_animated_gifs_public_sharing', 10); - $shareTmpl['previewEnabled'] = $this->config->getSystemValue('enable_previews', true); $this->shareManager ->expects($this->once()) ->method('getShareByToken') ->with('token') ->willReturn($share); - $this->config - ->expects($this->once()) - ->method('getAppValue') - ->with('core', 'shareapi_public_link_disclaimertext', null) - ->willReturn('My disclaimer text'); $this->userManager->method('get')->willReturnCallback(function (string $uid) use ($owner, $initiator) { if ($uid === 'ownerUID') { @@ -325,55 +310,45 @@ class ShareControllerTest extends \Test\TestCase { ->method('getProductName') ->willReturn('Nextcloud'); - $response = $this->shareController->showShare(); - $sharedTmplParams = [ - 'owner' => 'ownerUID', - 'filename' => $filename, - 'directory_path' => "/$filename", - 'mimetype' => 'text/plain', - 'dirToken' => 'token', + // Ensure the correct initial state is setup + // Shared node is a file so this is a single file share: + $view = 'public-file-share'; + // Set up initial state + $initialState = []; + $this->initialState->expects(self::any()) + ->method('provideInitialState') + ->willReturnCallback(function ($key, $value) use (&$initialState) { + $initialState[$key] = $value; + }); + $expectedInitialState = [ + 'isPublic' => true, 'sharingToken' => 'token', - 'server2serversharing' => true, - 'protected' => 'true', - 'dir' => '', - 'downloadURL' => 'downloadURL', - 'fileSize' => '33 B', - 'nonHumanFileSize' => 33, - 'maxSizeAnimateGif' => 10, - 'previewSupported' => true, - 'previewEnabled' => true, - 'previewMaxX' => 1024, - 'previewMaxY' => 1024, - 'hideFileList' => false, - 'shareOwner' => 'ownerDisplay', - 'disclaimer' => 'My disclaimer text', - 'shareUrl' => 'shareUrl', - 'previewImage' => 'previewImage', - 'previewURL' => 'downloadURL', - 'note' => $note, - 'hideDownload' => false, - 'showgridview' => false, - 'label' => '' + 'sharePermissions' => (Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE), + 'filename' => $filename, + 'view' => $view, ]; + $response = $this->shareController->showShare(); + + $this->assertEquals($expectedInitialState, $initialState); + $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); $csp->addAllowedFrameDomain('\'self\''); - $expectedResponse = new PublicTemplateResponse($this->appName, 'public', $sharedTmplParams); + $expectedResponse = new PublicTemplateResponse('files', 'index'); $expectedResponse->setContentSecurityPolicy($csp); - $expectedResponse->setHeaderTitle($sharedTmplParams['filename']); - $expectedResponse->setHeaderDetails('shared by ' . $sharedTmplParams['shareOwner']); + $expectedResponse->setHeaderTitle($filename); + $expectedResponse->setHeaderDetails('shared by ownerDisplay'); $expectedResponse->setHeaderActions([ - new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', $sharedTmplParams['downloadURL'], 0, $sharedTmplParams['fileSize']), - new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', $sharedTmplParams['previewURL']), - new ExternalShareMenuAction($this->l10n->t('Add to your Nextcloud'), 'icon-external', $sharedTmplParams['owner'], $sharedTmplParams['shareOwner'], $sharedTmplParams['filename']), + new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', 'downloadUrl', 0, '33'), + new ExternalShareMenuAction($this->l10n->t('Add to your Nextcloud'), 'icon-external', 'owner', 'ownerDisplay', $filename), + new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', 'downloadUrl'), ]); $this->assertEquals($expectedResponse, $response); } - public function testShowShareWithPrivateName() { - $note = 'personal note'; - $filename = 'file1.txt'; + public function testShowFileDropShare() { + $filename = 'folder1'; $this->shareController->setToken('token'); @@ -387,17 +362,15 @@ class ShareControllerTest extends \Test\TestCase { $initiator->method('getUID')->willReturn('initiatorUID'); $initiator->method('isEnabled')->willReturn(true); - $file = $this->createMock(File::class); - $file->method('getName')->willReturn($filename); - $file->method('getMimetype')->willReturn('text/plain'); - $file->method('getSize')->willReturn(33); + $file = $this->createMock(Folder::class); $file->method('isReadable')->willReturn(true); $file->method('isShareable')->willReturn(true); - $file->method('getId')->willReturn(111); + $file->method('getId')->willReturn(1234); + $file->method('getName')->willReturn($filename); $accountName = $this->createMock(IAccountProperty::class); $accountName->method('getScope') - ->willReturn(IAccountManager::SCOPE_LOCAL); + ->willReturn(IAccountManager::SCOPE_PUBLISHED); $account = $this->createMock(IAccount::class); $account->method('getProperty') ->with(IAccountManager::PROPERTY_DISPLAYNAME) @@ -407,31 +380,34 @@ class ShareControllerTest extends \Test\TestCase { ->with($owner) ->willReturn($account); - $share = \OC::$server->getShareManager()->newShare(); - $share->setId(42); - $share->setPassword('password') + /** @var Manager */ + $manager = \OCP\Server::get(Manager::class); + $share = $manager->newShare(); + $share->setId(42) + ->setPermissions(Constants::PERMISSION_CREATE) + ->setPassword('password') ->setShareOwner('ownerUID') ->setSharedBy('initiatorUID') ->setNode($file) - ->setNote($note) - ->setTarget("/$filename"); + ->setTarget("/$filename") + ->setToken('token'); + + $this->appConfig + ->expects($this->once()) + ->method('getValueString') + ->with('core', 'shareapi_public_link_disclaimertext', '') + ->willReturn('My disclaimer text'); $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); $this->session->method('get')->with('public_link_authenticated')->willReturn('42'); - $this->urlGenerator->expects($this->exactly(3)) + $this->urlGenerator->expects(self::atLeastOnce()) ->method('linkToRouteAbsolute') - ->withConsecutive( - ['files_sharing.sharecontroller.downloadShare', ['token' => 'token', 'filename' => $filename]], - ['files_sharing.sharecontroller.showShare', ['token' => 'token']], - ['files_sharing.PublicPreview.getPreview', ['token' => 'token', 'x' => 200, 'y' => 200, 'file' => '/'.$filename]], - )->willReturnOnConsecutiveCalls( - 'downloadURL', - 'shareUrl', - 'previewImage', - ); - - $this->previewManager->method('isMimeSupported')->with('text/plain')->willReturn(true); + ->willReturnMap([ + // every file has the show show share url in the opengraph url prop + ['files_sharing.sharecontroller.showShare', ['token' => 'token'], 'shareUrl'], + // there is no preview or folders so no other link for opengraph + ]); $this->config->method('getSystemValue') ->willReturnMap( @@ -442,19 +418,12 @@ class ShareControllerTest extends \Test\TestCase { ['preview_max_y', 1024, 1024], ] ); - $shareTmpl['maxSizeAnimateGif'] = $this->config->getSystemValue('max_filesize_animated_gifs_public_sharing', 10); - $shareTmpl['previewEnabled'] = $this->config->getSystemValue('enable_previews', true); $this->shareManager ->expects($this->once()) ->method('getShareByToken') ->with('token') ->willReturn($share); - $this->config - ->expects($this->once()) - ->method('getAppValue') - ->with('core', 'shareapi_public_link_disclaimertext', null) - ->willReturn('My disclaimer text'); $this->userManager->method('get')->willReturnCallback(function (string $uid) use ($owner, $initiator) { if ($uid === 'ownerUID') { @@ -478,67 +447,50 @@ class ShareControllerTest extends \Test\TestCase { $this->l10n->expects($this->any()) ->method('t') - ->will($this->returnCallback(function ($text, $parameters) { + ->willReturnCallback(function ($text, $parameters) { return vsprintf($text, $parameters); - })); - - $this->defaults->expects(self::any()) - ->method('getProductName') - ->willReturn('Nextcloud'); + }); - $response = $this->shareController->showShare(); - $sharedTmplParams = [ - 'owner' => '', - 'filename' => $filename, - 'directory_path' => "/$filename", - 'mimetype' => 'text/plain', - 'dirToken' => 'token', + // Set up initial state + $initialState = []; + $this->initialState->expects(self::any()) + ->method('provideInitialState') + ->willReturnCallback(function ($key, $value) use (&$initialState) { + $initialState[$key] = $value; + }); + $expectedInitialState = [ + 'isPublic' => true, 'sharingToken' => 'token', - 'server2serversharing' => true, - 'protected' => 'true', - 'dir' => '', - 'downloadURL' => 'downloadURL', - 'fileSize' => '33 B', - 'nonHumanFileSize' => 33, - 'maxSizeAnimateGif' => 10, - 'previewSupported' => true, - 'previewEnabled' => true, - 'previewMaxX' => 1024, - 'previewMaxY' => 1024, - 'hideFileList' => false, - 'shareOwner' => '', + 'sharePermissions' => Constants::PERMISSION_CREATE, + 'filename' => $filename, + 'view' => 'public-file-drop', 'disclaimer' => 'My disclaimer text', - 'shareUrl' => 'shareUrl', - 'previewImage' => 'previewImage', - 'previewURL' => 'downloadURL', - 'note' => $note, - 'hideDownload' => false, - 'showgridview' => false, - 'label' => '' ]; + $response = $this->shareController->showShare(); + + $this->assertEquals($expectedInitialState, $initialState); + $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); $csp->addAllowedFrameDomain('\'self\''); - $expectedResponse = new PublicTemplateResponse($this->appName, 'public', $sharedTmplParams); + $expectedResponse = new PublicTemplateResponse('files', 'index'); $expectedResponse->setContentSecurityPolicy($csp); - $expectedResponse->setHeaderTitle($sharedTmplParams['filename']); - $expectedResponse->setHeaderDetails(''); + $expectedResponse->setHeaderTitle($filename); + $expectedResponse->setHeaderDetails('shared by ownerDisplay'); $expectedResponse->setHeaderActions([ - new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', $sharedTmplParams['downloadURL'], 0, $sharedTmplParams['fileSize']), - new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', $sharedTmplParams['previewURL']), - new ExternalShareMenuAction($this->l10n->t('Add to your Nextcloud'), 'icon-external', $sharedTmplParams['owner'], $sharedTmplParams['shareOwner'], $sharedTmplParams['filename']), + new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', 'shareUrl'), ]); $this->assertEquals($expectedResponse, $response); } - public function testShowShareHideDownload() { + public function testShowShareWithPrivateName() { $note = 'personal note'; $filename = 'file1.txt'; $this->shareController->setToken('token'); - $owner = $this->getMockBuilder(IUser::class)->getMock(); + $owner = $this->createMock(IUser::class); $owner->method('getDisplayName')->willReturn('ownerDisplay'); $owner->method('getUID')->willReturn('ownerUID'); $owner->method('isEnabled')->willReturn(true); @@ -548,7 +500,7 @@ class ShareControllerTest extends \Test\TestCase { $initiator->method('getUID')->willReturn('initiatorUID'); $initiator->method('isEnabled')->willReturn(true); - $file = $this->getMockBuilder('OCP\Files\File')->getMock(); + $file = $this->createMock(File::class); $file->method('getName')->willReturn($filename); $file->method('getMimetype')->willReturn('text/plain'); $file->method('getSize')->willReturn(33); @@ -558,7 +510,7 @@ class ShareControllerTest extends \Test\TestCase { $accountName = $this->createMock(IAccountProperty::class); $accountName->method('getScope') - ->willReturn(IAccountManager::SCOPE_PUBLISHED); + ->willReturn(IAccountManager::SCOPE_LOCAL); $account = $this->createMock(IAccount::class); $account->method('getProperty') ->with(IAccountManager::PROPERTY_DISPLAYNAME) @@ -568,33 +520,31 @@ class ShareControllerTest extends \Test\TestCase { ->with($owner) ->willReturn($account); - $share = \OC::$server->getShareManager()->newShare(); + /** @var IShare */ + $share = \OCP\Server::get(Manager::class)->newShare(); $share->setId(42); $share->setPassword('password') ->setShareOwner('ownerUID') ->setSharedBy('initiatorUID') ->setNode($file) ->setNote($note) - ->setTarget("/$filename") - ->setHideDownload(true); + ->setToken('token') + ->setPermissions(\OCP\Constants::PERMISSION_ALL & ~\OCP\Constants::PERMISSION_SHARE) + ->setTarget("/$filename"); $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); $this->session->method('get')->with('public_link_authenticated')->willReturn('42'); - // Even if downloads are disabled the "downloadURL" parameter is - // provided to the template, as it is needed to preview audio and GIF - // files. - $this->urlGenerator->expects($this->exactly(3)) + $this->urlGenerator->expects(self::atLeast(2)) ->method('linkToRouteAbsolute') - ->withConsecutive( - ['files_sharing.sharecontroller.downloadShare', ['token' => 'token', 'filename' => $filename]], - ['files_sharing.sharecontroller.showShare', ['token' => 'token']], - ['files_sharing.PublicPreview.getPreview', ['token' => 'token', 'x' => 200, 'y' => 200, 'file' => '/'.$filename]], - )->willReturnOnConsecutiveCalls( - 'downloadURL', - 'shareUrl', - 'previewImage', - ); + ->willReturnMap([ + // every file has the show show share url in the opengraph url prop + ['files_sharing.sharecontroller.showShare', ['token' => 'token'], 'shareUrl'], + // this share is not an image to the default preview is used + ['files_sharing.PublicPreview.getPreview', ['x' => 256, 'y' => 256, 'file' => $share->getTarget(), 'token' => 'token'], 'previewUrl'], + // for the direct link + ['files_sharing.sharecontroller.downloadShare', ['token' => 'token', 'filename' => $filename ], 'downloadUrl'], + ]); $this->previewManager->method('isMimeSupported')->with('text/plain')->willReturn(true); @@ -615,11 +565,6 @@ class ShareControllerTest extends \Test\TestCase { ->method('getShareByToken') ->with('token') ->willReturn($share); - $this->config - ->expects($this->once()) - ->method('getAppValue') - ->with('core', 'shareapi_public_link_disclaimertext', null) - ->willReturn('My disclaimer text'); $this->userManager->method('get')->willReturnCallback(function (string $uid) use ($owner, $initiator) { if ($uid === 'ownerUID') { @@ -643,176 +588,29 @@ class ShareControllerTest extends \Test\TestCase { $this->l10n->expects($this->any()) ->method('t') - ->willReturnCallback(function ($text, $parameters) { + ->will($this->returnCallback(function ($text, $parameters) { return vsprintf($text, $parameters); - }); - - $response = $this->shareController->showShare(); - $sharedTmplParams = [ - 'owner' => 'ownerUID', - 'filename' => $filename, - 'directory_path' => "/$filename", - 'mimetype' => 'text/plain', - 'dirToken' => 'token', - 'sharingToken' => 'token', - 'server2serversharing' => true, - 'protected' => 'true', - 'dir' => '', - 'downloadURL' => 'downloadURL', - 'fileSize' => '33 B', - 'nonHumanFileSize' => 33, - 'maxSizeAnimateGif' => 10, - 'previewSupported' => true, - 'previewEnabled' => true, - 'previewMaxX' => 1024, - 'previewMaxY' => 1024, - 'hideFileList' => false, - 'shareOwner' => 'ownerDisplay', - 'disclaimer' => 'My disclaimer text', - 'shareUrl' => 'shareUrl', - 'previewImage' => 'previewImage', - 'previewURL' => 'downloadURL', - 'note' => $note, - 'hideDownload' => true, - 'showgridview' => false, - 'label' => '' - ]; - - $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); - $csp->addAllowedFrameDomain('\'self\''); - $expectedResponse = new PublicTemplateResponse($this->appName, 'public', $sharedTmplParams); - $expectedResponse->setContentSecurityPolicy($csp); - $expectedResponse->setHeaderTitle($sharedTmplParams['filename']); - $expectedResponse->setHeaderDetails('shared by ' . $sharedTmplParams['shareOwner']); - $expectedResponse->setHeaderActions([]); - - $this->assertEquals($expectedResponse, $response); - } - - /** - * Checks file drop shares: - * - there must not be any header action - * - the template param "hideFileList" should be true - * - * @test - * @return void - */ - public function testShareFileDrop() { - $this->shareController->setToken('token'); - - $owner = $this->getMockBuilder(IUser::class)->getMock(); - $owner->method('getDisplayName')->willReturn('ownerDisplay'); - $owner->method('getUID')->willReturn('ownerUID'); - $owner->method('isEnabled')->willReturn(true); - - $initiator = $this->createMock(IUser::class); - $initiator->method('getDisplayName')->willReturn('initiatorDisplay'); - $initiator->method('getUID')->willReturn('initiatorUID'); - $initiator->method('isEnabled')->willReturn(true); - - /* @var MockObject|Storage $storage */ - $storage = $this->getMockBuilder(Storage::class) - ->disableOriginalConstructor() - ->getMock(); - - /* @var MockObject|Folder $folder */ - $folder = $this->getMockBuilder(Folder::class) - ->disableOriginalConstructor() - ->getMock(); - $folder->method('getName')->willReturn('/fileDrop'); - $folder->method('isReadable')->willReturn(true); - $folder->method('isShareable')->willReturn(true); - $folder->method('getStorage')->willReturn($storage); - $folder->method('get')->with('')->willReturn($folder); - $folder->method('getSize')->willReturn(1337); - $folder->method('getId')->willReturn(111); - - $accountName = $this->createMock(IAccountProperty::class); - $accountName->method('getScope') - ->willReturn(IAccountManager::SCOPE_PUBLISHED); - $account = $this->createMock(IAccount::class); - $account->method('getProperty') - ->with(IAccountManager::PROPERTY_DISPLAYNAME) - ->willReturn($accountName); - $this->accountManager->expects($this->once()) - ->method('getAccount') - ->with($owner) - ->willReturn($account); - - $share = \OC::$server->getShareManager()->newShare(); - $share->setId(42); - $share->setPermissions(Constants::PERMISSION_CREATE) - ->setShareOwner('ownerUID') - ->setSharedBy('initiatorUID') - ->setNode($folder) - ->setTarget('/fileDrop'); - - $this->shareManager - ->expects($this->once()) - ->method('getShareByToken') - ->with('token') - ->willReturn($share); - - $this->userManager->method('get')->willReturnCallback(function (string $uid) use ($owner, $initiator) { - if ($uid === 'ownerUID') { - return $owner; - } - if ($uid === 'initiatorUID') { - return $initiator; - } - return null; - }); + })); - $this->l10n->expects($this->any()) - ->method('t') - ->willReturnCallback(function ($text, $parameters) { - return vsprintf($text, $parameters); - }); + $this->defaults->expects(self::any()) + ->method('getProductName') + ->willReturn('Nextcloud'); $response = $this->shareController->showShare(); - // skip the "folder" param for tests - $responseParams = $response->getParams(); - unset($responseParams['folder']); - $response->setParams($responseParams); - - $sharedTmplParams = [ - 'owner' => 'ownerUID', - 'filename' => '/fileDrop', - 'directory_path' => '/fileDrop', - 'mimetype' => null, - 'dirToken' => 'token', - 'sharingToken' => 'token', - 'server2serversharing' => true, - 'protected' => 'false', - 'dir' => null, - 'downloadURL' => '', - 'fileSize' => '1 KB', - 'nonHumanFileSize' => 1337, - 'maxSizeAnimateGif' => null, - 'previewSupported' => null, - 'previewEnabled' => null, - 'previewMaxX' => null, - 'previewMaxY' => null, - 'hideFileList' => true, - 'shareOwner' => 'ownerDisplay', - 'disclaimer' => null, - 'shareUrl' => '', - 'previewImage' => '', - 'previewURL' => '', - 'note' => '', - 'hideDownload' => false, - 'showgridview' => false, - 'label' => '' - ]; $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); $csp->addAllowedFrameDomain('\'self\''); - $expectedResponse = new PublicTemplateResponse($this->appName, 'public', $sharedTmplParams); + $expectedResponse = new PublicTemplateResponse('files', 'index'); $expectedResponse->setContentSecurityPolicy($csp); - $expectedResponse->setHeaderTitle($sharedTmplParams['filename']); - $expectedResponse->setHeaderDetails('shared by ' . $sharedTmplParams['shareOwner']); + $expectedResponse->setHeaderTitle($filename); + $expectedResponse->setHeaderDetails(''); + $expectedResponse->setHeaderActions([ + new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', 'downloadUrl', 0, '33'), + new ExternalShareMenuAction($this->l10n->t('Add to your Nextcloud'), 'icon-external', 'owner', 'ownerDisplay', $filename), + new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', 'downloadUrl'), + ]); - self::assertEquals($expectedResponse, $response); + $this->assertEquals($expectedResponse, $response); } |