MetadataTable   F
last analyzed

Complexity

Total Complexity 108

Size/Duplication

Total Lines 815
Duplicated Lines 16.32 %

Coupling/Cohesion

Components 2
Dependencies 9

Test Coverage

Coverage 2.7%

Importance

Changes 0
Metric Value
dl 133
loc 815
ccs 9
cts 333
cp 0.027
rs 1.785
c 0
b 0
f 0
wmc 108
lcom 2
cbo 9

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
A get() 0 3 1
A delete() 0 5 2
C create() 8 70 11
C update() 10 64 11
A createFromArray() 0 12 3
A getAll() 12 12 3
A deleteAll() 13 13 2
A disableAll() 14 14 2
A enableAll() 10 10 3
A getEntities() 0 26 2
F getEntityMetadataWhereSql() 66 253 57
A getUrl() 0 5 2
A registerMetadataAsIndependent() 0 7 2
A isMetadataIndependent() 0 8 3
A handleUpdate() 0 11 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MetadataTable often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MetadataTable, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Elgg\Database;
3
4
5
use Elgg\Database;
6
use Elgg\Database\EntityTable;
7
use Elgg\Database\MetastringsTable;
8
use Elgg\EventsService as Events;
9
use ElggSession as Session;
10
use Elgg\Cache\MetadataCache as Cache;
11
12
/**
13
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
14
 *
15
 * @access private
16
 *
17
 * @package    Elgg.Core
18
 * @subpackage Database
19
 * @since      1.10.0
20
 */
21
class MetadataTable {
22
	/** @var array */
23
	private $independents = array();
24
	
25
	/** @var Cache */
26
	private $cache;
27
	
28
	/** @var Database */
29
	private $db;
30
	
31
	/** @var EntityTable */
32
	private $entityTable;
33
	
34
	/** @var MetastringsTable */
35
	private $metastringsTable;
36
	
37
	/** @var Events */
38
	private $events;
39
	
40
	/** @var Session */
41
	private $session;
42
	
43
	/** @var string */
44
	private $table;
45
46
	/**
47
	 * Constructor
48
	 * 
49
	 * @param Cache            $cache            A cache for this table
50
	 * @param Database         $db               The Elgg database
51
	 * @param EntityTable      $entityTable      The entities table
52
	 * @param Events           $events           The events registry
53
	 * @param MetastringsTable $metastringsTable The metastrings table
54
	 * @param Session          $session          The session
55
	 */
56 1
	public function __construct(
57
			Cache $cache,
58
			Database $db,
59
			EntityTable $entityTable,
60
			Events $events,
61
			MetastringsTable $metastringsTable,
62
			Session $session) {
63 1
		$this->cache = $cache;
64 1
		$this->db = $db;
65 1
		$this->entityTable = $entityTable;
66 1
		$this->events = $events;
67 1
		$this->metastringsTable = $metastringsTable;
68 1
		$this->session = $session;
69 1
		$this->table = $this->db->getTablePrefix() . "metadata";
70 1
	}
71
72
	/**
73
	 * Get a specific metadata object by its id.
74
	 * If you want multiple metadata objects, use
75
	 * {@link elgg_get_metadata()}.
76
	 *
77
	 * @param int $id The id of the metadata object being retrieved.
78
	 *
79
	 * @return \ElggMetadata|false  false if not found
80
	 */
81
	function get($id) {
82
		return _elgg_get_metastring_based_object_from_id($id, 'metadata');
83
	}
84
	
85
	/**
86
	 * Deletes metadata using its ID.
87
	 *
88
	 * @param int $id The metadata ID to delete.
89
	 * @return bool
90
	 */
91
	function delete($id) {
92
		$metadata = $this->get($id);
93
94
		return $metadata ? $metadata->delete() : false;
95
	}
96
	
97
	/**
98
	 * Create a new metadata object, or update an existing one.
99
	 *
100
	 * Metadata can be an array by setting allow_multiple to true, but it is an
101
	 * indexed array with no control over the indexing.
102
	 *
103
	 * @param int    $entity_guid    The entity to attach the metadata to
104
	 * @param string $name           Name of the metadata
105
	 * @param string $value          Value of the metadata
106
	 * @param string $value_type     'text', 'integer', or '' for automatic detection
107
	 * @param int    $owner_guid     GUID of entity that owns the metadata. Default is logged in user.
108
	 * @param int    $access_id      Default is ACCESS_PRIVATE
109
	 * @param bool   $allow_multiple Allow multiple values for one key. Default is false
110
	 *
111
	 * @return int|false id of metadata or false if failure
112
	 */
113
	function create($entity_guid, $name, $value, $value_type = '', $owner_guid = 0,
114
			$access_id = ACCESS_PRIVATE, $allow_multiple = false) {
115
116
		$entity_guid = (int)$entity_guid;
117
		// name and value are encoded in add_metastring()
118
		$value_type = detect_extender_valuetype($value, $this->db->sanitizeString(trim($value_type)));
119
		$time = time();
120
		$owner_guid = (int)$owner_guid;
121
		$allow_multiple = (boolean)$allow_multiple;
122
	
123
		if (!isset($value)) {
124
			return false;
125
		}
126
	
127
		if ($owner_guid == 0) {
128
			$owner_guid = $this->session->getLoggedInUserGuid();
129
		}
130
	
131
		$access_id = (int)$access_id;
132
	
133
		$query = "SELECT * from {$this->table}"
134
			. " WHERE entity_guid = $entity_guid and name_id=" . $this->metastringsTable->getId($name) . " limit 1";
135
	
136
		$existing = $this->db->getDataRow($query);
137
		if ($existing && !$allow_multiple) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $existing of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
138
			$id = (int)$existing->id;
139
			$result = $this->update($id, $name, $value, $value_type, $owner_guid, $access_id);
140
	
141
			if (!$result) {
142
				return false;
143
			}
144
		} else {
145
			// Support boolean types
146
			if (is_bool($value)) {
147
				$value = (int)$value;
148
			}
149
	
150
			// Add the metastrings
151
			$value_id = $this->metastringsTable->getId($value);
152
			if (!$value_id) {
153
				return false;
154
			}
155
	
156
			$name_id = $this->metastringsTable->getId($name);
157
			if (!$name_id) {
158
				return false;
159
			}
160
	
161
			// If ok then add it
162
			$query = "INSERT into {$this->table}"
163
				. " (entity_guid, name_id, value_id, value_type, owner_guid, time_created, access_id)"
164
				. " VALUES ($entity_guid, '$name_id','$value_id','$value_type', $owner_guid, $time, $access_id)";
165
	
166
			$id = $this->db->insertData($query);
167
	
168
			if ($id !== false) {
169
				$obj = $this->get($id);
170 View Code Duplication
				if ($this->events->trigger('create', 'metadata', $obj)) {
171
172
					$this->cache->clear($entity_guid);
173
	
174
					return $id;
175
				} else {
176
					$this->delete($id);
177
				}
178
			}
179
		}
180
	
181
		return $id;
182
	}
183
	
184
	/**
185
	 * Update a specific piece of metadata.
186
	 *
187
	 * @param int    $id         ID of the metadata to update
188
	 * @param string $name       Metadata name
189
	 * @param string $value      Metadata value
190
	 * @param string $value_type Value type
191
	 * @param int    $owner_guid Owner guid
192
	 * @param int    $access_id  Access ID
193
	 *
194
	 * @return bool
195
	 */
196
	function update($id, $name, $value, $value_type, $owner_guid, $access_id) {
197
		$id = (int)$id;
198
	
199
		if (!$md = $this->get($id)) {
200
			return false;
201
		}
202
		if (!$md->canEdit()) {
203
			return false;
204
		}
205
	
206
		// If memcached then we invalidate the cache for this entry
207
		static $metabyname_memcache;
208
		if ((!$metabyname_memcache) && (is_memcache_available())) {
209
			$metabyname_memcache = new \ElggMemcache('metabyname_memcache');
210
		}
211
	
212
		if ($metabyname_memcache) {
213
			// @todo fix memcache (name_id is not a property of \ElggMetadata)
214
			$metabyname_memcache->delete("{$md->entity_guid}:{$md->name_id}");
215
		}
216
	
217
		$value_type = detect_extender_valuetype($value, $this->db->sanitizeString(trim($value_type)));
218
	
219
		$owner_guid = (int)$owner_guid;
220
		if ($owner_guid == 0) {
221
			$owner_guid = $this->session->getLoggedInUserGuid();
222
		}
223
	
224
		$access_id = (int)$access_id;
225
	
226
		// Support boolean types (as integers)
227
		if (is_bool($value)) {
228
			$value = (int)$value;
229
		}
230
	
231
		$value_id = $this->metastringsTable->getId($value);
232
		if (!$value_id) {
233
			return false;
234
		}
235
	
236
		$name_id = $this->metastringsTable->getId($name);
237
		if (!$name_id) {
238
			return false;
239
		}
240
	
241
		// If ok then add it
242
		$query = "UPDATE {$this->table}"
243
			. " set name_id='$name_id', value_id='$value_id', value_type='$value_type', access_id=$access_id,"
244
			. " owner_guid=$owner_guid where id=$id";
245
	
246
		$result = $this->db->updateData($query);
247 View Code Duplication
		if ($result !== false) {
248
	
249
			$this->cache->clear($md->entity_guid);
250
	
251
			// @todo this event tells you the metadata has been updated, but does not
252
			// let you do anything about it. What is needed is a plugin hook before
253
			// the update that passes old and new values.
254
			$obj = $this->get($id);
255
			$this->events->trigger('update', 'metadata', $obj);
256
		}
257
	
258
		return $result;
259
	}
260
	
261
	/**
262
	 * This function creates metadata from an associative array of "key => value" pairs.
263
	 *
264
	 * To achieve an array for a single key, pass in the same key multiple times with
265
	 * allow_multiple set to true. This creates an indexed array. It does not support
266
	 * associative arrays and there is no guarantee on the ordering in the array.
267
	 *
268
	 * @param int    $entity_guid     The entity to attach the metadata to
269
	 * @param array  $name_and_values Associative array - a value can be a string, number, bool
270
	 * @param string $value_type      'text', 'integer', or '' for automatic detection
271
	 * @param int    $owner_guid      GUID of entity that owns the metadata
272
	 * @param int    $access_id       Default is ACCESS_PRIVATE
273
	 * @param bool   $allow_multiple  Allow multiple values for one key. Default is false
274
	 *
275
	 * @return bool
276
	 */
277
	function createFromArray($entity_guid, array $name_and_values, $value_type, $owner_guid,
278
			$access_id = ACCESS_PRIVATE, $allow_multiple = false) {
279
	
280
		foreach ($name_and_values as $k => $v) {
281
			$result = $this->create($entity_guid, $k, $v, $value_type, $owner_guid,
282
				$access_id, $allow_multiple);
283
			if (!$result) {
284
				return false;
285
			}
286
		}
287
		return true;
288
	}
289
	
290
	/**
291
	 * Returns metadata.  Accepts all elgg_get_entities() options for entity
292
	 * restraints.
293
	 *
294
	 * @see elgg_get_entities
295
	 *
296
	 * @warning 1.7's find_metadata() didn't support limits and returned all metadata.
297
	 *          This function defaults to a limit of 25. There is probably not a reason
298
	 *          for you to return all metadata unless you're exporting an entity,
299
	 *          have other restraints in place, or are doing something horribly
300
	 *          wrong in your code.
301
	 *
302
	 * @param array $options Array in format:
303
	 *
304
	 * metadata_names               => null|ARR metadata names
305
	 * metadata_values              => null|ARR metadata values
306
	 * metadata_ids                 => null|ARR metadata ids
307
	 * metadata_case_sensitive      => BOOL Overall Case sensitive
308
	 * metadata_owner_guids         => null|ARR guids for metadata owners
309
	 * metadata_created_time_lower  => INT Lower limit for created time.
310
	 * metadata_created_time_upper  => INT Upper limit for created time.
311
	 * metadata_calculation         => STR Perform the MySQL function on the metadata values returned.
312
	 *                                   The "metadata_calculation" option causes this function to
313
	 *                                   return the result of performing a mathematical calculation on
314
	 *                                   all metadata that match the query instead of returning
315
	 *                                   \ElggMetadata objects.
316
	 *
317
	 * @return \ElggMetadata[]|mixed
318
	 */
319 View Code Duplication
	function getAll(array $options = array()) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
320
	
321
		// @todo remove support for count shortcut - see #4393
322
		// support shortcut of 'count' => true for 'metadata_calculation' => 'count'
323
		if (isset($options['count']) && $options['count']) {
324
			$options['metadata_calculation'] = 'count';
325
			unset($options['count']);
326
		}
327
	
328
		$options['metastring_type'] = 'metadata';
329
		return _elgg_get_metastring_based_objects($options);
330
	}
331
	
332
	/**
333
	 * Deletes metadata based on $options.
334
	 *
335
	 * @warning Unlike elgg_get_metadata() this will not accept an empty options array!
336
	 *          This requires at least one constraint: metadata_owner_guid(s),
337
	 *          metadata_name(s), metadata_value(s), or guid(s) must be set.
338
	 *
339
	 * @param array $options An options array. {@link elgg_get_metadata()}
340
	 * @return bool|null true on success, false on failure, null if no metadata to delete.
341
	 */
342 View Code Duplication
	function deleteAll(array $options) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
343
		if (!_elgg_is_valid_options_for_batch_operation($options, 'metadata')) {
344
			return false;
345
		}
346
		$options['metastring_type'] = 'metadata';
347
		$result = _elgg_batch_metastring_based_objects($options, 'elgg_batch_delete_callback', false);
348
	
349
		// This moved last in case an object's constructor sets metadata. Currently the batch
350
		// delete process has to create the entity to delete its metadata. See #5214
351
		$this->cache->invalidateByOptions($options);
352
	
353
		return $result;
354
	}
355
	
356
	/**
357
	 * Disables metadata based on $options.
358
	 *
359
	 * @warning Unlike elgg_get_metadata() this will not accept an empty options array!
360
	 *
361
	 * @param array $options An options array. {@link elgg_get_metadata()}
362
	 * @return bool|null true on success, false on failure, null if no metadata disabled.
363
	 */
364 View Code Duplication
	function disableAll(array $options) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
365
		if (!_elgg_is_valid_options_for_batch_operation($options, 'metadata')) {
366
			return false;
367
		}
368
	
369
		$this->cache->invalidateByOptions($options);
370
	
371
		// if we can see hidden (disabled) we need to use the offset
372
		// otherwise we risk an infinite loop if there are more than 50
373
		$inc_offset = access_get_show_hidden_status();
374
	
375
		$options['metastring_type'] = 'metadata';
376
		return _elgg_batch_metastring_based_objects($options, 'elgg_batch_disable_callback', $inc_offset);
377
	}
378
	
379
	/**
380
	 * Enables metadata based on $options.
381
	 *
382
	 * @warning Unlike elgg_get_metadata() this will not accept an empty options array!
383
	 *
384
	 * @warning In order to enable metadata, you must first use
385
	 * {@link access_show_hidden_entities()}.
386
	 *
387
	 * @param array $options An options array. {@link elgg_get_metadata()}
388
	 * @return bool|null true on success, false on failure, null if no metadata enabled.
389
	 */
390 View Code Duplication
	function enableAll(array $options) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
391
		if (!$options || !is_array($options)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
392
			return false;
393
		}
394
	
395
		$this->cache->invalidateByOptions($options);
396
	
397
		$options['metastring_type'] = 'metadata';
398
		return _elgg_batch_metastring_based_objects($options, 'elgg_batch_enable_callback');
399
	}
400
	
401
	/**
402
	 * Returns entities based upon metadata.  Also accepts all
403
	 * options available to elgg_get_entities().  Supports
404
	 * the singular option shortcut.
405
	 *
406
	 * @note Using metadata_names and metadata_values results in a
407
	 * "names IN (...) AND values IN (...)" clause.  This is subtly
408
	 * differently than default multiple metadata_name_value_pairs, which use
409
	 * "(name = value) AND (name = value)" clauses.
410
	 *
411
	 * When in doubt, use name_value_pairs.
412
	 *
413
	 * To ask for entities that do not have a metadata value, use a custom
414
	 * where clause like this:
415
	 *
416
	 * 	$options['wheres'][] = "NOT EXISTS (
417
	 *			SELECT 1 FROM {$dbprefix}metadata md
418
	 *			WHERE md.entity_guid = e.guid
419
	 *				AND md.name_id = $name_metastring_id
420
	 *				AND md.value_id = $value_metastring_id)";
421
	 *
422
	 * Note the metadata name and value has been denormalized in the above example.
423
	 *
424
	 * @see elgg_get_entities
425
	 *
426
	 * @param array $options Array in format:
427
	 *
428
	 * 	metadata_names => null|ARR metadata names
429
	 *
430
	 * 	metadata_values => null|ARR metadata values
431
	 *
432
	 * 	metadata_name_value_pairs => null|ARR (
433
	 *                                         name => 'name',
434
	 *                                         value => 'value',
435
	 *                                         'operand' => '=',
436
	 *                                         'case_sensitive' => true
437
	 *                                        )
438
	 *                               Currently if multiple values are sent via
439
	 *                               an array (value => array('value1', 'value2')
440
	 *                               the pair's operand will be forced to "IN".
441
	 *                               If passing "IN" as the operand and a string as the value, 
442
	 *                               the value must be a properly quoted and escaped string.
443
	 *
444
	 * 	metadata_name_value_pairs_operator => null|STR The operator to use for combining
445
	 *                                        (name = value) OPERATOR (name = value); default AND
446
	 *
447
	 * 	metadata_case_sensitive => BOOL Overall Case sensitive
448
	 *
449
	 *  order_by_metadata => null|ARR array(
450
	 *                                      'name' => 'metadata_text1',
451
	 *                                      'direction' => ASC|DESC,
452
	 *                                      'as' => text|integer
453
	 *                                     )
454
	 *                                Also supports array('name' => 'metadata_text1')
455
	 *
456
	 *  metadata_owner_guids => null|ARR guids for metadata owners
457
	 *
458
	 * @return \ElggEntity[]|mixed If count, int. If not count, array. false on errors.
459
	 */
460
	function getEntities(array $options = array()) {
461
		$defaults = array(
462
			'metadata_names'                     => ELGG_ENTITIES_ANY_VALUE,
463
			'metadata_values'                    => ELGG_ENTITIES_ANY_VALUE,
464
			'metadata_name_value_pairs'          => ELGG_ENTITIES_ANY_VALUE,
465
	
466
			'metadata_name_value_pairs_operator' => 'AND',
467
			'metadata_case_sensitive'            => FALSE,	// cyu - metadata should be case insensitive (for search) (Ilia - migrated from lib/metadata.php due to changes in core update)
468
			'order_by_metadata'                  => array(),
469
	
470
			'metadata_owner_guids'               => ELGG_ENTITIES_ANY_VALUE,
471
		);
472
	
473
		$options = array_merge($defaults, $options);
474
	
475
		$singulars = array('metadata_name', 'metadata_value',
476
			'metadata_name_value_pair', 'metadata_owner_guid');
477
	
478
		$options = _elgg_normalize_plural_options_array($options, $singulars);
479
	
480
		if (!$options = _elgg_entities_get_metastrings_options('metadata', $options)) {
481
			return false;
482
		}
483
	
484
		return $this->entityTable->getEntities($options);
485
	}
486
	
487
	/**
488
	 * Returns metadata name and value SQL where for entities.
489
	 * NB: $names and $values are not paired. Use $pairs for this.
490
	 * Pairs default to '=' operand.
491
	 *
492
	 * This function is reused for annotations because the tables are
493
	 * exactly the same.
494
	 *
495
	 * @param string     $e_table           Entities table name
496
	 * @param string     $n_table           Normalized metastrings table name (Where entities,
497
	 *                                    values, and names are joined. annotations / metadata)
498
	 * @param array|null $names             Array of names
499
	 * @param array|null $values            Array of values
500
	 * @param array|null $pairs             Array of names / values / operands
501
	 * @param string     $pair_operator     ("AND" or "OR") Operator to use to join the where clauses for pairs
502
	 * @param bool       $case_sensitive    Case sensitive metadata names?
503
	 * @param array|null $order_by_metadata Array of names / direction
504
	 * @param array|null $owner_guids       Array of owner GUIDs
505
	 *
506
	 * @return false|array False on fail, array('joins', 'wheres')
507
	 * @access private
508
	 */
509
	function getEntityMetadataWhereSql($e_table, $n_table, $names = null, $values = null,
510
			$pairs = null, $pair_operator = 'AND', $case_sensitive = true, $order_by_metadata = null,
511
			$owner_guids = null) {
512
		// short circuit if nothing requested
513
		// 0 is a valid (if not ill-conceived) metadata name.
514
		// 0 is also a valid metadata value for false, null, or 0
515
		// 0 is also a valid(ish) owner_guid
516
		if ((!$names && $names !== 0)
517
			&& (!$values && $values !== 0)
518
			&& (!$pairs && $pairs !== 0)
519
			&& (!$owner_guids && $owner_guids !== 0)
520
			&& !$order_by_metadata) {
521
			return '';
522
		}
523
	
524
		// join counter for incremental joins.
525
		$i = 1;
526
	
527
		// binary forces byte-to-byte comparision of strings, making
528
		// it case- and diacritical-mark- sensitive.
529
		// only supported on values.
530
		$binary = ($case_sensitive) ? ' BINARY ' : '';
531
	
532
		$access = _elgg_get_access_where_sql(array(
533
			'table_alias' => 'n_table',
534
			'guid_column' => 'entity_guid',
535
		));
536
	
537
		$return = array (
538
			'joins' => array (),
539
			'wheres' => array(),
540
			'orders' => array()
541
		);
542
	
543
		// will always want to join these tables if pulling metastrings.
544
		$return['joins'][] = "JOIN {$this->db->getTablePrefix()}{$n_table} n_table on
545
			{$e_table}.guid = n_table.entity_guid";
546
	
547
		$wheres = array();
548
	
549
		// get names wheres and joins
550
		$names_where = '';
551 View Code Duplication
		if ($names !== null) {
552
			if (!is_array($names)) {
553
				$names = array($names);
554
			}
555
	
556
			$sanitised_names = array();
557
			foreach ($names as $name) {
558
				// normalise to 0.
559
				if (!$name) {
560
					$name = '0';
561
				}
562
				$sanitised_names[] = '\'' . $this->db->sanitizeString($name) . '\'';
563
			}
564
	
565
			if ($names_str = implode(',', $sanitised_names)) {
566
				$return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msn on n_table.name_id = msn.id";
567
				$names_where = "(msn.string IN ($names_str))";
568
			}
569
		}
570
	
571
		// get values wheres and joins
572
		$values_where = '';
573 View Code Duplication
		if ($values !== null) {
574
			if (!is_array($values)) {
575
				$values = array($values);
576
			}
577
	
578
			$sanitised_values = array();
579
			foreach ($values as $value) {
580
				// normalize to 0
581
				if (!$value) {
582
					$value = 0;
583
				}
584
				$sanitised_values[] = '\'' . $this->db->sanitizeString($value) . '\'';
585
			}
586
	
587
			if ($values_str = implode(',', $sanitised_values)) {
588
				$return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msv on n_table.value_id = msv.id";
589
				$values_where = "({$binary}msv.string IN ($values_str))";
590
			}
591
		}
592
	
593
		if ($names_where && $values_where) {
594
			$wheres[] = "($names_where AND $values_where AND $access)";
595
		} elseif ($names_where) {
596
			$wheres[] = "($names_where AND $access)";
597
		} elseif ($values_where) {
598
			$wheres[] = "($values_where AND $access)";
599
		}
600
	
601
		// add pairs
602
		// pairs must be in arrays.
603
		if (is_array($pairs)) {
604
			// check if this is an array of pairs or just a single pair.
605
			if (isset($pairs['name']) || isset($pairs['value'])) {
606
				$pairs = array($pairs);
607
			}
608
	
609
			$pair_wheres = array();
610
	
611
			// @todo when the pairs are > 3 should probably split the query up to
612
			// denormalize the strings table.
613
	
614
			foreach ($pairs as $index => $pair) {
615
				// @todo move this elsewhere?
616
				// support shortcut 'n' => 'v' method.
617
				if (!is_array($pair)) {
618
					$pair = array(
619
						'name' => $index,
620
						'value' => $pair
621
					);
622
				}
623
	
624
				// must have at least a name and value
625
				if (!isset($pair['name']) || !isset($pair['value'])) {
626
					// @todo should probably return false.
627
					continue;
628
				}
629
	
630
				// case sensitivity can be specified per pair.
631
				// default to higher level setting.
632
				if (isset($pair['case_sensitive'])) {
633
					$pair_binary = ($pair['case_sensitive']) ? ' BINARY ' : '';
634
				} else {
635
					$pair_binary = $binary;
636
				}
637
	
638 View Code Duplication
				if (isset($pair['operand'])) {
639
					$operand = $this->db->sanitizeString($pair['operand']);
640
				} else {
641
					$operand = ' = ';
642
				}
643
	
644
				// for comparing
645
				$trimmed_operand = trim(strtolower($operand));
646
	
647
				$access = _elgg_get_access_where_sql(array(
648
					'table_alias' => "n_table{$i}",
649
					'guid_column' => 'entity_guid',
650
				));
651
652
				// certain operands can't work well with strings that can be interpreted as numbers
653
				// for direct comparisons like IN, =, != we treat them as strings
654
				// gt/lt comparisons need to stay unencapsulated because strings '5' > '15'
655
				// see https://github.com/Elgg/Elgg/issues/7009
656
				$num_safe_operands = array('>', '<', '>=', '<=');
657
				$num_test_operand = trim(strtoupper($operand));
658
	
659
				if (is_numeric($pair['value']) && in_array($num_test_operand, $num_safe_operands)) {
660
					$value = $this->db->sanitizeString($pair['value']);
661
				} else if (is_bool($pair['value'])) {
662
					$value = (int)$pair['value'];
663 View Code Duplication
				} else if (is_array($pair['value'])) {
664
					$values_array = array();
665
	
666
					foreach ($pair['value'] as $pair_value) {
667
						if (is_numeric($pair_value) && !in_array($num_test_operand, $num_safe_operands)) {
668
							$values_array[] = $this->db->sanitizeString($pair_value);
669
						} else {
670
							$values_array[] = "'" . $this->db->sanitizeString($pair_value) . "'";
671
						}
672
					}
673
	
674
					if ($values_array) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $values_array of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
675
						$value = '(' . implode(', ', $values_array) . ')';
676
					}
677
	
678
					// @todo allow support for non IN operands with array of values.
679
					// will have to do more silly joins.
680
					$operand = 'IN';
681
				} else if ($trimmed_operand == 'in') {
682
					$value = "({$pair['value']})";
683
				} else {
684
					$value = "'" . $this->db->sanitizeString($pair['value']) . "'";
685
				}
686
	
687
				$name = $this->db->sanitizeString($pair['name']);
688
	
689
				// @todo The multiple joins are only needed when the operator is AND
690
				$return['joins'][] = "JOIN {$this->db->getTablePrefix()}{$n_table} n_table{$i}
691
					on {$e_table}.guid = n_table{$i}.entity_guid";
692
				$return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msn{$i}
693
					on n_table{$i}.name_id = msn{$i}.id";
694
				$return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msv{$i}
695
					on n_table{$i}.value_id = msv{$i}.id";
696
	
697
				$pair_wheres[] = "(msn{$i}.string = '$name' AND {$pair_binary}msv{$i}.string
698
					$operand $value AND $access)";
0 ignored issues
show
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
699
	
700
				$i++;
701
			}
702
	
703
			if ($where = implode(" $pair_operator ", $pair_wheres)) {
704
				$wheres[] = "($where)";
705
			}
706
		}
707
	
708
		// add owner_guids
709
		if ($owner_guids) {
710
			if (is_array($owner_guids)) {
711
				$sanitised = array_map('sanitise_int', $owner_guids);
712
				$owner_str = implode(',', $sanitised);
713
			} else {
714
				$owner_str = (int)$owner_guids;
715
			}
716
	
717
			$wheres[] = "(n_table.owner_guid IN ($owner_str))";
718
		}
719
	
720
		if ($where = implode(' AND ', $wheres)) {
721
			$return['wheres'][] = "($where)";
722
		}
723
	
724
		if (is_array($order_by_metadata)) {
725
			if ((count($order_by_metadata) > 0) && !isset($order_by_metadata[0])) {
726
				// singleton, so fix
727
				$order_by_metadata = array($order_by_metadata);
728
			}
729
			foreach ($order_by_metadata as $order_by) {
730
				if (is_array($order_by) && isset($order_by['name'])) {
731
					$name = $this->db->sanitizeString($order_by['name']);
732
					if (isset($order_by['direction'])) {
733
						$direction = $this->db->sanitizeString($order_by['direction']);
734
					} else {
735
						$direction = 'ASC';
736
					}
737
					$return['joins'][] = "JOIN {$this->db->getTablePrefix()}{$n_table} n_table{$i}
738
						on {$e_table}.guid = n_table{$i}.entity_guid";
739
					$return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msn{$i}
740
						on n_table{$i}.name_id = msn{$i}.id";
741
					$return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msv{$i}
742
						on n_table{$i}.value_id = msv{$i}.id";
743
	
744
					$access = _elgg_get_access_where_sql(array(
745
						'table_alias' => "n_table{$i}",
746
						'guid_column' => 'entity_guid',
747
					));
748
	
749
					$return['wheres'][] = "(msn{$i}.string = '$name' AND $access)";
750
					if (isset($order_by['as']) && $order_by['as'] == 'integer') {
751
						$return['orders'][] = "CAST(msv{$i}.string AS SIGNED) $direction";
752
					} else {
753
						$return['orders'][] = "msv{$i}.string $direction";
754
					}
755
					$i++;
756
				}
757
			}
758
		}
759
	
760
		return $return;
761
	}
762
	
763
	/**
764
	 * Get the URL for this metadata
765
	 *
766
	 * By default this links to the export handler in the current view.
767
	 *
768
	 * @param int $id Metadata ID
769
	 *
770
	 * @return mixed
771
	 */
772
	function getUrl($id) {
773
		$extender = $this->get($id);
774
775
		return $extender ? $extender->getURL() : false;
776
	}
777
	
778
	/**
779
	 * Mark entities with a particular type and subtype as having access permissions
780
	 * that can be changed independently from their parent entity
781
	 *
782
	 * @param string $type    The type - object, user, etc
783
	 * @param string $subtype The subtype; all subtypes by default
784
	 *
785
	 * @return void
786
	 */
787
	function registerMetadataAsIndependent($type, $subtype = '*') {
788
		if (!isset($this->independents[$type])) {
789
			$this->independents[$type] = array();
790
		}
791
		
792
		$this->independents[$type][$subtype] = true;
793
	}
794
	
795
	/**
796
	 * Determines whether entities of a given type and subtype should not change
797
	 * their metadata in line with their parent entity
798
	 *
799
	 * @param string $type    The type - object, user, etc
800
	 * @param string $subtype The entity subtype
801
	 *
802
	 * @return bool
803
	 */
804
	function isMetadataIndependent($type, $subtype) {
805
		if (empty($this->independents[$type])) {
806
			return false;
807
		}
808
809
		return !empty($this->independents[$type][$subtype])
810
			|| !empty($this->independents[$type]['*']);
811
	}
812
	
813
	/**
814
	 * When an entity is updated, resets the access ID on all of its child metadata
815
	 *
816
	 * @param string      $event       The name of the event
817
	 * @param string      $object_type The type of object
818
	 * @param \ElggEntity $object      The entity itself
819
	 *
820
	 * @return true
821
	 * @access private Set as private in 1.9.0
822
	 */
823
	function handleUpdate($event, $object_type, $object) {
824
		if ($object instanceof \ElggEntity) {
825
			if (!$this->isMetadataIndependent($object->getType(), $object->getSubtype())) {
826
				$access_id = (int)$object->access_id;
827
				$guid = (int)$object->getGUID();
828
				$query = "update {$this->table} set access_id = {$access_id} where entity_guid = {$guid}";
829
				$this->db->updateData($query);
830
			}
831
		}
832
		return true;
833
	}
834
	
835
}