From ead859bd638979d039d0d6286426db81b3c115b9 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 20 Feb 2024 20:35:58 +0100 Subject: [PATCH 1/3] fix(FilePicker): Adjust DAV composable to be able to work on public `webdav` endpoint Signed-off-by: Ferdinand Thiessen --- lib/components/FilePicker/FilePicker.vue | 25 +++--- lib/usables/dav.ts | 96 +++++++++++++++++------- lib/usables/isPublic.ts | 15 ++++ 3 files changed, 97 insertions(+), 39 deletions(-) create mode 100644 lib/usables/isPublic.ts diff --git a/lib/components/FilePicker/FilePicker.vue b/lib/components/FilePicker/FilePicker.vue index eb5d6ebd..1dd27f6e 100644 --- a/lib/components/FilePicker/FilePicker.vue +++ b/lib/components/FilePicker/FilePicker.vue @@ -9,7 +9,7 @@ + @create-node="onCreateFolder" />

{{ viewHeadline }}

@@ -53,13 +53,12 @@ import FileList from './FileList.vue' import FilePickerBreadcrumbs from './FilePickerBreadcrumbs.vue' import FilePickerNavigation from './FilePickerNavigation.vue' -import { davRootPath } from '@nextcloud/files' import { NcEmptyContent } from '@nextcloud/vue' -import { join } from 'path' import { computed, onMounted, ref, toRef } from 'vue' import { showError } from '../../toast' import { useDAVFiles } from '../../usables/dav' import { useMimeFilter } from '../../usables/mime' +import { useIsPublic } from '../../usables/isPublic' import { t } from '../../utils/l10n' const props = withDefaults(defineProps<{ @@ -118,6 +117,11 @@ const emit = defineEmits<{ (e: 'close', v?: Node[]): void }>() +/** + * Whether we are on a public endpoint (e.g. public share) + */ +const { isPublic } = useIsPublic() + /** * Props to be passed to the underlying Dialog component */ @@ -203,7 +207,7 @@ const filterString = ref('') const { isSupportedMimeType } = useMimeFilter(toRef(props, 'mimetypeFilter')) // vue 3.3 will allow cleaner syntax of toRef(() => props.mimetypeFilter) -const { files, isLoading, loadFiles, getFile, client } = useDAVFiles(currentView, currentPath) +const { files, isLoading, loadFiles, getFile, createDirectory } = useDAVFiles(currentView, currentPath, isPublic) onMounted(() => loadFiles()) @@ -243,13 +247,14 @@ const noFilesDescription = computed(() => { * Handle creating new folder (breadcrumb menu) * @param name The new folder name */ -const onCreateFolder = (name: string) => { - client - .createDirectory(join(davRootPath, currentPath.value, name)) - // reload file list - .then(() => loadFiles()) +const onCreateFolder = async (name: string) => { + try { + await createDirectory(name) + } catch (error) { + console.warn('Could not create new folder', { name, error }) // show error to user - .catch((e) => showError(t('Could not create the new folder'))) + showError(t('Could not create the new folder')) + } } diff --git a/lib/usables/dav.ts b/lib/usables/dav.ts index 6fb70459..ef4bd4b5 100644 --- a/lib/usables/dav.ts +++ b/lib/usables/dav.ts @@ -19,25 +19,54 @@ * along with this program. If not, see . * */ -import type { Node } from '@nextcloud/files' +import type { Folder, Node } from '@nextcloud/files' import type { ComputedRef, Ref } from 'vue' -import type { FileStat, ResponseDataDetailed, WebDAVClient } from 'webdav' +import type { FileStat, ResponseDataDetailed, SearchResult } from 'webdav' -import { davGetClient, davGetDefaultPropfind, davGetFavoritesReport, davGetRecentSearch, davResultToNode, davRootPath } from '@nextcloud/files' +import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davRemoteURL, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files' import { generateRemoteUrl } from '@nextcloud/router' -import { ref, watch } from 'vue' +import { join } from 'path' +import { computed, ref, watch } from 'vue' /** * Handle file loading using WebDAV * * @param currentView Reference to the current files view * @param currentPath Reference to the current files path + * @param isPublicEndpoint Whether the filepicker is used on a public share */ -export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites'> | ComputedRef<'files'|'recent'|'favorites'>, currentPath: Ref | ComputedRef): { isLoading: Ref, client: WebDAVClient, files: Ref, loadFiles: () => void, getFile: (path: string) => Promise } { +export const useDAVFiles = function( + currentView: Ref<'files'|'recent'|'favorites'> | ComputedRef<'files'|'recent'|'favorites'>, + currentPath: Ref | ComputedRef, + isPublicEndpoint: Ref | ComputedRef, +): { isLoading: Ref, createDirectory: (name: string) => Promise, files: Ref, loadFiles: () => Promise, getFile: (path: string) => Promise } { + + const defaultRootPath = computed(() => isPublicEndpoint.value ? '/' : davRootPath) + + const defaultRemoteUrl = computed(() => { + if (isPublicEndpoint.value) { + return generateRemoteUrl('webdav').replace('/remote.php', '/public.php') + } + return davRemoteURL + }) + /** * The WebDAV client */ - const client = davGetClient(generateRemoteUrl('dav')) + const client = computed(() => { + if (isPublicEndpoint.value) { + const token = (document.getElementById('sharingToken')! as HTMLInputElement).value + const autorization = btoa(`${token}:null`) + + return davGetClient(defaultRemoteUrl.value, { + Authorization: `Basic ${autorization}`, + }) + } + + return davGetClient() + }) + + const resultToNode = (result: FileStat) => davResultToNode(result, defaultRootPath.value, defaultRemoteUrl.value) /** * All queried files @@ -49,15 +78,32 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites */ const isLoading = ref(true) + /** + * Create a new directory in the current path + * @param name Name of the new directory + * @return {Promise} The created directory + */ + async function createDirectory(name: string): Promise { + const path = join(currentPath.value, name) + + await client.value.createDirectory(join(defaultRootPath.value, path)) + const directory = await getFile(path) as Folder + files.value.push(directory) + return directory + } + /** * Get information for one file * @param path The path of the file or folder + * @param rootPath The dav root path to use (or the default is nothing set) */ - async function getFile(path: string) { - const result = await client.stat(`${davRootPath}${path}`, { + async function getFile(path: string, rootPath: string|undefined = undefined) { + rootPath = rootPath ?? defaultRootPath.value + + const { data } = await client.value.stat(`${rootPath}${path}`, { details: true, }) as ResponseDataDetailed - return davResultToNode(result.data) + return resultToNode(data) } /** @@ -67,34 +113,26 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites isLoading.value = true if (currentView.value === 'favorites') { - files.value = await client.getDirectoryContents(`${davRootPath}${currentPath.value}`, { - details: true, - data: davGetFavoritesReport(), - headers: { - method: 'REPORT', - }, - includeSelf: false, - }).then((result) => (result as ResponseDataDetailed).data.map((data) => davResultToNode(data))) + files.value = await getFavoriteNodes(client.value, currentPath.value, defaultRootPath.value) } else if (currentView.value === 'recent') { // unix timestamp in seconds, two weeks ago const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14) - const results = await client.getDirectoryContents(currentPath.value, { + const { data } = await client.value.search('/', { details: true, data: davGetRecentSearch(lastTwoWeek), - headers: { - method: 'SEARCH', - 'Content-Type': 'application/xml; charset=utf-8', - }, - deep: true, - }) as ResponseDataDetailed - - files.value = results.data.map((r) => davResultToNode(r)) + }) as ResponseDataDetailed + files.value = data.results.map(resultToNode) } else { - const results = await client.getDirectoryContents(`${davRootPath}${currentPath.value}`, { + const results = await client.value.getDirectoryContents(`${defaultRootPath.value}${currentPath.value}`, { details: true, data: davGetDefaultPropfind(), }) as ResponseDataDetailed - files.value = results.data.map((r) => davResultToNode(r)) + files.value = results.data.map(resultToNode) + + // Hack for the public endpoint which always returns folder itself + if (isPublicEndpoint.value) { + files.value = files.value.filter((file) => file.path !== currentPath.value) + } } isLoading.value = false @@ -110,6 +148,6 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites files, loadFiles: () => loadDAVFiles(), getFile, - client, + createDirectory, } } diff --git a/lib/usables/isPublic.ts b/lib/usables/isPublic.ts new file mode 100644 index 00000000..d41951a1 --- /dev/null +++ b/lib/usables/isPublic.ts @@ -0,0 +1,15 @@ +import { onBeforeMount, ref } from 'vue' + +/** + * Check whether the component is mounted in a public share + */ +export const useIsPublic = () => { + const checkIsPublic = () => (document.getElementById('isPublic') as HTMLInputElement|null)?.value === '1' + + const isPublic = ref(true) + onBeforeMount(() => { isPublic.value = checkIsPublic() }) + + return { + isPublic, + } +} From 640f2ccceb0bd4a9e87b2057cfe15fbdbf1d76c9 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 21 Feb 2024 13:39:52 +0100 Subject: [PATCH 2/3] fix(FilePicker): Do not show private views on public shares Signed-off-by: Ferdinand Thiessen --- .../FilePicker/FilePickerNavigation.vue | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/lib/components/FilePicker/FilePickerNavigation.vue b/lib/components/FilePicker/FilePickerNavigation.vue index 6e237c30..7b61a8d2 100644 --- a/lib/components/FilePicker/FilePickerNavigation.vue +++ b/lib/components/FilePicker/FilePickerNavigation.vue @@ -12,31 +12,33 @@ - -
    -
  • - - - {{ view.label }} - -
  • -
- + @@ -48,9 +50,12 @@ import IconMagnify from 'vue-material-design-icons/Magnify.vue' import IconStar from 'vue-material-design-icons/Star.vue' import { NcButton, NcSelect, NcTextField } from '@nextcloud/vue' -import { t } from '../../utils/l10n' import { computed } from 'vue' import { Fragment } from 'vue-frag' +import { t } from '../../utils/l10n' +import { useIsPublic } from '../../usables/isPublic' + +const { isPublic } = useIsPublic() const allViews = [{ id: 'files', From 78b2af5d9b656a78d83e98503e230689717e7e65 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 21 Feb 2024 14:23:13 +0100 Subject: [PATCH 3/3] fix(FilePicker): Adjust `resultToNode` as we are bound to legacy `@nextcloud/files` without fixes This was fixed in 3.1.0 but we have to use 3.0.0-beta for Nextcloud stable27. Signed-off-by: Ferdinand Thiessen --- lib/usables/dav.ts | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/usables/dav.ts b/lib/usables/dav.ts index ef4bd4b5..ff220875 100644 --- a/lib/usables/dav.ts +++ b/lib/usables/dav.ts @@ -25,7 +25,7 @@ import type { FileStat, ResponseDataDetailed, SearchResult } from 'webdav' import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davRemoteURL, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files' import { generateRemoteUrl } from '@nextcloud/router' -import { join } from 'path' +import { dirname, join } from 'path' import { computed, ref, watch } from 'vue' /** @@ -58,15 +58,37 @@ export const useDAVFiles = function( const token = (document.getElementById('sharingToken')! as HTMLInputElement).value const autorization = btoa(`${token}:null`) - return davGetClient(defaultRemoteUrl.value, { - Authorization: `Basic ${autorization}`, - }) + const client = davGetClient(defaultRemoteUrl.value) + client.setHeaders({ Authorization: `Basic ${autorization}` }) + return client } return davGetClient() }) - const resultToNode = (result: FileStat) => davResultToNode(result, defaultRootPath.value, defaultRemoteUrl.value) + const resultToNode = (result: FileStat) => { + const node = davResultToNode(result, defaultRootPath.value, defaultRemoteUrl.value) + // Fixed for @nextcloud/files 3.1.0 but not supported on Nextcloud 27 so patching it + if (isPublicEndpoint.value) { + return new Proxy(node, { + get(node, prop) { + if (prop === 'dirname' || prop === 'path') { + const source = node.source + let path = source.slice(defaultRemoteUrl.value.length) + if (path[0] !== '/') { + path = `/${path}` + } + if (prop === 'dirname') { + return dirname(path) + } + return path + } + return (node as never)[prop] + }, + }) + } + return node + } /** * All queried files