Skip to content

Commit

Permalink
Merge pull request #46753 from nextcloud/fix/renaming-full-validation
Browse files Browse the repository at this point in the history
fix(files): Use `@nextcloud/files` filename validation to show more details
  • Loading branch information
AndyScherzinger authored Jul 25, 2024
2 parents d5bb37a + c377205 commit ba91f42
Show file tree
Hide file tree
Showing 14 changed files with 88 additions and 79 deletions.
90 changes: 29 additions & 61 deletions apps/files/src/components/FileEntry/FileEntryName.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -16,7 +17,6 @@
:required="true"
:value.sync="newName"
enterkeyhint="done"
@keyup="checkInputValidity"
@keyup.esc="stopRenaming" />
</form>

Expand All @@ -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',
Expand Down Expand Up @@ -187,76 +185,51 @@ 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)
},
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
Expand All @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions apps/files/src/utils/filenameValidity.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 2 deletions dist/core-common.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/core-common.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/files-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-main.js.map

Large diffs are not rendered by default.

Loading

0 comments on commit ba91f42

Please sign in to comment.