ElggEntity::create()   F
last analyzed

Complexity

Conditions 23
Paths 194

Size

Total Lines 134
Code Lines 78

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 58
CRAP Score 32.9339

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 23
eloc 78
nc 194
nop 0
dl 0
loc 134
ccs 58
cts 79
cp 0.7342
crap 32.9339
rs 3.3833
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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