SharedStorage.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  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 OCA\Files_Sharing;
  8. use OC\Files\Cache\CacheDependencies;
  9. use OC\Files\Cache\CacheEntry;
  10. use OC\Files\Cache\FailedCache;
  11. use OC\Files\Cache\NullWatcher;
  12. use OC\Files\ObjectStore\HomeObjectStoreStorage;
  13. use OC\Files\Storage\Common;
  14. use OC\Files\Storage\FailedStorage;
  15. use OC\Files\Storage\Home;
  16. use OC\Files\Storage\Storage;
  17. use OC\Files\Storage\Wrapper\Jail;
  18. use OC\Files\Storage\Wrapper\PermissionsMask;
  19. use OC\Files\Storage\Wrapper\Wrapper;
  20. use OC\Files\View;
  21. use OC\Share\Share;
  22. use OC\User\NoUserException;
  23. use OCA\Files_Sharing\ISharedStorage as LegacyISharedStorage;
  24. use OCP\Constants;
  25. use OCP\Files\Cache\ICache;
  26. use OCP\Files\Cache\ICacheEntry;
  27. use OCP\Files\Cache\IScanner;
  28. use OCP\Files\Cache\IWatcher;
  29. use OCP\Files\Folder;
  30. use OCP\Files\IHomeStorage;
  31. use OCP\Files\IRootFolder;
  32. use OCP\Files\NotFoundException;
  33. use OCP\Files\Storage\IDisableEncryptionStorage;
  34. use OCP\Files\Storage\ILockingStorage;
  35. use OCP\Files\Storage\ISharedStorage;
  36. use OCP\Files\Storage\IStorage;
  37. use OCP\Lock\ILockingProvider;
  38. use OCP\Share\IShare;
  39. use OCP\Util;
  40. use Psr\Log\LoggerInterface;
  41. /**
  42. * Convert target path to source path and pass the function call to the correct storage provider
  43. */
  44. class SharedStorage extends Jail implements LegacyISharedStorage, ISharedStorage, IDisableEncryptionStorage {
  45. /** @var IShare */
  46. private $superShare;
  47. /** @var IShare[] */
  48. private $groupedShares;
  49. /**
  50. * @var View
  51. */
  52. private $ownerView;
  53. private $initialized = false;
  54. /**
  55. * @var ICacheEntry
  56. */
  57. private $sourceRootInfo;
  58. /** @var string */
  59. private $user;
  60. private LoggerInterface $logger;
  61. /** @var IStorage */
  62. private $nonMaskedStorage;
  63. private array $mountOptions = [];
  64. /** @var boolean */
  65. private $sharingDisabledForUser;
  66. /** @var ?Folder $ownerUserFolder */
  67. private $ownerUserFolder = null;
  68. private string $sourcePath = '';
  69. private static int $initDepth = 0;
  70. /**
  71. * @psalm-suppress NonInvariantDocblockPropertyType
  72. * @var ?Storage $storage
  73. */
  74. protected $storage;
  75. public function __construct(array $parameters) {
  76. $this->ownerView = $parameters['ownerView'];
  77. $this->logger = \OC::$server->get(LoggerInterface::class);
  78. $this->superShare = $parameters['superShare'];
  79. $this->groupedShares = $parameters['groupedShares'];
  80. $this->user = $parameters['user'];
  81. if (isset($parameters['sharingDisabledForUser'])) {
  82. $this->sharingDisabledForUser = $parameters['sharingDisabledForUser'];
  83. } else {
  84. $this->sharingDisabledForUser = false;
  85. }
  86. parent::__construct([
  87. 'storage' => null,
  88. 'root' => null,
  89. ]);
  90. }
  91. /**
  92. * @return ICacheEntry
  93. */
  94. private function getSourceRootInfo() {
  95. if (is_null($this->sourceRootInfo)) {
  96. if (is_null($this->superShare->getNodeCacheEntry())) {
  97. $this->init();
  98. $this->sourceRootInfo = $this->nonMaskedStorage->getCache()->get($this->rootPath);
  99. } else {
  100. $this->sourceRootInfo = $this->superShare->getNodeCacheEntry();
  101. }
  102. }
  103. return $this->sourceRootInfo;
  104. }
  105. /**
  106. * @psalm-assert Storage $this->storage
  107. */
  108. private function init() {
  109. if ($this->initialized) {
  110. if (!$this->storage) {
  111. // marked as initialized but no storage set
  112. // this is probably because some code path has caused recursion during the share setup
  113. // we setup a "failed storage" so `getWrapperStorage` doesn't return null.
  114. // If the share setup completes after this the "failed storage" will be overwritten by the correct one
  115. $this->logger->warning('Possible share setup recursion detected');
  116. $this->storage = new FailedStorage(['exception' => new \Exception('Possible share setup recursion detected')]);
  117. $this->cache = new FailedCache();
  118. $this->rootPath = '';
  119. }
  120. return;
  121. }
  122. $this->initialized = true;
  123. self::$initDepth++;
  124. try {
  125. if (self::$initDepth > 10) {
  126. throw new \Exception('Maximum share depth reached');
  127. }
  128. /** @var IRootFolder $rootFolder */
  129. $rootFolder = \OC::$server->get(IRootFolder::class);
  130. $this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner());
  131. $sourceId = $this->superShare->getNodeId();
  132. $ownerNodes = $this->ownerUserFolder->getById($sourceId);
  133. if (count($ownerNodes) === 0) {
  134. $this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]);
  135. $this->cache = new FailedCache();
  136. $this->rootPath = '';
  137. } else {
  138. foreach ($ownerNodes as $ownerNode) {
  139. $nonMaskedStorage = $ownerNode->getStorage();
  140. // check if potential source node would lead to a recursive share setup
  141. if ($nonMaskedStorage instanceof Wrapper && $nonMaskedStorage->isWrapperOf($this)) {
  142. continue;
  143. }
  144. $this->nonMaskedStorage = $nonMaskedStorage;
  145. $this->sourcePath = $ownerNode->getPath();
  146. $this->rootPath = $ownerNode->getInternalPath();
  147. $this->cache = null;
  148. break;
  149. }
  150. if (!$this->nonMaskedStorage) {
  151. // all potential source nodes would have been recursive
  152. throw new \Exception('recursive share detected');
  153. }
  154. $this->storage = new PermissionsMask([
  155. 'storage' => $this->nonMaskedStorage,
  156. 'mask' => $this->superShare->getPermissions(),
  157. ]);
  158. }
  159. } catch (NotFoundException $e) {
  160. // original file not accessible or deleted, set FailedStorage
  161. $this->storage = new FailedStorage(['exception' => $e]);
  162. $this->cache = new FailedCache();
  163. $this->rootPath = '';
  164. } catch (NoUserException $e) {
  165. // sharer user deleted, set FailedStorage
  166. $this->storage = new FailedStorage(['exception' => $e]);
  167. $this->cache = new FailedCache();
  168. $this->rootPath = '';
  169. } catch (\Exception $e) {
  170. $this->storage = new FailedStorage(['exception' => $e]);
  171. $this->cache = new FailedCache();
  172. $this->rootPath = '';
  173. $this->logger->error($e->getMessage(), ['exception' => $e]);
  174. }
  175. if (!$this->nonMaskedStorage) {
  176. $this->nonMaskedStorage = $this->storage;
  177. }
  178. self::$initDepth--;
  179. }
  180. public function instanceOfStorage(string $class): bool {
  181. if ($class === '\OC\Files\Storage\Common' || $class == Common::class) {
  182. return true;
  183. }
  184. if (in_array($class, [
  185. '\OC\Files\Storage\Home',
  186. '\OC\Files\ObjectStore\HomeObjectStoreStorage',
  187. '\OCP\Files\IHomeStorage',
  188. Home::class,
  189. HomeObjectStoreStorage::class,
  190. IHomeStorage::class
  191. ])) {
  192. return false;
  193. }
  194. return parent::instanceOfStorage($class);
  195. }
  196. /**
  197. * @return string
  198. */
  199. public function getShareId() {
  200. return $this->superShare->getId();
  201. }
  202. private function isValid(): bool {
  203. return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE;
  204. }
  205. public function getId(): string {
  206. return 'shared::' . $this->getMountPoint();
  207. }
  208. public function getPermissions(string $path = ''): int {
  209. if (!$this->isValid()) {
  210. return 0;
  211. }
  212. $permissions = parent::getPermissions($path) & $this->superShare->getPermissions();
  213. // part files and the mount point always have delete permissions
  214. if ($path === '' || pathinfo($path, PATHINFO_EXTENSION) === 'part') {
  215. $permissions |= Constants::PERMISSION_DELETE;
  216. }
  217. if ($this->sharingDisabledForUser) {
  218. $permissions &= ~Constants::PERMISSION_SHARE;
  219. }
  220. return $permissions;
  221. }
  222. public function isCreatable(string $path): bool {
  223. return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
  224. }
  225. public function isReadable(string $path): bool {
  226. if (!$this->isValid()) {
  227. return false;
  228. }
  229. if (!$this->file_exists($path)) {
  230. return false;
  231. }
  232. /** @var IStorage $storage */
  233. /** @var string $internalPath */
  234. [$storage, $internalPath] = $this->resolvePath($path);
  235. return $storage->isReadable($internalPath);
  236. }
  237. public function isUpdatable(string $path): bool {
  238. return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
  239. }
  240. public function isDeletable(string $path): bool {
  241. return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
  242. }
  243. public function isSharable(string $path): bool {
  244. if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) {
  245. return false;
  246. }
  247. return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
  248. }
  249. public function fopen(string $path, string $mode) {
  250. $source = $this->getUnjailedPath($path);
  251. switch ($mode) {
  252. case 'r+':
  253. case 'rb+':
  254. case 'w+':
  255. case 'wb+':
  256. case 'x+':
  257. case 'xb+':
  258. case 'a+':
  259. case 'ab+':
  260. case 'w':
  261. case 'wb':
  262. case 'x':
  263. case 'xb':
  264. case 'a':
  265. case 'ab':
  266. $creatable = $this->isCreatable(dirname($path));
  267. $updatable = $this->isUpdatable($path);
  268. // if neither permissions given, no need to continue
  269. if (!$creatable && !$updatable) {
  270. if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
  271. $updatable = $this->isUpdatable(dirname($path));
  272. }
  273. if (!$updatable) {
  274. return false;
  275. }
  276. }
  277. $exists = $this->file_exists($path);
  278. // if a file exists, updatable permissions are required
  279. if ($exists && !$updatable) {
  280. return false;
  281. }
  282. // part file is allowed if !$creatable but the final file is $updatable
  283. if (pathinfo($path, PATHINFO_EXTENSION) !== 'part') {
  284. if (!$exists && !$creatable) {
  285. return false;
  286. }
  287. }
  288. }
  289. $info = [
  290. 'target' => $this->getMountPoint() . '/' . $path,
  291. 'source' => $source,
  292. 'mode' => $mode,
  293. ];
  294. Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info);
  295. return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode);
  296. }
  297. public function rename(string $source, string $target): bool {
  298. $this->init();
  299. $isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part';
  300. $targetExists = $this->file_exists($target);
  301. $sameFolder = dirname($source) === dirname($target);
  302. if ($targetExists || ($sameFolder && !$isPartFile)) {
  303. if (!$this->isUpdatable('')) {
  304. return false;
  305. }
  306. } else {
  307. if (!$this->isCreatable('')) {
  308. return false;
  309. }
  310. }
  311. return $this->nonMaskedStorage->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target));
  312. }
  313. /**
  314. * return mount point of share, relative to data/user/files
  315. *
  316. * @return string
  317. */
  318. public function getMountPoint(): string {
  319. return $this->superShare->getTarget();
  320. }
  321. public function setMountPoint(string $path): void {
  322. $this->superShare->setTarget($path);
  323. foreach ($this->groupedShares as $share) {
  324. $share->setTarget($path);
  325. }
  326. }
  327. /**
  328. * get the user who shared the file
  329. *
  330. * @return string
  331. */
  332. public function getSharedFrom(): string {
  333. return $this->superShare->getShareOwner();
  334. }
  335. public function getShare(): IShare {
  336. return $this->superShare;
  337. }
  338. /**
  339. * return share type, can be "file" or "folder"
  340. *
  341. * @return string
  342. */
  343. public function getItemType(): string {
  344. return $this->superShare->getNodeType();
  345. }
  346. public function getCache(string $path = '', ?IStorage $storage = null): ICache {
  347. if ($this->cache) {
  348. return $this->cache;
  349. }
  350. if (!$storage) {
  351. $storage = $this;
  352. }
  353. $sourceRoot = $this->getSourceRootInfo();
  354. if ($this->storage instanceof FailedStorage) {
  355. return new FailedCache();
  356. }
  357. $this->cache = new Cache(
  358. $storage,
  359. $sourceRoot,
  360. \OC::$server->get(CacheDependencies::class),
  361. $this->getShare()
  362. );
  363. return $this->cache;
  364. }
  365. public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
  366. if (!$storage) {
  367. $storage = $this;
  368. }
  369. return new Scanner($storage);
  370. }
  371. public function getOwner(string $path): string|false {
  372. return $this->superShare->getShareOwner();
  373. }
  374. public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
  375. if ($this->watcher) {
  376. return $this->watcher;
  377. }
  378. // Get node information
  379. $node = $this->getShare()->getNodeCacheEntry();
  380. if ($node instanceof CacheEntry) {
  381. $storageId = $node->getData()['storage_string_id'];
  382. // for shares from the home storage we can rely on the home storage to keep itself up to date
  383. // for other storages we need use the proper watcher
  384. if (!(str_starts_with($storageId, 'home::') || str_starts_with($storageId, 'object::user'))) {
  385. $this->watcher = parent::getWatcher($path, $storage);
  386. return $this->watcher;
  387. }
  388. }
  389. // cache updating is handled by the share source
  390. $this->watcher = new NullWatcher();
  391. return $this->watcher;
  392. }
  393. /**
  394. * unshare complete storage, also the grouped shares
  395. *
  396. * @return bool
  397. */
  398. public function unshareStorage(): bool {
  399. foreach ($this->groupedShares as $share) {
  400. \OC::$server->getShareManager()->deleteFromSelf($share, $this->user);
  401. }
  402. return true;
  403. }
  404. public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
  405. /** @var ILockingStorage $targetStorage */
  406. [$targetStorage, $targetInternalPath] = $this->resolvePath($path);
  407. $targetStorage->acquireLock($targetInternalPath, $type, $provider);
  408. // lock the parent folders of the owner when locking the share as recipient
  409. if ($path === '') {
  410. $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath);
  411. $this->ownerView->lockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true);
  412. }
  413. }
  414. public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
  415. /** @var ILockingStorage $targetStorage */
  416. [$targetStorage, $targetInternalPath] = $this->resolvePath($path);
  417. $targetStorage->releaseLock($targetInternalPath, $type, $provider);
  418. // unlock the parent folders of the owner when unlocking the share as recipient
  419. if ($path === '') {
  420. $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath);
  421. $this->ownerView->unlockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true);
  422. }
  423. }
  424. public function changeLock(string $path, int $type, ILockingProvider $provider): void {
  425. /** @var ILockingStorage $targetStorage */
  426. [$targetStorage, $targetInternalPath] = $this->resolvePath($path);
  427. $targetStorage->changeLock($targetInternalPath, $type, $provider);
  428. }
  429. public function getAvailability(): array {
  430. // shares do not participate in availability logic
  431. return [
  432. 'available' => true,
  433. 'last_checked' => 0,
  434. ];
  435. }
  436. public function setAvailability(bool $isAvailable): void {
  437. // shares do not participate in availability logic
  438. }
  439. public function getSourceStorage() {
  440. $this->init();
  441. return $this->nonMaskedStorage;
  442. }
  443. public function getWrapperStorage(): Storage {
  444. $this->init();
  445. /**
  446. * @psalm-suppress DocblockTypeContradiction
  447. */
  448. if (!$this->storage) {
  449. $message = 'no storage set after init for share ' . $this->getShareId();
  450. $this->logger->error($message);
  451. $this->storage = new FailedStorage(['exception' => new \Exception($message)]);
  452. }
  453. return $this->storage;
  454. }
  455. public function file_get_contents(string $path): string|false {
  456. $info = [
  457. 'target' => $this->getMountPoint() . '/' . $path,
  458. 'source' => $this->getUnjailedPath($path),
  459. ];
  460. Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info);
  461. return parent::file_get_contents($path);
  462. }
  463. public function file_put_contents(string $path, mixed $data): int|float|false {
  464. $info = [
  465. 'target' => $this->getMountPoint() . '/' . $path,
  466. 'source' => $this->getUnjailedPath($path),
  467. ];
  468. Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info);
  469. return parent::file_put_contents($path, $data);
  470. }
  471. public function setMountOptions(array $options): void {
  472. /* Note: This value is never read */
  473. $this->mountOptions = $options;
  474. }
  475. public function getUnjailedPath(string $path): string {
  476. $this->init();
  477. return parent::getUnjailedPath($path);
  478. }
  479. }