Completed
Push — master ( d2654c...b59f6d )
by Morris
29:34 queued 18:06
created

QuerySearchHelper::addSearchOrdersToQuery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Robin Appelman <[email protected]>
4
 *
5
 * @license GNU AGPL version 3 or any later version
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as
9
 * published by the Free Software Foundation, either version 3 of the
10
 * License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 *
20
 */
21
22
namespace OC\Files\Cache;
23
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
/**
32
 * Tools for transforming search queries into database queries
33
 */
34
class QuerySearchHelper {
35
	static protected $searchOperatorMap = [
36
		ISearchComparison::COMPARE_LIKE => 'iLike',
37
		ISearchComparison::COMPARE_EQUAL => 'eq',
38
		ISearchComparison::COMPARE_GREATER_THAN => 'gt',
39
		ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte',
40
		ISearchComparison::COMPARE_LESS_THAN => 'lt',
41
		ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte'
42
	];
43
44
	static protected $searchOperatorNegativeMap = [
45
		ISearchComparison::COMPARE_LIKE => 'notLike',
46
		ISearchComparison::COMPARE_EQUAL => 'neq',
47
		ISearchComparison::COMPARE_GREATER_THAN => 'lte',
48
		ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt',
49
		ISearchComparison::COMPARE_LESS_THAN => 'gte',
50
		ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt'
51
	];
52
53
	const TAG_FAVORITE = '_$!<Favorite>!$_';
54
55
	/** @var IMimeTypeLoader */
56
	private $mimetypeLoader;
57
58
	/**
59
	 * QuerySearchUtil constructor.
60
	 *
61
	 * @param IMimeTypeLoader $mimetypeLoader
62
	 */
63
	public function __construct(IMimeTypeLoader $mimetypeLoader) {
64
		$this->mimetypeLoader = $mimetypeLoader;
65
	}
66
67
	/**
68
	 * Whether or not the tag tables should be joined to complete the search
69
	 *
70
	 * @param ISearchOperator $operator
71
	 * @return boolean
72
	 */
73
	public function shouldJoinTags(ISearchOperator $operator) {
74
		if ($operator instanceof ISearchBinaryOperator) {
75
			return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) {
76
				return $shouldJoin || $this->shouldJoinTags($operator);
77
			}, false);
78
		} else if ($operator instanceof ISearchComparison) {
79
			return $operator->getField() === 'tagname' || $operator->getField() === 'favorite';
80
		}
81
		return false;
82
	}
83
84
	public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) {
85
		$expr = $builder->expr();
86
		if ($operator instanceof ISearchBinaryOperator) {
87
			switch ($operator->getType()) {
88
				case ISearchBinaryOperator::OPERATOR_NOT:
89
					$negativeOperator = $operator->getArguments()[0];
90
					if ($negativeOperator instanceof ISearchComparison) {
91
						return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap);
92
					} else {
93
						throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
94
					}
95 View Code Duplication
				case ISearchBinaryOperator::OPERATOR_AND:
96
					return $expr->andX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1]));
97 View Code Duplication
				case ISearchBinaryOperator::OPERATOR_OR:
98
					return $expr->orX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1]));
99
				default:
100
					throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
101
			}
102
		} else if ($operator instanceof ISearchComparison) {
103
			return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap);
104
		} else {
105
			throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
106
		}
107
	}
108
109
	private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
110
		$this->validateComparison($comparison);
111
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
121
	private function getOperatorFieldAndValue(ISearchComparison $operator) {
122
		$field = $operator->getField();
123
		$value = $operator->getValue();
124
		$type = $operator->getType();
125
		if ($field === 'mimetype') {
126
			if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
127
				$value = $this->mimetypeLoader->getId($value);
128
			} else if ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
129
				// transform "mimetype='foo/%'" to "mimepart='foo'"
130
				if (preg_match('|(.+)/%|', $value, $matches)) {
131
					$field = 'mimepart';
132
					$value = $this->mimetypeLoader->getId($matches[1]);
133
					$type = ISearchComparison::COMPARE_EQUAL;
134
				}
135
				if (strpos($value, '%') !== false) {
136
					throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
137
				}
138
			}
139
		} else if ($field === 'favorite') {
140
			$field = 'tag.category';
141
			$value = self::TAG_FAVORITE;
142
		} else if ($field === 'tagname') {
143
			$field = 'tag.category';
144
		}
145
		return [$field, $value, $type];
146
	}
147
148
	private function validateComparison(ISearchComparison $operator) {
149
		$types = [
150
			'mimetype' => 'string',
151
			'mtime' => 'integer',
152
			'name' => 'string',
153
			'size' => 'integer',
154
			'tagname' => 'string',
155
			'favorite' => 'boolean'
156
		];
157
		$comparisons = [
158
			'mimetype' => ['eq', 'like'],
159
			'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
160
			'name' => ['eq', 'like'],
161
			'size' => ['eq', 'gt', 'lt', 'gte', 'lte'],
162
			'tagname' => ['eq', 'like'],
163
			'favorite' => ['eq'],
164
		];
165
166
		if (!isset($types[$operator->getField()])) {
167
			throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
168
		}
169
		$type = $types[$operator->getField()];
170
		if (gettype($operator->getValue()) !== $type) {
171
			throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
172
		}
173
		if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
174
			throw new \InvalidArgumentException('Unsupported comparison for field  ' . $operator->getField() . ': ' . $operator->getType());
175
		}
176
	}
177
178
	private function getParameterForValue(IQueryBuilder $builder, $value) {
179
		if ($value instanceof \DateTime) {
180
			$value = $value->getTimestamp();
181
		}
182
		if (is_numeric($value)) {
183
			$type = IQueryBuilder::PARAM_INT;
184
		} else {
185
			$type = IQueryBuilder::PARAM_STR;
186
		}
187
		return $builder->createNamedParameter($value, $type);
188
	}
189
190
	/**
191
	 * @param IQueryBuilder $query
192
	 * @param ISearchOrder[] $orders
193
	 */
194
	public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) {
195
		foreach ($orders as $order) {
196
			$query->addOrderBy($order->getField(), $order->getDirection());
197
		}
198
	}
199
}
200