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