aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-07-25 14:29:31 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-07-25 15:49:33 +0200
commit523e0d3f2b8bbeac041e7c8bfe463399fc0cb049 (patch)
treeb4af1a794c327010231b51758c865bdaab18a03a /apps
parent6953be7a0172b7f0d60f407c6b7516f108ad8931 (diff)
downloadnextcloud-server-523e0d3f2b8bbeac041e7c8bfe463399fc0cb049.tar.gz
nextcloud-server-523e0d3f2b8bbeac041e7c8bfe463399fc0cb049.zip
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 <opensource@fthiessen.de>
Diffstat (limited to 'apps')
-rw-r--r--apps/files/src/components/FileEntry/FileEntryName.vue90
-rw-r--r--apps/files/src/utils/filenameValidity.ts41
2 files changed, 70 insertions, 61 deletions
diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue
index 875c0892a72..be491db016f 100644
--- a/apps/files/src/components/FileEntry/FileEntryName.vue
+++ b/apps/files/src/components/FileEntry/FileEntryName.vue
@@ -5,6 +5,7 @@
<template>
<!-- Rename input -->
<form v-if="isRenaming"
+ ref="renameForm"
v-on-click-outside="onRename"
:aria-label="t('files', 'Rename file')"
class="files-list__row-rename"
@@ -16,7 +17,6 @@
:required="true"
:value.sync="newName"
enterkeyhint="done"
- @keyup="checkInputValidity"
@keyup.esc="stopRenaming" />
</form>
@@ -40,22 +40,20 @@
import type { Node } from '@nextcloud/files'
import type { PropType } from 'vue'
+import axios, { isAxiosError } from '@nextcloud/axios'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { FileType, NodeStatus, Permission } from '@nextcloud/files'
-import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
-import axios, { isAxiosError } from '@nextcloud/axios'
import { defineComponent } from 'vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import { useNavigation } from '../../composables/useNavigation'
import { useRenamingStore } from '../../store/renaming.ts'
+import { getFilenameValidity } from '../../utils/filenameValidity.ts'
import logger from '../../logger.js'
-const forbiddenCharacters = loadState<string[]>('files', 'forbiddenCharacters', [])
-
export default defineComponent({
name: 'FileEntryName',
@@ -187,55 +185,30 @@ export default defineComponent({
}
},
},
- },
- methods: {
- /**
- * Check if the file name is valid and update the
- * input validity using browser's native validation.
- * @param event the keyup event
- */
- checkInputValidity(event: KeyboardEvent) {
- const input = event.target as HTMLInputElement
+ newName() {
+ // Check validity of the new name
const newName = this.newName.trim?.() || ''
- logger.debug('Checking input validity', { newName })
- try {
- this.isFileNameValid(newName)
- input.setCustomValidity('')
- input.title = ''
- } catch (e) {
- if (e instanceof Error) {
- input.setCustomValidity(e.message)
- input.title = e.message
- } else {
- input.setCustomValidity(t('files', 'Invalid file name'))
- }
- } finally {
- input.reportValidity()
+ const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
+ if (!input) {
+ return
}
- },
- isFileNameValid(name: string) {
- const trimmedName = name.trim()
- const char = trimmedName.indexOf('/') !== -1
- ? '/'
- : forbiddenCharacters.find((char) => trimmedName.includes(char))
-
- if (trimmedName === '.' || trimmedName === '..') {
- throw new Error(t('files', '"{name}" is an invalid file name.', { name }))
- } else if (trimmedName.length === 0) {
- throw new Error(t('files', 'File name cannot be empty.'))
- } else if (char) {
- throw new Error(t('files', '"{char}" is not allowed inside a file name.', { char }))
- } else if (trimmedName.match(window.OC.config.blacklist_files_regex)) {
- throw new Error(t('files', '"{name}" is not an allowed filetype.', { name }))
- } else if (this.checkIfNodeExists(name)) {
- throw new Error(t('files', '{newName} already exists.', { newName: name }))
+ let validity = getFilenameValidity(newName)
+ // Checking if already exists
+ if (validity === '' && this.checkIfNodeExists(newName)) {
+ validity = t('files', 'Another entry with the same name already exists.')
}
-
- return true
+ this.$nextTick(() => {
+ if (this.isRenaming) {
+ input.setCustomValidity(validity)
+ input.reportValidity()
+ }
+ })
},
+ },
+ methods: {
checkIfNodeExists(name: string) {
return this.nodes.find(node => node.basename === name && node !== this.source)
},
@@ -243,20 +216,20 @@ export default defineComponent({
startRenaming() {
this.$nextTick(() => {
// Using split to get the true string length
- const extLength = (this.source.extension || '').split('').length
- const length = this.source.basename.split('').length - extLength
- const input = this.$refs.renameInput?.$refs?.inputField?.$refs?.input
+ const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
if (!input) {
logger.error('Could not find the rename input')
return
}
- input.setSelectionRange(0, length)
input.focus()
+ const length = this.source.basename.length - (this.source.extension ?? '').length
+ input.setSelectionRange(0, length)
// Trigger a keyup event to update the input validity
input.dispatchEvent(new Event('keyup'))
})
},
+
stopRenaming() {
if (!this.isRenaming) {
return
@@ -268,25 +241,20 @@ export default defineComponent({
// Rename and move the file
async onRename() {
- const oldName = this.source.basename
- const oldEncodedSource = this.source.encodedSource
const newName = this.newName.trim?.() || ''
- if (newName === '') {
- showError(t('files', 'Name cannot be empty'))
+ const form = this.$refs.renameForm as HTMLFormElement
+ if (!form.checkValidity()) {
+ showError(t('files', 'Invalid filename.') + ' ' + getFilenameValidity(newName))
return
}
+ const oldName = this.source.basename
+ const oldEncodedSource = this.source.encodedSource
if (oldName === newName) {
this.stopRenaming()
return
}
- // Checking if already exists
- if (this.checkIfNodeExists(newName)) {
- showError(t('files', 'Another entry with the same name already exists'))
- return
- }
-
// Set loading state
this.$set(this.source, 'status', NodeStatus.LOADING)
diff --git a/apps/files/src/utils/filenameValidity.ts b/apps/files/src/utils/filenameValidity.ts
new file mode 100644
index 00000000000..2666d530052
--- /dev/null
+++ b/apps/files/src/utils/filenameValidity.ts
@@ -0,0 +1,41 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { InvalidFilenameError, InvalidFilenameErrorReason, validateFilename } from '@nextcloud/files'
+import { t } from '@nextcloud/l10n'
+
+/**
+ * Get the validity of a filename (empty if valid).
+ * This can be used for `setCustomValidity` on input elements
+ * @param name The filename
+ * @param escape Escape the matched string in the error (only set when used in HTML)
+ */
+export function getFilenameValidity(name: string, escape = false): string {
+ if (name.trim() === '') {
+ return t('files', 'Filename must not be empty.')
+ }
+
+ try {
+ validateFilename(name)
+ return ''
+ } catch (error) {
+ if (!(error instanceof InvalidFilenameError)) {
+ throw error
+ }
+
+ switch (error.reason) {
+ case InvalidFilenameErrorReason.Character:
+ return t('files', '"{char}" is not allowed inside a filename.', { char: error.segment }, undefined, { escape })
+ case InvalidFilenameErrorReason.ReservedName:
+ return t('files', '"{segment}" is a reserved name and not allowed for filenames.', { segment: error.segment }, undefined, { escape: false })
+ case InvalidFilenameErrorReason.Extension:
+ if (error.segment.match(/\.[a-z]/i)) {
+ return t('files', '"{extension}" is not an allowed filetype.', { extension: error.segment }, undefined, { escape: false })
+ }
+ return t('files', 'Filenames must not end with "{extension}".', { extension: error.segment }, undefined, { escape: false })
+ default:
+ return t('files', 'Invalid filename.')
+ }
+ }
+}