You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

579 lines
18 KiB

3 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Georg Ehrke <oc.list@georgehrke.com>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Julius Härtl <jus@bitgrid.net>
  10. * @author Morris Jobke <hey@morrisjobke.de>
  11. * @author Robin Appelman <robin@icewind.nl>
  12. * @author Robin McCorkell <robin@mccorkell.me.uk>
  13. * @author Roeland Jago Douma <roeland@famdouma.nl>
  14. * @author Vincent Petry <pvince81@owncloud.com>
  15. *
  16. * @license AGPL-3.0
  17. *
  18. * This code is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License, version 3,
  20. * as published by the Free Software Foundation.
  21. *
  22. * This program is distributed in the hope that it will be useful,
  23. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. * GNU Affero General Public License for more details.
  26. *
  27. * You should have received a copy of the GNU Affero General Public License, version 3,
  28. * along with this program. If not, see <http://www.gnu.org/licenses/>
  29. *
  30. */
  31. namespace OC\Files\Node;
  32. use OC\DB\QueryBuilder\Literal;
  33. use OC\Files\Storage\Wrapper\Jail;
  34. use OCA\Files_Sharing\SharedStorage;
  35. use OCP\DB\QueryBuilder\IQueryBuilder;
  36. use OCP\Files\Config\ICachedMountInfo;
  37. use OCP\Files\FileInfo;
  38. use OCP\Files\Mount\IMountPoint;
  39. use OCP\Files\NotFoundException;
  40. use OCP\Files\NotPermittedException;
  41. use OCP\Files\Search\ISearchQuery;
  42. class Folder extends Node implements \OCP\Files\Folder {
  43. /**
  44. * Creates a Folder that represents a non-existing path
  45. *
  46. * @param string $path path
  47. * @return string non-existing node class
  48. */
  49. protected function createNonExistingNode($path) {
  50. return new NonExistingFolder($this->root, $this->view, $path);
  51. }
  52. /**
  53. * @param string $path path relative to the folder
  54. * @return string
  55. * @throws \OCP\Files\NotPermittedException
  56. */
  57. public function getFullPath($path) {
  58. if (!$this->isValidPath($path)) {
  59. throw new NotPermittedException('Invalid path');
  60. }
  61. return $this->path . $this->normalizePath($path);
  62. }
  63. /**
  64. * @param string $path
  65. * @return string
  66. */
  67. public function getRelativePath($path) {
  68. if ($this->path === '' or $this->path === '/') {
  69. return $this->normalizePath($path);
  70. }
  71. if ($path === $this->path) {
  72. return '/';
  73. } elseif (strpos($path, $this->path . '/') !== 0) {
  74. return null;
  75. } else {
  76. $path = substr($path, strlen($this->path));
  77. return $this->normalizePath($path);
  78. }
  79. }
  80. /**
  81. * check if a node is a (grand-)child of the folder
  82. *
  83. * @param \OC\Files\Node\Node $node
  84. * @return bool
  85. */
  86. public function isSubNode($node) {
  87. return strpos($node->getPath(), $this->path . '/') === 0;
  88. }
  89. /**
  90. * get the content of this directory
  91. *
  92. * @throws \OCP\Files\NotFoundException
  93. * @return Node[]
  94. */
  95. public function getDirectoryListing() {
  96. $folderContent = $this->view->getDirectoryContent($this->path);
  97. return array_map(function (FileInfo $info) {
  98. if ($info->getMimetype() === 'httpd/unix-directory') {
  99. return new Folder($this->root, $this->view, $info->getPath(), $info);
  100. } else {
  101. return new File($this->root, $this->view, $info->getPath(), $info);
  102. }
  103. }, $folderContent);
  104. }
  105. /**
  106. * @param string $path
  107. * @param FileInfo $info
  108. * @return File|Folder
  109. */
  110. protected function createNode($path, FileInfo $info = null) {
  111. if (is_null($info)) {
  112. $isDir = $this->view->is_dir($path);
  113. } else {
  114. $isDir = $info->getType() === FileInfo::TYPE_FOLDER;
  115. }
  116. if ($isDir) {
  117. return new Folder($this->root, $this->view, $path, $info);
  118. } else {
  119. return new File($this->root, $this->view, $path, $info);
  120. }
  121. }
  122. /**
  123. * Get the node at $path
  124. *
  125. * @param string $path
  126. * @return \OC\Files\Node\Node
  127. * @throws \OCP\Files\NotFoundException
  128. */
  129. public function get($path) {
  130. return $this->root->get($this->getFullPath($path));
  131. }
  132. /**
  133. * @param string $path
  134. * @return bool
  135. */
  136. public function nodeExists($path) {
  137. try {
  138. $this->get($path);
  139. return true;
  140. } catch (NotFoundException $e) {
  141. return false;
  142. }
  143. }
  144. /**
  145. * @param string $path
  146. * @return \OC\Files\Node\Folder
  147. * @throws \OCP\Files\NotPermittedException
  148. */
  149. public function newFolder($path) {
  150. if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
  151. $fullPath = $this->getFullPath($path);
  152. $nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath);
  153. $this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
  154. if (!$this->view->mkdir($fullPath)) {
  155. throw new NotPermittedException('Could not create folder');
  156. }
  157. $node = new Folder($this->root, $this->view, $fullPath);
  158. $this->sendHooks(['postWrite', 'postCreate'], [$node]);
  159. return $node;
  160. } else {
  161. throw new NotPermittedException('No create permission for folder');
  162. }
  163. }
  164. /**
  165. * @param string $path
  166. * @param string | resource | null $content
  167. * @return \OC\Files\Node\File
  168. * @throws \OCP\Files\NotPermittedException
  169. */
  170. public function newFile($path, $content = null) {
  171. if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
  172. $fullPath = $this->getFullPath($path);
  173. $nonExisting = new NonExistingFile($this->root, $this->view, $fullPath);
  174. $this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
  175. if ($content !== null) {
  176. $result = $this->view->file_put_contents($fullPath, $content);
  177. } else {
  178. $result = $this->view->touch($fullPath);
  179. }
  180. if ($result === false) {
  181. throw new NotPermittedException('Could not create path');
  182. }
  183. $node = new File($this->root, $this->view, $fullPath);
  184. $this->sendHooks(['postWrite', 'postCreate'], [$node]);
  185. return $node;
  186. }
  187. throw new NotPermittedException('No create permission for path');
  188. }
  189. /**
  190. * search for files with the name matching $query
  191. *
  192. * @param string|ISearchQuery $query
  193. * @return \OC\Files\Node\Node[]
  194. */
  195. public function search($query) {
  196. if (is_string($query)) {
  197. return $this->searchCommon('search', ['%' . $query . '%']);
  198. } else {
  199. return $this->searchCommon('searchQuery', [$query]);
  200. }
  201. }
  202. /**
  203. * search for files by mimetype
  204. *
  205. * @param string $mimetype
  206. * @return Node[]
  207. */
  208. public function searchByMime($mimetype) {
  209. return $this->searchCommon('searchByMime', [$mimetype]);
  210. }
  211. /**
  212. * search for files by tag
  213. *
  214. * @param string|int $tag name or tag id
  215. * @param string $userId owner of the tags
  216. * @return Node[]
  217. */
  218. public function searchByTag($tag, $userId) {
  219. return $this->searchCommon('searchByTag', [$tag, $userId]);
  220. }
  221. /**
  222. * @param string $method cache method
  223. * @param array $args call args
  224. * @return \OC\Files\Node\Node[]
  225. */
  226. private function searchCommon($method, $args) {
  227. $limitToHome = ($method === 'searchQuery')? $args[0]->limitToHome(): false;
  228. if ($limitToHome && count(explode('/', $this->path)) !== 3) {
  229. throw new \InvalidArgumentException('searching by owner is only allows on the users home folder');
  230. }
  231. $files = [];
  232. $rootLength = strlen($this->path);
  233. $mount = $this->root->getMount($this->path);
  234. $storage = $mount->getStorage();
  235. $internalPath = $mount->getInternalPath($this->path);
  236. $internalPath = rtrim($internalPath, '/');
  237. if ($internalPath !== '') {
  238. $internalPath = $internalPath . '/';
  239. }
  240. $internalRootLength = strlen($internalPath);
  241. $cache = $storage->getCache('');
  242. $results = call_user_func_array([$cache, $method], $args);
  243. foreach ($results as $result) {
  244. if ($internalRootLength === 0 or substr($result['path'], 0, $internalRootLength) === $internalPath) {
  245. $result['internalPath'] = $result['path'];
  246. $result['path'] = substr($result['path'], $internalRootLength);
  247. $result['storage'] = $storage;
  248. $files[] = new \OC\Files\FileInfo($this->path . '/' . $result['path'], $storage, $result['internalPath'], $result, $mount);
  249. }
  250. }
  251. if (!$limitToHome) {
  252. $mounts = $this->root->getMountsIn($this->path);
  253. foreach ($mounts as $mount) {
  254. $storage = $mount->getStorage();
  255. if ($storage) {
  256. $cache = $storage->getCache('');
  257. $relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
  258. $results = call_user_func_array([$cache, $method], $args);
  259. foreach ($results as $result) {
  260. $result['internalPath'] = $result['path'];
  261. $result['path'] = $relativeMountPoint . $result['path'];
  262. $result['storage'] = $storage;
  263. $files[] = new \OC\Files\FileInfo($this->path . '/' . $result['path'], $storage,
  264. $result['internalPath'], $result, $mount);
  265. }
  266. }
  267. }
  268. }
  269. return array_map(function (FileInfo $file) {
  270. return $this->createNode($file->getPath(), $file);
  271. }, $files);
  272. }
  273. /**
  274. * @param int $id
  275. * @return \OC\Files\Node\Node[]
  276. */
  277. public function getById($id) {
  278. $mountCache = $this->root->getUserMountCache();
  279. if (strpos($this->getPath(), '/', 1) > 0) {
  280. list(, $user) = explode('/', $this->getPath());
  281. } else {
  282. $user = null;
  283. }
  284. $mountsContainingFile = $mountCache->getMountsForFileId((int)$id, $user);
  285. $mounts = $this->root->getMountsIn($this->path);
  286. $mounts[] = $this->root->getMount($this->path);
  287. /** @var IMountPoint[] $folderMounts */
  288. $folderMounts = array_combine(array_map(function (IMountPoint $mountPoint) {
  289. return $mountPoint->getMountPoint();
  290. }, $mounts), $mounts);
  291. /** @var ICachedMountInfo[] $mountsContainingFile */
  292. $mountsContainingFile = array_values(array_filter($mountsContainingFile, function (ICachedMountInfo $cachedMountInfo) use ($folderMounts) {
  293. return isset($folderMounts[$cachedMountInfo->getMountPoint()]);
  294. }));
  295. if (count($mountsContainingFile) === 0) {
  296. if ($user === $this->getAppDataDirectoryName()) {
  297. return $this->getByIdInRootMount((int) $id);
  298. }
  299. return [];
  300. }
  301. $nodes = array_map(function (ICachedMountInfo $cachedMountInfo) use ($folderMounts, $id) {
  302. $mount = $folderMounts[$cachedMountInfo->getMountPoint()];
  303. $cacheEntry = $mount->getStorage()->getCache()->get((int)$id);
  304. if (!$cacheEntry) {
  305. return null;
  306. }
  307. // cache jails will hide the "true" internal path
  308. $internalPath = ltrim($cachedMountInfo->getRootInternalPath() . '/' . $cacheEntry->getPath(), '/');
  309. $pathRelativeToMount = substr($internalPath, strlen($cachedMountInfo->getRootInternalPath()));
  310. $pathRelativeToMount = ltrim($pathRelativeToMount, '/');
  311. $absolutePath = rtrim($cachedMountInfo->getMountPoint() . $pathRelativeToMount, '/');
  312. return $this->root->createNode($absolutePath, new \OC\Files\FileInfo(
  313. $absolutePath, $mount->getStorage(), $cacheEntry->getPath(), $cacheEntry, $mount,
  314. \OC::$server->getUserManager()->get($mount->getStorage()->getOwner($pathRelativeToMount))
  315. ));
  316. }, $mountsContainingFile);
  317. $nodes = array_filter($nodes);
  318. return array_filter($nodes, function (Node $node) {
  319. return $this->getRelativePath($node->getPath());
  320. });
  321. }
  322. protected function getAppDataDirectoryName(): string {
  323. $instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid');
  324. return 'appdata_' . $instanceId;
  325. }
  326. /**
  327. * In case the path we are currently in is inside the appdata_* folder,
  328. * the original getById method does not work, because it can only look inside
  329. * the user's mount points. But the user has no mount point for the root storage.
  330. *
  331. * So in that case we directly check the mount of the root if it contains
  332. * the id. If it does we check if the path is inside the path we are working
  333. * in.
  334. *
  335. * @param int $id
  336. * @return array
  337. */
  338. protected function getByIdInRootMount(int $id): array {
  339. $mount = $this->root->getMount('');
  340. $cacheEntry = $mount->getStorage()->getCache($this->path)->get($id);
  341. if (!$cacheEntry) {
  342. return [];
  343. }
  344. $absolutePath = '/' . ltrim($cacheEntry->getPath(), '/');
  345. $currentPath = rtrim($this->path, '/') . '/';
  346. if (strpos($absolutePath, $currentPath) !== 0) {
  347. return [];
  348. }
  349. return [$this->root->createNode(
  350. $absolutePath, new \OC\Files\FileInfo(
  351. $absolutePath,
  352. $mount->getStorage(),
  353. $cacheEntry->getPath(),
  354. $cacheEntry,
  355. $mount
  356. ))];
  357. }
  358. public function getFreeSpace() {
  359. return $this->view->free_space($this->path);
  360. }
  361. public function delete() {
  362. if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) {
  363. $this->sendHooks(['preDelete']);
  364. $fileInfo = $this->getFileInfo();
  365. $this->view->rmdir($this->path);
  366. $nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo);
  367. $this->sendHooks(['postDelete'], [$nonExisting]);
  368. $this->exists = false;
  369. } else {
  370. throw new NotPermittedException('No delete permission for path');
  371. }
  372. }
  373. /**
  374. * Add a suffix to the name in case the file exists
  375. *
  376. * @param string $name
  377. * @return string
  378. * @throws NotPermittedException
  379. */
  380. public function getNonExistingName($name) {
  381. $uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view);
  382. return trim($this->getRelativePath($uniqueName), '/');
  383. }
  384. /**
  385. * @param int $limit
  386. * @param int $offset
  387. * @return \OCP\Files\Node[]
  388. */
  389. public function getRecent($limit, $offset = 0) {
  390. $mimetypeLoader = \OC::$server->getMimeTypeLoader();
  391. $mounts = $this->root->getMountsIn($this->path);
  392. $mounts[] = $this->getMountPoint();
  393. $mounts = array_filter($mounts, function (IMountPoint $mount) {
  394. return $mount->getStorage();
  395. });
  396. $storageIds = array_map(function (IMountPoint $mount) {
  397. return $mount->getStorage()->getCache()->getNumericStorageId();
  398. }, $mounts);
  399. /** @var IMountPoint[] $mountMap */
  400. $mountMap = array_combine($storageIds, $mounts);
  401. $folderMimetype = $mimetypeLoader->getId(FileInfo::MIMETYPE_FOLDER);
  402. /*
  403. * Construct an array of the storage id with their prefix path
  404. * This helps us to filter in the final query
  405. */
  406. $filters = array_map(function (IMountPoint $mount) {
  407. $storage = $mount->getStorage();
  408. $storageId = $storage->getCache()->getNumericStorageId();
  409. $prefix = '';
  410. if ($storage->instanceOfStorage(Jail::class)) {
  411. $prefix = $storage->getUnJailedPath('');
  412. }
  413. return [
  414. 'storageId' => $storageId,
  415. 'pathPrefix' => $prefix,
  416. ];
  417. }, $mounts);
  418. // Search in batches of 500 entries
  419. $searchLimit = 500;
  420. $results = [];
  421. $searchResultCount = 0;
  422. $count = 0;
  423. do {
  424. $searchResult = $this->recentSearch($searchLimit, $offset, $folderMimetype, $filters);
  425. // Exit condition if there are no more results
  426. if (count($searchResult) === 0) {
  427. break;
  428. }
  429. $searchResultCount += count($searchResult);
  430. $parseResult = $this->recentParse($searchResult, $mountMap, $mimetypeLoader);
  431. foreach ($parseResult as $result) {
  432. $results[] = $result;
  433. }
  434. $offset += $searchLimit;
  435. $count++;
  436. } while (count($results) < $limit && ($searchResultCount < (3 * $limit) || $count < 5));
  437. return array_slice($results, 0, $limit);
  438. }
  439. private function recentSearch($limit, $offset, $folderMimetype, $filters) {
  440. $dbconn = \OC::$server->getDatabaseConnection();
  441. $builder = $dbconn->getQueryBuilder();
  442. $query = $builder
  443. ->select('f.*')
  444. ->from('filecache', 'f');
  445. /*
  446. * Here is where we construct the filtering.
  447. * Note that this is expensive filtering as it is a lot of like queries.
  448. * However the alternative is we do this filtering and parsing later in php with the risk of looping endlessly
  449. */
  450. $storageFilters = $builder->expr()->orX();
  451. foreach ($filters as $filter) {
  452. $storageFilter = $builder->expr()->andX(
  453. $builder->expr()->eq('f.storage', $builder->createNamedParameter($filter['storageId']))
  454. );
  455. if ($filter['pathPrefix'] !== '') {
  456. $storageFilter->add(
  457. $builder->expr()->like('f.path', $builder->createNamedParameter($dbconn->escapeLikeParameter($filter['pathPrefix']) . '/%'))
  458. );
  459. }
  460. $storageFilters->add($storageFilter);
  461. }
  462. $query->andWhere($storageFilters);
  463. $query->andWhere($builder->expr()->orX(
  464. // handle non empty folders separate
  465. $builder->expr()->neq('f.mimetype', $builder->createNamedParameter($folderMimetype, IQueryBuilder::PARAM_INT)),
  466. $builder->expr()->eq('f.size', new Literal(0))
  467. ))
  468. ->andWhere($builder->expr()->notLike('f.path', $builder->createNamedParameter('files_versions/%')))
  469. ->andWhere($builder->expr()->notLike('f.path', $builder->createNamedParameter('files_trashbin/%')))
  470. ->orderBy('f.mtime', 'DESC')
  471. ->setMaxResults($limit)
  472. ->setFirstResult($offset);
  473. return $query->execute()->fetchAll();
  474. }
  475. private function recentParse($result, $mountMap, $mimetypeLoader) {
  476. $files = array_filter(array_map(function (array $entry) use ($mountMap, $mimetypeLoader) {
  477. $mount = $mountMap[$entry['storage']];
  478. $entry['internalPath'] = $entry['path'];
  479. $entry['mimetype'] = $mimetypeLoader->getMimetypeById($entry['mimetype']);
  480. $entry['mimepart'] = $mimetypeLoader->getMimetypeById($entry['mimepart']);
  481. $path = $this->getAbsolutePath($mount, $entry['path']);
  482. if (is_null($path)) {
  483. return null;
  484. }
  485. $fileInfo = new \OC\Files\FileInfo($path, $mount->getStorage(), $entry['internalPath'], $entry, $mount);
  486. return $this->root->createNode($fileInfo->getPath(), $fileInfo);
  487. }, $result));
  488. return array_values(array_filter($files, function (Node $node) {
  489. $cacheEntry = $node->getMountPoint()->getStorage()->getCache()->get($node->getId());
  490. if (!$cacheEntry) {
  491. return false;
  492. }
  493. $relative = $this->getRelativePath($node->getPath());
  494. return $relative !== null && $relative !== '/'
  495. && ($cacheEntry->getPermissions() & \OCP\Constants::PERMISSION_READ) === \OCP\Constants::PERMISSION_READ;
  496. }));
  497. }
  498. private function getAbsolutePath(IMountPoint $mount, $path) {
  499. $storage = $mount->getStorage();
  500. if ($storage->instanceOfStorage('\OC\Files\Storage\Wrapper\Jail')) {
  501. if ($storage->instanceOfStorage(SharedStorage::class)) {
  502. $storage->getSourceStorage();
  503. }
  504. /** @var \OC\Files\Storage\Wrapper\Jail $storage */
  505. $jailRoot = $storage->getUnjailedPath('');
  506. $rootLength = strlen($jailRoot) + 1;
  507. if ($path === $jailRoot) {
  508. return $mount->getMountPoint();
  509. } elseif (substr($path, 0, $rootLength) === $jailRoot . '/') {
  510. return $mount->getMountPoint() . substr($path, $rootLength);
  511. } else {
  512. return null;
  513. }
  514. } else {
  515. return $mount->getMountPoint() . $path;
  516. }
  517. }
  518. }