Skip to content

Commit aae5ecb

Browse files
authored
Merge pull request #1243 from nextcloud-libraries/fix/stable4-public-shares
2 parents c5b6b72 + 78b2af5 commit aae5ecb

4 files changed

Lines changed: 150 additions & 65 deletions

File tree

lib/components/FilePicker/FilePicker.vue

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<FilePickerBreadcrumbs v-if="currentView === 'files'"
1010
:path.sync="currentPath"
1111
:show-menu="allowPickDirectory"
12-
@create-node="onCreateFolder"/>
12+
@create-node="onCreateFolder" />
1313
<div v-else class="file-picker__view">
1414
<h3>{{ viewHeadline }}</h3>
1515
</div>
@@ -53,13 +53,12 @@ import FileList from './FileList.vue'
5353
import FilePickerBreadcrumbs from './FilePickerBreadcrumbs.vue'
5454
import FilePickerNavigation from './FilePickerNavigation.vue'
5555
56-
import { davRootPath } from '@nextcloud/files'
5756
import { NcEmptyContent } from '@nextcloud/vue'
58-
import { join } from 'path'
5957
import { computed, onMounted, ref, toRef } from 'vue'
6058
import { showError } from '../../toast'
6159
import { useDAVFiles } from '../../usables/dav'
6260
import { useMimeFilter } from '../../usables/mime'
61+
import { useIsPublic } from '../../usables/isPublic'
6362
import { t } from '../../utils/l10n'
6463
6564
const props = withDefaults(defineProps<{
@@ -118,6 +117,11 @@ const emit = defineEmits<{
118117
(e: 'close', v?: Node[]): void
119118
}>()
120119
120+
/**
121+
* Whether we are on a public endpoint (e.g. public share)
122+
*/
123+
const { isPublic } = useIsPublic()
124+
121125
/**
122126
* Props to be passed to the underlying Dialog component
123127
*/
@@ -203,7 +207,7 @@ const filterString = ref('')
203207
204208
const { isSupportedMimeType } = useMimeFilter(toRef(props, 'mimetypeFilter')) // vue 3.3 will allow cleaner syntax of toRef(() => props.mimetypeFilter)
205209
206-
const { files, isLoading, loadFiles, getFile, client } = useDAVFiles(currentView, currentPath)
210+
const { files, isLoading, loadFiles, getFile, createDirectory } = useDAVFiles(currentView, currentPath, isPublic)
207211
208212
onMounted(() => loadFiles())
209213
@@ -243,13 +247,14 @@ const noFilesDescription = computed(() => {
243247
* Handle creating new folder (breadcrumb menu)
244248
* @param name The new folder name
245249
*/
246-
const onCreateFolder = (name: string) => {
247-
client
248-
.createDirectory(join(davRootPath, currentPath.value, name))
249-
// reload file list
250-
.then(() => loadFiles())
250+
const onCreateFolder = async (name: string) => {
251+
try {
252+
await createDirectory(name)
253+
} catch (error) {
254+
console.warn('Could not create new folder', { name, error })
251255
// show error to user
252-
.catch((e) => showError(t('Could not create the new folder')))
256+
showError(t('Could not create the new folder'))
257+
}
253258
}
254259
</script>
255260

lib/components/FilePicker/FilePickerNavigation.vue

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,33 @@
1212
<IconClose :size="16" />
1313
</template>
1414
</NcTextField>
15-
<!-- On non collapsed dialogs show the tablist, otherwise a dropdown is shown -->
16-
<ul v-if="!isCollapsed"
17-
class="file-picker__side"
18-
role="tablist"
19-
:aria-label="t('Filepicker sections')">
20-
<li v-for="view in allViews" :key="view.id">
21-
<NcButton :aria-selected="currentView === view.id"
22-
:type="currentView === view.id ? 'primary' : 'tertiary'"
23-
:wide="true"
24-
role="tab"
25-
@click="$emit('update:currentView', view.id)">
26-
<template #icon>
27-
<component :is="view.icon" :size="20" />
28-
</template>
29-
{{ view.label }}
30-
</NcButton>
31-
</li>
32-
</ul>
33-
<NcSelect v-else
34-
:aria-label="t('Current view selector')"
35-
:clearable="false"
36-
:searchable="false"
37-
:options="allViews"
38-
:value="currentViewObject"
39-
@input="v => emit('update:currentView', v.id)" />
15+
<template v-if="!isPublic">
16+
<!-- On non collapsed dialogs show the tablist, otherwise a dropdown is shown -->
17+
<ul v-if="!isCollapsed"
18+
class="file-picker__side"
19+
role="tablist"
20+
:aria-label="t('Filepicker sections')">
21+
<li v-for="view in allViews" :key="view.id">
22+
<NcButton :aria-selected="currentView === view.id"
23+
:type="currentView === view.id ? 'primary' : 'tertiary'"
24+
:wide="true"
25+
role="tab"
26+
@click="$emit('update:currentView', view.id)">
27+
<template #icon>
28+
<component :is="view.icon" :size="20" />
29+
</template>
30+
{{ view.label }}
31+
</NcButton>
32+
</li>
33+
</ul>
34+
<NcSelect v-else
35+
:aria-label="t('Current view selector')"
36+
:clearable="false"
37+
:searchable="false"
38+
:options="allViews"
39+
:value="currentViewObject"
40+
@input="v => emit('update:currentView', v.id)" />
41+
</template>
4042
</Fragment>
4143
</template>
4244

@@ -48,9 +50,12 @@ import IconMagnify from 'vue-material-design-icons/Magnify.vue'
4850
import IconStar from 'vue-material-design-icons/Star.vue'
4951
5052
import { NcButton, NcSelect, NcTextField } from '@nextcloud/vue'
51-
import { t } from '../../utils/l10n'
5253
import { computed } from 'vue'
5354
import { Fragment } from 'vue-frag'
55+
import { t } from '../../utils/l10n'
56+
import { useIsPublic } from '../../usables/isPublic'
57+
58+
const { isPublic } = useIsPublic()
5459
5560
const allViews = [{
5661
id: 'files',

lib/usables/dav.ts

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,76 @@
1919
* along with this program. If not, see <http://www.gnu.org/licenses/>.
2020
*
2121
*/
22-
import type { Node } from '@nextcloud/files'
22+
import type { Folder, Node } from '@nextcloud/files'
2323
import type { ComputedRef, Ref } from 'vue'
24-
import type { FileStat, ResponseDataDetailed, WebDAVClient } from 'webdav'
24+
import type { FileStat, ResponseDataDetailed, SearchResult } from 'webdav'
2525

26-
import { davGetClient, davGetDefaultPropfind, davGetFavoritesReport, davGetRecentSearch, davResultToNode, davRootPath } from '@nextcloud/files'
26+
import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davRemoteURL, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files'
2727
import { generateRemoteUrl } from '@nextcloud/router'
28-
import { ref, watch } from 'vue'
28+
import { dirname, join } from 'path'
29+
import { computed, ref, watch } from 'vue'
2930

3031
/**
3132
* Handle file loading using WebDAV
3233
*
3334
* @param currentView Reference to the current files view
3435
* @param currentPath Reference to the current files path
36+
* @param isPublicEndpoint Whether the filepicker is used on a public share
3537
*/
36-
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> } {
38+
export const useDAVFiles = function(
39+
currentView: Ref<'files'|'recent'|'favorites'> | ComputedRef<'files'|'recent'|'favorites'>,
40+
currentPath: Ref<string> | ComputedRef<string>,
41+
isPublicEndpoint: Ref<boolean> | ComputedRef<boolean>,
42+
): { isLoading: Ref<boolean>, createDirectory: (name: string) => Promise<Folder>, files: Ref<Node[]>, loadFiles: () => Promise<void>, getFile: (path: string) => Promise<Node> } {
43+
44+
const defaultRootPath = computed(() => isPublicEndpoint.value ? '/' : davRootPath)
45+
46+
const defaultRemoteUrl = computed(() => {
47+
if (isPublicEndpoint.value) {
48+
return generateRemoteUrl('webdav').replace('/remote.php', '/public.php')
49+
}
50+
return davRemoteURL
51+
})
52+
3753
/**
3854
* The WebDAV client
3955
*/
40-
const client = davGetClient(generateRemoteUrl('dav'))
56+
const client = computed(() => {
57+
if (isPublicEndpoint.value) {
58+
const token = (document.getElementById('sharingToken')! as HTMLInputElement).value
59+
const autorization = btoa(`${token}:null`)
60+
61+
const client = davGetClient(defaultRemoteUrl.value)
62+
client.setHeaders({ Authorization: `Basic ${autorization}` })
63+
return client
64+
}
65+
66+
return davGetClient()
67+
})
68+
69+
const resultToNode = (result: FileStat) => {
70+
const node = davResultToNode(result, defaultRootPath.value, defaultRemoteUrl.value)
71+
// Fixed for @nextcloud/files 3.1.0 but not supported on Nextcloud 27 so patching it
72+
if (isPublicEndpoint.value) {
73+
return new Proxy(node, {
74+
get(node, prop) {
75+
if (prop === 'dirname' || prop === 'path') {
76+
const source = node.source
77+
let path = source.slice(defaultRemoteUrl.value.length)
78+
if (path[0] !== '/') {
79+
path = `/${path}`
80+
}
81+
if (prop === 'dirname') {
82+
return dirname(path)
83+
}
84+
return path
85+
}
86+
return (node as never)[prop]
87+
},
88+
})
89+
}
90+
return node
91+
}
4192

4293
/**
4394
* All queried files
@@ -49,15 +100,32 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
49100
*/
50101
const isLoading = ref(true)
51102

103+
/**
104+
* Create a new directory in the current path
105+
* @param name Name of the new directory
106+
* @return {Promise<Folder>} The created directory
107+
*/
108+
async function createDirectory(name: string): Promise<Folder> {
109+
const path = join(currentPath.value, name)
110+
111+
await client.value.createDirectory(join(defaultRootPath.value, path))
112+
const directory = await getFile(path) as Folder
113+
files.value.push(directory)
114+
return directory
115+
}
116+
52117
/**
53118
* Get information for one file
54119
* @param path The path of the file or folder
120+
* @param rootPath The dav root path to use (or the default is nothing set)
55121
*/
56-
async function getFile(path: string) {
57-
const result = await client.stat(`${davRootPath}${path}`, {
122+
async function getFile(path: string, rootPath: string|undefined = undefined) {
123+
rootPath = rootPath ?? defaultRootPath.value
124+
125+
const { data } = await client.value.stat(`${rootPath}${path}`, {
58126
details: true,
59127
}) as ResponseDataDetailed<FileStat>
60-
return davResultToNode(result.data)
128+
return resultToNode(data)
61129
}
62130

63131
/**
@@ -67,34 +135,26 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
67135
isLoading.value = true
68136

69137
if (currentView.value === 'favorites') {
70-
files.value = await client.getDirectoryContents(`${davRootPath}${currentPath.value}`, {
71-
details: true,
72-
data: davGetFavoritesReport(),
73-
headers: {
74-
method: 'REPORT',
75-
},
76-
includeSelf: false,
77-
}).then((result) => (result as ResponseDataDetailed<FileStat[]>).data.map((data) => davResultToNode(data)))
138+
files.value = await getFavoriteNodes(client.value, currentPath.value, defaultRootPath.value)
78139
} else if (currentView.value === 'recent') {
79140
// unix timestamp in seconds, two weeks ago
80141
const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14)
81-
const results = await client.getDirectoryContents(currentPath.value, {
142+
const { data } = await client.value.search('/', {
82143
details: true,
83144
data: davGetRecentSearch(lastTwoWeek),
84-
headers: {
85-
method: 'SEARCH',
86-
'Content-Type': 'application/xml; charset=utf-8',
87-
},
88-
deep: true,
89-
}) as ResponseDataDetailed<FileStat[]>
90-
91-
files.value = results.data.map((r) => davResultToNode(r))
145+
}) as ResponseDataDetailed<SearchResult>
146+
files.value = data.results.map(resultToNode)
92147
} else {
93-
const results = await client.getDirectoryContents(`${davRootPath}${currentPath.value}`, {
148+
const results = await client.value.getDirectoryContents(`${defaultRootPath.value}${currentPath.value}`, {
94149
details: true,
95150
data: davGetDefaultPropfind(),
96151
}) as ResponseDataDetailed<FileStat[]>
97-
files.value = results.data.map((r) => davResultToNode(r))
152+
files.value = results.data.map(resultToNode)
153+
154+
// Hack for the public endpoint which always returns folder itself
155+
if (isPublicEndpoint.value) {
156+
files.value = files.value.filter((file) => file.path !== currentPath.value)
157+
}
98158
}
99159

100160
isLoading.value = false
@@ -110,6 +170,6 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
110170
files,
111171
loadFiles: () => loadDAVFiles(),
112172
getFile,
113-
client,
173+
createDirectory,
114174
}
115175
}

lib/usables/isPublic.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { onBeforeMount, ref } from 'vue'
2+
3+
/**
4+
* Check whether the component is mounted in a public share
5+
*/
6+
export const useIsPublic = () => {
7+
const checkIsPublic = () => (document.getElementById('isPublic') as HTMLInputElement|null)?.value === '1'
8+
9+
const isPublic = ref(true)
10+
onBeforeMount(() => { isPublic.value = checkIsPublic() })
11+
12+
return {
13+
isPublic,
14+
}
15+
}

0 commit comments

Comments
 (0)