1
0

Filesystem.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Files;
  8. use OC\Files\Mount\MountPoint;
  9. use OC\User\NoUserException;
  10. use OCP\Cache\CappedMemoryCache;
  11. use OCP\EventDispatcher\IEventDispatcher;
  12. use OCP\Files\Events\Node\FilesystemTornDownEvent;
  13. use OCP\Files\Mount\IMountManager;
  14. use OCP\Files\NotFoundException;
  15. use OCP\Files\Storage\IStorageFactory;
  16. use OCP\IUser;
  17. use OCP\IUserManager;
  18. use OCP\IUserSession;
  19. use Psr\Log\LoggerInterface;
  20. class Filesystem {
  21. private static ?Mount\Manager $mounts = null;
  22. public static bool $loaded = false;
  23. private static ?View $defaultInstance = null;
  24. private static ?CappedMemoryCache $normalizedPathCache = null;
  25. /** @var string[]|null */
  26. private static ?array $blacklist = null;
  27. /**
  28. * classname which used for hooks handling
  29. * used as signalclass in OC_Hooks::emit()
  30. */
  31. public const CLASSNAME = 'OC_Filesystem';
  32. /**
  33. * signalname emitted before file renaming
  34. *
  35. * @param string $oldpath
  36. * @param string $newpath
  37. */
  38. public const signal_rename = 'rename';
  39. /**
  40. * signal emitted after file renaming
  41. *
  42. * @param string $oldpath
  43. * @param string $newpath
  44. */
  45. public const signal_post_rename = 'post_rename';
  46. /**
  47. * signal emitted before file/dir creation
  48. *
  49. * @param string $path
  50. * @param bool $run changing this flag to false in hook handler will cancel event
  51. */
  52. public const signal_create = 'create';
  53. /**
  54. * signal emitted after file/dir creation
  55. *
  56. * @param string $path
  57. * @param bool $run changing this flag to false in hook handler will cancel event
  58. */
  59. public const signal_post_create = 'post_create';
  60. /**
  61. * signal emits before file/dir copy
  62. *
  63. * @param string $oldpath
  64. * @param string $newpath
  65. * @param bool $run changing this flag to false in hook handler will cancel event
  66. */
  67. public const signal_copy = 'copy';
  68. /**
  69. * signal emits after file/dir copy
  70. *
  71. * @param string $oldpath
  72. * @param string $newpath
  73. */
  74. public const signal_post_copy = 'post_copy';
  75. /**
  76. * signal emits before file/dir save
  77. *
  78. * @param string $path
  79. * @param bool $run changing this flag to false in hook handler will cancel event
  80. */
  81. public const signal_write = 'write';
  82. /**
  83. * signal emits after file/dir save
  84. *
  85. * @param string $path
  86. */
  87. public const signal_post_write = 'post_write';
  88. /**
  89. * signal emitted before file/dir update
  90. *
  91. * @param string $path
  92. * @param bool $run changing this flag to false in hook handler will cancel event
  93. */
  94. public const signal_update = 'update';
  95. /**
  96. * signal emitted after file/dir update
  97. *
  98. * @param string $path
  99. * @param bool $run changing this flag to false in hook handler will cancel event
  100. */
  101. public const signal_post_update = 'post_update';
  102. /**
  103. * signal emits when reading file/dir
  104. *
  105. * @param string $path
  106. */
  107. public const signal_read = 'read';
  108. /**
  109. * signal emits when removing file/dir
  110. *
  111. * @param string $path
  112. */
  113. public const signal_delete = 'delete';
  114. /**
  115. * parameters definitions for signals
  116. */
  117. public const signal_param_path = 'path';
  118. public const signal_param_oldpath = 'oldpath';
  119. public const signal_param_newpath = 'newpath';
  120. /**
  121. * run - changing this flag to false in hook handler will cancel event
  122. */
  123. public const signal_param_run = 'run';
  124. public const signal_create_mount = 'create_mount';
  125. public const signal_delete_mount = 'delete_mount';
  126. public const signal_param_mount_type = 'mounttype';
  127. public const signal_param_users = 'users';
  128. private static ?\OC\Files\Storage\StorageFactory $loader = null;
  129. private static bool $logWarningWhenAddingStorageWrapper = true;
  130. /**
  131. * @param bool $shouldLog
  132. * @return bool previous value
  133. * @internal
  134. */
  135. public static function logWarningWhenAddingStorageWrapper(bool $shouldLog): bool {
  136. $previousValue = self::$logWarningWhenAddingStorageWrapper;
  137. self::$logWarningWhenAddingStorageWrapper = $shouldLog;
  138. return $previousValue;
  139. }
  140. /**
  141. * @param string $wrapperName
  142. * @param callable $wrapper
  143. * @param int $priority
  144. */
  145. public static function addStorageWrapper($wrapperName, $wrapper, $priority = 50) {
  146. if (self::$logWarningWhenAddingStorageWrapper) {
  147. \OCP\Server::get(LoggerInterface::class)->warning("Storage wrapper '{wrapper}' was not registered via the 'OC_Filesystem - preSetup' hook which could cause potential problems.", [
  148. 'wrapper' => $wrapperName,
  149. 'app' => 'filesystem',
  150. ]);
  151. }
  152. $mounts = self::getMountManager()->getAll();
  153. if (!self::getLoader()->addStorageWrapper($wrapperName, $wrapper, $priority, $mounts)) {
  154. // do not re-wrap if storage with this name already existed
  155. return;
  156. }
  157. }
  158. /**
  159. * Returns the storage factory
  160. *
  161. * @return IStorageFactory
  162. */
  163. public static function getLoader() {
  164. if (!self::$loader) {
  165. self::$loader = \OC::$server->get(IStorageFactory::class);
  166. }
  167. return self::$loader;
  168. }
  169. /**
  170. * Returns the mount manager
  171. */
  172. public static function getMountManager(): Mount\Manager {
  173. self::initMountManager();
  174. assert(self::$mounts !== null);
  175. return self::$mounts;
  176. }
  177. /**
  178. * get the mountpoint of the storage object for a path
  179. * ( note: because a storage is not always mounted inside the fakeroot, the
  180. * returned mountpoint is relative to the absolute root of the filesystem
  181. * and doesn't take the chroot into account )
  182. *
  183. * @param string $path
  184. * @return string
  185. */
  186. public static function getMountPoint($path) {
  187. if (!self::$mounts) {
  188. \OC_Util::setupFS();
  189. }
  190. $mount = self::$mounts->find($path);
  191. return $mount->getMountPoint();
  192. }
  193. /**
  194. * get a list of all mount points in a directory
  195. *
  196. * @param string $path
  197. * @return string[]
  198. */
  199. public static function getMountPoints($path) {
  200. if (!self::$mounts) {
  201. \OC_Util::setupFS();
  202. }
  203. $result = [];
  204. $mounts = self::$mounts->findIn($path);
  205. foreach ($mounts as $mount) {
  206. $result[] = $mount->getMountPoint();
  207. }
  208. return $result;
  209. }
  210. /**
  211. * get the storage mounted at $mountPoint
  212. *
  213. * @param string $mountPoint
  214. * @return \OC\Files\Storage\Storage|null
  215. */
  216. public static function getStorage($mountPoint) {
  217. $mount = self::getMountManager()->find($mountPoint);
  218. return $mount->getStorage();
  219. }
  220. /**
  221. * @param string $id
  222. * @return Mount\MountPoint[]
  223. */
  224. public static function getMountByStorageId($id) {
  225. return self::getMountManager()->findByStorageId($id);
  226. }
  227. /**
  228. * @param int $id
  229. * @return Mount\MountPoint[]
  230. */
  231. public static function getMountByNumericId($id) {
  232. return self::getMountManager()->findByNumericId($id);
  233. }
  234. /**
  235. * resolve a path to a storage and internal path
  236. *
  237. * @param string $path
  238. * @return array{?\OCP\Files\Storage\IStorage, string} an array consisting of the storage and the internal path
  239. */
  240. public static function resolvePath($path): array {
  241. $mount = self::getMountManager()->find($path);
  242. return [$mount->getStorage(), rtrim($mount->getInternalPath($path), '/')];
  243. }
  244. public static function init(string|IUser|null $user, string $root): bool {
  245. if (self::$defaultInstance) {
  246. return false;
  247. }
  248. self::initInternal($root);
  249. //load custom mount config
  250. self::initMountPoints($user);
  251. return true;
  252. }
  253. public static function initInternal(string $root): bool {
  254. if (self::$defaultInstance) {
  255. return false;
  256. }
  257. self::getLoader();
  258. self::$defaultInstance = new View($root);
  259. /** @var IEventDispatcher $eventDispatcher */
  260. $eventDispatcher = \OC::$server->get(IEventDispatcher::class);
  261. $eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
  262. self::$defaultInstance = null;
  263. self::$loaded = false;
  264. });
  265. self::initMountManager();
  266. self::$loaded = true;
  267. return true;
  268. }
  269. public static function initMountManager(): void {
  270. if (!self::$mounts) {
  271. self::$mounts = \OC::$server->get(IMountManager::class);
  272. }
  273. }
  274. /**
  275. * Initialize system and personal mount points for a user
  276. *
  277. * @throws \OC\User\NoUserException if the user is not available
  278. */
  279. public static function initMountPoints(string|IUser|null $user = ''): void {
  280. /** @var IUserManager $userManager */
  281. $userManager = \OC::$server->get(IUserManager::class);
  282. $userObject = ($user instanceof IUser) ? $user : $userManager->get($user);
  283. if ($userObject) {
  284. /** @var SetupManager $setupManager */
  285. $setupManager = \OC::$server->get(SetupManager::class);
  286. $setupManager->setupForUser($userObject);
  287. } else {
  288. throw new NoUserException();
  289. }
  290. }
  291. /**
  292. * Get the default filesystem view
  293. */
  294. public static function getView(): ?View {
  295. if (!self::$defaultInstance) {
  296. /** @var IUserSession $session */
  297. $session = \OC::$server->get(IUserSession::class);
  298. $user = $session->getUser();
  299. if ($user) {
  300. $userDir = '/' . $user->getUID() . '/files';
  301. self::initInternal($userDir);
  302. }
  303. }
  304. return self::$defaultInstance;
  305. }
  306. /**
  307. * tear down the filesystem, removing all storage providers
  308. */
  309. public static function tearDown() {
  310. \OC_Util::tearDownFS();
  311. }
  312. /**
  313. * get the relative path of the root data directory for the current user
  314. *
  315. * @return ?string
  316. *
  317. * Returns path like /admin/files
  318. */
  319. public static function getRoot() {
  320. if (!self::$defaultInstance) {
  321. return null;
  322. }
  323. return self::$defaultInstance->getRoot();
  324. }
  325. /**
  326. * mount an \OC\Files\Storage\Storage in our virtual filesystem
  327. *
  328. * @param \OC\Files\Storage\Storage|string $class
  329. * @param array $arguments
  330. * @param string $mountpoint
  331. */
  332. public static function mount($class, $arguments, $mountpoint) {
  333. if (!self::$mounts) {
  334. \OC_Util::setupFS();
  335. }
  336. $mount = new Mount\MountPoint($class, $mountpoint, $arguments, self::getLoader());
  337. self::$mounts->addMount($mount);
  338. }
  339. /**
  340. * return the path to a local version of the file
  341. * we need this because we can't know if a file is stored local or not from
  342. * outside the filestorage and for some purposes a local file is needed
  343. */
  344. public static function getLocalFile(string $path): string|false {
  345. return self::$defaultInstance->getLocalFile($path);
  346. }
  347. /**
  348. * return path to file which reflects one visible in browser
  349. *
  350. * @param string $path
  351. * @return string
  352. */
  353. public static function getLocalPath($path) {
  354. $datadir = \OC_User::getHome(\OC_User::getUser()) . '/files';
  355. $newpath = $path;
  356. if (strncmp($newpath, $datadir, strlen($datadir)) == 0) {
  357. $newpath = substr($path, strlen($datadir));
  358. }
  359. return $newpath;
  360. }
  361. /**
  362. * check if the requested path is valid
  363. *
  364. * @param string $path
  365. * @return bool
  366. */
  367. public static function isValidPath($path) {
  368. $path = self::normalizePath($path);
  369. if (!$path || $path[0] !== '/') {
  370. $path = '/' . $path;
  371. }
  372. if (str_contains($path, '/../') || strrchr($path, '/') === '/..') {
  373. return false;
  374. }
  375. return true;
  376. }
  377. /**
  378. * @param string $filename
  379. * @return bool
  380. */
  381. public static function isFileBlacklisted($filename) {
  382. $filename = self::normalizePath($filename);
  383. if (self::$blacklist === null) {
  384. self::$blacklist = \OC::$server->getConfig()->getSystemValue('blacklisted_files', ['.htaccess']);
  385. }
  386. $filename = strtolower(basename($filename));
  387. return in_array($filename, self::$blacklist);
  388. }
  389. /**
  390. * check if the directory should be ignored when scanning
  391. * NOTE: the special directories . and .. would cause never ending recursion
  392. *
  393. * @param string $dir
  394. * @return boolean
  395. */
  396. public static function isIgnoredDir($dir) {
  397. if ($dir === '.' || $dir === '..') {
  398. return true;
  399. }
  400. return false;
  401. }
  402. /**
  403. * following functions are equivalent to their php builtin equivalents for arguments/return values.
  404. */
  405. public static function mkdir($path) {
  406. return self::$defaultInstance->mkdir($path);
  407. }
  408. public static function rmdir($path) {
  409. return self::$defaultInstance->rmdir($path);
  410. }
  411. public static function is_dir($path) {
  412. return self::$defaultInstance->is_dir($path);
  413. }
  414. public static function is_file($path) {
  415. return self::$defaultInstance->is_file($path);
  416. }
  417. public static function stat($path) {
  418. return self::$defaultInstance->stat($path);
  419. }
  420. public static function filetype($path) {
  421. return self::$defaultInstance->filetype($path);
  422. }
  423. public static function filesize($path) {
  424. return self::$defaultInstance->filesize($path);
  425. }
  426. public static function readfile($path) {
  427. return self::$defaultInstance->readfile($path);
  428. }
  429. public static function isCreatable($path) {
  430. return self::$defaultInstance->isCreatable($path);
  431. }
  432. public static function isReadable($path) {
  433. return self::$defaultInstance->isReadable($path);
  434. }
  435. public static function isUpdatable($path) {
  436. return self::$defaultInstance->isUpdatable($path);
  437. }
  438. public static function isDeletable($path) {
  439. return self::$defaultInstance->isDeletable($path);
  440. }
  441. public static function isSharable($path) {
  442. return self::$defaultInstance->isSharable($path);
  443. }
  444. public static function file_exists($path) {
  445. return self::$defaultInstance->file_exists($path);
  446. }
  447. public static function filemtime($path) {
  448. return self::$defaultInstance->filemtime($path);
  449. }
  450. public static function touch($path, $mtime = null) {
  451. return self::$defaultInstance->touch($path, $mtime);
  452. }
  453. /**
  454. * @return string|false
  455. */
  456. public static function file_get_contents($path) {
  457. return self::$defaultInstance->file_get_contents($path);
  458. }
  459. public static function file_put_contents($path, $data) {
  460. return self::$defaultInstance->file_put_contents($path, $data);
  461. }
  462. public static function unlink($path) {
  463. return self::$defaultInstance->unlink($path);
  464. }
  465. public static function rename($source, $target) {
  466. return self::$defaultInstance->rename($source, $target);
  467. }
  468. public static function copy($source, $target) {
  469. return self::$defaultInstance->copy($source, $target);
  470. }
  471. public static function fopen($path, $mode) {
  472. return self::$defaultInstance->fopen($path, $mode);
  473. }
  474. /**
  475. * @param string $path
  476. * @throws \OCP\Files\InvalidPathException
  477. */
  478. public static function toTmpFile($path): string|false {
  479. return self::$defaultInstance->toTmpFile($path);
  480. }
  481. public static function fromTmpFile($tmpFile, $path) {
  482. return self::$defaultInstance->fromTmpFile($tmpFile, $path);
  483. }
  484. public static function getMimeType($path) {
  485. return self::$defaultInstance->getMimeType($path);
  486. }
  487. public static function hash($type, $path, $raw = false) {
  488. return self::$defaultInstance->hash($type, $path, $raw);
  489. }
  490. public static function free_space($path = '/') {
  491. return self::$defaultInstance->free_space($path);
  492. }
  493. public static function search($query) {
  494. return self::$defaultInstance->search($query);
  495. }
  496. /**
  497. * @param string $query
  498. */
  499. public static function searchByMime($query) {
  500. return self::$defaultInstance->searchByMime($query);
  501. }
  502. /**
  503. * @param string|int $tag name or tag id
  504. * @param string $userId owner of the tags
  505. * @return FileInfo[] array or file info
  506. */
  507. public static function searchByTag($tag, $userId) {
  508. return self::$defaultInstance->searchByTag($tag, $userId);
  509. }
  510. /**
  511. * check if a file or folder has been updated since $time
  512. *
  513. * @param string $path
  514. * @param int $time
  515. * @return bool
  516. */
  517. public static function hasUpdated($path, $time) {
  518. return self::$defaultInstance->hasUpdated($path, $time);
  519. }
  520. /**
  521. * Fix common problems with a file path
  522. *
  523. * @param string $path
  524. * @param bool $stripTrailingSlash whether to strip the trailing slash
  525. * @param bool $isAbsolutePath whether the given path is absolute
  526. * @param bool $keepUnicode true to disable unicode normalization
  527. * @psalm-taint-escape file
  528. * @return string
  529. */
  530. public static function normalizePath($path, $stripTrailingSlash = true, $isAbsolutePath = false, $keepUnicode = false) {
  531. /**
  532. * FIXME: This is a workaround for existing classes and files which call
  533. * this function with another type than a valid string. This
  534. * conversion should get removed as soon as all existing
  535. * function calls have been fixed.
  536. */
  537. $path = (string)$path;
  538. if ($path === '') {
  539. return '/';
  540. }
  541. if (is_null(self::$normalizedPathCache)) {
  542. self::$normalizedPathCache = new CappedMemoryCache(2048);
  543. }
  544. $cacheKey = json_encode([$path, $stripTrailingSlash, $isAbsolutePath, $keepUnicode]);
  545. if ($cacheKey && isset(self::$normalizedPathCache[$cacheKey])) {
  546. return self::$normalizedPathCache[$cacheKey];
  547. }
  548. //normalize unicode if possible
  549. if (!$keepUnicode) {
  550. $path = \OC_Util::normalizeUnicode($path);
  551. }
  552. //add leading slash, if it is already there we strip it anyway
  553. $path = '/' . $path;
  554. $patterns = [
  555. '#\\\\#s', // no windows style '\\' slashes
  556. '#/\.(/\.)*/#s', // remove '/./'
  557. '#\//+#s', // remove sequence of slashes
  558. '#/\.$#s', // remove trailing '/.'
  559. ];
  560. do {
  561. $count = 0;
  562. $path = preg_replace($patterns, '/', $path, -1, $count);
  563. } while ($count > 0);
  564. //remove trailing slash
  565. if ($stripTrailingSlash && strlen($path) > 1) {
  566. $path = rtrim($path, '/');
  567. }
  568. self::$normalizedPathCache[$cacheKey] = $path;
  569. return $path;
  570. }
  571. /**
  572. * get the filesystem info
  573. *
  574. * @param string $path
  575. * @param bool|string $includeMountPoints whether to add mountpoint sizes,
  576. * defaults to true
  577. * @return \OC\Files\FileInfo|false False if file does not exist
  578. */
  579. public static function getFileInfo($path, $includeMountPoints = true) {
  580. return self::getView()->getFileInfo($path, $includeMountPoints);
  581. }
  582. /**
  583. * change file metadata
  584. *
  585. * @param string $path
  586. * @param array $data
  587. * @return int
  588. *
  589. * returns the fileid of the updated file
  590. */
  591. public static function putFileInfo($path, $data) {
  592. return self::$defaultInstance->putFileInfo($path, $data);
  593. }
  594. /**
  595. * get the content of a directory
  596. *
  597. * @param string $directory path under datadirectory
  598. * @param string $mimetype_filter limit returned content to this mimetype or mimepart
  599. * @return \OC\Files\FileInfo[]
  600. */
  601. public static function getDirectoryContent($directory, $mimetype_filter = '') {
  602. return self::$defaultInstance->getDirectoryContent($directory, $mimetype_filter);
  603. }
  604. /**
  605. * Get the path of a file by id
  606. *
  607. * Note that the resulting path is not guaranteed to be unique for the id, multiple paths can point to the same file
  608. *
  609. * @param int $id
  610. * @throws NotFoundException
  611. * @return string
  612. */
  613. public static function getPath($id) {
  614. return self::$defaultInstance->getPath($id);
  615. }
  616. /**
  617. * Get the owner for a file or folder
  618. *
  619. * @param string $path
  620. * @return string
  621. */
  622. public static function getOwner($path) {
  623. return self::$defaultInstance->getOwner($path);
  624. }
  625. /**
  626. * get the ETag for a file or folder
  627. */
  628. public static function getETag(string $path): string|false {
  629. return self::$defaultInstance->getETag($path);
  630. }
  631. }