Skip to content

Commit a6b201e

Browse files
authored
Merge pull request #46728 from nextcloud/backport/46690/stable29
2 parents 4aac0b1 + 456aeb9 commit a6b201e

134 files changed

Lines changed: 783 additions & 645 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/files/src/actions/openFolderAction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const action = new FileAction({
2727
id: 'open-folder',
2828
displayName(files: Node[]) {
2929
// Only works on single node
30-
const displayName = files[0].attributes.displayname || files[0].basename
30+
const displayName = files[0].displayname
3131
return t('files', 'Open folder {displayName}', { displayName })
3232
},
3333
iconSvgInline: () => FolderSvg,

apps/files/src/components/BreadCrumbs.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@ export default defineComponent({
175175
return this.$navigation?.active?.name || t('files', 'Home')
176176
}
177177
178-
const source: FileSource | null = this.getFileSourceFromPath(path)
179-
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
180-
return node?.attributes?.displayname || basename(path)
178+
const source = this.getFileSourceFromPath(path)
179+
const node = source ? this.getNodeFromSource(source) : undefined
180+
return node?.displayname || basename(path)
181181
},
182182
183183
onClick(to) {

apps/files/src/components/FileEntry.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
@click.native="execDefaultAction" />
5252

5353
<FileEntryName ref="name"
54-
:display-name="displayName"
54+
:basename="basename"
5555
:extension="extension"
5656
:files-list-width="filesListWidth"
5757
:nodes="nodes"

apps/files/src/components/FileEntry/FileEntryName.vue

Lines changed: 48 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
<template>
2323
<!-- Rename input -->
2424
<form v-if="isRenaming"
25-
v-on-click-outside="stopRenaming"
25+
ref="renameForm"
26+
v-on-click-outside="onRename"
2627
:aria-label="t('files', 'Rename file')"
2728
class="files-list__row-rename"
2829
@submit.prevent.stop="onRename">
@@ -33,7 +34,6 @@
3334
:required="true"
3435
:value.sync="newName"
3536
enterkeyhint="done"
36-
@keyup="checkInputValidity"
3737
@keyup.esc="stopRenaming" />
3838
</form>
3939

@@ -46,8 +46,8 @@
4646
v-bind="linkTo.params">
4747
<!-- File name -->
4848
<span class="files-list__row-name-text">
49-
<!-- Keep the displayName stuck to the extension to avoid whitespace rendering issues-->
50-
<span class="files-list__row-name-" v-text="displayName" />
49+
<!-- Keep the filename stuck to the extension to avoid whitespace rendering issues-->
50+
<span class="files-list__row-name-" v-text="basename" />
5151
<span class="files-list__row-name-ext" v-text="extension" />
5252
</span>
5353
</component>
@@ -57,34 +57,38 @@
5757
import type { Node } from '@nextcloud/files'
5858
import type { PropType } from 'vue'
5959
60+
import axios from '@nextcloud/axios'
61+
import { showError, showSuccess } from '@nextcloud/dialogs'
6062
import { emit } from '@nextcloud/event-bus'
6163
import { FileType, NodeStatus, Permission } from '@nextcloud/files'
62-
import { loadState } from '@nextcloud/initial-state'
63-
import { showError, showSuccess } from '@nextcloud/dialogs'
6464
import { translate as t } from '@nextcloud/l10n'
65-
import axios from '@nextcloud/axios'
66-
import Vue from 'vue'
65+
import { defineComponent } from 'vue'
6766
6867
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
6968
7069
import { useNavigation } from '../../composables/useNavigation'
7170
import { useRenamingStore } from '../../store/renaming.ts'
71+
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
7272
import logger from '../../logger.js'
7373
74-
const forbiddenCharacters = loadState<string>('files', 'forbiddenCharacters', '').split('')
75-
76-
export default Vue.extend({
74+
export default defineComponent({
7775
name: 'FileEntryName',
7876
7977
components: {
8078
NcTextField,
8179
},
8280
8381
props: {
84-
displayName: {
82+
/**
83+
* The filename without extension
84+
*/
85+
basename: {
8586
type: String,
8687
required: true,
8788
},
89+
/**
90+
* The extension of the filename
91+
*/
8892
extension: {
8993
type: String,
9094
required: true,
@@ -172,7 +176,7 @@ export default Vue.extend({
172176
params: {
173177
download: this.source.basename,
174178
href: this.source.source,
175-
title: t('files', 'Download file {name}', { name: this.displayName }),
179+
title: t('files', 'Download file {name}', { name: `${this.basename}${this.extension}` }),
176180
tabindex: '0',
177181
},
178182
}
@@ -198,70 +202,51 @@ export default Vue.extend({
198202
}
199203
},
200204
},
201-
},
202205
203-
methods: {
204-
/**
205-
* Check if the file name is valid and update the
206-
* input validity using browser's native validation.
207-
* @param event the keyup event
208-
*/
209-
checkInputValidity(event?: KeyboardEvent) {
210-
const input = event.target as HTMLInputElement
206+
newName() {
207+
// Check validity of the new name
211208
const newName = this.newName.trim?.() || ''
212-
logger.debug('Checking input validity', { newName })
213-
try {
214-
this.isFileNameValid(newName)
215-
input.setCustomValidity('')
216-
input.title = ''
217-
} catch (e) {
218-
input.setCustomValidity(e.message)
219-
input.title = e.message
220-
} finally {
221-
input.reportValidity()
222-
}
223-
},
224-
isFileNameValid(name) {
225-
const trimmedName = name.trim()
226-
const char = trimmedName.indexOf('/') !== -1
227-
? '/'
228-
: forbiddenCharacters.find((char) => trimmedName.includes(char))
229-
230-
if (trimmedName === '.' || trimmedName === '..') {
231-
throw new Error(t('files', '"{name}" is an invalid file name.', { name }))
232-
} else if (trimmedName.length === 0) {
233-
throw new Error(t('files', 'File name cannot be empty.'))
234-
} else if (char) {
235-
throw new Error(t('files', '"{char}" is not allowed inside a file name.', { char }))
236-
} else if (trimmedName.match(OC.config.blacklist_files_regex)) {
237-
throw new Error(t('files', '"{name}" is not an allowed filetype.', { name }))
238-
} else if (this.checkIfNodeExists(name)) {
239-
throw new Error(t('files', '{newName} already exists.', { newName: name }))
209+
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
210+
if (!input) {
211+
return
240212
}
241213
242-
return true
214+
let validity = getFilenameValidity(newName)
215+
// Checking if already exists
216+
if (validity === '' && this.checkIfNodeExists(newName)) {
217+
validity = t('files', 'Another entry with the same name already exists.')
218+
}
219+
this.$nextTick(() => {
220+
if (this.isRenaming) {
221+
input.setCustomValidity(validity)
222+
input.reportValidity()
223+
}
224+
})
243225
},
244-
checkIfNodeExists(name) {
226+
},
227+
228+
methods: {
229+
checkIfNodeExists(name: string) {
245230
return this.nodes.find(node => node.basename === name && node !== this.source)
246231
},
247232
248233
startRenaming() {
249234
this.$nextTick(() => {
250235
// Using split to get the true string length
251-
const extLength = (this.source.extension || '').split('').length
252-
const length = this.source.basename.split('').length - extLength
253-
const input = this.$refs.renameInput?.$refs?.inputField?.$refs?.input
236+
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
254237
if (!input) {
255238
logger.error('Could not find the rename input')
256239
return
257240
}
258-
input.setSelectionRange(0, length)
259241
input.focus()
242+
const length = this.source.basename.length - (this.source.extension ?? '').length
243+
input.setSelectionRange(0, length)
260244
261245
// Trigger a keyup event to update the input validity
262246
input.dispatchEvent(new Event('keyup'))
263247
})
264248
},
249+
265250
stopRenaming() {
266251
if (!this.isRenaming) {
267252
return
@@ -273,28 +258,23 @@ export default Vue.extend({
273258
274259
// Rename and move the file
275260
async onRename() {
276-
const oldName = this.source.basename
277-
const oldEncodedSource = this.source.encodedSource
278261
const newName = this.newName.trim?.() || ''
279-
if (newName === '') {
280-
showError(t('files', 'Name cannot be empty'))
262+
const form = this.$refs.renameForm as HTMLFormElement
263+
if (!form.checkValidity()) {
264+
showError(t('files', 'Invalid filename.') + ' ' + getFilenameValidity(newName))
281265
return
282266
}
283267
268+
const oldName = this.source.basename
269+
const oldEncodedSource = this.source.encodedSource
284270
if (oldName === newName) {
285271
this.stopRenaming()
286272
return
287273
}
288274
289-
// Checking if already exists
290-
if (this.checkIfNodeExists(newName)) {
291-
showError(t('files', 'Another entry with the same name already exists'))
292-
return
293-
}
294-
295275
// Set loading state
296276
this.loading = 'renaming'
297-
Vue.set(this.source, 'status', NodeStatus.LOADING)
277+
this.$set(this.source, 'status', NodeStatus.LOADING)
298278
299279
// Update node
300280
this.source.rename(newName)
@@ -338,7 +318,7 @@ export default Vue.extend({
338318
showError(t('files', 'Could not rename "{oldName}"', { oldName }))
339319
} finally {
340320
this.loading = false
341-
Vue.set(this.source, 'status', undefined)
321+
this.$set(this.source, 'status', undefined)
342322
}
343323
},
344324

apps/files/src/components/FileEntryGrid.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
@click.native="execDefaultAction" />
5454

5555
<FileEntryName ref="name"
56-
:display-name="displayName"
56+
:basename="basename"
5757
:extension="extension"
5858
:files-list-width="filesListWidth"
5959
:grid-mode="true"

apps/files/src/components/FileEntryMixin.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,31 @@ export default defineComponent({
8383
return this.source.status === NodeStatus.LOADING
8484
},
8585

86-
extension() {
87-
if (this.source.attributes?.displayname) {
88-
return extname(this.source.attributes.displayname)
86+
/**
87+
* The display name of the current node
88+
* Either the nodes filename or a custom display name (e.g. for shares)
89+
*/
90+
displayName() {
91+
return this.source.displayname
92+
},
93+
/**
94+
* The display name without extension
95+
*/
96+
basename() {
97+
if (this.extension === '') {
98+
return this.displayName
8999
}
90-
return this.source.extension || ''
100+
return this.displayName.slice(0, 0 - this.extension.length)
91101
},
92-
displayName() {
93-
const ext = this.extension
94-
const name = String(this.source.attributes.displayname || this.source.basename)
102+
/**
103+
* The extension of the file
104+
*/
105+
extension() {
106+
if (this.source.type === FileType.Folder) {
107+
return ''
108+
}
95109

96-
// Strip extension from name if defined
97-
return !ext ? name : name.slice(0, 0 - ext.length)
110+
return extname(this.displayName)
98111
},
99112

100113
draggingFiles() {

apps/files/src/services/Files.ts

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,64 +23,21 @@ import type { ContentsWithRoot } from '@nextcloud/files'
2323
import type { FileStat, ResponseDataDetailed, DAVResultResponseProps } from 'webdav'
2424

2525
import { CancelablePromise } from 'cancelable-promise'
26-
import { File, Folder, davParsePermissions, davGetDefaultPropfind } from '@nextcloud/files'
27-
import { generateRemoteUrl } from '@nextcloud/router'
28-
import { getCurrentUser } from '@nextcloud/auth'
29-
30-
import { getClient, rootPath } from './WebdavClient'
31-
import { hashCode } from '../utils/hashUtils'
26+
import { File, Folder, davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
3227
import logger from '../logger'
3328

34-
const client = getClient()
35-
36-
interface ResponseProps extends DAVResultResponseProps {
37-
permissions: string,
38-
fileid: number,
39-
size: number,
29+
/**
30+
* Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map`
31+
* @param node The node returned by the webdav library
32+
*/
33+
export const resultToNode = (node: FileStat): File | Folder => {
34+
return davResultToNode(node)
4035
}
4136

42-
export const resultToNode = function(node: FileStat): File | Folder {
43-
const userId = getCurrentUser()?.uid
44-
if (!userId) {
45-
throw new Error('No user id found')
46-
}
47-
48-
const props = node.props as ResponseProps
49-
const permissions = davParsePermissions(props?.permissions)
50-
const owner = String(props['owner-id'] || userId)
51-
52-
const source = generateRemoteUrl('dav' + rootPath + node.filename)
53-
const id = props?.fileid < 0
54-
? hashCode(source)
55-
: props?.fileid as number || 0
56-
57-
const nodeData = {
58-
id,
59-
source,
60-
mtime: new Date(node.lastmod),
61-
mime: node.mime || 'application/octet-stream',
62-
size: props?.size as number || 0,
63-
permissions,
64-
owner,
65-
root: rootPath,
66-
attributes: {
67-
...node,
68-
...props,
69-
'owner-id': owner,
70-
'owner-display-name': String(props['owner-display-name']),
71-
hasPreview: !!props?.['has-preview'],
72-
failed: props?.fileid < 0,
73-
},
74-
}
75-
76-
delete nodeData.attributes.props
77-
78-
return node.type === 'file'
79-
? new File(nodeData)
80-
: new Folder(nodeData)
81-
}
37+
const client = davGetClient()
8238

8339
export const getContents = (path = '/'): Promise<ContentsWithRoot> => {
40+
path = `${davRootPath}${path}`
8441
const controller = new AbortController()
8542
const propfindPayload = davGetDefaultPropfind()
8643

@@ -96,7 +53,7 @@ export const getContents = (path = '/'): Promise<ContentsWithRoot> => {
9653

9754
const root = contentsResponse.data[0]
9855
const contents = contentsResponse.data.slice(1)
99-
if (root.filename !== path) {
56+
if (root.filename !== path && `${root.filename}/` !== path) {
10057
throw new Error('Root node does not match requested path')
10158
}
10259

0 commit comments

Comments
 (0)