diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-07-25 14:29:31 +0200 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-07-25 15:49:33 +0200 |
commit | 523e0d3f2b8bbeac041e7c8bfe463399fc0cb049 (patch) | |
tree | b4af1a794c327010231b51758c865bdaab18a03a /apps | |
parent | 6953be7a0172b7f0d60f407c6b7516f108ad8931 (diff) | |
download | nextcloud-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.vue | 90 | ||||
-rw-r--r-- | apps/files/src/utils/filenameValidity.ts | 41 |
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.') + } + } +} |