aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-05-02 20:32:58 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-05-07 16:10:54 +0200
commit30a981bc5c4d51c810bd8cb6f66663ffefca5a70 (patch)
tree6dfe474931a082915897090a9232052b1c21b52d
parent60838bf5f439f7ff2f7d4bc6fb1362f320feac32 (diff)
downloadnextcloud-server-fix/allow-enforcing-windows-support.tar.gz
nextcloud-server-fix/allow-enforcing-windows-support.zip
feat: Allow to enforce Windows compatible file and folder namesfix/allow-enforcing-windows-support
This will: * Deny characters forbidden on Windows * Deny files with names which are reserved on Windows * Deny trailing dot or space * Deny files or folders which are not case-insensitive unique in a folder Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r--config/config.sample.php15
-rw-r--r--lib/private/Files/Storage/Common.php29
-rw-r--r--lib/private/Files/View.php565
-rw-r--r--lib/public/Util.php25
4 files changed, 348 insertions, 286 deletions
diff --git a/config/config.sample.php b/config/config.sample.php
index ab582ba79df..b2a5cd15aa7 100644
--- a/config/config.sample.php
+++ b/config/config.sample.php
@@ -1961,6 +1961,21 @@ $CONFIG = [
'updatedirectory' => '',
/**
+ * Allow to enforce Windows compatible file and folder names.
+ * Nextcloud by default supports all files valid on Linux,
+ * but as Windows has some stricter filename rules this can lead to sync errors when using Windows clients.
+ *
+ * To enforce only Windows compatible filenames, also on the webui, set this value to ``true``.
+ *
+ * This will deny filenames with characters not valid on Windows, as well as some reserved filenames and nameing rules (no trailing dot or space).
+ * Additionally this will enforce files to be case in-sensitivly unique in a folder.
+ *
+ * Defaults to ``false``
+ *
+ */
+'enforce_windows_compatibility' => false,
+
+/**
* Deny a specific file or files and disallow the upload of files
* with this name. ``.htaccess`` is blocked by default.
*
diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php
index d2962008e40..818a08ac5ec 100644
--- a/lib/private/Files/Storage/Common.php
+++ b/lib/private/Files/Storage/Common.php
@@ -63,6 +63,7 @@ use OCP\Files\Storage\ILockingStorage;
use OCP\Files\Storage\IStorage;
use OCP\Files\Storage\IWriteStreamStorage;
use OCP\Files\StorageNotAvailableException;
+use OCP\IConfig;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use OCP\Server;
@@ -375,7 +376,7 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
}
if (!isset($this->watcher)) {
$this->watcher = new Watcher($storage);
- $globalPolicy = \OC::$server->getConfig()->getSystemValue('filesystem_check_changes', Watcher::CHECK_NEVER);
+ $globalPolicy = \OCP\Server::get(IConfig::class)->getSystemValue('filesystem_check_changes', Watcher::CHECK_NEVER);
$this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
}
return $this->watcher;
@@ -565,6 +566,32 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
if (\OC\Files\Filesystem::hasFilenameInvalidCharacters($fileName)) {
throw new InvalidCharacterInPathException();
}
+
+ $config = \OCP\Server::get(IConfig::class);
+ if ($config->getSystemValueBool('enforce_windows_compatibility', false)) {
+ // Windows does not allow filenames to end with a trailing dot or space
+ if (str_ends_with($fileName, '.') || str_ends_with($fileName, ' ')) {
+ throw new InvalidCharacterInPathException('Filenames must not end with a dot or space');
+ }
+
+ // Windows has path namespaces so e.g. `NUL` is a reserved word,
+ // but `NUL.txt` or `NUL.tar.gz` is considered the same and thus also reserved.
+ $basename = substr($fileName, 0, strpos($fileName, '.') ?: null);
+ if (\OC\Files\Filesystem::isFileBlacklisted($basename)) {
+ throw new ReservedWordException();
+ }
+
+ // Some windows systems are case insensitive,
+ // so to guarantee files can be synced we need to enfore case insensitivity
+ $content = $this->getDirectoryContent(dirname($path));
+ $fileName = strtolower($fileName);
+ foreach ($content as $subPath) {
+ if (strtolower($subPath['name']) === $fileName) {
+ throw new InvalidPathException('Filename is not case insensitivly unique');
+ }
+ }
+ }
+
// NOTE: $path will remain unverified for now
}
diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php
index 98b597dbd4d..b1089d6acf4 100644
--- a/lib/private/Files/View.php
+++ b/lib/private/Files/View.php
@@ -621,65 +621,61 @@ class View {
* @param string|resource $data
* @return bool|mixed
* @throws LockedException
+ * @throws InvalidPathException When path is not within the expected root
*/
public function file_put_contents($path, $data) {
- if (is_resource($data)) { //not having to deal with streams in file_put_contents makes life easier
+ if (!is_resource($data)) {
+ //not having to deal with streams in file_put_contents makes life easier
+ $hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
+ return $this->basicOperation('file_put_contents', $path, $hooks, $data);
+ } else {
$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
- if (Filesystem::isValidPath($path)
- && !Filesystem::isFileBlacklisted($path)
- ) {
- $path = $this->getRelativePath($absolutePath);
- if ($path === null) {
- throw new InvalidPathException("Path $absolutePath is not in the expected root");
- }
+ $path = $this->getRelativePath($absolutePath);
+ if ($path === null) {
+ throw new InvalidPathException("Path $absolutePath is not in the expected root");
+ }
+ $this->verifyPath(dirname($path), basename($path));
- $this->lockFile($path, ILockingProvider::LOCK_SHARED);
+ $this->lockFile($path, ILockingProvider::LOCK_SHARED);
- $exists = $this->file_exists($path);
- $run = true;
- if ($this->shouldEmitHooks($path)) {
- $this->emit_file_hooks_pre($exists, $path, $run);
- }
- if (!$run) {
- $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
- return false;
- }
+ $exists = $this->file_exists($path);
+ $run = true;
+ if ($this->shouldEmitHooks($path)) {
+ $this->emit_file_hooks_pre($exists, $path, $run);
+ }
+ if (!$run) {
+ $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
+ return false;
+ }
- try {
- $this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
- } catch (\Exception $e) {
- // Release the shared lock before throwing.
- $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
- throw $e;
- }
+ try {
+ $this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
+ } catch (\Exception $e) {
+ // Release the shared lock before throwing.
+ $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
+ throw $e;
+ }
- /** @var Storage $storage */
- [$storage, $internalPath] = $this->resolvePath($path);
- $target = $storage->fopen($internalPath, 'w');
- if ($target) {
- [, $result] = \OC_Helper::streamCopy($data, $target);
- fclose($target);
- fclose($data);
+ /** @var Storage $storage */
+ [$storage, $internalPath] = $this->resolvePath($path);
+ $target = $storage->fopen($internalPath, 'w');
+ if ($target) {
+ [, $result] = \OC_Helper::streamCopy($data, $target);
+ fclose($target);
+ fclose($data);
- $this->writeUpdate($storage, $internalPath);
+ $this->writeUpdate($storage, $internalPath);
- $this->changeLock($path, ILockingProvider::LOCK_SHARED);
+ $this->changeLock($path, ILockingProvider::LOCK_SHARED);
- if ($this->shouldEmitHooks($path) && $result !== false) {
- $this->emit_file_hooks_post($exists, $path);
- }
- $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
- return $result;
- } else {
- $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
- return false;
+ if ($this->shouldEmitHooks($path) && $result !== false) {
+ $this->emit_file_hooks_post($exists, $path);
}
+ $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
+ return $result;
} else {
return false;
}
- } else {
- $hooks = $this->file_exists($path) ? ['update', 'write'] : ['create', 'write'];
- return $this->basicOperation('file_put_contents', $path, $hooks, $data);
}
}
@@ -735,127 +731,125 @@ class View {
$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
$targetParts = explode('/', $absolutePath2);
$targetUser = $targetParts[1] ?? null;
- $result = false;
- if (
- Filesystem::isValidPath($target)
- && Filesystem::isValidPath($source)
- && !Filesystem::isFileBlacklisted($target)
- ) {
- $source = $this->getRelativePath($absolutePath1);
- $target = $this->getRelativePath($absolutePath2);
- $exists = $this->file_exists($target);
- if ($source == null || $target == null) {
- return false;
- }
+ if (!Filesystem::isValidPath($source) || !Filesystem::isValidPath($target)) {
+ return false;
+ }
- $this->lockFile($source, ILockingProvider::LOCK_SHARED, true);
- try {
- $this->lockFile($target, ILockingProvider::LOCK_SHARED, true);
-
- $run = true;
- if ($this->shouldEmitHooks($source) && (Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target))) {
- // if it was a rename from a part file to a regular file it was a write and not a rename operation
- $this->emit_file_hooks_pre($exists, $target, $run);
- } elseif ($this->shouldEmitHooks($source)) {
- $sourcePath = $this->getHookPath($source);
- $targetPath = $this->getHookPath($target);
- if ($sourcePath !== null && $targetPath !== null) {
- \OC_Hook::emit(
- Filesystem::CLASSNAME, Filesystem::signal_rename,
- [
- Filesystem::signal_param_oldpath => $sourcePath,
- Filesystem::signal_param_newpath => $targetPath,
- Filesystem::signal_param_run => &$run
- ]
- );
- }
+ $source = $this->getRelativePath($absolutePath1);
+ $target = $this->getRelativePath($absolutePath2);
+ $exists = $this->file_exists($target);
+ if ($source == null || $target == null) {
+ return false;
+ }
+
+ $this->verifyPath(dirname($target), basename($target));
+
+ $result = false;
+ $this->lockFile($source, ILockingProvider::LOCK_SHARED, true);
+ try {
+ $this->lockFile($target, ILockingProvider::LOCK_SHARED, true);
+
+ $run = true;
+ if ($this->shouldEmitHooks($source) && (Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target))) {
+ // if it was a rename from a part file to a regular file it was a write and not a rename operation
+ $this->emit_file_hooks_pre($exists, $target, $run);
+ } elseif ($this->shouldEmitHooks($source)) {
+ $sourcePath = $this->getHookPath($source);
+ $targetPath = $this->getHookPath($target);
+ if ($sourcePath !== null && $targetPath !== null) {
+ \OC_Hook::emit(
+ Filesystem::CLASSNAME, Filesystem::signal_rename,
+ [
+ Filesystem::signal_param_oldpath => $sourcePath,
+ Filesystem::signal_param_newpath => $targetPath,
+ Filesystem::signal_param_run => &$run
+ ]
+ );
}
- if ($run) {
- $this->verifyPath(dirname($target), basename($target));
-
- $manager = Filesystem::getMountManager();
- $mount1 = $this->getMount($source);
- $mount2 = $this->getMount($target);
- $storage1 = $mount1->getStorage();
- $storage2 = $mount2->getStorage();
- $internalPath1 = $mount1->getInternalPath($absolutePath1);
- $internalPath2 = $mount2->getInternalPath($absolutePath2);
-
- $this->changeLock($source, ILockingProvider::LOCK_EXCLUSIVE, true);
- try {
- $this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
-
- if ($internalPath1 === '') {
- if ($mount1 instanceof MoveableMount) {
- $sourceParentMount = $this->getMount(dirname($source));
- if ($sourceParentMount === $mount2 && $this->targetIsNotShared($targetUser, $absolutePath2)) {
- /**
- * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
- */
- $sourceMountPoint = $mount1->getMountPoint();
- $result = $mount1->moveMount($absolutePath2);
- $manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
- } else {
- $result = false;
- }
- } else {
- $result = false;
- }
- // moving a file/folder within the same mount point
- } elseif ($storage1 === $storage2) {
- if ($storage1) {
- $result = $storage1->rename($internalPath1, $internalPath2);
+ }
+ if ($run) {
+ $manager = Filesystem::getMountManager();
+ $mount1 = $this->getMount($source);
+ $mount2 = $this->getMount($target);
+ $storage1 = $mount1->getStorage();
+ $storage2 = $mount2->getStorage();
+ $internalPath1 = $mount1->getInternalPath($absolutePath1);
+ $internalPath2 = $mount2->getInternalPath($absolutePath2);
+
+ $this->changeLock($source, ILockingProvider::LOCK_EXCLUSIVE, true);
+ try {
+ $this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);
+
+ if ($internalPath1 === '') {
+ if ($mount1 instanceof MoveableMount) {
+ $sourceParentMount = $this->getMount(dirname($source));
+ if ($sourceParentMount === $mount2 && $this->targetIsNotShared($targetUser, $absolutePath2)) {
+ /**
+ * @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
+ */
+ $sourceMountPoint = $mount1->getMountPoint();
+ $result = $mount1->moveMount($absolutePath2);
+ $manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
} else {
$result = false;
}
- // moving a file/folder between storages (from $storage1 to $storage2)
} else {
- $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
+ $result = false;
}
-
- if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
- // if it was a rename from a part file to a regular file it was a write and not a rename operation
- $this->writeUpdate($storage2, $internalPath2);
- } elseif ($result) {
- if ($internalPath1 !== '') { // don't do a cache update for moved mounts
- $this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2);
- }
+ // moving a file/folder within the same mount point
+ } elseif ($storage1 === $storage2) {
+ if ($storage1) {
+ $result = $storage1->rename($internalPath1, $internalPath2);
+ } else {
+ $result = false;
}
- } catch (\Exception $e) {
- throw $e;
- } finally {
- $this->changeLock($source, ILockingProvider::LOCK_SHARED, true);
- $this->changeLock($target, ILockingProvider::LOCK_SHARED, true);
+ // moving a file/folder between storages (from $storage1 to $storage2)
+ } else {
+ $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
}
if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
- if ($this->shouldEmitHooks()) {
- $this->emit_file_hooks_post($exists, $target);
- }
+ // if it was a rename from a part file to a regular file it was a write and not a rename operation
+ $this->writeUpdate($storage2, $internalPath2);
} elseif ($result) {
- if ($this->shouldEmitHooks($source) && $this->shouldEmitHooks($target)) {
- $sourcePath = $this->getHookPath($source);
- $targetPath = $this->getHookPath($target);
- if ($sourcePath !== null && $targetPath !== null) {
- \OC_Hook::emit(
- Filesystem::CLASSNAME,
- Filesystem::signal_post_rename,
- [
- Filesystem::signal_param_oldpath => $sourcePath,
- Filesystem::signal_param_newpath => $targetPath,
- ]
- );
- }
+ if ($internalPath1 !== '') { // don't do a cache update for moved mounts
+ $this->renameUpdate($storage1, $storage2, $internalPath1, $internalPath2);
+ }
+ }
+ } catch (\Exception $e) {
+ throw $e;
+ } finally {
+ $this->changeLock($source, ILockingProvider::LOCK_SHARED, true);
+ $this->changeLock($target, ILockingProvider::LOCK_SHARED, true);
+ }
+
+ if ((Cache\Scanner::isPartialFile($source) && !Cache\Scanner::isPartialFile($target)) && $result !== false) {
+ if ($this->shouldEmitHooks()) {
+ $this->emit_file_hooks_post($exists, $target);
+ }
+ } elseif ($result) {
+ if ($this->shouldEmitHooks($source) && $this->shouldEmitHooks($target)) {
+ $sourcePath = $this->getHookPath($source);
+ $targetPath = $this->getHookPath($target);
+ if ($sourcePath !== null && $targetPath !== null) {
+ \OC_Hook::emit(
+ Filesystem::CLASSNAME,
+ Filesystem::signal_post_rename,
+ [
+ Filesystem::signal_param_oldpath => $sourcePath,
+ Filesystem::signal_param_newpath => $targetPath,
+ ]
+ );
}
}
}
- } catch (\Exception $e) {
- throw $e;
- } finally {
- $this->unlockFile($source, ILockingProvider::LOCK_SHARED, true);
- $this->unlockFile($target, ILockingProvider::LOCK_SHARED, true);
}
+ } catch (\Exception $e) {
+ throw $e;
+ } finally {
+ $this->unlockFile($source, ILockingProvider::LOCK_SHARED, true);
+ $this->unlockFile($target, ILockingProvider::LOCK_SHARED, true);
}
return $result;
}
@@ -872,83 +866,81 @@ class View {
public function copy($source, $target, $preserveMtime = false) {
$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));
+
+ if (!Filesystem::isValidPath($source) || !Filesystem::isValidPath($target)) {
+ return false;
+ }
+
+ $source = $this->getRelativePath($absolutePath1);
+ $target = $this->getRelativePath($absolutePath2);
+ if ($source == null || $target == null) {
+ return false;
+ }
+
+ $this->verifyPath(dirname($target), basename($target));
+
+ $this->lockFile($target, ILockingProvider::LOCK_SHARED);
+ $this->lockFile($source, ILockingProvider::LOCK_SHARED);
+ $lockTypePath1 = ILockingProvider::LOCK_SHARED;
+ $lockTypePath2 = ILockingProvider::LOCK_SHARED;
+
$result = false;
- if (
- Filesystem::isValidPath($target)
- && Filesystem::isValidPath($source)
- && !Filesystem::isFileBlacklisted($target)
- ) {
- $source = $this->getRelativePath($absolutePath1);
- $target = $this->getRelativePath($absolutePath2);
-
- if ($source == null || $target == null) {
- return false;
- }
+ try {
$run = true;
+ $exists = $this->file_exists($target);
+ if ($this->shouldEmitHooks()) {
+ \OC_Hook::emit(
+ Filesystem::CLASSNAME,
+ Filesystem::signal_copy,
+ [
+ Filesystem::signal_param_oldpath => $this->getHookPath($source),
+ Filesystem::signal_param_newpath => $this->getHookPath($target),
+ Filesystem::signal_param_run => &$run
+ ]
+ );
+ $this->emit_file_hooks_pre($exists, $target, $run);
+ }
+ if ($run) {
+ $mount1 = $this->getMount($source);
+ $mount2 = $this->getMount($target);
+ $storage1 = $mount1->getStorage();
+ $internalPath1 = $mount1->getInternalPath($absolutePath1);
+ $storage2 = $mount2->getStorage();
+ $internalPath2 = $mount2->getInternalPath($absolutePath2);
+
+ $this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE);
+ $lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE;
+
+ if ($mount1->getMountPoint() == $mount2->getMountPoint()) {
+ if ($storage1) {
+ $result = $storage1->copy($internalPath1, $internalPath2);
+ } else {
+ $result = false;
+ }
+ } else {
+ $result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2);
+ }
- $this->lockFile($target, ILockingProvider::LOCK_SHARED);
- $this->lockFile($source, ILockingProvider::LOCK_SHARED);
- $lockTypePath1 = ILockingProvider::LOCK_SHARED;
- $lockTypePath2 = ILockingProvider::LOCK_SHARED;
+ $this->writeUpdate($storage2, $internalPath2);
- try {
- $exists = $this->file_exists($target);
- if ($this->shouldEmitHooks()) {
+ $this->changeLock($target, ILockingProvider::LOCK_SHARED);
+ $lockTypePath2 = ILockingProvider::LOCK_SHARED;
+
+ if ($this->shouldEmitHooks() && $result !== false) {
\OC_Hook::emit(
Filesystem::CLASSNAME,
- Filesystem::signal_copy,
+ Filesystem::signal_post_copy,
[
Filesystem::signal_param_oldpath => $this->getHookPath($source),
- Filesystem::signal_param_newpath => $this->getHookPath($target),
- Filesystem::signal_param_run => &$run
+ Filesystem::signal_param_newpath => $this->getHookPath($target)
]
);
- $this->emit_file_hooks_pre($exists, $target, $run);
- }
- if ($run) {
- $mount1 = $this->getMount($source);
- $mount2 = $this->getMount($target);
- $storage1 = $mount1->getStorage();
- $internalPath1 = $mount1->getInternalPath($absolutePath1);
- $storage2 = $mount2->getStorage();
- $internalPath2 = $mount2->getInternalPath($absolutePath2);
-
- $this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE);
- $lockTypePath2 = ILockingProvider::LOCK_EXCLUSIVE;
-
- if ($mount1->getMountPoint() == $mount2->getMountPoint()) {
- if ($storage1) {
- $result = $storage1->copy($internalPath1, $internalPath2);
- } else {
- $result = false;
- }
- } else {
- $result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2);
- }
-
- $this->writeUpdate($storage2, $internalPath2);
-
- $this->changeLock($target, ILockingProvider::LOCK_SHARED);
- $lockTypePath2 = ILockingProvider::LOCK_SHARED;
-
- if ($this->shouldEmitHooks() && $result !== false) {
- \OC_Hook::emit(
- Filesystem::CLASSNAME,
- Filesystem::signal_post_copy,
- [
- Filesystem::signal_param_oldpath => $this->getHookPath($source),
- Filesystem::signal_param_newpath => $this->getHookPath($target)
- ]
- );
- $this->emit_file_hooks_post($exists, $target);
- }
+ $this->emit_file_hooks_post($exists, $target);
}
- } catch (\Exception $e) {
- $this->unlockFile($target, $lockTypePath2);
- $this->unlockFile($source, $lockTypePath1);
- throw $e;
}
-
+ } catch (\Exception $e) {
+ throw $e;
+ } finally {
$this->unlockFile($target, $lockTypePath2);
$this->unlockFile($source, $lockTypePath1);
}
@@ -1132,92 +1124,95 @@ class View {
private function basicOperation(string $operation, string $path, array $hooks = [], $extraParam = null) {
$postFix = (substr($path, -1) === '/') ? '/' : '';
$absolutePath = Filesystem::normalizePath($this->getAbsolutePath($path));
- if (Filesystem::isValidPath($path)
- && !Filesystem::isFileBlacklisted($path)
- ) {
- $path = $this->getRelativePath($absolutePath);
- if ($path == null) {
- return false;
- }
- if (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks)) {
- // always a shared lock during pre-hooks so the hook can read the file
- $this->lockFile($path, ILockingProvider::LOCK_SHARED);
- }
+ if (!Filesystem::isValidPath($path)) {
+ return false;
+ }
- $run = $this->runHooks($hooks, $path);
- [$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
- if ($run && $storage) {
- /** @var Storage $storage */
- if (in_array('write', $hooks) || in_array('delete', $hooks)) {
- try {
- $this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
- } catch (LockedException $e) {
- // release the shared lock we acquired before quitting
- $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
- throw $e;
- }
- }
+ $path = $this->getRelativePath($absolutePath);
+ if ($path == null) {
+ return false;
+ }
+
+ $this->verifyPath(dirname($path), basename($path));
+
+ if (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks)) {
+ // always a shared lock during pre-hooks so the hook can read the file
+ $this->lockFile($path, ILockingProvider::LOCK_SHARED);
+ }
+
+ $run = $this->runHooks($hooks, $path);
+ [$storage, $internalPath] = Filesystem::resolvePath($absolutePath . $postFix);
+ if ($run && $storage) {
+ /** @var Storage $storage */
+ if (in_array('write', $hooks) || in_array('delete', $hooks)) {
try {
- if (!is_null($extraParam)) {
- $result = $storage->$operation($internalPath, $extraParam);
- } else {
- $result = $storage->$operation($internalPath);
- }
- } catch (\Exception $e) {
- if (in_array('write', $hooks) || in_array('delete', $hooks)) {
- $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
- } elseif (in_array('read', $hooks)) {
- $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
- }
+ $this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE);
+ } catch (LockedException $e) {
+ // release the shared lock we acquired before quitting
+ $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
throw $e;
}
-
- if ($result !== false && in_array('delete', $hooks)) {
- $this->removeUpdate($storage, $internalPath);
- }
- if ($result !== false && in_array('write', $hooks, true) && $operation !== 'fopen' && $operation !== 'touch') {
- $isCreateOperation = $operation === 'mkdir' || ($operation === 'file_put_contents' && in_array('create', $hooks, true));
- $sizeDifference = $operation === 'mkdir' ? 0 : $result;
- $this->writeUpdate($storage, $internalPath, null, $isCreateOperation ? $sizeDifference : null);
+ }
+ try {
+ if (!is_null($extraParam)) {
+ $result = $storage->$operation($internalPath, $extraParam);
+ } else {
+ $result = $storage->$operation($internalPath);
}
- if ($result !== false && in_array('touch', $hooks)) {
- $this->writeUpdate($storage, $internalPath, $extraParam);
+ } catch (\Exception $e) {
+ if (in_array('write', $hooks) || in_array('delete', $hooks)) {
+ $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
+ } elseif (in_array('read', $hooks)) {
+ $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
}
+ throw $e;
+ }
- if ((in_array('write', $hooks) || in_array('delete', $hooks)) && ($operation !== 'fopen' || $result === false)) {
- $this->changeLock($path, ILockingProvider::LOCK_SHARED);
- }
+ if ($result !== false && in_array('delete', $hooks)) {
+ $this->removeUpdate($storage, $internalPath);
+ }
+ if ($result !== false && in_array('write', $hooks, true) && $operation !== 'fopen' && $operation !== 'touch') {
+ $isCreateOperation = $operation === 'mkdir' || ($operation === 'file_put_contents' && in_array('create', $hooks, true));
+ $sizeDifference = $operation === 'mkdir' ? 0 : $result;
+ $this->writeUpdate($storage, $internalPath, null, $isCreateOperation ? $sizeDifference : null);
+ }
+ if ($result !== false && in_array('touch', $hooks)) {
+ $this->writeUpdate($storage, $internalPath, $extraParam);
+ }
- $unlockLater = false;
- if ($this->lockingEnabled && $operation === 'fopen' && is_resource($result)) {
- $unlockLater = true;
- // make sure our unlocking callback will still be called if connection is aborted
- ignore_user_abort(true);
- $result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) {
- if (in_array('write', $hooks)) {
- $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
- } elseif (in_array('read', $hooks)) {
- $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
- }
- });
- }
+ if ((in_array('write', $hooks) || in_array('delete', $hooks)) && ($operation !== 'fopen' || $result === false)) {
+ $this->changeLock($path, ILockingProvider::LOCK_SHARED);
+ }
- if ($this->shouldEmitHooks($path) && $result !== false) {
- if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open
- $this->runHooks($hooks, $path, true);
+ $unlockLater = false;
+ if ($this->lockingEnabled && $operation === 'fopen' && is_resource($result)) {
+ $unlockLater = true;
+ // make sure our unlocking callback will still be called if connection is aborted
+ ignore_user_abort(true);
+ $result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) {
+ if (in_array('write', $hooks)) {
+ $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE);
+ } elseif (in_array('read', $hooks)) {
+ $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
}
- }
+ });
+ }
- if (!$unlockLater
- && (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks))
- ) {
- $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
+ if ($this->shouldEmitHooks($path) && $result !== false) {
+ if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open
+ $this->runHooks($hooks, $path, true);
}
- return $result;
- } else {
+ }
+
+ if (!$unlockLater
+ && (in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks))
+ ) {
$this->unlockFile($path, ILockingProvider::LOCK_SHARED);
}
+ return $result;
+ } else {
+ $this->unlockFile($path, ILockingProvider::LOCK_SHARED);
}
return null;
}
diff --git a/lib/public/Util.php b/lib/public/Util.php
index 6e172e68d71..bdd2dd25f01 100644
--- a/lib/public/Util.php
+++ b/lib/public/Util.php
@@ -540,6 +540,20 @@ class Util {
\OCP\Server::get(LoggerInterface::class)->error('Invalid system config value for "blacklisted_files" is ignored.');
$invalidFilenames = ['.htaccess'];
}
+
+ if ($config->getSystemValueBool('enforce_windows_compatibility', false)) {
+ $invalidFilenames = array_merge(
+ $invalidFilenames,
+ [
+ "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³",
+ ],
+ );
+ }
self::$invalidFilenames = array_map('mb_strtolower', $invalidFilenames);
}
return self::$invalidFilenames;
@@ -564,6 +578,17 @@ class Util {
$invalidChars = [];
}
+ // If windows compatibility is enabled we also forbidd reserved win32 API characters
+ if ($config->getSystemValueBool('enforce_windows_compatibility', false)) {
+ $invalidChars = array_merge(
+ $invalidChars,
+ // reserved characters of the win32 API
+ ['<', '>', ':', '"', '|', '?', '*'],
+ // character 0-31 are also forbiden on windows but already filtered
+ // see IStorage::verifyPath
+ );
+ }
+
// Get admin defined invalid characters
$additionalChars = $config->getSystemValue('forbidden_chars', []);
if (!is_array($additionalChars)) {