1<?php
2
3use Elgg\EntityIcon;
4use Elgg\Database\QueryBuilder;
5use Elgg\Database\Update;
6
7/**
8 * The parent class for all Elgg Entities.
9 *
10 * An \ElggEntity is one of the basic data models in Elgg.
11 * It is the primary means of storing and retrieving data from the database.
12 * An \ElggEntity represents one row of the entities table.
13 *
14 * The \ElggEntity class handles CRUD operations for the entities table.
15 * \ElggEntity should always be extended by another class to handle CRUD
16 * operations on the type-specific table.
17 *
18 * \ElggEntity uses magic methods for get and set, so any property that isn't
19 * declared will be assumed to be metadata and written to the database
20 * as metadata on the object.  All children classes must declare which
21 * properties are columns of the type table or they will be assumed
22 * to be metadata.  See \ElggObject::initializeAttributes() for examples.
23 *
24 * Core supports 4 types of entities: \ElggObject, \ElggUser, \ElggGroup, and \ElggSite.
25 *
26 * @tip Plugin authors will want to extend the \ElggObject class, not this class.
27 *
28 * @property       string $type           object, user, group, or site (read-only after save)
29 * @property       string $subtype        Further clarifies the nature of the entity
30 * @property-read  int    $guid           The unique identifier for this entity (read only)
31 * @property       int    $owner_guid     The GUID of the owner of this entity (usually the creator)
32 * @property       int    $container_guid The GUID of the entity containing this entity
33 * @property       int    $access_id      Specifies the visibility level of this entity
34 * @property       int    $time_created   A UNIX timestamp of when the entity was created
35 * @property-read  int    $time_updated   A UNIX timestamp of when the entity was last updated (automatically updated on save)
36 * @property-read  int    $last_action    A UNIX timestamp of when the entity was last acted upon
37 * @property       string $enabled        Is this entity enabled ('yes' or 'no')
38 *
39 * Metadata (the above are attributes)
40 * @property       string $location       A location of the entity
41 */
42abstract class ElggEntity extends \ElggData implements
43	Locatable, // Geocoding interface
44	EntityIcon // Icon interface
45{
46
47	public static $primary_attr_names = [
48		'guid',
49		'type',
50		'subtype',
51		'owner_guid',
52		'container_guid',
53		'access_id',
54		'time_created',
55		'time_updated',
56		'last_action',
57		'enabled',
58	];
59
60	protected static $integer_attr_names = [
61		'guid',
62		'owner_guid',
63		'container_guid',
64		'access_id',
65		'time_created',
66		'time_updated',
67		'last_action',
68	];
69
70	/**
71	 * Holds metadata until entity is saved.  Once the entity is saved,
72	 * metadata are written immediately to the database.
73	 * @var array
74	 */
75	protected $temp_metadata = [];
76
77	/**
78	 * Holds annotations until entity is saved.  Once the entity is saved,
79	 * annotations are written immediately to the database.
80	 * @var array
81	 */
82	protected $temp_annotations = [];
83
84	/**
85	 * Holds private settings until entity is saved. Once the entity is saved,
86	 * private settings are written immediately to the database.
87	 * @var array
88	 */
89	protected $temp_private_settings = [];
90
91	/**
92	 * Volatile data structure for this object, allows for storage of data
93	 * in-memory that isn't sync'd back to the metadata table.
94	 * @var array
95	 */
96	protected $volatile = [];
97
98	/**
99	 * Holds the original (persisted) attribute values that have been changed but not yet saved.
100	 * @var array
101	 */
102	protected $orig_attributes = [];
103
104	/**
105	 * @var bool
106	 */
107	protected $_is_cacheable = true;
108
109	/**
110	 * Holds metadata key/value pairs acquired from the metadata cache
111	 * Entity metadata may have mutated since last call to __get,
112	 * do not rely on this value for any business logic
113	 * This storage is intended to help with debugging objects during dump,
114	 * because otherwise it's hard to tell what the object is from it's attributes
115	 *
116	 * @var array
117	 * @internal
118	 */
119	protected $_cached_metadata;
120
121	/**
122	 * Create a new entity.
123	 *
124	 * Plugin developers should only use the constructor to create a new entity.
125	 * To retrieve entities, use get_entity() and the elgg_get_entities* functions.
126	 *
127	 * If no arguments are passed, it creates a new entity.
128	 * If a database result is passed as a \stdClass instance, it instantiates
129	 * that entity.
130	 *
131	 * @param stdClass $row Database row result. Default is null to create a new object.
132	 *
133	 * @throws IOException If cannot load remaining data from db
134	 */
135	public function __construct(stdClass $row = null) {
136		$this->initializeAttributes();
137
138		if ($row && !$this->load($row)) {
139			$msg = "Failed to load new " . get_class() . " for GUID:" . $row->guid;
140			throw new \IOException($msg);
141		}
142	}
143
144	/**
145	 * Initialize the attributes array.
146	 *
147	 * This is vital to distinguish between metadata and base parameters.
148	 *
149	 * @return void
150	 */
151	protected function initializeAttributes() {
152		parent::initializeAttributes();
153
154		$this->attributes['guid'] = null;
155		$this->attributes['type'] = $this->getType();
156		$this->attributes['subtype'] = null;
157
158		$this->attributes['owner_guid'] = _elgg_services()->session->getLoggedInUserGuid();
159		$this->attributes['container_guid'] = _elgg_services()->session->getLoggedInUserGuid();
160
161		$this->attributes['access_id'] = ACCESS_PRIVATE;
162		$this->attributes['time_updated'] = null;
163		$this->attributes['last_action'] = null;
164		$this->attributes['enabled'] = "yes";
165	}
166
167	/**
168	 * Clone an entity
169	 *
170	 * Resets the guid so that the entity can be saved as a distinct entity from
171	 * the original. Creation time will be set when this new entity is saved.
172	 * The owner and container guids come from the original entity. The clone
173	 * method copies metadata but does not copy annotations or private settings.
174	 *
175	 * @return void
176	 */
177	public function __clone() {
178		$orig_entity = get_entity($this->guid);
179		if (!$orig_entity) {
180			_elgg_services()->logger->error("Failed to clone entity with GUID $this->guid");
181			return;
182		}
183
184		$metadata_array = elgg_get_metadata([
185			'guid' => $this->guid,
186			'limit' => 0
187		]);
188
189		$this->attributes['guid'] = null;
190		$this->attributes['time_created'] = null;
191		$this->attributes['time_updated'] = null;
192		$this->attributes['last_action'] = null;
193
194		$this->attributes['subtype'] = $orig_entity->getSubtype();
195
196		// copy metadata over to new entity - slightly convoluted due to
197		// handling of metadata arrays
198		if (is_array($metadata_array)) {
199			// create list of metadata names
200			$metadata_names = [];
201			foreach ($metadata_array as $metadata) {
202				$metadata_names[] = $metadata->name;
203			}
204			// arrays are stored with multiple enties per name
205			$metadata_names = array_unique($metadata_names);
206
207			// move the metadata over
208			foreach ($metadata_names as $name) {
209				$this->__set($name, $orig_entity->$name);
210			}
211		}
212	}
213
214	/**
215	 * Set an attribute or metadata value for this entity
216	 *
217	 * Anything that is not an attribute is saved as metadata.
218	 *
219	 * Be advised that metadata values are cast to integer or string.
220	 * You can save booleans, but they will be stored and returned as integers.
221	 *
222	 * @param string $name  Name of the attribute or metadata
223	 * @param mixed  $value The value to be set
224	 * @return void
225	 * @see \ElggEntity::setMetadata()
226	 */
227	public function __set($name, $value) {
228		if ($this->$name === $value) {
229			// quick return if value is not changing
230			return;
231		}
232
233		if (array_key_exists($name, $this->attributes)) {
234			// if an attribute is 1 (integer) and it's set to "1" (string), don't consider that a change.
235			if (is_int($this->attributes[$name])
236					&& is_string($value)
237					&& ((string) $this->attributes[$name] === $value)) {
238				return;
239			}
240
241			// keep original values
242			if ($this->guid && !array_key_exists($name, $this->orig_attributes)) {
243				$this->orig_attributes[$name] = $this->attributes[$name];
244			}
245
246			// Certain properties should not be manually changed!
247			switch ($name) {
248				case 'guid':
249				case 'time_updated':
250				case 'last_action':
251					return;
252				case 'access_id':
253				case 'owner_guid':
254				case 'container_guid':
255					if ($value !== null) {
256						$this->attributes[$name] = (int) $value;
257					} else {
258						$this->attributes[$name] = null;
259					}
260					break;
261				default:
262					$this->attributes[$name] = $value;
263					break;
264			}
265			return;
266		}
267
268		$this->setMetadata($name, $value);
269	}
270
271	/**
272	 * Get the original values of attribute(s) that have been modified since the entity was persisted.
273	 *
274	 * @return array
275	 */
276	public function getOriginalAttributes() {
277		return $this->orig_attributes;
278	}
279
280	/**
281	 * Get an attribute or metadata value
282	 *
283	 * If the name matches an attribute, the attribute is returned. If metadata
284	 * does not exist with that name, a null is returned.
285	 *
286	 * This only returns an array if there are multiple values for a particular
287	 * $name key.
288	 *
289	 * @param string $name Name of the attribute or metadata
290	 * @return mixed
291	 */
292	public function __get($name) {
293		if (array_key_exists($name, $this->attributes)) {
294			return $this->attributes[$name];
295		}
296
297		return $this->getMetadata($name);
298	}
299
300	/**
301	 * Get the entity's display name
302	 *
303	 * @return string The title or name of this entity.
304	 */
305	public function getDisplayName() {
306		return $this->name;
307	}
308
309	/**
310	 * Sets the title or name of this entity.
311	 *
312	 * @param string $display_name The title or name of this entity.
313	 * @return void
314	 */
315	public function setDisplayName($display_name) {
316		$this->name = $display_name;
317	}
318
319	/**
320	 * Return the value of a piece of metadata.
321	 *
322	 * @param string $name Name
323	 *
324	 * @return mixed The value, or null if not found.
325	 */
326	public function getMetadata($name) {
327		$metadata = $this->getAllMetadata();
328		return elgg_extract($name, $metadata);
329	}
330
331	/**
332	 * Get all entity metadata
333	 *
334	 * @return array
335	 */
336	public function getAllMetadata() {
337		if (!$this->guid) {
338			return array_map(function($values) {
339				return count($values) > 1 ? $values : $values[0];
340			}, $this->temp_metadata);
341		}
342
343		$this->_cached_metadata = _elgg_services()->metadataCache->getAll($this->guid);
344
345		return $this->_cached_metadata;
346	}
347
348	/**
349	 * Set metadata on this entity.
350	 *
351	 * Plugin developers usually want to use the magic set method ($entity->name = 'value').
352	 * Use this method if you want to explicitly set the owner or access of the metadata.
353	 * You cannot set the owner/access before the entity has been saved.
354	 *
355	 * @param string $name       Name of the metadata
356	 * @param mixed  $value      Value of the metadata (doesn't support assoc arrays)
357	 * @param string $value_type 'text', 'integer', or '' for automatic detection
358	 * @param bool   $multiple   Allow multiple values for a single name.
359	 *                           Does not support associative arrays.
360	 *
361	 * @return bool
362	 * @throws InvalidArgumentException
363	 */
364	public function setMetadata($name, $value, $value_type = '', $multiple = false) {
365
366		if ($value === null) {
367			return $this->deleteMetadata($name);
368		}
369
370		// normalize value to an array that we will loop over
371		// remove indexes if value already an array.
372		if (is_array($value)) {
373			$value = array_values($value);
374		} else {
375			$value = [$value];
376		}
377
378		// strip null values from array
379		$value = array_filter($value, function($var) {
380			return !is_null($var);
381		});
382
383		if (empty($this->guid)) {
384			// unsaved entity. store in temp array
385			return $this->setTempMetadata($name, $value, $multiple);
386		}
387
388		// saved entity. persist md to db.
389		if (!$multiple) {
390			$current_metadata = $this->getMetadata($name);
391
392			if ((is_array($current_metadata) || count($value) > 1 || $value === []) && isset($current_metadata)) {
393				// remove current metadata if needed
394				// need to remove access restrictions right now to delete
395				// because this is the expected behavior
396				$delete_result = elgg_call(ELGG_IGNORE_ACCESS, function() use ($name) {
397					return elgg_delete_metadata([
398						'guid' => $this->guid,
399						'metadata_name' => $name,
400						'limit' => false,
401					]);
402				});
403
404				if (false === $delete_result) {
405					return false;
406				}
407			}
408
409			if (count($value) > 1) {
410				// new value is a multiple valued metadata
411				$multiple = true;
412			}
413		}
414
415		// create new metadata
416		foreach ($value as $value_tmp) {
417			$metadata = new ElggMetadata();
418			$metadata->entity_guid = $this->guid;
419			$metadata->name = $name;
420			$metadata->value_type = $value_type;
421			$metadata->value = $value_tmp;
422			$md_id = _elgg_services()->metadataTable->create($metadata, $multiple);
423			if ($md_id === false) {
424				return false;
425			}
426		}
427
428		return true;
429	}
430
431	/**
432	 * Set temp metadata on this entity.
433	 *
434	 * @param string $name     Name of the metadata
435	 * @param mixed  $value    Value of the metadata (doesn't support assoc arrays)
436	 * @param bool   $multiple Allow multiple values for a single name.
437	 *                         Does not support associative arrays.
438	 *
439	 * @return bool
440	 */
441	protected function setTempMetadata($name, $value, $multiple = false) {
442		// if overwrite, delete first
443		if (!$multiple) {
444			unset($this->temp_metadata[$name]);
445			if (count($value)) {
446				// only save if value array contains data
447				$this->temp_metadata[$name] = $value;
448			}
449			return true;
450		}
451
452		if (!isset($this->temp_metadata[$name])) {
453			$this->temp_metadata[$name] = [];
454		}
455
456		$this->temp_metadata[$name] = array_merge($this->temp_metadata[$name], $value);
457
458		return true;
459	}
460
461
462
463	/**
464	 * Deletes all metadata on this object (metadata.entity_guid = $this->guid).
465	 * If you pass a name, only metadata matching that name will be deleted.
466	 *
467	 * @warning Calling this with no $name will clear all metadata on the entity.
468	 *
469	 * @param null|string $name The name of the metadata to remove.
470	 * @return bool|null
471	 * @since 1.8
472	 */
473	public function deleteMetadata($name = null) {
474
475		if (!$this->guid) {
476			// remove from temp_metadata
477			if ($name) {
478				if (!isset($this->temp_metadata[$name])) {
479					return null;
480				} else {
481					unset($this->temp_metadata[$name]);
482					return true;
483				}
484			} else {
485				$this->temp_metadata = [];
486				return true;
487			}
488		}
489
490		$options = [
491			'guid' => $this->guid,
492			'limit' => 0
493		];
494		if ($name) {
495			$options['metadata_name'] = $name;
496		}
497
498		return elgg_delete_metadata($options);
499	}
500
501	/**
502	 * Get a piece of volatile (non-persisted) data on this entity.
503	 *
504	 * @param string $name The name of the volatile data
505	 *
506	 * @return mixed The value or null if not found.
507	 */
508	public function getVolatileData($name) {
509		return array_key_exists($name, $this->volatile) ? $this->volatile[$name] : null;
510	}
511
512	/**
513	 * Set a piece of volatile (non-persisted) data on this entity
514	 *
515	 * @param string $name  Name
516	 * @param mixed  $value Value
517	 *
518	 * @return void
519	 */
520	public function setVolatileData($name, $value) {
521		$this->volatile[$name] = $value;
522	}
523
524	/**
525	 * Remove all relationships to and from this entity.
526	 * If you pass a relationship name, only relationships matching that name
527	 * will be deleted.
528	 *
529	 * @warning Calling this with no $relationship will clear all relationships
530	 * for this entity.
531	 *
532	 * @param null|string $relationship The name of the relationship to remove.
533	 * @return bool
534	 * @see \ElggEntity::addRelationship()
535	 * @see \ElggEntity::removeRelationship()
536	 */
537	public function deleteRelationships($relationship = null) {
538		$relationship = (string) $relationship;
539		$result = remove_entity_relationships($this->getGUID(), $relationship);
540		return $result && remove_entity_relationships($this->getGUID(), $relationship, true);
541	}
542
543	/**
544	 * Add a relationship between this an another entity.
545	 *
546	 * @tip Read the relationship like "This entity is a $relationship of $guid_two."
547	 *
548	 * @param int    $guid_two     GUID of the target entity of the relationship.
549	 * @param string $relationship The type of relationship.
550	 *
551	 * @return bool
552	 * @see \ElggEntity::removeRelationship()
553	 * @see \ElggEntity::deleteRelationships()
554	 */
555	public function addRelationship($guid_two, $relationship) {
556		return add_entity_relationship($this->getGUID(), $relationship, $guid_two);
557	}
558
559	/**
560	 * Remove a relationship
561	 *
562	 * @param int    $guid_two     GUID of the target entity of the relationship.
563	 * @param string $relationship The type of relationship.
564	 *
565	 * @return bool
566	 * @see \ElggEntity::addRelationship()
567	 * @see \ElggEntity::deleteRelationships()
568	 */
569	public function removeRelationship($guid_two, $relationship) {
570		return remove_entity_relationship($this->getGUID(), $relationship, $guid_two);
571	}
572
573	/**
574	 * Adds a private setting to this entity.
575	 *
576	 * @warning Private settings are stored as strings
577	 *          Unlike metadata and annotations there is no reverse casting when you retrieve the setting
578	 *          When saving integers, they will be cast to strings
579	 *          Booleans will be cast to '0' and '1'
580	 *
581	 * @param string $name  Name of private setting
582	 * @param mixed  $value Value of private setting
583	 *
584	 * @return bool
585	 * @throws DatabaseException
586	 */
587	public function setPrivateSetting($name, $value) {
588
589		if ($value === null) {
590			return $this->removePrivateSetting($name);
591		}
592
593		if (is_bool($value)) {
594			$value = (int) $value;
595		}
596
597		// checking if string value matches saved value (as get privatesettings does not know value type) and db column is a string
598		if (strval($value) === $this->getPrivateSetting($name)) {
599			// no need to update value
600			return true;
601		}
602
603		if (!$this->guid) {
604			$this->temp_private_settings[$name] = $value;
605
606			return true;
607		}
608
609		return _elgg_services()->privateSettings->set($this, $name, $value);
610	}
611
612	/**
613	 * Returns a private setting value
614	 *
615	 * @warning Private settings are always returned as strings
616	 *          Make sure you can your values back to expected type
617	 *
618	 * @param string $name Name of the private setting
619	 *
620	 * @return string|null
621	 * @throws DatabaseException
622	 */
623	public function getPrivateSetting($name) {
624		if (!$this->guid) {
625			return elgg_extract($name, $this->temp_private_settings);
626		}
627
628		return _elgg_services()->privateSettings->get($this, $name);
629	}
630
631	/**
632	 * Returns all private settings
633	 *
634	 * @return array
635	 * @throws DatabaseException
636	 */
637	public function getAllPrivateSettings() {
638		if (!$this->guid) {
639			return $this->temp_private_settings;
640		}
641
642		return _elgg_services()->privateSettings->getAllForEntity($this);
643	}
644
645	/**
646	 * Removes private setting
647	 *
648	 * @param string $name Name of the private setting
649	 *
650	 * @return bool
651	 * @throws DatabaseException
652	 */
653	public function removePrivateSetting($name) {
654		if (!$this->guid) {
655			unset($this->temp_private_settings[$name]);
656			return true;
657		}
658
659		return _elgg_services()->privateSettings->remove($this, $name);
660	}
661
662	/**
663	 * Removes all private settings
664	 *
665	 * @return bool
666	 * @throws DatabaseException
667	 */
668	public function removeAllPrivateSettings() {
669		if (!$this->guid) {
670			$this->temp_private_settings = [];
671			return true;
672		}
673
674		return _elgg_services()->privateSettings->removeAllForEntity($this);
675	}
676
677	/**
678	 * Removes all river items related to this entity
679	 *
680	 * @return void
681	 */
682	public function removeAllRelatedRiverItems() {
683		elgg_delete_river(['subject_guid' => $this->guid, 'limit' => false]);
684		elgg_delete_river(['object_guid' => $this->guid, 'limit' => false]);
685		elgg_delete_river(['target_guid' => $this->guid, 'limit' => false]);
686	}
687
688	/**
689	 * Deletes all annotations on this object (annotations.entity_guid = $this->guid).
690	 * If you pass a name, only annotations matching that name will be deleted.
691	 *
692	 * @warning Calling this with no or empty arguments will clear all annotations on the entity.
693	 *
694	 * @param null|string $name The annotations name to remove.
695	 * @return bool
696	 * @since 1.8
697	 */
698	public function deleteAnnotations($name = null) {
699		if ($this->guid) {
700			$options = [
701				'guid' => $this->guid,
702				'limit' => 0
703			];
704			if ($name) {
705				$options['annotation_name'] = $name;
706			}
707
708			return elgg_delete_annotations($options);
709		}
710
711		if ($name) {
712			unset($this->temp_annotations[$name]);
713		} else {
714			$this->temp_annotations = [];
715		}
716
717		return true;
718	}
719
720	/**
721	 * Deletes all annotations owned by this object (annotations.owner_guid = $this->guid).
722	 * If you pass a name, only annotations matching that name will be deleted.
723	 *
724	 * @param null|string $name The name of annotations to delete.
725	 * @return bool
726	 * @since 1.8
727	 */
728	public function deleteOwnedAnnotations($name = null) {
729		// access is turned off for this because they might
730		// no longer have access to an entity they created annotations on
731
732		$flags = ELGG_IGNORE_ACCESS;
733		$callback = function() use ($name) {
734			return elgg_delete_annotations([
735				'annotation_owner_guid' => $this->guid,
736				'limit' => 0,
737				'annotation_name' => $name,
738			]);
739		};
740
741		return elgg_call($flags, $callback);
742	}
743
744	/**
745	 * Disables annotations for this entity, optionally based on name.
746	 *
747	 * @param string $name An options name of annotations to disable.
748	 * @return bool
749	 * @since 1.8
750	 */
751	public function disableAnnotations($name = '') {
752		$options = [
753			'guid' => $this->guid,
754			'limit' => 0
755		];
756		if ($name) {
757			$options['annotation_name'] = $name;
758		}
759
760		return elgg_disable_annotations($options);
761	}
762
763	/**
764	 * Enables annotations for this entity, optionally based on name.
765	 *
766	 * @param string $name An options name of annotations to enable.
767	 * @return bool
768	 * @since 1.8
769	 */
770	public function enableAnnotations($name = '') {
771		$options = [
772			'guid' => $this->guid,
773			'limit' => 0
774		];
775		if ($name) {
776			$options['annotation_name'] = $name;
777		}
778
779		return elgg_enable_annotations($options);
780	}
781
782	/**
783	 * Helper function to return annotation calculation results
784	 *
785	 * @param string $name        The annotation name.
786	 * @param string $calculation A valid MySQL function to run its values through
787	 * @return mixed
788	 */
789	private function getAnnotationCalculation($name, $calculation) {
790		$options = [
791			'guid' => $this->getGUID(),
792			'distinct' => false,
793			'annotation_name' => $name,
794			'annotation_calculation' => $calculation
795		];
796
797		return elgg_get_annotations($options);
798	}
799
800	/**
801	 * Adds an annotation to an entity.
802	 *
803	 * @warning By default, annotations are private.
804	 *
805	 * @warning Annotating an unsaved entity more than once with the same name
806	 *          will only save the last annotation.
807	 *
808	 * @todo Update temp_annotations to store an instance of ElggAnnotation and simply call ElggAnnotation::save(),
809	 *       after entity is saved
810	 *
811	 * @param string $name       Annotation name
812	 * @param mixed  $value      Annotation value
813	 * @param int    $access_id  Access ID
814	 * @param int    $owner_guid GUID of the annotation owner
815	 * @param string $value_type The type of annotation value
816	 *
817	 * @return bool|int Returns int if an annotation is saved
818	 */
819	public function annotate($name, $value, $access_id = ACCESS_PRIVATE, $owner_guid = 0, $value_type = "") {
820		if ($this->guid) {
821			if (!$owner_guid) {
822				$owner_guid = _elgg_services()->session->getLoggedInUserGuid();
823			}
824			$annotation = new ElggAnnotation();
825			$annotation->entity_guid = $this->guid;
826			$annotation->name = $name;
827			$annotation->value_type = $value_type;
828			$annotation->value = $value;
829			$annotation->owner_guid = $owner_guid;
830			$annotation->access_id = $access_id;
831			return $annotation->save();
832		} else {
833			$this->temp_annotations[$name] = $value;
834		}
835		return true;
836	}
837
838	/**
839	 * Gets an array of annotations.
840	 *
841	 * To retrieve annotations on an unsaved entity, pass array('name' => [annotation name])
842	 * as the options array.
843	 *
844	 * @param array $options Array of options for elgg_get_annotations() except guid.
845	 *
846	 * @return \ElggAnnotation[]|mixed
847	 * @see elgg_get_annotations()
848	 */
849	public function getAnnotations(array $options = []) {
850		if ($this->guid) {
851			$options['guid'] = $this->guid;
852
853			return elgg_get_annotations($options);
854		} else {
855			$name = elgg_extract('annotation_name', $options, '');
856
857			if (isset($this->temp_annotations[$name])) {
858				return [$this->temp_annotations[$name]];
859			}
860		}
861
862		return [];
863	}
864
865	/**
866	 * Count annotations.
867	 *
868	 * @param string $name The type of annotation.
869	 *
870	 * @return int
871	 */
872	public function countAnnotations($name = "") {
873		return $this->getAnnotationCalculation($name, 'count');
874	}
875
876	/**
877	 * Get the average of an integer type annotation.
878	 *
879	 * @param string $name Annotation name
880	 *
881	 * @return int
882	 */
883	public function getAnnotationsAvg($name) {
884		return $this->getAnnotationCalculation($name, 'avg');
885	}
886
887	/**
888	 * Get the sum of integer type annotations of a given name.
889	 *
890	 * @param string $name Annotation name
891	 *
892	 * @return int
893	 */
894	public function getAnnotationsSum($name) {
895		return $this->getAnnotationCalculation($name, 'sum');
896	}
897
898	/**
899	 * Get the minimum of integer type annotations of given name.
900	 *
901	 * @param string $name Annotation name
902	 *
903	 * @return int
904	 */
905	public function getAnnotationsMin($name) {
906		return $this->getAnnotationCalculation($name, 'min');
907	}
908
909	/**
910	 * Get the maximum of integer type annotations of a given name.
911	 *
912	 * @param string $name Annotation name
913	 *
914	 * @return int
915	 */
916	public function getAnnotationsMax($name) {
917		return $this->getAnnotationCalculation($name, 'max');
918	}
919
920	/**
921	 * Count the number of comments attached to this entity.
922	 *
923	 * @return int Number of comments
924	 * @since 1.8.0
925	 */
926	public function countComments() {
927		$params = ['entity' => $this];
928		$num = _elgg_services()->hooks->trigger('comments:count', $this->getType(), $params);
929
930		if (is_int($num)) {
931			return $num;
932		}
933
934		return elgg_count_entities([
935			'type' => 'object',
936			'subtype' => 'comment',
937			'container_guid' => $this->getGUID(),
938			'distinct' => false,
939		]);
940	}
941
942	/**
943	 * Returns the ACLs owned by the entity
944	 *
945	 * @param array $options additional options to get the access collections with
946	 *
947	 * @return \ElggAccessCollection[]
948	 *
949	 * @see elgg_get_access_collections()
950	 * @since 3.0
951	 */
952	public function getOwnedAccessCollections($options = []) {
953		$options['owner_guid'] = $this->guid;
954		return _elgg_services()->accessCollections->getEntityCollections($options);
955	}
956
957	/**
958	 * Returns the first ACL owned by the entity with a given subtype
959	 *
960	 * @param string $subtype subtype of the ACL
961	 *
962	 * @return \ElggAccessCollection|false
963	 *
964	 * @since 3.0
965	 */
966	public function getOwnedAccessCollection($subtype) {
967		if (!is_string($subtype) || $subtype === '') {
968			return false;
969		}
970
971		$acls = $this->getOwnedAccessCollections([
972			'subtype' => $subtype,
973		]);
974
975		return elgg_extract(0, $acls, false);
976	}
977
978	/**
979	 * Gets an array of entities with a relationship to this entity.
980	 *
981	 * @param array $options Options array. See elgg_get_entities()
982	 *                       for a list of options. 'relationship_guid' is set to
983	 *                       this entity.
984	 *
985	 * @return \ElggEntity[]|int|mixed
986	 * @see elgg_get_entities()
987	 */
988	public function getEntitiesFromRelationship(array $options = []) {
989		$options['relationship_guid'] = $this->guid;
990		return elgg_get_entities($options);
991	}
992
993	/**
994	 * Gets the number of entities from a specific relationship type
995	 *
996	 * @param string $relationship         Relationship type (eg "friends")
997	 * @param bool   $inverse_relationship Invert relationship
998	 *
999	 * @return int
1000	 */
1001	public function countEntitiesFromRelationship($relationship, $inverse_relationship = false) {
1002		return elgg_count_entities([
1003			'relationship' => $relationship,
1004			'relationship_guid' => $this->getGUID(),
1005			'inverse_relationship' => $inverse_relationship,
1006		]);
1007	}
1008
1009	/**
1010	 * Can a user edit this entity?
1011	 *
1012	 * @tip Can be overridden by registering for the permissions_check plugin hook.
1013	 *
1014	 * @param int $user_guid The user GUID, optionally (default: logged in user)
1015	 *
1016	 * @return bool Whether this entity is editable by the given user.
1017	 */
1018	public function canEdit($user_guid = 0) {
1019		return _elgg_services()->userCapabilities->canEdit($this, $user_guid);
1020	}
1021
1022	/**
1023	 * Can a user delete this entity?
1024	 *
1025	 * @tip Can be overridden by registering for the permissions_check:delete plugin hook.
1026	 *
1027	 * @param int $user_guid The user GUID, optionally (default: logged in user)
1028	 *
1029	 * @return bool Whether this entity is deletable by the given user.
1030	 * @since 1.11
1031	 */
1032	public function canDelete($user_guid = 0) {
1033		return _elgg_services()->userCapabilities->canDelete($this, $user_guid);
1034	}
1035
1036	/**
1037	 * Can a user edit metadata on this entity?
1038	 *
1039	 * If no specific metadata is passed, it returns whether the user can
1040	 * edit any metadata on the entity.
1041	 *
1042	 * @param \ElggMetadata $metadata  The piece of metadata to specifically check or null for any metadata
1043	 * @param int           $user_guid The user GUID, optionally (default: logged in user)
1044	 *
1045	 * @return bool
1046	 * @deprecated 3.0
1047	 */
1048	public function canEditMetadata($metadata = null, $user_guid = 0) {
1049		return _elgg_services()->userCapabilities->canEditMetadata($this, $user_guid, $metadata);
1050	}
1051
1052	/**
1053	 * Can a user add an entity to this container
1054	 *
1055	 * @param int    $user_guid The GUID of the user creating the entity (0 for logged in user).
1056	 * @param string $type      The type of entity we're looking to write
1057	 * @param string $subtype   The subtype of the entity we're looking to write
1058	 *
1059	 * @return bool
1060	 */
1061	public function canWriteToContainer($user_guid = 0, $type = 'all', $subtype = 'all') {
1062		return _elgg_services()->userCapabilities->canWriteToContainer($this, $user_guid, $type, $subtype);
1063	}
1064
1065	/**
1066	 * Can a user comment on an entity?
1067	 *
1068	 * @tip Can be overridden by registering for the permissions_check:comment,
1069	 * <entity type> plugin hook.
1070	 *
1071	 * @param int  $user_guid User guid (default is logged in user)
1072	 * @param bool $default   Default permission
1073	 *
1074	 * @return bool|null
1075	 */
1076	public function canComment($user_guid = 0, $default = null) {
1077		return _elgg_services()->userCapabilities->canComment($this, $user_guid, $default);
1078	}
1079
1080	/**
1081	 * Can a user annotate an entity?
1082	 *
1083	 * @tip Can be overridden by registering for the plugin hook [permissions_check:annotate:<name>,
1084	 * <entity type>] or [permissions_check:annotate, <entity type>]. The hooks are called in that order.
1085	 *
1086	 * @tip If you want logged out users to annotate an object, do not call
1087	 * canAnnotate(). It's easier than using the plugin hook.
1088	 *
1089	 * @param int    $user_guid       User guid (default is logged in user)
1090	 * @param string $annotation_name The name of the annotation (default is unspecified)
1091	 *
1092	 * @return bool
1093	 */
1094	public function canAnnotate($user_guid = 0, $annotation_name = '') {
1095		return _elgg_services()->userCapabilities->canAnnotate($this, $user_guid, $annotation_name);
1096	}
1097
1098	/**
1099	 * Returns the access_id.
1100	 *
1101	 * @return int The access ID
1102	 */
1103	public function getAccessID() {
1104		return $this->access_id;
1105	}
1106
1107	/**
1108	 * Returns the guid.
1109	 *
1110	 * @return int|null GUID
1111	 */
1112	public function getGUID() {
1113		return $this->guid;
1114	}
1115
1116	/**
1117	 * Returns the entity type
1118	 *
1119	 * @return string The entity type
1120	 */
1121	public function getType() {
1122		// this is just for the PHPUnit mocking framework
1123		return $this->type;
1124	}
1125
1126	/**
1127	 * Get the entity subtype
1128	 *
1129	 * @return string The entity subtype
1130	 */
1131	public function getSubtype() {
1132		return $this->attributes['subtype'];
1133	}
1134
1135	/**
1136	 * Get the guid of the entity's owner.
1137	 *
1138	 * @return int The owner GUID
1139	 */
1140	public function getOwnerGUID() {
1141		return (int) $this->owner_guid;
1142	}
1143
1144	/**
1145	 * Gets the \ElggEntity that owns this entity.
1146	 *
1147	 * @return \ElggEntity The owning entity
1148	 */
1149	public function getOwnerEntity() {
1150		return get_entity($this->owner_guid);
1151	}
1152
1153	/**
1154	 * Set the container for this object.
1155	 *
1156	 * @param int $container_guid The ID of the container.
1157	 *
1158	 * @return int
1159	 */
1160	public function setContainerGUID($container_guid) {
1161		return $this->container_guid = (int) $container_guid;
1162	}
1163
1164	/**
1165	 * Gets the container GUID for this entity.
1166	 *
1167	 * @return int
1168	 */
1169	public function getContainerGUID() {
1170		return (int) $this->container_guid;
1171	}
1172
1173	/**
1174	 * Get the container entity for this object.
1175	 *
1176	 * @return \ElggEntity
1177	 * @since 1.8.0
1178	 */
1179	public function getContainerEntity() {
1180		return get_entity($this->getContainerGUID());
1181	}
1182
1183	/**
1184	 * Returns the UNIX epoch time that this entity was last updated
1185	 *
1186	 * @return int UNIX epoch time
1187	 */
1188	public function getTimeUpdated() {
1189		return $this->time_updated;
1190	}
1191
1192	/**
1193	 * Gets the URL for this entity.
1194	 *
1195	 * Plugins can register for the 'entity:url', <type> plugin hook to
1196	 * customize the url for an entity.
1197	 *
1198	 * @return string The URL of the entity
1199	 */
1200	public function getURL() {
1201		$url = elgg_generate_entity_url($this, 'view');
1202
1203		$url = _elgg_services()->hooks->trigger('entity:url', $this->getType(), ['entity' => $this], $url);
1204
1205		if (empty($url)) {
1206			return '';
1207		}
1208
1209		return elgg_normalize_url($url);
1210	}
1211
1212	/**
1213	 * Saves icons using an uploaded file as the source.
1214	 *
1215	 * @param string $input_name Form input name
1216	 * @param string $type       The name of the icon. e.g., 'icon', 'cover_photo'
1217	 * @param array  $coords     An array of cropping coordinates x1, y1, x2, y2
1218	 * @return bool
1219	 */
1220	public function saveIconFromUploadedFile($input_name, $type = 'icon', array $coords = []) {
1221		return _elgg_services()->iconService->saveIconFromUploadedFile($this, $input_name, $type, $coords);
1222	}
1223
1224	/**
1225	 * Saves icons using a local file as the source.
1226	 *
1227	 * @param string $filename The full path to the local file
1228	 * @param string $type     The name of the icon. e.g., 'icon', 'cover_photo'
1229	 * @param array  $coords   An array of cropping coordinates x1, y1, x2, y2
1230	 * @return bool
1231	 */
1232	public function saveIconFromLocalFile($filename, $type = 'icon', array $coords = []) {
1233		return _elgg_services()->iconService->saveIconFromLocalFile($this, $filename, $type, $coords);
1234	}
1235
1236	/**
1237	 * Saves icons using a file located in the data store as the source.
1238	 *
1239	 * @param string $file   An ElggFile instance
1240	 * @param string $type   The name of the icon. e.g., 'icon', 'cover_photo'
1241	 * @param array  $coords An array of cropping coordinates x1, y1, x2, y2
1242	 * @return bool
1243	 */
1244	public function saveIconFromElggFile(\ElggFile $file, $type = 'icon', array $coords = []) {
1245		return _elgg_services()->iconService->saveIconFromElggFile($this, $file, $type, $coords);
1246	}
1247
1248	/**
1249	 * Returns entity icon as an ElggIcon object
1250	 * The icon file may or may not exist on filestore
1251	 *
1252	 * @param string $size Size of the icon
1253	 * @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
1254	 * @return \ElggIcon
1255	 */
1256	public function getIcon($size, $type = 'icon') {
1257		return _elgg_services()->iconService->getIcon($this, $size, $type);
1258	}
1259
1260	/**
1261	 * Removes all icon files and metadata for the passed type of icon.
1262	 *
1263	 * @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
1264	 * @return bool
1265	 */
1266	public function deleteIcon($type = 'icon') {
1267		return _elgg_services()->iconService->deleteIcon($this, $type);
1268	}
1269
1270	/**
1271	 * Returns the timestamp of when the icon was changed.
1272	 *
1273	 * @param string $size The size of the icon
1274	 * @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
1275	 *
1276	 * @return int|null A unix timestamp of when the icon was last changed, or null if not set.
1277	 */
1278	public function getIconLastChange($size, $type = 'icon') {
1279		return _elgg_services()->iconService->getIconLastChange($this, $size, $type);
1280	}
1281
1282	/**
1283	 * Returns if the entity has an icon of the passed type.
1284	 *
1285	 * @param string $size The size of the icon
1286	 * @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
1287	 * @return bool
1288	 */
1289	public function hasIcon($size, $type = 'icon') {
1290		return _elgg_services()->iconService->hasIcon($this, $size, $type);
1291	}
1292
1293	/**
1294	 * Get the URL for this entity's icon
1295	 *
1296	 * Plugins can register for the 'entity:icon:url', <type> plugin hook
1297	 * to customize the icon for an entity.
1298	 *
1299	 * @param mixed $params A string defining the size of the icon (e.g. tiny, small, medium, large)
1300	 *                      or an array of parameters including 'size'
1301	 * @return string The URL
1302	 * @since 1.8.0
1303	 */
1304	public function getIconURL($params = []) {
1305		return _elgg_services()->iconService->getIconURL($this, $params);
1306	}
1307
1308	/**
1309	 * Save an entity.
1310	 *
1311	 * @return bool|int
1312	 */
1313	public function save() {
1314		$guid = $this->guid;
1315		if ($guid > 0) {
1316			$guid = $this->update();
1317		} else {
1318			$guid = $this->create();
1319			if ($guid === false) {
1320				return false;
1321			}
1322
1323			if (!_elgg_services()->events->trigger('create', $this->type, $this)) {
1324				// plugins that return false to event don't need to override the access system
1325				elgg_call(ELGG_IGNORE_ACCESS, function() {
1326					return $this->delete();
1327				});
1328				return false;
1329			}
1330		}
1331
1332		if ($guid) {
1333			$this->cache();
1334		}
1335
1336		return $guid;
1337	}
1338
1339	/**
1340	 * Create a new entry in the entities table.
1341	 *
1342	 * Saves the base information in the entities table for the entity.  Saving
1343	 * the type-specific information is handled in the calling class method.
1344	 *
1345	 * @return int|false The new entity's GUID or false on failure
1346	 * @throws InvalidParameterException If the entity's type has not been set.
1347	 * @throws IOException If the new row fails to write to the DB.
1348	 */
1349	protected function create() {
1350
1351		$type = $this->attributes['type'];
1352		if (!in_array($type, \Elgg\Config::getEntityTypes())) {
1353			throw new \InvalidParameterException('Entity type must be one of the allowed types: '
1354					. implode(', ', \Elgg\Config::getEntityTypes()));
1355		}
1356
1357		$subtype = $this->attributes['subtype'];
1358		if (!$subtype) {
1359			throw new \InvalidParameterException("All entities must have a subtype");
1360		}
1361
1362		$owner_guid = (int) $this->attributes['owner_guid'];
1363		$access_id = (int) $this->attributes['access_id'];
1364		$now = $this->getCurrentTime()->getTimestamp();
1365		$time_created = isset($this->attributes['time_created']) ? (int) $this->attributes['time_created'] : $now;
1366
1367		$container_guid = $this->attributes['container_guid'];
1368		if ($container_guid == 0) {
1369			$container_guid = $owner_guid;
1370			$this->attributes['container_guid'] = $container_guid;
1371		}
1372		$container_guid = (int) $container_guid;
1373
1374		if ($access_id == ACCESS_DEFAULT) {
1375			throw new \InvalidParameterException('ACCESS_DEFAULT is not a valid access level. See its documentation in constants.php');
1376		}
1377
1378		if ($access_id == ACCESS_FRIENDS) {
1379			throw new \InvalidParameterException('ACCESS_FRIENDS is not a valid access level. See its documentation in constants.php');
1380		}
1381
1382		$user_guid = _elgg_services()->session->getLoggedInUserGuid();
1383
1384		// If given an owner, verify it can be loaded
1385		if ($owner_guid) {
1386			$owner = $this->getOwnerEntity();
1387			if (!$owner) {
1388				_elgg_services()->logger->error("User $user_guid tried to create a ($type, $subtype), but the given"
1389					. " owner $owner_guid could not be loaded.");
1390				return false;
1391			}
1392
1393			// If different owner than logged in, verify can write to container.
1394
1395			if ($user_guid != $owner_guid && !$owner->canEdit() && !$owner->canWriteToContainer($user_guid, $type, $subtype)) {
1396				_elgg_services()->logger->error("User $user_guid tried to create a ($type, $subtype) with owner"
1397					. " $owner_guid, but the user wasn't permitted to write to the owner's container.");
1398				return false;
1399			}
1400		}
1401
1402		// If given a container, verify it can be loaded and that the current user can write to it
1403		if ($container_guid) {
1404			$container = $this->getContainerEntity();
1405			if (!$container) {
1406				_elgg_services()->logger->error("User $user_guid tried to create a ($type, $subtype), but the given"
1407					. " container $container_guid could not be loaded.");
1408				return false;
1409			}
1410
1411			if (!$container->canWriteToContainer($user_guid, $type, $subtype)) {
1412				_elgg_services()->logger->error("User $user_guid tried to create a ($type, $subtype), but was not"
1413					. " permitted to write to container $container_guid.");
1414				return false;
1415			}
1416		}
1417
1418		// Create primary table row
1419		$guid = _elgg_services()->entityTable->insertRow((object) [
1420			'type' => $type,
1421			'subtype' => $subtype,
1422			'owner_guid' => $owner_guid,
1423			'container_guid' => $container_guid,
1424			'access_id' => $access_id,
1425			'time_created' => $time_created,
1426			'time_updated' => $now,
1427			'last_action' => $now,
1428		], $this->attributes);
1429
1430		if (!$guid) {
1431			throw new \IOException("Unable to save new object's base entity information!");
1432		}
1433
1434		$this->attributes['subtype'] = $subtype;
1435		$this->attributes['guid'] = (int) $guid;
1436		$this->attributes['time_created'] = (int) $time_created;
1437		$this->attributes['time_updated'] = (int) $now;
1438		$this->attributes['last_action'] = (int) $now;
1439		$this->attributes['container_guid'] = (int) $container_guid;
1440
1441		// We are writing this new entity to cache to make sure subsequent calls
1442		// to get_entity() load the entity from cache and not from the DB. This
1443		// MUST come before the metadata and annotation writes below!
1444		$this->cache();
1445
1446		// Save any unsaved metadata
1447		if (sizeof($this->temp_metadata) > 0) {
1448			foreach ($this->temp_metadata as $name => $value) {
1449				if (count($value) == 1) {
1450					// temp metadata is always an array, but if there is only one value return just the value
1451					$this->$name = $value[0];
1452				} else {
1453					$this->$name = $value;
1454				}
1455			}
1456
1457			$this->temp_metadata = [];
1458		}
1459
1460		// Save any unsaved annotations.
1461		if (sizeof($this->temp_annotations) > 0) {
1462			foreach ($this->temp_annotations as $name => $value) {
1463				$this->annotate($name, $value);
1464			}
1465
1466			$this->temp_annotations = [];
1467		}
1468
1469		// Save any unsaved private settings.
1470		if (sizeof($this->temp_private_settings) > 0) {
1471			foreach ($this->temp_private_settings as $name => $value) {
1472				$this->setPrivateSetting($name, $value);
1473			}
1474
1475			$this->temp_private_settings = [];
1476		}
1477
1478		if (isset($container) && !$container instanceof ElggUser) {
1479			// users have their own logic for setting last action
1480			$container->updateLastAction();
1481		}
1482
1483		return $guid;
1484	}
1485
1486	/**
1487	 * Update the entity in the database.
1488	 *
1489	 * @return bool Whether the update was successful.
1490	 *
1491	 * @throws InvalidParameterException
1492	 */
1493	protected function update() {
1494
1495		if (!$this->canEdit()) {
1496			return false;
1497		}
1498
1499		// give old update event a chance to stop the update
1500		if (!_elgg_services()->events->trigger('update', $this->type, $this)) {
1501			return false;
1502		}
1503
1504		$this->invalidateCache();
1505
1506		// See #6225. We copy these after the update event in case a handler changed one of them.
1507		$guid = (int) $this->guid;
1508		$owner_guid = (int) $this->owner_guid;
1509		$access_id = (int) $this->access_id;
1510		$container_guid = (int) $this->container_guid;
1511		$time_created = (int) $this->time_created;
1512		$time = $this->getCurrentTime()->getTimestamp();
1513
1514		if ($access_id == ACCESS_DEFAULT) {
1515			throw new \InvalidParameterException('ACCESS_DEFAULT is not a valid access level. See its documentation in constants.php');
1516		}
1517
1518		if ($access_id == ACCESS_FRIENDS) {
1519			throw new \InvalidParameterException('ACCESS_FRIENDS is not a valid access level. See its documentation in constants.php');
1520		}
1521
1522		// Update primary table
1523		$ret = _elgg_services()->entityTable->updateRow($guid, (object) [
1524			'owner_guid' => $owner_guid,
1525			'container_guid' => $container_guid,
1526			'access_id' => $access_id,
1527			'time_created' => $time_created,
1528			'time_updated' => $time,
1529			'guid' => $guid,
1530		]);
1531		if ($ret === false) {
1532			return false;
1533		}
1534
1535		$this->attributes['time_updated'] = $time;
1536
1537		_elgg_services()->events->triggerAfter('update', $this->type, $this);
1538
1539		$this->orig_attributes = [];
1540
1541		$this->cache();
1542
1543		// Handle cases where there was no error BUT no rows were updated!
1544		return true;
1545	}
1546
1547	/**
1548	 * Loads attributes from the entities table into the object.
1549	 *
1550	 * @param stdClass $row Object of properties from database row(s)
1551	 *
1552	 * @return bool
1553	 */
1554	protected function load(stdClass $row) {
1555		$attributes = array_merge($this->attributes, (array) $row);
1556
1557		if (array_diff(self::$primary_attr_names, array_keys($attributes)) !== []) {
1558			// Some primary attributes are missing
1559			return false;
1560		}
1561
1562		foreach ($attributes as $name => $value) {
1563			if (!in_array($name, self::$primary_attr_names)) {
1564				$this->setVolatileData("select:$name", $value);
1565				unset($attributes[$name]);
1566				continue;
1567			}
1568
1569			if (in_array($name, self::$integer_attr_names)) {
1570				$attributes[$name] = (int) $value;
1571			}
1572		}
1573
1574		$this->attributes = $attributes;
1575
1576		$this->cache();
1577
1578		return true;
1579	}
1580
1581	/**
1582	 * Load new data from database into existing entity. Overwrites data but
1583	 * does not change values not included in the latest data.
1584	 *
1585	 * @internal This is used when the same entity is selected twice during a
1586	 * request in case different select clauses were used to load different data
1587	 * into volatile data.
1588	 *
1589	 * @param stdClass $row DB row with new entity data
1590	 *
1591	 * @return bool
1592	 * @internal
1593	 */
1594	public function refresh(stdClass $row) {
1595		return $this->load($row);
1596	}
1597
1598	/**
1599	 * Disable this entity.
1600	 *
1601	 * Disabled entities are not returned by getter functions.
1602	 * To enable an entity, use {@link \ElggEntity::enable()}.
1603	 *
1604	 * Recursively disabling an entity will disable all entities
1605	 * owned or contained by the parent entity.
1606	 *
1607	 * @note Internal: Disabling an entity sets the 'enabled' column to 'no'.
1608	 *
1609	 * @param string $reason    Optional reason
1610	 * @param bool   $recursive Recursively disable all contained entities?
1611	 *
1612	 * @return bool
1613	 * @see \ElggEntity::enable()
1614	 */
1615	public function disable($reason = "", $recursive = true) {
1616		if (!$this->guid) {
1617			return false;
1618		}
1619
1620		if (!_elgg_services()->events->trigger('disable', $this->type, $this)) {
1621			return false;
1622		}
1623
1624		if (!$this->canEdit()) {
1625			return false;
1626		}
1627
1628		if ($this instanceof ElggUser && !$this->isBanned()) {
1629			// temporarily ban to prevent using the site during disable
1630			$this->ban();
1631			$unban_after = true;
1632		} else {
1633			$unban_after = false;
1634		}
1635
1636		if ($reason) {
1637			$this->disable_reason = $reason;
1638		}
1639
1640		$guid = (int) $this->guid;
1641
1642		if ($recursive) {
1643			elgg_call(ELGG_IGNORE_ACCESS | ELGG_HIDE_DISABLED_ENTITIES, function () use ($guid, $reason) {
1644				$base_options = [
1645					'wheres' => [
1646						function(QueryBuilder $qb, $main_alias) use ($guid) {
1647							return $qb->compare("{$main_alias}.guid", '!=', $guid, ELGG_VALUE_GUID);
1648						},
1649					],
1650					'limit' => false,
1651					'batch' => true,
1652					'batch_inc_offset' => false,
1653				];
1654
1655				foreach (['owner_guid', 'container_guid'] as $db_column) {
1656					$options = $base_options;
1657					$options[$db_column] = $guid;
1658
1659					$subentities = elgg_get_entities($options);
1660
1661					foreach ($subentities as $subentity) {
1662						/* @var $subentity \ElggEntity */
1663						if (!$subentity->isEnabled()) {
1664							continue;
1665						}
1666						add_entity_relationship($subentity->guid, 'disabled_with', $guid);
1667						$subentity->disable($reason, true);
1668					}
1669				}
1670			});
1671		}
1672
1673		$this->disableAnnotations();
1674
1675		$qb = Update::table('entities');
1676		$qb->set('enabled', $qb->param('no', ELGG_VALUE_STRING))
1677			->where($qb->compare('guid', '=', $guid, ELGG_VALUE_GUID));
1678
1679		$disabled = $this->getDatabase()->updateData($qb);
1680
1681		if ($unban_after) {
1682			$this->unban();
1683		}
1684
1685		if ($disabled) {
1686			$this->invalidateCache();
1687
1688			$this->attributes['enabled'] = 'no';
1689			_elgg_services()->events->triggerAfter('disable', $this->type, $this);
1690		}
1691
1692		return (bool) $disabled;
1693	}
1694
1695	/**
1696	 * Enable the entity
1697	 *
1698	 * @param bool $recursive Recursively enable all entities disabled with the entity?
1699	 * @see access_show_hiden_entities()
1700	 * @return bool
1701	 */
1702	public function enable($recursive = true) {
1703		$guid = (int) $this->guid;
1704		if (!$guid) {
1705			return false;
1706		}
1707
1708		if (!_elgg_services()->events->trigger('enable', $this->type, $this)) {
1709			return false;
1710		}
1711
1712		if (!$this->canEdit()) {
1713			return false;
1714		}
1715
1716		$result = elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function() use ($guid, $recursive) {
1717			$qb = Update::table('entities');
1718			$qb->set('enabled', $qb->param('yes', ELGG_VALUE_STRING))
1719				->where($qb->compare('guid', '=', $guid, ELGG_VALUE_GUID));
1720
1721			$result = $this->getDatabase()->updateData($qb);
1722
1723			$this->deleteMetadata('disable_reason');
1724			$this->enableAnnotations();
1725
1726			if ($recursive) {
1727				$disabled_with_it = elgg_get_entities([
1728					'relationship' => 'disabled_with',
1729					'relationship_guid' => $guid,
1730					'inverse_relationship' => true,
1731					'limit' => false,
1732					'batch' => true,
1733					'batch_inc_offset' => false,
1734				]);
1735
1736				foreach ($disabled_with_it as $e) {
1737					$e->enable($recursive);
1738					remove_entity_relationship($e->guid, 'disabled_with', $guid);
1739				}
1740			}
1741
1742			return $result;
1743		});
1744
1745		if ($result) {
1746			$this->attributes['enabled'] = 'yes';
1747			_elgg_services()->events->triggerAfter('enable', $this->type, $this);
1748		}
1749
1750		return $result;
1751	}
1752
1753	/**
1754	 * Is this entity enabled?
1755	 *
1756	 * @return boolean Whether this entity is enabled.
1757	 */
1758	public function isEnabled() {
1759		return $this->enabled == 'yes';
1760	}
1761
1762	/**
1763	 * Deletes the entity.
1764	 *
1765	 * Removes the entity and its metadata, annotations, relationships,
1766	 * river entries, and private data.
1767	 *
1768	 * Optionally can remove entities contained and owned by this entity.
1769	 *
1770	 * @warning If deleting recursively, this bypasses ownership of items contained by
1771	 * the entity.  That means that if the container_guid = $this->guid, the item will
1772	 * be deleted regardless of who owns it.
1773	 *
1774	 * @param bool $recursive If true (default) then all entities which are
1775	 *                        owned or contained by $this will also be deleted.
1776	 *
1777	 * @return bool
1778	 */
1779	public function delete($recursive = true) {
1780		// first check if we can delete this entity
1781		// NOTE: in Elgg <= 1.10.3 this was after the delete event,
1782		// which could potentially remove some content if the user didn't have access
1783		if (!$this->canDelete()) {
1784			return false;
1785		}
1786
1787		try {
1788			return _elgg_services()->entityTable->delete($this, $recursive);
1789		} catch (DatabaseException $ex) {
1790			elgg_log($ex, 'ERROR');
1791			return false;
1792		}
1793	}
1794
1795	/**
1796	 * Export an entity
1797	 *
1798	 * @param array $params Params to pass to the hook
1799	 * @return \Elgg\Export\Entity
1800	 */
1801	public function toObject(array $params = []) {
1802		$object = $this->prepareObject(new \Elgg\Export\Entity());
1803
1804		$params['entity'] = $this;
1805
1806		return _elgg_services()->hooks->trigger('to:object', 'entity', $params, $object);
1807	}
1808
1809	/**
1810	 * Prepare an object copy for toObject()
1811	 *
1812	 * @param \Elgg\Export\Entity $object Object representation of the entity
1813	 * @return \Elgg\Export\Entity
1814	 */
1815	protected function prepareObject(\Elgg\Export\Entity $object) {
1816		$object->guid = $this->guid;
1817		$object->type = $this->getType();
1818		$object->subtype = $this->getSubtype();
1819		$object->owner_guid = $this->getOwnerGUID();
1820		$object->container_guid = $this->getContainerGUID();
1821		$object->time_created = date('c', $this->getTimeCreated());
1822		$object->time_updated = date('c', $this->getTimeUpdated());
1823		$object->url = $this->getURL();
1824		$object->read_access = (int) $this->access_id;
1825		return $object;
1826	}
1827
1828	/*
1829	 * LOCATABLE INTERFACE
1830	 */
1831
1832	/**
1833	 * Gets the 'location' metadata for the entity
1834	 *
1835	 * @return string The location
1836	 */
1837	public function getLocation() {
1838		return $this->location;
1839	}
1840
1841	/**
1842	 * Sets the 'location' metadata for the entity
1843	 *
1844	 * @param string $location String representation of the location
1845	 *
1846	 * @return void
1847	 */
1848	public function setLocation($location) {
1849		$this->location = $location;
1850	}
1851
1852	/**
1853	 * Set latitude and longitude metadata tags for a given entity.
1854	 *
1855	 * @param float $lat  Latitude
1856	 * @param float $long Longitude
1857	 *
1858	 * @return void
1859	 * @todo Unimplemented
1860	 */
1861	public function setLatLong($lat, $long) {
1862		$this->{"geo:lat"} = $lat;
1863		$this->{"geo:long"} = $long;
1864	}
1865
1866	/**
1867	 * Return the entity's latitude.
1868	 *
1869	 * @return float
1870	 * @todo Unimplemented
1871	 */
1872	public function getLatitude() {
1873		return (float) $this->{"geo:lat"};
1874	}
1875
1876	/**
1877	 * Return the entity's longitude
1878	 *
1879	 * @return float
1880	 * @todo Unimplemented
1881	 */
1882	public function getLongitude() {
1883		return (float) $this->{"geo:long"};
1884	}
1885
1886	/*
1887	 * SYSTEM LOG INTERFACE
1888	 */
1889
1890	/**
1891	 * Return an identification for the object for storage in the system log.
1892	 * This id must be an integer.
1893	 *
1894	 * @return int
1895	 */
1896	public function getSystemLogID() {
1897		return $this->getGUID();
1898	}
1899
1900	/**
1901	 * For a given ID, return the object associated with it.
1902	 * This is used by the system log. It can be called on any Loggable object.
1903	 *
1904	 * @param int $id GUID.
1905	 * @return \ElggEntity|false
1906	 */
1907	public function getObjectFromID($id) {
1908		return get_entity($id);
1909	}
1910
1911	/**
1912	 * Returns tags for this entity.
1913	 *
1914	 * @warning Tags must be registered by {@link elgg_register_tag_metadata_name()}.
1915	 *
1916	 * @param array $tag_names Optionally restrict by tag metadata names.
1917	 *
1918	 * @return array
1919	 */
1920	public function getTags($tag_names = null) {
1921		if ($tag_names && !is_array($tag_names)) {
1922			$tag_names = [$tag_names];
1923		}
1924
1925		$valid_tags = elgg_get_registered_tag_metadata_names();
1926		$entity_tags = [];
1927
1928		foreach ($valid_tags as $tag_name) {
1929			if (is_array($tag_names) && !in_array($tag_name, $tag_names)) {
1930				continue;
1931			}
1932
1933			if ($tags = $this->$tag_name) {
1934				// if a single tag, metadata returns a string.
1935				// if multiple tags, metadata returns an array.
1936				if (is_array($tags)) {
1937					$entity_tags = array_merge($entity_tags, $tags);
1938				} else {
1939					$entity_tags[] = $tags;
1940				}
1941			}
1942		}
1943
1944		return $entity_tags;
1945	}
1946
1947	/**
1948	 * Remove the membership of all access collections for this entity (if the entity is a user)
1949	 *
1950	 * @return bool
1951	 * @since 1.11
1952	 */
1953	public function deleteAccessCollectionMemberships() {
1954
1955		if (!$this->guid) {
1956			return false;
1957		}
1958
1959		if ($this->type !== 'user') {
1960			return true;
1961		}
1962
1963		$ac = _elgg_services()->accessCollections;
1964
1965		$collections = $ac->getCollectionsByMember($this->guid);
1966		if (empty($collections)) {
1967			return true;
1968		}
1969
1970		$result = true;
1971		foreach ($collections as $collection) {
1972			$result &= $ac->removeUser($this->guid, $collection->id);
1973		}
1974
1975		return $result;
1976	}
1977
1978	/**
1979	 * Remove all access collections owned by this entity
1980	 *
1981	 * @return bool
1982	 * @since 1.11
1983	 */
1984	public function deleteOwnedAccessCollections() {
1985
1986		if (!$this->guid) {
1987			return false;
1988		}
1989
1990		$collections = $this->getOwnedAccessCollections();
1991		if (empty($collections)) {
1992			return true;
1993		}
1994
1995		$result = true;
1996		foreach ($collections as $collection) {
1997			$result = $result & $collection->delete();
1998		}
1999
2000		return $result;
2001	}
2002
2003	/**
2004	 * Update the last_action column in the entities table.
2005	 *
2006	 * @warning This is different to time_updated.  Time_updated is automatically set,
2007	 * while last_action is only set when explicitly called.
2008	 *
2009	 * @param int $posted Timestamp of last action
2010	 * @return int|false
2011	 * @internal
2012	 */
2013	public function updateLastAction($posted = null) {
2014		$posted = _elgg_services()->entityTable->updateLastAction($this, $posted);
2015		if ($posted) {
2016			$this->attributes['last_action'] = $posted;
2017			$this->cache();
2018		}
2019		return $posted;
2020	}
2021
2022	/**
2023	 * Disable runtime caching for entity
2024	 *
2025	 * @return void
2026	 * @internal
2027	 */
2028	public function disableCaching() {
2029		$this->_is_cacheable = false;
2030		if ($this->guid) {
2031			_elgg_services()->entityCache->delete($this->guid);
2032		}
2033	}
2034
2035	/**
2036	 * Enable runtime caching for entity
2037	 *
2038	 * @return void
2039	 * @internal
2040	 */
2041	public function enableCaching() {
2042		$this->_is_cacheable = true;
2043	}
2044
2045	/**
2046	 * Is entity cacheable in the runtime cache
2047	 *
2048	 * @return bool
2049	 * @internal
2050	 */
2051	public function isCacheable() {
2052		if (!$this->guid) {
2053			return false;
2054		}
2055
2056		if (_elgg_services()->session->getIgnoreAccess()) {
2057			return false;
2058		}
2059		return $this->_is_cacheable;
2060	}
2061
2062	/**
2063	 * Cache the entity in a session and persisted caches
2064	 *
2065	 * @param bool $persist Store in persistent cache
2066	 *
2067	 * @return void
2068	 * @internal
2069	 */
2070	public function cache($persist = true) {
2071		if (!$this->isCacheable()) {
2072			return;
2073		}
2074
2075		_elgg_services()->entityCache->save($this);
2076
2077		if (!$persist) {
2078			return;
2079		}
2080
2081		$tmp = $this->volatile;
2082
2083		// don't store volatile data
2084		$this->volatile = [];
2085
2086		_elgg_services()->dataCache->entities->save($this->guid, $this);
2087
2088		$this->volatile = $tmp;
2089	}
2090
2091	/**
2092	 * Invalidate cache for entity
2093	 *
2094	 * @return void
2095	 * @internal
2096	 */
2097	public function invalidateCache() {
2098		if (!$this->guid) {
2099			return;
2100		}
2101
2102		_elgg_services()->entityCache->delete($this->guid);
2103
2104		$namespaces = [
2105			'entities',
2106			'metadata',
2107			'private_settings',
2108		];
2109
2110		foreach ($namespaces as $namespace) {
2111			_elgg_services()->dataCache->get($namespace)->delete($this->guid);
2112		}
2113	}
2114}
2115