Passed
Push — master ( 4c60ff...437d93 )
by Julius
15:25 queued 12s
created

SearchBuilder::getParameterForValue()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
nc 4
nop 2
dl 0
loc 10
rs 10
c 1
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Robin Appelman <[email protected]>
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Robin Appelman <[email protected]>
7
 * @author Roeland Jago Douma <[email protected]>
8
 * @author Tobias Kaminsky <[email protected]>
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
27
namespace OC\Files\Cache;
28
29
use OCP\DB\QueryBuilder\IQueryBuilder;
30
use OCP\Files\IMimeTypeLoader;
31
use OCP\Files\Search\ISearchBinaryOperator;
32
use OCP\Files\Search\ISearchComparison;
33
use OCP\Files\Search\ISearchOperator;
34
use OCP\Files\Search\ISearchOrder;
35
36
/**
37
 * Tools for transforming search queries into database queries
38
 */
39
class SearchBuilder {
40
	protected static $searchOperatorMap = [
41
		ISearchComparison::COMPARE_LIKE => 'iLike',
42
		ISearchComparison::COMPARE_EQUAL => 'eq',
43
		ISearchComparison::COMPARE_GREATER_THAN => 'gt',
44
		ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte',
45
		ISearchComparison::COMPARE_LESS_THAN => 'lt',
46
		ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte',
47
	];
48
49
	protected static $searchOperatorNegativeMap = [
50
		ISearchComparison::COMPARE_LIKE => 'notLike',
51
		ISearchComparison::COMPARE_EQUAL => 'neq',
52
		ISearchComparison::COMPARE_GREATER_THAN => 'lte',
53
		ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt',
54
		ISearchComparison::COMPARE_LESS_THAN => 'gte',
55
		ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt',
56
	];
57
58
	public const TAG_FAVORITE = '_$!<Favorite>!$_';
59
60
	/** @var IMimeTypeLoader */
61
	private $mimetypeLoader;
62
63
	public function __construct(
64
		IMimeTypeLoader $mimetypeLoader
65
	) {
66
		$this->mimetypeLoader = $mimetypeLoader;
67
	}
68
69
	/**
70
	 * Whether or not the tag tables should be joined to complete the search
71
	 *
72
	 * @param ISearchOperator $operator
73
	 * @return boolean
74
	 */
75
	public function shouldJoinTags(ISearchOperator $operator) {
76
		if ($operator instanceof ISearchBinaryOperator) {
77
			return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) {
78
				return $shouldJoin || $this->shouldJoinTags($operator);
79
			}, false);
80
		} elseif ($operator instanceof ISearchComparison) {
81
			return $operator->getField() === 'tagname' || $operator->getField() === 'favorite';
82
		}
83
		return false;
84
	}
85
86
	/**
87
	 * @param IQueryBuilder $builder
88
	 * @param ISearchOperator $operator
89
	 */
90
	public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) {
91
		return array_filter(array_map(function ($operator) use ($builder) {
92
			return $this->searchOperatorToDBExpr($builder, $operator);
93
		}, $operators));
94
	}
95
96
	public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) {
97
		$expr = $builder->expr();
98
		if ($operator instanceof ISearchBinaryOperator) {
99
			if (count($operator->getArguments()) === 0) {
100
				return null;
101
			}
102
103
			switch ($operator->getType()) {
104
				case ISearchBinaryOperator::OPERATOR_NOT:
105
					$negativeOperator = $operator->getArguments()[0];
106
					if ($negativeOperator instanceof ISearchComparison) {
107
						return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap);
108
					} else {
109
						throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
110
					}
111
				// no break
112
				case ISearchBinaryOperator::OPERATOR_AND:
113
					return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
114
				case ISearchBinaryOperator::OPERATOR_OR:
115
					return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
116
				default:
117
					throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
118
			}
119
		} elseif ($operator instanceof ISearchComparison) {
120
			return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap);
121
		} else {
122
			throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
123
		}
124
	}
125
126
	private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
127
		$this->validateComparison($comparison);
128
129
		[$field, $value, $type] = $this->getOperatorFieldAndValue($comparison);
130
		if (isset($operatorMap[$type])) {
131
			$queryOperator = $operatorMap[$type];
132
			return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value));
133
		} else {
134
			throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
135
		}
136
	}
137
138
	private function getOperatorFieldAndValue(ISearchComparison $operator) {
139
		$field = $operator->getField();
140
		$value = $operator->getValue();
141
		$type = $operator->getType();
142
		if ($field === 'mimetype') {
143
			$value = (string)$value;
144
			if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
145
				$value = (int)$this->mimetypeLoader->getId($value);
146
			} elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
147
				// transform "mimetype='foo/%'" to "mimepart='foo'"
148
				if (preg_match('|(.+)/%|', $value, $matches)) {
149
					$field = 'mimepart';
150
					$value = (int)$this->mimetypeLoader->getId($matches[1]);
151
					$type = ISearchComparison::COMPARE_EQUAL;
152
				} elseif (strpos($value, '%') !== false) {
153
					throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
154
				} else {
155
					$field = 'mimetype';
156
					$value = (int)$this->mimetypeLoader->getId($value);
157
					$type = ISearchComparison::COMPARE_EQUAL;
158
				}
159
			}
160
		} elseif ($field === 'favorite') {
161
			$field = 'tag.category';
162
			$value = self::TAG_FAVORITE;
163
		} elseif ($field === 'tagname') {
164
			$field = 'tag.category';
165
		} elseif ($field === 'fileid') {
166
			$field = 'file.fileid';
167
		} elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL) {
168
			$field = 'path_hash';
169
			$value = md5((string)$value);
170
		}
171
		return [$field, $value, $type];
172
	}
173
174
	private function validateComparison(ISearchComparison $operator) {
175
		$types = [
176
			'mimetype' => 'string',
177
			'mtime' => 'integer',
178
			'name' => 'string',
179
			'path' => 'string',
180
			'size' => 'integer',
181
			'tagname' => 'string',
182
			'favorite' => 'boolean',
183
			'fileid' => 'integer',
184
			'storage' => 'integer',
185
		];
186
		$comparisons = [
187
			'mimetype' => ['eq', 'like'],
188
			'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
189
			'name' => ['eq', 'like'],
190
			'path' => ['eq', 'like'],
191
			'size' => ['eq', 'gt', 'lt', 'gte', 'lte'],
192
			'tagname' => ['eq', 'like'],
193
			'favorite' => ['eq'],
194
			'fileid' => ['eq'],
195
			'storage' => ['eq'],
196
		];
197
198
		if (!isset($types[$operator->getField()])) {
199
			throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
200
		}
201
		$type = $types[$operator->getField()];
202
		if (gettype($operator->getValue()) !== $type) {
203
			throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
204
		}
205
		if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
206
			throw new \InvalidArgumentException('Unsupported comparison for field  ' . $operator->getField() . ': ' . $operator->getType());
207
		}
208
	}
209
210
	private function getParameterForValue(IQueryBuilder $builder, $value) {
211
		if ($value instanceof \DateTime) {
212
			$value = $value->getTimestamp();
213
		}
214
		if (is_numeric($value)) {
215
			$type = IQueryBuilder::PARAM_INT;
216
		} else {
217
			$type = IQueryBuilder::PARAM_STR;
218
		}
219
		return $builder->createNamedParameter($value, $type);
220
	}
221
222
	/**
223
	 * @param IQueryBuilder $query
224
	 * @param ISearchOrder[] $orders
225
	 */
226
	public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) {
227
		foreach ($orders as $order) {
228
			$field = $order->getField();
229
			if ($field === 'fileid') {
230
				$field = 'file.fileid';
231
			}
232
			$query->addOrderBy($field, $order->getDirection());
233
		}
234
	}
235
}
236