1
0

QuerySearchHelper.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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 OC\Files\Cache;
  24. use OCP\DB\QueryBuilder\IQueryBuilder;
  25. use OCP\Files\IMimeTypeLoader;
  26. use OCP\Files\Search\ISearchBinaryOperator;
  27. use OCP\Files\Search\ISearchComparison;
  28. use OCP\Files\Search\ISearchOperator;
  29. use OCP\Files\Search\ISearchOrder;
  30. /**
  31. * Tools for transforming search queries into database queries
  32. */
  33. class QuerySearchHelper {
  34. static protected $searchOperatorMap = [
  35. ISearchComparison::COMPARE_LIKE => 'iLike',
  36. ISearchComparison::COMPARE_EQUAL => 'eq',
  37. ISearchComparison::COMPARE_GREATER_THAN => 'gt',
  38. ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte',
  39. ISearchComparison::COMPARE_LESS_THAN => 'lt',
  40. ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte'
  41. ];
  42. static protected $searchOperatorNegativeMap = [
  43. ISearchComparison::COMPARE_LIKE => 'notLike',
  44. ISearchComparison::COMPARE_EQUAL => 'neq',
  45. ISearchComparison::COMPARE_GREATER_THAN => 'lte',
  46. ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt',
  47. ISearchComparison::COMPARE_LESS_THAN => 'gte',
  48. ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt'
  49. ];
  50. const TAG_FAVORITE = '_$!<Favorite>!$_';
  51. /** @var IMimeTypeLoader */
  52. private $mimetypeLoader;
  53. /**
  54. * QuerySearchUtil constructor.
  55. *
  56. * @param IMimeTypeLoader $mimetypeLoader
  57. */
  58. public function __construct(IMimeTypeLoader $mimetypeLoader) {
  59. $this->mimetypeLoader = $mimetypeLoader;
  60. }
  61. /**
  62. * Whether or not the tag tables should be joined to complete the search
  63. *
  64. * @param ISearchOperator $operator
  65. * @return boolean
  66. */
  67. public function shouldJoinTags(ISearchOperator $operator) {
  68. if ($operator instanceof ISearchBinaryOperator) {
  69. return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) {
  70. return $shouldJoin || $this->shouldJoinTags($operator);
  71. }, false);
  72. } else if ($operator instanceof ISearchComparison) {
  73. return $operator->getField() === 'tagname' || $operator->getField() === 'favorite';
  74. }
  75. return false;
  76. }
  77. /**
  78. * @param IQueryBuilder $builder
  79. * @param ISearchOperator $operator
  80. */
  81. public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) {
  82. return array_map(function ($operator) use ($builder) {
  83. return $this->searchOperatorToDBExpr($builder, $operator);
  84. }, $operators);
  85. }
  86. public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) {
  87. $expr = $builder->expr();
  88. if ($operator instanceof ISearchBinaryOperator) {
  89. switch ($operator->getType()) {
  90. case ISearchBinaryOperator::OPERATOR_NOT:
  91. $negativeOperator = $operator->getArguments()[0];
  92. if ($negativeOperator instanceof ISearchComparison) {
  93. return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap);
  94. } else {
  95. throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
  96. }
  97. case ISearchBinaryOperator::OPERATOR_AND:
  98. return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
  99. case ISearchBinaryOperator::OPERATOR_OR:
  100. return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
  101. default:
  102. throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
  103. }
  104. } else if ($operator instanceof ISearchComparison) {
  105. return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap);
  106. } else {
  107. throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
  108. }
  109. }
  110. private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
  111. $this->validateComparison($comparison);
  112. list($field, $value, $type) = $this->getOperatorFieldAndValue($comparison);
  113. if (isset($operatorMap[$type])) {
  114. $queryOperator = $operatorMap[$type];
  115. return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value));
  116. } else {
  117. throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
  118. }
  119. }
  120. private function getOperatorFieldAndValue(ISearchComparison $operator) {
  121. $field = $operator->getField();
  122. $value = $operator->getValue();
  123. $type = $operator->getType();
  124. if ($field === 'mimetype') {
  125. if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
  126. $value = $this->mimetypeLoader->getId($value);
  127. } else if ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
  128. // transform "mimetype='foo/%'" to "mimepart='foo'"
  129. if (preg_match('|(.+)/%|', $value, $matches)) {
  130. $field = 'mimepart';
  131. $value = $this->mimetypeLoader->getId($matches[1]);
  132. $type = ISearchComparison::COMPARE_EQUAL;
  133. }
  134. if (strpos($value, '%') !== false) {
  135. throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
  136. }
  137. }
  138. } else if ($field === 'favorite') {
  139. $field = 'tag.category';
  140. $value = self::TAG_FAVORITE;
  141. } else if ($field === 'tagname') {
  142. $field = 'tag.category';
  143. }
  144. return [$field, $value, $type];
  145. }
  146. private function validateComparison(ISearchComparison $operator) {
  147. $types = [
  148. 'mimetype' => 'string',
  149. 'mtime' => 'integer',
  150. 'name' => 'string',
  151. 'size' => 'integer',
  152. 'tagname' => 'string',
  153. 'favorite' => 'boolean',
  154. 'fileid' => 'integer'
  155. ];
  156. $comparisons = [
  157. 'mimetype' => ['eq', 'like'],
  158. 'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
  159. 'name' => ['eq', 'like'],
  160. 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'],
  161. 'tagname' => ['eq', 'like'],
  162. 'favorite' => ['eq'],
  163. 'fileid' => ['eq']
  164. ];
  165. if (!isset($types[$operator->getField()])) {
  166. throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
  167. }
  168. $type = $types[$operator->getField()];
  169. if (gettype($operator->getValue()) !== $type) {
  170. throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
  171. }
  172. if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
  173. throw new \InvalidArgumentException('Unsupported comparison for field ' . $operator->getField() . ': ' . $operator->getType());
  174. }
  175. }
  176. private function getParameterForValue(IQueryBuilder $builder, $value) {
  177. if ($value instanceof \DateTime) {
  178. $value = $value->getTimestamp();
  179. }
  180. if (is_numeric($value)) {
  181. $type = IQueryBuilder::PARAM_INT;
  182. } else {
  183. $type = IQueryBuilder::PARAM_STR;
  184. }
  185. return $builder->createNamedParameter($value, $type);
  186. }
  187. /**
  188. * @param IQueryBuilder $query
  189. * @param ISearchOrder[] $orders
  190. */
  191. public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) {
  192. foreach ($orders as $order) {
  193. $query->addOrderBy($order->getField(), $order->getDirection());
  194. }
  195. }
  196. }