diff options
Diffstat (limited to 'lib/private/Files/Storage/Local.php')
-rw-r--r-- | lib/private/Files/Storage/Local.php | 209 |
1 files changed, 75 insertions, 134 deletions
diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index 0fca853da59..260f9218a88 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -1,45 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author aler9 <46489434+aler9@users.noreply.github.com> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Boris Rybalkin <ribalkin@gmail.com> - * @author Brice Maron <brice@bmaron.net> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Jakob Sack <mail@jakobsack.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Johannes Leuker <j.leuker@hosting.de> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Klaas Freitag <freitag@owncloud.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Martin Brugnara <martin@0x6d62.eu> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sjors van der Pluijm <sjors@desjors.nl> - * @author Stefan Weil <sw@weilnetz.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Files\Storage; @@ -53,6 +17,7 @@ use OCP\Files\IMimeTypeDetector; use OCP\Files\Storage\IStorage; use OCP\Files\StorageNotAvailableException; use OCP\IConfig; +use OCP\Server; use OCP\Util; use Psr\Log\LoggerInterface; @@ -76,11 +41,11 @@ class Local extends \OC\Files\Storage\Common { protected bool $caseInsensitive = false; - public function __construct($arguments) { - if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) { + public function __construct(array $parameters) { + if (!isset($parameters['datadir']) || !is_string($parameters['datadir'])) { throw new \InvalidArgumentException('No data directory set for local storage'); } - $this->datadir = str_replace('//', '/', $arguments['datadir']); + $this->datadir = str_replace('//', '/', $parameters['datadir']); // some crazy code uses a local storage on root... if ($this->datadir === '/') { $this->realDataDir = $this->datadir; @@ -92,15 +57,15 @@ class Local extends \OC\Files\Storage\Common { $this->datadir .= '/'; } $this->dataDirLength = strlen($this->realDataDir); - $this->config = \OC::$server->get(IConfig::class); - $this->mimeTypeDetector = \OC::$server->get(IMimeTypeDetector::class); + $this->config = Server::get(IConfig::class); + $this->mimeTypeDetector = Server::get(IMimeTypeDetector::class); $this->defUMask = $this->config->getSystemValue('localstorage.umask', 0022); $this->caseInsensitive = $this->config->getSystemValueBool('localstorage.case_insensitive', false); // support Write-Once-Read-Many file systems $this->unlinkOnTruncate = $this->config->getSystemValueBool('localstorage.unlink_on_truncate', false); - if (isset($arguments['isExternal']) && $arguments['isExternal'] && !$this->stat('')) { + if (isset($parameters['isExternal']) && $parameters['isExternal'] && !$this->stat('')) { // data dir not accessible or available, can happen when using an external storage of type Local // on an unmounted system mount point throw new StorageNotAvailableException('Local storage path does not exist "' . $this->getSourcePath('') . '"'); @@ -110,11 +75,11 @@ class Local extends \OC\Files\Storage\Common { public function __destruct() { } - public function getId() { + public function getId(): string { return 'local::' . $this->datadir; } - public function mkdir($path) { + public function mkdir(string $path): bool { $sourcePath = $this->getSourcePath($path); $oldMask = umask($this->defUMask); $result = @mkdir($sourcePath, 0777, true); @@ -122,7 +87,7 @@ class Local extends \OC\Files\Storage\Common { return $result; } - public function rmdir($path) { + public function rmdir(string $path): bool { if (!$this->isDeletable($path)) { return false; } @@ -142,7 +107,7 @@ class Local extends \OC\Files\Storage\Common { * @var \SplFileInfo $file */ $file = $it->current(); - clearstatcache(true, $this->getSourcePath($file)); + clearstatcache(true, $file->getRealPath()); if (in_array($file->getBasename(), ['.', '..'])) { $it->next(); continue; @@ -153,6 +118,7 @@ class Local extends \OC\Files\Storage\Common { } $it->next(); } + unset($it); // Release iterator and thereby its potential directory lock (e.g. in case of VirtualBox shared folders) clearstatcache(true, $this->getSourcePath($path)); return rmdir($this->getSourcePath($path)); } catch (\UnexpectedValueException $e) { @@ -160,11 +126,11 @@ class Local extends \OC\Files\Storage\Common { } } - public function opendir($path) { + public function opendir(string $path) { return opendir($this->getSourcePath($path)); } - public function is_dir($path) { + public function is_dir(string $path): bool { if ($this->caseInsensitive && !$this->file_exists($path)) { return false; } @@ -174,14 +140,14 @@ class Local extends \OC\Files\Storage\Common { return is_dir($this->getSourcePath($path)); } - public function is_file($path) { + public function is_file(string $path): bool { if ($this->caseInsensitive && !$this->file_exists($path)) { return false; } return is_file($this->getSourcePath($path)); } - public function stat($path) { + public function stat(string $path): array|false { $fullPath = $this->getSourcePath($path); clearstatcache(true, $fullPath); if (!file_exists($fullPath)) { @@ -199,10 +165,7 @@ class Local extends \OC\Files\Storage\Common { return $statResult; } - /** - * @inheritdoc - */ - public function getMetaData($path) { + public function getMetaData(string $path): ?array { try { $stat = $this->stat($path); } catch (ForbiddenException $e) { @@ -251,7 +214,7 @@ class Local extends \OC\Files\Storage\Common { return $data; } - public function filetype($path) { + public function filetype(string $path): string|false { $filetype = filetype($this->getSourcePath($path)); if ($filetype == 'link') { $filetype = filetype(realpath($this->getSourcePath($path))); @@ -259,7 +222,7 @@ class Local extends \OC\Files\Storage\Common { return $filetype; } - public function filesize($path): false|int|float { + public function filesize(string $path): int|float|false { if (!$this->is_file($path)) { return 0; } @@ -271,25 +234,29 @@ class Local extends \OC\Files\Storage\Common { return filesize($fullPath); } - public function isReadable($path) { + public function isReadable(string $path): bool { return is_readable($this->getSourcePath($path)); } - public function isUpdatable($path) { + public function isUpdatable(string $path): bool { return is_writable($this->getSourcePath($path)); } - public function file_exists($path) { + public function file_exists(string $path): bool { if ($this->caseInsensitive) { $fullPath = $this->getSourcePath($path); - $content = scandir(dirname($fullPath), SCANDIR_SORT_NONE); + $parentPath = dirname($fullPath); + if (!is_dir($parentPath)) { + return false; + } + $content = scandir($parentPath, SCANDIR_SORT_NONE); return is_array($content) && array_search(basename($fullPath), $content) !== false; } else { return file_exists($this->getSourcePath($path)); } } - public function filemtime($path) { + public function filemtime(string $path): int|false { $fullPath = $this->getSourcePath($path); clearstatcache(true, $fullPath); if (!$this->file_exists($path)) { @@ -302,11 +269,11 @@ class Local extends \OC\Files\Storage\Common { return filemtime($fullPath); } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { // sets the modification time of the file to the given value. // If mtime is nil the current time is set. // note that the access time of the file always changes to the current time. - if ($this->file_exists($path) and !$this->isUpdatable($path)) { + if ($this->file_exists($path) && !$this->isUpdatable($path)) { return false; } $oldMask = umask($this->defUMask); @@ -323,11 +290,11 @@ class Local extends \OC\Files\Storage\Common { return $result; } - public function file_get_contents($path) { + public function file_get_contents(string $path): string|false { return file_get_contents($this->getSourcePath($path)); } - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { $oldMask = umask($this->defUMask); if ($this->unlinkOnTruncate) { $this->unlink($path); @@ -337,7 +304,7 @@ class Local extends \OC\Files\Storage\Common { return $result; } - public function unlink($path) { + public function unlink(string $path): bool { if ($this->is_dir($path)) { return $this->rmdir($path); } elseif ($this->is_file($path)) { @@ -347,7 +314,7 @@ class Local extends \OC\Files\Storage\Common { } } - private function checkTreeForForbiddenItems(string $path) { + private function checkTreeForForbiddenItems(string $path): void { $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path)); foreach ($iterator as $file) { /** @var \SplFileInfo $file */ @@ -357,29 +324,31 @@ class Local extends \OC\Files\Storage\Common { } } - public function rename($source, $target): bool { + public function rename(string $source, string $target): bool { $srcParent = dirname($source); $dstParent = dirname($target); if (!$this->isUpdatable($srcParent)) { - \OC::$server->get(LoggerInterface::class)->error('unable to rename, source directory is not writable : ' . $srcParent, ['app' => 'core']); + Server::get(LoggerInterface::class)->error('unable to rename, source directory is not writable : ' . $srcParent, ['app' => 'core']); return false; } if (!$this->isUpdatable($dstParent)) { - \OC::$server->get(LoggerInterface::class)->error('unable to rename, destination directory is not writable : ' . $dstParent, ['app' => 'core']); + Server::get(LoggerInterface::class)->error('unable to rename, destination directory is not writable : ' . $dstParent, ['app' => 'core']); return false; } if (!$this->file_exists($source)) { - \OC::$server->get(LoggerInterface::class)->error('unable to rename, file does not exists : ' . $source, ['app' => 'core']); + Server::get(LoggerInterface::class)->error('unable to rename, file does not exists : ' . $source, ['app' => 'core']); return false; } - if ($this->is_dir($target)) { - $this->rmdir($target); - } elseif ($this->is_file($target)) { - $this->unlink($target); + if ($this->file_exists($target)) { + if ($this->is_dir($target)) { + $this->rmdir($target); + } elseif ($this->is_file($target)) { + $this->unlink($target); + } } if ($this->is_dir($source)) { @@ -398,7 +367,7 @@ class Local extends \OC\Files\Storage\Common { return $this->copy($source, $target) && $this->unlink($source); } - public function copy($source, $target) { + public function copy(string $source, string $target): bool { if ($this->is_dir($source)) { return parent::copy($source, $target); } else { @@ -417,7 +386,7 @@ class Local extends \OC\Files\Storage\Common { } } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $sourcePath = $this->getSourcePath($path); if (!file_exists($sourcePath) && $mode === 'r') { return false; @@ -431,11 +400,11 @@ class Local extends \OC\Files\Storage\Common { return $result; } - public function hash($type, $path, $raw = false) { + public function hash(string $type, string $path, bool $raw = false): string|false { return hash_file($type, $this->getSourcePath($path), $raw); } - public function free_space($path) { + public function free_space(string $path): int|float|false { $sourcePath = $this->getSourcePath($path); // using !is_dir because $sourcePath might be a part file or // non-existing file, so we'd still want to use the parent dir @@ -451,20 +420,15 @@ class Local extends \OC\Files\Storage\Common { return Util::numericToNumber($space); } - public function search($query) { + public function search(string $query): array { return $this->searchInDir($query); } - public function getLocalFile($path) { + public function getLocalFile(string $path): string|false { return $this->getSourcePath($path); } - /** - * @param string $query - * @param string $dir - * @return array - */ - protected function searchInDir($query, $dir = '') { + protected function searchInDir(string $query, string $dir = ''): array { $files = []; $physicalDir = $this->getSourcePath($dir); foreach (scandir($physicalDir) as $item) { @@ -483,14 +447,7 @@ class Local extends \OC\Files\Storage\Common { return $files; } - /** - * check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @return bool - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { if ($this->file_exists($path)) { return $this->filemtime($path) > $time; } else { @@ -501,11 +458,9 @@ class Local extends \OC\Files\Storage\Common { /** * Get the source path (on disk) of a given path * - * @param string $path - * @return string * @throws ForbiddenException */ - public function getSourcePath($path) { + public function getSourcePath(string $path): string { if (Filesystem::isFileBlacklisted($path)) { throw new ForbiddenException('Invalid path: ' . $path, false); } @@ -520,6 +475,7 @@ class Local extends \OC\Files\Storage\Common { $realPath = realpath($pathToResolve); while ($realPath === false) { // for non existing files check the parent directory $currentPath = dirname($currentPath); + /** @psalm-suppress TypeDoesNotContainType Let's be extra cautious and still check for empty string */ if ($currentPath === '' || $currentPath === '.') { return $fullPath; } @@ -532,28 +488,19 @@ class Local extends \OC\Files\Storage\Common { return $fullPath; } - \OC::$server->get(LoggerInterface::class)->error("Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ['app' => 'core']); + Server::get(LoggerInterface::class)->error("Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ['app' => 'core']); throw new ForbiddenException('Following symlinks is not allowed', false); } - /** - * {@inheritdoc} - */ - public function isLocal() { + public function isLocal(): bool { return true; } - /** - * get the ETag for a file or folder - * - * @param string $path - * @return string - */ - public function getETag($path) { + public function getETag(string $path): string|false { return $this->calculateEtag($path, $this->stat($path)); } - private function calculateEtag(string $path, array $stat): string { + private function calculateEtag(string $path, array $stat): string|false { if ($stat['mode'] & 0x4000 && !($stat['mode'] & 0x8000)) { // is_dir & not socket return parent::getETag($path); } else { @@ -579,30 +526,28 @@ class Local extends \OC\Files\Storage\Common { } } - private function canDoCrossStorageMove(IStorage $sourceStorage) { + private function canDoCrossStorageMove(IStorage $sourceStorage): bool { + /** @psalm-suppress UndefinedClass,InvalidArgument */ return $sourceStorage->instanceOfStorage(Local::class) // Don't treat ACLStorageWrapper like local storage where copy can be done directly. // Instead, use the slower recursive copying in php from Common::copyFromStorage with // more permissions checks. && !$sourceStorage->instanceOfStorage('OCA\GroupFolders\ACL\ACLStorageWrapper') + // Same for access control + && !$sourceStorage->instanceOfStorage(\OCA\FilesAccessControl\StorageWrapper::class) // when moving encrypted files we have to handle keys and the target might not be encrypted && !$sourceStorage->instanceOfStorage(Encryption::class); } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @param bool $preserveMtime - * @return bool - */ - public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) { + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool { if ($this->canDoCrossStorageMove($sourceStorage)) { - if ($sourceStorage->instanceOfStorage(Jail::class)) { + // resolve any jailed paths + while ($sourceStorage->instanceOfStorage(Jail::class)) { /** * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage */ $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath); + $sourceStorage = $sourceStorage->getUnjailedStorage(); } /** * @var \OC\Files\Storage\Local $sourceStorage @@ -614,19 +559,15 @@ class Local extends \OC\Files\Storage\Common { } } - /** - * @param IStorage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($this->canDoCrossStorageMove($sourceStorage)) { - if ($sourceStorage->instanceOfStorage(Jail::class)) { + // resolve any jailed paths + while ($sourceStorage->instanceOfStorage(Jail::class)) { /** * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage */ $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath); + $sourceStorage = $sourceStorage->getUnjailedStorage(); } /** * @var \OC\Files\Storage\Local $sourceStorage @@ -638,7 +579,7 @@ class Local extends \OC\Files\Storage\Common { } } - public function writeStream(string $path, $stream, int $size = null): int { + public function writeStream(string $path, $stream, ?int $size = null): int { /** @var int|false $result We consider here that returned size will never be a float because we write less than 4GB */ $result = $this->file_put_contents($path, $stream); if (is_resource($stream)) { |