Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/ElggEntity.php (1 issue)

return values of null are not assigned.

Bug Minor
1
<?php
2
3
use Elgg\EntityIcon;
4
5
/**
6
 * The parent class for all Elgg Entities.
7
 *
8
 * An \ElggEntity is one of the basic data models in Elgg.
9
 * It is the primary means of storing and retrieving data from the database.
10
 * An \ElggEntity represents one row of the entities table.
11
 *
12
 * The \ElggEntity class handles CRUD operations for the entities table.
13
 * \ElggEntity should always be extended by another class to handle CRUD
14
 * operations on the type-specific table.
15
 *
16
 * \ElggEntity uses magic methods for get and set, so any property that isn't
17
 * declared will be assumed to be metadata and written to the database
18
 * as metadata on the object.  All children classes must declare which
19
 * properties are columns of the type table or they will be assumed
20
 * to be metadata.  See \ElggObject::initializeAttributes() for examples.
21
 *
22
 * Core supports 4 types of entities: \ElggObject, \ElggUser, \ElggGroup, and \ElggSite.
23
 *
24
 * @tip Plugin authors will want to extend the \ElggObject class, not this class.
25
 *
26
 * @package    Elgg.Core
27
 * @subpackage DataModel.Entities
28
 *
29
 * @property       string $type           object, user, group, or site (read-only after save)
30
 * @property       string $subtype        Further clarifies the nature of the entity (this should not be read)
31
 * @property-read  int    $guid           The unique identifier for this entity (read only)
32
 * @property       int    $owner_guid     The GUID of the owner of this entity (usually the creator)
33
 * @property       int    $container_guid The GUID of the entity containing this entity
34
 * @property       int    $access_id      Specifies the visibility level of this entity
35
 * @property       int    $time_created   A UNIX timestamp of when the entity was created
36
 * @property-read  int    $time_updated   A UNIX timestamp of when the entity was last updated (automatically updated on save)
37
 * @property-read  int    $last_action    A UNIX timestamp of when the entity was last acted upon
38
 * @property       string $enabled        Is this entity enabled ('yes' or 'no')
39
 *
40
 * Metadata (the above are attributes)
41
 * @property       string $location       A location of the entity
42
 */
43
abstract class ElggEntity extends \ElggData implements
44
	Locatable, // Geocoding interface
45
	EntityIcon // Icon interface
46
{
47
48
	public static $primary_attr_names = [
49
		'guid',
50
		'type',
51
		'subtype',
52
		'owner_guid',
53
		'container_guid',
54
		'access_id',
55
		'time_created',
56
		'time_updated',
57
		'last_action',
58
		'enabled',
59
	];
60
61
	protected static $integer_attr_names = [
62
		'guid',
63
		'owner_guid',
64
		'container_guid',
65
		'access_id',
66
		'time_created',
67
		'time_updated',
68
		'last_action',
69
	];
70
71
	/**
72
	 * Holds metadata until entity is saved.  Once the entity is saved,
73
	 * metadata are written immediately to the database.
74
	 * @var array
75
	 */
76
	protected $temp_metadata = [];
77
78
	/**
79
	 * Holds annotations until entity is saved.  Once the entity is saved,
80
	 * annotations are written immediately to the database.
81
	 * @var array
82
	 */
83
	protected $temp_annotations = [];
84
85
	/**
86
	 * Holds private settings until entity is saved. Once the entity is saved,
87
	 * private settings are written immediately to the database.
88
	 * @var array
89
	 */
90
	protected $temp_private_settings = [];
91
92
	/**
93
	 * Volatile data structure for this object, allows for storage of data
94
	 * in-memory that isn't sync'd back to the metadata table.
95
	 * @var array
96
	 */
97
	protected $volatile = [];
98
99
	/**
100
	 * Holds the original (persisted) attribute values that have been changed but not yet saved.
101
	 * @var array
102
	 */
103
	protected $orig_attributes = [];
104
105
	/**
106
	 * @var bool
107
	 */
108
	protected $_is_cacheable = true;
109
110
	/**
111
	 * Create a new entity.
112
	 *
113
	 * Plugin developers should only use the constructor to create a new entity.
114
	 * To retrieve entities, use get_entity() and the elgg_get_entities* functions.
115
	 *
116
	 * If no arguments are passed, it creates a new entity.
117
	 * If a database result is passed as a \stdClass instance, it instantiates
118
	 * that entity.
119
	 *
120
	 * @param stdClass $row Database row result. Default is null to create a new object.
121
	 *
122
	 * @throws IOException If cannot load remaining data from db
123
	 */
124 5322
	public function __construct(stdClass $row = null) {
125 5322
		$this->initializeAttributes();
126
127 5322
		if ($row && !$this->load($row)) {
128
			$msg = "Failed to load new " . get_class() . " for GUID:" . $row->guid;
129
			throw new \IOException($msg);
130
		}
131 5322
	}
132
133
	/**
134
	 * Initialize the attributes array.
135
	 *
136
	 * This is vital to distinguish between metadata and base parameters.
137
	 *
138
	 * @return void
139
	 */
140 5322
	protected function initializeAttributes() {
141 5322
		parent::initializeAttributes();
142
143 5322
		$this->attributes['guid'] = null;
144 5322
		$this->attributes['type'] = null;
145 5322
		$this->attributes['subtype'] = null;
146
147 5322
		$this->attributes['owner_guid'] = _elgg_services()->session->getLoggedInUserGuid();
148 5322
		$this->attributes['container_guid'] = _elgg_services()->session->getLoggedInUserGuid();
149
150 5322
		$this->attributes['access_id'] = ACCESS_PRIVATE;
151 5322
		$this->attributes['time_updated'] = null;
152 5322
		$this->attributes['last_action'] = null;
153 5322
		$this->attributes['enabled'] = "yes";
154
155 5322
		$this->attributes['type'] = $this->getType();
156 5322
	}
157
158
	/**
159
	 * Clone an entity
160
	 *
161
	 * Resets the guid so that the entity can be saved as a distinct entity from
162
	 * the original. Creation time will be set when this new entity is saved.
163
	 * The owner and container guids come from the original entity. The clone
164
	 * method copies metadata but does not copy annotations or private settings.
165
	 *
166
	 * @note metadata will have its owner and access id set when the entity is saved
167
	 * and it will be the same as that of the entity.
168
	 *
169
	 * @return void
170
	 */
171 1
	public function __clone() {
172 1
		$orig_entity = get_entity($this->guid);
173 1
		if (!$orig_entity) {
174
			_elgg_services()->logger->error("Failed to clone entity with GUID $this->guid");
175
			return;
176
		}
177
178 1
		$metadata_array = elgg_get_metadata([
179 1
			'guid' => $this->guid,
180 1
			'limit' => 0
181
		]);
182
183 1
		$this->attributes['guid'] = null;
184 1
		$this->attributes['time_created'] = null;
185 1
		$this->attributes['time_updated'] = null;
186 1
		$this->attributes['last_action'] = null;
187
188 1
		$this->attributes['subtype'] = $orig_entity->getSubtype();
189
190
		// copy metadata over to new entity - slightly convoluted due to
191
		// handling of metadata arrays
192 1
		if (is_array($metadata_array)) {
193
			// create list of metadata names
194 1
			$metadata_names = [];
195 1
			foreach ($metadata_array as $metadata) {
196 1
				$metadata_names[] = $metadata['name'];
197
			}
198
			// arrays are stored with multiple enties per name
199 1
			$metadata_names = array_unique($metadata_names);
200
201
			// move the metadata over
202 1
			foreach ($metadata_names as $name) {
203 1
				$this->__set($name, $orig_entity->$name);
204
			}
205
		}
206 1
	}
207
208
	/**
209
	 * Set an attribute or metadata value for this entity
210
	 *
211
	 * Anything that is not an attribute is saved as metadata.
212
	 *
213
	 * @warning Metadata set this way will inherit the entity's owner and
214
	 * access ID. If you want more control over metadata, use \ElggEntity::setMetadata()
215
	 *
216
	 * @param string $name  Name of the attribute or metadata
217
	 * @param mixed  $value The value to be set
218
	 * @return void
219
	 * @see \ElggEntity::setMetadata()
220
	 */
221 1092
	public function __set($name, $value) {
222 1092
		if ($this->$name === $value) {
223
			// quick return if value is not changing
224 289
			return;
225
		}
226
227
		// Due to https://github.com/Elgg/Elgg/pull/5456#issuecomment-17785173, certain attributes
228
		// will store empty strings as null in the DB. In the somewhat common case that we're re-setting
229
		// the value to empty string, don't consider this a change.
230 1092
		if (in_array($name, ['title', 'name', 'description'])
231 1092
			&& $this->$name === null
232 1092
			&& $value === "") {
233
			return;
234
		}
235
236 1092
		if (array_key_exists($name, $this->attributes)) {
237
			// if an attribute is 1 (integer) and it's set to "1" (string), don't consider that a change.
238 589
			if (is_int($this->attributes[$name])
239 589
					&& is_string($value)
240 589
					&& ((string) $this->attributes[$name] === $value)) {
241 1
				return;
242
			}
243
244
			// keep original values
245 589
			if ($this->guid && !array_key_exists($name, $this->orig_attributes)) {
246 9
				$this->orig_attributes[$name] = $this->attributes[$name];
247
			}
248
249
			// Certain properties should not be manually changed!
250 589
			switch ($name) {
251
				case 'guid':
252
				case 'time_updated':
253
				case 'last_action':
254
					return;
255
					break;
256
				case 'access_id':
257
				case 'owner_guid':
258
				case 'container_guid':
259 482
					if ($value !== null) {
260 482
						$this->attributes[$name] = (int) $value;
261
					} else {
262
						$this->attributes[$name] = null;
263
					}
264 482
					break;
265
				default:
266 443
					$this->attributes[$name] = $value;
267 443
					break;
268
			}
269 589
			return;
270
		}
271
272 1062
		$this->setMetadata($name, $value);
273 1062
	}
274
275
	/**
276
	 * Get the original values of attribute(s) that have been modified since the entity was persisted.
277
	 *
278
	 * @return array
279
	 */
280 59
	public function getOriginalAttributes() {
281 59
		return $this->orig_attributes;
282
	}
283
284
	/**
285
	 * Get an attribute or metadata value
286
	 *
287
	 * If the name matches an attribute, the attribute is returned. If metadata
288
	 * does not exist with that name, a null is returned.
289
	 *
290
	 * This only returns an array if there are multiple values for a particular
291
	 * $name key.
292
	 *
293
	 * @param string $name Name of the attribute or metadata
294
	 * @return mixed
295
	 */
296 5387
	public function __get($name) {
297 5387
		if (array_key_exists($name, $this->attributes)) {
298 5386
			return $this->attributes[$name];
299
		}
300
301 5363
		return $this->getMetadata($name);
302
	}
303
304
	/**
305
	 * Get the entity's display name
306
	 *
307
	 * @return string The title or name of this entity.
308
	 */
309 19
	public function getDisplayName() {
310 19
		return $this->name;
311
	}
312
313
	/**
314
	 * Sets the title or name of this entity.
315
	 *
316
	 * @param string $display_name The title or name of this entity.
317
	 * @return void
318
	 */
319
	public function setDisplayName($display_name) {
320
		$this->name = $display_name;
321
	}
322
323
	/**
324
	 * Return the value of a piece of metadata.
325
	 *
326
	 * @param string $name Name
327
	 *
328
	 * @return mixed The value, or null if not found.
329
	 */
330 5363
	public function getMetadata($name) {
331 5363
		$guid = $this->guid;
332
333 5363
		if (!$guid) {
334 572
			if (isset($this->temp_metadata[$name])) {
335
				// md is returned as an array only if more than 1 entry
336 285
				if (count($this->temp_metadata[$name]) == 1) {
337 285
					return $this->temp_metadata[$name][0];
338
				} else {
339 158
					return $this->temp_metadata[$name];
340
				}
341
			} else {
342 572
				return null;
343
			}
344
		}
345
346
		// upon first cache miss, just load/cache all the metadata and retry.
347
		// if this works, the rest of this function may not be needed!
348 5354
		$cache = _elgg_services()->metadataCache;
349 5354
		if ($cache->isLoaded($guid)) {
350 2238
			return $cache->getSingle($guid, $name);
351
		} else {
352 4136
			$cache->populateFromEntities([$guid]);
353
			// in case ignore_access was on, we have to check again...
354 4136
			if ($cache->isLoaded($guid)) {
355 1082
				return $cache->getSingle($guid, $name);
356
			}
357
		}
358
359 4083
		$md = elgg_get_metadata([
360 4083
			'guid' => $guid,
361 4083
			'metadata_name' => $name,
362 4083
			'limit' => 0,
363
			'distinct' => false,
364
		]);
365
366 4083
		$value = null;
367
368 4083
		if ($md && !is_array($md)) {
369
			$value = $md->value;
370 4083
		} elseif (count($md) == 1) {
371
			$value = $md[0]->value;
372 4083
		} else if ($md && is_array($md)) {
373
			$value = metadata_array_to_values($md);
374
		}
375
376 4083
		return $value;
377
	}
378
379
	/**
380
	 * Unset a property from metadata or attribute.
381
	 *
382
	 * @warning If you use this to unset an attribute, you must save the object!
383
	 *
384
	 * @param string $name The name of the attribute or metadata.
385
	 *
386
	 * @return void
387
	 * @todo some attributes should be set to null or other default values
388
	 */
389 49
	public function __unset($name) {
390 49
		if (array_key_exists($name, $this->attributes)) {
391
			$this->attributes[$name] = "";
392
		} else {
393 49
			$this->deleteMetadata($name);
394
		}
395 49
	}
396
397
	/**
398
	 * Set metadata on this entity.
399
	 *
400
	 * Plugin developers usually want to use the magic set method ($entity->name = 'value').
401
	 * Use this method if you want to explicitly set the owner or access of the metadata.
402
	 * You cannot set the owner/access before the entity has been saved.
403
	 *
404
	 * @param string $name       Name of the metadata
405
	 * @param mixed  $value      Value of the metadata (doesn't support assoc arrays)
406
	 * @param string $value_type 'text', 'integer', or '' for automatic detection
407
	 * @param bool   $multiple   Allow multiple values for a single name.
408
	 *                           Does not support associative arrays.
409
	 *
410
	 * @return bool
411
	 * @throws InvalidArgumentException
412
	 */
413 1062
	public function setMetadata($name, $value, $value_type = '', $multiple = false) {
414
415
		// normalize value to an array that we will loop over
416
		// remove indexes if value already an array.
417 1062
		if (is_array($value)) {
418 162
			$value = array_values($value);
419
		} else {
420 1061
			$value = [$value];
421
		}
422
423
		// strip null values from array
424 1062
		$value = array_filter($value, function($var) {
425 1062
			return !is_null($var);
426 1062
		});
427
428 1062
		if (empty($this->guid)) {
429
			// unsaved entity. store in temp array
430 569
			return $this->setTempMetadata($name, $value, $multiple);
431
		}
432
433
		// saved entity. persist md to db.
434 1028
		if (!$multiple) {
435 1028
			$current_metadata = $this->getMetadata($name);
436
437 1028
			if ((is_array($current_metadata) || count($value) > 1 || $value === []) && isset($current_metadata)) {
438
				// remove current metadata if needed
439
				// need to remove access restrictions right now to delete
440
				// because this is the expected behavior
441 8
				$ia = elgg_set_ignore_access(true);
442 8
				$delete_result = elgg_delete_metadata([
443 8
					'guid' => $this->guid,
444 8
					'metadata_name' => $name,
445
					'limit' => false,
446
				]);
447 8
				elgg_set_ignore_access($ia);
448
449 8
				if (false === $delete_result) {
450
					return false;
451
				}
452
			}
453
454 1028
			if (count($value) > 1) {
455
				// new value is a multiple valued metadata
456 162
				$multiple = true;
457
			}
458
		}
459
460
		// create new metadata
461 1028
		foreach ($value as $value_tmp) {
462 1028
			$metadata = new ElggMetadata();
463 1028
			$metadata->entity_guid = $this->guid;
464 1028
			$metadata->name = $name;
465 1028
			$metadata->value_type = $value_type;
466 1028
			$metadata->value = $value_tmp;
467 1028
			$md_id = _elgg_services()->metadataTable->create($metadata, $multiple);
468 1028
			if (!$md_id) {
469 1028
				return false;
470
			}
471
		}
472
473 1028
		return true;
474
	}
475
476
	/**
477
	 * Set temp metadata on this entity.
478
	 *
479
	 * @param string $name     Name of the metadata
480
	 * @param mixed  $value    Value of the metadata (doesn't support assoc arrays)
481
	 * @param bool   $multiple Allow multiple values for a single name.
482
	 *                         Does not support associative arrays.
483
	 *
484
	 * @return bool
485
	 */
486 569
	protected function setTempMetadata($name, $value, $multiple = false) {
487
		// if overwrite, delete first
488 569
		if (!$multiple) {
489 569
			unset($this->temp_metadata[$name]);
490 569
			if (count($value)) {
491
				// only save if value array contains data
492 569
				$this->temp_metadata[$name] = $value;
493
			}
494 569
			return true;
495
		}
496
497 3
		if (!isset($this->temp_metadata[$name])) {
498
			$this->temp_metadata[$name] = [];
499
		}
500
501 3
		$this->temp_metadata[$name] = array_merge($this->temp_metadata[$name], $value);
502
503 3
		return true;
504
	}
505
506
507
508
	/**
509
	 * Deletes all metadata on this object (metadata.entity_guid = $this->guid).
510
	 * If you pass a name, only metadata matching that name will be deleted.
511
	 *
512
	 * @warning Calling this with no $name will clear all metadata on the entity.
513
	 *
514
	 * @param null|string $name The name of the metadata to remove.
515
	 * @return bool|null
516
	 * @since 1.8
517
	 */
518 266
	public function deleteMetadata($name = null) {
519
520 266
		if (!$this->guid) {
521
			// remove from temp_metadata
522 2
			if ($name) {
523 2
				if (!isset($this->temp_metadata[$name])) {
524
					return null;
525
				} else {
526 2
					unset($this->temp_metadata[$name]);
527 2
					return true;
528
				}
529
			} else {
530
				$this->temp_metadata = [];
531
				return true;
532
			}
533
		}
534
535
		$options = [
536 266
			'guid' => $this->guid,
537 266
			'limit' => 0
538
		];
539 266
		if ($name) {
540 55
			$options['metadata_name'] = $name;
541
		}
542
543 266
		return elgg_delete_metadata($options);
544
	}
545
546
	/**
547
	 * Get a piece of volatile (non-persisted) data on this entity.
548
	 *
549
	 * @param string $name The name of the volatile data
550
	 *
551
	 * @return mixed The value or null if not found.
552
	 */
553 25
	public function getVolatileData($name) {
554 25
		return array_key_exists($name, $this->volatile) ? $this->volatile[$name] : null;
555
	}
556
557
	/**
558
	 * Set a piece of volatile (non-persisted) data on this entity
559
	 *
560
	 * @param string $name  Name
561
	 * @param mixed  $value Value
562
	 *
563
	 * @return void
564
	 */
565 26
	public function setVolatileData($name, $value) {
566 26
		$this->volatile[$name] = $value;
567 26
	}
568
569
	/**
570
	 * Remove all relationships to and from this entity.
571
	 * If you pass a relationship name, only relationships matching that name
572
	 * will be deleted.
573
	 *
574
	 * @warning Calling this with no $relationship will clear all relationships
575
	 * for this entity.
576
	 *
577
	 * @param null|string $relationship The name of the relationship to remove.
578
	 * @return bool
579
	 * @see \ElggEntity::addRelationship()
580
	 * @see \ElggEntity::removeRelationship()
581
	 */
582 214
	public function deleteRelationships($relationship = null) {
583 214
		$relationship = (string) $relationship;
584 214
		$result = remove_entity_relationships($this->getGUID(), $relationship);
585 214
		return $result && remove_entity_relationships($this->getGUID(), $relationship, true);
586
	}
587
588
	/**
589
	 * Add a relationship between this an another entity.
590
	 *
591
	 * @tip Read the relationship like "This entity is a $relationship of $guid_two."
592
	 *
593
	 * @param int    $guid_two     GUID of the target entity of the relationship.
594
	 * @param string $relationship The type of relationship.
595
	 *
596
	 * @return bool
597
	 * @see \ElggEntity::removeRelationship()
598
	 * @see \ElggEntity::deleteRelationships()
599
	 */
600 5
	public function addRelationship($guid_two, $relationship) {
601 5
		return add_entity_relationship($this->getGUID(), $relationship, $guid_two);
602
	}
603
604
	/**
605
	 * Remove a relationship
606
	 *
607
	 * @param int    $guid_two     GUID of the target entity of the relationship.
608
	 * @param string $relationship The type of relationship.
609
	 *
610
	 * @return bool
611
	 * @see \ElggEntity::addRelationship()
612
	 * @see \ElggEntity::deleteRelationships()
613
	 */
614 1
	public function removeRelationship($guid_two, $relationship) {
615 1
		return remove_entity_relationship($this->getGUID(), $relationship, $guid_two);
616
	}
617
618
	/**
619
	 * Adds a private setting to this entity.
620
	 *
621
	 * Private settings are similar to metadata but will not
622
	 * be searched and there are fewer helper functions for them.
623
	 *
624
	 * @param string $name  Name of private setting
625
	 * @param mixed  $value Value of private setting
626
	 *
627
	 * @return bool
628
	 */
629 124
	public function setPrivateSetting($name, $value) {
630 124
		if ((int) $this->guid > 0) {
631 123
			return set_private_setting($this->getGUID(), $name, $value);
632
		} else {
633 45
			$this->temp_private_settings[$name] = $value;
634 45
			return true;
635
		}
636
	}
637
638
	/**
639
	 * Returns a private setting value
640
	 *
641
	 * @param string $name Name of the private setting
642
	 *
643
	 * @return mixed Null if the setting does not exist
644
	 */
645 37
	public function getPrivateSetting($name) {
646 37
		if ((int) ($this->guid) > 0) {
647 37
			return get_private_setting($this->getGUID(), $name);
648
		} else {
649 5
			if (isset($this->temp_private_settings[$name])) {
650 5
				return $this->temp_private_settings[$name];
651
			}
652
		}
653 5
		return null;
654
	}
655
656
	/**
657
	 * Removes private setting
658
	 *
659
	 * @param string $name Name of the private setting
660
	 *
661
	 * @return bool
662
	 */
663 3
	public function removePrivateSetting($name) {
664 3
		return remove_private_setting($this->getGUID(), $name);
665
	}
666
667
	/**
668
	 * Deletes all annotations on this object (annotations.entity_guid = $this->guid).
669
	 * If you pass a name, only annotations matching that name will be deleted.
670
	 *
671
	 * @warning Calling this with no or empty arguments will clear all annotations on the entity.
672
	 *
673
	 * @param null|string $name The annotations name to remove.
674
	 * @return bool
675
	 * @since 1.8
676
	 */
677 214
	public function deleteAnnotations($name = null) {
678
		$options = [
679 214
			'guid' => $this->guid,
680 214
			'limit' => 0
681
		];
682 214
		if ($name) {
683 1
			$options['annotation_name'] = $name;
684
		}
685
686 214
		return elgg_delete_annotations($options);
687
	}
688
689
	/**
690
	 * Deletes all annotations owned by this object (annotations.owner_guid = $this->guid).
691
	 * If you pass a name, only annotations matching that name will be deleted.
692
	 *
693
	 * @param null|string $name The name of annotations to delete.
694
	 * @return bool
695
	 * @since 1.8
696
	 */
697 214
	public function deleteOwnedAnnotations($name = null) {
698
		// access is turned off for this because they might
699
		// no longer have access to an entity they created annotations on.
700 214
		$ia = elgg_set_ignore_access(true);
701
		$options = [
702 214
			'annotation_owner_guid' => $this->guid,
703 214
			'limit' => 0
704
		];
705 214
		if ($name) {
706
			$options['annotation_name'] = $name;
707
		}
708
709 214
		$r = elgg_delete_annotations($options);
710 214
		elgg_set_ignore_access($ia);
711 214
		return $r;
712
	}
713
714
	/**
715
	 * Disables annotations for this entity, optionally based on name.
716
	 *
717
	 * @param string $name An options name of annotations to disable.
718
	 * @return bool
719
	 * @since 1.8
720
	 */
721 5
	public function disableAnnotations($name = '') {
722
		$options = [
723 5
			'guid' => $this->guid,
724 5
			'limit' => 0
725
		];
726 5
		if ($name) {
727
			$options['annotation_name'] = $name;
728
		}
729
730 5
		return elgg_disable_annotations($options);
731
	}
732
733
	/**
734
	 * Enables annotations for this entity, optionally based on name.
735
	 *
736
	 * @warning Before calling this, you must use {@link access_show_hidden_entities()}
737
	 *
738
	 * @param string $name An options name of annotations to enable.
739
	 * @return bool
740
	 * @since 1.8
741
	 */
742 3
	public function enableAnnotations($name = '') {
743
		$options = [
744 3
			'guid' => $this->guid,
745 3
			'limit' => 0
746
		];
747 3
		if ($name) {
748
			$options['annotation_name'] = $name;
749
		}
750
751 3
		return elgg_enable_annotations($options);
752
	}
753
754
	/**
755
	 * Helper function to return annotation calculation results
756
	 *
757
	 * @param string $name        The annotation name.
758
	 * @param string $calculation A valid MySQL function to run its values through
759
	 * @return mixed
760
	 */
761 2
	private function getAnnotationCalculation($name, $calculation) {
762
		$options = [
763 2
			'guid' => $this->getGUID(),
764
			'distinct' => false,
765 2
			'annotation_name' => $name,
766 2
			'annotation_calculation' => $calculation
767
		];
768
769 2
		return elgg_get_annotations($options);
770
	}
771
772
	/**
773
	 * Adds an annotation to an entity.
774
	 *
775
	 * @warning By default, annotations are private.
776
	 *
777
	 * @warning Annotating an unsaved entity more than once with the same name
778
	 *          will only save the last annotation.
779
	 *
780
	 * @todo Update temp_annotations to store an instance of ElggAnnotation and simply call ElggAnnotation::save(),
781
	 *       after entity is saved
782
	 *
783
	 * @param string $name       Annotation name
784
	 * @param mixed  $value      Annotation value
785
	 * @param int    $access_id  Access ID
786
	 * @param int    $owner_guid GUID of the annotation owner
787
	 * @param string $value_type The type of annotation value
788
	 *
789
	 * @return bool|int Returns int if an annotation is saved
790
	 */
791 93
	public function annotate($name, $value, $access_id = ACCESS_PRIVATE, $owner_guid = 0, $value_type = "") {
792 93
		if ($this->guid) {
793 93
			if (!$owner_guid) {
794 92
				$owner_guid = elgg_get_logged_in_user_guid();
795
			}
796 93
			$annotation = new ElggAnnotation();
797 93
			$annotation->entity_guid = $this->guid;
798 93
			$annotation->name = $name;
799 93
			$annotation->value_type = $value_type;
800 93
			$annotation->value = $value;
801 93
			$annotation->owner_guid = $owner_guid;
802 93
			$annotation->access_id = $access_id;
803 93
			return $annotation->save();
804
		} else {
805 17
			$this->temp_annotations[$name] = $value;
806
		}
807 17
		return true;
808
	}
809
810
	/**
811
	 * Gets an array of annotations.
812
	 *
813
	 * To retrieve annotations on an unsaved entity, pass array('name' => [annotation name])
814
	 * as the options array.
815
	 *
816
	 * @param array $options Array of options for elgg_get_annotations() except guid.
817
	 *
818
	 * @return array
819
	 * @see elgg_get_annotations()
820
	 */
821 9
	public function getAnnotations(array $options = []) {
822 9
		if ($this->guid) {
823 9
			$options['guid'] = $this->guid;
824
825 9
			return elgg_get_annotations($options);
826
		} else {
827
			$name = elgg_extract('annotation_name', $options, '');
828
829
			if (isset($this->temp_annotations[$name])) {
830
				return [$this->temp_annotations[$name]];
831
			}
832
		}
833
834
		return [];
835
	}
836
837
	/**
838
	 * Count annotations.
839
	 *
840
	 * @param string $name The type of annotation.
841
	 *
842
	 * @return int
843
	 */
844 2
	public function countAnnotations($name = "") {
845 2
		return $this->getAnnotationCalculation($name, 'count');
846
	}
847
848
	/**
849
	 * Get the average of an integer type annotation.
850
	 *
851
	 * @param string $name Annotation name
852
	 *
853
	 * @return int
854
	 */
855
	public function getAnnotationsAvg($name) {
856
		return $this->getAnnotationCalculation($name, 'avg');
857
	}
858
859
	/**
860
	 * Get the sum of integer type annotations of a given name.
861
	 *
862
	 * @param string $name Annotation name
863
	 *
864
	 * @return int
865
	 */
866
	public function getAnnotationsSum($name) {
867
		return $this->getAnnotationCalculation($name, 'sum');
868
	}
869
870
	/**
871
	 * Get the minimum of integer type annotations of given name.
872
	 *
873
	 * @param string $name Annotation name
874
	 *
875
	 * @return int
876
	 */
877
	public function getAnnotationsMin($name) {
878
		return $this->getAnnotationCalculation($name, 'min');
879
	}
880
881
	/**
882
	 * Get the maximum of integer type annotations of a given name.
883
	 *
884
	 * @param string $name Annotation name
885
	 *
886
	 * @return int
887
	 */
888
	public function getAnnotationsMax($name) {
889
		return $this->getAnnotationCalculation($name, 'max');
890
	}
891
892
	/**
893
	 * Count the number of comments attached to this entity.
894
	 *
895
	 * @return int Number of comments
896
	 * @since 1.8.0
897
	 */
898 2
	public function countComments() {
899 2
		$params = ['entity' => $this];
900 2
		$num = _elgg_services()->hooks->trigger('comments:count', $this->getType(), $params);
901
902 2
		if (is_int($num)) {
903
			return $num;
904
		}
905
906 2
		return elgg_get_entities([
907 2
			'type' => 'object',
908 2
			'subtype' => 'comment',
909 2
			'container_guid' => $this->getGUID(),
910
			'count' => true,
911
			'distinct' => false,
912
		]);
913
	}
914
915
	/**
916
	 * Returns the ACLs owned by the entity
917
	 *
918
	 * @param array $options additional options to get the access collections with
919
	 *
920
	 * @return \ElggAccessCollection[]
921
	 *
922
	 * @see elgg_get_access_collections()
923
	 * @since 3.0
924
	 */
925 218
	public function getOwnedAccessCollections($options = []) {
926 218
		$options['owner_guid'] = $this->guid;
927 218
		return _elgg_services()->accessCollections->getEntityCollections($options);
928
	}
929
	
930
	/**
931
	 * Returns the first ACL owned by the entity with a given subtype
932
	 *
933
	 * @param string $subtype subtype of the ACL
934
	 *
935
	 * @return \ElggAccessCollection|false
936
	 *
937
	 * @since 3.0
938
	 */
939
	public function getOwnedAccessCollection($subtype) {
940
		if (!is_string($subtype) || $subtype === '') {
941
			return false;
942
		}
943
		
944
		$acls = $this->getOwnedAccessCollections([
945
			'subtype' => $subtype,
946
		]);
947
		
948
		return elgg_extract(0, $acls, false);
949
	}
950
951
	/**
952
	 * Gets an array of entities with a relationship to this entity.
953
	 *
954
	 * @param array $options Options array. See elgg_get_entities_from_relationship()
955
	 *                       for a list of options. 'relationship_guid' is set to
956
	 *                       this entity.
957
	 *
958
	 * @return array|false An array of entities or false on failure
959
	 * @see elgg_get_entities_from_relationship()
960
	 */
961
	public function getEntitiesFromRelationship(array $options = []) {
962
		$options['relationship_guid'] = $this->guid;
963
		return elgg_get_entities($options);
964
	}
965
966
	/**
967
	 * Gets the number of entities from a specific relationship type
968
	 *
969
	 * @param string $relationship         Relationship type (eg "friends")
970
	 * @param bool   $inverse_relationship Invert relationship
971
	 *
972
	 * @return int|false The number of entities or false on failure
973
	 */
974
	public function countEntitiesFromRelationship($relationship, $inverse_relationship = false) {
975
		return elgg_get_entities([
976
			'relationship' => $relationship,
977
			'relationship_guid' => $this->getGUID(),
978
			'inverse_relationship' => $inverse_relationship,
979
			'count' => true
980
		]);
981
	}
982
983
	/**
984
	 * Can a user edit this entity?
985
	 *
986
	 * @tip Can be overridden by registering for the permissions_check plugin hook.
987
	 *
988
	 * @param int $user_guid The user GUID, optionally (default: logged in user)
989
	 *
990
	 * @return bool Whether this entity is editable by the given user.
991
	 * @see elgg_set_ignore_access()
992
	 */
993 399
	public function canEdit($user_guid = 0) {
994 399
		return _elgg_services()->userCapabilities->canEdit($this, $user_guid);
995
	}
996
997
	/**
998
	 * Can a user delete this entity?
999
	 *
1000
	 * @tip Can be overridden by registering for the permissions_check:delete plugin hook.
1001
	 *
1002
	 * @param int $user_guid The user GUID, optionally (default: logged in user)
1003
	 *
1004
	 * @return bool Whether this entity is deletable by the given user.
1005
	 * @since 1.11
1006
	 * @see elgg_set_ignore_access()
1007
	 */
1008 462
	public function canDelete($user_guid = 0) {
1009 462
		return _elgg_services()->userCapabilities->canDelete($this, $user_guid);
1010
	}
1011
1012
	/**
1013
	 * Can a user edit metadata on this entity?
1014
	 *
1015
	 * If no specific metadata is passed, it returns whether the user can
1016
	 * edit any metadata on the entity.
1017
	 *
1018
	 * @tip Can be overridden by by registering for the permissions_check:metadata
1019
	 * plugin hook.
1020
	 *
1021
	 * @param \ElggMetadata $metadata  The piece of metadata to specifically check or null for any metadata
1022
	 * @param int           $user_guid The user GUID, optionally (default: logged in user)
1023
	 *
1024
	 * @return bool
1025
	 * @see elgg_set_ignore_access()
1026
	 */
1027 338
	public function canEditMetadata($metadata = null, $user_guid = 0) {
1028 338
		return _elgg_services()->userCapabilities->canEditMetadata($this, $user_guid, $metadata);
1029
	}
1030
1031
	/**
1032
	 * Can a user add an entity to this container
1033
	 *
1034
	 * @param int    $user_guid The GUID of the user creating the entity (0 for logged in user).
1035
	 * @param string $type      The type of entity we're looking to write
1036
	 * @param string $subtype   The subtype of the entity we're looking to write
1037
	 *
1038
	 * @return bool
1039
	 * @see elgg_set_ignore_access()
1040
	 */
1041 407
	public function canWriteToContainer($user_guid = 0, $type = 'all', $subtype = 'all') {
1042 407
		return _elgg_services()->userCapabilities->canWriteToContainer($this, $user_guid, $type, $subtype);
1043
	}
1044
1045
	/**
1046
	 * Can a user comment on an entity?
1047
	 *
1048
	 * @tip Can be overridden by registering for the permissions_check:comment,
1049
	 * <entity type> plugin hook.
1050
	 *
1051
	 * @param int  $user_guid User guid (default is logged in user)
1052
	 * @param bool $default   Default permission
1053
	 * @return bool
1054
	 */
1055 4
	public function canComment($user_guid = 0, $default = null) {
1056 4
		return _elgg_services()->userCapabilities->canComment($this, $user_guid, $default);
1057
	}
1058
1059
	/**
1060
	 * Can a user annotate an entity?
1061
	 *
1062
	 * @tip Can be overridden by registering for the plugin hook [permissions_check:annotate:<name>,
1063
	 * <entity type>] or [permissions_check:annotate, <entity type>]. The hooks are called in that order.
1064
	 *
1065
	 * @tip If you want logged out users to annotate an object, do not call
1066
	 * canAnnotate(). It's easier than using the plugin hook.
1067
	 *
1068
	 * @param int    $user_guid       User guid (default is logged in user)
1069
	 * @param string $annotation_name The name of the annotation (default is unspecified)
1070
	 *
1071
	 * @return bool
1072
	 */
1073 8
	public function canAnnotate($user_guid = 0, $annotation_name = '') {
1074 8
		return _elgg_services()->userCapabilities->canAnnotate($this, $user_guid, $annotation_name);
1075
	}
1076
1077
	/**
1078
	 * Returns the access_id.
1079
	 *
1080
	 * @return int The access ID
1081
	 */
1082
	public function getAccessID() {
1083
		return $this->access_id;
1084
	}
1085
1086
	/**
1087
	 * Returns the guid.
1088
	 *
1089
	 * @return int|null GUID
1090
	 */
1091 548
	public function getGUID() {
1092 548
		return $this->guid;
1093
	}
1094
1095
	/**
1096
	 * Returns the entity type
1097
	 *
1098
	 * @return string The entity type
1099
	 */
1100 1
	public function getType() {
1101
		// this is just for the PHPUnit mocking framework
1102 1
		return $this->type;
1103
	}
1104
1105
	/**
1106
	 * Get the entity subtype
1107
	 *
1108
	 * @return string The entity subtype
1109
	 */
1110 566
	public function getSubtype() {
1111 566
		return $this->attributes['subtype'];
1112
	}
1113
1114
	/**
1115
	 * Get the guid of the entity's owner.
1116
	 *
1117
	 * @return int The owner GUID
1118
	 */
1119 95
	public function getOwnerGUID() {
1120 95
		return (int) $this->owner_guid;
1121
	}
1122
1123
	/**
1124
	 * Gets the \ElggEntity that owns this entity.
1125
	 *
1126
	 * @return \ElggEntity The owning entity
1127
	 */
1128 414
	public function getOwnerEntity() {
1129 414
		return get_entity($this->owner_guid);
1130
	}
1131
1132
	/**
1133
	 * Set the container for this object.
1134
	 *
1135
	 * @param int $container_guid The ID of the container.
1136
	 *
1137
	 * @return bool
1138
	 */
1139 1
	public function setContainerGUID($container_guid) {
1140 1
		return $this->container_guid = (int) $container_guid;
1141
	}
1142
1143
	/**
1144
	 * Gets the container GUID for this entity.
1145
	 *
1146
	 * @return int
1147
	 */
1148 425
	public function getContainerGUID() {
1149 425
		return (int) $this->container_guid;
1150
	}
1151
1152
	/**
1153
	 * Get the container entity for this object.
1154
	 *
1155
	 * @return \ElggEntity
1156
	 * @since 1.8.0
1157
	 */
1158 420
	public function getContainerEntity() {
1159 420
		return get_entity($this->getContainerGUID());
1160
	}
1161
1162
	/**
1163
	 * Returns the UNIX epoch time that this entity was last updated
1164
	 *
1165
	 * @return int UNIX epoch time
1166
	 */
1167 5
	public function getTimeUpdated() {
1168 5
		return $this->time_updated;
1169
	}
1170
1171
	/**
1172
	 * Gets the URL for this entity.
1173
	 *
1174
	 * Plugins can register for the 'entity:url', <type> plugin hook to
1175
	 * customize the url for an entity.
1176
	 *
1177
	 * @return string The URL of the entity
1178
	 */
1179 42
	public function getURL() {
1180 42
		$url = _elgg_services()->hooks->trigger('entity:url', $this->getType(), ['entity' => $this]);
0 ignored issues
show
Are you sure the assignment to $url is correct as _elgg_services()->hooks-...ray('entity' => $this)) targeting Elgg\PluginHooksService::trigger() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

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.

Loading history...
1181
1182 42
		if ($url === null || $url === '' || $url === false) {
1183 39
			return '';
1184
		}
1185
1186 3
		return elgg_normalize_url($url);
1187
	}
1188
1189
	/**
1190
	 * Saves icons using an uploaded file as the source.
1191
	 *
1192
	 * @param string $input_name Form input name
1193
	 * @param string $type       The name of the icon. e.g., 'icon', 'cover_photo'
1194
	 * @param array  $coords     An array of cropping coordinates x1, y1, x2, y2
1195
	 * @return bool
1196
	 */
1197
	public function saveIconFromUploadedFile($input_name, $type = 'icon', array $coords = []) {
1198
		return _elgg_services()->iconService->saveIconFromUploadedFile($this, $input_name, $type, $coords);
1199
	}
1200
1201
	/**
1202
	 * Saves icons using a local file as the source.
1203
	 *
1204
	 * @param string $filename The full path to the local file
1205
	 * @param string $type     The name of the icon. e.g., 'icon', 'cover_photo'
1206
	 * @param array  $coords   An array of cropping coordinates x1, y1, x2, y2
1207
	 * @return bool
1208
	 */
1209
	public function saveIconFromLocalFile($filename, $type = 'icon', array $coords = []) {
1210
		return _elgg_services()->iconService->saveIconFromLocalFile($this, $filename, $type, $coords);
1211
	}
1212
1213
	/**
1214
	 * Saves icons using a file located in the data store as the source.
1215
	 *
1216
	 * @param string $file   An ElggFile instance
1217
	 * @param string $type   The name of the icon. e.g., 'icon', 'cover_photo'
1218
	 * @param array  $coords An array of cropping coordinates x1, y1, x2, y2
1219
	 * @return bool
1220
	 */
1221
	public function saveIconFromElggFile(\ElggFile $file, $type = 'icon', array $coords = []) {
1222
		return _elgg_services()->iconService->saveIconFromElggFile($this, $file, $type, $coords);
1223
	}
1224
1225
	/**
1226
	 * Returns entity icon as an ElggIcon object
1227
	 * The icon file may or may not exist on filestore
1228
	 *
1229
	 * @param string $size Size of the icon
1230
	 * @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
1231
	 * @return \ElggIcon
1232
	 */
1233 7
	public function getIcon($size, $type = 'icon') {
1234 7
		return _elgg_services()->iconService->getIcon($this, $size, $type);
1235
	}
1236
1237
	/**
1238
	 * Removes all icon files and metadata for the passed type of icon.
1239
	 *
1240
	 * @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
1241
	 * @return bool
1242
	 */
1243 1
	public function deleteIcon($type = 'icon') {
1244 1
		return _elgg_services()->iconService->deleteIcon($this, $type);
1245
	}
1246
1247
	/**
1248
	 * Returns the timestamp of when the icon was changed.
1249
	 *
1250
	 * @param string $size The size of the icon
1251
	 * @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
1252
	 *
1253
	 * @return int|null A unix timestamp of when the icon was last changed, or null if not set.
1254
	 */
1255
	public function getIconLastChange($size, $type = 'icon') {
1256
		return _elgg_services()->iconService->getIconLastChange($this, $size, $type);
1257
	}
1258
1259
	/**
1260
	 * Returns if the entity has an icon of the passed type.
1261
	 *
1262
	 * @param string $size The size of the icon
1263
	 * @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
1264
	 * @return bool
1265
	 */
1266 3
	public function hasIcon($size, $type = 'icon') {
1267 3
		return _elgg_services()->iconService->hasIcon($this, $size, $type);
1268
	}
1269
1270
	/**
1271
	 * Get the URL for this entity's icon
1272
	 *
1273
	 * Plugins can register for the 'entity:icon:url', <type> plugin hook
1274
	 * to customize the icon for an entity.
1275
	 *
1276
	 * @param mixed $params A string defining the size of the icon (e.g. tiny, small, medium, large)
1277
	 *                      or an array of parameters including 'size'
1278
	 * @return string The URL
1279
	 * @since 1.8.0
1280
	 */
1281 12
	public function getIconURL($params = []) {
1282 12
		return _elgg_services()->iconService->getIconURL($this, $params);
1283
	}
1284
1285
	/**
1286
	 * Save an entity.
1287
	 *
1288
	 * @return bool|int
1289
	 */
1290 533
	public function save() {
1291 533
		$guid = $this->guid;
1292 533
		if ($guid > 0) {
1293 194
			$guid = $this->update();
1294
		} else {
1295 509
			$guid = $this->create();
1296 509
			if ($guid && !_elgg_services()->hooks->getEvents()->trigger('create', $this->type, $this)) {
1297
				// plugins that return false to event don't need to override the access system
1298
				$ia = elgg_set_ignore_access(true);
1299
				$this->delete();
1300
				elgg_set_ignore_access($ia);
1301
				return false;
1302
			}
1303
		}
1304
1305 533
		if ($guid) {
1306 513
			$this->cache();
1307
		}
1308
1309 533
		return $guid;
1310
	}
1311
1312
	/**
1313
	 * Create a new entry in the entities table.
1314
	 *
1315
	 * Saves the base information in the entities table for the entity.  Saving
1316
	 * the type-specific information is handled in the calling class method.
1317
	 *
1318
	 * @warning Entities must have an entry in both the entities table and their type table
1319
	 * or they will throw an exception when loaded.
1320
	 *
1321
	 * @return int The new entity's GUID
1322
	 * @throws InvalidParameterException If the entity's type has not been set.
1323
	 * @throws IOException If the new row fails to write to the DB.
1324
	 */
1325 509
	protected function create() {
1326
1327 509
		$type = $this->attributes['type'];
1328 509
		if (!in_array($type, \Elgg\Config::getEntityTypes())) {
1329
			throw new \InvalidParameterException('Entity type must be one of the allowed types: '
1330
					. implode(', ', \Elgg\Config::getEntityTypes()));
1331
		}
1332
1333 509
		$subtype = $this->attributes['subtype'];
1334 509
		if (!$subtype) {
1335
			throw new \InvalidParameterException("All entities must have a subtype");
1336
		}
1337
1338 509
		$owner_guid = (int) $this->attributes['owner_guid'];
1339 509
		$access_id = (int) $this->attributes['access_id'];
1340 509
		$now = $this->getCurrentTime()->getTimestamp();
1341 509
		$time_created = isset($this->attributes['time_created']) ? (int) $this->attributes['time_created'] : $now;
1342
1343 509
		$container_guid = $this->attributes['container_guid'];
1344 509
		if ($container_guid == 0) {
1345 179
			$container_guid = $owner_guid;
1346 179
			$this->attributes['container_guid'] = $container_guid;
1347
		}
1348 509
		$container_guid = (int) $container_guid;
1349
1350 509
		if ($access_id == ACCESS_DEFAULT) {
1351
			throw new \InvalidParameterException('ACCESS_DEFAULT is not a valid access level. See its documentation in constants.php');
1352
		}
1353
		
1354 509
		if ($access_id == ACCESS_FRIENDS) {
1355
			throw new \InvalidParameterException('ACCESS_FRIENDS is not a valid access level. See its documentation in constants.php');
1356
		}
1357
1358 509
		$user_guid = elgg_get_logged_in_user_guid();
1359
1360
		// If given an owner, verify it can be loaded
1361 509
		if ($owner_guid) {
1362 402
			$owner = $this->getOwnerEntity();
1363 402
			if (!$owner) {
1364
				_elgg_services()->logger->error("User $user_guid tried to create a ($type, $subtype), but the given"
1365
					. " owner $owner_guid could not be loaded.");
1366
				return false;
1367
			}
1368
1369
			// If different owner than logged in, verify can write to container.
1370
1371 402
			if ($user_guid != $owner_guid && !$owner->canWriteToContainer($user_guid, $type, $subtype)) {
1372
				_elgg_services()->logger->error("User $user_guid tried to create a ($type, $subtype) with owner"
1373
					. " $owner_guid, but the user wasn't permitted to write to the owner's container.");
1374
				return false;
1375
			}
1376
		}
1377
1378
		// If given a container, verify it can be loaded and that the current user can write to it
1379 509
		if ($container_guid) {
1380 403
			$container = $this->getContainerEntity();
1381 403
			if (!$container) {
1382
				_elgg_services()->logger->error("User $user_guid tried to create a ($type, $subtype), but the given"
1383
					. " container $container_guid could not be loaded.");
1384
				return false;
1385
			}
1386
1387 403
			if (!$container->canWriteToContainer($user_guid, $type, $subtype)) {
1388
				_elgg_services()->logger->error("User $user_guid tried to create a ($type, $subtype), but was not"
1389
					. " permitted to write to container $container_guid.");
1390
				return false;
1391
			}
1392
		}
1393
1394
		// Create primary table row
1395 509
		$guid = _elgg_services()->entityTable->insertRow((object) [
1396 509
			'type' => $type,
1397 509
			'subtype' => $subtype,
1398 509
			'owner_guid' => $owner_guid,
1399 509
			'container_guid' => $container_guid,
1400 509
			'access_id' => $access_id,
1401 509
			'time_created' => $time_created,
1402 509
			'time_updated' => $now,
1403 509
			'last_action' => $now,
1404 509
		], $this->attributes);
1405
1406 509
		if (!$guid) {
1407
			throw new \IOException("Unable to save new object's base entity information!");
1408
		}
1409
1410 509
		$this->attributes['subtype'] = $subtype;
1411 509
		$this->attributes['guid'] = (int) $guid;
1412 509
		$this->attributes['time_created'] = (int) $time_created;
1413 509
		$this->attributes['time_updated'] = (int) $now;
1414 509
		$this->attributes['last_action'] = (int) $now;
1415 509
		$this->attributes['container_guid'] = (int) $container_guid;
1416
1417
		// We are writing this new entity to cache to make sure subsequent calls
1418
		// to get_entity() load the entity from cache and not from the DB. This
1419
		// MUST come before the metadata and annotation writes below!
1420 509
		$this->cache();
1421
1422
		// Save any unsaved metadata
1423 509
		if (sizeof($this->temp_metadata) > 0) {
1424 475
			foreach ($this->temp_metadata as $name => $value) {
1425 475
				if (count($value) == 1) {
1426
					// temp metadata is always an array, but if there is only one value return just the value
1427 475
					$this->$name = $value[0];
1428
				} else {
1429 475
					$this->$name = $value;
1430
				}
1431
			}
1432
1433 475
			$this->temp_metadata = [];
1434
		}
1435
1436
		// Save any unsaved annotations.
1437 509
		if (sizeof($this->temp_annotations) > 0) {
1438 17
			foreach ($this->temp_annotations as $name => $value) {
1439 17
				$this->annotate($name, $value);
1440
			}
1441
1442 17
			$this->temp_annotations = [];
1443
		}
1444
1445
		// Save any unsaved private settings.
1446 509
		if (sizeof($this->temp_private_settings) > 0) {
1447 44
			foreach ($this->temp_private_settings as $name => $value) {
1448 44
				$this->setPrivateSetting($name, $value);
1449
			}
1450
1451 44
			$this->temp_private_settings = [];
1452
		}
1453
1454 509
		return $guid;
1455
	}
1456
1457
	/**
1458
	 * Update the entity in the database.
1459
	 *
1460
	 * @return bool Whether the update was successful.
1461
	 *
1462
	 * @throws InvalidParameterException
1463
	 */
1464 194
	protected function update() {
1465
1466 194
		if (!$this->canEdit()) {
1467 26
			return false;
1468
		}
1469
1470
		// give old update event a chance to stop the update
1471 172
		if (!_elgg_services()->hooks->getEvents()->trigger('update', $this->type, $this)) {
1472
			return false;
1473
		}
1474
1475 172
		$this->invalidateCache();
1476
1477
		// See #6225. We copy these after the update event in case a handler changed one of them.
1478 172
		$guid = (int) $this->guid;
1479 172
		$owner_guid = (int) $this->owner_guid;
1480 172
		$access_id = (int) $this->access_id;
1481 172
		$container_guid = (int) $this->container_guid;
1482 172
		$time_created = (int) $this->time_created;
1483 172
		$time = $this->getCurrentTime()->getTimestamp();
1484
1485 172
		if ($access_id == ACCESS_DEFAULT) {
1486
			throw new \InvalidParameterException('ACCESS_DEFAULT is not a valid access level. See its documentation in constants.php');
1487
		}
1488
	
1489 172
		if ($access_id == ACCESS_FRIENDS) {
1490
			throw new \InvalidParameterException('ACCESS_FRIENDS is not a valid access level. See its documentation in constants.php');
1491
		}
1492
1493
		// Update primary table
1494 172
		$ret = _elgg_services()->entityTable->updateRow($guid, (object) [
1495 172
			'owner_guid' => $owner_guid,
1496 172
			'container_guid' => $container_guid,
1497 172
			'access_id' => $access_id,
1498 172
			'time_created' => $time_created,
1499 172
			'time_updated' => $time,
1500 172
			'guid' => $guid,
1501
		]);
1502 172
		if ($ret === false) {
1503
			return false;
1504
		}
1505
1506 172
		$this->attributes['time_updated'] = $time;
1507
1508 172
		elgg_trigger_after_event('update', $this->type, $this);
1509
1510
		// TODO(evan): Move this to \ElggObject?
1511 172
		if ($this instanceof \ElggObject && isset($this->orig_attributes['access_id'])) {
1512 5
			update_river_access_by_object($guid, $access_id);
1513
		}
1514
1515 172
		$this->orig_attributes = [];
1516
1517 172
		$this->cache();
1518
1519
		// Handle cases where there was no error BUT no rows were updated!
1520 172
		return true;
1521
	}
1522
1523
	/**
1524
	 * Loads attributes from the entities table into the object.
1525
	 *
1526
	 * @param stdClass $row Object of properties from database row(s)
1527
	 *
1528
	 * @return bool
1529
	 */
1530 5313
	protected function load(stdClass $row) {
1531 5313
		$attributes = array_merge($this->attributes, (array) $row);
1532
1533 5313
		if (array_diff(self::$primary_attr_names, array_keys($attributes)) !== []) {
1534
			// Some primary attributes are missing
1535
			return false;
1536
		}
1537
1538 5313
		foreach ($attributes as $name => $value) {
1539 5313
			if (!in_array($name, self::$primary_attr_names)) {
1540 24
				$this->setVolatileData("select:$name", $value);
1541 24
				unset($attributes[$name]);
1542 24
				continue;
1543
			}
1544
1545 5313
			if (in_array($name, self::$integer_attr_names)) {
1546 5313
				$attributes[$name] = (int) $value;
1547
			}
1548
		}
1549
1550 5313
		$this->attributes = $attributes;
1551
1552 5313
		$this->cache();
1553
1554 5313
		return true;
1555
	}
1556
1557
	/**
1558
	 * Load new data from database into existing entity. Overwrites data but
1559
	 * does not change values not included in the latest data.
1560
	 *
1561
	 * @internal This is used when the same entity is selected twice during a
1562
	 * request in case different select clauses were used to load different data
1563
	 * into volatile data.
1564
	 *
1565
	 * @param stdClass $row DB row with new entity data
1566
	 * @return bool
1567
	 * @access private
1568
	 */
1569
	public function refresh(stdClass $row) {
1570
		if ($row instanceof stdClass) {
1571
			return $this->load($row);
1572
		}
1573
		return false;
1574
	}
1575
1576
	/**
1577
	 * Disable this entity.
1578
	 *
1579
	 * Disabled entities are not returned by getter functions.
1580
	 * To enable an entity, use {@link \ElggEntity::enable()}.
1581
	 *
1582
	 * Recursively disabling an entity will disable all entities
1583
	 * owned or contained by the parent entity.
1584
	 *
1585
	 * You can ignore the disabled field by using {@link access_show_hidden_entities()}.
1586
	 *
1587
	 * @note Internal: Disabling an entity sets the 'enabled' column to 'no'.
1588
	 *
1589
	 * @param string $reason    Optional reason
1590
	 * @param bool   $recursive Recursively disable all contained entities?
1591
	 *
1592
	 * @return bool
1593
	 * @see \ElggEntity::enable()
1594
	 */
1595 5
	public function disable($reason = "", $recursive = true) {
1596 5
		if (!$this->guid) {
1597
			return false;
1598
		}
1599
1600 5
		if (!_elgg_services()->hooks->getEvents()->trigger('disable', $this->type, $this)) {
1601
			return false;
1602
		}
1603
1604 5
		if (!$this->canEdit()) {
1605
			return false;
1606
		}
1607
1608 5
		if ($this instanceof ElggUser && !$this->isBanned()) {
1609
			// temporarily ban to prevent using the site during disable
1610 1
			$this->ban();
1611 1
			$unban_after = true;
1612
		} else {
1613 4
			$unban_after = false;
1614
		}
1615
1616 5
		if ($reason) {
1617
			$this->disable_reason = $reason;
1618
		}
1619
1620 5
		$dbprefix = _elgg_config()->dbprefix;
1621
1622 5
		$guid = (int) $this->guid;
1623
1624 5
		if ($recursive) {
1625
			// Only disable enabled subentities
1626 5
			$hidden = access_get_show_hidden_status();
1627 5
			access_show_hidden_entities(false);
1628
1629 5
			$ia = elgg_set_ignore_access(true);
1630
1631
			$base_options = [
1632 5
				'wheres' => [
1633 5
					"e.guid != $guid",
1634
				],
1635
				'limit' => false,
1636
			];
1637
1638 5
			foreach (['owner_guid', 'container_guid'] as $db_column) {
1639 5
				$options = $base_options;
1640 5
				$options[$db_column] = $guid;
1641
1642 5
				$subentities = new \ElggBatch('elgg_get_entities', $options);
1643 5
				$subentities->setIncrementOffset(false);
1644
1645 5
				foreach ($subentities as $subentity) {
1646
					/* @var $subentity \ElggEntity */
1647 2
					if (!$subentity->isEnabled()) {
1648
						continue;
1649
					}
1650 2
					add_entity_relationship($subentity->guid, 'disabled_with', $guid);
1651 5
					$subentity->disable($reason);
1652
				}
1653
			}
1654
1655 5
			access_show_hidden_entities($hidden);
1656 5
			elgg_set_ignore_access($ia);
1657
		}
1658
1659 5
		$this->disableAnnotations();
1660
1661
		$sql = "
1662 5
			UPDATE {$dbprefix}entities
1663
			SET enabled = 'no'
1664
			WHERE guid = :guid
1665
		";
1666
		$params = [
1667 5
			':guid' => $guid,
1668
		];
1669 5
		$disabled = $this->getDatabase()->updateData($sql, false, $params);
1670
1671 5
		if ($unban_after) {
1672 1
			$this->unban();
1673
		}
1674
1675 5
		if ($disabled) {
1676 5
			$this->invalidateCache();
1677
1678 5
			$this->attributes['enabled'] = 'no';
1679 5
			_elgg_services()->hooks->getEvents()->trigger('disable:after', $this->type, $this);
1680
		}
1681
1682 5
		return (bool) $disabled;
1683
	}
1684
1685
	/**
1686
	 * Enable the entity
1687
	 *
1688
	 * @warning Disabled entities can't be loaded unless
1689
	 * {@link access_show_hidden_entities(true)} has been called.
1690
	 *
1691
	 * @param bool $recursive Recursively enable all entities disabled with the entity?
1692
	 * @see access_show_hiden_entities()
1693
	 * @return bool
1694
	 */
1695 3
	public function enable($recursive = true) {
1696 3
		$guid = (int) $this->guid;
1697 3
		if (!$guid) {
1698
			return false;
1699
		}
1700
1701 3
		if (!_elgg_services()->hooks->getEvents()->trigger('enable', $this->type, $this)) {
1702
			return false;
1703
		}
1704
1705 3
		if (!$this->canEdit()) {
1706
			return false;
1707
		}
1708
1709
		// Override access only visible entities
1710 3
		$old_access_status = access_get_show_hidden_status();
1711 3
		access_show_hidden_entities(true);
1712
1713 3
		$db = $this->getDatabase();
1714 3
		$result = $db->updateData("
1715 3
			UPDATE {$db->prefix}entities
1716
			SET enabled = 'yes'
1717 3
			WHERE guid = $guid
1718
		");
1719
1720 3
		$this->deleteMetadata('disable_reason');
1721 3
		$this->enableAnnotations();
1722
1723 3
		if ($recursive) {
1724 3
			$disabled_with_it = elgg_get_entities([
1725 3
				'relationship' => 'disabled_with',
1726 3
				'relationship_guid' => $guid,
1727
				'inverse_relationship' => true,
1728 3
				'limit' => 0,
1729
			]);
1730
1731 3
			foreach ($disabled_with_it as $e) {
1732 1
				$e->enable();
1733 1
				remove_entity_relationship($e->guid, 'disabled_with', $guid);
1734
			}
1735
		}
1736
1737 3
		access_show_hidden_entities($old_access_status);
1738
1739 3
		if ($result) {
1740 3
			$this->attributes['enabled'] = 'yes';
1741 3
			_elgg_services()->hooks->getEvents()->trigger('enable:after', $this->type, $this);
1742
		}
1743
1744 3
		return $result;
1745
	}
1746
1747
	/**
1748
	 * Is this entity enabled?
1749
	 *
1750
	 * @return boolean Whether this entity is enabled.
1751
	 */
1752 8
	public function isEnabled() {
1753 8
		return $this->enabled == 'yes';
1754
	}
1755
1756
	/**
1757
	 * Deletes the entity.
1758
	 *
1759
	 * Removes the entity and its metadata, annotations, relationships,
1760
	 * river entries, and private data.
1761
	 *
1762
	 * Optionally can remove entities contained and owned by this entity.
1763
	 *
1764
	 * @warning If deleting recursively, this bypasses ownership of items contained by
1765
	 * the entity.  That means that if the container_guid = $this->guid, the item will
1766
	 * be deleted regardless of who owns it.
1767
	 *
1768
	 * @param bool $recursive If true (default) then all entities which are
1769
	 *                        owned or contained by $this will also be deleted.
1770
	 *
1771
	 * @return bool
1772
	 */
1773 459
	public function delete($recursive = true) {
1774
		// first check if we can delete this entity
1775
		// NOTE: in Elgg <= 1.10.3 this was after the delete event,
1776
		// which could potentially remove some content if the user didn't have access
1777 459
		if (!$this->canDelete()) {
1778 247
			return false;
1779
		}
1780
1781
		try {
1782 215
			return _elgg_services()->entityTable->delete($this, $recursive);
1783
		} catch (DatabaseException $ex) {
1784
			elgg_log($ex->getMessage(), 'ERROR');
1785
			return false;
1786
		}
1787
	}
1788
1789
	/**
1790
	 * {@inheritdoc}
1791
	 */
1792 5
	public function toObject() {
1793 5
		$object = $this->prepareObject(new stdClass());
1794 5
		$params = ['entity' => $this];
1795 5
		$object = _elgg_services()->hooks->trigger('to:object', 'entity', $params, $object);
1796 5
		return $object;
1797
	}
1798
1799
	/**
1800
	 * Prepare an object copy for toObject()
1801
	 *
1802
	 * @param stdClass $object Object representation of the entity
1803
	 * @return stdClass
1804
	 */
1805 5
	protected function prepareObject($object) {
1806 5
		$object->guid = $this->guid;
1807 5
		$object->type = $this->getType();
1808 5
		$object->subtype = $this->getSubtype();
1809 5
		$object->owner_guid = $this->getOwnerGUID();
1810 5
		$object->container_guid = $this->getContainerGUID();
1811 5
		$object->time_created = date('c', $this->getTimeCreated());
1812 5
		$object->time_updated = date('c', $this->getTimeUpdated());
1813 5
		$object->url = $this->getURL();
1814 5
		$object->read_access = (int) $this->access_id;
1815 5
		return $object;
1816
	}
1817
1818
	/*
1819
	 * LOCATABLE INTERFACE
1820
	 */
1821
1822
	/**
1823
	 * Gets the 'location' metadata for the entity
1824
	 *
1825
	 * @return string The location
1826
	 */
1827
	public function getLocation() {
1828
		return $this->location;
1829
	}
1830
1831
	/**
1832
	 * Sets the 'location' metadata for the entity
1833
	 *
1834
	 * @param string $location String representation of the location
1835
	 *
1836
	 * @return void
1837
	 */
1838
	public function setLocation($location) {
1839
		$this->location = $location;
1840
	}
1841
1842
	/**
1843
	 * Set latitude and longitude metadata tags for a given entity.
1844
	 *
1845
	 * @param float $lat  Latitude
1846
	 * @param float $long Longitude
1847
	 *
1848
	 * @return void
1849
	 * @todo Unimplemented
1850
	 */
1851
	public function setLatLong($lat, $long) {
1852
		$this->{"geo:lat"} = $lat;
1853
		$this->{"geo:long"} = $long;
1854
	}
1855
1856
	/**
1857
	 * Return the entity's latitude.
1858
	 *
1859
	 * @return float
1860
	 * @todo Unimplemented
1861
	 */
1862 3
	public function getLatitude() {
1863 3
		return (float) $this->{"geo:lat"};
1864
	}
1865
1866
	/**
1867
	 * Return the entity's longitude
1868
	 *
1869
	 * @return float
1870
	 * @todo Unimplemented
1871
	 */
1872 3
	public function getLongitude() {
1873 3
		return (float) $this->{"geo:long"};
1874
	}
1875
1876
	/*
1877
	 * SYSTEM LOG INTERFACE
1878
	 */
1879
1880
	/**
1881
	 * Return an identification for the object for storage in the system log.
1882
	 * This id must be an integer.
1883
	 *
1884
	 * @return int
1885
	 */
1886 350
	public function getSystemLogID() {
1887 350
		return $this->getGUID();
1888
	}
1889
1890
	/**
1891
	 * For a given ID, return the object associated with it.
1892
	 * This is used by the system log. It can be called on any Loggable object.
1893
	 *
1894
	 * @param int $id GUID.
1895
	 * @return int GUID
1896
	 */
1897
	public function getObjectFromID($id) {
1898
		return get_entity($id);
1899
	}
1900
1901
	/**
1902
	 * Returns tags for this entity.
1903
	 *
1904
	 * @warning Tags must be registered by {@link elgg_register_tag_metadata_name()}.
1905
	 *
1906
	 * @param array $tag_names Optionally restrict by tag metadata names.
1907
	 *
1908
	 * @return array
1909
	 */
1910
	public function getTags($tag_names = null) {
1911
		if ($tag_names && !is_array($tag_names)) {
1912
			$tag_names = [$tag_names];
1913
		}
1914
1915
		$valid_tags = elgg_get_registered_tag_metadata_names();
1916
		$entity_tags = [];
1917
1918
		foreach ($valid_tags as $tag_name) {
1919
			if (is_array($tag_names) && !in_array($tag_name, $tag_names)) {
1920
				continue;
1921
			}
1922
1923
			if ($tags = $this->$tag_name) {
1924
				// if a single tag, metadata returns a string.
1925
				// if multiple tags, metadata returns an array.
1926
				if (is_array($tags)) {
1927
					$entity_tags = array_merge($entity_tags, $tags);
1928
				} else {
1929
					$entity_tags[] = $tags;
1930
				}
1931
			}
1932
		}
1933
1934
		return $entity_tags;
1935
	}
1936
1937
	/**
1938
	 * Remove the membership of all access collections for this entity (if the entity is a user)
1939
	 *
1940
	 * @return bool
1941
	 * @since 1.11
1942
	 */
1943 214
	public function deleteAccessCollectionMemberships() {
1944
1945 214
		if (!$this->guid) {
1946
			return false;
1947
		}
1948
1949 214
		if ($this->type !== 'user') {
1950 191
			return true;
1951
		}
1952
1953 70
		$ac = _elgg_services()->accessCollections;
1954
1955 70
		$collections = $ac->getCollectionsByMember($this->guid);
1956 70
		if (empty($collections)) {
1957 69
			return true;
1958
		}
1959
1960 4
		$result = true;
1961 4
		foreach ($collections as $collection) {
1962 4
			$result = $result & $ac->removeUser($this->guid, $collection->id);
1963
		}
1964
1965 4
		return $result;
1966
	}
1967
1968
	/**
1969
	 * Remove all access collections owned by this entity
1970
	 *
1971
	 * @return bool
1972
	 * @since 1.11
1973
	 */
1974 214
	public function deleteOwnedAccessCollections() {
1975
1976 214
		if (!$this->guid) {
1977
			return false;
1978
		}
1979
1980 214
		$collections = $this->getOwnedAccessCollections();
1981 214
		if (empty($collections)) {
1982 208
			return true;
1983
		}
1984
1985 45
		$result = true;
1986 45
		foreach ($collections as $collection) {
1987 45
			$result = $result & $collection->delete();
1988
		}
1989
1990 45
		return $result;
1991
	}
1992
1993
	/**
1994
	 * Update the last_action column in the entities table.
1995
	 *
1996
	 * @warning This is different to time_updated.  Time_updated is automatically set,
1997
	 * while last_action is only set when explicitly called.
1998
	 *
1999
	 * @param int $posted Timestamp of last action
2000
	 * @return int|false
2001
	 * @access private
2002
	 */
2003 122
	public function updateLastAction($posted = null) {
2004 122
		$posted = _elgg_services()->entityTable->updateLastAction($this, $posted);
2005 122
		if ($posted) {
2006 122
			$this->attributes['last_action'] = $posted;
2007 122
			$this->cache();
2008
		}
2009 122
		return $posted;
2010
	}
2011
2012
	/**
2013
	 * Disable runtime caching for entity
2014
	 *
2015
	 * @return void
2016
	 * @internal
2017
	 */
2018 2
	public function disableCaching() {
2019 2
		$this->_is_cacheable = false;
2020 2
		if ($this->guid) {
2021 2
			_elgg_services()->entityCache->delete($this->guid);
2022
		}
2023 2
	}
2024
2025
	/**
2026
	 * Enable runtime caching for entity
2027
	 *
2028
	 * @return void
2029
	 * @internal
2030
	 */
2031 1
	public function enableCaching() {
2032 1
		$this->_is_cacheable = true;
2033 1
	}
2034
2035
	/**
2036
	 * Is entity cacheable in the runtime cache
2037
	 *
2038
	 * @return bool
2039
	 * @internal
2040
	 */
2041 5308
	public function isCacheable() {
2042 5308
		if (_elgg_services()->session->getIgnoreAccess()) {
2043 827
			return false;
2044
		}
2045 5118
		return $this->_is_cacheable;
2046
	}
2047
2048
	/**
2049
	 * Cache the entity in a session and persisted caches
2050
	 *
2051
	 * @param bool $persist Store in persistent cache
2052
	 *
2053
	 * @return void
2054
	 * @internal
2055
	 */
2056 5313
	public function cache($persist = true) {
2057 5313
		if (!$this->isCacheable()) {
2058 828
			return;
2059
		}
2060
2061 5125
		_elgg_services()->entityCache->save($this);
2062
2063 5125
		if ($persist) {
2064 5125
			$tmp = $this->volatile;
2065
2066
			// don't store volatile data
2067 5125
			$this->volatile = [];
2068
2069 5125
			_elgg_services()->dataCache->entities->save($this->guid, $this);
2070
2071 5125
			$this->volatile = $tmp;
2072
		}
2073 5125
	}
2074
2075
	/**
2076
	 * Invalidate cache for entity
2077
	 *
2078
	 * @return void
2079
	 * @internal
2080
	 */
2081 1068
	public function invalidateCache() {
2082 1068
		if (!$this->guid) {
2083
			return;
2084
		}
2085
2086 1068
		_elgg_services()->boot->invalidateCache();
2087
2088 1068
		_elgg_services()->entityCache->delete($this->guid);
2089
2090
		$namespaces = [
2091 1068
			'entities',
2092
			'metadata',
2093
			'private_settings',
2094
		];
2095
2096 1068
		foreach ($namespaces as $namespace) {
2097 1068
			_elgg_services()->dataCache->get($namespace)->delete($this->guid);
2098
		}
2099 1068
	}
2100
}
2101