nextcloud/lib/private/Files/Filesystem.php
Côme Chilliet 9c92cc1617
chore: Suppress last known impure static properties
We will keep these legacy ones for now. We can search for the
 ImpureStaticProperty suppression and add special treatement for them in
 the frankenphp PR if needed.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-06-02 09:46:20 +02:00

724 lines
18 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Files;
use OC\Files\Mount\MountPoint;
use OC\Files\Storage\Storage;
use OC\Files\Storage\StorageFactory;
use OC\User\NoUserException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\Node\FilesystemTornDownEvent;
use OCP\Files\InvalidPathException;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\IStorage;
use OCP\Files\Storage\IStorageFactory;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Server;
use Psr\Log\LoggerInterface;
class Filesystem {
/**
* @psalm-suppress ImpureStaticProperty
*/
private static ?Mount\Manager $mounts = null;
/**
* @psalm-suppress ImpureStaticProperty
*/
public static bool $loaded = false;
/**
* @psalm-suppress ImpureStaticProperty
*/
private static ?View $defaultInstance = null;
/**
* @psalm-suppress ImpureStaticProperty
*/
private static ?FilenameValidator $validator = null;
/**
* @psalm-suppress ImpureStaticProperty
*/
private static ?StorageFactory $loader = null;
/**
* @psalm-suppress ImpureStaticProperty
*/
private static bool $logWarningWhenAddingStorageWrapper = true;
/**
* classname which used for hooks handling
* used as signalclass in OC_Hooks::emit()
*/
public const CLASSNAME = 'OC_Filesystem';
/**
* signalname emitted before file renaming
*
* @param string $oldpath
* @param string $newpath
*/
public const signal_rename = 'rename';
/**
* signal emitted after file renaming
*
* @param string $oldpath
* @param string $newpath
*/
public const signal_post_rename = 'post_rename';
/**
* signal emitted before file/dir creation
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_create = 'create';
/**
* signal emitted after file/dir creation
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_post_create = 'post_create';
/**
* signal emits before file/dir copy
*
* @param string $oldpath
* @param string $newpath
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_copy = 'copy';
/**
* signal emits after file/dir copy
*
* @param string $oldpath
* @param string $newpath
*/
public const signal_post_copy = 'post_copy';
/**
* signal emits before file/dir save
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_write = 'write';
/**
* signal emits after file/dir save
*
* @param string $path
*/
public const signal_post_write = 'post_write';
/**
* signal emitted before file/dir update
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_update = 'update';
/**
* signal emitted after file/dir update
*
* @param string $path
* @param bool $run changing this flag to false in hook handler will cancel event
*/
public const signal_post_update = 'post_update';
/**
* signal emits when reading file/dir
*
* @param string $path
*/
public const signal_read = 'read';
/**
* signal emits when removing file/dir
*
* @param string $path
*/
public const signal_delete = 'delete';
/**
* parameters definitions for signals
*/
public const signal_param_path = 'path';
public const signal_param_oldpath = 'oldpath';
public const signal_param_newpath = 'newpath';
/**
* run - changing this flag to false in hook handler will cancel event
*/
public const signal_param_run = 'run';
public const signal_create_mount = 'create_mount';
public const signal_delete_mount = 'delete_mount';
public const signal_param_mount_type = 'mounttype';
public const signal_param_users = 'users';
/**
* @param bool $shouldLog
* @return bool previous value
* @internal
*/
public static function logWarningWhenAddingStorageWrapper(bool $shouldLog): bool {
$previousValue = self::$logWarningWhenAddingStorageWrapper;
self::$logWarningWhenAddingStorageWrapper = $shouldLog;
return $previousValue;
}
/**
* @param string $wrapperName
* @param callable $wrapper
* @param int $priority
*/
public static function addStorageWrapper($wrapperName, $wrapper, $priority = 50) {
if (self::$logWarningWhenAddingStorageWrapper) {
Server::get(LoggerInterface::class)->warning("Storage wrapper '{wrapper}' was not registered via the 'OC_Filesystem - preSetup' hook which could cause potential problems.", [
'wrapper' => $wrapperName,
'app' => 'filesystem',
]);
}
$mounts = self::getMountManager()->getAll();
/** @var StorageFactory $loader */
$loader = self::getLoader();
if (!$loader->addStorageWrapper($wrapperName, $wrapper, $priority, $mounts)) {
// do not re-wrap if storage with this name already existed
return;
}
}
/**
* Returns the storage factory
*
* @return IStorageFactory
*/
public static function getLoader() {
if (!self::$loader) {
self::$loader = Server::get(IStorageFactory::class);
}
return self::$loader;
}
/**
* Returns the mount manager
*/
public static function getMountManager(): Mount\Manager {
self::initMountManager();
assert(self::$mounts !== null);
return self::$mounts;
}
/**
* get the mountpoint of the storage object for a path
* ( note: because a storage is not always mounted inside the fakeroot, the
* returned mountpoint is relative to the absolute root of the filesystem
* and doesn't take the chroot into account )
*
* @param string $path
* @return string
*/
public static function getMountPoint($path) {
if (!self::$mounts) {
\OC_Util::setupFS();
}
$mount = self::$mounts->find($path);
return $mount->getMountPoint();
}
/**
* get a list of all mount points in a directory
*
* @param string $path
* @return string[]
*/
public static function getMountPoints($path) {
if (!self::$mounts) {
\OC_Util::setupFS();
}
$result = [];
$mounts = self::$mounts->findIn($path);
foreach ($mounts as $mount) {
$result[] = $mount->getMountPoint();
}
return $result;
}
/**
* get the storage mounted at $mountPoint
*
* @param string $mountPoint
* @return IStorage|null
*/
public static function getStorage($mountPoint) {
$mount = self::getMountManager()->find($mountPoint);
return $mount->getStorage();
}
/**
* @param string $id
* @return IMountPoint[]
*/
public static function getMountByStorageId($id) {
return self::getMountManager()->findByStorageId($id);
}
/**
* @param int $id
* @return IMountPoint[]
*/
public static function getMountByNumericId($id) {
return self::getMountManager()->findByNumericId($id);
}
/**
* resolve a path to a storage and internal path
*
* @param string $path
* @return array{?IStorage, string} an array consisting of the storage and the internal path
*/
public static function resolvePath($path): array {
$mount = self::getMountManager()->find($path);
return [$mount->getStorage(), rtrim($mount->getInternalPath($path), '/')];
}
public static function init(string|IUser|null $user, string $root): bool {
if (self::$defaultInstance) {
return false;
}
self::initInternal($root);
//load custom mount config
self::initMountPoints($user);
return true;
}
public static function initInternal(string $root): bool {
if (self::$defaultInstance) {
return false;
}
self::getLoader();
self::$defaultInstance = new View($root);
/** @var IEventDispatcher $eventDispatcher */
$eventDispatcher = Server::get(IEventDispatcher::class);
$eventDispatcher->addListener(FilesystemTornDownEvent::class, function (): void {
self::$defaultInstance = null;
self::$loaded = false;
});
self::initMountManager();
self::$loaded = true;
return true;
}
public static function initMountManager(): void {
if (!self::$mounts) {
self::$mounts = Server::get(IMountManager::class);
}
}
/**
* Initialize system and personal mount points for a user
*
* @throws NoUserException if the user is not available
*/
public static function initMountPoints(string|IUser|null $user = ''): void {
/** @var IUserManager $userManager */
$userManager = Server::get(IUserManager::class);
$userObject = ($user instanceof IUser) ? $user : $userManager->get($user);
if ($userObject) {
/** @var SetupManager $setupManager */
$setupManager = Server::get(SetupManager::class);
$setupManager->setupForUser($userObject);
} else {
throw new NoUserException();
}
}
/**
* Get the default filesystem view
*/
public static function getView(): ?View {
if (!self::$defaultInstance) {
/** @var IUserSession $session */
$session = Server::get(IUserSession::class);
$user = $session->getUser();
if ($user) {
$userDir = '/' . $user->getUID() . '/files';
self::initInternal($userDir);
}
}
return self::$defaultInstance;
}
/**
* tear down the filesystem, removing all storage providers
*/
public static function tearDown() {
\OC_Util::tearDownFS();
}
/**
* get the relative path of the root data directory for the current user
*
* @return ?string
*
* Returns path like /admin/files
*/
public static function getRoot() {
if (!self::$defaultInstance) {
return null;
}
return self::$defaultInstance->getRoot();
}
/**
* mount an \OC\Files\Storage\Storage in our virtual filesystem
*
* @param Storage|string $class
* @param array $arguments
* @param string $mountpoint
*/
public static function mount($class, $arguments, $mountpoint) {
if (!self::$mounts) {
\OC_Util::setupFS();
}
$mount = new MountPoint($class, $mountpoint, $arguments, self::getLoader());
self::$mounts->addMount($mount);
}
/**
* check if the requested path is valid
*
* @param string $path
* @return bool
*/
public static function isValidPath($path) {
$path = self::normalizePath($path);
if (!$path || $path[0] !== '/') {
$path = '/' . $path;
}
if (str_contains($path, '/../') || strrchr($path, '/') === '/..') {
return false;
}
return true;
}
/**
* @param string $filename
* @return bool
*
* @deprecated 30.0.0 - use \OC\Files\FilenameValidator::isForbidden
*/
public static function isFileBlacklisted($filename) {
if (self::$validator === null) {
self::$validator = Server::get(FilenameValidator::class);
}
$filename = self::normalizePath($filename);
return self::$validator->isForbidden($filename);
}
/**
* check if the directory should be ignored when scanning
* NOTE: the special directories . and .. would cause never ending recursion
*
* @param string $dir
* @return boolean
*/
public static function isIgnoredDir($dir) {
if ($dir === '.' || $dir === '..') {
return true;
}
return false;
}
/**
* following functions are equivalent to their php builtin equivalents for arguments/return values.
*/
public static function mkdir($path) {
return self::$defaultInstance->mkdir($path);
}
public static function rmdir($path) {
return self::$defaultInstance->rmdir($path);
}
public static function is_dir($path) {
return self::$defaultInstance->is_dir($path);
}
public static function is_file($path) {
return self::$defaultInstance->is_file($path);
}
public static function stat($path) {
return self::$defaultInstance->stat($path);
}
public static function filetype($path) {
return self::$defaultInstance->filetype($path);
}
public static function filesize($path) {
return self::$defaultInstance->filesize($path);
}
public static function readfile($path) {
return self::$defaultInstance->readfile($path);
}
public static function isCreatable($path) {
return self::$defaultInstance->isCreatable($path);
}
public static function isReadable($path) {
return self::$defaultInstance->isReadable($path);
}
public static function isUpdatable($path) {
return self::$defaultInstance->isUpdatable($path);
}
public static function isDeletable($path) {
return self::$defaultInstance->isDeletable($path);
}
public static function isSharable($path) {
return self::$defaultInstance->isSharable($path);
}
public static function file_exists($path) {
return self::$defaultInstance->file_exists($path);
}
public static function filemtime($path) {
return self::$defaultInstance->filemtime($path);
}
public static function touch($path, $mtime = null) {
return self::$defaultInstance->touch($path, $mtime);
}
/**
* @return string|false
*/
public static function file_get_contents($path) {
return self::$defaultInstance->file_get_contents($path);
}
public static function file_put_contents($path, $data) {
return self::$defaultInstance->file_put_contents($path, $data);
}
public static function unlink($path) {
return self::$defaultInstance->unlink($path);
}
public static function rename($source, $target) {
return self::$defaultInstance->rename($source, $target);
}
public static function copy($source, $target) {
return self::$defaultInstance->copy($source, $target);
}
public static function fopen($path, $mode) {
return self::$defaultInstance->fopen($path, $mode);
}
/**
* @param string $path
* @throws InvalidPathException
*/
public static function toTmpFile($path): string|false {
return self::$defaultInstance->toTmpFile($path);
}
public static function fromTmpFile($tmpFile, $path) {
return self::$defaultInstance->fromTmpFile($tmpFile, $path);
}
public static function getMimeType($path) {
return self::$defaultInstance->getMimeType($path);
}
public static function hash($type, $path, $raw = false) {
return self::$defaultInstance->hash($type, $path, $raw);
}
public static function free_space($path = '/') {
return self::$defaultInstance->free_space($path);
}
public static function search($query) {
return self::$defaultInstance->search($query);
}
/**
* @param string $query
*/
public static function searchByMime($query) {
return self::$defaultInstance->searchByMime($query);
}
/**
* @param string|int $tag name or tag id
* @param string $userId owner of the tags
* @return FileInfo[] array or file info
*/
public static function searchByTag($tag, $userId) {
return self::$defaultInstance->searchByTag($tag, $userId);
}
/**
* check if a file or folder has been updated since $time
*
* @param string $path
* @param int $time
* @return bool
*/
public static function hasUpdated($path, $time) {
return self::$defaultInstance->hasUpdated($path, $time);
}
/**
* Fix common problems with a file path
*
* @param string $path
* @param bool $stripTrailingSlash whether to strip the trailing slash
* @param bool $isAbsolutePath whether the given path is absolute
* @param bool $keepUnicode true to disable unicode normalization
* @psalm-taint-escape file
* @return string
*/
public static function normalizePath($path, $stripTrailingSlash = true, $isAbsolutePath = false, $keepUnicode = false) {
/**
* FIXME: This is a workaround for existing classes and files which call
* this function with another type than a valid string. This
* conversion should get removed as soon as all existing
* function calls have been fixed.
*/
$path = (string)$path;
if ($path === '') {
return '/';
}
//normalize unicode if possible
if (!$keepUnicode) {
$path = \OC_Util::normalizeUnicode($path);
}
//add leading slash, if it is already there we strip it anyway
$path = '/' . $path;
$patterns = [
'#\\\\#s', // no windows style '\\' slashes
'#/\.(/\.)*/#s', // remove '/./'
'#\//+#s', // remove sequence of slashes
'#/\.$#s', // remove trailing '/.'
];
do {
$count = 0;
$path = preg_replace($patterns, '/', $path, -1, $count);
} while ($count > 0);
//remove trailing slash
if ($stripTrailingSlash && strlen($path) > 1) {
$path = rtrim($path, '/');
}
return $path;
}
/**
* get the filesystem info
*
* @param string $path
* @param bool|string $includeMountPoints whether to add mountpoint sizes,
* defaults to true
* @return FileInfo|false False if file does not exist
*/
public static function getFileInfo($path, $includeMountPoints = true) {
return self::getView()->getFileInfo($path, $includeMountPoints);
}
/**
* change file metadata
*
* @param string $path
* @param array $data
* @return int
*
* returns the fileid of the updated file
*/
public static function putFileInfo($path, $data) {
return self::$defaultInstance->putFileInfo($path, $data);
}
/**
* Get the content of a directory.
*
* @param string $directory path under datadirectory
* @param ?non-empty-string $mimeTypeFilter limit returned content to this mimetype or mimepart
* @return FileInfo[]
*/
public static function getDirectoryContent($directory, ?string $mimeTypeFilter = null): array {
return self::$defaultInstance->getDirectoryContent($directory, $mimeTypeFilter);
}
/**
* Get the path of a file by id
*
* Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file
*
* @param int $id
* @throws NotFoundException
* @return string
*/
public static function getPath($id) {
return self::$defaultInstance->getPath($id);
}
/**
* Get the owner for a file or folder
*
* @param string $path
* @return string
*/
public static function getOwner($path) {
return self::$defaultInstance->getOwner($path);
}
/**
* get the ETag for a file or folder
*/
public static function getETag(string $path): string|false {
return self::$defaultInstance->getETag($path);
}
}