Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable4] fix public shares #1243

Merged
merged 3 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions lib/components/FilePicker/FilePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<FilePickerBreadcrumbs v-if="currentView === 'files'"
:path.sync="currentPath"
:show-menu="allowPickDirectory"
@create-node="onCreateFolder"/>
@create-node="onCreateFolder" />
<div v-else class="file-picker__view">
<h3>{{ viewHeadline }}</h3>
</div>
Expand Down Expand Up @@ -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<{
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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'))
}
}
</script>

Expand Down
57 changes: 31 additions & 26 deletions lib/components/FilePicker/FilePickerNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,33 @@
<IconClose :size="16" />
</template>
</NcTextField>
<!-- On non collapsed dialogs show the tablist, otherwise a dropdown is shown -->
<ul v-if="!isCollapsed"
class="file-picker__side"
role="tablist"
:aria-label="t('Filepicker sections')">
<li v-for="view in allViews" :key="view.id">
<NcButton :aria-selected="currentView === view.id"
:type="currentView === view.id ? 'primary' : 'tertiary'"
:wide="true"
role="tab"
@click="$emit('update:currentView', view.id)">
<template #icon>
<component :is="view.icon" :size="20" />
</template>
{{ view.label }}
</NcButton>
</li>
</ul>
<NcSelect v-else
:aria-label="t('Current view selector')"
:clearable="false"
:searchable="false"
:options="allViews"
:value="currentViewObject"
@input="v => emit('update:currentView', v.id)" />
<template v-if="!isPublic">
<!-- On non collapsed dialogs show the tablist, otherwise a dropdown is shown -->
<ul v-if="!isCollapsed"
class="file-picker__side"
role="tablist"
:aria-label="t('Filepicker sections')">
<li v-for="view in allViews" :key="view.id">
<NcButton :aria-selected="currentView === view.id"
:type="currentView === view.id ? 'primary' : 'tertiary'"
:wide="true"
role="tab"
@click="$emit('update:currentView', view.id)">
<template #icon>
<component :is="view.icon" :size="20" />
</template>
{{ view.label }}
</NcButton>
</li>
</ul>
<NcSelect v-else
:aria-label="t('Current view selector')"
:clearable="false"
:searchable="false"
:options="allViews"
:value="currentViewObject"
@input="v => emit('update:currentView', v.id)" />
</template>
</Fragment>
</template>

Expand All @@ -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',
Expand Down
118 changes: 89 additions & 29 deletions lib/usables/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,76 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 { dirname, 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<string> | ComputedRef<string>): { isLoading: Ref<boolean>, client: WebDAVClient, files: Ref<Node[]>, loadFiles: () => void, getFile: (path: string) => Promise<Node> } {
export const useDAVFiles = function(
currentView: Ref<'files'|'recent'|'favorites'> | ComputedRef<'files'|'recent'|'favorites'>,
currentPath: Ref<string> | ComputedRef<string>,
isPublicEndpoint: Ref<boolean> | ComputedRef<boolean>,
): { isLoading: Ref<boolean>, createDirectory: (name: string) => Promise<Folder>, files: Ref<Node[]>, loadFiles: () => Promise<void>, getFile: (path: string) => Promise<Node> } {

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

const client = davGetClient(defaultRemoteUrl.value)
client.setHeaders({ Authorization: `Basic ${autorization}` })
return client
}

return davGetClient()
})

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
Expand All @@ -49,15 +100,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<Folder>} The created directory
*/
async function createDirectory(name: string): Promise<Folder> {
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<FileStat>
return davResultToNode(result.data)
return resultToNode(data)
}

/**
Expand All @@ -67,34 +135,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<FileStat[]>).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<FileStat[]>

files.value = results.data.map((r) => davResultToNode(r))
}) as ResponseDataDetailed<SearchResult>
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<FileStat[]>
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
Expand All @@ -110,6 +170,6 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
files,
loadFiles: () => loadDAVFiles(),
getFile,
client,
createDirectory,
}
}
15 changes: 15 additions & 0 deletions lib/usables/isPublic.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
Loading