Merge pull request #46753 from nextcloud/fix/renaming-full-validation

fix(files): Use `@nextcloud/files` filename validation to show more details
This commit is contained in:
Andy Scherzinger 2024-07-25 18:28:13 +02:00 committed by GitHub
commit ba91f42c8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 94 additions and 85 deletions

View file

@ -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({
}
},
},
newName() {
// Check validity of the new name
const newName = this.newName.trim?.() || ''
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
if (!input) {
return
}
let validity = getFilenameValidity(newName)
// Checking if already exists
if (validity === '' && this.checkIfNodeExists(newName)) {
validity = t('files', 'Another entry with the same name already exists.')
}
this.$nextTick(() => {
if (this.isRenaming) {
input.setCustomValidity(validity)
input.reportValidity()
}
})
},
},
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
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()
}
},
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 }))
}
return true
},
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 newName = this.newName.trim?.() || ''
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
const newName = this.newName.trim?.() || ''
if (newName === '') {
showError(t('files', 'Name cannot be empty'))
return
}
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)

View file

@ -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.')
}
}
}

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long