FileSearchBackend.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
  4. *
  5. * @author Robin Appelman <robin@icewind.nl>
  6. *
  7. * @license GNU AGPL version 3 or any later version
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  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
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. namespace OCA\DAV\Files;
  24. use OC\Files\Search\SearchBinaryOperator;
  25. use OC\Files\Search\SearchComparison;
  26. use OC\Files\Search\SearchOrder;
  27. use OC\Files\Search\SearchQuery;
  28. use OC\Files\View;
  29. use OCA\DAV\Connector\Sabre\CachingTree;
  30. use OCA\DAV\Connector\Sabre\Directory;
  31. use OCA\DAV\Connector\Sabre\FilesPlugin;
  32. use OCA\DAV\Connector\Sabre\TagsPlugin;
  33. use OCP\Files\Cache\ICacheEntry;
  34. use OCP\Files\Folder;
  35. use OCP\Files\IRootFolder;
  36. use OCP\Files\Node;
  37. use OCP\Files\Search\ISearchOperator;
  38. use OCP\Files\Search\ISearchOrder;
  39. use OCP\Files\Search\ISearchQuery;
  40. use OCP\IUser;
  41. use OCP\Share\IManager;
  42. use Sabre\DAV\Exception\NotFound;
  43. use SearchDAV\Backend\ISearchBackend;
  44. use SearchDAV\Backend\SearchPropertyDefinition;
  45. use SearchDAV\Backend\SearchResult;
  46. use SearchDAV\Query\Query;
  47. use SearchDAV\Query\Literal;
  48. use SearchDAV\Query\Operator;
  49. use SearchDAV\Query\Order;
  50. class FileSearchBackend implements ISearchBackend {
  51. /** @var CachingTree */
  52. private $tree;
  53. /** @var IUser */
  54. private $user;
  55. /** @var IRootFolder */
  56. private $rootFolder;
  57. /** @var IManager */
  58. private $shareManager;
  59. /** @var View */
  60. private $view;
  61. /**
  62. * FileSearchBackend constructor.
  63. *
  64. * @param CachingTree $tree
  65. * @param IUser $user
  66. * @param IRootFolder $rootFolder
  67. * @param IManager $shareManager
  68. * @param View $view
  69. * @internal param IRootFolder $rootFolder
  70. */
  71. public function __construct(CachingTree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) {
  72. $this->tree = $tree;
  73. $this->user = $user;
  74. $this->rootFolder = $rootFolder;
  75. $this->shareManager = $shareManager;
  76. $this->view = $view;
  77. }
  78. /**
  79. * Search endpoint will be remote.php/dav
  80. *
  81. * @return string
  82. */
  83. public function getArbiterPath() {
  84. return '';
  85. }
  86. public function isValidScope($href, $depth, $path) {
  87. // only allow scopes inside the dav server
  88. if (is_null($path)) {
  89. return false;
  90. }
  91. try {
  92. $node = $this->tree->getNodeForPath($path);
  93. return $node instanceof Directory;
  94. } catch (NotFound $e) {
  95. return false;
  96. }
  97. }
  98. public function getPropertyDefinitionsForScope($href, $path) {
  99. // all valid scopes support the same schema
  100. //todo dynamically load all propfind properties that are supported
  101. return [
  102. // queryable properties
  103. new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
  104. new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
  105. new SearchPropertyDefinition('{DAV:}getlastmodified', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
  106. new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
  107. new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
  108. new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
  109. // select only properties
  110. new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false),
  111. new SearchPropertyDefinition('{DAV:}getcontentlength', false, true, false),
  112. new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, false, true, false),
  113. new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, false, true, false),
  114. new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, false, true, false),
  115. new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, false, true, false),
  116. new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, false, true, false),
  117. new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, false, true, false),
  118. new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
  119. new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
  120. ];
  121. }
  122. /**
  123. * @param Query $search
  124. * @return SearchResult[]
  125. */
  126. public function search(Query $search) {
  127. if (count($search->from) !== 1) {
  128. throw new \InvalidArgumentException('Searching more than one folder is not supported');
  129. }
  130. $query = $this->transformQuery($search);
  131. $scope = $search->from[0];
  132. if ($scope->path === null) {
  133. throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
  134. }
  135. $node = $this->tree->getNodeForPath($scope->path);
  136. if (!$node instanceof Directory) {
  137. throw new \InvalidArgumentException('Search is only supported on directories');
  138. }
  139. $fileInfo = $node->getFileInfo();
  140. $folder = $this->rootFolder->get($fileInfo->getPath());
  141. /** @var Folder $folder $results */
  142. $results = $folder->search($query);
  143. /** @var SearchResult[] $nodes */
  144. $nodes = array_map(function (Node $node) {
  145. if ($node instanceof Folder) {
  146. $davNode = new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager);
  147. } else {
  148. $davNode = new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager);
  149. }
  150. $path = $this->getHrefForNode($node);
  151. $this->tree->cacheNode($davNode, $path);
  152. return new SearchResult($davNode, $path);
  153. }, $results);
  154. // Sort again, since the result from multiple storages is appended and not sorted
  155. usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
  156. return $this->sort($a, $b, $search->orderBy);
  157. });
  158. // If a limit is provided use only return that number of files
  159. if ($search->limit->maxResults !== 0) {
  160. $nodes = \array_slice($nodes, 0, $search->limit->maxResults);
  161. }
  162. return $nodes;
  163. }
  164. private function sort(SearchResult $a, SearchResult $b, array $orders) {
  165. /** @var Order $order */
  166. foreach ($orders as $order) {
  167. $v1 = $this->getSearchResultProperty($a, $order->property);
  168. $v2 = $this->getSearchResultProperty($b, $order->property);
  169. if ($v1 === null && $v2 === null) {
  170. continue;
  171. }
  172. if ($v1 === null) {
  173. return $order->order === Order::ASC ? 1 : -1;
  174. }
  175. if ($v2 === null) {
  176. return $order->order === Order::ASC ? -1 : 1;
  177. }
  178. $s = $this->compareProperties($v1, $v2, $order);
  179. if ($s === 0) {
  180. continue;
  181. }
  182. if ($order->order === Order::DESC) {
  183. $s = -$s;
  184. }
  185. return $s;
  186. }
  187. return 0;
  188. }
  189. private function compareProperties($a, $b, Order $order) {
  190. switch ($order->property->dataType) {
  191. case SearchPropertyDefinition::DATATYPE_STRING:
  192. return strcmp($a, $b);
  193. case SearchPropertyDefinition::DATATYPE_BOOLEAN:
  194. if ($a === $b) {
  195. return 0;
  196. }
  197. if ($a === false) {
  198. return -1;
  199. }
  200. return 1;
  201. default:
  202. if ($a === $b) {
  203. return 0;
  204. }
  205. if ($a < $b) {
  206. return -1;
  207. }
  208. return 1;
  209. }
  210. }
  211. private function getSearchResultProperty(SearchResult $result, SearchPropertyDefinition $property) {
  212. /** @var \OCA\DAV\Connector\Sabre\Node $node */
  213. $node = $result->node;
  214. switch ($property->name) {
  215. case '{DAV:}displayname':
  216. return $node->getName();
  217. case '{DAV:}getlastmodified':
  218. return $node->getLastModified();
  219. case FilesPlugin::SIZE_PROPERTYNAME:
  220. return $node->getSize();
  221. case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
  222. return $node->getInternalFileId();
  223. default:
  224. return null;
  225. }
  226. }
  227. /**
  228. * @param Node $node
  229. * @return string
  230. */
  231. private function getHrefForNode(Node $node) {
  232. $base = '/files/' . $this->user->getUID();
  233. return $base . $this->view->getRelativePath($node->getPath());
  234. }
  235. /**
  236. * @param Query $query
  237. * @return ISearchQuery
  238. */
  239. private function transformQuery(Query $query) {
  240. // TODO offset
  241. $limit = $query->limit;
  242. $orders = array_map([$this, 'mapSearchOrder'], $query->orderBy);
  243. return new SearchQuery($this->transformSearchOperation($query->where), (int)$limit->maxResults, 0, $orders, $this->user);
  244. }
  245. /**
  246. * @param Order $order
  247. * @return ISearchOrder
  248. */
  249. private function mapSearchOrder(Order $order) {
  250. return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property));
  251. }
  252. /**
  253. * @param Operator $operator
  254. * @return ISearchOperator
  255. */
  256. private function transformSearchOperation(Operator $operator) {
  257. list(, $trimmedType) = explode('}', $operator->type);
  258. switch ($operator->type) {
  259. case Operator::OPERATION_AND:
  260. case Operator::OPERATION_OR:
  261. case Operator::OPERATION_NOT:
  262. $arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
  263. return new SearchBinaryOperator($trimmedType, $arguments);
  264. case Operator::OPERATION_EQUAL:
  265. case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
  266. case Operator::OPERATION_GREATER_THAN:
  267. case Operator::OPERATION_LESS_OR_EQUAL_THAN:
  268. case Operator::OPERATION_LESS_THAN:
  269. case Operator::OPERATION_IS_LIKE:
  270. if (count($operator->arguments) !== 2) {
  271. throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
  272. }
  273. if (!($operator->arguments[0] instanceof SearchPropertyDefinition)) {
  274. throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property');
  275. }
  276. if (!($operator->arguments[1] instanceof Literal)) {
  277. throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal');
  278. }
  279. return new SearchComparison($trimmedType, $this->mapPropertyNameToColumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value));
  280. case Operator::OPERATION_IS_COLLECTION:
  281. return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
  282. default:
  283. throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType . ' (' . $operator->type . ')');
  284. }
  285. }
  286. /**
  287. * @param SearchPropertyDefinition $property
  288. * @return string
  289. */
  290. private function mapPropertyNameToColumn(SearchPropertyDefinition $property) {
  291. switch ($property->name) {
  292. case '{DAV:}displayname':
  293. return 'name';
  294. case '{DAV:}getcontenttype':
  295. return 'mimetype';
  296. case '{DAV:}getlastmodified':
  297. return 'mtime';
  298. case FilesPlugin::SIZE_PROPERTYNAME:
  299. return 'size';
  300. case TagsPlugin::FAVORITE_PROPERTYNAME:
  301. return 'favorite';
  302. case TagsPlugin::TAGS_PROPERTYNAME:
  303. return 'tagname';
  304. case FilesPlugin::INTERNAL_FILEID_PROPERTYNAME:
  305. return 'fileid';
  306. default:
  307. throw new \InvalidArgumentException('Unsupported property for search or order: ' . $property->name);
  308. }
  309. }
  310. private function castValue(SearchPropertyDefinition $property, $value) {
  311. switch ($property->dataType) {
  312. case SearchPropertyDefinition::DATATYPE_BOOLEAN:
  313. return $value === 'yes';
  314. case SearchPropertyDefinition::DATATYPE_DECIMAL:
  315. case SearchPropertyDefinition::DATATYPE_INTEGER:
  316. case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
  317. return 0 + $value;
  318. case SearchPropertyDefinition::DATATYPE_DATETIME:
  319. if (is_numeric($value)) {
  320. return max(0, 0 + $value);
  321. }
  322. $date = \DateTime::createFromFormat(\DateTime::ATOM, $value);
  323. return ($date instanceof \DateTime && $date->getTimestamp() !== false) ? $date->getTimestamp() : 0;
  324. default:
  325. return $value;
  326. }
  327. }
  328. }