FilesReportPlugin.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Joas Schilling <coding@schilljs.com>
  7. * @author Morris Jobke <hey@morrisjobke.de>
  8. * @author Roeland Jago Douma <roeland@famdouma.nl>
  9. * @author Thomas Müller <thomas.mueller@tmit.eu>
  10. * @author Vincent Petry <vincent@nextcloud.com>
  11. * @author Vinicius Cubas Brand <vinicius@eita.org.br>
  12. *
  13. * @license AGPL-3.0
  14. *
  15. * This code is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU Affero General Public License, version 3,
  17. * as published by the Free Software Foundation.
  18. *
  19. * This program is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU Affero General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU Affero General Public License, version 3,
  25. * along with this program. If not, see <http://www.gnu.org/licenses/>
  26. *
  27. */
  28. namespace OCA\DAV\Connector\Sabre;
  29. use OC\Files\View;
  30. use OCP\App\IAppManager;
  31. use OCP\Files\Folder;
  32. use OCP\IGroupManager;
  33. use OCP\ITagManager;
  34. use OCP\IUserSession;
  35. use OCP\SystemTag\ISystemTagManager;
  36. use OCP\SystemTag\ISystemTagObjectMapper;
  37. use OCP\SystemTag\TagNotFoundException;
  38. use Sabre\DAV\Exception\BadRequest;
  39. use Sabre\DAV\Exception\PreconditionFailed;
  40. use Sabre\DAV\PropFind;
  41. use Sabre\DAV\ServerPlugin;
  42. use Sabre\DAV\Tree;
  43. use Sabre\DAV\Xml\Element\Response;
  44. use Sabre\DAV\Xml\Response\MultiStatus;
  45. class FilesReportPlugin extends ServerPlugin {
  46. // namespace
  47. public const NS_OWNCLOUD = 'http://owncloud.org/ns';
  48. public const REPORT_NAME = '{http://owncloud.org/ns}filter-files';
  49. public const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag';
  50. public const CIRCLE_PROPERTYNAME = '{http://owncloud.org/ns}circle';
  51. /**
  52. * Reference to main server object
  53. *
  54. * @var \Sabre\DAV\Server
  55. */
  56. private $server;
  57. /**
  58. * @var Tree
  59. */
  60. private $tree;
  61. /**
  62. * @var View
  63. */
  64. private $fileView;
  65. /**
  66. * @var ISystemTagManager
  67. */
  68. private $tagManager;
  69. /**
  70. * @var ISystemTagObjectMapper
  71. */
  72. private $tagMapper;
  73. /**
  74. * Manager for private tags
  75. *
  76. * @var ITagManager
  77. */
  78. private $fileTagger;
  79. /**
  80. * @var IUserSession
  81. */
  82. private $userSession;
  83. /**
  84. * @var IGroupManager
  85. */
  86. private $groupManager;
  87. /**
  88. * @var Folder
  89. */
  90. private $userFolder;
  91. /**
  92. * @var IAppManager
  93. */
  94. private $appManager;
  95. /**
  96. * @param Tree $tree
  97. * @param View $view
  98. * @param ISystemTagManager $tagManager
  99. * @param ISystemTagObjectMapper $tagMapper
  100. * @param ITagManager $fileTagger manager for private tags
  101. * @param IUserSession $userSession
  102. * @param IGroupManager $groupManager
  103. * @param Folder $userFolder
  104. * @param IAppManager $appManager
  105. */
  106. public function __construct(Tree $tree,
  107. View $view,
  108. ISystemTagManager $tagManager,
  109. ISystemTagObjectMapper $tagMapper,
  110. ITagManager $fileTagger,
  111. IUserSession $userSession,
  112. IGroupManager $groupManager,
  113. Folder $userFolder,
  114. IAppManager $appManager
  115. ) {
  116. $this->tree = $tree;
  117. $this->fileView = $view;
  118. $this->tagManager = $tagManager;
  119. $this->tagMapper = $tagMapper;
  120. $this->fileTagger = $fileTagger;
  121. $this->userSession = $userSession;
  122. $this->groupManager = $groupManager;
  123. $this->userFolder = $userFolder;
  124. $this->appManager = $appManager;
  125. }
  126. /**
  127. * This initializes the plugin.
  128. *
  129. * This function is called by \Sabre\DAV\Server, after
  130. * addPlugin is called.
  131. *
  132. * This method should set up the required event subscriptions.
  133. *
  134. * @param \Sabre\DAV\Server $server
  135. * @return void
  136. */
  137. public function initialize(\Sabre\DAV\Server $server) {
  138. $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
  139. $this->server = $server;
  140. $this->server->on('report', [$this, 'onReport']);
  141. }
  142. /**
  143. * Returns a list of reports this plugin supports.
  144. *
  145. * This will be used in the {DAV:}supported-report-set property.
  146. *
  147. * @param string $uri
  148. * @return array
  149. */
  150. public function getSupportedReportSet($uri) {
  151. return [self::REPORT_NAME];
  152. }
  153. /**
  154. * REPORT operations to look for files
  155. *
  156. * @param string $reportName
  157. * @param $report
  158. * @param string $uri
  159. * @return bool
  160. * @throws BadRequest
  161. * @throws PreconditionFailed
  162. * @internal param $ [] $report
  163. */
  164. public function onReport($reportName, $report, $uri) {
  165. $reportTargetNode = $this->server->tree->getNodeForPath($uri);
  166. if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) {
  167. return;
  168. }
  169. $ns = '{' . $this::NS_OWNCLOUD . '}';
  170. $requestedProps = [];
  171. $filterRules = [];
  172. // parse report properties and gather filter info
  173. foreach ($report as $reportProps) {
  174. $name = $reportProps['name'];
  175. if ($name === $ns . 'filter-rules') {
  176. $filterRules = $reportProps['value'];
  177. } elseif ($name === '{DAV:}prop') {
  178. // propfind properties
  179. foreach ($reportProps['value'] as $propVal) {
  180. $requestedProps[] = $propVal['name'];
  181. }
  182. }
  183. }
  184. if (empty($filterRules)) {
  185. // an empty filter would return all existing files which would be slow
  186. throw new BadRequest('Missing filter-rule block in request');
  187. }
  188. // gather all file ids matching filter
  189. try {
  190. $resultFileIds = $this->processFilterRules($filterRules);
  191. } catch (TagNotFoundException $e) {
  192. throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
  193. }
  194. // find sabre nodes by file id, restricted to the root node path
  195. $results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
  196. $filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
  197. $responses = $this->prepareResponses($filesUri, $requestedProps, $results);
  198. $xml = $this->server->xml->write(
  199. '{DAV:}multistatus',
  200. new MultiStatus($responses)
  201. );
  202. $this->server->httpResponse->setStatus(207);
  203. $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
  204. $this->server->httpResponse->setBody($xml);
  205. return false;
  206. }
  207. /**
  208. * Returns the base uri of the files root by removing
  209. * the subpath from the URI
  210. *
  211. * @param string $uri URI from this request
  212. * @param string $subPath subpath to remove from the URI
  213. *
  214. * @return string files base uri
  215. */
  216. private function getFilesBaseUri(string $uri, string $subPath): string {
  217. $uri = trim($uri, '/');
  218. $subPath = trim($subPath, '/');
  219. if (empty($subPath)) {
  220. $filesUri = $uri;
  221. } else {
  222. $filesUri = substr($uri, 0, strlen($uri) - strlen($subPath));
  223. }
  224. $filesUri = trim($filesUri, '/');
  225. if (empty($filesUri)) {
  226. return '';
  227. }
  228. return '/' . $filesUri;
  229. }
  230. /**
  231. * Find file ids matching the given filter rules
  232. *
  233. * @param array $filterRules
  234. * @return array array of unique file id results
  235. *
  236. * @throws TagNotFoundException whenever a tag was not found
  237. */
  238. protected function processFilterRules($filterRules) {
  239. $ns = '{' . $this::NS_OWNCLOUD . '}';
  240. $resultFileIds = null;
  241. $systemTagIds = [];
  242. $circlesIds = [];
  243. $favoriteFilter = null;
  244. foreach ($filterRules as $filterRule) {
  245. if ($filterRule['name'] === $ns . 'systemtag') {
  246. $systemTagIds[] = $filterRule['value'];
  247. }
  248. if ($filterRule['name'] === self::CIRCLE_PROPERTYNAME) {
  249. $circlesIds[] = $filterRule['value'];
  250. }
  251. if ($filterRule['name'] === $ns . 'favorite') {
  252. $favoriteFilter = true;
  253. }
  254. }
  255. if ($favoriteFilter !== null) {
  256. $resultFileIds = $this->fileTagger->load('files')->getFavorites();
  257. if (empty($resultFileIds)) {
  258. return [];
  259. }
  260. }
  261. if (!empty($systemTagIds)) {
  262. $fileIds = $this->getSystemTagFileIds($systemTagIds);
  263. if (empty($resultFileIds)) {
  264. $resultFileIds = $fileIds;
  265. } else {
  266. $resultFileIds = array_intersect($fileIds, $resultFileIds);
  267. }
  268. }
  269. if (!empty($circlesIds)) {
  270. $fileIds = $this->getCirclesFileIds($circlesIds);
  271. if (empty($resultFileIds)) {
  272. $resultFileIds = $fileIds;
  273. } else {
  274. $resultFileIds = array_intersect($fileIds, $resultFileIds);
  275. }
  276. }
  277. return $resultFileIds;
  278. }
  279. private function getSystemTagFileIds($systemTagIds) {
  280. $resultFileIds = null;
  281. // check user permissions, if applicable
  282. if (!$this->isAdmin()) {
  283. // check visibility/permission
  284. $tags = $this->tagManager->getTagsByIds($systemTagIds);
  285. $unknownTagIds = [];
  286. foreach ($tags as $tag) {
  287. if (!$tag->isUserVisible()) {
  288. $unknownTagIds[] = $tag->getId();
  289. }
  290. }
  291. if (!empty($unknownTagIds)) {
  292. throw new TagNotFoundException('Tag with ids ' . implode(', ', $unknownTagIds) . ' not found');
  293. }
  294. }
  295. // fetch all file ids and intersect them
  296. foreach ($systemTagIds as $systemTagId) {
  297. $fileIds = $this->tagMapper->getObjectIdsForTags($systemTagId, 'files');
  298. if (empty($fileIds)) {
  299. // This tag has no files, nothing can ever show up
  300. return [];
  301. }
  302. // first run ?
  303. if ($resultFileIds === null) {
  304. $resultFileIds = $fileIds;
  305. } else {
  306. $resultFileIds = array_intersect($resultFileIds, $fileIds);
  307. }
  308. if (empty($resultFileIds)) {
  309. // Empty intersection, nothing can show up anymore
  310. return [];
  311. }
  312. }
  313. return $resultFileIds;
  314. }
  315. /**
  316. * @suppress PhanUndeclaredClassMethod
  317. * @param array $circlesIds
  318. * @return array
  319. */
  320. private function getCirclesFileIds(array $circlesIds) {
  321. if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
  322. return [];
  323. }
  324. return \OCA\Circles\Api\v1\Circles::getFilesForCircles($circlesIds);
  325. }
  326. /**
  327. * Prepare propfind response for the given nodes
  328. *
  329. * @param string $filesUri $filesUri URI leading to root of the files URI,
  330. * with a leading slash but no trailing slash
  331. * @param string[] $requestedProps requested properties
  332. * @param Node[] nodes nodes for which to fetch and prepare responses
  333. * @return Response[]
  334. */
  335. public function prepareResponses($filesUri, $requestedProps, $nodes) {
  336. $responses = [];
  337. foreach ($nodes as $node) {
  338. $propFind = new PropFind($filesUri . $node->getPath(), $requestedProps);
  339. $this->server->getPropertiesByNode($propFind, $node);
  340. // copied from Sabre Server's getPropertiesForPath
  341. $result = $propFind->getResultForMultiStatus();
  342. $result['href'] = $propFind->getPath();
  343. $resourceType = $this->server->getResourceTypeForNode($node);
  344. if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
  345. $result['href'] .= '/';
  346. }
  347. $responses[] = new Response(
  348. rtrim($this->server->getBaseUri(), '/') . $filesUri . $node->getPath(),
  349. $result,
  350. 200
  351. );
  352. }
  353. return $responses;
  354. }
  355. /**
  356. * Find Sabre nodes by file ids
  357. *
  358. * @param Node $rootNode root node for search
  359. * @param array $fileIds file ids
  360. * @return Node[] array of Sabre nodes
  361. */
  362. public function findNodesByFileIds($rootNode, $fileIds) {
  363. $folder = $this->userFolder;
  364. if (trim($rootNode->getPath(), '/') !== '') {
  365. $folder = $folder->get($rootNode->getPath());
  366. }
  367. $results = [];
  368. foreach ($fileIds as $fileId) {
  369. $entry = $folder->getById($fileId);
  370. if ($entry) {
  371. $entry = current($entry);
  372. if ($entry instanceof \OCP\Files\File) {
  373. $results[] = new File($this->fileView, $entry);
  374. } elseif ($entry instanceof \OCP\Files\Folder) {
  375. $results[] = new Directory($this->fileView, $entry);
  376. }
  377. }
  378. }
  379. return $results;
  380. }
  381. /**
  382. * Returns whether the currently logged in user is an administrator
  383. */
  384. private function isAdmin() {
  385. $user = $this->userSession->getUser();
  386. if ($user !== null) {
  387. return $this->groupManager->isAdmin($user->getUID());
  388. }
  389. return false;
  390. }
  391. }