Passed
Push — master ( 03ce1e...93bb93 )
by Jeroen
23:30 queued 15s
created

ElggEntity::delete()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7.6024

Importance

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