Passed
Push — master ( 5063d9...a1994b )
by Jeroen
22:08
created

engine/classes/Elgg/Database/EntityTable.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Elgg\Database;
4
5
use ClassException;
6
use Elgg\Cache\EntityCache;
7
use Elgg\Cache\MetadataCache;
8
use Elgg\Config as Conf;
9
use Elgg\Database;
10
use Elgg\Database\EntityTable\UserFetchFailureException;
11
use Elgg\Database\SubtypeTable;
12
use Elgg\EntityPreloader;
13
use Elgg\EventsService;
14
use Elgg\I18n\Translator;
15
use Elgg\Logger;
16
use ElggBatch;
17
use ElggEntity;
18
use ElggGroup;
19
use ElggObject;
20
use ElggPlugin;
21
use ElggSession;
22
use ElggSite;
23
use ElggUser;
24
use IncompleteEntityException;
25
use InstallationException;
26
use InvalidArgumentException;
27
use LogicException;
28
use stdClass;
29
30
/**
31
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
32
 *
33
 * @access private
34
 *
35
 * @package    Elgg.Core
36
 * @subpackage Database
37
 * @since      1.10.0
38
 */
39
class EntityTable {
40
41
	use \Elgg\TimeUsing;
42
	
43
	/**
44
	 * @var Conf
45
	 */
46
	protected $config;
47
48
	/**
49
	 * @var Database
50
	 */
51
	protected $db;
52
53
	/**
54
	 * @var string
55
	 */
56
	protected $table;
57
58
	/**
59
	 * @var SubtypeTable
60
	 */
61
	protected $subtype_table;
62
63
	/**
64
	 * @var EntityCache
65
	 */
66
	protected $entity_cache;
67
68
	/**
69
	 * @var EntityPreloader
70
	 */
71
	protected $entity_preloader;
72
73
	/**
74
	 * @var MetadataCache
75
	 */
76
	protected $metadata_cache;
77
78
	/**
79
	 * @var EventsService
80
	 */
81
	protected $events;
82
83
	/**
84
	 * @var ElggSession
85
	 */
86
	protected $session;
87
88
	/**
89
	 * @var Translator
90
	 */
91
	protected $translator;
92
93
	/**
94
	 * @var Logger
95
	 */
96
	protected $logger;
97
98
	/**
99
	 * Constructor
100
	 *
101
	 * @param Conf          $config         Config
102
	 * @param Database      $db             Database
103
	 * @param EntityCache   $entity_cache   Entity cache
104
	 * @param MetadataCache $metadata_cache Metadata cache
105
	 * @param SubtypeTable  $subtype_table  Subtype table
106
	 * @param EventsService $events         Events service
107
	 * @param ElggSession   $session        Session
108
	 * @param Translator    $translator     Translator
109
	 * @param Logger        $logger         Logger
110
	 */
111 3715 View Code Duplication
	public function __construct(
112
		Conf $config,
113
		Database $db,
114
		EntityCache $entity_cache,
115
		MetadataCache $metadata_cache,
116
		SubtypeTable $subtype_table,
117
		EventsService $events,
118
		ElggSession $session,
119
		Translator $translator,
120
		Logger $logger
121
	) {
122 3715
		$this->config = $config;
123 3715
		$this->db = $db;
124 3715
		$this->table = $this->db->prefix . 'entities';
125 3715
		$this->entity_cache = $entity_cache;
126 3715
		$this->metadata_cache = $metadata_cache;
127 3715
		$this->subtype_table = $subtype_table;
128 3715
		$this->events = $events;
129 3715
		$this->session = $session;
130 3715
		$this->translator = $translator;
131 3715
		$this->logger = $logger;
132 3715
	}
133
134
	/**
135
	 * Returns a database row from the entities table.
136
	 *
137
	 * @see entity_row_to_elggstar()
138
	 *
139
	 * @tip Use get_entity() to return the fully loaded entity.
140
	 *
141
	 * @warning This will only return results if a) it exists, b) you have access to it.
142
	 * see {@link _elgg_get_access_where_sql()}.
143
	 *
144
	 * @param int $guid      The GUID of the object to extract
145
	 * @param int $user_guid GUID of the user accessing the row
146
	 *                       Defaults to logged in user if null
147
	 *                       Builds an access query for a logged out user if 0
148
	 * @return stdClass|false
149
	 * @access private
150
	 */
151 3715
	public function getRow($guid, $user_guid = null) {
152
153 3715
		if (!$guid) {
154
			return false;
155
		}
156
157 3715
		$access = _elgg_get_access_where_sql([
158 3715
			'table_alias' => '',
159 3715
			'user_guid' => $user_guid,
160
		]);
161
162 3715
		$sql = "SELECT * FROM {$this->db->prefix}entities
163 3715
			WHERE guid = :guid AND $access";
164
165
		$params = [
166 3715
			':guid' => (int) $guid,
167
		];
168
169 3715
		return $this->db->getDataRow($sql, null, $params);
170
	}
171
172
	/**
173
	 * Adds a new row to the entity table
174
	 *
175
	 * @param stdClass $row        Entity base information
176
	 * @param array    $attributes All primary and secondary table attributes
177
	 *                             Used by database mock services to allow mocking
178
	 *                             entities that were instantiated using new keyword
179
	 *                             and calling ElggEntity::save()
180
	 * @return int|false
181
	 */
182 210
	public function insertRow(stdClass $row, array $attributes = []) {
183
184 210
		$sql = "INSERT INTO {$this->db->prefix}entities
185
			(type, subtype, owner_guid, container_guid,
186
				access_id, time_created, time_updated, last_action)
187
			VALUES
188
			(:type, :subtype_id, :owner_guid, :container_guid,
189
				:access_id, :time_created, :time_updated, :last_action)";
190
191 210
		return $this->db->insertData($sql, [
192 210
			':type' => $row->type,
193 210
			':subtype_id' => $row->subtype_id,
194 210
			':owner_guid' => $row->owner_guid,
195 210
			':container_guid' => $row->container_guid,
196 210
			':access_id' => $row->access_id,
197 210
			':time_created' => $row->time_created,
198 210
			':time_updated' => $row->time_updated,
199 210
			':last_action' => $row->last_action,
200
		]);
201
	}
202
203
	/**
204
	 * Update entity table row
205
	 *
206
	 * @param int      $guid Entity guid
207
	 * @param stdClass $row  Updated data
208
	 * @return int|false
209
	 */
210 69
	public function updateRow($guid, stdClass $row) {
211
		$sql = "
212 69
			UPDATE {$this->db->prefix}entities
213
			SET owner_guid = :owner_guid,
214
			    access_id = :access_id,
215
				container_guid = :container_guid,
216
				time_created = :time_created,
217
				time_updated = :time_updated
218
			WHERE guid = :guid
219
		";
220
221
		$params = [
222 69
			':owner_guid' => $row->owner_guid,
223 69
			':access_id' => $row->access_id,
224 69
			':container_guid' => $row->container_guid,
225 69
			':time_created' => $row->time_created,
226 69
			':time_updated' => $row->time_updated,
227 69
			':guid' => $guid,
228
		];
229
230 69
		return $this->db->updateData($sql, false, $params);
231
	}
232
233
	/**
234
	 * Create an Elgg* object from a given entity row.
235
	 *
236
	 * Handles loading all tables into the correct class.
237
	 *
238
	 * @see get_entity_as_row()
239
	 * @see add_subtype()
240
	 * @see get_entity()
241
	 *
242
	 * @access private
243
	 *
244
	 * @param stdClass $row The row of the entry in the entities table.
245
	 * @return ElggEntity|false
246
	 * @throws ClassException
247
	 * @throws InstallationException
248
	 */
249 3715
	public function rowToElggStar($row) {
250 3715
		if (!$row instanceof stdClass) {
251
			return $row;
252
		}
253
254 3715
		if (!isset($row->guid) || !isset($row->subtype)) {
255
			return $row;
256
		}
257
	
258 3715
		$class_name = $this->subtype_table->getClassFromId($row->subtype);
259 3715
		if ($class_name && !class_exists($class_name)) {
260
			$this->logger->error("Class '$class_name' was not found, missing plugin?");
261
			$class_name = '';
262
		}
263
264 3715
		if (!$class_name) {
265
			$map = [
266 3715
				'object' => ElggObject::class,
267
				'user' => ElggUser::class,
268
				'group' => ElggGroup::class,
269
				'site' => ElggSite::class,
270
			];
271
272 3715
			if (isset($map[$row->type])) {
273 3715
				$class_name = $map[$row->type];
274
			} else {
275
				throw new InstallationException("Entity type {$row->type} is not supported.");
276
			}
277
		}
278
279 3715
		$entity = new $class_name($row);
280 3715
		if (!$entity instanceof ElggEntity) {
281
			throw new ClassException("$class_name must extend " . ElggEntity::class);
282
		}
283
284 3715
		return $entity;
285
	}
286
287
	/**
288
	 * Get an entity from the in-memory or memcache caches
289
	 *
290
	 * @param int $guid GUID
291
	 *
292
	 * @return \ElggEntity
293
	 */
294 3715
	protected function getFromCache($guid) {
295 3715
		$entity = $this->entity_cache->get($guid);
296 3715
		if ($entity) {
297 3574
			return $entity;
298
		}
299
300 3715
		$memcache = _elgg_get_memcache('new_entity_cache');
301 3715
		$entity = $memcache->load($guid);
302 3715
		if (!$entity instanceof ElggEntity) {
303 3715
			return false;
304
		}
305
306
		// Validate accessibility if from memcache
307 156
		if (!elgg_get_ignore_access() && !has_access_to_entity($entity)) {
308 1
			return false;
309
		}
310
311 156
		$this->entity_cache->set($entity);
312 156
		return $entity;
313
	}
314
315
	/**
316
	 * Loads and returns an entity object from a guid.
317
	 *
318
	 * @param int    $guid The GUID of the entity
319
	 * @param string $type The type of the entity. If given, even an existing entity with the given GUID
320
	 *                     will not be returned unless its type matches.
321
	 *
322
	 * @return ElggEntity|stdClass|false The correct Elgg or custom object based upon entity type and subtype
323
	 * @throws ClassException
324
	 * @throws InstallationException
325
	 */
326 3715
	public function get($guid, $type = '') {
327
		// We could also use: if (!(int) $guid) { return false },
328
		// but that evaluates to a false positive for $guid = true.
329
		// This is a bit slower, but more thorough.
330 3715
		if (!is_numeric($guid) || $guid === 0 || $guid === '0') {
331 39
			return false;
332
		}
333
334 3715
		$guid = (int) $guid;
335
336 3715
		$entity = $this->getFromCache($guid);
337 3715
		if ($entity && (!$type || elgg_instanceof($entity, $type))) {
338 3621
			return $entity;
339
		}
340
341 3715
		$row = $this->getRow($guid);
342 3715
		if (!$row) {
343 3432
			return false;
344
		}
345
346 346
		if ($type && $row->type != $type) {
347 1
			return false;
348
		}
349
350 346
		$entity = $this->rowToElggStar($row);
351
352 346
		if ($entity instanceof ElggEntity) {
353 346
			$entity->storeInPersistedCache(_elgg_get_memcache('new_entity_cache'));
354
		}
355
356 346
		return $entity;
357
	}
358
359
	/**
360
	 * Does an entity exist?
361
	 *
362
	 * This function checks for the existence of an entity independent of access
363
	 * permissions. It is useful for situations when a user cannot access an entity
364
	 * and it must be determined whether entity has been deleted or the access level
365
	 * has changed.
366
	 *
367
	 * @param int $guid The GUID of the entity
368
	 * @return bool
369
	 */
370 43
	public function exists($guid) {
371
372
		// need to ignore access and show hidden entities to check existence
373 43
		$ia = $this->session->setIgnoreAccess(true);
374 43
		$show_hidden = access_show_hidden_entities(true);
375
376 43
		$result = $this->getRow($guid);
377
378 43
		$this->session->setIgnoreAccess($ia);
379 43
		access_show_hidden_entities($show_hidden);
380
381 43
		return !empty($result);
382
	}
383
384
	/**
385
	 * Enable an entity.
386
	 *
387
	 * @param int  $guid      GUID of entity to enable
388
	 * @param bool $recursive Recursively enable all entities disabled with the entity?
389
	 * @return bool
390
	 */
391
	public function enable($guid, $recursive = true) {
392
393
		// Override access only visible entities
394
		$old_access_status = access_get_show_hidden_status();
395
		access_show_hidden_entities(true);
396
397
		$result = false;
398
		$entity = get_entity($guid);
399
		if ($entity) {
400
			$result = $entity->enable($recursive);
401
		}
402
403
		access_show_hidden_entities($old_access_status);
404
		return $result;
405
	}
406
407
	/**
408
	 * Returns an array of entities with optional filtering.
409
	 *
410
	 * Entities are the basic unit of storage in Elgg.  This function
411
	 * provides the simplest way to get an array of entities.  There
412
	 * are many options available that can be passed to filter
413
	 * what sorts of entities are returned.
414
	 *
415
	 * @tip To output formatted strings of entities, use {@link elgg_list_entities()} and
416
	 * its cousins.
417
	 *
418
	 * @tip Plural arguments can be written as singular if only specifying a
419
	 * single element.  ('type' => 'object' vs 'types' => array('object')).
420
	 *
421
	 * @see elgg_get_entities_from_metadata()
422
	 * @see elgg_get_entities_from_relationship()
423
	 * @see elgg_get_entities_from_access_id()
424
	 * @see elgg_get_entities_from_annotations()
425
	 * @see elgg_list_entities()
426
	 *
427
	 * @param array $options Array in format:
428
	 *
429
	 * 	types => null|STR entity type (type IN ('type1', 'type2')
430
	 *           Joined with subtypes by AND. See below)
431
	 *
432
	 * 	subtypes => null|STR entity subtype (SQL: subtype IN ('subtype1', 'subtype2))
433
	 *              Use ELGG_ENTITIES_NO_VALUE to match the default subtype.
434
	 *              Use ELGG_ENTITIES_ANY_VALUE to match any subtype.
435
	 *
436
	 * 	type_subtype_pairs => null|ARR (array('type' => 'subtype'))
437
	 *                        array(
438
	 *                            'object' => array('blog', 'file'), // All objects with subtype of 'blog' or 'file'
439
	 *                            'user' => ELGG_ENTITY_ANY_VALUE, // All users irrespective of subtype
440
	 *                        );
441
	 *
442
	 * 	guids => null|ARR Array of entity guids
443
	 *
444
	 * 	owner_guids => null|ARR Array of owner guids
445
	 *
446
	 * 	container_guids => null|ARR Array of container_guids
447
	 *
448
	 * 	order_by => null (time_created desc)|STR SQL order by clause
449
	 *
450
	 *  reverse_order_by => BOOL Reverse the default order by clause
451
	 *
452
	 * 	limit => null (10)|INT SQL limit clause (0 means no limit)
453
	 *
454
	 * 	offset => null (0)|INT SQL offset clause
455
	 *
456
	 * 	created_time_lower => null|INT Created time lower boundary in epoch time
457
	 *
458
	 * 	created_time_upper => null|INT Created time upper boundary in epoch time
459
	 *
460
	 * 	modified_time_lower => null|INT Modified time lower boundary in epoch time
461
	 *
462
	 * 	modified_time_upper => null|INT Modified time upper boundary in epoch time
463
	 *
464
	 * 	count => true|false return a count instead of entities
465
	 *
466
	 * 	wheres => array() Additional where clauses to AND together
467
	 *
468
	 * 	joins => array() Additional joins
469
	 *
470
	 * 	preload_owners => bool (false) If set to true, this function will preload
471
	 * 					  all the owners of the returned entities resulting in better
472
	 * 					  performance if those owners need to be displayed
473
	 *
474
	 *  preload_containers => bool (false) If set to true, this function will preload
475
	 * 					      all the containers of the returned entities resulting in better
476
	 * 					      performance if those containers need to be displayed
477
	 *
478
	 *
479
	 * 	callback => string A callback function to pass each row through
480
	 *
481
	 * 	distinct => bool (true) If set to false, Elgg will drop the DISTINCT clause from
482
	 * 				the MySQL query, which will improve performance in some situations.
483
	 * 				Avoid setting this option without a full understanding of the underlying
484
	 * 				SQL query Elgg creates.
485
	 *
486
	 *  batch => bool (false) If set to true, an Elgg\BatchResult object will be returned instead of an array.
487
	 *           Since 2.3
488
	 *
489
	 *  batch_inc_offset => bool (true) If "batch" is used, this tells the batch to increment the offset
490
	 *                      on each fetch. This must be set to false if you delete the batched results.
491
	 *
492
	 *  batch_size => int (25) If "batch" is used, this is the number of entities/rows to pull in before
493
	 *                requesting more.
494
	 *
495
	 * @return \ElggEntity[]|int|mixed If count, int. Otherwise an array or an Elgg\BatchResult. false on errors.
496
	 *
497
	 * @see elgg_get_entities_from_metadata()
498
	 * @see elgg_get_entities_from_relationship()
499
	 * @see elgg_get_entities_from_access_id()
500
	 * @see elgg_get_entities_from_annotations()
501
	 * @see elgg_list_entities()
502
	 */
503 300
	public function getEntities(array $options = []) {
504 300
		_elgg_check_unsupported_site_guid($options);
505
506
		$defaults = [
507 300
			'types'                 => ELGG_ENTITIES_ANY_VALUE,
508 300
			'subtypes'              => ELGG_ENTITIES_ANY_VALUE,
509 300
			'type_subtype_pairs'    => ELGG_ENTITIES_ANY_VALUE,
510
511 300
			'guids'                 => ELGG_ENTITIES_ANY_VALUE,
512 300
			'owner_guids'           => ELGG_ENTITIES_ANY_VALUE,
513 300
			'container_guids'       => ELGG_ENTITIES_ANY_VALUE,
514
515 300
			'modified_time_lower'   => ELGG_ENTITIES_ANY_VALUE,
516 300
			'modified_time_upper'   => ELGG_ENTITIES_ANY_VALUE,
517 300
			'created_time_lower'    => ELGG_ENTITIES_ANY_VALUE,
518 300
			'created_time_upper'    => ELGG_ENTITIES_ANY_VALUE,
519
520
			'reverse_order_by'      => false,
521 300
			'order_by'              => 'e.time_created desc',
522 300
			'group_by'              => ELGG_ENTITIES_ANY_VALUE,
523 300
			'limit'                 => $this->config->default_limit,
524 300
			'offset'                => 0,
525
			'count'                 => false,
526
			'selects'               => [],
527
			'wheres'                => [],
528
			'joins'                 => [],
529
530
			'preload_owners'        => false,
531
			'preload_containers'    => false,
532 300
			'callback'              => 'entity_row_to_elggstar',
533
			'distinct'              => true,
534
535
			'batch'                 => false,
536
			'batch_inc_offset'      => true,
537 300
			'batch_size'            => 25,
538
539
			// private API
540
			'__ElggBatch'           => null,
541
		];
542
543 300
		$options = array_merge($defaults, $options);
544
545 300 View Code Duplication
		if ($options['batch'] && !$options['count']) {
546 2
			$batch_size = $options['batch_size'];
547 2
			$batch_inc_offset = $options['batch_inc_offset'];
548
549
			// clean batch keys from $options.
550 2
			unset($options['batch'], $options['batch_size'], $options['batch_inc_offset']);
551
552 2
			return new \ElggBatch([$this, 'getEntities'], $options, null, $batch_size, $batch_inc_offset);
553
		}
554
	
555
		// can't use helper function with type_subtype_pair because
556
		// it's already an array...just need to merge it
557 300 View Code Duplication
		if (isset($options['type_subtype_pair'])) {
558
			if (isset($options['type_subtype_pairs'])) {
559
				$options['type_subtype_pairs'] = array_merge($options['type_subtype_pairs'],
560
					$options['type_subtype_pair']);
561
			} else {
562
				$options['type_subtype_pairs'] = $options['type_subtype_pair'];
563
			}
564
		}
565
566 300
		$singulars = ['type', 'subtype', 'guid', 'owner_guid', 'container_guid'];
567 300
		$options = _elgg_normalize_plural_options_array($options, $singulars);
568
569 300
		$options = $this->autoJoinTables($options);
570
571
		// evaluate where clauses
572 300
		if (!is_array($options['wheres'])) {
573
			$options['wheres'] = [$options['wheres']];
574
		}
575
576 300
		$wheres = $options['wheres'];
577
578 300
		$wheres[] = $this->getEntityTypeSubtypeWhereSql('e', $options['types'],
579 300
			$options['subtypes'], $options['type_subtype_pairs']);
580
581 300
		$wheres[] = $this->getGuidBasedWhereSql('e.guid', $options['guids']);
582 300
		$wheres[] = $this->getGuidBasedWhereSql('e.owner_guid', $options['owner_guids']);
583 300
		$wheres[] = $this->getGuidBasedWhereSql('e.container_guid', $options['container_guids']);
584
585 300
		$wheres[] = $this->getEntityTimeWhereSql('e', $options['created_time_upper'],
586 300
			$options['created_time_lower'], $options['modified_time_upper'], $options['modified_time_lower']);
587
588
		// see if any functions failed
589
		// remove empty strings on successful functions
590 300 View Code Duplication
		foreach ($wheres as $i => $where) {
591 300
			if ($where === false) {
592 69
				return false;
593
			} elseif (empty($where)) {
594 300
				unset($wheres[$i]);
595
			}
596
		}
597
598
		// remove identical where clauses
599 299
		$wheres = array_unique($wheres);
600
601
		// evaluate join clauses
602 299 View Code Duplication
		if (!is_array($options['joins'])) {
603
			$options['joins'] = [$options['joins']];
604
		}
605
606
		// remove identical join clauses
607 299
		$joins = array_unique($options['joins']);
608
609 299 View Code Duplication
		foreach ($joins as $i => $join) {
610 297
			if ($join === false) {
611
				return false;
612
			} elseif (empty($join)) {
613 297
				unset($joins[$i]);
614
			}
615
		}
616
617
		// evalutate selects
618 299
		if ($options['selects']) {
619 297
			$selects = '';
620 297
			foreach ($options['selects'] as $select) {
621 297
				$selects .= ", $select";
622
			}
623
		} else {
624 299
			$selects = '';
625
		}
626
627 299
		if (!$options['count']) {
628 299
			$distinct = $options['distinct'] ? "DISTINCT" : "";
629 299
			$query = "SELECT $distinct e.*{$selects} FROM {$this->db->prefix}entities e ";
630
		} else {
631
			// note: when DISTINCT unneeded, it's slightly faster to compute COUNT(*) than GUIDs
632 13
			$count_expr = $options['distinct'] ? "DISTINCT e.guid" : "*";
633 13
			$query = "SELECT COUNT($count_expr) as total FROM {$this->db->prefix}entities e ";
634
		}
635
636
		// add joins
637 299
		foreach ($joins as $j) {
638 297
			$query .= " $j ";
639
		}
640
641
		// add wheres
642 299
		$query .= ' WHERE ';
643
644 299
		foreach ($wheres as $w) {
645 299
			$query .= " $w AND ";
646
		}
647
648
		// Add access controls
649 299
		$query .= _elgg_get_access_where_sql();
650
651
		// reverse order by
652 299
		if ($options['reverse_order_by']) {
653
			$options['order_by'] = _elgg_sql_reverse_order_by_clause($options['order_by']);
654
		}
655
656 299
		if ($options['count']) {
657 13
			$total = $this->db->getDataRow($query);
658 13
			return (int) $total->total;
659
		}
660
661 299
		if ($options['group_by']) {
662 12
			$query .= " GROUP BY {$options['group_by']}";
663
		}
664
665 299
		if ($options['order_by']) {
666 299
			$query .= " ORDER BY {$options['order_by']}";
667
		}
668
669 299 View Code Duplication
		if ($options['limit']) {
670 299
			$limit = sanitise_int($options['limit'], false);
671 299
			$offset = sanitise_int($options['offset'], false);
672 299
			$query .= " LIMIT $offset, $limit";
673
		}
674
675 299
		if ($options['callback'] === 'entity_row_to_elggstar') {
676 299
			$results = $this->fetchFromSql($query, $options['__ElggBatch']);
677
		} else {
678 7
			$results = $this->db->getData($query, $options['callback']);
679
		}
680
681 299
		if (!$results) {
682
			// no results, no preloading
683 208
			return $results;
684
		}
685
686
		// populate entity and metadata caches, and prepare $entities for preloader
687 297
		$guids = [];
688 297
		foreach ($results as $item) {
689
			// A custom callback could result in items that aren't \ElggEntity's, so check for them
690 297
			if ($item instanceof ElggEntity) {
691 297
				$this->entity_cache->set($item);
692
				// plugins usually have only settings
693 297
				if (!$item instanceof ElggPlugin) {
694 297
					$guids[] = $item->guid;
695
				}
696
			}
697
		}
698
		// @todo Without this, recursive delete fails. See #4568
699 297
		reset($results);
700
701 297
		if ($guids) {
702
			// there were entities in the result set, preload metadata for them
703 297
			$this->metadata_cache->populateFromEntities($guids);
704
		}
705
706 297
		if (count($results) > 1) {
707 297
			$props_to_preload = [];
708 297
			if ($options['preload_owners']) {
709 1
				$props_to_preload[] = 'owner_guid';
710
			}
711 297
			if ($options['preload_containers']) {
712
				$props_to_preload[] = 'container_guid';
713
			}
714 297
			if ($props_to_preload) {
715
				// note, ElggEntityPreloaderIntegrationTest assumes it can swap out
716
				// the preloader after boot. If you inject this component at construction
717
				// time that unit test will break. :/
718 1
				_elgg_services()->entityPreloader->preload($results, $props_to_preload);
719
			}
720
		}
721
722 297
		return $results;
723
	}
724
725
	/**
726
	 * Decorate getEntities() options in order to auto-join secondary tables where it's
727
	 * safe to do so.
728
	 *
729
	 * @param array $options Options array in getEntities() after normalization
730
	 * @return array
731
	 */
732 300
	protected function autoJoinTables(array $options) {
733
		// we must be careful that the query doesn't specify any options that may join
734
		// tables or change the selected columns
735 300
		if (!is_array($options['types'])
736 298
				|| count($options['types']) !== 1
737 298
				|| !empty($options['selects'])
738 124
				|| !empty($options['wheres'])
739 43
				|| !empty($options['joins'])
740 43
				|| $options['callback'] !== 'entity_row_to_elggstar'
741 300
				|| $options['count']) {
742
			// Too dangerous to auto-join
743 300
			return $options;
744
		}
745
746
		$join_types = [
747
			// Each class must have a static getExternalAttributes() : array
748 42
			'user' => 'ElggUser',
749
		];
750
751
		// We use reset() because $options['types'] may not have a numeric key
752 42
		$type = reset($options['types']);
753
754
		// Get the columns we'll need to select. We can't use st.* because the order_by
755
		// clause may reference "guid", which MySQL will complain about being ambiguous
756
		try {
757 42
			$attributes = \ElggEntity::getExtraAttributeDefaults($type);
758 38
			if (empty($attributes)) {
759 38
				return $options;
760
			}
761 4
		} catch (\Exception $e) {
762 4
			$this->logger->error("Unrecognized type: $type");
763 4
			return $options;
764
		}
765
766 11
		foreach (array_keys($attributes) as $col) {
767 11
			$options['selects'][] = "st.$col";
768
		}
769
770
		// join the secondary table
771 11
		$options['joins'][] = "JOIN {$this->db->prefix}{$type}s_entity st ON (e.guid = st.guid)";
772
773 11
		return $options;
774
	}
775
776
	/**
777
	 * Return entities from an SQL query generated by elgg_get_entities.
778
	 *
779
	 * @access private
780
	 *
781
	 * @param string    $sql
782
	 * @param ElggBatch $batch
783
	 * @return ElggEntity[]
784
	 * @throws LogicException
785
	 */
786 299
	public function fetchFromSql($sql, \ElggBatch $batch = null) {
787 299
		$plugin_subtype = $this->subtype_table->getId('object', 'plugin');
788
789
		// Keys are types, values are columns that, if present, suggest that the secondary
790
		// table is already JOINed. Note it's OK if guess incorrectly because entity load()
791
		// will fetch any missing attributes.
792
		$types_to_optimize = [
793 299
			'user' => 'password_hash',
794
		];
795
796 299
		$rows = $this->db->getData($sql);
797
798
		// guids to look up in each type
799 299
		$lookup_types = [];
800
		// maps GUIDs to the $rows key
801 299
		$guid_to_key = [];
802
803 299
		if (isset($rows[0]->type, $rows[0]->subtype)
804 299
				&& $rows[0]->type === 'object'
805 299
				&& $rows[0]->subtype == $plugin_subtype) {
806
			// Likely the entire resultset is plugins, which have already been optimized
807
			// to JOIN the secondary table. In this case we allow retrieving from cache,
808
			// but abandon the extra queries.
809 297
			$types_to_optimize = [];
810
		}
811
812
		// First pass: use cache where possible, gather GUIDs that we're optimizing
813 299
		foreach ($rows as $i => $row) {
814 297
			if (empty($row->guid) || empty($row->type)) {
815
				throw new LogicException('Entity row missing guid or type');
816
			}
817
818
			// We try ephemeral cache because it's blazingly fast and we ideally want to access
819
			// the same PHP instance. We don't try memcache because it isn't worth the overhead.
820 297
			$entity = $this->entity_cache->get($row->guid);
821 297
			if ($entity) {
822
				// from static var, must be refreshed in case row has extra columns
823 36
				$entity->refresh($row);
824 36
				$rows[$i] = $entity;
825 36
				continue;
826
			}
827
828 297
			if (isset($types_to_optimize[$row->type])) {
829
				// check if row already looks JOINed.
830 297
				if (isset($row->{$types_to_optimize[$row->type]})) {
831
					// Row probably already contains JOINed secondary table. Don't make another query just
832
					// to pull data that's already there
833 11
					continue;
834
				}
835 297
				$lookup_types[$row->type][] = $row->guid;
836 297
				$guid_to_key[$row->guid] = $i;
837
			}
838
		}
839
		// Do secondary queries and merge rows
840 299
		if ($lookup_types) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lookup_types 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...
841 297
			foreach ($lookup_types as $type => $guids) {
842 297
				$set = "(" . implode(',', $guids) . ")";
843 297
				$sql = "SELECT * FROM {$this->db->prefix}{$type}s_entity WHERE guid IN $set";
844 297
				$secondary_rows = $this->db->getData($sql);
845 297
				if ($secondary_rows) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $secondary_rows 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...
846 297
					foreach ($secondary_rows as $secondary_row) {
847 297
						$key = $guid_to_key[$secondary_row->guid];
848
						// cast to arrays to merge then cast back
849 297
						$rows[$key] = (object) array_merge((array) $rows[$key], (array) $secondary_row);
850
					}
851
				}
852
			}
853
		}
854
		// Second pass to finish conversion
855 299
		foreach ($rows as $i => $row) {
856 297
			if ($row instanceof ElggEntity) {
857 36
				continue;
858
			} else {
859
				try {
860 297
					$rows[$i] = $this->rowToElggStar($row);
861
				} catch (IncompleteEntityException $e) {
862
					// don't let incomplete entities throw fatal errors
863
					unset($rows[$i]);
864
865
					// report incompletes to the batch process that spawned this query
866
					if ($batch) {
867 297
						$batch->reportIncompleteEntity($row);
868
					}
869
				}
870
			}
871
		}
872 299
		return $rows;
873
	}
874
875
	/**
876
	 * Returns SQL where clause for type and subtype on main entity table
877
	 *
878
	 * @param string     $table    Entity table prefix as defined in SELECT...FROM entities $table
879
	 * @param null|array $types    Array of types or null if none.
880
	 * @param null|array $subtypes Array of subtypes or null if none
881
	 * @param null|array $pairs    Array of pairs of types and subtypes
882
	 *
883
	 * @return false|string
884
	 * @access private
885
	 */
886 3701
	public function getEntityTypeSubtypeWhereSql($table, $types, $subtypes, $pairs) {
887
		// subtype depends upon type.
888 3701
		if ($subtypes && !$types) {
889
			$this->logger->warn("Cannot set subtypes without type.");
890
			return false;
891
		}
892
893
		// short circuit if nothing is requested
894 3701
		if (!$types && !$subtypes && !$pairs) {
895 3701
			return '';
896
		}
897
898
		// pairs override
899 298
		$wheres = [];
900 298
		if (!is_array($pairs)) {
901 298
			if (!is_array($types)) {
902 83
				$types = [$types];
903
			}
904
905 298
			if ($subtypes && !is_array($subtypes)) {
906
				$subtypes = [$subtypes];
907
			}
908
909
			// decrementer for valid types.  Return false if no valid types
910 298
			$valid_types_count = count($types);
911 298
			$valid_subtypes_count = 0;
912
			// remove invalid types to get an accurate count of
913
			// valid types for the invalid subtype detection to use
914
			// below.
915
			// also grab the count of ALL subtypes on valid types to decrement later on
916
			// and check against.
917
			//
918
			// yes this is duplicating a foreach on $types.
919 298
			foreach ($types as $type) {
920 298
				if (!in_array($type, \Elgg\Config::getEntityTypes())) {
921 10
					$valid_types_count--;
922 10
					unset($types[array_search($type, $types)]);
923
				} else {
924
					// do the checking (and decrementing) in the subtype section.
925 298
					$valid_subtypes_count += count($subtypes);
926
				}
927
			}
928
929
			// return false if nothing is valid.
930 298
			if (!$valid_types_count) {
931 8
				return false;
932
			}
933
934
			// subtypes are based upon types, so we need to look at each
935
			// type individually to get the right subtype id.
936 298
			foreach ($types as $type) {
937 298
				$subtype_ids = [];
938 298
				if ($subtypes) {
939 298
					foreach ($subtypes as $subtype) {
940
						// check that the subtype is valid
941 298
						if (!$subtype && ELGG_ENTITIES_NO_VALUE === $subtype) {
942
							// subtype value is 0
943 2
							$subtype_ids[] = ELGG_ENTITIES_NO_VALUE;
944 298
						} elseif (!$subtype) {
945
							// subtype is ignored.
946
							// this handles ELGG_ENTITIES_ANY_VALUE, '', and anything falsy that isn't 0
947 1
							continue;
948
						} else {
949 298
							$subtype_id = get_subtype_id($type, $subtype);
950
951 298
							if ($subtype_id) {
952 297
								$subtype_ids[] = $subtype_id;
953
							} else {
954 57
								$valid_subtypes_count--;
955 57
								$this->logger->notice("Type-subtype '$type:$subtype' does not exist!");
956 298
								continue;
957
							}
958
						}
959
					}
960
961
					// return false if we're all invalid subtypes in the only valid type
962 298
					if ($valid_subtypes_count <= 0) {
963 56
						return false;
964
					}
965
				}
966
967 297
				if (is_array($subtype_ids) && count($subtype_ids)) {
968 297
					$subtype_ids_str = implode(',', $subtype_ids);
969 297
					$wheres[] = "({$table}.type = '$type' AND {$table}.subtype IN ($subtype_ids_str))";
970
				} else {
971 297
					$wheres[] = "({$table}.type = '$type')";
972
				}
973
			}
974
		} else {
975
			// using type/subtype pairs
976 8
			$valid_pairs_count = count($pairs);
977 8
			$valid_pairs_subtypes_count = 0;
978
979
			// same deal as above--we need to know how many valid types
980
			// and subtypes we have before hitting the subtype section.
981
			// also normalize the subtypes into arrays here.
982 8
			foreach ($pairs as $paired_type => $paired_subtypes) {
983 8
				if (!in_array($paired_type, \Elgg\Config::getEntityTypes())) {
984 5
					$valid_pairs_count--;
985 5
					unset($pairs[array_search($paired_type, $pairs)]);
986
				} else {
987 3
					if ($paired_subtypes && !is_array($paired_subtypes)) {
988 1
						$pairs[$paired_type] = [$paired_subtypes];
989
					}
990 8
					$valid_pairs_subtypes_count += count($paired_subtypes);
991
				}
992
			}
993
994 8
			if ($valid_pairs_count <= 0) {
995 5
				return false;
996
			}
997 3
			foreach ($pairs as $paired_type => $paired_subtypes) {
998
				// this will always be an array because of line 2027, right?
999
				// no...some overly clever person can say pair => array('object' => null)
1000 3
				if (is_array($paired_subtypes)) {
1001 3
					$paired_subtype_ids = [];
1002 3
					foreach ($paired_subtypes as $paired_subtype) {
1003 3
						if (ELGG_ENTITIES_NO_VALUE === $paired_subtype || ($paired_subtype_id = get_subtype_id($paired_type, $paired_subtype))) {
1004 3
							$paired_subtype_ids[] = (ELGG_ENTITIES_NO_VALUE === $paired_subtype) ?
1005 3
									ELGG_ENTITIES_NO_VALUE : $paired_subtype_id;
1006
						} else {
1007 1
							$valid_pairs_subtypes_count--;
1008 1
							$this->logger->notice("Type-subtype '$paired_type:$paired_subtype' does not exist!");
1009
							// return false if we're all invalid subtypes in the only valid type
1010 3
							continue;
1011
						}
1012
					}
1013
1014
					// return false if there are no valid subtypes.
1015 3
					if ($valid_pairs_subtypes_count <= 0) {
1016
						return false;
1017
					}
1018
1019
1020 3
					if ($paired_subtype_ids_str = implode(',', $paired_subtype_ids)) {
1021 3
						$wheres[] = "({$table}.type = '$paired_type'"
1022 3
								. " AND {$table}.subtype IN ($paired_subtype_ids_str))";
1023
					}
1024
				} else {
1025 3
					$wheres[] = "({$table}.type = '$paired_type')";
1026
				}
1027
			}
1028
		}
1029
1030
		// pairs override the above.  return false if they don't exist.
1031 297 View Code Duplication
		if (is_array($wheres) && count($wheres)) {
1032 297
			$where = implode(' OR ', $wheres);
1033 297
			return "($where)";
1034
		}
1035
1036
		return '';
1037
	}
1038
1039
	/**
1040
	 * Returns SQL where clause for owner and containers.
1041
	 *
1042
	 * @param string     $column Column name the guids should be checked against. Usually
1043
	 *                           best to provide in table.column format.
1044
	 * @param null|array $guids  Array of GUIDs.
1045
	 *
1046
	 * @return false|string
1047
	 * @access private
1048
	 */
1049 3701
	public function getGuidBasedWhereSql($column, $guids) {
1050
		// short circuit if nothing requested
1051
		// 0 is a valid guid
1052 3701
		if (!$guids && $guids !== 0) {
1053 3701
			return '';
1054
		}
1055
1056
		// normalize and sanitise owners
1057 3628
		if (!is_array($guids)) {
1058 5
			$guids = [$guids];
1059
		}
1060
1061 3628
		$guids_sanitized = [];
1062 3628
		foreach ($guids as $guid) {
1063 3628
			if ($guid !== ELGG_ENTITIES_NO_VALUE) {
1064 3628
				$guid = sanitise_int($guid);
1065
1066 3628
				if (!$guid) {
1067 1
					return false;
1068
				}
1069
			}
1070 3628
			$guids_sanitized[] = $guid;
1071
		}
1072
1073 3628
		$where = '';
1074 3628
		$guid_str = implode(',', $guids_sanitized);
1075
1076
		// implode(',', 0) returns 0.
1077 3628
		if ($guid_str !== false && $guid_str !== '') {
1078 3628
			$where = "($column IN ($guid_str))";
1079
		}
1080
1081 3628
		return $where;
1082
	}
1083
1084
	/**
1085
	 * Returns SQL where clause for entity time limits.
1086
	 *
1087
	 * @param string   $table              Entity table prefix as defined in
1088
	 *                                     SELECT...FROM entities $table
1089
	 * @param null|int $time_created_upper Time created upper limit
1090
	 * @param null|int $time_created_lower Time created lower limit
1091
	 * @param null|int $time_updated_upper Time updated upper limit
1092
	 * @param null|int $time_updated_lower Time updated lower limit
1093
	 *
1094
	 * @return false|string false on fail, string on success.
1095
	 * @access private
1096
	 */
1097 3701
	public function getEntityTimeWhereSql($table, $time_created_upper = null,
1098
	$time_created_lower = null, $time_updated_upper = null, $time_updated_lower = null) {
1099
1100 3701
		$wheres = [];
1101
1102
		// exploit PHP's loose typing (quack) to check that they are INTs and not str cast to 0
1103 3701
		if ($time_created_upper && $time_created_upper == sanitise_int($time_created_upper)) {
1104 1
			$wheres[] = "{$table}.time_created <= $time_created_upper";
1105
		}
1106
1107 3701
		if ($time_created_lower && $time_created_lower == sanitise_int($time_created_lower)) {
1108 1
			$wheres[] = "{$table}.time_created >= $time_created_lower";
1109
		}
1110
1111 3701
		if ($time_updated_upper && $time_updated_upper == sanitise_int($time_updated_upper)) {
1112
			$wheres[] = "{$table}.time_updated <= $time_updated_upper";
1113
		}
1114
1115 3701
		if ($time_updated_lower && $time_updated_lower == sanitise_int($time_updated_lower)) {
1116
			$wheres[] = "{$table}.time_updated >= $time_updated_lower";
1117
		}
1118
1119 3701 View Code Duplication
		if (is_array($wheres) && count($wheres) > 0) {
1120 1
			$where_str = implode(' AND ', $wheres);
1121 1
			return "($where_str)";
1122
		}
1123
1124 3701
		return '';
1125
	}
1126
1127
	/**
1128
	 * Returns a list of months in which entities were updated or created.
1129
	 *
1130
	 * @tip Use this to generate a list of archives by month for when entities were added or updated.
1131
	 *
1132
	 * @todo document how to pass in array for $subtype
1133
	 *
1134
	 * @warning Months are returned in the form YYYYMM.
1135
	 *
1136
	 * @param string $type           The type of entity
1137
	 * @param string $subtype        The subtype of entity
1138
	 * @param int    $container_guid The container GUID that the entities belong to
1139
	 * @param string $order_by       Order_by SQL order by clause
1140
	 *
1141
	 * @return array|false Either an array months as YYYYMM, or false on failure
1142
	 */
1143
	public function getDates($type = '', $subtype = '', $container_guid = 0, $order_by = 'time_created') {
1144
1145
		$where = [];
1146
1147
		if ($type != "") {
1148
			$type = sanitise_string($type);
1149
			$where[] = "type='$type'";
1150
		}
1151
1152
		if (is_array($subtype)) {
1153
			$tempwhere = "";
1154
			if (sizeof($subtype)) {
1155
				foreach ($subtype as $typekey => $subtypearray) {
1156
					foreach ($subtypearray as $subtypeval) {
1157
						$typekey = sanitise_string($typekey);
1158
						if (!empty($subtypeval)) {
1159
							if (!$subtypeval = (int) get_subtype_id($typekey, $subtypeval)) {
1160
								return false;
1161
							}
1162
						} else {
1163
							$subtypeval = 0;
1164
						}
1165
						if (!empty($tempwhere)) {
1166
							$tempwhere .= " or ";
1167
						}
1168
						$tempwhere .= "(type = '{$typekey}' and subtype = {$subtypeval})";
1169
					}
1170
				}
1171
			}
1172
			if (!empty($tempwhere)) {
1173
				$where[] = "({$tempwhere})";
1174
			}
1175
		} else {
1176
			if ($subtype) {
1177
				if (!$subtype_id = get_subtype_id($type, $subtype)) {
1178
					return false;
1179
				} else {
1180
					$where[] = "subtype=$subtype_id";
1181
				}
1182
			}
1183
		}
1184
1185
		if ($container_guid !== 0) {
1186
			if (is_array($container_guid)) {
1187
				foreach ($container_guid as $key => $val) {
1188
					$container_guid[$key] = (int) $val;
1189
				}
1190
				$where[] = "container_guid in (" . implode(",", $container_guid) . ")";
1191
			} else {
1192
				$container_guid = (int) $container_guid;
1193
				$where[] = "container_guid = {$container_guid}";
1194
			}
1195
		}
1196
1197
		$where[] = _elgg_get_access_where_sql(['table_alias' => '']);
1198
1199
		$sql = "SELECT DISTINCT EXTRACT(YEAR_MONTH FROM FROM_UNIXTIME(time_created)) AS yearmonth
1200
			FROM {$this->db->prefix}entities where ";
1201
1202
		foreach ($where as $w) {
1203
			$sql .= " $w and ";
1204
		}
1205
1206
		$sql .= "1=1 ORDER BY $order_by";
1207
		if ($result = $this->db->getData($sql)) {
1208
			$endresult = [];
1209
			foreach ($result as $res) {
1210
				$endresult[] = $res->yearmonth;
1211
			}
1212
			return $endresult;
1213
		}
1214
		return false;
1215
	}
1216
1217
	/**
1218
	 * Update the last_action column in the entities table for $guid.
1219
	 *
1220
	 * @warning This is different to time_updated.  Time_updated is automatically set,
1221
	 * while last_action is only set when explicitly called.
1222
	 *
1223
	 * @param ElggEntity $entity Entity annotation|relationship action carried out on
1224
	 * @param int        $posted Timestamp of last action
1225
	 * @return int
1226
	 * @access private
1227
	 */
1228 19
	public function updateLastAction(ElggEntity $entity, $posted = null) {
1229
1230 19
		if (!$posted) {
1231 2
			$posted = $this->getCurrentTime()->getTimestamp();
1232
		}
1233
		
1234
		$query = "
1235 19
			UPDATE {$this->db->prefix}entities
1236
			SET last_action = :last_action
1237
			WHERE guid = :guid
1238
		";
1239
1240
		$params = [
1241 19
			':last_action' => (int) $posted,
1242 19
			':guid' => (int) $entity->guid,
1243
		];
1244
		
1245 19
		$this->db->updateData($query, true, $params);
1246
1247 19
		return (int) $posted;
1248
	}
1249
1250
	/**
1251
	 * Get a user by GUID even if the entity is hidden or disabled
1252
	 *
1253
	 * @param int $guid User GUID. Default is logged in user
1254
	 *
1255
	 * @return ElggUser|false
1256
	 * @throws UserFetchFailureException
1257
	 * @access private
1258
	 */
1259 3715
	public function getUserForPermissionsCheck($guid = 0) {
1260 3715
		if (!$guid) {
1261 3701
			return $this->session->getLoggedInUser();
1262
		}
1263
1264
		// need to ignore access and show hidden entities for potential hidden/disabled users
1265 3624
		$ia = $this->session->setIgnoreAccess(true);
1266 3624
		$show_hidden = access_show_hidden_entities(true);
1267
	
1268 3624
		$user = $this->get($guid, 'user');
1269
1270 3624
		$this->session->setIgnoreAccess($ia);
1271 3624
		access_show_hidden_entities($show_hidden);
1272
1273 3624
		if (!$user) {
1274
			// requested to check access for a specific user_guid, but there is no user entity, so the caller
1275
			// should cancel the check and return false
1276 3418
			$message = $this->translator->translate('UserFetchFailureException', [$guid]);
1277
			// $this->logger->warn($message);
1278
1279 3418
			throw new UserFetchFailureException($message);
1280
		}
1281
1282 367
		return $user;
1283
	}
1284
1285
	/**
1286
	 * Disables all entities owned and contained by a user (or another entity)
1287
	 *
1288
	 * @param int $owner_guid The owner GUID
1289
	 * @return bool
1290
	 */
1291
	public function disableEntities($owner_guid) {
1292
		$entity = get_entity($owner_guid);
1293
		if (!$entity || !$entity->canEdit()) {
1294
			return false;
1295
		}
1296
1297
		if (!$this->events->trigger('disable', $entity->type, $entity)) {
1298
			return false;
1299
		}
1300
1301
		$query = "
1302
			UPDATE {$this->table}entities
1303
			SET enabled='no'
1304
			WHERE owner_guid = :owner_guid
1305
			OR container_guid = :owner_guid";
1306
1307
		$params = [
1308
			':owner_guid' => (int) $owner_guid,
1309
		];
1310
1311
		_elgg_invalidate_cache_for_entity($entity->guid);
1312
		_elgg_invalidate_memcache_for_entity($entity->guid);
1313
		
1314
		if ($this->db->updateData($query, true, $params)) {
1315
			return true;
1316
		}
1317
1318
		return false;
1319
	}
1320
1321
}
1322