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.

Share.php 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Bart Visscher <bartv@thisnet.nl>
  7. * @author Bernhard Reiter <ockham@raz.or.at>
  8. * @author Björn Schießle <bjoern@schiessle.org>
  9. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  10. * @author Joas Schilling <coding@schilljs.com>
  11. * @author Morris Jobke <hey@morrisjobke.de>
  12. * @author Robin Appelman <robin@icewind.nl>
  13. * @author Robin McCorkell <robin@mccorkell.me.uk>
  14. * @author Roeland Jago Douma <roeland@famdouma.nl>
  15. * @author Sebastian Döll <sebastian.doell@libasys.de>
  16. * @author Thomas Müller <thomas.mueller@tmit.eu>
  17. * @author Vincent Petry <vincent@nextcloud.com>
  18. * @author Volkan Gezer <volkangezer@gmail.com>
  19. *
  20. * @license AGPL-3.0
  21. *
  22. * This code is free software: you can redistribute it and/or modify
  23. * it under the terms of the GNU Affero General Public License, version 3,
  24. * as published by the Free Software Foundation.
  25. *
  26. * This program is distributed in the hope that it will be useful,
  27. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  28. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  29. * GNU Affero General Public License for more details.
  30. *
  31. * You should have received a copy of the GNU Affero General Public License, version 3,
  32. * along with this program. If not, see <http://www.gnu.org/licenses/>
  33. *
  34. */
  35. namespace OC\Share;
  36. use OCA\Files_Sharing\ShareBackend\File;
  37. use OCP\DB\Exception;
  38. use OCP\DB\QueryBuilder\IQueryBuilder;
  39. use OCP\IDBConnection;
  40. use OCP\Share\IShare;
  41. use Psr\Log\LoggerInterface;
  42. /**
  43. * This class provides the ability for apps to share their content between users.
  44. * Apps must create a backend class that implements OCP\Share_Backend and register it with this class.
  45. *
  46. * It provides the following hooks:
  47. * - post_shared
  48. */
  49. class Share extends Constants {
  50. /** CRUDS permissions (Create, Read, Update, Delete, Share) using a bitmask
  51. * Construct permissions for share() and setPermissions with Or (|) e.g.
  52. * Give user read and update permissions: PERMISSION_READ | PERMISSION_UPDATE
  53. *
  54. * Check if permission is granted with And (&) e.g. Check if delete is
  55. * granted: if ($permissions & PERMISSION_DELETE)
  56. *
  57. * Remove permissions with And (&) and Not (~) e.g. Remove the update
  58. * permission: $permissions &= ~PERMISSION_UPDATE
  59. *
  60. * Apps are required to handle permissions on their own, this class only
  61. * stores and manages the permissions of shares
  62. *
  63. * @see lib/public/constants.php
  64. */
  65. /**
  66. * Register a sharing backend class that implements OCP\Share_Backend for an item type
  67. *
  68. * @param string $itemType Item type
  69. * @param string $class Backend class
  70. * @param string $collectionOf (optional) Depends on item type
  71. * @param array $supportedFileExtensions (optional) List of supported file extensions if this item type depends on files
  72. * @return boolean true if backend is registered or false if error
  73. */
  74. public static function registerBackend($itemType, $class, $collectionOf = null, $supportedFileExtensions = null) {
  75. if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_enabled', 'yes') == 'yes') {
  76. if (!isset(self::$backendTypes[$itemType])) {
  77. self::$backendTypes[$itemType] = [
  78. 'class' => $class,
  79. 'collectionOf' => $collectionOf,
  80. 'supportedFileExtensions' => $supportedFileExtensions
  81. ];
  82. return true;
  83. }
  84. \OC::$server->get(LoggerInterface::class)->warning(
  85. 'Sharing backend '.$class.' not registered, '.self::$backendTypes[$itemType]['class']
  86. .' is already registered for '.$itemType,
  87. ['app' => 'files_sharing']);
  88. }
  89. return false;
  90. }
  91. /**
  92. * Get the items of item type shared with the current user
  93. *
  94. * @param string $itemType
  95. * @param int $format (optional) Format type must be defined by the backend
  96. * @param mixed $parameters (optional)
  97. * @param int $limit Number of items to return (optional) Returns all by default
  98. * @param boolean $includeCollections (optional)
  99. * @return mixed Return depends on format
  100. * @deprecated TESTS ONLY - this methods is only used by tests
  101. * called like this:
  102. * \OC\Share\Share::getItemsSharedWith('folder'); (apps/files_sharing/tests/UpdaterTest.php)
  103. */
  104. public static function getItemsSharedWith() {
  105. return self::getItems('folder', null, self::$shareTypeUserAndGroups, \OC_User::getUser());
  106. }
  107. /**
  108. * Get the items of item type shared with a user
  109. *
  110. * @param string $itemType
  111. * @param string $user id for which user we want the shares
  112. * @param int $format (optional) Format type must be defined by the backend
  113. * @param mixed $parameters (optional)
  114. * @param int $limit Number of items to return (optional) Returns all by default
  115. * @param boolean $includeCollections (optional)
  116. * @return mixed Return depends on format
  117. * @deprecated TESTS ONLY - this methods is only used by tests
  118. * called like this:
  119. * \OC\Share\Share::getItemsSharedWithUser('test', $shareWith); (tests/lib/Share/Backend.php)
  120. */
  121. public static function getItemsSharedWithUser($itemType, $user) {
  122. return self::getItems('test', null, self::$shareTypeUserAndGroups, $user);
  123. }
  124. /**
  125. * Get the item of item type shared with a given user by source
  126. *
  127. * @param string $itemType
  128. * @param string $itemSource
  129. * @param ?string $user User to whom the item was shared
  130. * @param ?string $owner Owner of the share
  131. * @param ?int $shareType only look for a specific share type
  132. * @return array Return list of items with file_target, permissions and expiration
  133. * @throws Exception
  134. */
  135. public static function getItemSharedWithUser(string $itemType, string $itemSource, ?string $user = null, ?string $owner = null, ?int $shareType = null) {
  136. $shares = [];
  137. $fileDependent = $itemType === 'file' || $itemType === 'folder';
  138. $qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent);
  139. $qb->from('share', 's');
  140. if ($fileDependent) {
  141. $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'));
  142. $qb->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
  143. $column = 'file_source';
  144. } else {
  145. $column = 'item_source';
  146. }
  147. $qb->where($qb->expr()->eq($column, $qb->createNamedParameter($itemSource)))
  148. ->andWhere($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType)));
  149. // for link shares $user === null
  150. if ($user !== null) {
  151. $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($user)));
  152. }
  153. if ($shareType !== null) {
  154. $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT)));
  155. }
  156. if ($owner !== null) {
  157. $qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($owner)));
  158. }
  159. $result = $qb->executeQuery();
  160. while ($row = $result->fetch()) {
  161. if ($fileDependent && !self::isFileReachable($row['path'], $row['storage_id'])) {
  162. continue;
  163. }
  164. if ($fileDependent && (int)$row['file_parent'] === -1) {
  165. // if it is a mount point we need to get the path from the mount manager
  166. $mountManager = \OC\Files\Filesystem::getMountManager();
  167. $mountPoint = $mountManager->findByStorageId($row['storage_id']);
  168. if (!empty($mountPoint)) {
  169. $path = $mountPoint[0]->getMountPoint();
  170. $path = trim($path, '/');
  171. $path = substr($path, strlen($owner) + 1); //normalize path to 'files/foo.txt`
  172. $row['path'] = $path;
  173. } else {
  174. \OC::$server->get(LoggerInterface::class)->warning(
  175. 'Could not resolve mount point for ' . $row['storage_id'],
  176. ['app' => 'OCP\Share']
  177. );
  178. }
  179. }
  180. $shares[] = $row;
  181. }
  182. $result->closeCursor();
  183. // if we didn't found a result then let's look for a group share.
  184. if (empty($shares) && $user !== null) {
  185. $userObject = \OC::$server->getUserManager()->get($user);
  186. $groups = [];
  187. if ($userObject) {
  188. $groups = \OC::$server->getGroupManager()->getUserGroupIds($userObject);
  189. }
  190. if (!empty($groups)) {
  191. $qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent);
  192. $qb->from('share', 's');
  193. if ($fileDependent) {
  194. $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'))
  195. ->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
  196. }
  197. $qb->where($qb->expr()->eq($column, $qb->createNamedParameter($itemSource)))
  198. ->andWhere($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType, IQueryBuilder::PARAM_STR)))
  199. ->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY)));
  200. if ($owner !== null) {
  201. $qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($owner)));
  202. }
  203. $result = $qb->executeQuery();
  204. while ($row = $result->fetch()) {
  205. $shares[] = $row;
  206. }
  207. $result->closeCursor();
  208. }
  209. }
  210. return $shares;
  211. }
  212. /**
  213. * Get the shared item of item type owned by the current user
  214. *
  215. * @param string $itemType
  216. * @param string $itemSource
  217. * @param int $format (optional) Format type must be defined by the backend
  218. * @param mixed $parameters
  219. * @param boolean $includeCollections
  220. * @return mixed Return depends on format
  221. *
  222. * Refactoring notes:
  223. * * defacto $parameters and $format is always the default and therefore is removed in the subsequent call
  224. */
  225. public static function getItemShared($itemType, $itemSource, $format = self::FORMAT_NONE,
  226. $parameters = null, $includeCollections = false) {
  227. return self::getItems($itemType, $itemSource, null, null, \OC_User::getUser(), self::FORMAT_NONE,
  228. null, -1, $includeCollections);
  229. }
  230. /**
  231. * Get the backend class for the specified item type
  232. *
  233. * @param string $itemType
  234. * @return \OCP\Share_Backend
  235. * @throws \Exception
  236. */
  237. public static function getBackend($itemType) {
  238. $l = \OC::$server->getL10N('lib');
  239. $logger = \OC::$server->get(LoggerInterface::class);
  240. if (isset(self::$backends[$itemType])) {
  241. return self::$backends[$itemType];
  242. } elseif (isset(self::$backendTypes[$itemType]['class'])) {
  243. $class = self::$backendTypes[$itemType]['class'];
  244. if (class_exists($class)) {
  245. self::$backends[$itemType] = new $class;
  246. if (!(self::$backends[$itemType] instanceof \OCP\Share_Backend)) {
  247. $message = 'Sharing backend %s must implement the interface OCP\Share_Backend';
  248. $message_t = $l->t('Sharing backend %s must implement the interface OCP\Share_Backend', [$class]);
  249. $logger->error(sprintf($message, $class), ['app' => 'OCP\Share']);
  250. throw new \Exception($message_t);
  251. }
  252. return self::$backends[$itemType];
  253. } else {
  254. $message = 'Sharing backend %s not found';
  255. $message_t = $l->t('Sharing backend %s not found', [$class]);
  256. $logger->error(sprintf($message, $class), ['app' => 'OCP\Share']);
  257. throw new \Exception($message_t);
  258. }
  259. }
  260. $message = 'Sharing backend for %s not found';
  261. $message_t = $l->t('Sharing backend for %s not found', [$itemType]);
  262. $logger->error(sprintf($message, $itemType), ['app' => 'OCP\Share']);
  263. throw new \Exception($message_t);
  264. }
  265. /**
  266. * Check if resharing is allowed
  267. *
  268. * @return boolean true if allowed or false
  269. *
  270. * Resharing is allowed by default if not configured
  271. */
  272. public static function isResharingAllowed() {
  273. if (!isset(self::$isResharingAllowed)) {
  274. if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_allow_resharing', 'yes') == 'yes') {
  275. self::$isResharingAllowed = true;
  276. } else {
  277. self::$isResharingAllowed = false;
  278. }
  279. }
  280. return self::$isResharingAllowed;
  281. }
  282. /**
  283. * Get a list of collection item types for the specified item type
  284. *
  285. * @param string $itemType
  286. * @return array|false
  287. */
  288. private static function getCollectionItemTypes(string $itemType) {
  289. $collectionTypes = [$itemType];
  290. foreach (self::$backendTypes as $type => $backend) {
  291. if (in_array($backend['collectionOf'], $collectionTypes)) {
  292. $collectionTypes[] = $type;
  293. }
  294. }
  295. // TODO Add option for collections to be collection of themselves, only 'folder' does it now...
  296. if (isset(self::$backendTypes[$itemType]) && (!self::getBackend($itemType) instanceof \OCP\Share_Backend_Collection || $itemType != 'folder')) {
  297. unset($collectionTypes[0]);
  298. }
  299. // Return array if collections were found or the item type is a
  300. // collection itself - collections can be inside collections
  301. if (count($collectionTypes) > 0) {
  302. return $collectionTypes;
  303. }
  304. return false;
  305. }
  306. /**
  307. * Get shared items from the database
  308. *
  309. * @param string $itemType
  310. * @param string $item Item source or target (optional)
  311. * @param int $shareType SHARE_TYPE_USER, SHARE_TYPE_GROUP, SHARE_TYPE_LINK, $shareTypeUserAndGroups, or $shareTypeGroupUserUnique
  312. * @param string $shareWith User or group the item is being shared with
  313. * @param string $uidOwner User that is the owner of shared items (optional)
  314. * @param int $format Format to convert items to with formatItems() (optional)
  315. * @param mixed $parameters to pass to formatItems() (optional)
  316. * @param int $limit Number of items to return, -1 to return all matches (optional)
  317. * @param boolean $includeCollections Include collection item types (optional)
  318. * @param boolean $itemShareWithBySource (optional)
  319. * @param boolean $checkExpireDate
  320. * @return array
  321. *
  322. * See public functions getItem(s)... for parameter usage
  323. *
  324. * Refactoring notes:
  325. * * defacto $limit, $itemsShareWithBySource, $checkExpireDate, $parameters and $format is always the default and therefore is removed in the subsequent call
  326. */
  327. public static function getItems($itemType, ?string $item = null, ?int $shareType = null, $shareWith = null,
  328. $uidOwner = null, $format = self::FORMAT_NONE, $parameters = null, $limit = -1,
  329. $includeCollections = false, $itemShareWithBySource = false, $checkExpireDate = true) {
  330. if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_enabled', 'yes') != 'yes') {
  331. return [];
  332. }
  333. $fileDependent = $itemType == 'file' || $itemType == 'folder';
  334. $qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent, $uidOwner);
  335. $qb->from('share', 's');
  336. $backend = self::getBackend($itemType);
  337. $collectionTypes = false;
  338. // Get filesystem root to add it to the file target and remove from the
  339. // file source, match file_source with the file cache
  340. if ($fileDependent) {
  341. if (!is_null($uidOwner)) {
  342. $root = \OC\Files\Filesystem::getRoot();
  343. } else {
  344. $root = '';
  345. }
  346. if (isset($item)) {
  347. $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'));
  348. } else {
  349. $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->andX(
  350. $qb->expr()->eq('file_source', 'f.fileid'),
  351. $qb->expr()->isNotNull('file_target')
  352. ));
  353. }
  354. $qb->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
  355. } else {
  356. $root = '';
  357. $collectionTypes = self::getCollectionItemTypes($itemType);
  358. if ($includeCollections && !isset($item) && $collectionTypes) {
  359. // If includeCollections is true, find collections of this item type, e.g. a music album contains songs
  360. if (!in_array($itemType, $collectionTypes)) {
  361. $itemTypes = array_merge([$itemType], $collectionTypes);
  362. } else {
  363. $itemTypes = $collectionTypes;
  364. }
  365. $qb->where($qb->expr()->in('item_type', $qb->createNamedParameter($itemTypes, IQueryBuilder::PARAM_STR_ARRAY)));
  366. } else {
  367. $qb->where($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType)));
  368. }
  369. }
  370. if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
  371. $qb->andWhere($qb->expr()->neq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK, IQueryBuilder::PARAM_INT)));
  372. }
  373. if (isset($shareType)) {
  374. // Include all user and group items
  375. if ($shareType === self::$shareTypeUserAndGroups && isset($shareWith)) {
  376. $qb->andWhere($qb->expr()->andX(
  377. $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, self::$shareTypeGroupUserUnique], IQueryBuilder::PARAM_INT_ARRAY)),
  378. $qb->expr()->eq('share_with', $qb->createNamedParameter($shareWith))
  379. ));
  380. $user = \OC::$server->getUserManager()->get($shareWith);
  381. $groups = [];
  382. if ($user) {
  383. $groups = \OC::$server->getGroupManager()->getUserGroupIds($user);
  384. }
  385. if (!empty($groups)) {
  386. $qb->orWhere($qb->expr()->andX(
  387. $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP, IQueryBuilder::PARAM_INT)),
  388. $qb->expr()->in('share_with', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY))
  389. ));
  390. }
  391. // Don't include own group shares
  392. $qb->andWhere($qb->expr()->neq('uid_owner', $qb->createNamedParameter($shareWith)));
  393. } else {
  394. $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT)));
  395. if (isset($shareWith)) {
  396. $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($shareWith, IQueryBuilder::PARAM_STR)));
  397. }
  398. }
  399. }
  400. if (isset($uidOwner)) {
  401. $qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uidOwner)));
  402. if (!isset($shareType)) {
  403. // Prevent unique user targets for group shares from being selected
  404. $qb->andWhere($qb->expr()->neq('share_type', $qb->createNamedParameter(self::$shareTypeGroupUserUnique, IQueryBuilder::PARAM_INT)));
  405. }
  406. if ($fileDependent) {
  407. $column = 'file_source';
  408. } else {
  409. $column = 'item_source';
  410. }
  411. } else {
  412. if ($fileDependent) {
  413. $column = 'file_target';
  414. } else {
  415. $column = 'item_target';
  416. }
  417. }
  418. if (isset($item)) {
  419. $collectionTypes = self::getCollectionItemTypes($itemType);
  420. // If looking for own shared items, check item_source else check item_target
  421. if (isset($uidOwner)) {
  422. // If item type is a file, file source needs to be checked in case the item was converted
  423. if ($fileDependent) {
  424. $expr = $qb->expr()->eq('file_source', $qb->createNamedParameter($item));
  425. $column = 'file_source';
  426. } else {
  427. $expr = $qb->expr()->eq('item_source', $qb->createNamedParameter($item));
  428. $column = 'item_source';
  429. }
  430. } else {
  431. if ($fileDependent) {
  432. $item = \OC\Files\Filesystem::normalizePath($item);
  433. $expr = $qb->expr()->eq('file_target', $qb->createNamedParameter($item));
  434. } else {
  435. $expr = $qb->expr()->eq('item_target', $qb->createNamedParameter($item));
  436. }
  437. }
  438. if ($includeCollections && $collectionTypes && !in_array('folder', $collectionTypes)) {
  439. $qb->andWhere($qb->expr()->orX(
  440. $expr,
  441. $qb->expr()->in('item_type', $qb->createNamedParameter($collectionTypes, IQueryBuilder::PARAM_STR_ARRAY))
  442. ));
  443. } else {
  444. $qb->andWhere($expr);
  445. }
  446. }
  447. $qb->orderBy('s.id', 'ASC');
  448. try {
  449. $result = $qb->executeQuery();
  450. } catch (\Exception $e) {
  451. \OCP\Server::get(LoggerInterface::class)->error(
  452. 'Error while selecting shares: ' . $qb->getSQL(),
  453. [
  454. 'app' => 'files_sharing',
  455. 'exception' => $e
  456. ]);
  457. throw new \RuntimeException('Wrong SQL query', 500, $e);
  458. }
  459. $root = strlen($root);
  460. $items = [];
  461. $targets = [];
  462. $switchedItems = [];
  463. $mounts = [];
  464. while ($row = $result->fetch()) {
  465. //var_dump($row);
  466. self::transformDBResults($row);
  467. // Filter out duplicate group shares for users with unique targets
  468. if ($fileDependent && !self::isFileReachable($row['path'], $row['storage_id'])) {
  469. continue;
  470. }
  471. if ($row['share_type'] == self::$shareTypeGroupUserUnique && isset($items[$row['parent']])) {
  472. $row['share_type'] = IShare::TYPE_GROUP;
  473. $row['unique_name'] = true; // remember that we use a unique name for this user
  474. $row['share_with'] = $items[$row['parent']]['share_with'];
  475. // if the group share was unshared from the user we keep the permission, otherwise
  476. // we take the permission from the parent because this is always the up-to-date
  477. // permission for the group share
  478. if ($row['permissions'] > 0) {
  479. $row['permissions'] = $items[$row['parent']]['permissions'];
  480. }
  481. // Remove the parent group share
  482. unset($items[$row['parent']]);
  483. if ($row['permissions'] == 0) {
  484. continue;
  485. }
  486. } elseif (!isset($uidOwner)) {
  487. // Check if the same target already exists
  488. if (isset($targets[$row['id']])) {
  489. // Check if the same owner shared with the user twice
  490. // through a group and user share - this is allowed
  491. $id = $targets[$row['id']];
  492. if (isset($items[$id]) && $items[$id]['uid_owner'] == $row['uid_owner']) {
  493. // Switch to group share type to ensure resharing conditions aren't bypassed
  494. if ($items[$id]['share_type'] != IShare::TYPE_GROUP) {
  495. $items[$id]['share_type'] = IShare::TYPE_GROUP;
  496. $items[$id]['share_with'] = $row['share_with'];
  497. }
  498. // Switch ids if sharing permission is granted on only
  499. // one share to ensure correct parent is used if resharing
  500. if (~(int)$items[$id]['permissions'] & \OCP\Constants::PERMISSION_SHARE
  501. && (int)$row['permissions'] & \OCP\Constants::PERMISSION_SHARE) {
  502. $items[$row['id']] = $items[$id];
  503. $switchedItems[$id] = $row['id'];
  504. unset($items[$id]);
  505. $id = $row['id'];
  506. }
  507. $items[$id]['permissions'] |= (int)$row['permissions'];
  508. }
  509. continue;
  510. } elseif (!empty($row['parent'])) {
  511. $targets[$row['parent']] = $row['id'];
  512. }
  513. }
  514. // Remove root from file source paths if retrieving own shared items
  515. if (isset($uidOwner) && isset($row['path'])) {
  516. if (isset($row['parent'])) {
  517. $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
  518. $query->select('file_target')
  519. ->from('share')
  520. ->where($query->expr()->eq('id', $query->createNamedParameter($row['parent'])));
  521. $parentRow = false;
  522. try {
  523. $parentResult = $query->executeQuery();
  524. $parentRow = $parentResult->fetchOne();
  525. $parentResult->closeCursor();
  526. $tmpPath = $parentRow['file_target'];
  527. // find the right position where the row path continues from the target path
  528. $pos = strrpos($row['path'], $parentRow['file_target']);
  529. $subPath = substr($row['path'], $pos);
  530. $splitPath = explode('/', $subPath);
  531. foreach (array_slice($splitPath, 2) as $pathPart) {
  532. $tmpPath = $tmpPath . '/' . $pathPart;
  533. }
  534. $row['path'] = $tmpPath;
  535. } catch (Exception $e) {
  536. \OCP\Server::get(LoggerInterface::class)
  537. ->error('Can\'t select parent :' . $e->getMessage() . ' query=' . $query->getSQL(), [
  538. 'exception' => $e,
  539. 'app' => 'core'
  540. ]);
  541. }
  542. } else {
  543. if (!isset($mounts[$row['storage']])) {
  544. $mountPoints = \OC\Files\Filesystem::getMountByNumericId($row['storage']);
  545. if (is_array($mountPoints) && !empty($mountPoints)) {
  546. $mounts[$row['storage']] = current($mountPoints);
  547. }
  548. }
  549. if (!empty($mounts[$row['storage']])) {
  550. $path = $mounts[$row['storage']]->getMountPoint() . $row['path'];
  551. $relPath = substr($path, $root); // path relative to data/user
  552. $row['path'] = rtrim($relPath, '/');
  553. }
  554. }
  555. }
  556. // Check if resharing is allowed, if not remove share permission
  557. if (isset($row['permissions']) && (!self::isResharingAllowed() | \OCP\Util::isSharingDisabledForUser())) {
  558. $row['permissions'] &= ~\OCP\Constants::PERMISSION_SHARE;
  559. }
  560. // Add display names to result
  561. $row['share_with_displayname'] = $row['share_with'];
  562. if (isset($row['share_with']) && $row['share_with'] != '' &&
  563. $row['share_type'] === IShare::TYPE_USER) {
  564. $shareWithUser = \OC::$server->getUserManager()->get($row['share_with']);
  565. $row['share_with_displayname'] = $shareWithUser === null ? $row['share_with'] : $shareWithUser->getDisplayName();
  566. } elseif (isset($row['share_with']) && $row['share_with'] != '' &&
  567. $row['share_type'] === IShare::TYPE_REMOTE) {
  568. $addressBookEntries = \OC::$server->getContactsManager()->search($row['share_with'], ['CLOUD'], [
  569. 'limit' => 1,
  570. 'enumeration' => false,
  571. 'fullmatch' => false,
  572. 'strict_search' => true,
  573. ]);
  574. foreach ($addressBookEntries as $entry) {
  575. foreach ($entry['CLOUD'] as $cloudID) {
  576. if ($cloudID === $row['share_with']) {
  577. $row['share_with_displayname'] = $entry['FN'];
  578. }
  579. }
  580. }
  581. }
  582. if (isset($row['uid_owner']) && $row['uid_owner'] != '') {
  583. $ownerUser = \OC::$server->getUserManager()->get($row['uid_owner']);
  584. $row['displayname_owner'] = $ownerUser === null ? $row['uid_owner'] : $ownerUser->getDisplayName();
  585. }
  586. if ($row['permissions'] > 0) {
  587. $items[$row['id']] = $row;
  588. }
  589. }
  590. $result->closeCursor();
  591. // group items if we are looking for items shared with the current user
  592. if (isset($shareWith) && $shareWith === \OC_User::getUser()) {
  593. $items = self::groupItems($items, $itemType);
  594. }
  595. if (!empty($items)) {
  596. $collectionItems = [];
  597. foreach ($items as &$row) {
  598. // Check if this is a collection of the requested item type
  599. if ($includeCollections && $collectionTypes && $row['item_type'] !== 'folder' && in_array($row['item_type'], $collectionTypes)) {
  600. if (($collectionBackend = self::getBackend($row['item_type']))
  601. && $collectionBackend instanceof \OCP\Share_Backend_Collection) {
  602. // Collections can be inside collections, check if the item is a collection
  603. if (isset($item) && $row['item_type'] == $itemType && $row[$column] == $item) {
  604. $collectionItems[] = $row;
  605. } else {
  606. $collection = [];
  607. $collection['item_type'] = $row['item_type'];
  608. if ($row['item_type'] == 'file' || $row['item_type'] == 'folder') {
  609. $collection['path'] = basename($row['path']);
  610. }
  611. $row['collection'] = $collection;
  612. // Fetch all the children sources
  613. $children = $collectionBackend->getChildren($row[$column]);
  614. foreach ($children as $child) {
  615. $childItem = $row;
  616. $childItem['item_type'] = $itemType;
  617. if ($row['item_type'] != 'file' && $row['item_type'] != 'folder') {
  618. $childItem['item_source'] = $child['source'];
  619. $childItem['item_target'] = $child['target'];
  620. }
  621. if ($backend instanceof \OCP\Share_Backend_File_Dependent) {
  622. if ($row['item_type'] == 'file' || $row['item_type'] == 'folder') {
  623. $childItem['file_source'] = $child['source'];
  624. } else { // TODO is this really needed if we already know that we use the file backend?
  625. $meta = \OC\Files\Filesystem::getFileInfo($child['file_path']);
  626. $childItem['file_source'] = $meta['fileid'];
  627. }
  628. $childItem['file_target'] =
  629. \OC\Files\Filesystem::normalizePath($child['file_path']);
  630. }
  631. if (isset($item)) {
  632. if ($childItem[$column] == $item) {
  633. $collectionItems[] = $childItem;
  634. }
  635. } else {
  636. $collectionItems[] = $childItem;
  637. }
  638. }
  639. }
  640. }
  641. // Remove collection item
  642. $toRemove = $row['id'];
  643. if (array_key_exists($toRemove, $switchedItems)) {
  644. $toRemove = $switchedItems[$toRemove];
  645. }
  646. unset($items[$toRemove]);
  647. } elseif ($includeCollections && $collectionTypes && in_array($row['item_type'], $collectionTypes)) {
  648. // FIXME: Thats a dirty hack to improve file sharing performance,
  649. // see github issue #10588 for more details
  650. // Need to find a solution which works for all back-ends
  651. $collectionBackend = self::getBackend($row['item_type']);
  652. $sharedParents = $collectionBackend->getParents($row['item_source']);
  653. foreach ($sharedParents as $parent) {
  654. $collectionItems[] = $parent;
  655. }
  656. }
  657. }
  658. if (!empty($collectionItems)) {
  659. $collectionItems = array_unique($collectionItems, SORT_REGULAR);
  660. $items = array_merge($items, $collectionItems);
  661. }
  662. // filter out invalid items, these can appear when subshare entries exist
  663. // for a group in which the requested user isn't a member any more
  664. $items = array_filter($items, function ($item) {
  665. return $item['share_type'] !== self::$shareTypeGroupUserUnique;
  666. });
  667. return self::formatResult($items, $column, $backend);
  668. } elseif ($includeCollections && $collectionTypes && in_array('folder', $collectionTypes)) {
  669. // FIXME: Thats a dirty hack to improve file sharing performance,
  670. // see github issue #10588 for more details
  671. // Need to find a solution which works for all back-ends
  672. $collectionItems = [];
  673. $collectionBackend = self::getBackend('folder');
  674. $sharedParents = $collectionBackend->getParents($item, $shareWith, $uidOwner);
  675. foreach ($sharedParents as $parent) {
  676. $collectionItems[] = $parent;
  677. }
  678. return self::formatResult($collectionItems, $column, $backend);
  679. }
  680. return [];
  681. }
  682. /**
  683. * group items with link to the same source
  684. *
  685. * @param array $items
  686. * @param string $itemType
  687. * @return array of grouped items
  688. */
  689. protected static function groupItems($items, $itemType) {
  690. $fileSharing = $itemType === 'file' || $itemType === 'folder';
  691. $result = [];
  692. foreach ($items as $item) {
  693. $grouped = false;
  694. foreach ($result as $key => $r) {
  695. // for file/folder shares we need to compare file_source, otherwise we compare item_source
  696. // only group shares if they already point to the same target, otherwise the file where shared
  697. // before grouping of shares was added. In this case we don't group them to avoid confusions
  698. if (($fileSharing && $item['file_source'] === $r['file_source'] && $item['file_target'] === $r['file_target']) ||
  699. (!$fileSharing && $item['item_source'] === $r['item_source'] && $item['item_target'] === $r['item_target'])) {
  700. // add the first item to the list of grouped shares
  701. if (!isset($result[$key]['grouped'])) {
  702. $result[$key]['grouped'][] = $result[$key];
  703. }
  704. $result[$key]['permissions'] = (int)$item['permissions'] | (int)$r['permissions'];
  705. $result[$key]['grouped'][] = $item;
  706. $grouped = true;
  707. break;
  708. }
  709. }
  710. if (!$grouped) {
  711. $result[] = $item;
  712. }
  713. }
  714. return $result;
  715. }
  716. /**
  717. * Construct select statement
  718. *
  719. * @param bool $fileDependent ist it a file/folder share or a general share
  720. */
  721. private static function getSelectStatement(int $format, bool $fileDependent, ?string $uidOwner = null): IQueryBuilder {
  722. /** @var IDBConnection $connection */
  723. $connection = \OC::$server->get(IDBConnection::class);
  724. $qb = $connection->getQueryBuilder();
  725. if ($format == self::FORMAT_STATUSES) {
  726. if ($fileDependent) {
  727. return $qb->select(
  728. 's.id',
  729. 's.parent',
  730. 'share_type',
  731. 'path',
  732. 'storage',
  733. 'share_with',
  734. 'uid_owner',
  735. 'file_source',
  736. 'stime',
  737. 's.permissions',
  738. 'uid_initiator'
  739. )->selectAlias('st.id', 'storage_id')
  740. ->selectAlias('f.parent', 'file_parent');
  741. }
  742. return $qb->select('id', 'parent', 'share_type', 'share_with', 'uid_owner', 'item_source', 'stime', 's.permissions');
  743. }
  744. if (isset($uidOwner)) {
  745. if ($fileDependent) {
  746. return $qb->select(
  747. 's.id',
  748. 'item_type',
  749. 'item_source',
  750. 's.parent',
  751. 'share_type',
  752. 'share_with',
  753. 'file_source',
  754. 'file_target',
  755. 'path',
  756. 's.permissions',
  757. 'stime',
  758. 'expiration',
  759. 'token',
  760. 'storage',
  761. 'mail_send',
  762. 'uid_owner',
  763. 'uid_initiator'
  764. )->selectAlias('st.id', 'storage_id')
  765. ->selectAlias('f.parent', 'file_parent');
  766. }
  767. return $qb->select('id', 'item_type', 'item_source', 'parent', 'share_type',
  768. 'share_with', 'uid_owner', 'file_source', 'stime', 's.permissions',
  769. 'expiration', 'token', 'mail_send');
  770. }
  771. if ($fileDependent) {
  772. if ($format == File::FORMAT_GET_FOLDER_CONTENTS || $format == File::FORMAT_FILE_APP_ROOT) {
  773. return $qb->select(
  774. 's.id',
  775. 'item_type',
  776. 'item_source',
  777. 's.parent',
  778. 'uid_owner',
  779. 'share_type',
  780. 'share_with',
  781. 'file_source',
  782. 'path',
  783. 'file_target',
  784. 's.permissions',
  785. 'stime',
  786. 'expiration',
  787. 'storage',
  788. 'name',
  789. 'mtime',
  790. 'mimepart',
  791. 'size',
  792. 'encrypted',
  793. 'etag',
  794. 'mail_send'
  795. )->selectAlias('f.parent', 'file_parent');
  796. }
  797. return $qb->select(
  798. 's.id',
  799. 'item_type',
  800. 'item_source',
  801. 'item_target',
  802. 's.parent',
  803. 'share_type',
  804. 'share_with',
  805. 'uid_owner',
  806. 'file_source',
  807. 'path',
  808. 'file_target',
  809. 's.permissions',
  810. 'stime',
  811. 'expiration',
  812. 'token',
  813. 'storage',
  814. 'mail_send',
  815. )->selectAlias('f.parent', 'file_parent')
  816. ->selectAlias('st.id', 'storage_id');
  817. }
  818. return $qb->select('*');
  819. }
  820. /**
  821. * transform db results
  822. *
  823. * @param array $row result
  824. */
  825. private static function transformDBResults(&$row) {
  826. if (isset($row['id'])) {
  827. $row['id'] = (int)$row['id'];
  828. }
  829. if (isset($row['share_type'])) {
  830. $row['share_type'] = (int)$row['share_type'];
  831. }
  832. if (isset($row['parent'])) {
  833. $row['parent'] = (int)$row['parent'];
  834. }
  835. if (isset($row['file_parent'])) {
  836. $row['file_parent'] = (int)$row['file_parent'];
  837. }
  838. if (isset($row['file_source'])) {
  839. $row['file_source'] = (int)$row['file_source'];
  840. }
  841. if (isset($row['permissions'])) {
  842. $row['permissions'] = (int)$row['permissions'];
  843. }
  844. if (isset($row['storage'])) {
  845. $row['storage'] = (int)$row['storage'];
  846. }
  847. if (isset($row['stime'])) {
  848. $row['stime'] = (int)$row['stime'];
  849. }
  850. if (isset($row['expiration']) && $row['share_type'] !== IShare::TYPE_LINK) {
  851. // discard expiration date for non-link shares, which might have been
  852. // set by ancient bugs
  853. $row['expiration'] = null;
  854. }
  855. }
  856. /**
  857. * format result
  858. *
  859. * @param array $items result
  860. * @param string $column is it a file share or a general share ('file_target' or 'item_target')
  861. * @param \OCP\Share_Backend $backend sharing backend
  862. * @param int $format
  863. * @param array $parameters additional format parameters
  864. * @return array format result
  865. */
  866. private static function formatResult($items, $column, $backend, $format = self::FORMAT_NONE, $parameters = null) {
  867. if ($format === self::FORMAT_NONE) {
  868. return $items;
  869. } elseif ($format === self::FORMAT_STATUSES) {
  870. $statuses = [];
  871. foreach ($items as $item) {
  872. if ($item['share_type'] === IShare::TYPE_LINK) {
  873. if ($item['uid_initiator'] !== \OC::$server->getUserSession()->getUser()->getUID()) {
  874. continue;
  875. }
  876. $statuses[$item[$column]]['link'] = true;
  877. } elseif (!isset($statuses[$item[$column]])) {
  878. $statuses[$item[$column]]['link'] = false;
  879. }
  880. if (!empty($item['file_target'])) {
  881. $statuses[$item[$column]]['path'] = $item['path'];
  882. }
  883. }
  884. return $statuses;
  885. } else {
  886. return $backend->formatItems($items, $format, $parameters);
  887. }
  888. }
  889. /**
  890. * remove protocol from URL
  891. *
  892. * @param string $url
  893. * @return string
  894. */
  895. public static function removeProtocolFromUrl($url) {
  896. if (strpos($url, 'https://') === 0) {
  897. return substr($url, strlen('https://'));
  898. } elseif (strpos($url, 'http://') === 0) {
  899. return substr($url, strlen('http://'));
  900. }
  901. return $url;
  902. }
  903. /**
  904. * @return int
  905. */
  906. public static function getExpireInterval() {
  907. return (int)\OC::$server->getConfig()->getAppValue('core', 'shareapi_expire_after_n_days', '7');
  908. }
  909. /**
  910. * Checks whether the given path is reachable for the given owner
  911. *
  912. * @param string $path path relative to files
  913. * @param string $ownerStorageId storage id of the owner
  914. *
  915. * @return boolean true if file is reachable, false otherwise
  916. */
  917. private static function isFileReachable($path, $ownerStorageId) {
  918. // if outside the home storage, file is always considered reachable
  919. if (!(substr($ownerStorageId, 0, 6) === 'home::' ||
  920. substr($ownerStorageId, 0, 13) === 'object::user:'
  921. )) {
  922. return true;
  923. }
  924. // if inside the home storage, the file has to be under "/files/"
  925. $path = ltrim($path, '/');
  926. if (substr($path, 0, 6) === 'files/') {
  927. return true;
  928. }
  929. return false;
  930. }
  931. }