From f2f28a02620da127734a6ef72f05976fa75b21b3 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 19 Aug 2024 10:43:40 +0200 Subject: [PATCH 01/65] solve git conflict Signed-off-by: alperozturk --- .../FileNameValidationResult.kt | 17 ++++++++ .../fileNameValidator/FileNameValidator.kt | 42 +++++++++++++++++++ app/src/main/res/values/strings.xml | 5 +++ 3 files changed, 64 insertions(+) create mode 100644 app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidationResult.kt create mode 100644 app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidationResult.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidationResult.kt new file mode 100644 index 000000000000..849ba4a71d60 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidationResult.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.fileNameValidator + +import com.owncloud.android.R + +enum class FileNameValidationResult(val messageId: Int) { + EMPTY(R.string.filename_empty), + INVALID_CHARACTER(R.string.file_name_validator_error_invalid_character), + RESERVED_NAME(R.string.file_name_validator_error_reserved_names), + ENDS_WITH_SPACE_OR_PERIOD(R.string.file_name_validator_error_ends_with_space_period) +} diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt new file mode 100644 index 000000000000..49142210b53c --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.fileNameValidator + +import android.text.TextUtils + +object FileNameValidator { + private val reservedWindowsChars = "[<>:\"/\\\\|?*]".toRegex() + private val reservedWindowsNames = listOf( + "CON", "PRN", "AUX", "NUL", + "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "COM¹", "COM²", "COM³", + "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", + "LPT¹", "LPT²", "LPT³" + ) + private val reservedUnixChars = "[/<>|:&]".toRegex() + + fun isValid(name: String): FileNameValidationResult? { + if (name.contains(reservedWindowsChars) || name.contains(reservedUnixChars)) { + return FileNameValidationResult.INVALID_CHARACTER + } + + if (reservedWindowsNames.contains(name.uppercase())) { + return FileNameValidationResult.RESERVED_NAME + } + + if (name.endsWith(" ") || name.endsWith(".")) { + return FileNameValidationResult.ENDS_WITH_SPACE_OR_PERIOD + } + + if (TextUtils.isEmpty(name)) { + return FileNameValidationResult.EMPTY + } + + return null + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c06a52d4527..310ce9b1c2fa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1214,7 +1214,12 @@ Secure sharing is not set up for this user Resharing is not allowed during secure file drop No file or folder matching your search + Event not found, you can always sync to update. Redirecting to web… Contact not found, you can always sync to update. Redirecting to web… Permissions are required to open search result otherwise it will redirected to web… + + File name contains invalid characters + File name is a reserved name + File name ends with a space or a period From 90c495ff5eb89a8a7fce944211a03978e2938662 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 11:11:37 +0200 Subject: [PATCH 02/65] Use New Validator inside CreateFolderDialogFragment Signed-off-by: alperozturk --- .../ui/dialog/CreateFolderDialogFragment.kt | 117 ++++++++---------- 1 file changed, 55 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index 463ea2c40aef..acb9df079f37 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -14,7 +14,6 @@ import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import android.text.Editable -import android.text.TextUtils import android.text.TextWatcher import android.view.View import android.widget.TextView @@ -25,11 +24,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.common.collect.Sets import com.nextcloud.client.di.Injectable import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.R import com.owncloud.android.databinding.EditBoxDialogBinding import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.resources.files.FileUtils import com.owncloud.android.ui.activity.ComponentsGetter import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.KeyboardUtils @@ -43,17 +42,16 @@ import javax.inject.Inject * Triggers the folder creation when name is confirmed. */ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickListener, Injectable { - @JvmField + @Inject - var fileDataStorageManager: FileDataStorageManager? = null + lateinit var fileDataStorageManager: FileDataStorageManager - @JvmField @Inject - var viewThemeUtils: ViewThemeUtils? = null + lateinit var viewThemeUtils: ViewThemeUtils - @JvmField @Inject - var keyboardUtils: KeyboardUtils? = null + lateinit var keyboardUtils: KeyboardUtils + private var mParentFolder: OCFile? = null private var positiveButton: MaterialButton? = null @@ -71,105 +69,98 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton val negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton - viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton!!) - viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton) + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton!!) + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton) } } override fun onResume() { super.onResume() bindButton() - keyboardUtils!!.showKeyboardForEditText(requireDialog().window, binding.userInput) + keyboardUtils.showKeyboardForEditText(requireDialog().window, binding.userInput) } @Suppress("EmptyFunctionBlock") override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { mParentFolder = arguments?.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java) - // Inflate the layout for the dialog val inflater = requireActivity().layoutInflater binding = EditBoxDialogBinding.inflate(inflater, null, false) val view: View = binding.root - // Setup layout binding.userInput.setText(R.string.empty) - viewThemeUtils?.material?.colorTextInputLayout(binding.userInputContainer) + viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer) val parentFolder = requireArguments().getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java) - val folderContent = fileDataStorageManager!!.getFolderContent(parentFolder, false) + val folderContent = fileDataStorageManager.getFolderContent(parentFolder, false) val fileNames: MutableSet = Sets.newHashSetWithExpectedSize(folderContent.size) for (file in folderContent) { fileNames.add(file.fileName) } - // Add TextChangedListener to handle showing/hiding the input warning message binding.userInput.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable) {} override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} - - /** - * When user enters a hidden file name, the 'hidden file' message is shown. Otherwise, - * the message is ensured to be hidden. - */ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - var newFileName = "" - if (binding.userInput.text != null) { - newFileName = binding.userInput.text.toString().trim { it <= ' ' } - } - if (!TextUtils.isEmpty(newFileName) && newFileName[0] == '.') { - binding.userInputContainer.error = getText(R.string.hidden_file_name_warning) - } else if (TextUtils.isEmpty(newFileName)) { - binding.userInputContainer.error = getString(R.string.filename_empty) - if (positiveButton == null) { - bindButton() - } - positiveButton!!.isEnabled = false - } else if (!FileUtils.isValidName(newFileName)) { - binding.userInputContainer.error = getString(R.string.filename_forbidden_charaters_from_server) - positiveButton!!.isEnabled = false - } else if (fileNames.contains(newFileName)) { - binding.userInputContainer.error = getText(R.string.file_already_exists) - positiveButton!!.isEnabled = false - } else if (binding.userInputContainer.error != null) { - binding.userInputContainer.error = null - // Called to remove extra padding - binding.userInputContainer.isErrorEnabled = false - positiveButton!!.isEnabled = true - } + checkFileNameAfterEachType(fileNames) } }) - // Build the dialog val builder = buildMaterialAlertDialog(view) - viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(binding.userInputContainer.context, builder) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.context, builder) return builder.create() } + private fun checkFileNameAfterEachType(fileNames: MutableSet) { + val newFileName = binding.userInput.text?.toString()?.trim() ?: "" + val errorMessageId: Int? = FileNameValidator.isValid(newFileName)?.messageId + + val error = when { + newFileName.isEmpty() -> null + newFileName[0] == '.' -> R.string.hidden_file_name_warning + errorMessageId != null -> errorMessageId + fileNames.contains(newFileName) -> R.string.file_already_exists + else -> null + } + + if (error != null) { + binding.userInputContainer.error = getString(error) + positiveButton?.isEnabled = false + if (positiveButton == null) { + bindButton() + } + } else { + binding.userInputContainer.error = null + binding.userInputContainer.isErrorEnabled = false + positiveButton?.isEnabled = true + } + } + private fun buildMaterialAlertDialog(view: View): MaterialAlertDialogBuilder { - val builder = MaterialAlertDialogBuilder(requireActivity()) - builder + return MaterialAlertDialogBuilder(requireActivity()) .setView(view) .setPositiveButton(R.string.folder_confirm_create, this) .setNegativeButton(R.string.common_cancel, this) .setTitle(R.string.uploader_info_dirname) - return builder } override fun onClick(dialog: DialogInterface, which: Int) { if (which == AlertDialog.BUTTON_POSITIVE) { val newFolderName = (getDialog()!!.findViewById(R.id.user_input) as TextView) .text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(newFolderName)) { - DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_empty) + + val errorMessageId: Int? = FileNameValidator.isValid(newFolderName)?.messageId + + if (errorMessageId != null) { + DisplayUtils.showSnackMessage(requireActivity(), errorMessageId) return } - if (!FileUtils.isValidName(newFolderName)) { - DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_forbidden_charaters_from_server) - return + + val path = mParentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR + if (requireActivity() is ComponentsGetter) { + (requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path) } - val path = mParentFolder!!.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR - (requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path) } } @@ -185,11 +176,13 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList */ @JvmStatic fun newInstance(parentFolder: OCFile?): CreateFolderDialogFragment { - val frag = CreateFolderDialogFragment() - val args = Bundle() - args.putParcelable(ARG_PARENT_FOLDER, parentFolder) - frag.arguments = args - return frag + val bundle = Bundle().apply { + putParcelable(ARG_PARENT_FOLDER, parentFolder) + } + + return CreateFolderDialogFragment().apply { + arguments = bundle + } } } } From 52d258ae9d6689cae8fb1f1af91ac4807dac26aa Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 11:31:19 +0200 Subject: [PATCH 03/65] Use New Validator inside RenameFileDialogFragment Signed-off-by: alperozturk --- .../ui/dialog/CreateFolderDialogFragment.kt | 7 +++++-- .../ui/dialog/RenameFileDialogFragment.java | 21 ++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index acb9df079f37..e4f311801562 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -69,7 +69,10 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton val negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton - viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton!!) + positiveButton?.let { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it) + } + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton) } } @@ -147,7 +150,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList override fun onClick(dialog: DialogInterface, which: Int) { if (which == AlertDialog.BUTTON_POSITIVE) { - val newFolderName = (getDialog()!!.findViewById(R.id.user_input) as TextView) + val newFolderName = (getDialog()?.findViewById(R.id.user_input) as TextView) .text.toString().trim { it <= ' ' } val errorMessageId: Int? = FileNameValidator.isValid(newFolderName)?.messageId diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java index e2817f99227f..c4441475ff23 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java @@ -24,6 +24,8 @@ import com.google.common.collect.Sets; import com.nextcloud.client.di.Injectable; import com.nextcloud.utils.extensions.BundleExtensionsKt; +import com.nextcloud.utils.fileNameValidator.FileNameValidationResult; +import com.nextcloud.utils.fileNameValidator.FileNameValidator; import com.owncloud.android.R; import com.owncloud.android.databinding.EditBoxDialogBinding; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -159,18 +161,15 @@ public void onClick(DialogInterface dialog, int which) { newFileName = binding.userInput.getText().toString().trim(); } - if (TextUtils.isEmpty(newFileName)) { - DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_empty); + FileNameValidationResult fileNameValidationResult = FileNameValidator.INSTANCE.isValid(newFileName); + if (fileNameValidationResult != null) { + DisplayUtils.showSnackMessage(requireActivity(), fileNameValidationResult.getMessageId()); return; } - if (!FileUtils.isValidName(newFileName)) { - DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_forbidden_charaters_from_server); - - return; + if (requireActivity() instanceof ComponentsGetter componentsGetter) { + componentsGetter.getFileOperationsHelper().renameFile(mTargetFile, newFileName); } - - ((ComponentsGetter) requireActivity()).getFileOperationsHelper().renameFile(mTargetFile, newFileName); } } @@ -196,10 +195,12 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { newFileName = binding.userInput.getText().toString().trim(); } + FileNameValidationResult fileNameValidationResult = FileNameValidator.INSTANCE.isValid(newFileName); + if (!TextUtils.isEmpty(newFileName) && newFileName.charAt(0) == '.') { binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning)); - } else if (TextUtils.isEmpty(newFileName)) { - binding.userInputContainer.setError(getString(R.string.filename_empty)); + } else if (fileNameValidationResult != null) { + binding.userInputContainer.setError(getString(fileNameValidationResult.getMessageId())); positiveButton.setEnabled(false); } else if (fileNames.contains(newFileName)) { binding.userInputContainer.setError(getText(R.string.file_already_exists)); From 47a4ac4191f7742d9f1f64c3a55b7960b5707378 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 12:04:30 +0200 Subject: [PATCH 04/65] Use New Validator inside ChooseTemplateDialogFragment Signed-off-by: alperozturk --- .../ui/dialog/ChooseTemplateDialogFragment.kt | 241 +++++++++--------- 1 file changed, 120 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index 9f461f7aa0ee..fc0a2e46223d 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2023 TSI-mc * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2019 Chris Narkiewicz @@ -10,6 +11,7 @@ */ package com.owncloud.android.ui.dialog +import android.annotation.SuppressLint import android.app.Dialog import android.content.Intent import android.os.AsyncTask @@ -30,6 +32,7 @@ import com.nextcloud.client.di.Injectable import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.ChooseTemplateBinding @@ -79,12 +82,6 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem private var positiveButton: MaterialButton? = null private var creator: Creator? = null - enum class Type { - DOCUMENT, - SPREADSHEET, - PRESENTATION - } - private var _binding: ChooseTemplateBinding? = null val binding get() = _binding!! @@ -92,18 +89,21 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem super.onStart() val alertDialog = dialog as AlertDialog - val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton - viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton) - - val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton) + val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton + negativeButton?.let { + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton) + } - positiveButton.setOnClickListener(this) - positiveButton.isEnabled = false - positiveButton.isClickable = false + val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton + positiveButton?.let { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton) + positiveButton.setOnClickListener(this) + positiveButton.isEnabled = false + positiveButton.isClickable = false + this.positiveButton = positiveButton + } - this.positiveButton = positiveButton - checkEnablingCreateButton() + checkFileNameAfterEachType() } override fun onResume() { @@ -126,10 +126,8 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem fileNames = fileDataStorageManager.getFolderContent(parentFolder, false).map { it.fileName } - // Inflate the layout for the dialog val inflater = requireActivity().layoutInflater _binding = ChooseTemplateBinding.inflate(inflater, null, false) - val view: View = binding.root viewThemeUtils.material.colorTextInputLayout( binding.filenameContainer @@ -137,13 +135,9 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem binding.filename.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit - - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - // not needed - } - + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit override fun afterTextChanged(s: Editable) { - checkEnablingCreateButton() + checkFileNameAfterEachType() } }) @@ -152,7 +146,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem binding.list.setHasFixedSize(true) binding.list.layoutManager = GridLayoutManager(activity, 2) adapter = TemplateAdapter( - creator!!.mimetype, + creator?.mimetype, this, context, currentAccount, @@ -161,9 +155,8 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem ) binding.list.adapter = adapter - // Build the dialog val builder = MaterialAlertDialogBuilder(activity) - builder.setView(view) + .setView(binding.root) .setPositiveButton(R.string.create, null) .setNegativeButton(R.string.common_cancel, null) .setTitle(title) @@ -197,6 +190,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem CreateFileFromTemplateTask(this, clientFactory, currentAccount.user, template, path, creator).execute() } + @SuppressLint("NotifyDataSetChanged") fun setTemplateList(templateList: TemplateList?) { adapter?.setTemplateList(templateList) adapter?.notifyDataSetChanged() @@ -207,9 +201,9 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem } private fun onTemplateChosen(template: Template) { - adapter!!.setTemplateAsActive(template) + adapter?.setTemplateAsActive(template) prefillFilenameIfEmpty(template) - checkEnablingCreateButton() + checkFileNameAfterEachType() } private fun prefillFilenameIfEmpty(template: Template) { @@ -224,13 +218,16 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem override fun onClick(v: View) { val name = binding.filename.text.toString() - val path = parentFolder!!.remotePath + name + val path = parentFolder?.remotePath + name + val selectedTemplate = adapter?.selectedTemplate - val selectedTemplate = adapter!!.selectedTemplate + val errorMessageId: Int? = FileNameValidator.isValid(name)?.messageId if (selectedTemplate == null) { DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template) - } else if (name.isEmpty() || name.equals(DOT + selectedTemplate.extension, ignoreCase = true)) { + } else if (errorMessageId != null) { + DisplayUtils.showSnackMessage(requireActivity(), errorMessageId) + } else if (name.equals(DOT + selectedTemplate.extension, ignoreCase = true)) { DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename) } else if (!name.endsWith(selectedTemplate.extension)) { createFromTemplate(selectedTemplate, path + DOT + selectedTemplate.extension) @@ -239,56 +236,55 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem } } - private fun checkEnablingCreateButton() { - if (positiveButton != null) { - val selectedTemplate = adapter!!.selectedTemplate - val name = binding.filename.text.toString().trim() - val isNameJustExtension = selectedTemplate != null && name.equals( - DOT + selectedTemplate.extension, - ignoreCase = true - ) - val isNameEmpty = name.isEmpty() || isNameJustExtension - val state = selectedTemplate != null && !isNameEmpty && !fileNames.contains(name) - - positiveButton?.isEnabled = state - positiveButton?.isClickable = state - binding.filenameContainer.isErrorEnabled = !state - - if (!state) { - if (isNameEmpty) { - binding.filenameContainer.error = getText(R.string.filename_empty) - } else { - binding.filenameContainer.error = getText(R.string.file_already_exists) - } - } + private fun checkFileNameAfterEachType() { + if (positiveButton == null) return + + val selectedTemplate = adapter?.selectedTemplate + val name = binding.filename.text.toString().trim() + val isNameJustExtension = selectedTemplate != null && name.equals( + DOT + selectedTemplate.extension, + ignoreCase = true + ) + val errorMessageId: Int? = FileNameValidator.isValid(name)?.messageId + val error = when { + name.isEmpty() || isNameJustExtension -> null + name[0] == '.' -> R.string.hidden_file_name_warning + errorMessageId != null -> errorMessageId + fileNames.contains(name) -> R.string.file_already_exists + else -> null + } + + positiveButton?.isEnabled = (error == null) + positiveButton?.isClickable = (error == null) + binding.filenameContainer.isErrorEnabled = (error != null) + if (error != null) { + binding.filenameContainer.error = getText(error) } } - @Suppress("LongParameterList") // legacy code + @Suppress("LongParameterList", "DEPRECATION") private class CreateFileFromTemplateTask( chooseTemplateDialogFragment: ChooseTemplateDialogFragment, private val clientFactory: ClientFactory?, - user: User, - template: Template, - path: String, - creator: Creator? - ) : AsyncTask() { - private val chooseTemplateDialogFragmentWeakReference: WeakReference - private val template: Template - private val path: String + private val user: User, + private val template: Template, + private val path: String, private val creator: Creator? - private val user: User + ) : AsyncTask() { + private val chooseTemplateDialogFragmentWeakReference: WeakReference = + WeakReference(chooseTemplateDialogFragment) private var file: OCFile? = null + @Deprecated("Deprecated in Java") @Suppress("ReturnCount") // legacy code override fun doInBackground(vararg params: Void): String { return try { - val client = clientFactory!!.create(user) + val client = clientFactory?.create(user) ?: return "" val nextcloudClient = clientFactory.createNextcloudClient(user) val result = DirectEditingCreateFileRemoteOperation( path, - creator!!.editor, - creator.id, + creator?.editor, + creator?.id, template.title ).execute(nextcloudClient) if (!result.isSuccess) { @@ -316,51 +312,51 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem } } + @Deprecated("Deprecated in Java") override fun onPostExecute(url: String) { val fragment = chooseTemplateDialogFragmentWeakReference.get() - if (fragment != null && fragment.isAdded) { - if (url.isEmpty()) { - DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_creating_file_from_template) - } else { - val editorWebView = Intent(MainApp.getAppContext(), TextEditorWebView::class.java) - editorWebView.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text") - editorWebView.putExtra(ExternalSiteWebView.EXTRA_URL, url) - editorWebView.putExtra(ExternalSiteWebView.EXTRA_FILE, file) - editorWebView.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) - fragment.startActivity(editorWebView) - fragment.dismiss() - } - } else { + if (fragment == null || !fragment.isAdded) { Log_OC.e(TAG, "Error creating file from template!") + return } - } - init { - chooseTemplateDialogFragmentWeakReference = WeakReference(chooseTemplateDialogFragment) - this.template = template - this.path = path - this.creator = creator - this.user = user + if (url.isEmpty()) { + DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_creating_file_from_template) + return + } + + val editorWebView = Intent(MainApp.getAppContext(), TextEditorWebView::class.java).apply { + putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text") + putExtra(ExternalSiteWebView.EXTRA_URL, url) + putExtra(ExternalSiteWebView.EXTRA_FILE, file) + putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) + } + + fragment.run { + startActivity(editorWebView) + dismiss() + } } } + @Suppress("DEPRECATION") private class FetchTemplateTask( chooseTemplateDialogFragment: ChooseTemplateDialogFragment, private val clientFactory: ClientFactory?, private val user: User, - creator: Creator? - ) : AsyncTask() { - private val chooseTemplateDialogFragmentWeakReference: WeakReference private val creator: Creator? + ) : AsyncTask() { + private val chooseTemplateDialogFragmentWeakReference: WeakReference = + WeakReference(chooseTemplateDialogFragment) + @Deprecated("Deprecated in Java") override fun doInBackground(vararg voids: Void): TemplateList { return try { - val client = clientFactory!!.createNextcloudClient(user) + val client = clientFactory?.createNextcloudClient(user) ?: return TemplateList() val result = DirectEditingObtainListOfTemplatesRemoteOperation( - creator!!.editor, - creator.id - ) - .execute(client) + creator?.editor, + creator?.id + ).execute(client) if (!result.isSuccess) { TemplateList() } else { @@ -372,30 +368,31 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem } } + @Deprecated("Deprecated in Java") override fun onPostExecute(templateList: TemplateList) { val fragment = chooseTemplateDialogFragmentWeakReference.get() - if (fragment != null && fragment.isAdded) { - if (templateList.templates.isEmpty()) { - DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_retrieving_templates) - } else { - if (templateList.templates.size == SINGLE_TEMPLATE) { - fragment.onTemplateChosen(templateList.templates.values.iterator().next()) - fragment.binding.list.visibility = View.GONE - } else { - val name = DOT + templateList.templates.values.iterator().next().extension - fragment.binding.filename.setText(name) - fragment.binding.helperText.visibility = View.VISIBLE - } - fragment.setTemplateList(templateList) - } - } else { + if (fragment == null || !fragment.isAdded) { Log_OC.e(TAG, "Error streaming file: no previewMediaFragment!") + return } - } - init { - chooseTemplateDialogFragmentWeakReference = WeakReference(chooseTemplateDialogFragment) - this.creator = creator + if (templateList.templates.isEmpty()) { + DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_retrieving_templates) + return + } + + fragment.run { + if (templateList.templates.size == SINGLE_TEMPLATE) { + onTemplateChosen(templateList.templates.values.iterator().next()) + binding.list.visibility = View.GONE + } else { + val name = DOT + templateList.templates.values.iterator().next().extension + binding.filename.setText(name) + binding.helperText.visibility = View.VISIBLE + } + + setTemplateList(templateList) + } } } @@ -409,13 +406,15 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem @JvmStatic fun newInstance(parentFolder: OCFile?, creator: Creator?, headline: String?): ChooseTemplateDialogFragment { - val frag = ChooseTemplateDialogFragment() - val args = Bundle() - args.putParcelable(ARG_PARENT_FOLDER, parentFolder) - args.putParcelable(ARG_CREATOR, creator) - args.putString(ARG_HEADLINE, headline) - frag.arguments = args - return frag + val bundle = Bundle().apply { + putParcelable(ARG_PARENT_FOLDER, parentFolder) + putParcelable(ARG_CREATOR, creator) + putString(ARG_HEADLINE, headline) + } + + return ChooseTemplateDialogFragment().apply { + arguments = bundle + } } } } From 5ae25ec56df53e99ed316fc7e5e7ed46d8168403 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 14:21:09 +0200 Subject: [PATCH 05/65] Invalid characters added into error message Signed-off-by: alperozturk --- .../FileNameValidationResult.kt | 17 ----------------- .../fileNameValidator/FileNameValidator.kt | 17 ++++++++++------- .../ui/dialog/ChooseTemplateDialogFragment.kt | 17 +++++++++-------- .../ui/dialog/CreateFolderDialogFragment.kt | 16 ++++++++-------- .../ui/dialog/RenameFileDialogFragment.java | 14 ++++++-------- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 34 insertions(+), 49 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidationResult.kt diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidationResult.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidationResult.kt deleted file mode 100644 index 849ba4a71d60..000000000000 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidationResult.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.utils.fileNameValidator - -import com.owncloud.android.R - -enum class FileNameValidationResult(val messageId: Int) { - EMPTY(R.string.filename_empty), - INVALID_CHARACTER(R.string.file_name_validator_error_invalid_character), - RESERVED_NAME(R.string.file_name_validator_error_reserved_names), - ENDS_WITH_SPACE_OR_PERIOD(R.string.file_name_validator_error_ends_with_space_period) -} diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 49142210b53c..fac6415e6b7a 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -7,10 +7,13 @@ package com.nextcloud.utils.fileNameValidator +import android.content.Context import android.text.TextUtils +import com.owncloud.android.R object FileNameValidator { private val reservedWindowsChars = "[<>:\"/\\\\|?*]".toRegex() + private val reservedUnixChars = "[/<>|:&]".toRegex() private val reservedWindowsNames = listOf( "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", @@ -18,23 +21,23 @@ object FileNameValidator { "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³" ) - private val reservedUnixChars = "[/<>|:&]".toRegex() - fun isValid(name: String): FileNameValidationResult? { - if (name.contains(reservedWindowsChars) || name.contains(reservedUnixChars)) { - return FileNameValidationResult.INVALID_CHARACTER + fun isValid(name: String, context: Context): String? { + val invalidCharacter = name.find { it.toString().matches(reservedWindowsChars) || it.toString().matches(reservedUnixChars) } + if (invalidCharacter != null) { + return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) } if (reservedWindowsNames.contains(name.uppercase())) { - return FileNameValidationResult.RESERVED_NAME + return context.getString(R.string.file_name_validator_error_reserved_names) } if (name.endsWith(" ") || name.endsWith(".")) { - return FileNameValidationResult.ENDS_WITH_SPACE_OR_PERIOD + return context.getString(R.string.file_name_validator_error_ends_with_space_period) } if (TextUtils.isEmpty(name)) { - return FileNameValidationResult.EMPTY + return context.getString(R.string.filename_empty) } return null diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index fc0a2e46223d..a405d112f0db 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -221,12 +221,12 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem val path = parentFolder?.remotePath + name val selectedTemplate = adapter?.selectedTemplate - val errorMessageId: Int? = FileNameValidator.isValid(name)?.messageId + val errorMessage = FileNameValidator.isValid(name, requireContext()) if (selectedTemplate == null) { DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template) - } else if (errorMessageId != null) { - DisplayUtils.showSnackMessage(requireActivity(), errorMessageId) + } else if (errorMessage != null) { + DisplayUtils.showSnackMessage(requireActivity(), errorMessage) } else if (name.equals(DOT + selectedTemplate.extension, ignoreCase = true)) { DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename) } else if (!name.endsWith(selectedTemplate.extension)) { @@ -245,12 +245,13 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem DOT + selectedTemplate.extension, ignoreCase = true ) - val errorMessageId: Int? = FileNameValidator.isValid(name)?.messageId + val errorMessage = FileNameValidator.isValid(name, requireContext()) + val error = when { name.isEmpty() || isNameJustExtension -> null - name[0] == '.' -> R.string.hidden_file_name_warning - errorMessageId != null -> errorMessageId - fileNames.contains(name) -> R.string.file_already_exists + name[0] == '.' -> getText(R.string.hidden_file_name_warning) + errorMessage != null -> errorMessage + fileNames.contains(name) -> getText(R.string.file_already_exists) else -> null } @@ -258,7 +259,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem positiveButton?.isClickable = (error == null) binding.filenameContainer.isErrorEnabled = (error != null) if (error != null) { - binding.filenameContainer.error = getText(error) + binding.filenameContainer.error = error } } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index e4f311801562..2aa4d244bdf6 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -117,18 +117,18 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList private fun checkFileNameAfterEachType(fileNames: MutableSet) { val newFileName = binding.userInput.text?.toString()?.trim() ?: "" - val errorMessageId: Int? = FileNameValidator.isValid(newFileName)?.messageId + val errorMessage: String? = FileNameValidator.isValid(newFileName, requireContext()) val error = when { newFileName.isEmpty() -> null - newFileName[0] == '.' -> R.string.hidden_file_name_warning - errorMessageId != null -> errorMessageId - fileNames.contains(newFileName) -> R.string.file_already_exists + newFileName[0] == '.' -> getString(R.string.hidden_file_name_warning) + errorMessage != null -> errorMessage + fileNames.contains(newFileName) -> getString(R.string.file_already_exists) else -> null } if (error != null) { - binding.userInputContainer.error = getString(error) + binding.userInputContainer.error = error positiveButton?.isEnabled = false if (positiveButton == null) { bindButton() @@ -153,10 +153,10 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList val newFolderName = (getDialog()?.findViewById(R.id.user_input) as TextView) .text.toString().trim { it <= ' ' } - val errorMessageId: Int? = FileNameValidator.isValid(newFolderName)?.messageId + val errorMessage: String? = FileNameValidator.isValid(newFolderName, requireContext()) - if (errorMessageId != null) { - DisplayUtils.showSnackMessage(requireActivity(), errorMessageId) + if (errorMessage != null) { + DisplayUtils.showSnackMessage(requireActivity(), errorMessage) return } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java index c4441475ff23..bbde6a529061 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java @@ -24,13 +24,11 @@ import com.google.common.collect.Sets; import com.nextcloud.client.di.Injectable; import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.nextcloud.utils.fileNameValidator.FileNameValidationResult; import com.nextcloud.utils.fileNameValidator.FileNameValidator; import com.owncloud.android.R; import com.owncloud.android.databinding.EditBoxDialogBinding; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.resources.files.FileUtils; import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.KeyboardUtils; @@ -161,9 +159,9 @@ public void onClick(DialogInterface dialog, int which) { newFileName = binding.userInput.getText().toString().trim(); } - FileNameValidationResult fileNameValidationResult = FileNameValidator.INSTANCE.isValid(newFileName); - if (fileNameValidationResult != null) { - DisplayUtils.showSnackMessage(requireActivity(), fileNameValidationResult.getMessageId()); + String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, requireContext()); + if (errorMessage != null) { + DisplayUtils.showSnackMessage(requireActivity(), errorMessage); return; } @@ -195,12 +193,12 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { newFileName = binding.userInput.getText().toString().trim(); } - FileNameValidationResult fileNameValidationResult = FileNameValidator.INSTANCE.isValid(newFileName); + String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, requireContext()); if (!TextUtils.isEmpty(newFileName) && newFileName.charAt(0) == '.') { binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning)); - } else if (fileNameValidationResult != null) { - binding.userInputContainer.setError(getString(fileNameValidationResult.getMessageId())); + } else if (errorMessage != null) { + binding.userInputContainer.setError(errorMessage); positiveButton.setEnabled(false); } else if (fileNames.contains(newFileName)) { binding.userInputContainer.setError(getText(R.string.file_already_exists)); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 310ce9b1c2fa..ffad679425fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1219,7 +1219,7 @@ Contact not found, you can always sync to update. Redirecting to web… Permissions are required to open search result otherwise it will redirected to web… - File name contains invalid characters + File name contains invalid characters %s File name is a reserved name File name ends with a space or a period From b5e7946569154ffafb1b5b3368b7b1fbd3d7bc56 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 15:03:19 +0200 Subject: [PATCH 06/65] Remove duplicated logics Signed-off-by: alperozturk --- .../fileNameValidator/FileNameValidator.kt | 10 +++++++- .../ui/dialog/ChooseTemplateDialogFragment.kt | 23 ++++++++++++------- .../ui/dialog/CreateFolderDialogFragment.kt | 8 ++++--- .../ui/dialog/RenameFileDialogFragment.java | 9 +++----- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index fac6415e6b7a..c4cd6389d3e9 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -22,7 +22,7 @@ object FileNameValidator { "LPT¹", "LPT²", "LPT³" ) - fun isValid(name: String, context: Context): String? { + fun isValid(name: String, context: Context, fileNames: MutableSet? = null): String? { val invalidCharacter = name.find { it.toString().matches(reservedWindowsChars) || it.toString().matches(reservedUnixChars) } if (invalidCharacter != null) { return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) @@ -40,6 +40,14 @@ object FileNameValidator { return context.getString(R.string.filename_empty) } + if (isFileNameAlreadyExist(name, fileNames ?: mutableSetOf())) { + return context.getString(R.string.file_already_exists) + } + return null } + + fun isFileHidden(name: String): Boolean = !TextUtils.isEmpty(name) && name[0] == '.' + + private fun isFileNameAlreadyExist(name: String, fileNames: MutableSet): Boolean = fileNames.contains(name) } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index a405d112f0db..1e5835b29723 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -248,18 +248,25 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem val errorMessage = FileNameValidator.isValid(name, requireContext()) val error = when { - name.isEmpty() || isNameJustExtension -> null - name[0] == '.' -> getText(R.string.hidden_file_name_warning) + isNameJustExtension -> null errorMessage != null -> errorMessage - fileNames.contains(name) -> getText(R.string.file_already_exists) else -> null } - positiveButton?.isEnabled = (error == null) - positiveButton?.isClickable = (error == null) - binding.filenameContainer.isErrorEnabled = (error != null) - if (error != null) { - binding.filenameContainer.error = error + if (error != null || name.equals(DOT + selectedTemplate?.extension, ignoreCase = true)) { + binding.filenameContainer.error = error ?: getString(R.string.enter_filename) + positiveButton?.isEnabled = false + positiveButton?.isClickable = false + binding.filenameContainer.isErrorEnabled = true + } else if (FileNameValidator.isFileHidden(name)) { + positiveButton?.isEnabled = true + positiveButton?.isClickable = true + binding.filenameContainer.isErrorEnabled = true + binding.filenameContainer.error = getText(R.string.hidden_file_name_warning) + } else { + positiveButton?.isEnabled = true + positiveButton?.isClickable = true + binding.filenameContainer.isErrorEnabled = false } } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index 2aa4d244bdf6..f38797ad5012 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -117,13 +117,11 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList private fun checkFileNameAfterEachType(fileNames: MutableSet) { val newFileName = binding.userInput.text?.toString()?.trim() ?: "" - val errorMessage: String? = FileNameValidator.isValid(newFileName, requireContext()) + val errorMessage: String? = FileNameValidator.isValid(newFileName, requireContext(), fileNames) val error = when { newFileName.isEmpty() -> null - newFileName[0] == '.' -> getString(R.string.hidden_file_name_warning) errorMessage != null -> errorMessage - fileNames.contains(newFileName) -> getString(R.string.file_already_exists) else -> null } @@ -133,6 +131,10 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList if (positiveButton == null) { bindButton() } + } else if (FileNameValidator.isFileHidden(newFileName)) { + binding.userInputContainer.error = requireContext().getString(R.string.hidden_file_name_warning) + binding.userInputContainer.isErrorEnabled = true + positiveButton?.isEnabled = true } else { binding.userInputContainer.error = null binding.userInputContainer.isErrorEnabled = false diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java index bbde6a529061..dce02fe14c33 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java @@ -159,7 +159,7 @@ public void onClick(DialogInterface dialog, int which) { newFileName = binding.userInput.getText().toString().trim(); } - String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, requireContext()); + String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, requireContext(), null); if (errorMessage != null) { DisplayUtils.showSnackMessage(requireActivity(), errorMessage); return; @@ -193,16 +193,13 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { newFileName = binding.userInput.getText().toString().trim(); } - String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, requireContext()); + String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, requireContext(), fileNames); - if (!TextUtils.isEmpty(newFileName) && newFileName.charAt(0) == '.') { + if (FileNameValidator.INSTANCE.isFileHidden(newFileName)) { binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning)); } else if (errorMessage != null) { binding.userInputContainer.setError(errorMessage); positiveButton.setEnabled(false); - } else if (fileNames.contains(newFileName)) { - binding.userInputContainer.setError(getText(R.string.file_already_exists)); - positiveButton.setEnabled(false); } else if (binding.userInputContainer.getError() != null) { binding.userInputContainer.setError(null); // Called to remove extra padding From d77af05969fc013a516164a550f11e2057ca2398 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 15:09:13 +0200 Subject: [PATCH 07/65] Simplify logic for ChooseTemplateDialogFragment Signed-off-by: alperozturk --- .../ui/dialog/ChooseTemplateDialogFragment.kt | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index 1e5835b29723..b96e4ce8051e 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -223,16 +223,24 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem val errorMessage = FileNameValidator.isValid(name, requireContext()) - if (selectedTemplate == null) { - DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template) - } else if (errorMessage != null) { - DisplayUtils.showSnackMessage(requireActivity(), errorMessage) - } else if (name.equals(DOT + selectedTemplate.extension, ignoreCase = true)) { - DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename) - } else if (!name.endsWith(selectedTemplate.extension)) { - createFromTemplate(selectedTemplate, path + DOT + selectedTemplate.extension) - } else { - createFromTemplate(selectedTemplate, path) + when { + selectedTemplate == null -> { + DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template) + } + errorMessage != null -> { + DisplayUtils.showSnackMessage(requireActivity(), errorMessage) + } + name.equals(DOT + selectedTemplate.extension, ignoreCase = true) -> { + DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename) + } + else -> { + val fullPath = if (!name.endsWith(selectedTemplate.extension)) { + path + DOT + selectedTemplate.extension + } else { + path + } + createFromTemplate(selectedTemplate, fullPath) + } } } @@ -245,28 +253,27 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem DOT + selectedTemplate.extension, ignoreCase = true ) - val errorMessage = FileNameValidator.isValid(name, requireContext()) + val fileNameValidatorResult = FileNameValidator.isValid(name, requireContext()) - val error = when { + val errorMessage = when { isNameJustExtension -> null - errorMessage != null -> errorMessage + fileNameValidatorResult != null -> fileNameValidatorResult + else -> null + } + + val isNameValid = (errorMessage == null) && !name.equals(DOT + selectedTemplate?.extension, ignoreCase = true) + val isHiddenFileName = FileNameValidator.isFileHidden(name) + + binding.filenameContainer.isErrorEnabled = !isNameValid || isHiddenFileName + binding.filenameContainer.error = when { + !isNameValid -> errorMessage ?: getString(R.string.enter_filename) + isHiddenFileName -> getText(R.string.hidden_file_name_warning) else -> null } - if (error != null || name.equals(DOT + selectedTemplate?.extension, ignoreCase = true)) { - binding.filenameContainer.error = error ?: getString(R.string.enter_filename) - positiveButton?.isEnabled = false - positiveButton?.isClickable = false - binding.filenameContainer.isErrorEnabled = true - } else if (FileNameValidator.isFileHidden(name)) { - positiveButton?.isEnabled = true - positiveButton?.isClickable = true - binding.filenameContainer.isErrorEnabled = true - binding.filenameContainer.error = getText(R.string.hidden_file_name_warning) - } else { - positiveButton?.isEnabled = true - positiveButton?.isClickable = true - binding.filenameContainer.isErrorEnabled = false + positiveButton?.apply { + isEnabled = isNameValid && !isHiddenFileName + isClickable = isEnabled } } From 1ba56bdf206e981588bb98a225147871ddfcde54 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 15:19:25 +0200 Subject: [PATCH 08/65] Simplify Signed-off-by: alperozturk --- .../android/ui/dialog/CreateFolderDialogFragment.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index f38797ad5012..870b3c4bf1b1 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -89,7 +89,6 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList val inflater = requireActivity().layoutInflater binding = EditBoxDialogBinding.inflate(inflater, null, false) - val view: View = binding.root binding.userInput.setText(R.string.empty) viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer) @@ -110,23 +109,23 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList } }) - val builder = buildMaterialAlertDialog(view) + val builder = buildMaterialAlertDialog(binding.root) viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.context, builder) return builder.create() } private fun checkFileNameAfterEachType(fileNames: MutableSet) { val newFileName = binding.userInput.text?.toString()?.trim() ?: "" - val errorMessage: String? = FileNameValidator.isValid(newFileName, requireContext(), fileNames) + val fileNameValidatorResult: String? = FileNameValidator.isValid(newFileName, requireContext(), fileNames) - val error = when { + val errorMessage = when { newFileName.isEmpty() -> null - errorMessage != null -> errorMessage + fileNameValidatorResult != null -> fileNameValidatorResult else -> null } - if (error != null) { - binding.userInputContainer.error = error + if (errorMessage != null) { + binding.userInputContainer.error = errorMessage positiveButton?.isEnabled = false if (positiveButton == null) { bindButton() From f547e50e4e900faf71f34f39e544a816dbaf6b83 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 15:35:19 +0200 Subject: [PATCH 09/65] Add FileNameValidatorTests Signed-off-by: alperozturk --- .../nextcloud/utils/FileNameValidatorTests.kt | 73 +++++++++++++++++++ .../fileNameValidator/FileNameValidator.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt new file mode 100644 index 000000000000..92541e2f2370 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -0,0 +1,73 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import com.nextcloud.utils.fileNameValidator.FileNameValidator +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class FileNameValidatorTests: AbstractIT() { + + @Test + fun testInvalidCharacter() { + val result = FileNameValidator.isValid("file): Boolean = fileNames.contains(name) + fun isFileNameAlreadyExist(name: String, fileNames: MutableSet): Boolean = fileNames.contains(name) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ffad679425fe..9ac58e4a1187 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1219,7 +1219,7 @@ Contact not found, you can always sync to update. Redirecting to web… Permissions are required to open search result otherwise it will redirected to web… - File name contains invalid characters %s + File name contains invalid characters: %s File name is a reserved name File name ends with a space or a period From d48b043e02b6014b6f0ef0924c5995c64fc58b22 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Jul 2024 15:37:23 +0200 Subject: [PATCH 10/65] Fix code analytics Signed-off-by: alperozturk --- .../java/com/nextcloud/utils/FileNameValidatorTests.kt | 2 +- .../nextcloud/utils/fileNameValidator/FileNameValidator.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt index 92541e2f2370..65e4f4c10288 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -16,7 +16,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test -class FileNameValidatorTests: AbstractIT() { +class FileNameValidatorTests : AbstractIT() { @Test fun testInvalidCharacter() { diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 392cbd920f4d..441a8a66c1bc 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -22,8 +22,11 @@ object FileNameValidator { "LPT¹", "LPT²", "LPT³" ) + @Suppress("ReturnCount") fun isValid(name: String, context: Context, fileNames: MutableSet? = null): String? { - val invalidCharacter = name.find { it.toString().matches(reservedWindowsChars) || it.toString().matches(reservedUnixChars) } + val invalidCharacter = name.find { + it.toString().matches(reservedWindowsChars) || it.toString().matches(reservedUnixChars) + } if (invalidCharacter != null) { return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) } From 5d36d7617551f7c4ec04af5ec57a5e7f9b261cd1 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 3 Jul 2024 11:49:19 +0200 Subject: [PATCH 11/65] Add New OCCapabilities Signed-off-by: alperozturk --- .../nextcloud/utils/FileNameValidatorTests.kt | 28 +++++++++++---- .../fileNameValidator/FileNameValidator.kt | 36 ++++++++++++------- .../ui/dialog/ChooseTemplateDialogFragment.kt | 7 ++-- .../ui/dialog/CreateFolderDialogFragment.kt | 13 +++++-- .../ui/dialog/RenameFileDialogFragment.java | 12 +++++-- 5 files changed, 70 insertions(+), 26 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt index 65e4f4c10288..3dbc613b0f10 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -10,51 +10,65 @@ package com.nextcloud.utils import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.AbstractIT import com.owncloud.android.R +import com.owncloud.android.lib.resources.status.CapabilityBooleanType +import com.owncloud.android.lib.resources.status.OCCapability import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test class FileNameValidatorTests : AbstractIT() { + private var capability: OCCapability = fileDataStorageManager.getCapability(account.name) + + @Before + fun setup() { + capability = capability.apply { + forbiddenFilenames = CapabilityBooleanType.TRUE + forbiddenFilenameExtension = CapabilityBooleanType.TRUE + forbiddenFilenameCharacters = CapabilityBooleanType.TRUE + } + } + @Test fun testInvalidCharacter() { - val result = FileNameValidator.isValid("file? = null): String? { - val invalidCharacter = name.find { - it.toString().matches(reservedWindowsChars) || it.toString().matches(reservedUnixChars) - } - if (invalidCharacter != null) { - return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) + fun isValid( + name: String, + capability: OCCapability, + context: Context, + fileNames: MutableSet? = null + ): String? { + if (TextUtils.isEmpty(name)) { + return context.getString(R.string.filename_empty) } - if (reservedWindowsNames.contains(name.uppercase())) { - return context.getString(R.string.file_name_validator_error_reserved_names) + if (isFileNameAlreadyExist(name, fileNames ?: mutableSetOf())) { + return context.getString(R.string.file_already_exists) } if (name.endsWith(" ") || name.endsWith(".")) { return context.getString(R.string.file_name_validator_error_ends_with_space_period) } - if (TextUtils.isEmpty(name)) { - return context.getString(R.string.filename_empty) + if (capability.forbiddenFilenameCharacters.isTrue) { + val invalidCharacter = name.find { + it.toString().matches(reservedWindowsChars) || it.toString().matches(reservedUnixChars) + } + if (invalidCharacter != null) { + return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) + } } - if (isFileNameAlreadyExist(name, fileNames ?: mutableSetOf())) { - return context.getString(R.string.file_already_exists) + if (capability.forbiddenFilenames.isTrue && reservedWindowsNames.contains(name.uppercase())) { + return context.getString(R.string.file_name_validator_error_reserved_names) + } + + if (capability.forbiddenFilenameExtension.isTrue) { + // TODO add logic } return null diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index b96e4ce8051e..50e30884f5e0 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -44,6 +44,7 @@ import com.owncloud.android.lib.common.TemplateList import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.ui.activity.ExternalSiteWebView import com.owncloud.android.ui.activity.TextEditorWebView import com.owncloud.android.ui.adapter.TemplateAdapter @@ -216,12 +217,14 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem } } + private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(currentAccount.user.accountName) + override fun onClick(v: View) { val name = binding.filename.text.toString() val path = parentFolder?.remotePath + name val selectedTemplate = adapter?.selectedTemplate - val errorMessage = FileNameValidator.isValid(name, requireContext()) + val errorMessage = FileNameValidator.isValid(name, getOCCapability(), requireContext()) when { selectedTemplate == null -> { @@ -253,7 +256,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem DOT + selectedTemplate.extension, ignoreCase = true ) - val fileNameValidatorResult = FileNameValidator.isValid(name, requireContext()) + val fileNameValidatorResult = FileNameValidator.isValid(name, getOCCapability(), requireContext()) val errorMessage = when { isNameJustExtension -> null diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index 870b3c4bf1b1..01f0fe39f244 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -22,6 +22,7 @@ import androidx.fragment.app.DialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.common.collect.Sets +import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.di.Injectable import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.fileNameValidator.FileNameValidator @@ -29,6 +30,7 @@ import com.owncloud.android.R import com.owncloud.android.databinding.EditBoxDialogBinding import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.ui.activity.ComponentsGetter import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.KeyboardUtils @@ -52,6 +54,9 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList @Inject lateinit var keyboardUtils: KeyboardUtils + @Inject + lateinit var currentAccount: CurrentAccountProvider + private var mParentFolder: OCFile? = null private var positiveButton: MaterialButton? = null @@ -114,9 +119,13 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList return builder.create() } + private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(currentAccount.user.accountName) + private fun checkFileNameAfterEachType(fileNames: MutableSet) { val newFileName = binding.userInput.text?.toString()?.trim() ?: "" - val fileNameValidatorResult: String? = FileNameValidator.isValid(newFileName, requireContext(), fileNames) + + val fileNameValidatorResult: String? = + FileNameValidator.isValid(newFileName, getOCCapability(), requireContext(), fileNames) val errorMessage = when { newFileName.isEmpty() -> null @@ -154,7 +163,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList val newFolderName = (getDialog()?.findViewById(R.id.user_input) as TextView) .text.toString().trim { it <= ' ' } - val errorMessage: String? = FileNameValidator.isValid(newFolderName, requireContext()) + val errorMessage: String? = FileNameValidator.isValid(newFolderName, getOCCapability(), requireContext()) if (errorMessage != null) { DisplayUtils.showSnackMessage(requireActivity(), errorMessage) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java index dce02fe14c33..e938a01d4f15 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java @@ -14,7 +14,6 @@ import android.content.DialogInterface; import android.os.Bundle; import android.text.Editable; -import android.text.TextUtils; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; @@ -22,6 +21,7 @@ import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.collect.Sets; +import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.di.Injectable; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.fileNameValidator.FileNameValidator; @@ -29,6 +29,7 @@ import com.owncloud.android.databinding.EditBoxDialogBinding; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.KeyboardUtils; @@ -56,6 +57,7 @@ public class RenameFileDialogFragment @Inject ViewThemeUtils viewThemeUtils; @Inject FileDataStorageManager fileDataStorageManager; @Inject KeyboardUtils keyboardUtils; + @Inject CurrentAccountProvider currentAccount; private EditBoxDialogBinding binding; private OCFile mTargetFile; @@ -150,6 +152,10 @@ private void initAlertDialog() { } } + private OCCapability getOCCapability() { + return fileDataStorageManager.getCapability(currentAccount.getUser().getAccountName()); + } + @Override public void onClick(DialogInterface dialog, int which) { if (which == AlertDialog.BUTTON_POSITIVE) { @@ -159,7 +165,7 @@ public void onClick(DialogInterface dialog, int which) { newFileName = binding.userInput.getText().toString().trim(); } - String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, requireContext(), null); + String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, getOCCapability(), requireContext(), null); if (errorMessage != null) { DisplayUtils.showSnackMessage(requireActivity(), errorMessage); return; @@ -193,7 +199,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { newFileName = binding.userInput.getText().toString().trim(); } - String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, requireContext(), fileNames); + String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, getOCCapability(), requireContext(), fileNames); if (FileNameValidator.INSTANCE.isFileHidden(newFileName)) { binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning)); From 5800a45c22db972474166d3eba4087711e3969a5 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 3 Jul 2024 12:37:03 +0200 Subject: [PATCH 12/65] Add forbiddenFilenameCharacterList values coming from server Signed-off-by: alperozturk --- .../nextcloud/utils/fileNameValidator/FileNameValidator.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 0cb7a88f6f78..e2ff97618b05 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -44,7 +44,9 @@ object FileNameValidator { if (capability.forbiddenFilenameCharacters.isTrue) { val invalidCharacter = name.find { - it.toString().matches(reservedWindowsChars) || it.toString().matches(reservedUnixChars) + it.toString().matches(reservedWindowsChars) || + it.toString().matches(reservedUnixChars) || + capability.forbiddenFilenameCharacterList.contains(it.toString()) } if (invalidCharacter != null) { return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) From 366a384653edd6177466a2cc0096c9bbc56277ab Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 3 Jul 2024 14:47:59 +0200 Subject: [PATCH 13/65] Add new capability to the local DB Signed-off-by: alperozturk --- .../82.json | 1227 +++++++++++++++++ .../client/database/NextcloudDatabase.kt | 3 +- .../database/entity/CapabilityEntity.kt | 8 +- .../fileNameValidator/FileNameValidator.kt | 3 +- .../datamodel/FileDataStorageManager.java | 9 + .../com/owncloud/android/db/ProviderMeta.java | 5 +- 6 files changed, 1250 insertions(+), 5 deletions(-) create mode 100644 app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json new file mode 100644 index 000000000000..fcdcac2b2ddd --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json @@ -0,0 +1,1227 @@ +{ + "formatVersion": 1, + "database": { + "version": 82, + "identityHash": "b829756cd63109c324f0f7bbf478e992", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b829756cd63109c324f0f7bbf478e992')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 1c9ec8c7de99..bed77c0436a9 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -59,7 +59,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 77, to = 78), AutoMigration(from = 78, to = 79, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 79, to = 80), - AutoMigration(from = 80, to = 81) + AutoMigration(from = 80, to = 81), + AutoMigration(from = 81, to = 82) ], exportSchema = true ) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt index 7fd31416e6f2..6f7f751c63df 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt @@ -122,5 +122,11 @@ data class CapabilityEntity( @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT) val dropAccount: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD) - val securityGuard: Int? + val securityGuard: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS) + val forbiddenFileNameCharacters: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES) + val forbiddenFileNames: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS) + val forbiddenFileNameExtensions: Int? ) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index e2ff97618b05..1fc24c9eddc8 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -45,8 +45,7 @@ object FileNameValidator { if (capability.forbiddenFilenameCharacters.isTrue) { val invalidCharacter = name.find { it.toString().matches(reservedWindowsChars) || - it.toString().matches(reservedUnixChars) || - capability.forbiddenFilenameCharacterList.contains(it.toString()) + it.toString().matches(reservedUnixChars) } if (invalidCharacter != null) { return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 31672084c093..54ff115fc3fe 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -2053,6 +2053,10 @@ private ContentValues createContentValues(String accountName, OCCapability capab contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS, capability.getForbiddenFilenameCharacters().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES, capability.getForbiddenFilenames().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS, capability.getForbiddenFilenameExtension().getValue()); + return contentValues; } @@ -2221,6 +2225,11 @@ private OCCapability createCapabilityInstance(Cursor cursor) { capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)); capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)); capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)); + + capability.setForbiddenFilenameCharacters(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS)); + capability.setForbiddenFilenames(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES)); + capability.setForbiddenFilenameExtension(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS)); + } return capability; } diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index 4e43ca5806cc..d99afdc57d03 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -25,7 +25,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 81; + public static final int DB_VERSION = 82; private ProviderMeta() { // No instance @@ -259,6 +259,9 @@ static public class ProviderTableMeta implements BaseColumns { public static final String CAPABILITIES_GROUPFOLDERS = "groupfolders"; public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account"; public static final String CAPABILITIES_SECURITY_GUARD = "security_guard"; + public static final String CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS = "forbidden_filename_characters"; + public static final String CAPABILITIES_FORBIDDEN_FILENAMES = "forbidden_filenames"; + public static final String CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS = "forbidden_filename_extensions"; //Columns of Uploads table public static final String UPLOADS_LOCAL_PATH = "local_path"; From 94b6e919093540487bf838da7590fac6bbbacee4 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 3 Jul 2024 14:53:07 +0200 Subject: [PATCH 14/65] Extract checkInvalidCharacters Signed-off-by: alperozturk --- .../fileNameValidator/FileNameValidator.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 1fc24c9eddc8..cf8aab9c7514 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -42,14 +42,8 @@ object FileNameValidator { return context.getString(R.string.file_name_validator_error_ends_with_space_period) } - if (capability.forbiddenFilenameCharacters.isTrue) { - val invalidCharacter = name.find { - it.toString().matches(reservedWindowsChars) || - it.toString().matches(reservedUnixChars) - } - if (invalidCharacter != null) { - return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) - } + checkInvalidCharacters(name, capability, context)?.let { + return it } if (capability.forbiddenFilenames.isTrue && reservedWindowsNames.contains(name.uppercase())) { @@ -63,6 +57,20 @@ object FileNameValidator { return null } + private fun checkInvalidCharacters(name: String, capability: OCCapability, context: Context): String? { + if (capability.forbiddenFilenameCharacters.isTrue) { + val invalidCharacter = name.find { + it.toString().matches(reservedWindowsChars) || + it.toString().matches(reservedUnixChars) + } + if (invalidCharacter != null) { + return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) + } + } + + return null + } + fun isFileHidden(name: String): Boolean = !TextUtils.isEmpty(name) && name[0] == '.' fun isFileNameAlreadyExist(name: String, fileNames: MutableSet): Boolean = fileNames.contains(name) From 7e85d80d8e5b41003c2482c1bd7ea64d13b44f0a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 3 Jul 2024 15:06:15 +0200 Subject: [PATCH 15/65] Simplify checkInvalidCharacters Signed-off-by: alperozturk --- .../fileNameValidator/FileNameValidator.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index cf8aab9c7514..93d1e836717e 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -58,17 +58,16 @@ object FileNameValidator { } private fun checkInvalidCharacters(name: String, capability: OCCapability, context: Context): String? { - if (capability.forbiddenFilenameCharacters.isTrue) { - val invalidCharacter = name.find { - it.toString().matches(reservedWindowsChars) || - it.toString().matches(reservedUnixChars) - } - if (invalidCharacter != null) { - return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) - } + if (capability.forbiddenFilenameCharacters.isFalse) return null + + val invalidCharacter = name.find { + val input = it.toString() + input.matches(reservedWindowsChars) || input.matches(reservedUnixChars) } - return null + if (invalidCharacter == null) return null + + return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) } fun isFileHidden(name: String): Boolean = !TextUtils.isEmpty(name) && name[0] == '.' From b6a732cdfd2c9f121a205d15b0717302a49966af Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 3 Jul 2024 16:38:08 +0200 Subject: [PATCH 16/65] Add checks for move or copy Signed-off-by: alperozturk --- .../nextcloud/utils/FileNameValidatorTests.kt | 72 +++++++++++++++++++ .../utils/extensions/StringExtensions.kt | 9 +++ .../fileNameValidator/FileNameValidator.kt | 23 +++++- .../ui/activity/FolderPickerActivity.kt | 16 +++++ app/src/main/res/values/strings.xml | 2 + 5 files changed, 121 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt index 3dbc613b0f10..6f21ab79a25b 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -84,4 +84,76 @@ class FileNameValidatorTests : AbstractIT() { assertTrue(FileNameValidator.isFileNameAlreadyExist("existingFile", existingFiles)) assertFalse(FileNameValidator.isFileNameAlreadyExist("newFile", existingFiles)) } + + @Test + fun testValidFolderAndFilePaths() { + val folderPath = "validFolder" + val filePaths = listOf("file1.txt", "file2.doc", "file3.jpg") + + val result = FileNameValidator.checkPath(folderPath, filePaths, capability, targetContext) + assertTrue(result) + } + + @Test + fun testFolderPathWithReservedName() { + val folderPath = "CON" + val filePaths = listOf("file1.txt", "file2.doc", "file3.jpg") + + val result = FileNameValidator.checkPath(folderPath, filePaths, capability, targetContext) + assertFalse(result) + } + + @Test + fun testFilePathWithReservedName() { + val folderPath = "validFolder" + val filePaths = listOf("file1.txt", "PRN.doc", "file3.jpg") + + val result = FileNameValidator.checkPath(folderPath, filePaths, capability, targetContext) + assertFalse(result) + } + + @Test + fun testFolderPathWithInvalidCharacter() { + val folderPath = "invalid, capability: OCCapability, context: Context): Boolean { + val folderPaths = folderPath.split("/", "\\") + + for (item in folderPaths) { + if (isValid(item, capability, context) != null) { + return false + } + } + + for (item in filePaths) { + if (isValid(item, capability, context) != null) { + return false + } + } + + return true + } + private fun checkInvalidCharacters(name: String, capability: OCCapability, context: Context): String? { if (capability.forbiddenFilenameCharacters.isFalse) return null diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index ed9574f1fb1d..4dad09c58266 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -15,6 +15,8 @@ import android.content.Intent import android.content.IntentFilter import android.content.res.Resources import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.os.Parcelable import android.view.ActionMode import android.view.Menu @@ -23,6 +25,7 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.nextcloud.client.di.Injectable +import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.R import com.owncloud.android.databinding.FilesFolderPickerBinding import com.owncloud.android.databinding.FilesPickerBinding @@ -441,6 +444,7 @@ open class FolderPickerActivity : } } + @Suppress("MagicNumber") private fun processOperation(action: String?) { val i = intent val resultData = Intent() @@ -451,6 +455,18 @@ open class FolderPickerActivity : } targetFilePaths?.let { filePaths -> + + val isPathValid = FileNameValidator.checkPath(file.remotePath, filePaths, capabilities, this) + if (!isPathValid) { + DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_copy_or_move) + Handler(Looper.getMainLooper()).postDelayed({ + setResult(RESULT_CANCELED, resultData) + finish() + + }, 1000L) + return + } + action?.let { action -> fileOperationsHelper.moveOrCopyFiles(action, filePaths, file) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ac58e4a1187..9fde5b47f4c5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1219,6 +1219,8 @@ Contact not found, you can always sync to update. Redirecting to web… Permissions are required to open search result otherwise it will redirected to web… + + Folder path contains reserved names or invalid character File name contains invalid characters: %s File name is a reserved name File name ends with a space or a period From 1aedf275d96ecb4c4a695ce2538d73069275cb0a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 4 Jul 2024 09:42:06 +0200 Subject: [PATCH 17/65] local lib support Signed-off-by: alperozturk --- gradle.properties | 2 +- settings.gradle | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 64ef75a03b48..a570c75f3d08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,4 +21,4 @@ org.gradle.parallel=true org.gradle.configureondemand=true # Needed for local libs -# org.gradle.dependency.verification=lenient \ No newline at end of file +org.gradle.dependency.verification=lenient \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 352229bd93d9..dd5848fdddc0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,3 +20,9 @@ include ':appscan' // substitute module('com.github.nextcloud:android-library') using project(':library') // } //} + +includeBuild('/Users/alperozturk/Desktop/nextcloud/android/nextcloud_android_library') { + dependencySubstitution { + substitute module('com.github.nextcloud:android-library') using project(':library') + } +} From 34055bbfeae1f06be6b2cd8dcb38da0ea5cce162 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 4 Jul 2024 13:55:12 +0200 Subject: [PATCH 18/65] Rename .java to .kt Signed-off-by: alperozturk --- ...{RenameFileDialogFragment.java => RenameFileDialogFragment.kt} | 0 ...hareDialogFragment.java => RenamePublicShareDialogFragment.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/dialog/{RenameFileDialogFragment.java => RenameFileDialogFragment.kt} (100%) rename app/src/main/java/com/owncloud/android/ui/dialog/{RenamePublicShareDialogFragment.java => RenamePublicShareDialogFragment.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java rename to app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.java rename to app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.kt From 5c6439f1113e01f3fd38c2bb058deb91ff41db5a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 4 Jul 2024 13:55:13 +0200 Subject: [PATCH 19/65] Convert to Kotlin Signed-off-by: alperozturk --- .../fileNameValidator/FileNameValidator.kt | 7 +- .../ui/activity/ConflictsResolveActivity.kt | 1 + .../ui/activity/FolderPickerActivity.kt | 1 - .../ui/dialog/RenameFileDialogFragment.kt | 284 +++++++++--------- .../dialog/RenamePublicShareDialogFragment.kt | 168 +++++------ 5 files changed, 222 insertions(+), 239 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index b684b0479665..1cb7c864676e 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -48,7 +48,10 @@ object FileNameValidator { } if (capability.forbiddenFilenames.isTrue && - (reservedWindowsNames.contains(name.uppercase()) || reservedWindowsNames.contains(name.removeFileExtension().uppercase())) + ( + reservedWindowsNames.contains(name.uppercase()) || + reservedWindowsNames.contains(name.removeFileExtension().uppercase()) + ) ) { return context.getString(R.string.file_name_validator_error_reserved_names) } @@ -60,6 +63,7 @@ object FileNameValidator { return null } + @Suppress("ReturnCount") fun checkPath(folderPath: String, filePaths: List, capability: OCCapability, context: Context): Boolean { val folderPaths = folderPath.split("/", "\\") @@ -78,6 +82,7 @@ object FileNameValidator { return true } + @Suppress("ReturnCount") private fun checkInvalidCharacters(name: String, capability: OCCapability, context: Context): String? { if (capability.forbiddenFilenameCharacters.isFalse) return null diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt index 0567a215c7a3..8d334101bee7 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt @@ -154,6 +154,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener listener?.conflictDecisionMade(decision) } + @Suppress("ReturnCount") override fun onStart() { super.onStart() if (account == null) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index 4dad09c58266..a398217ab081 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -462,7 +462,6 @@ open class FolderPickerActivity : Handler(Looper.getMainLooper()).postDelayed({ setResult(RESULT_CANCELED, resultData) finish() - }, 1000L) return } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt index e938a01d4f15..df21b752b1c6 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt @@ -8,214 +8,198 @@ * SPDX-FileCopyrightText: 2014 David A. Velasco * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ -package com.owncloud.android.ui.dialog; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.common.collect.Sets; -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.nextcloud.utils.fileNameValidator.FileNameValidator; -import com.owncloud.android.R; -import com.owncloud.android.databinding.EditBoxDialogBinding; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.ui.activity.ComponentsGetter; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.KeyboardUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.List; -import java.util.Set; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; +package com.owncloud.android.ui.dialog + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.common.collect.Sets +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.client.di.Injectable +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.fileNameValidator.FileNameValidator.isFileHidden +import com.nextcloud.utils.fileNameValidator.FileNameValidator.isValid +import com.owncloud.android.R +import com.owncloud.android.databinding.EditBoxDialogBinding +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.ui.activity.ComponentsGetter +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.KeyboardUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject /** - * Dialog to input a new name for an {@link OCFile} being renamed. - * Triggers the rename operation. + * Dialog to input a new name for an [OCFile] being renamed. + * Triggers the rename operation. */ -public class RenameFileDialogFragment - extends DialogFragment implements DialogInterface.OnClickListener, TextWatcher, Injectable { +class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListener, TextWatcher, Injectable { + @Inject + lateinit var viewThemeUtils: ViewThemeUtils - private static final String ARG_TARGET_FILE = "TARGET_FILE"; - private static final String ARG_PARENT_FOLDER = "PARENT_FOLDER"; + @Inject + lateinit var fileDataStorageManager: FileDataStorageManager - @Inject ViewThemeUtils viewThemeUtils; - @Inject FileDataStorageManager fileDataStorageManager; - @Inject KeyboardUtils keyboardUtils; - @Inject CurrentAccountProvider currentAccount; + @Inject + lateinit var keyboardUtils: KeyboardUtils - private EditBoxDialogBinding binding; - private OCFile mTargetFile; - private MaterialButton positiveButton; - private Set fileNames; + @Inject + lateinit var currentAccount: CurrentAccountProvider - /** - * Public factory method to create new RenameFileDialogFragment instances. - * - * @param file File to rename. - * @return Dialog ready to show. - */ - public static RenameFileDialogFragment newInstance(OCFile file, OCFile parentFolder) { - RenameFileDialogFragment frag = new RenameFileDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_TARGET_FILE, file); - args.putParcelable(ARG_PARENT_FOLDER, parentFolder); - frag.setArguments(args); - return frag; - } + private lateinit var binding: EditBoxDialogBinding + private var mTargetFile: OCFile? = null + private var positiveButton: MaterialButton? = null + private var fileNames: MutableSet? = null - @Override - public void onStart() { - super.onStart(); - initAlertDialog(); + override fun onStart() { + super.onStart() + initAlertDialog() } - @Override - public void onResume() { - super.onResume(); - keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.userInput); + override fun onResume() { + super.onResume() + keyboardUtils.showKeyboardForEditText(requireDialog().window, binding.userInput) } - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - mTargetFile = BundleExtensionsKt.getParcelableArgument(requireArguments(), ARG_TARGET_FILE, OCFile.class); - - // Inflate the layout for the dialog - LayoutInflater inflater = requireActivity().getLayoutInflater(); - binding = EditBoxDialogBinding.inflate(inflater, null, false); - View view = binding.getRoot(); - - // Setup layout - String currentName = mTargetFile.getFileName(); - binding.userInput.setText(currentName); - viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer); - int extensionStart = mTargetFile.isFolder() ? -1 : currentName.lastIndexOf('.'); - int selectionEnd = extensionStart >= 0 ? extensionStart : currentName.length(); - binding.userInput.setSelection(0, selectionEnd); - - OCFile parentFolder = BundleExtensionsKt.getParcelableArgument(getArguments(), ARG_PARENT_FOLDER, OCFile.class); - List folderContent = fileDataStorageManager.getFolderContent(parentFolder, false); - fileNames = Sets.newHashSetWithExpectedSize(folderContent.size()); - - for (OCFile file : folderContent) { - fileNames.add(file.getFileName()); + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + mTargetFile = requireArguments().getParcelableArgument(ARG_TARGET_FILE, OCFile::class.java) + + val inflater = requireActivity().layoutInflater + binding = EditBoxDialogBinding.inflate(inflater, null, false) + + val currentName = mTargetFile?.fileName + binding.userInput.setText(currentName) + viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer) + val extensionStart = if (mTargetFile?.isFolder == true) -1 else currentName?.lastIndexOf('.') + val selectionEnd = if ((extensionStart ?: -1) >= 0) extensionStart else currentName?.length + if (selectionEnd != null) { + binding.userInput.setSelection(0, selectionEnd) } - // Add TextChangedListener to handle showing/hiding the input warning message - binding.userInput.addTextChangedListener(this); + val parentFolder = arguments.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java) + val folderContent = fileDataStorageManager.getFolderContent(parentFolder, false) + fileNames = Sets.newHashSetWithExpectedSize(folderContent.size) - // Build the dialog - MaterialAlertDialogBuilder builder = buildMaterialAlertDialog(view); + for (file in folderContent) { + fileNames?.add(file.fileName) + } + + binding.userInput.addTextChangedListener(this) - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.getContext(), builder); + val builder = buildMaterialAlertDialog(binding.root) - return builder.create(); + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.context, builder) + + return builder.create() } - private MaterialAlertDialogBuilder buildMaterialAlertDialog(View view) { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + private fun buildMaterialAlertDialog(view: View): MaterialAlertDialogBuilder { + val builder = MaterialAlertDialogBuilder(requireActivity()) builder .setView(view) .setPositiveButton(R.string.file_rename, this) .setNegativeButton(R.string.common_cancel, this) - .setTitle(R.string.rename_dialog_title); + .setTitle(R.string.rename_dialog_title) - return builder; + return builder } - private void initAlertDialog() { - AlertDialog alertDialog = (AlertDialog) getDialog(); + private fun initAlertDialog() { + val alertDialog = dialog as AlertDialog? if (alertDialog != null) { - positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton + val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton - viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); + positiveButton?.let { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it) + } + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton) } } - private OCCapability getOCCapability() { - return fileDataStorageManager.getCapability(currentAccount.getUser().getAccountName()); - } + private val oCCapability: OCCapability + get() = fileDataStorageManager.getCapability(currentAccount.user.accountName) - @Override - public void onClick(DialogInterface dialog, int which) { + override fun onClick(dialog: DialogInterface, which: Int) { if (which == AlertDialog.BUTTON_POSITIVE) { - String newFileName = ""; + var newFileName = "" - if (binding.userInput.getText() != null) { - newFileName = binding.userInput.getText().toString().trim(); + if (binding.userInput.text != null) { + newFileName = binding.userInput.text.toString().trim { it <= ' ' } } - String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, getOCCapability(), requireContext(), null); + val errorMessage = isValid(newFileName, oCCapability, requireContext(), null) if (errorMessage != null) { - DisplayUtils.showSnackMessage(requireActivity(), errorMessage); - return; + DisplayUtils.showSnackMessage(requireActivity(), errorMessage) + return } - if (requireActivity() instanceof ComponentsGetter componentsGetter) { - componentsGetter.getFileOperationsHelper().renameFile(mTargetFile, newFileName); + if (requireActivity() is ComponentsGetter) { + val componentsGetter = requireActivity() as ComponentsGetter + componentsGetter.getFileOperationsHelper().renameFile(mTargetFile, newFileName) } } } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit /** * When user enters a hidden file name, the 'hidden file' message is shown. * Otherwise, the message is ensured to be hidden. */ - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - String newFileName = ""; - if (binding.userInput.getText() != null) { - newFileName = binding.userInput.getText().toString().trim(); + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + var newFileName = "" + if (binding.userInput.text != null) { + newFileName = binding.userInput.text.toString().trim { it <= ' ' } } - String errorMessage = FileNameValidator.INSTANCE.isValid(newFileName, getOCCapability(), requireContext(), fileNames); + val errorMessage = isValid(newFileName, oCCapability, requireContext(), fileNames) - if (FileNameValidator.INSTANCE.isFileHidden(newFileName)) { - binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning)); + if (isFileHidden(newFileName)) { + binding.userInputContainer.error = getText(R.string.hidden_file_name_warning) } else if (errorMessage != null) { - binding.userInputContainer.setError(errorMessage); - positiveButton.setEnabled(false); - } else if (binding.userInputContainer.getError() != null) { - binding.userInputContainer.setError(null); + binding.userInputContainer.error = errorMessage + positiveButton?.isEnabled = false + } else if (binding.userInputContainer.error != null) { + binding.userInputContainer.error = null // Called to remove extra padding - binding.userInputContainer.setErrorEnabled(false); - positiveButton.setEnabled(true); + binding.userInputContainer.isErrorEnabled = false + positiveButton?.isEnabled = true } } - @Override - public void afterTextChanged(Editable s) { + override fun afterTextChanged(s: Editable) = Unit + + companion object { + private const val ARG_TARGET_FILE = "TARGET_FILE" + private const val ARG_PARENT_FOLDER = "PARENT_FOLDER" + + /** + * Public factory method to create new RenameFileDialogFragment instances. + * + * @param file File to rename. + * @return Dialog ready to show. + */ + @JvmStatic + fun newInstance(file: OCFile?, parentFolder: OCFile?): RenameFileDialogFragment { + val bundle = Bundle().apply { + putParcelable(ARG_TARGET_FILE, file) + putParcelable(ARG_PARENT_FOLDER, parentFolder) + } + return RenameFileDialogFragment().apply { + arguments = bundle + } + } } } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.kt index a1f37df4f6f2..845d8748b217 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.kt @@ -5,122 +5,116 @@ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.ui.dialog; - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.owncloud.android.R; -import com.owncloud.android.databinding.EditBoxDialogBinding; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.ui.activity.ComponentsGetter; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.KeyboardUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; +package com.owncloud.android.ui.dialog + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.di.Injectable +import com.nextcloud.utils.extensions.getParcelableArgument +import com.owncloud.android.R +import com.owncloud.android.databinding.EditBoxDialogBinding +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.ui.activity.ComponentsGetter +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.KeyboardUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject /** * Dialog to rename a public share. */ -public class RenamePublicShareDialogFragment - extends DialogFragment implements DialogInterface.OnClickListener, Injectable { +class RenamePublicShareDialogFragment : DialogFragment(), DialogInterface.OnClickListener, Injectable { + @Inject + lateinit var viewThemeUtils: ViewThemeUtils - private static final String ARG_PUBLIC_SHARE = "PUBLIC_SHARE"; + @Inject + lateinit var keyboardUtils: KeyboardUtils - @Inject ViewThemeUtils viewThemeUtils; - @Inject KeyboardUtils keyboardUtils; + private lateinit var binding: EditBoxDialogBinding + private var publicShare: OCShare? = null - private EditBoxDialogBinding binding; - private OCShare publicShare; + override fun onStart() { + super.onStart() - public static RenamePublicShareDialogFragment newInstance(OCShare share) { - RenamePublicShareDialogFragment frag = new RenamePublicShareDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_PUBLIC_SHARE, share); - frag.setArguments(args); - return frag; - } + val alertDialog = dialog as AlertDialog? ?: return - @Override - public void onStart() { - super.onStart(); + val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton + val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton - AlertDialog alertDialog = (AlertDialog) getDialog(); + positiveButton?.let { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton) + } - if (alertDialog != null) { - MaterialButton positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); - viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); + negativeButton?.let { + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton) } } - @Override - public void onResume() { - super.onResume(); - keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.userInput); + override fun onResume() { + super.onResume() + keyboardUtils.showKeyboardForEditText(requireDialog().window, binding.userInput) } - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - publicShare = BundleExtensionsKt.getParcelableArgument(requireArguments(), ARG_PUBLIC_SHARE, OCShare.class); + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + publicShare = requireArguments().getParcelableArgument(ARG_PUBLIC_SHARE, OCShare::class.java) - // Inflate the layout for the dialog - LayoutInflater inflater = requireActivity().getLayoutInflater(); - binding = EditBoxDialogBinding.inflate(inflater, null, false); - View view = binding.getRoot(); + val inflater = requireActivity().layoutInflater + binding = EditBoxDialogBinding.inflate(inflater, null, false) + val view: View = binding.root - // Setup layout - viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer); - binding.userInput.setText(publicShare.getLabel()); + viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer) + binding.userInput.setText(publicShare?.label) - // Build the dialog - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(view.getContext()); - builder.setView(view) + val builder = MaterialAlertDialogBuilder(view.context) + .setView(view) .setPositiveButton(R.string.file_rename, this) .setNegativeButton(R.string.common_cancel, this) - .setTitle(R.string.public_share_name); + .setTitle(R.string.public_share_name) - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInput.getContext(), builder); + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInput.context, builder) - return builder.create(); + return builder.create() } - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == AlertDialog.BUTTON_POSITIVE) { - String newName = ""; - if (binding.userInput.getText() != null) { - newName = binding.userInput.getText().toString().trim(); + override fun onClick(dialog: DialogInterface, which: Int) { + when (which) { + AlertDialog.BUTTON_POSITIVE -> { + var newName = "" + if (binding.userInput.text != null) { + newName = binding.userInput.text.toString().trim { it <= ' ' } + } + + if (TextUtils.isEmpty(newName)) { + DisplayUtils.showSnackMessage(requireActivity(), R.string.label_empty) + return + } + + (requireActivity() as ComponentsGetter).fileOperationsHelper.setLabelToPublicShare( + publicShare, + newName + ) } + } + } - if (TextUtils.isEmpty(newName)) { - DisplayUtils.showSnackMessage(requireActivity(), R.string.label_empty); - return; + companion object { + private const val ARG_PUBLIC_SHARE = "PUBLIC_SHARE" + + fun newInstance(share: OCShare?): RenamePublicShareDialogFragment { + val bundle = Bundle().apply { + putParcelable(ARG_PUBLIC_SHARE, share) } - ((ComponentsGetter) requireActivity()).getFileOperationsHelper().setLabelToPublicShare(publicShare, - newName); + return RenamePublicShareDialogFragment().apply { + arguments = bundle + } } } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } } From 199cd2bbd887a11e2b022f82572420133c26d08f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 4 Jul 2024 13:56:54 +0200 Subject: [PATCH 20/65] revert Signed-off-by: alperozturk --- settings.gradle | 6 ------ 1 file changed, 6 deletions(-) diff --git a/settings.gradle b/settings.gradle index dd5848fdddc0..352229bd93d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,9 +20,3 @@ include ':appscan' // substitute module('com.github.nextcloud:android-library') using project(':library') // } //} - -includeBuild('/Users/alperozturk/Desktop/nextcloud/android/nextcloud_android_library') { - dependencySubstitution { - substitute module('com.github.nextcloud:android-library') using project(':library') - } -} From 15f9fcce2536bae5420b3712e015072722d5be5f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 10 Jul 2024 11:32:54 +0200 Subject: [PATCH 21/65] Check file name before add new file Signed-off-by: alperozturk --- .../fileNameValidator/FileNameValidator.kt | 2 +- .../ui/activity/UploadFilesActivity.java | 25 +++++++++++++++++-- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 1cb7c864676e..d1bb52628212 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -53,7 +53,7 @@ object FileNameValidator { reservedWindowsNames.contains(name.removeFileExtension().uppercase()) ) ) { - return context.getString(R.string.file_name_validator_error_reserved_names) + return context.getString(R.string.file_name_validator_error_reserved_names, name.substringBefore(".")) } if (capability.forbiddenFilenameExtension.isTrue) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java index 3bbdfe7752db..67b75994a0df 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java @@ -35,6 +35,7 @@ import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.utils.extensions.ActivityExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; +import com.nextcloud.utils.fileNameValidator.FileNameValidator; import com.owncloud.android.R; import com.owncloud.android.databinding.UploadFilesLayoutBinding; import com.owncloud.android.lib.common.utils.Log_OC; @@ -642,8 +643,15 @@ public void onClick(View v) { finish(); } else { - new CheckAvailableSpaceTask(this, mFileListFragment.getCheckedFilePaths()) - .execute(binding.uploadFilesSpinnerBehaviour.getSelectedItemPosition() == 0); + String[] selectedFilePaths = mFileListFragment.getCheckedFilePaths(); + String filenameErrorMessage = checkFileNameBeforeUpload(selectedFilePaths); + if (filenameErrorMessage != null) { + DisplayUtils.showSnackMessage(this, filenameErrorMessage); + return; + } + + boolean isPositionZero = (binding.uploadFilesSpinnerBehaviour.getSelectedItemPosition() == 0); + new CheckAvailableSpaceTask(this, selectedFilePaths).execute(isPositionZero); } } else { requestPermissions(); @@ -651,6 +659,19 @@ public void onClick(View v) { } } + private String checkFileNameBeforeUpload(String[] selectedFilePaths) { + for (String filePath : selectedFilePaths) { + File file = new File(filePath); + String filenameErrorMessage = FileNameValidator.INSTANCE.isValid(file.getName(), getCapabilities(), this, null); + + if (filenameErrorMessage != null) { + return filenameErrorMessage; + } + } + + return null; + } + @Override public void onConfirmation(String callerTag) { Log_OC.d(TAG, "Positive button in dialog was clicked; dialog tag is " + callerTag); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9fde5b47f4c5..e79e65de1b7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1222,6 +1222,6 @@ Folder path contains reserved names or invalid character File name contains invalid characters: %s - File name is a reserved name + %s is a reserved name File name ends with a space or a period From c74254918a468cab46888393f07069e8f70690e1 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 10 Jul 2024 12:22:43 +0200 Subject: [PATCH 22/65] Fix folderPaths check Signed-off-by: alperozturk --- .../nextcloud/utils/FileNameValidatorTests.kt | 18 +++++++++++++++++- .../fileNameValidator/FileNameValidator.kt | 2 +- .../activity/ReceiveExternalFilesActivity.java | 12 +++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt index 6f21ab79a25b..df2e4afd13f3 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -41,7 +41,7 @@ class FileNameValidatorTests : AbstractIT() { @Test fun testReservedName() { val result = FileNameValidator.isValid("CON", capability, targetContext) - assertEquals(targetContext.getString(R.string.file_name_validator_error_reserved_names), result) + assertEquals(targetContext.getString(R.string.file_name_validator_error_reserved_names, "CON"), result) } @Test @@ -156,4 +156,20 @@ class FileNameValidatorTests : AbstractIT() { val result = FileNameValidator.checkPath(folderPath, filePaths, capability, targetContext) assertFalse(result) } + + @Test + fun testOnlyFolderPath() { + val folderPath = "/A1/Aaaww/W/C2/" + + val result = FileNameValidator.checkPath(folderPath, listOf(), capability, targetContext) + assertTrue(result) + } + + @Test + fun testOnlyFolderPathWithOneReservedName() { + val folderPath = "/A1/Aaaww/CON/W/C2/" + + val result = FileNameValidator.checkPath(folderPath, listOf(), capability, targetContext) + assertFalse(result) + } } diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index d1bb52628212..fdfce63d3ec4 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -65,7 +65,7 @@ object FileNameValidator { @Suppress("ReturnCount") fun checkPath(folderPath: String, filePaths: List, capability: OCCapability, context: Context): Boolean { - val folderPaths = folderPath.split("/", "\\") + val folderPaths = folderPath.split("/", "\\").filter { it.isNotEmpty() } for (item in folderPaths) { if (isValid(item, capability, context) != null) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index 60d8c944a7bd..2d1a3105fef6 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -55,6 +55,7 @@ import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; +import com.nextcloud.utils.fileNameValidator.FileNameValidator; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.databinding.ReceiveExternalFilesBinding; @@ -661,8 +662,17 @@ public void onClick(View v) { if (id == R.id.uploader_choose_folder) { mUploadPath = ""; // first element in mParents is root dir, represented by ""; // init mUploadPath with "/" results in a "//" prefix + + StringBuilder stringBuilder = new StringBuilder(); for (String p : mParents) { - mUploadPath += p + OCFile.PATH_SEPARATOR; + stringBuilder.append(p).append(OCFile.PATH_SEPARATOR); + } + mUploadPath = stringBuilder.toString(); + + boolean isPathValid = FileNameValidator.INSTANCE.checkPath(mUploadPath, new ArrayList<>(), getCapabilities(), this); + if (!isPathValid) { + DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_copy_or_move); + return; } if (mUploadFromTmpFile) { From 59683a25e17429335817b6a207a795c2fa3a6b98 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 10 Jul 2024 12:35:14 +0200 Subject: [PATCH 23/65] Refactor current logic Signed-off-by: alperozturk --- .../nextcloud/utils/FileNameValidatorTests.kt | 34 ++++++------- .../fileNameValidator/FileNameValidator.kt | 49 +++++++++---------- .../ui/activity/FolderPickerActivity.kt | 10 ++-- .../ReceiveExternalFilesActivity.java | 2 +- .../ui/activity/UploadFilesActivity.java | 2 +- .../ui/dialog/ChooseTemplateDialogFragment.kt | 4 +- .../ui/dialog/CreateFolderDialogFragment.kt | 4 +- .../ui/dialog/RenameFileDialogFragment.kt | 6 +-- 8 files changed, 57 insertions(+), 54 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt index df2e4afd13f3..b0fc815be839 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -34,41 +34,41 @@ class FileNameValidatorTests : AbstractIT() { @Test fun testInvalidCharacter() { - val result = FileNameValidator.isValid("file? = null + existedFileNames: MutableSet? = null ): String? { - if (TextUtils.isEmpty(name)) { + if (TextUtils.isEmpty(filename)) { return context.getString(R.string.filename_empty) } - if (isFileNameAlreadyExist(name, fileNames ?: mutableSetOf())) { + if (isFileNameAlreadyExist(filename, existedFileNames ?: mutableSetOf())) { return context.getString(R.string.file_already_exists) } - if (name.endsWith(" ") || name.endsWith(".")) { + if (filename.endsWith(" ") || filename.endsWith(".")) { return context.getString(R.string.file_name_validator_error_ends_with_space_period) } - checkInvalidCharacters(name, capability, context)?.let { + checkInvalidCharacters(filename, capability, context)?.let { return it } if (capability.forbiddenFilenames.isTrue && ( - reservedWindowsNames.contains(name.uppercase()) || - reservedWindowsNames.contains(name.removeFileExtension().uppercase()) + reservedWindowsNames.contains(filename.uppercase()) || + reservedWindowsNames.contains(filename.removeFileExtension().uppercase()) ) ) { - return context.getString(R.string.file_name_validator_error_reserved_names, name.substringBefore(".")) + return context.getString(R.string.file_name_validator_error_reserved_names, filename.substringBefore(".")) } if (capability.forbiddenFilenameExtension.isTrue) { @@ -63,23 +63,22 @@ object FileNameValidator { return null } - @Suppress("ReturnCount") - fun checkPath(folderPath: String, filePaths: List, capability: OCCapability, context: Context): Boolean { - val folderPaths = folderPath.split("/", "\\").filter { it.isNotEmpty() } - - for (item in folderPaths) { - if (isValid(item, capability, context) != null) { - return false - } - } + fun checkFolderAndFilePaths( + folderPath: String, + filePaths: List, + capability: OCCapability, + context: Context + ): Boolean { + return checkFolderPath(folderPath, capability, context) && checkFilePaths(filePaths, capability, context) + } - for (item in filePaths) { - if (isValid(item, capability, context) != null) { - return false - } - } + fun checkFilePaths(filePaths: List, capability: OCCapability, context: Context): Boolean { + return filePaths.all { checkFileName(it, capability, context) == null } + } - return true + fun checkFolderPath(folderPath: String, capability: OCCapability, context: Context): Boolean { + return folderPath.split("[/\\\\]".toRegex()) + .none { it.isNotEmpty() && checkFileName(it, capability, context) != null } } @Suppress("ReturnCount") diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index a398217ab081..d422814b52f4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -455,9 +455,7 @@ open class FolderPickerActivity : } targetFilePaths?.let { filePaths -> - - val isPathValid = FileNameValidator.checkPath(file.remotePath, filePaths, capabilities, this) - if (!isPathValid) { + if (!isFolderAndFilePathsValid(filePaths)) { DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_copy_or_move) Handler(Looper.getMainLooper()).postDelayed({ setResult(RESULT_CANCELED, resultData) @@ -477,6 +475,12 @@ open class FolderPickerActivity : finish() } + private fun isFolderAndFilePathsValid(filePaths: List): Boolean { + val isFolderPathValid = FileNameValidator.checkFolderPath(file.remotePath, capabilities, this) + val isFilePathsValid = FileNameValidator.checkFilePaths(filePaths, capabilities, this) + return isFilePathsValid && isFolderPathValid + } + override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { super.onRemoteOperationFinish(operation, result) if (operation is CreateFolderOperation) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index 2d1a3105fef6..c71724ca02f2 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -669,7 +669,7 @@ public void onClick(View v) { } mUploadPath = stringBuilder.toString(); - boolean isPathValid = FileNameValidator.INSTANCE.checkPath(mUploadPath, new ArrayList<>(), getCapabilities(), this); + boolean isPathValid = FileNameValidator.INSTANCE.checkFolderPath(mUploadPath, getCapabilities(), this); if (!isPathValid) { DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_copy_or_move); return; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java index 67b75994a0df..01d93439542a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java @@ -662,7 +662,7 @@ public void onClick(View v) { private String checkFileNameBeforeUpload(String[] selectedFilePaths) { for (String filePath : selectedFilePaths) { File file = new File(filePath); - String filenameErrorMessage = FileNameValidator.INSTANCE.isValid(file.getName(), getCapabilities(), this, null); + String filenameErrorMessage = FileNameValidator.INSTANCE.checkFileName(file.getName(), getCapabilities(), this, null); if (filenameErrorMessage != null) { return filenameErrorMessage; diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index 50e30884f5e0..6b7a7a1dda71 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -224,7 +224,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem val path = parentFolder?.remotePath + name val selectedTemplate = adapter?.selectedTemplate - val errorMessage = FileNameValidator.isValid(name, getOCCapability(), requireContext()) + val errorMessage = FileNameValidator.checkFileName(name, getOCCapability(), requireContext()) when { selectedTemplate == null -> { @@ -256,7 +256,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem DOT + selectedTemplate.extension, ignoreCase = true ) - val fileNameValidatorResult = FileNameValidator.isValid(name, getOCCapability(), requireContext()) + val fileNameValidatorResult = FileNameValidator.checkFileName(name, getOCCapability(), requireContext()) val errorMessage = when { isNameJustExtension -> null diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index 01f0fe39f244..1679ba3ef59e 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -125,7 +125,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList val newFileName = binding.userInput.text?.toString()?.trim() ?: "" val fileNameValidatorResult: String? = - FileNameValidator.isValid(newFileName, getOCCapability(), requireContext(), fileNames) + FileNameValidator.checkFileName(newFileName, getOCCapability(), requireContext(), fileNames) val errorMessage = when { newFileName.isEmpty() -> null @@ -163,7 +163,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList val newFolderName = (getDialog()?.findViewById(R.id.user_input) as TextView) .text.toString().trim { it <= ' ' } - val errorMessage: String? = FileNameValidator.isValid(newFolderName, getOCCapability(), requireContext()) + val errorMessage: String? = FileNameValidator.checkFileName(newFolderName, getOCCapability(), requireContext()) if (errorMessage != null) { DisplayUtils.showSnackMessage(requireActivity(), errorMessage) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt index df21b752b1c6..31b8b1d19359 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt @@ -25,7 +25,7 @@ import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.di.Injectable import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.fileNameValidator.FileNameValidator.isFileHidden -import com.nextcloud.utils.fileNameValidator.FileNameValidator.isValid +import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFileName import com.owncloud.android.R import com.owncloud.android.databinding.EditBoxDialogBinding import com.owncloud.android.datamodel.FileDataStorageManager @@ -138,7 +138,7 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen newFileName = binding.userInput.text.toString().trim { it <= ' ' } } - val errorMessage = isValid(newFileName, oCCapability, requireContext(), null) + val errorMessage = checkFileName(newFileName, oCCapability, requireContext(), null) if (errorMessage != null) { DisplayUtils.showSnackMessage(requireActivity(), errorMessage) return @@ -163,7 +163,7 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen newFileName = binding.userInput.text.toString().trim { it <= ' ' } } - val errorMessage = isValid(newFileName, oCCapability, requireContext(), fileNames) + val errorMessage = checkFileName(newFileName, oCCapability, requireContext(), fileNames) if (isFileHidden(newFileName)) { binding.userInputContainer.error = getText(R.string.hidden_file_name_warning) From 63e7c298caa2fd418cf99a9251c983d4b56ef88a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 10 Jul 2024 12:45:10 +0200 Subject: [PATCH 24/65] add documentation Signed-off-by: alperozturk --- .../fileNameValidator/FileNameValidator.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index a29fbedb4d8f..0dd7f91eedc3 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -24,6 +24,15 @@ object FileNameValidator { "LPT¹", "LPT²", "LPT³" ) + /** + * Checks the validity of a file name. + * + * @param filename The name of the file to validate. + * @param capability The capabilities affecting the validation criteria such as forbiddenFilenames, forbiddenCharacters. + * @param context The context used for retrieving error messages. + * @param existedFileNames Set of existing file names to avoid duplicates. + * @return An error message if the filename is invalid, null otherwise. + */ @Suppress("ReturnCount") fun checkFileName( filename: String, @@ -63,6 +72,15 @@ object FileNameValidator { return null } + /** + * Checks the validity of file paths wanted to move or copied inside the folder. + * + * @param folderPath Target folder to be used for move or copy. + * @param filePaths The list of file paths to move or copy to folderPath. + * @param capability The capabilities affecting the validation criteria. + * @param context The context used for retrieving error messages. + * @return True if folder path and file paths are valid, false otherwise. + */ fun checkFolderAndFilePaths( folderPath: String, filePaths: List, From 27774fca7863f0e997290f2748786ef773e0f51b Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 10 Jul 2024 14:57:21 +0200 Subject: [PATCH 25/65] Check file names for upload content from other apps Signed-off-by: alperozturk --- .../ui/activity/FolderPickerActivity.kt | 2 +- .../ReceiveExternalFilesActivity.java | 2 +- .../android/ui/helpers/UriUploader.kt | 21 ++++++++++++++++++- app/src/main/res/values/strings.xml | 3 ++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index d422814b52f4..834de5573346 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -456,7 +456,7 @@ open class FolderPickerActivity : targetFilePaths?.let { filePaths -> if (!isFolderAndFilePathsValid(filePaths)) { - DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_copy_or_move) + DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters) Handler(Looper.getMainLooper()).postDelayed({ setResult(RESULT_CANCELED, resultData) finish() diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index c71724ca02f2..b5dae3bbb1a8 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -671,7 +671,7 @@ public void onClick(View v) { boolean isPathValid = FileNameValidator.INSTANCE.checkFolderPath(mUploadPath, getCapabilities(), this); if (!isPathValid) { - DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_copy_or_move); + DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters); return; } diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt b/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt index 3db05a6805e8..e58b8b5155dc 100644 --- a/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt +++ b/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt @@ -15,6 +15,7 @@ import android.net.Uri import android.os.Parcelable import com.nextcloud.client.account.User import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.R import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.utils.Log_OC @@ -23,6 +24,7 @@ import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.asynctasks.CopyAndUploadContentUrisTask import com.owncloud.android.ui.asynctasks.CopyAndUploadContentUrisTask.OnCopyTmpFilesTaskListener import com.owncloud.android.ui.fragment.TaskRetainerFragment +import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.UriUtils.getDisplayNameForUri /** @@ -70,9 +72,26 @@ class UriUploader( Log_OC.e(TAG, "Sensitive URI detected, aborting upload.") code = UriUploaderResultCode.ERROR_SENSITIVE_PATH } else { - val uris = mUrisToUpload.filterNotNull() + var isInvalidPathMessageDisplayed = false + val uris = mUrisToUpload + .filterNotNull() .map { it as Uri } .map { Pair(it, getRemotePathForUri(it)) } + .filter { (_, filename) -> + val isValid = (FileNameValidator.checkFileName( + filename.removePrefix("/"), + mActivity.capabilities, + mActivity, null + ) == null) + if (!isValid && !isInvalidPathMessageDisplayed) { + isInvalidPathMessageDisplayed = true + DisplayUtils.showSnackMessage( + mActivity, + R.string.file_name_validator_upload_content_error + ) + } + isValid + } val fileUris = uris .filter { it.first.scheme == ContentResolver.SCHEME_FILE } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e79e65de1b7b..b8bbe7a445cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1220,7 +1220,8 @@ Permissions are required to open search result otherwise it will redirected to web… - Folder path contains reserved names or invalid character + Some contents cannot able to uploaded due to contains reserved names or invalid character. + Folder path contains reserved names or invalid character File name contains invalid characters: %s %s is a reserved name File name ends with a space or a period From aa6783094148a96daf1615aa2075df55c182474c Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 11 Jul 2024 12:18:36 +0200 Subject: [PATCH 26/65] Add todos Signed-off-by: alperozturk --- .../com/nextcloud/utils/fileNameValidator/FileNameValidator.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 0dd7f91eedc3..ddf0e64484a1 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -44,6 +44,8 @@ object FileNameValidator { return context.getString(R.string.filename_empty) } + // TODO must only work for live type + // TODO Native Files app can create forbidden file names we need to check it as well if (isFileNameAlreadyExist(filename, existedFileNames ?: mutableSetOf())) { return context.getString(R.string.file_already_exists) } From eeb711052ee7cab88a37d14eb65136ab81515973 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 17 Jul 2024 10:12:24 +0200 Subject: [PATCH 27/65] Add dot and space strings globally Signed-off-by: alperozturk --- .../java/com/nextcloud/utils/extensions/StringExtensions.kt | 4 ++++ .../nextcloud/utils/fileNameValidator/FileNameValidator.kt | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt index ce44fcf9a4d1..0bd25d1b26ad 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt @@ -24,3 +24,7 @@ fun String.removeFileExtension(): String { this } } + +fun dot(): String = "." + +fun space(): String = " " diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index ddf0e64484a1..4eeee888b6ea 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -9,7 +9,9 @@ package com.nextcloud.utils.fileNameValidator import android.content.Context import android.text.TextUtils +import com.nextcloud.utils.extensions.dot import com.nextcloud.utils.extensions.removeFileExtension +import com.nextcloud.utils.extensions.space import com.owncloud.android.R import com.owncloud.android.lib.resources.status.OCCapability @@ -50,7 +52,7 @@ object FileNameValidator { return context.getString(R.string.file_already_exists) } - if (filename.endsWith(" ") || filename.endsWith(".")) { + if (filename.endsWith(space()) || filename.endsWith(dot())) { return context.getString(R.string.file_name_validator_error_ends_with_space_period) } From ab4b63f327323491642e89e792cef860744a80f2 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 17 Jul 2024 10:13:59 +0200 Subject: [PATCH 28/65] Check isFileNameAlreadyExist when existedFileNames available Signed-off-by: alperozturk --- .../nextcloud/utils/fileNameValidator/FileNameValidator.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 4eeee888b6ea..db8ab9142acc 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -46,10 +46,11 @@ object FileNameValidator { return context.getString(R.string.filename_empty) } - // TODO must only work for live type // TODO Native Files app can create forbidden file names we need to check it as well - if (isFileNameAlreadyExist(filename, existedFileNames ?: mutableSetOf())) { - return context.getString(R.string.file_already_exists) + existedFileNames?.let { + if (isFileNameAlreadyExist(filename, existedFileNames)) { + return context.getString(R.string.file_already_exists) + } } if (filename.endsWith(space()) || filename.endsWith(dot())) { From 03281fa85e66bb0398e222592198e89bfbad917e Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 17 Jul 2024 10:48:10 +0200 Subject: [PATCH 29/65] Check folder creation from other file manager apps Signed-off-by: alperozturk --- .../android/providers/DocumentsStorageProvider.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java b/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java index ec6e15c161d0..d2c8749ea6c7 100644 --- a/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java @@ -34,6 +34,8 @@ import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferencesImpl; import com.nextcloud.client.utils.HashUtil; +import com.nextcloud.utils.extensions.ContextExtensionsKt; +import com.nextcloud.utils.fileNameValidator.FileNameValidator; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -47,6 +49,7 @@ import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation; import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; +import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.CopyFileOperation; import com.owncloud.android.operations.CreateFolderOperation; import com.owncloud.android.operations.DownloadFileOperation; @@ -59,6 +62,7 @@ import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.FileUtil; import com.owncloud.android.utils.MimeTypeUtil; +import com.owncloud.android.utils.theme.CapabilityUtils; import org.nextcloud.providers.cursors.FileCursor; import org.nextcloud.providers.cursors.RootCursor; @@ -469,6 +473,13 @@ public String createDocument(String documentId, String mimeType, String displayN Document folderDocument = toDocument(documentId); + OCCapability capabilities = CapabilityUtils.getCapability(accountManager.getUser(), getNonNullContext()); + String errorMessage = FileNameValidator.INSTANCE.checkFileName(displayName, capabilities, getNonNullContext(),null);; + if (errorMessage != null) { + ContextExtensionsKt.showToast(getNonNullContext(), errorMessage); + return null; + } + if (DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType)) { return createFolder(folderDocument, displayName); } else { From ec3e9cfc0c6095c8438021a6b5259ae492adff65 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 17 Jul 2024 10:48:37 +0200 Subject: [PATCH 30/65] Check folder creation from other file manager apps Signed-off-by: alperozturk --- .../com/nextcloud/utils/fileNameValidator/FileNameValidator.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index db8ab9142acc..0baffb495c7c 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -46,7 +46,6 @@ object FileNameValidator { return context.getString(R.string.filename_empty) } - // TODO Native Files app can create forbidden file names we need to check it as well existedFileNames?.let { if (isFileNameAlreadyExist(filename, existedFileNames)) { return context.getString(R.string.file_already_exists) From 0a802e883325d69d068f1a5fa59a824aab5db624 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 17 Jul 2024 11:58:20 +0200 Subject: [PATCH 31/65] Check template creation Signed-off-by: alperozturk --- ...ooseRichDocumentsTemplateDialogFragment.kt | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt index 34dbc63d150d..7beefa72f2cc 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt @@ -27,6 +27,7 @@ import com.nextcloud.client.di.Injectable import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.ChooseTemplateBinding @@ -211,22 +212,23 @@ class ChooseRichDocumentsTemplateDialogFragment : Type.PRESENTATION -> { R.string.create_new_presentation } - - else -> R.string.select_template } } @Suppress("DEPRECATION") private fun createFromTemplate(template: Template, path: String) { - waitDialog = newInstance(R.string.wait_a_moment, false) - waitDialog?.show(parentFragmentManager, WAIT_DIALOG_TAG) + waitDialog = newInstance(R.string.wait_a_moment, false).also { + it.show(parentFragmentManager, WAIT_DIALOG_TAG) + } CreateFileFromTemplateTask(this, client, template, path, currentAccount.user).execute() } @SuppressLint("NotifyDataSetChanged") fun setTemplateList(templateList: List