From eda5dca4cbafa5e6d6f12d13d68d831d30a7ff4e Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 2 May 2024 20:32:58 +0200 Subject: [PATCH] feat: Allow to enforce Windows compatible file and folder names 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 --- config/config.sample.php | 15 + lib/private/Files/Storage/Common.php | 28 +- lib/private/Files/View.php | 565 +++++++++++++-------------- lib/public/Util.php | 25 ++ 4 files changed, 347 insertions(+), 286 deletions(-) diff --git a/config/config.sample.php b/config/config.sample.php index cb8e0342eda..8dbadeebb9e 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -1960,6 +1960,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..42d5d1916e4 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,31 @@ 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(strtoupper($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 666f440056e..1d1653a6486 100644 --- a/lib/public/Util.php +++ b/lib/public/Util.php @@ -539,6 +539,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 = $invalidFilenames; } return self::$invalidFilenames; @@ -563,6 +577,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)) {