diff options
Diffstat (limited to 'apps/files_sharing/src')
-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 |
8 files changed, 310 insertions, 10 deletions
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) +} |