diff options
author | Maksim Sukharev <antreesy.web@gmail.com> | 2024-07-22 18:02:41 +0200 |
---|---|---|
committer | Maksim Sukharev <antreesy.web@gmail.com> | 2024-07-24 18:33:41 +0200 |
commit | 9ed5c96e3aed3d27966e06041a4e899a91d34b00 (patch) | |
tree | 858c4c87a66f78b9b4efabdaaa91cd2617d4837b /apps | |
parent | 09eb3075ea2c3aa46b86e6d32fc3e7557220cca7 (diff) | |
download | nextcloud-server-9ed5c96e3aed3d27966e06041a4e899a91d34b00.tar.gz nextcloud-server-9ed5c96e3aed3d27966e06041a4e899a91d34b00.zip |
fix(files): validate input when creating file/directory
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/components/FileEntry/FileEntryName.vue | 15 | ||||
-rw-r--r-- | apps/files/src/components/NewNodeDialog.vue | 62 |
2 files changed, 67 insertions, 10 deletions
diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue index 4b387071a4a..5411d8657b9 100644 --- a/apps/files/src/components/FileEntry/FileEntryName.vue +++ b/apps/files/src/components/FileEntry/FileEntryName.vue @@ -223,25 +223,22 @@ export default Vue.extend({ }, isFileNameValid(name) { 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 (trimmedName.indexOf('/') !== -1) { - throw new Error(t('files', '"/" is not allowed inside a file name.')) + } else if (char) { + throw new Error(t('files', '"{char}" is not allowed inside a file name.', { char })) } else if (trimmedName.match(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 })) } - const toCheck = trimmedName.split('') - toCheck.forEach(char => { - if (forbiddenCharacters.indexOf(char) !== -1) { - throw new Error(this.t('files', '"{char}" is not allowed inside a file name.', { char })) - } - }) - return true }, checkIfNodeExists(name) { diff --git a/apps/files/src/components/NewNodeDialog.vue b/apps/files/src/components/NewNodeDialog.vue index 4087b58c607..46c39890dc9 100644 --- a/apps/files/src/components/NewNodeDialog.vue +++ b/apps/files/src/components/NewNodeDialog.vue @@ -34,10 +34,12 @@ </template> <form @submit.prevent="onCreate"> <NcTextField ref="input" + class="dialog__input" :error="!isUniqueName" :helper-text="errorMessage" :label="label" - :value.sync="localDefaultName" /> + :value.sync="localDefaultName" + @keyup="checkInputValidity" /> </form> </NcDialog> </template> @@ -48,15 +50,19 @@ import type { PropType } from 'vue' import { defineComponent } from 'vue' import { translate as t } from '@nextcloud/l10n' import { getUniqueName } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' +import logger from '../logger.js' interface ICanFocus { focus: () => void } +const forbiddenCharacters = loadState<string[]>('files', 'forbiddenCharacters', []) + export default defineComponent({ name: 'NewNodeDialog', components: { @@ -161,6 +167,60 @@ export default defineComponent({ this.$emit('close', null) } }, + + /** + * 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.localDefaultName.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 })) + } + + return true + }, }, }) </script> + +<style lang="scss" scoped> +.dialog__input { + :deep(input:invalid) { + // Show red border on invalid input + border-color: var(--color-error); + color: red; + } +} +</style> |