Completed
Push — master ( caf222...deba87 )
by Jeroen
72:32 queued 44:47
created

engine/classes/ElggEntity.php (2 issues)

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