1
0

SharedStorage.php 15 KB

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