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 <opensource@fthiessen.de>
Ferdinand Thiessen 2 weeks ago
parent cc308f7b6a
commit eda5dca4cb
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400

@ -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.

@ -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
}

@ -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;
}

@ -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)) {

Loading…
Cancel
Save