1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Elgg\Search; |
4
|
|
|
|
5
|
|
|
use Doctrine\DBAL\Query\Expression\CompositeExpression; |
6
|
|
|
use Elgg\Config; |
7
|
|
|
use Elgg\Database; |
8
|
|
|
use Elgg\Database\Clauses\AnnotationWhereClause; |
9
|
|
|
use Elgg\Database\Clauses\AttributeWhereClause; |
10
|
|
|
use Elgg\Database\Clauses\MetadataWhereClause; |
11
|
|
|
use Elgg\Database\Clauses\PrivateSettingWhereClause; |
12
|
|
|
use Elgg\Database\QueryBuilder; |
13
|
|
|
use Elgg\PluginHooksService; |
14
|
|
|
use ElggBatch; |
15
|
|
|
use ElggEntity; |
16
|
|
|
use InvalidParameterException; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* WARNING: API IN FLUX. DO NOT USE DIRECTLY. |
20
|
|
|
* |
21
|
|
|
* Use elgg_search() instead. |
22
|
|
|
* |
23
|
|
|
* @todo Implement type/subtype normalization into types => subtypes pairs and add logic to search for multiple |
24
|
|
|
* subtypes |
25
|
|
|
* |
26
|
|
|
* @access private |
27
|
|
|
* @since 3.0 |
28
|
|
|
*/ |
29
|
|
|
class SearchService { |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var Config |
33
|
|
|
*/ |
34
|
|
|
private $config; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var PluginHooksService |
38
|
|
|
*/ |
39
|
|
|
private $hooks; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var Database |
43
|
|
|
*/ |
44
|
|
|
private $db; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Constructor |
48
|
|
|
* |
49
|
|
|
* @param \Elgg\Config $config Config |
50
|
|
|
* @param \Elgg\PluginHooksService $hooks Hook registration service |
51
|
|
|
* @param Database $db Database |
52
|
|
|
*/ |
53
|
35 |
|
public function __construct(Config $config, PluginHooksService $hooks, Database $db) { |
54
|
35 |
|
$this->config = $config; |
55
|
35 |
|
$this->hooks = $hooks; |
56
|
35 |
|
$this->db = $db; |
57
|
35 |
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Returns search results as an array of entities, as a batch, or a count, |
61
|
|
|
* depending on parameters given. |
62
|
|
|
* |
63
|
|
|
* @param array $options Search parameters |
64
|
|
|
* Accepts all options supported by {@link elgg_get_entities()} |
65
|
|
|
* |
66
|
|
|
* @option string $query Search query |
67
|
|
|
* @option string $type Entity type. Required if no search type is set |
68
|
|
|
* @option string $search_type Custom search type. Required if no type is set |
69
|
|
|
* @option array $fields An array of fields to search in |
70
|
|
|
* @option string $sort An array containing 'property', 'property_type', 'direction' and 'signed' |
71
|
|
|
* @option bool $partial_match Allow partial matches, e.g. find 'elgg' when search for 'el' |
72
|
|
|
* @option bool $tokenize Break down search query into tokens, |
73
|
|
|
* e.g. find 'elgg has been released' when searching for 'elgg released' |
74
|
|
|
* |
75
|
|
|
* @return ElggBatch|ElggEntity[]|int|false |
76
|
|
|
* @throws InvalidParameterException |
77
|
|
|
* |
78
|
|
|
* @see elgg_get_entities() |
79
|
|
|
*/ |
80
|
197 |
|
public function search(array $options = []) { |
81
|
197 |
|
$options = $this->prepareSearchOptions($options); |
82
|
|
|
|
83
|
197 |
|
$query_parts = elgg_extract('query_parts', $options); |
84
|
197 |
|
$fields = elgg_extract('fields', $options); |
85
|
|
|
|
86
|
197 |
|
if (empty($query_parts) || empty(array_filter($fields))) { |
87
|
5 |
|
return false; |
88
|
|
|
} |
89
|
|
|
|
90
|
192 |
|
$entity_type = elgg_extract('type', $options, 'all', false); |
91
|
192 |
|
$entity_subtype = elgg_extract('subtype', $options); |
92
|
192 |
|
$search_type = elgg_extract('search_type', $options, 'entities'); |
93
|
|
|
|
94
|
192 |
|
if ($entity_type !== 'all' && !in_array($entity_type, Config::getEntityTypes())) { |
95
|
|
|
throw new InvalidParameterException("'$entity_type' is not a valid entity type"); |
96
|
|
|
} |
97
|
|
|
|
98
|
192 |
|
$options = $this->hooks->trigger('search:options', $entity_type, $options, $options); |
99
|
192 |
|
if ($entity_subtype) { |
100
|
12 |
|
$options = $this->hooks->trigger('search:options', "$entity_type:$entity_subtype", $options, $options); |
101
|
|
|
} |
102
|
|
|
|
103
|
192 |
|
$options = $this->hooks->trigger('search:options', $search_type, $options, $options); |
104
|
|
|
|
105
|
192 |
|
if ($this->hooks->hasHandler('search:results', $search_type)) { |
106
|
1 |
|
$results = $this->hooks->trigger('search:results', $search_type, $options); |
|
|
|
|
107
|
1 |
|
if (isset($results)) { |
108
|
|
|
// allow hooks to conditionally replace the result set |
109
|
1 |
|
return $results; |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
|
113
|
191 |
|
return elgg_get_entities($options); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Normalize options |
118
|
|
|
* |
119
|
|
|
* @param array $options Options |
120
|
|
|
* |
121
|
|
|
* @return array |
122
|
|
|
*/ |
123
|
213 |
|
public function normalizeOptions(array $options = []) { |
124
|
213 |
|
$search_type = elgg_extract('search_type', $options, 'entities', false); |
125
|
213 |
|
$options['search_type'] = $search_type; |
126
|
|
|
|
127
|
213 |
|
$options = $this->hooks->trigger('search:params', $search_type, $options, $options); |
128
|
|
|
|
129
|
213 |
|
$options = $this->normalizeQuery($options); |
130
|
213 |
|
$options = $this->normalizeSearchFields($options); |
131
|
|
|
|
132
|
213 |
|
return $options; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Prepare ege* options |
137
|
|
|
* |
138
|
|
|
* @param array $options Entity search params |
139
|
|
|
* |
140
|
|
|
* @return array |
141
|
|
|
*/ |
142
|
200 |
|
public function prepareSearchOptions(array $options = []) { |
143
|
200 |
|
$options = $this->normalizeOptions($options); |
144
|
|
|
|
145
|
200 |
|
$fields = elgg_extract('fields', $options); |
146
|
200 |
|
$query_parts = elgg_extract('query_parts', $options); |
147
|
200 |
|
$partial = elgg_extract('partial_match', $options, true); |
148
|
|
|
|
149
|
191 |
|
$options['wheres']['search'] = function (QueryBuilder $qb, $alias) use ($fields, $query_parts, $partial) { |
150
|
191 |
|
return $this->buildSearchWhereQuery($qb, $alias, $fields, $query_parts, $partial); |
151
|
|
|
}; |
152
|
|
|
|
153
|
200 |
|
$options = $this->prepareSortOptions($options); |
154
|
|
|
|
155
|
200 |
|
return $options; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Normalize query parts |
160
|
|
|
* |
161
|
|
|
* @param array $options Options |
162
|
|
|
* |
163
|
|
|
* @return array |
164
|
|
|
*/ |
165
|
213 |
|
public function normalizeQuery(array $options = []) { |
166
|
|
|
|
167
|
213 |
|
$query = elgg_extract('query', $options); |
168
|
213 |
|
$query = filter_var($query, FILTER_SANITIZE_STRING); |
169
|
213 |
|
$query = trim($query); |
170
|
|
|
|
171
|
213 |
|
$words = preg_split('/\s+/', $query); |
172
|
213 |
|
$words = array_map(function ($e) { |
173
|
213 |
|
return trim($e); |
174
|
213 |
|
}, $words); |
|
|
|
|
175
|
|
|
|
176
|
213 |
|
$query = implode(' ', $words); |
177
|
|
|
|
178
|
213 |
|
$options['query'] = $query; |
179
|
|
|
|
180
|
213 |
|
$tokenize = elgg_extract('tokenize', $options, true); |
181
|
213 |
|
if ($tokenize) { |
182
|
118 |
|
$parts = $words; |
183
|
|
|
} else { |
184
|
95 |
|
$parts = [$query]; |
185
|
|
|
} |
186
|
|
|
|
187
|
213 |
|
$options['query_parts'] = array_unique(array_filter($parts)); |
188
|
|
|
|
189
|
213 |
|
return $options; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Normalizes an array of search fields |
194
|
|
|
* |
195
|
|
|
* @param array $options Search parameters |
196
|
|
|
* |
197
|
|
|
* @return array |
198
|
|
|
*/ |
199
|
213 |
|
public function normalizeSearchFields(array $options = []) { |
200
|
|
|
|
201
|
|
|
$fields = [ |
202
|
213 |
|
'attributes' => [], |
203
|
|
|
'metadata' => [], |
204
|
|
|
'annotations' => [], |
205
|
|
|
'private_settings' => [], |
206
|
|
|
]; |
207
|
|
|
|
208
|
213 |
|
$property_types = array_keys($fields); |
209
|
|
|
|
210
|
213 |
|
$entity_type = elgg_extract('type', $options); |
211
|
213 |
|
$entity_subtype = elgg_extract('subtype', $options); |
212
|
213 |
|
$search_type = elgg_extract('search_type', $options, 'entities'); |
213
|
|
|
|
214
|
213 |
|
if ($entity_type) { |
215
|
21 |
|
$fields = $this->hooks->trigger('search:fields', $entity_type, $options, $fields); |
216
|
|
|
} |
217
|
|
|
|
218
|
213 |
|
if ($entity_subtype) { |
219
|
16 |
|
$fields = $this->hooks->trigger('search:fields', "$entity_type:$entity_subtype", $options, $fields); |
220
|
|
|
} |
221
|
|
|
|
222
|
213 |
|
if ($search_type) { |
223
|
213 |
|
$fields = $this->hooks->trigger('search:fields', $search_type, $options, $fields); |
224
|
|
|
} |
225
|
|
|
|
226
|
213 |
|
foreach ($property_types as $property_type) { |
227
|
213 |
|
if (empty($fields[$property_type])) { |
228
|
213 |
|
$fields[$property_type] = []; |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
|
232
|
213 |
|
if (empty($options['fields'])) { |
233
|
21 |
|
$options['fields'] = $fields; |
234
|
|
|
} else { |
235
|
|
|
// only allow known fields |
236
|
192 |
|
foreach ($fields as $property_type => $property_type_fields) { |
237
|
192 |
|
if (empty($options['fields'][$property_type])) { |
238
|
191 |
|
$options['fields'][$property_type] = []; |
239
|
191 |
|
continue; |
240
|
|
|
} |
241
|
|
|
|
242
|
192 |
|
$allowed = array_intersect($property_type_fields, (array) $options['fields'][$property_type]); |
243
|
192 |
|
$options['fields'][$property_type] = array_values(array_unique($allowed)); |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
213 |
|
return $options; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Normalizes sort options |
252
|
|
|
* |
253
|
|
|
* @param array $options Search parameters |
254
|
|
|
* |
255
|
|
|
* @return array |
256
|
|
|
*/ |
257
|
200 |
|
public function prepareSortOptions(array $options = []) { |
258
|
|
|
|
259
|
200 |
|
$sort = elgg_extract('sort', $options); |
260
|
200 |
|
if (is_string($sort)) { |
261
|
|
|
$sort = [ |
262
|
2 |
|
'property' => $sort, |
263
|
2 |
|
'direction' => elgg_extract('order', $options) |
264
|
|
|
]; |
265
|
|
|
} |
266
|
|
|
|
267
|
200 |
|
if (!isset($sort['property'])) { |
268
|
|
|
$sort = [ |
269
|
197 |
|
'property' => 'time_created', |
270
|
|
|
'property_type' => 'attribute', |
271
|
|
|
'direction' => 'desc', |
272
|
|
|
]; |
273
|
|
|
} |
274
|
|
|
|
275
|
200 |
|
$clause = new Database\Clauses\EntitySortByClause(); |
276
|
200 |
|
$clause->property = elgg_extract('property', $sort); |
277
|
200 |
|
$clause->property_type = elgg_extract('property_type', $sort); |
278
|
200 |
|
$clause->direction = elgg_extract('direction', $sort, 'asc'); |
279
|
200 |
|
$clause->signed = elgg_extract('signed', $sort, false); |
280
|
|
|
|
281
|
200 |
|
$options['order_by'] = [$clause]; |
282
|
|
|
|
283
|
200 |
|
return $options; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* Builds search clause |
288
|
|
|
* |
289
|
|
|
* @param QueryBuilder $qb Query builder |
290
|
|
|
* @param string $alias Entity table alias |
291
|
|
|
* @param array $fields Fields to match against |
292
|
|
|
* @param array $query_parts Search query |
293
|
|
|
* @param bool $partial_match Allow partial matches |
294
|
|
|
* |
295
|
|
|
* @return CompositeExpression |
296
|
|
|
* @throws InvalidParameterException |
297
|
|
|
*/ |
298
|
191 |
|
public function buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match = true) { |
299
|
|
|
|
300
|
191 |
|
$attributes = elgg_extract('attributes', $fields, [], false); |
301
|
191 |
|
$metadata = elgg_extract('metadata', $fields, [], false); |
302
|
191 |
|
$annotations = elgg_extract('annotations', $fields, [], false); |
303
|
191 |
|
$private_settings = elgg_extract('private_settings', $fields, [], false); |
304
|
|
|
|
305
|
191 |
|
$ors = []; |
306
|
|
|
|
307
|
191 |
|
$populate_where = function ($where, $part) use ($partial_match) { |
308
|
191 |
|
$where->values = $partial_match ? "%{$part}%" : $part; |
309
|
191 |
|
$where->comparison = 'LIKE'; |
310
|
191 |
|
$where->value_type = ELGG_VALUE_STRING; |
311
|
191 |
|
$where->case_sensitive = false; |
312
|
191 |
|
}; |
313
|
|
|
|
314
|
|
|
if (!empty($attributes)) { |
315
|
|
|
foreach ($attributes as $attribute) { |
316
|
|
|
$attribute_ands = []; |
317
|
|
|
foreach ($query_parts as $part) { |
318
|
|
|
$where = new AttributeWhereClause(); |
319
|
|
|
$where->names = $attribute; |
320
|
|
|
$populate_where($where, $part); |
321
|
|
|
$attribute_ands[] = $where->prepare($qb, $alias); |
322
|
|
|
} |
323
|
|
|
$ors[] = $qb->merge($attribute_ands, 'AND'); |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
if (!empty($metadata)) { |
328
|
|
|
$metadata_ands = []; |
329
|
|
|
$md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata, 'left'); |
|
|
|
|
330
|
|
|
foreach ($query_parts as $part) { |
331
|
|
|
$where = new MetadataWhereClause(); |
332
|
|
|
$populate_where($where, $part); |
333
|
|
|
$metadata_ands[] = $where->prepare($qb, $md_alias); |
334
|
|
|
} |
335
|
|
|
$ors[] = $qb->merge($metadata_ands, 'AND'); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
if (!empty($annotations)) { |
339
|
|
|
$annotations_ands = []; |
340
|
|
|
$an_alias = $qb->joinAnnotationTable($alias, 'guid', $annotations, 'left'); |
|
|
|
|
341
|
|
|
foreach ($query_parts as $part) { |
342
|
|
|
$where = new AnnotationWhereClause(); |
343
|
|
|
$populate_where($where, $part); |
344
|
|
|
$annotations_ands[] = $where->prepare($qb, $an_alias); |
345
|
|
|
} |
346
|
|
|
$ors[] = $qb->merge($annotations_ands, 'AND'); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
if (!empty($private_settings)) { |
350
|
|
|
$private_settings_ands = []; |
351
|
|
|
$ps_alias = $qb->joinPrivateSettingsTable($alias, 'guid', $private_settings, 'left'); |
|
|
|
|
352
|
|
|
foreach ($query_parts as $part) { |
353
|
|
|
$where = new PrivateSettingWhereClause(); |
354
|
|
|
$populate_where($where, $part); |
355
|
|
|
$private_settings_ands[] = $where->prepare($qb, $ps_alias); |
356
|
|
|
} |
357
|
|
|
$ors[] = $qb->merge($private_settings_ands, 'AND'); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
return $qb->merge($ors, 'OR'); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
} |
364
|
|
|
|
This check looks for function or method calls that always return null and whose return value is assigned to a variable.
The method
getObject()
can return nothing but null, so it makes no sense to assign that value to a variable.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.