Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

SearchBuilder.php 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Robin Appelman <robin@icewind.nl>
  7. * @author Roeland Jago Douma <roeland@famdouma.nl>
  8. * @author Tobias Kaminsky <tobias@kaminsky.me>
  9. *
  10. * @license GNU AGPL version 3 or any later version
  11. *
  12. * This program is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License as
  14. * published by the Free Software Foundation, either version 3 of the
  15. * License, or (at your option) any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU Affero General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public License
  23. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. *
  25. */
  26. namespace OC\Files\Cache;
  27. use OCP\DB\QueryBuilder\IQueryBuilder;
  28. use OCP\Files\IMimeTypeLoader;
  29. use OCP\Files\Search\ISearchBinaryOperator;
  30. use OCP\Files\Search\ISearchComparison;
  31. use OCP\Files\Search\ISearchOperator;
  32. use OCP\Files\Search\ISearchOrder;
  33. /**
  34. * Tools for transforming search queries into database queries
  35. */
  36. class SearchBuilder {
  37. protected static $searchOperatorMap = [
  38. ISearchComparison::COMPARE_LIKE => 'iLike',
  39. ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'like',
  40. ISearchComparison::COMPARE_EQUAL => 'eq',
  41. ISearchComparison::COMPARE_GREATER_THAN => 'gt',
  42. ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte',
  43. ISearchComparison::COMPARE_LESS_THAN => 'lt',
  44. ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte',
  45. ];
  46. protected static $searchOperatorNegativeMap = [
  47. ISearchComparison::COMPARE_LIKE => 'notLike',
  48. ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'notLike',
  49. ISearchComparison::COMPARE_EQUAL => 'neq',
  50. ISearchComparison::COMPARE_GREATER_THAN => 'lte',
  51. ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt',
  52. ISearchComparison::COMPARE_LESS_THAN => 'gte',
  53. ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt',
  54. ];
  55. public const TAG_FAVORITE = '_$!<Favorite>!$_';
  56. /** @var IMimeTypeLoader */
  57. private $mimetypeLoader;
  58. public function __construct(
  59. IMimeTypeLoader $mimetypeLoader
  60. ) {
  61. $this->mimetypeLoader = $mimetypeLoader;
  62. }
  63. /**
  64. * Whether or not the tag tables should be joined to complete the search
  65. *
  66. * @param ISearchOperator $operator
  67. * @return boolean
  68. */
  69. public function shouldJoinTags(ISearchOperator $operator) {
  70. if ($operator instanceof ISearchBinaryOperator) {
  71. return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) {
  72. return $shouldJoin || $this->shouldJoinTags($operator);
  73. }, false);
  74. } elseif ($operator instanceof ISearchComparison) {
  75. return $operator->getField() === 'tagname' || $operator->getField() === 'favorite' || $operator->getField() === 'systemtag';
  76. }
  77. return false;
  78. }
  79. /**
  80. * @param IQueryBuilder $builder
  81. * @param ISearchOperator[] $operators
  82. */
  83. public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) {
  84. return array_filter(array_map(function ($operator) use ($builder) {
  85. return $this->searchOperatorToDBExpr($builder, $operator);
  86. }, $operators));
  87. }
  88. public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) {
  89. $expr = $builder->expr();
  90. if ($operator instanceof ISearchBinaryOperator) {
  91. if (count($operator->getArguments()) === 0) {
  92. return null;
  93. }
  94. switch ($operator->getType()) {
  95. case ISearchBinaryOperator::OPERATOR_NOT:
  96. $negativeOperator = $operator->getArguments()[0];
  97. if ($negativeOperator instanceof ISearchComparison) {
  98. return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap);
  99. } else {
  100. throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
  101. }
  102. // no break
  103. case ISearchBinaryOperator::OPERATOR_AND:
  104. return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
  105. case ISearchBinaryOperator::OPERATOR_OR:
  106. return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
  107. default:
  108. throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
  109. }
  110. } elseif ($operator instanceof ISearchComparison) {
  111. return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap);
  112. } else {
  113. throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
  114. }
  115. }
  116. private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
  117. $this->validateComparison($comparison);
  118. [$field, $value, $type] = $this->getOperatorFieldAndValue($comparison);
  119. if (isset($operatorMap[$type])) {
  120. $queryOperator = $operatorMap[$type];
  121. return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value));
  122. } else {
  123. throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
  124. }
  125. }
  126. private function getOperatorFieldAndValue(ISearchComparison $operator) {
  127. $field = $operator->getField();
  128. $value = $operator->getValue();
  129. $type = $operator->getType();
  130. if ($field === 'mimetype') {
  131. $value = (string)$value;
  132. if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
  133. $value = (int)$this->mimetypeLoader->getId($value);
  134. } elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
  135. // transform "mimetype='foo/%'" to "mimepart='foo'"
  136. if (preg_match('|(.+)/%|', $value, $matches)) {
  137. $field = 'mimepart';
  138. $value = (int)$this->mimetypeLoader->getId($matches[1]);
  139. $type = ISearchComparison::COMPARE_EQUAL;
  140. } elseif (strpos($value, '%') !== false) {
  141. throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
  142. } else {
  143. $field = 'mimetype';
  144. $value = (int)$this->mimetypeLoader->getId($value);
  145. $type = ISearchComparison::COMPARE_EQUAL;
  146. }
  147. }
  148. } elseif ($field === 'favorite') {
  149. $field = 'tag.category';
  150. $value = self::TAG_FAVORITE;
  151. } elseif ($field === 'name') {
  152. $field = 'file.name';
  153. } elseif ($field === 'tagname') {
  154. $field = 'tag.category';
  155. } elseif ($field === 'systemtag') {
  156. $field = 'systemtag.name';
  157. } elseif ($field === 'fileid') {
  158. $field = 'file.fileid';
  159. } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) {
  160. $field = 'path_hash';
  161. $value = md5((string)$value);
  162. }
  163. return [$field, $value, $type];
  164. }
  165. private function validateComparison(ISearchComparison $operator) {
  166. $types = [
  167. 'mimetype' => 'string',
  168. 'mtime' => 'integer',
  169. 'name' => 'string',
  170. 'path' => 'string',
  171. 'size' => 'integer',
  172. 'tagname' => 'string',
  173. 'systemtag' => 'string',
  174. 'favorite' => 'boolean',
  175. 'fileid' => 'integer',
  176. 'storage' => 'integer',
  177. ];
  178. $comparisons = [
  179. 'mimetype' => ['eq', 'like'],
  180. 'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
  181. 'name' => ['eq', 'like', 'clike'],
  182. 'path' => ['eq', 'like', 'clike'],
  183. 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'],
  184. 'tagname' => ['eq', 'like'],
  185. 'systemtag' => ['eq', 'like'],
  186. 'favorite' => ['eq'],
  187. 'fileid' => ['eq'],
  188. 'storage' => ['eq'],
  189. ];
  190. if (!isset($types[$operator->getField()])) {
  191. throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
  192. }
  193. $type = $types[$operator->getField()];
  194. if (gettype($operator->getValue()) !== $type) {
  195. throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
  196. }
  197. if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
  198. throw new \InvalidArgumentException('Unsupported comparison for field ' . $operator->getField() . ': ' . $operator->getType());
  199. }
  200. }
  201. private function getParameterForValue(IQueryBuilder $builder, $value) {
  202. if ($value instanceof \DateTime) {
  203. $value = $value->getTimestamp();
  204. }
  205. if (is_numeric($value)) {
  206. $type = IQueryBuilder::PARAM_INT;
  207. } else {
  208. $type = IQueryBuilder::PARAM_STR;
  209. }
  210. return $builder->createNamedParameter($value, $type);
  211. }
  212. /**
  213. * @param IQueryBuilder $query
  214. * @param ISearchOrder[] $orders
  215. */
  216. public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) {
  217. foreach ($orders as $order) {
  218. $field = $order->getField();
  219. if ($field === 'fileid') {
  220. $field = 'file.fileid';
  221. }
  222. // Mysql really likes to pick an index for sorting if it can't fully satisfy the where
  223. // filter with an index, since search queries pretty much never are fully filtered by index
  224. // mysql often picks an index for sorting instead of the much more useful index for filtering.
  225. //
  226. // By changing the order by to an expression, mysql isn't smart enough to see that it could still
  227. // use the index, so it instead picks an index for the filtering
  228. if ($field === 'mtime') {
  229. $field = $query->func()->add($field, $query->createNamedParameter(0));
  230. }
  231. $query->addOrderBy($field, $order->getDirection());
  232. }
  233. }
  234. }