Browse Source

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>
pull/45652/head
Ferdinand Thiessen 1 year ago
parent
commit
96c8275586
No known key found for this signature in database GPG Key ID: 45FAE7268762B400
  1. 8
      apps/files/src/main.ts
  2. 11
      apps/files/src/services/Files.ts
  3. 8
      apps/files/src/services/RouterService.ts
  4. 7
      apps/files/src/views/Sidebar.vue
  5. 303
      apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php
  6. 28
      apps/files_sharing/src/init-public.ts
  7. 54
      apps/files_sharing/src/router/index.ts
  8. 4
      apps/files_sharing/src/services/SharingService.spec.ts
  9. 13
      apps/files_sharing/src/services/SharingService.ts
  10. 67
      apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue
  11. 59
      apps/files_sharing/src/views/publicFileDrop.ts
  12. 67
      apps/files_sharing/src/views/publicFileShare.ts
  13. 28
      apps/files_sharing/src/views/publicShare.ts
  14. 498
      apps/files_sharing/tests/Controller/ShareControllerTest.php
  15. 1
      webpack.modules.js

8
apps/files/src/main.ts

@ -30,8 +30,10 @@ window.OCA.Files = window.OCA.Files ?? {}
window.OCP.Files = window.OCP.Files ?? {}
// Expose router
const Router = new RouterService(router)
Object.assign(window.OCP.Files, { Router })
if (!window.OCP.Files.Router) {
const Router = new RouterService(router)
Object.assign(window.OCP.Files, { Router })
}
// Init Pinia store
Vue.use(PiniaVuePlugin)
@ -48,6 +50,6 @@ Object.assign(window.OCA.Files.Settings, { Setting: SettingsModel })
const FilesAppVue = Vue.extend(FilesApp)
new FilesAppVue({
router,
router: window.OCP.Files.Router.router,
pinia,
}).$mount('#content')

11
apps/files/src/services/Files.ts

@ -5,25 +5,26 @@
import type { ContentsWithRoot, File, Folder } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed } from 'webdav'
import { CancelablePromise } from 'cancelable-promise'
import { davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import { CancelablePromise } from 'cancelable-promise'
import { join } from 'path'
import { client } from './WebdavClient.ts'
import logger from '../logger.ts'
/**
* Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map`
* @param node The node returned by the webdav library
* @param stat The result returned by the webdav library
*/
export const resultToNode = (node: FileStat): File | Folder => davResultToNode(node)
export const resultToNode = (stat: FileStat): File | Folder => davResultToNode(stat)
export const getContents = (path = '/'): CancelablePromise<ContentsWithRoot> => {
path = join(davRootPath, path)
const controller = new AbortController()
const propfindPayload = davGetDefaultPropfind()
path = `${davRootPath}${path}`
return new CancelablePromise(async (resolve, reject, onCancel) => {
onCancel(() => controller.abort())
try {
const contentsResponse = await client.getDirectoryContents(path, {
details: true,

8
apps/files/src/services/RouterService.ts

@ -26,6 +26,14 @@ export default class RouterService {
return this._router.currentRoute.params || {}
}
/**
* This is a protected getter only for internal use
* @private
*/
get router() {
return this._router
}
/**
* Trigger a route change on the files app
*

7
apps/files/src/views/Sidebar.vue

@ -91,9 +91,9 @@ import { getCurrentUser } from '@nextcloud/auth'
import { getCapabilities } from '@nextcloud/capabilities'
import { showError } from '@nextcloud/dialogs'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { File, Folder, formatFileSize } from '@nextcloud/files'
import { File, Folder, davRemoteURL, davRootPath, formatFileSize } from '@nextcloud/files'
import { encodePath } from '@nextcloud/paths'
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
import { generateUrl } from '@nextcloud/router'
import { ShareType } from '@nextcloud/sharing'
import { mdiStar, mdiStarOutline } from '@mdi/js'
import axios from '@nextcloud/axios'
@ -187,8 +187,7 @@ export default {
* @return {string}
*/
davPath() {
const user = this.currentUser.uid
return generateRemoteUrl(`dav/files/${user}${encodePath(this.file)}`)
return `${davRemoteURL}/${davRootPath}${encodePath(this.file)}`
},
/**

303
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;
}
}

28
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)

54
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

4
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', () => {

13
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,

67
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>

59
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)
}

67
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)
}

28
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)
}

498
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);
}

1
webpack.modules.js

@ -53,6 +53,7 @@ module.exports = {
collaboration: path.join(__dirname, 'apps/files_sharing/src', 'collaborationresourceshandler.js'),
files_sharing_tab: path.join(__dirname, 'apps/files_sharing/src', 'files_sharing_tab.js'),
init: path.join(__dirname, 'apps/files_sharing/src', 'init.ts'),
'init-public': path.join(__dirname, 'apps/files_sharing/src', 'init-public.ts'),
main: path.join(__dirname, 'apps/files_sharing/src', 'main.ts'),
'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'),
'public-file-request': path.join(__dirname, 'apps/files_sharing/src', 'public-file-request.ts'),

Loading…
Cancel
Save