Skip to content

Commit e7edbb8

Browse files
committed
fix(files): Use @nextcloud/files filename validation to show more details
This will enable showing more details what exactly is wrong with the filename. Especially with the new capabilities introduced with Nextcloud 30. Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent e989feb commit e7edbb8

2 files changed

Lines changed: 69 additions & 61 deletions

File tree

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

Lines changed: 29 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<template>
66
<!-- Rename input -->
77
<form v-if="isRenaming"
8+
ref="renameForm"
89
v-on-click-outside="onRename"
910
:aria-label="t('files', 'Rename file')"
1011
class="files-list__row-rename"
@@ -16,7 +17,6 @@
1617
:required="true"
1718
:value.sync="newName"
1819
enterkeyhint="done"
19-
@keyup="checkInputValidity"
2020
@keyup.esc="stopRenaming" />
2121
</form>
2222

@@ -40,22 +40,20 @@
4040
import type { Node } from '@nextcloud/files'
4141
import type { PropType } from 'vue'
4242
43+
import axios, { isAxiosError } from '@nextcloud/axios'
4344
import { showError, showSuccess } from '@nextcloud/dialogs'
4445
import { emit } from '@nextcloud/event-bus'
4546
import { FileType, NodeStatus, Permission } from '@nextcloud/files'
46-
import { loadState } from '@nextcloud/initial-state'
4747
import { translate as t } from '@nextcloud/l10n'
48-
import axios, { isAxiosError } from '@nextcloud/axios'
4948
import { defineComponent } from 'vue'
5049
5150
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
5251
5352
import { useNavigation } from '../../composables/useNavigation'
5453
import { useRenamingStore } from '../../store/renaming.ts'
54+
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
5555
import logger from '../../logger.js'
5656
57-
const forbiddenCharacters = loadState<string[]>('files', 'forbiddenCharacters', [])
58-
5957
export default defineComponent({
6058
name: 'FileEntryName',
6159
@@ -187,76 +185,51 @@ export default defineComponent({
187185
}
188186
},
189187
},
190-
},
191188
192-
methods: {
193-
/**
194-
* Check if the file name is valid and update the
195-
* input validity using browser's native validation.
196-
* @param event the keyup event
197-
*/
198-
checkInputValidity(event: KeyboardEvent) {
199-
const input = event.target as HTMLInputElement
189+
newName() {
190+
// Check validity of the new name
200191
const newName = this.newName.trim?.() || ''
201-
logger.debug('Checking input validity', { newName })
202-
try {
203-
this.isFileNameValid(newName)
204-
input.setCustomValidity('')
205-
input.title = ''
206-
} catch (e) {
207-
if (e instanceof Error) {
208-
input.setCustomValidity(e.message)
209-
input.title = e.message
210-
} else {
211-
input.setCustomValidity(t('files', 'Invalid file name'))
212-
}
213-
} finally {
214-
input.reportValidity()
192+
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
193+
if (!input) {
194+
return
215195
}
216-
},
217196
218-
isFileNameValid(name: string) {
219-
const trimmedName = name.trim()
220-
const char = trimmedName.indexOf('/') !== -1
221-
? '/'
222-
: forbiddenCharacters.find((char) => trimmedName.includes(char))
223-
224-
if (trimmedName === '.' || trimmedName === '..') {
225-
throw new Error(t('files', '"{name}" is an invalid file name.', { name }))
226-
} else if (trimmedName.length === 0) {
227-
throw new Error(t('files', 'File name cannot be empty.'))
228-
} else if (char) {
229-
throw new Error(t('files', '"{char}" is not allowed inside a file name.', { char }))
230-
} else if (trimmedName.match(window.OC.config.blacklist_files_regex)) {
231-
throw new Error(t('files', '"{name}" is not an allowed filetype.', { name }))
232-
} else if (this.checkIfNodeExists(name)) {
233-
throw new Error(t('files', '{newName} already exists.', { newName: name }))
197+
let validity = getFilenameValidity(newName)
198+
// Checking if already exists
199+
if (validity === '' && this.checkIfNodeExists(newName)) {
200+
validity = t('files', 'Another entry with the same name already exists.')
234201
}
235-
236-
return true
202+
this.$nextTick(() => {
203+
if (this.isRenaming) {
204+
input.setCustomValidity(validity)
205+
input.reportValidity()
206+
}
207+
})
237208
},
209+
},
238210
211+
methods: {
239212
checkIfNodeExists(name: string) {
240213
return this.nodes.find(node => node.basename === name && node !== this.source)
241214
},
242215
243216
startRenaming() {
244217
this.$nextTick(() => {
245218
// Using split to get the true string length
246-
const extLength = (this.source.extension || '').split('').length
247-
const length = this.source.basename.split('').length - extLength
248-
const input = this.$refs.renameInput?.$refs?.inputField?.$refs?.input
219+
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
249220
if (!input) {
250221
logger.error('Could not find the rename input')
251222
return
252223
}
253-
input.setSelectionRange(0, length)
254224
input.focus()
225+
const length = this.source.basename.length - (this.source.extension ?? '').length
226+
input.setSelectionRange(0, length)
255227
256228
// Trigger a keyup event to update the input validity
257229
input.dispatchEvent(new Event('keyup'))
258230
})
259231
},
232+
260233
stopRenaming() {
261234
if (!this.isRenaming) {
262235
return
@@ -268,25 +241,20 @@ export default defineComponent({
268241
269242
// Rename and move the file
270243
async onRename() {
271-
const oldName = this.source.basename
272-
const oldEncodedSource = this.source.encodedSource
273244
const newName = this.newName.trim?.() || ''
274-
if (newName === '') {
275-
showError(t('files', 'Name cannot be empty'))
245+
const form = this.$refs.renameForm as HTMLFormElement
246+
if (!form.checkValidity()) {
247+
showError(t('files', 'Invalid filename.') + ' ' + getFilenameValidity(newName))
276248
return
277249
}
278250
251+
const oldName = this.source.basename
252+
const oldEncodedSource = this.source.encodedSource
279253
if (oldName === newName) {
280254
this.stopRenaming()
281255
return
282256
}
283257
284-
// Checking if already exists
285-
if (this.checkIfNodeExists(newName)) {
286-
showError(t('files', 'Another entry with the same name already exists'))
287-
return
288-
}
289-
290258
// Set loading state
291259
this.$set(this.source, 'status', NodeStatus.LOADING)
292260
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
import { InvalidFilenameError, InvalidFilenameErrorReason, validateFilename } from '@nextcloud/files'
6+
import { t } from '@nextcloud/l10n'
7+
8+
/**
9+
* Get the validity of a filename (empty if valid).
10+
* This can be used for `setCustomValidity` on input elements
11+
* @param name The filename
12+
*/
13+
export function getFilenameValidity(name: string): string {
14+
if (name.trim() === '') {
15+
return t('files', 'Filename must not be empty.')
16+
}
17+
18+
try {
19+
validateFilename(name)
20+
return ''
21+
} catch (error) {
22+
if (!(error instanceof InvalidFilenameError)) {
23+
throw error
24+
}
25+
26+
switch (error.reason) {
27+
case InvalidFilenameErrorReason.Character:
28+
return t('files', '"{char}" is not allowed inside a filename.', { char: error.segment })
29+
case InvalidFilenameErrorReason.ReservedName:
30+
return t('files', '"{segment}" is a reserved name and not allowed for filenames.', { segment: error.segment })
31+
case InvalidFilenameErrorReason.Extension:
32+
if (error.segment.match(/\.[a-z]/i)) {
33+
return t('files', '"{extension}" is not an allowed filetype.', { extension: error.segment })
34+
}
35+
return t('files', 'Filenames must not end with "{extension}".', { extension: error.segment })
36+
default:
37+
return t('files', 'Invalid filename.')
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)