FilesReportPlugin.php 10 KB

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