diff options
Diffstat (limited to 'engine/lib/entities.php')
| -rw-r--r-- | engine/lib/entities.php | 859 |
1 files changed, 583 insertions, 276 deletions
diff --git a/engine/lib/entities.php b/engine/lib/entities.php index cb197b569..4fcf1c657 100644 --- a/engine/lib/entities.php +++ b/engine/lib/entities.php @@ -17,13 +17,50 @@ global $ENTITY_CACHE; $ENTITY_CACHE = array(); /** - * Cache subtypes and related class names once loaded. + * GUIDs of entities banned from the entity cache (during this request) * - * @global array $SUBTYPE_CACHE + * @global array $ENTITY_CACHE_DISABLED_GUIDS + * @access private + */ +global $ENTITY_CACHE_DISABLED_GUIDS; +$ENTITY_CACHE_DISABLED_GUIDS = array(); + +/** + * Cache subtypes and related class names. + * + * @global array|null $SUBTYPE_CACHE array once populated from DB, initially null * @access private */ global $SUBTYPE_CACHE; -$SUBTYPE_CACHE = NULL; +$SUBTYPE_CACHE = null; + +/** + * Remove this entity from the entity cache and make sure it is not re-added + * + * @param int $guid The entity guid + * + * @access private + * @todo this is a workaround until #5604 can be implemented + */ +function _elgg_disable_caching_for_entity($guid) { + global $ENTITY_CACHE_DISABLED_GUIDS; + + _elgg_invalidate_cache_for_entity($guid); + $ENTITY_CACHE_DISABLED_GUIDS[$guid] = true; +} + +/** + * Allow this entity to be stored in the entity cache + * + * @param int $guid The entity guid + * + * @access private + */ +function _elgg_enable_caching_for_entity($guid) { + global $ENTITY_CACHE_DISABLED_GUIDS; + + unset($ENTITY_CACHE_DISABLED_GUIDS[$guid]); +} /** * Invalidate this class's entry in the cache. @@ -33,12 +70,14 @@ $SUBTYPE_CACHE = NULL; * @return void * @access private */ -function invalidate_cache_for_entity($guid) { +function _elgg_invalidate_cache_for_entity($guid) { global $ENTITY_CACHE; $guid = (int)$guid; unset($ENTITY_CACHE[$guid]); + + elgg_get_metadata_cache()->clear($guid); } /** @@ -49,60 +88,59 @@ function invalidate_cache_for_entity($guid) { * @param ElggEntity $entity Entity to cache * * @return void - * @see retrieve_cached_entity() - * @see invalidate_cache_for_entity() + * @see _elgg_retrieve_cached_entity() + * @see _elgg_invalidate_cache_for_entity() * @access private + * @todo Use an ElggCache object */ -function cache_entity(ElggEntity $entity) { - global $ENTITY_CACHE; +function _elgg_cache_entity(ElggEntity $entity) { + global $ENTITY_CACHE, $ENTITY_CACHE_DISABLED_GUIDS; - $ENTITY_CACHE[$entity->guid] = $entity; -} + // Don't cache non-plugin entities while access control is off, otherwise they could be + // exposed to users who shouldn't see them when control is re-enabled. + if (!($entity instanceof ElggPlugin) && elgg_get_ignore_access()) { + return; + } -/** - * Retrieve a entity from the cache. - * - * @param int $guid The guid - * - * @return void - * @see cache_entity() - * @see invalidate_cache_for_entity() - * @access private - */ -function retrieve_cached_entity($guid) { - global $ENTITY_CACHE; + $guid = $entity->getGUID(); + if (isset($ENTITY_CACHE_DISABLED_GUIDS[$guid])) { + return; + } - $guid = (int)$guid; + // Don't store too many or we'll have memory problems + // @todo Pick a less arbitrary limit + if (count($ENTITY_CACHE) > 256) { + $random_guid = array_rand($ENTITY_CACHE); - if (isset($ENTITY_CACHE[$guid])) { - if ($ENTITY_CACHE[$guid]->isFullyLoaded()) { - return $ENTITY_CACHE[$guid]; - } + unset($ENTITY_CACHE[$random_guid]); + + // Purge separate metadata cache. Original idea was to do in entity destructor, but that would + // have caused a bunch of unnecessary purges at every shutdown. Doing it this way we have no way + // to know that the expunged entity will be GCed (might be another reference living), but that's + // OK; the metadata will reload if necessary. + elgg_get_metadata_cache()->clear($random_guid); } - return false; + $ENTITY_CACHE[$guid] = $entity; } /** - * As retrieve_cached_entity, but returns the result as a stdClass - * (compatible with load functions that expect a database row.) + * Retrieve a entity from the cache. * * @param int $guid The guid * - * @return mixed - * @todo unused + * @return ElggEntity|bool false if entity not cached, or not fully loaded + * @see _elgg_cache_entity() + * @see _elgg_invalidate_cache_for_entity() * @access private */ -function retrieve_cached_entity_row($guid) { - $obj = retrieve_cached_entity($guid); - if ($obj) { - $tmp = new stdClass; +function _elgg_retrieve_cached_entity($guid) { + global $ENTITY_CACHE; - foreach ($obj as $k => $v) { - $tmp->$k = $v; + if (isset($ENTITY_CACHE[$guid])) { + if ($ENTITY_CACHE[$guid]->isFullyLoaded()) { + return $ENTITY_CACHE[$guid]; } - - return $tmp; } return false; @@ -124,8 +162,6 @@ function retrieve_cached_entity_row($guid) { * @internal Subtypes are stored in the entity_subtypes table. There is a foreign * key in the entities table. * - * @todo Move to a nicer place? - * * @param string $type Type * @param string $subtype Subtype * @@ -135,75 +171,99 @@ function retrieve_cached_entity_row($guid) { * @access private */ function get_subtype_id($type, $subtype) { - global $CONFIG, $SUBTYPE_CACHE; + global $SUBTYPE_CACHE; - $type = sanitise_string($type); - $subtype = sanitise_string($subtype); - - if ($subtype == "") { - return FALSE; + if (!$subtype) { + return false; } - // Todo: cache here? Or is looping less efficient that going to the db each time? - $result = get_data_row("SELECT * from {$CONFIG->dbprefix}entity_subtypes - where type='$type' and subtype='$subtype'"); - - if ($result) { - if (!$SUBTYPE_CACHE) { - $SUBTYPE_CACHE = array(); - } + if ($SUBTYPE_CACHE === null) { + _elgg_populate_subtype_cache(); + } - $SUBTYPE_CACHE[$result->id] = $result; + // use the cache before hitting database + $result = _elgg_retrieve_cached_subtype($type, $subtype); + if ($result !== null) { return $result->id; } - return FALSE; + return false; } /** * Return string name for a given subtype ID. * - * @todo Move to a nicer place? - * * @param int $subtype_id Subtype ID * - * @return string Subtype name + * @return string|false Subtype name, false if subtype not found * @link http://docs.elgg.org/DataModel/Entities/Subtypes * @see get_subtype_from_id() * @access private */ function get_subtype_from_id($subtype_id) { - global $CONFIG, $SUBTYPE_CACHE; - - $subtype_id = (int)$subtype_id; + global $SUBTYPE_CACHE; if (!$subtype_id) { return false; } + if ($SUBTYPE_CACHE === null) { + _elgg_populate_subtype_cache(); + } + if (isset($SUBTYPE_CACHE[$subtype_id])) { return $SUBTYPE_CACHE[$subtype_id]->subtype; } - $result = get_data_row("SELECT * from {$CONFIG->dbprefix}entity_subtypes where id=$subtype_id"); - if ($result) { - if (!$SUBTYPE_CACHE) { - $SUBTYPE_CACHE = array(); - } + return false; +} - $SUBTYPE_CACHE[$subtype_id] = $result; - return $result->subtype; +/** + * Retrieve subtype from the cache. + * + * @param string $type + * @param string $subtype + * @return stdClass|null + * + * @access private + */ +function _elgg_retrieve_cached_subtype($type, $subtype) { + global $SUBTYPE_CACHE; + + if ($SUBTYPE_CACHE === null) { + _elgg_populate_subtype_cache(); } - return false; + foreach ($SUBTYPE_CACHE as $obj) { + if ($obj->type === $type && $obj->subtype === $subtype) { + return $obj; + } + } + return null; +} + +/** + * Fetch all suptypes from DB to local cache. + * + * @access private + */ +function _elgg_populate_subtype_cache() { + global $CONFIG, $SUBTYPE_CACHE; + + $results = get_data("SELECT * FROM {$CONFIG->dbprefix}entity_subtypes"); + + $SUBTYPE_CACHE = array(); + foreach ($results as $row) { + $SUBTYPE_CACHE[$row->id] = $row; + } } /** - * Return a classname for a registered type and subtype. + * Return the class name for a registered type and subtype. * * Entities can be registered to always be loaded as a certain class - * with {@link register_entity_subtype()}. This function returns - * the class name if found, and NULL if not. + * with add_subtype() or update_subtype(). This function returns the class + * name if found and NULL if not. * * @param string $type The type * @param string $subtype The subtype @@ -214,29 +274,23 @@ function get_subtype_from_id($subtype_id) { * @access private */ function get_subtype_class($type, $subtype) { - global $CONFIG, $SUBTYPE_CACHE; - - $type = sanitise_string($type); - $subtype = sanitise_string($subtype); - - // Todo: cache here? Or is looping less efficient that going to the db each time? - $result = get_data_row("SELECT * from {$CONFIG->dbprefix}entity_subtypes - where type='$type' and subtype='$subtype'"); + global $SUBTYPE_CACHE; - if ($result) { - if (!$SUBTYPE_CACHE) { - $SUBTYPE_CACHE = array(); - } - - $SUBTYPE_CACHE[$result->id] = $result; - return $result->class; + if ($SUBTYPE_CACHE === null) { + _elgg_populate_subtype_cache(); + } + + // use the cache before going to the database + $obj = _elgg_retrieve_cached_subtype($type, $subtype); + if ($obj) { + return $obj->class; } - return NULL; + return null; } /** - * Returns the classname for a subtype id. + * Returns the class name for a subtype id. * * @param int $subtype_id The subtype id * @@ -246,29 +300,21 @@ function get_subtype_class($type, $subtype) { * @access private */ function get_subtype_class_from_id($subtype_id) { - global $CONFIG, $SUBTYPE_CACHE; - - $subtype_id = (int)$subtype_id; + global $SUBTYPE_CACHE; if (!$subtype_id) { - return false; + return null; } + if ($SUBTYPE_CACHE === null) { + _elgg_populate_subtype_cache(); + } + if (isset($SUBTYPE_CACHE[$subtype_id])) { return $SUBTYPE_CACHE[$subtype_id]->class; } - $result = get_data_row("SELECT * from {$CONFIG->dbprefix}entity_subtypes where id=$subtype_id"); - - if ($result) { - if (!$SUBTYPE_CACHE) { - $SUBTYPE_CACHE = array(); - } - $SUBTYPE_CACHE[$subtype_id] = $result; - return $result->class; - } - - return NULL; + return null; } /** @@ -279,6 +325,9 @@ function get_subtype_class_from_id($subtype_id) { * it will be loaded as that class automatically when retrieved from the database with * {@link get_entity()}. * + * @warning This function cannot be used to change the class for a type-subtype pair. + * Use update_subtype() for that. + * * @param string $type The type you're subtyping (site, user, object, or group) * @param string $subtype The subtype * @param string $class Optional class name for the object @@ -291,21 +340,32 @@ function get_subtype_class_from_id($subtype_id) { * @see get_entity() */ function add_subtype($type, $subtype, $class = "") { - global $CONFIG; - $type = sanitise_string($type); - $subtype = sanitise_string($subtype); - $class = sanitise_string($class); + global $CONFIG, $SUBTYPE_CACHE; - // Short circuit if no subtype is given - if ($subtype == "") { + if (!$subtype) { return 0; } $id = get_subtype_id($type, $subtype); - if ($id == 0) { - return insert_data("insert into {$CONFIG->dbprefix}entity_subtypes" - . " (type, subtype, class) values ('$type','$subtype','$class')"); + if (!$id) { + // In cache we store non-SQL-escaped strings because that's what's returned by query + $cache_obj = (object) array( + 'type' => $type, + 'subtype' => $subtype, + 'class' => $class, + ); + + $type = sanitise_string($type); + $subtype = sanitise_string($subtype); + $class = sanitise_string($class); + + $id = insert_data("INSERT INTO {$CONFIG->dbprefix}entity_subtypes" + . " (type, subtype, class) VALUES ('$type', '$subtype', '$class')"); + + // add entry to cache + $cache_obj->id = $id; + $SUBTYPE_CACHE[$id] = $cache_obj; } return $id; @@ -314,6 +374,10 @@ function add_subtype($type, $subtype, $class = "") { /** * Removes a registered ElggEntity type, subtype, and classname. * + * @warning You do not want to use this function. If you want to unregister + * a class for a subtype, use update_subtype(). Using this function will + * permanently orphan all the objects created with the specified subtype. + * * @param string $type Type * @param string $subtype Subtype * @@ -332,7 +396,7 @@ function remove_subtype($type, $subtype) { } /** - * Update a registered ElggEntity type, subtype, and classname + * Update a registered ElggEntity type, subtype, and class name * * @param string $type Type * @param string $subtype Subtype @@ -341,18 +405,33 @@ function remove_subtype($type, $subtype) { * @return bool */ function update_subtype($type, $subtype, $class = '') { - global $CONFIG; + global $CONFIG, $SUBTYPE_CACHE; - if (!$id = get_subtype_id($type, $subtype)) { - return FALSE; + $id = get_subtype_id($type, $subtype); + if (!$id) { + return false; + } + + if ($SUBTYPE_CACHE === null) { + _elgg_populate_subtype_cache(); } + + $unescaped_class = $class; + $type = sanitise_string($type); $subtype = sanitise_string($subtype); - - return update_data("UPDATE {$CONFIG->dbprefix}entity_subtypes + $class = sanitise_string($class); + + $success = update_data("UPDATE {$CONFIG->dbprefix}entity_subtypes SET type = '$type', subtype = '$subtype', class = '$class' WHERE id = $id "); + + if ($success && isset($SUBTYPE_CACHE[$id])) { + $SUBTYPE_CACHE[$id]->class = $unescaped_class; + } + + return $success; } /** @@ -370,7 +449,7 @@ function update_subtype($type, $subtype, $class = '') { * @param int $time_created The time creation timestamp * * @return bool - * @link http://docs.elgg.org/DataModel/Entities + * @throws InvalidParameterException * @access private */ function update_entity($guid, $owner_guid, $access_id, $container_guid = null, $time_created = null) { @@ -393,6 +472,10 @@ function update_entity($guid, $owner_guid, $access_id, $container_guid = null, $ $time_created = (int) $time_created; } + if ($access_id == ACCESS_DEFAULT) { + throw new InvalidParameterException('ACCESS_DEFAULT is not a valid access level. See its documentation in elgglib.h'); + } + if ($entity && $entity->canEdit()) { if (elgg_trigger_event('update', $entity->type, $entity)) { $ret = update_data("UPDATE {$CONFIG->dbprefix}entities @@ -410,7 +493,7 @@ function update_entity($guid, $owner_guid, $access_id, $container_guid = null, $ $newentity_cache = new ElggMemcache('new_entity_cache'); } if ($newentity_cache) { - $new_entity = $newentity_cache->delete($guid); + $newentity_cache->delete($guid); } // Handle cases where there was no error BUT no rows were updated! @@ -452,8 +535,10 @@ function can_write_to_container($user_guid = 0, $container_guid = 0, $type = 'al $container_guid = elgg_get_page_owner_guid(); } + $return = false; + if (!$container_guid) { - $return = TRUE; + $return = true; } $container = get_entity($container_guid); @@ -461,16 +546,15 @@ function can_write_to_container($user_guid = 0, $container_guid = 0, $type = 'al if ($container) { // If the user can edit the container, they can also write to it if ($container->canEdit($user_guid)) { - $return = TRUE; + $return = true; } - // Basics, see if the user is a member of the group. + // If still not approved, see if the user is a member of the group // @todo this should be moved to the groups plugin/library - if ($user && $container instanceof ElggGroup) { - if (!$container->isMember($user)) { - $return = FALSE; - } else { - $return = TRUE; + if (!$return && $user && $container instanceof ElggGroup) { + /* @var ElggGroup $container */ + if ($container->isMember($user)) { + $return = true; } } } @@ -507,8 +591,8 @@ function can_write_to_container($user_guid = 0, $container_guid = 0, $type = 'al * * @return int|false The new entity's GUID, or false on failure * @throws InvalidParameterException - * @access private * @link http://docs.elgg.org/DataModel/Entities + * @access private */ function create_entity($type, $subtype, $owner_guid, $access_id, $site_guid = 0, $container_guid = 0) { @@ -518,7 +602,6 @@ $container_guid = 0) { $type = sanitise_string($type); $subtype_id = add_subtype($type, $subtype); $owner_guid = (int)$owner_guid; - $access_id = (int)$access_id; $time = time(); if ($site_guid == 0) { $site_guid = $CONFIG->site_guid; @@ -527,13 +610,17 @@ $container_guid = 0) { if ($container_guid == 0) { $container_guid = $owner_guid; } + $access_id = (int)$access_id; + if ($access_id == ACCESS_DEFAULT) { + throw new InvalidParameterException('ACCESS_DEFAULT is not a valid access level. See its documentation in elgglib.h'); + } - $user = elgg_get_logged_in_user_entity(); - if (!can_write_to_container($user->guid, $owner_guid, $type, $subtype)) { + $user_guid = elgg_get_logged_in_user_guid(); + if (!can_write_to_container($user_guid, $owner_guid, $type, $subtype)) { return false; } if ($owner_guid != $container_guid) { - if (!can_write_to_container($user->guid, $container_guid, $type, $subtype)) { + if (!can_write_to_container($user_guid, $container_guid, $type, $subtype)) { return false; } } @@ -584,12 +671,14 @@ function get_entity_as_row($guid) { * * @param stdClass $row The row of the entry in the entities table. * - * @return object|false + * @return ElggEntity|false * @link http://docs.elgg.org/DataModel/Entities * @see get_entity_as_row() * @see add_subtype() * @see get_entity() * @access private + * + * @throws ClassException|InstallationException */ function entity_row_to_elggstar($row) { if (!($row instanceof stdClass)) { @@ -667,26 +756,59 @@ function entity_row_to_elggstar($row) { * @link http://docs.elgg.org/DataModel/Entities */ function get_entity($guid) { - static $newentity_cache; - $new_entity = false; + // This should not be a static local var. Notice that cache writing occurs in a completely + // different instance outside this function. + // @todo We need a single Memcache instance with a shared pool of namespace wrappers. This function would pull an instance from the pool. + static $shared_cache; + + // We could also use: if (!(int) $guid) { return FALSE }, + // but that evaluates to a false positive for $guid = TRUE. + // This is a bit slower, but more thorough. + if (!is_numeric($guid) || $guid === 0 || $guid === '0') { + return false; + } + + // Check local cache first + $new_entity = _elgg_retrieve_cached_entity($guid); + if ($new_entity) { + return $new_entity; + } - if (!is_numeric($guid)) { - return FALSE; + // Check shared memory cache, if available + if (null === $shared_cache) { + if (is_memcache_available()) { + $shared_cache = new ElggMemcache('new_entity_cache'); + } else { + $shared_cache = false; + } } - if ((!$newentity_cache) && (is_memcache_available())) { - $newentity_cache = new ElggMemcache('new_entity_cache'); + // until ACLs in memcache, DB query is required to determine access + $entity_row = get_entity_as_row($guid); + if (!$entity_row) { + return false; } - if ($newentity_cache) { - $new_entity = $newentity_cache->load($guid); + if ($shared_cache) { + $cached_entity = $shared_cache->load($guid); + // @todo store ACLs in memcache https://github.com/elgg/elgg/issues/3018#issuecomment-13662617 + if ($cached_entity) { + // @todo use ACL and cached entity access_id to determine if user can see it + return $cached_entity; + } } - if ($new_entity) { - return $new_entity; + // don't let incomplete entities cause fatal exceptions + try { + $new_entity = entity_row_to_elggstar($entity_row); + } catch (IncompleteEntityException $e) { + return false; } - return entity_row_to_elggstar(get_entity_as_row($guid)); + if ($new_entity) { + _elgg_cache_entity($new_entity); + } + return $new_entity; } /** @@ -736,6 +858,7 @@ function elgg_entity_exists($guid) { * Joined with subtypes by AND. See below) * * subtypes => NULL|STR entity subtype (SQL: subtype IN ('subtype1', 'subtype2)) + * Use ELGG_ENTITIES_NO_VALUE for no subtype. * * type_subtype_pairs => NULL|ARR (array('type' => 'subtype')) * (type = '$type' AND subtype = '$subtype') pairs @@ -772,7 +895,7 @@ function elgg_entity_exists($guid) { * * callback => string A callback function to pass each row through * - * @return mixed int if count is true, an array of entity objects, or false on failure + * @return mixed If count, int. If not count, array. false on errors. * @since 1.7.0 * @see elgg_get_entities_from_metadata() * @see elgg_get_entities_from_relationship() @@ -810,6 +933,8 @@ function elgg_get_entities(array $options = array()) { 'joins' => array(), 'callback' => 'entity_row_to_elggstar', + + '__ElggBatch' => null, ); $options = array_merge($defaults, $options); @@ -846,9 +971,6 @@ function elgg_get_entities(array $options = array()) { $wheres[] = elgg_get_entity_time_where_sql('e', $options['created_time_upper'], $options['created_time_lower'], $options['modified_time_upper'], $options['modified_time_lower']); - // remove identical where clauses - $wheres = array_unique($wheres); - // see if any functions failed // remove empty strings on successful functions foreach ($wheres as $i => $where) { @@ -859,6 +981,9 @@ function elgg_get_entities(array $options = array()) { } } + // remove identical where clauses + $wheres = array_unique($wheres); + // evaluate join clauses if (!is_array($options['joins'])) { $options['joins'] = array($options['joins']); @@ -912,21 +1037,46 @@ function elgg_get_entities(array $options = array()) { } if (!$options['count']) { - if ($options['group_by'] = sanitise_string($options['group_by'])) { + if ($options['group_by']) { $query .= " GROUP BY {$options['group_by']}"; } - if ($options['order_by'] = sanitise_string($options['order_by'])) { + if ($options['order_by']) { $query .= " ORDER BY {$options['order_by']}"; } if ($options['limit']) { - $limit = sanitise_int($options['limit']); + $limit = sanitise_int($options['limit'], false); $offset = sanitise_int($options['offset'], false); $query .= " LIMIT $offset, $limit"; } - $dt = get_data($query, $options['callback']); + if ($options['callback'] === 'entity_row_to_elggstar') { + $dt = _elgg_fetch_entities_from_sql($query, $options['__ElggBatch']); + } else { + $dt = get_data($query, $options['callback']); + } + + if ($dt) { + // populate entity and metadata caches + $guids = array(); + foreach ($dt as $item) { + // A custom callback could result in items that aren't ElggEntity's, so check for them + if ($item instanceof ElggEntity) { + _elgg_cache_entity($item); + // plugins usually have only settings + if (!$item instanceof ElggPlugin) { + $guids[] = $item->guid; + } + } + } + // @todo Without this, recursive delete fails. See #4568 + reset($dt); + + if ($guids) { + elgg_get_metadata_cache()->populateFromEntities($guids); + } + } return $dt; } else { $total = get_data_row($query); @@ -935,6 +1085,104 @@ function elgg_get_entities(array $options = array()) { } /** + * Return entities from an SQL query generated by elgg_get_entities. + * + * @param string $sql + * @param ElggBatch $batch + * @return ElggEntity[] + * + * @access private + * @throws LogicException + */ +function _elgg_fetch_entities_from_sql($sql, ElggBatch $batch = null) { + static $plugin_subtype; + if (null === $plugin_subtype) { + $plugin_subtype = get_subtype_id('object', 'plugin'); + } + + // Keys are types, values are columns that, if present, suggest that the secondary + // table is already JOINed + $types_to_optimize = array( + 'object' => 'title', + 'user' => 'password', + 'group' => 'name', + ); + + $rows = get_data($sql); + + // guids to look up in each type + $lookup_types = array(); + // maps GUIDs to the $rows key + $guid_to_key = array(); + + if (isset($rows[0]->type, $rows[0]->subtype) + && $rows[0]->type === 'object' + && $rows[0]->subtype == $plugin_subtype) { + // Likely the entire resultset is plugins, which have already been optimized + // to JOIN the secondary table. In this case we allow retrieving from cache, + // but abandon the extra queries. + $types_to_optimize = array(); + } + + // First pass: use cache where possible, gather GUIDs that we're optimizing + foreach ($rows as $i => $row) { + if (empty($row->guid) || empty($row->type)) { + throw new LogicException('Entity row missing guid or type'); + } + if ($entity = _elgg_retrieve_cached_entity($row->guid)) { + $rows[$i] = $entity; + continue; + } + if (isset($types_to_optimize[$row->type])) { + // check if row already looks JOINed. + if (isset($row->{$types_to_optimize[$row->type]})) { + // Row probably already contains JOINed secondary table. Don't make another query just + // to pull data that's already there + continue; + } + $lookup_types[$row->type][] = $row->guid; + $guid_to_key[$row->guid] = $i; + } + } + // Do secondary queries and merge rows + if ($lookup_types) { + $dbprefix = elgg_get_config('dbprefix'); + + foreach ($lookup_types as $type => $guids) { + $set = "(" . implode(',', $guids) . ")"; + $sql = "SELECT * FROM {$dbprefix}{$type}s_entity WHERE guid IN $set"; + $secondary_rows = get_data($sql); + if ($secondary_rows) { + foreach ($secondary_rows as $secondary_row) { + $key = $guid_to_key[$secondary_row->guid]; + // cast to arrays to merge then cast back + $rows[$key] = (object)array_merge((array)$rows[$key], (array)$secondary_row); + } + } + } + } + // Second pass to finish conversion + foreach ($rows as $i => $row) { + if ($row instanceof ElggEntity) { + continue; + } else { + try { + $rows[$i] = entity_row_to_elggstar($row); + } catch (IncompleteEntityException $e) { + // don't let incomplete entities throw fatal errors + unset($rows[$i]); + + // report incompletes to the batch process that spawned this query + if ($batch) { + $batch->reportIncompleteEntity($row); + } + } + } + } + return $rows; +} + +/** * Returns SQL where clause for type and subtype on main entity table * * @param string $table Entity table prefix as defined in SELECT...FROM entities $table @@ -958,8 +1206,8 @@ function elgg_get_entity_type_subtype_where_sql($table, $types, $subtypes, $pair return ''; } - // these are the only valid types for entities in elgg as defined in the DB. - $valid_types = array('object', 'user', 'group', 'site'); + // these are the only valid types for entities in elgg + $valid_types = elgg_get_config('entity_types'); // pairs override $wheres = array(); @@ -985,7 +1233,7 @@ function elgg_get_entity_type_subtype_where_sql($table, $types, $subtypes, $pair foreach ($types as $type) { if (!in_array($type, $valid_types)) { $valid_types_count--; - unset ($types[array_search($type, $types)]); + unset($types[array_search($type, $types)]); } else { // do the checking (and decrementing) in the subtype section. $valid_subtypes_count += count($subtypes); @@ -1003,13 +1251,24 @@ function elgg_get_entity_type_subtype_where_sql($table, $types, $subtypes, $pair $subtype_ids = array(); if ($subtypes) { foreach ($subtypes as $subtype) { - // check that the subtype is valid (with ELGG_ENTITIES_NO_VALUE being a valid subtype) - if (ELGG_ENTITIES_NO_VALUE === $subtype || $subtype_id = get_subtype_id($type, $subtype)) { - $subtype_ids[] = (ELGG_ENTITIES_NO_VALUE === $subtype) ? ELGG_ENTITIES_NO_VALUE : $subtype_id; - } else { - $valid_subtypes_count--; - elgg_log("Type-subtype '$type:$subtype' does not exist!", 'NOTICE'); + // check that the subtype is valid + if (!$subtype && ELGG_ENTITIES_NO_VALUE === $subtype) { + // subtype value is 0 + $subtype_ids[] = ELGG_ENTITIES_NO_VALUE; + } elseif (!$subtype) { + // subtype is ignored. + // this handles ELGG_ENTITIES_ANY_VALUE, '', and anything falsy that isn't 0 continue; + } else { + $subtype_id = get_subtype_id($type, $subtype); + + if ($subtype_id) { + $subtype_ids[] = $subtype_id; + } else { + $valid_subtypes_count--; + elgg_log("Type-subtype '$type:$subtype' does not exist!", 'NOTICE'); + continue; + } } } @@ -1037,7 +1296,7 @@ function elgg_get_entity_type_subtype_where_sql($table, $types, $subtypes, $pair foreach ($pairs as $paired_type => $paired_subtypes) { if (!in_array($paired_type, $valid_types)) { $valid_pairs_count--; - unset ($pairs[array_search($paired_type, $pairs)]); + unset($pairs[array_search($paired_type, $pairs)]); } else { if ($paired_subtypes && !is_array($paired_subtypes)) { $pairs[$paired_type] = array($paired_subtypes); @@ -1100,7 +1359,7 @@ function elgg_get_entity_type_subtype_where_sql($table, $types, $subtypes, $pair * best to provide in table.column format. * @param NULL|array $guids Array of GUIDs. * - * @return false|str + * @return false|string * @since 1.8.0 * @access private */ @@ -1118,8 +1377,12 @@ function elgg_get_guid_based_where_sql($column, $guids) { $guids_sanitized = array(); foreach ($guids as $guid) { - if (($guid != sanitise_int($guid))) { - return FALSE; + if ($guid !== ELGG_ENTITIES_NO_VALUE) { + $guid = sanitise_int($guid); + + if (!$guid) { + return false; + } } $guids_sanitized[] = $guid; } @@ -1145,7 +1408,7 @@ function elgg_get_guid_based_where_sql($column, $guids) { * @param NULL|int $time_updated_upper Time updated upper limit * @param NULL|int $time_updated_lower Time updated lower limit * - * @return FALSE|str FALSE on fail, string on success. + * @return FALSE|string FALSE on fail, string on success. * @since 1.7.0 * @access private */ @@ -1190,10 +1453,10 @@ $time_created_lower = NULL, $time_updated_upper = NULL, $time_updated_lower = NU * @internal This also provides the views for elgg_view_annotation(). * * @param array $options Any options from $getter options plus: - * full_view => BOOL Display full view entities - * list_type_toggle => BOOL Display gallery / list switch - * pagination => BOOL Display pagination links - * gallery => BOOL display in gallery view + * full_view => BOOL Display full view entities + * list_type => STR 'list' or 'gallery' + * list_type_toggle => BOOL Display gallery / list switch + * pagination => BOOL Display pagination links * * @param mixed $getter The entity getter function to use to fetch the entities * @param mixed $viewer The function to use to view the entity list. @@ -1210,13 +1473,14 @@ function elgg_list_entities(array $options = array(), $getter = 'elgg_get_entiti global $autofeed; $autofeed = true; + $offset_key = isset($options['offset_key']) ? $options['offset_key'] : 'offset'; + $defaults = array( - 'offset' => (int) max(get_input('offset', 0), 0), + 'offset' => (int) max(get_input($offset_key, 0), 0), 'limit' => (int) max(get_input('limit', 10), 0), 'full_view' => TRUE, 'list_type_toggle' => FALSE, 'pagination' => TRUE, - 'gallery' => FALSE, ); $options = array_merge($defaults, $options); @@ -1242,13 +1506,15 @@ function elgg_list_entities(array $options = array(), $getter = 'elgg_get_entiti * * @tip Use this to generate a list of archives by month for when entities were added or updated. * + * @todo document how to pass in array for $subtype + * * @warning Months are returned in the form YYYYMM. * * @param string $type The type of entity * @param string $subtype The subtype of entity - * @param int $container_guid The container GUID that the entinties belong to + * @param int $container_guid The container GUID that the entities belong to * @param int $site_guid The site GUID - * @param str $order_by Order_by SQL order by clause + * @param string $order_by Order_by SQL order by clause * * @return array|false Either an array months as YYYYMM, or false on failure */ @@ -1355,9 +1621,9 @@ $order_by = 'time_created') { * @param bool $recursive Recursively disable all entities owned or contained by $guid? * * @return bool - * @access private * @see access_show_hidden_entities() * @link http://docs.elgg.org/Entities + * @access private */ function disable_entity($guid, $reason = "", $recursive = true) { global $CONFIG; @@ -1373,34 +1639,34 @@ function disable_entity($guid, $reason = "", $recursive = true) { } if ($recursive) { - // Temporary token overriding access controls - // @todo Do this better. - static $__RECURSIVE_DELETE_TOKEN; - // Make it slightly harder to guess - $__RECURSIVE_DELETE_TOKEN = md5(elgg_get_logged_in_user_guid()); - - $sub_entities = get_data("SELECT * from {$CONFIG->dbprefix}entities - WHERE container_guid=$guid - or owner_guid=$guid - or site_guid=$guid", 'entity_row_to_elggstar'); + $hidden = access_get_show_hidden_status(); + access_show_hidden_entities(true); + $ia = elgg_set_ignore_access(true); + + $sub_entities = get_data("SELECT * FROM {$CONFIG->dbprefix}entities + WHERE ( + container_guid = $guid + OR owner_guid = $guid + OR site_guid = $guid + ) AND enabled='yes'", 'entity_row_to_elggstar'); if ($sub_entities) { foreach ($sub_entities as $e) { + add_entity_relationship($e->guid, 'disabled_with', $entity->guid); $e->disable($reason); } } - - $__RECURSIVE_DELETE_TOKEN = null; + access_show_hidden_entities($hidden); + elgg_set_ignore_access($ia); } $entity->disableMetadata(); $entity->disableAnnotations(); - // relationships can't be disabled. hope they join to the entities table. - //$entity->disableRelationships(); + _elgg_invalidate_cache_for_entity($guid); $res = update_data("UPDATE {$CONFIG->dbprefix}entities - set enabled='no' - where guid={$guid}"); + SET enabled = 'no' + WHERE guid = $guid"); return $res; } @@ -1412,43 +1678,55 @@ function disable_entity($guid, $reason = "", $recursive = true) { /** * Enable an entity. * - * @warning In order to enable an entity using ElggEntity::enable(), - * you must first use {@link access_show_hidden_entities()}. + * @warning In order to enable an entity, you must first use + * {@link access_show_hidden_entities()}. * - * @param int $guid GUID of entity to enable + * @param int $guid GUID of entity to enable + * @param bool $recursive Recursively enable all entities disabled with the entity? * * @return bool */ -function enable_entity($guid) { +function enable_entity($guid, $recursive = true) { global $CONFIG; $guid = (int)$guid; // Override access only visible entities - $access_status = access_get_show_hidden_status(); + $old_access_status = access_get_show_hidden_status(); access_show_hidden_entities(true); + $result = false; if ($entity = get_entity($guid)) { if (elgg_trigger_event('enable', $entity->type, $entity)) { if ($entity->canEdit()) { - access_show_hidden_entities($access_status); - $result = update_data("UPDATE {$CONFIG->dbprefix}entities - set enabled='yes' - where guid={$guid}"); + SET enabled = 'yes' + WHERE guid = $guid"); $entity->deleteMetadata('disable_reason'); $entity->enableMetadata(); $entity->enableAnnotations(); - return $result; + if ($recursive) { + $disabled_with_it = elgg_get_entities_from_relationship(array( + 'relationship' => 'disabled_with', + 'relationship_guid' => $entity->guid, + 'inverse_relationship' => true, + 'limit' => 0, + )); + + foreach ($disabled_with_it as $e) { + $e->enable(); + remove_entity_relationship($e->guid, 'disabled_with', $entity->guid); + } + } } } } - access_show_hidden_entities($access_status); - return false; + access_show_hidden_entities($old_access_status); + return $result; } /** @@ -1469,8 +1747,8 @@ function enable_entity($guid) { * @param bool $recursive If true (default) then all entities which are * owned or contained by $guid will also be deleted. * - * @access private * @return bool + * @access private */ function delete_entity($guid, $recursive = true) { global $CONFIG, $ENTITY_CACHE; @@ -1482,7 +1760,16 @@ function delete_entity($guid, $recursive = true) { // delete cache if (isset($ENTITY_CACHE[$guid])) { - invalidate_cache_for_entity($guid); + _elgg_invalidate_cache_for_entity($guid); + } + + // If memcache is available then delete this entry from the cache + static $newentity_cache; + if ((!$newentity_cache) && (is_memcache_available())) { + $newentity_cache = new ElggMemcache('new_entity_cache'); + } + if ($newentity_cache) { + $newentity_cache->delete($guid); } // Delete contained owned and otherwise releated objects (depth first) @@ -1495,20 +1782,35 @@ function delete_entity($guid, $recursive = true) { $entity_disable_override = access_get_show_hidden_status(); access_show_hidden_entities(true); - $sub_entities = get_data("SELECT * from {$CONFIG->dbprefix}entities - WHERE container_guid=$guid - or owner_guid=$guid - or site_guid=$guid", 'entity_row_to_elggstar'); - if ($sub_entities) { - foreach ($sub_entities as $e) { - $e->delete(true); - } + $ia = elgg_set_ignore_access(true); + + // @todo there was logic in the original code that ignored + // entities with owner or container guids of themselves. + // this should probably be prevented in ElggEntity instead of checked for here + $options = array( + 'wheres' => array( + "((container_guid = $guid OR owner_guid = $guid OR site_guid = $guid)" + . " AND guid != $guid)" + ), + 'limit' => 0 + ); + + $batch = new ElggBatch('elgg_get_entities', $options); + $batch->setIncrementOffset(false); + + foreach ($batch as $e) { + $e->delete(true); } access_show_hidden_entities($entity_disable_override); $__RECURSIVE_DELETE_TOKEN = null; + elgg_set_ignore_access($ia); } + $entity_disable_override = access_get_show_hidden_status(); + access_show_hidden_entities(true); + $ia = elgg_set_ignore_access(true); + // Now delete the entity itself $entity->deleteMetadata(); $entity->deleteOwnedMetadata(); @@ -1516,8 +1818,11 @@ function delete_entity($guid, $recursive = true) { $entity->deleteOwnedAnnotations(); $entity->deleteRelationships(); - remove_from_river_by_subject($guid); - remove_from_river_by_object($guid); + access_show_hidden_entities($entity_disable_override); + elgg_set_ignore_access($ia); + + elgg_delete_river(array('subject_guid' => $guid)); + elgg_delete_river(array('object_guid' => $guid)); remove_all_private_settings($guid); $res = delete_data("DELETE from {$CONFIG->dbprefix}entities where guid={$guid}"); @@ -1545,7 +1850,7 @@ function delete_entity($guid, $recursive = true) { } } - return $res; + return (bool)$res; } } } @@ -1561,7 +1866,7 @@ function delete_entity($guid, $recursive = true) { * @param string $returnvalue Return value from previous hook * @param array $params The parameters, passed 'guid' and 'varname' * - * @return null + * @return ElggMetadata|null * @elgg_plugin_hook_handler volatile metadata * @todo investigate more. * @access private @@ -1605,6 +1910,9 @@ function volatile_data_export_plugin_hook($hook, $entity_type, $returnvalue, $pa * * @elgg_event_handler export all * @return mixed + * @access private + * + * @throws InvalidParameterException|InvalidClassException */ function export_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) { // Sanity check values @@ -1646,6 +1954,9 @@ function export_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) { * * @return ElggEntity the unsaved entity which should be populated by items. * @todo Remove this. + * @access private + * + * @throws ClassException|InstallationException|ImportException */ function oddentity_to_elggentity(ODDEntity $element) { $class = $element->getAttribute('class'); @@ -1657,7 +1968,7 @@ function oddentity_to_elggentity(ODDEntity $element) { if (!$tmp) { // Construct new class with owner from session $classname = get_subtype_class($class, $subclass); - if ($classname != "") { + if ($classname) { if (class_exists($classname)) { $tmp = new $classname(); @@ -1716,12 +2027,14 @@ function oddentity_to_elggentity(ODDEntity $element) { * @return mixed * @elgg_plugin_hook_handler import all * @todo document + * @access private * + * @throws ImportException */ function import_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) { $element = $params['element']; - $tmp = NULL; + $tmp = null; if ($element instanceof ODDEntity) { $tmp = oddentity_to_elggentity($element); @@ -1729,7 +2042,7 @@ function import_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) { if ($tmp) { // Make sure its saved if (!$tmp->save()) { - elgg_echo('ImportException:ProblemSaving', array($element->getAttribute('uuid'))); + $msg = elgg_echo('ImportException:ProblemSaving', array($element->getAttribute('uuid'))); throw new ImportException($msg); } @@ -1763,8 +2076,6 @@ function import_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) { * @link http://docs.elgg.org/Entities/AccessControl */ function can_edit_entity($entity_guid, $user_guid = 0) { - global $CONFIG; - $user_guid = (int)$user_guid; $user = get_entity($user_guid); if (!$user) { @@ -1817,14 +2128,19 @@ function can_edit_entity_metadata($entity_guid, $user_guid = 0, $metadata = null $return = null; - if ($metadata->owner_guid == 0) { + if ($metadata && ($metadata->owner_guid == 0)) { $return = true; } if (is_null($return)) { $return = can_edit_entity($entity_guid, $user_guid); } - $user = get_entity($user_guid); + if ($user_guid) { + $user = get_entity($user_guid); + } else { + $user = elgg_get_logged_in_user_entity(); + } + $params = array('entity' => $entity, 'user' => $user, 'metadata' => $metadata); $return = elgg_trigger_plugin_hook('permissions_check:metadata', $entity->type, $params, $return); return $return; @@ -1883,7 +2199,7 @@ function get_entity_url($entity_guid) { * @param string $entity_subtype The entity subtype * @param string $function_name The function to register * - * @return true|false Depending on success + * @return bool Depending on success * @see get_entity_url() * @see ElggEntity::getURL() * @since 1.8.0 @@ -1891,7 +2207,7 @@ function get_entity_url($entity_guid) { function elgg_register_entity_url_handler($entity_type, $entity_subtype, $function_name) { global $CONFIG; - if (!is_callable($function_name)) { + if (!is_callable($function_name, true)) { return false; } @@ -1919,7 +2235,7 @@ function elgg_register_entity_url_handler($entity_type, $entity_subtype, $functi * @param string $type The type of entity (object, site, user, group) * @param string $subtype The subtype to register (may be blank) * - * @return true|false Depending on success + * @return bool Depending on success * @see get_registered_entity_types() * @link http://docs.elgg.org/Search * @link http://docs.elgg.org/Tutorials/Search @@ -1928,7 +2244,7 @@ function elgg_register_entity_type($type, $subtype = null) { global $CONFIG; $type = strtolower($type); - if (!in_array($type, array('object', 'site', 'group', 'user'))) { + if (!in_array($type, $CONFIG->entity_types)) { return FALSE; } @@ -1956,14 +2272,14 @@ function elgg_register_entity_type($type, $subtype = null) { * @param string $type The type of entity (object, site, user, group) * @param string $subtype The subtype to register (may be blank) * - * @return true|false Depending on success + * @return bool Depending on success * @see elgg_register_entity_type() */ function unregister_entity_type($type, $subtype) { global $CONFIG; $type = strtolower($type); - if (!in_array($type, array('object', 'site', 'group', 'user'))) { + if (!in_array($type, $CONFIG->entity_types)) { return FALSE; } @@ -2023,7 +2339,7 @@ function get_registered_entity_types($type = null) { * @param string $type The type of entity (object, site, user, group) * @param string $subtype The subtype (may be blank) * - * @return true|false Depending on whether or not the type has been registered + * @return bool Depending on whether or not the type has been registered */ function is_registered_entity_type($type, $subtype = null) { global $CONFIG; @@ -2051,15 +2367,18 @@ function is_registered_entity_type($type, $subtype = null) { * * @param array $page Page elements from pain page handler * - * @return void + * @return bool * @elgg_page_handler view + * @access private */ function entities_page_handler($page) { if (isset($page[0])) { global $CONFIG; set_input('guid', $page[0]); include($CONFIG->path . "pages/entities/index.php"); + return true; } + return false; } /** @@ -2120,34 +2439,16 @@ function elgg_list_registered_entities(array $options = array()) { } } - $count = elgg_get_entities(array_merge(array('count' => TRUE), $options)); - $entities = elgg_get_entities($options); - - return elgg_view_entity_list($entities, $count, $options['offset'], - $options['limit'], $options['full_view'], $options['list_type_toggle'], $options['pagination']); -} - -/** - * Check the recursive delete permissions token. - * - * If an entity is deleted recursively, a permissions override is required to allow - * contained or owned entities to be removed. - * - * @access private - * @return bool - * @elgg_plugin_hook_handler permissions_check all - * @elgg_plugin_hook_handler permissions_check:metadata all - */ -function recursive_delete_permissions_check() { - static $__RECURSIVE_DELETE_TOKEN; - - if ((elgg_is_logged_in()) && ($__RECURSIVE_DELETE_TOKEN) - && (strcmp($__RECURSIVE_DELETE_TOKEN, md5(elgg_get_logged_in_user_guid())))) { - return true; + if (!empty($options['type_subtype_pairs'])) { + $count = elgg_get_entities(array_merge(array('count' => TRUE), $options)); + $entities = elgg_get_entities($options); + } else { + $count = 0; + $entities = array(); } - // consult next function - return NULL; + $options['count'] = $count; + return elgg_view_entity_list($entities, $options); } /** @@ -2168,6 +2469,7 @@ function elgg_instanceof($entity, $type = NULL, $subtype = NULL, $class = NULL) $return = ($entity instanceof ElggEntity); if ($type) { + /* @var ElggEntity $entity */ $return = $return && ($entity->getType() == $type); } @@ -2185,8 +2487,6 @@ function elgg_instanceof($entity, $type = NULL, $subtype = NULL, $class = NULL) /** * Update the last_action column in the entities table for $guid. * - * This determines the sort order of 1.8's default river. - * * @warning This is different to time_updated. Time_updated is automatically set, * while last_action is only set when explicitly called. * @@ -2194,7 +2494,8 @@ function elgg_instanceof($entity, $type = NULL, $subtype = NULL, $class = NULL) * @param int $posted Timestamp of last action * * @return bool - **/ + * @access private + */ function update_entity_last_action($guid, $posted = NULL) { global $CONFIG; $guid = (int)$guid; @@ -2223,27 +2524,36 @@ function update_entity_last_action($guid, $posted = NULL) { * * @return void * @elgg_plugin_hook_handler gc system + * @access private */ function entities_gc() { global $CONFIG; - $tables = array ('sites_entity', 'objects_entity', 'groups_entity', 'users_entity'); + $tables = array( + 'site' => 'sites_entity', + 'object' => 'objects_entity', + 'group' => 'groups_entity', + 'user' => 'users_entity' + ); - foreach ($tables as $table) { - delete_data("DELETE from {$CONFIG->dbprefix}{$table} - where guid NOT IN (SELECT guid from {$CONFIG->dbprefix}entities)"); + foreach ($tables as $type => $table) { + delete_data("DELETE FROM {$CONFIG->dbprefix}{$table} + WHERE guid NOT IN (SELECT guid FROM {$CONFIG->dbprefix}entities)"); + delete_data("DELETE FROM {$CONFIG->dbprefix}entities + WHERE type = '$type' AND guid NOT IN (SELECT guid FROM {$CONFIG->dbprefix}{$table})"); } } /** * Runs unit tests for the entity objects. * - * @param sting $hook unit_test + * @param string $hook unit_test * @param string $type system * @param mixed $value Array of tests * @param mixed $params Params * * @return array + * @access private */ function entities_test($hook, $type, $value, $params) { global $CONFIG; @@ -2256,17 +2566,13 @@ function entities_test($hook, $type, $value, $params) { * * @return void * @elgg_event_handler init system + * @access private */ function entities_init() { elgg_register_page_handler('view', 'entities_page_handler'); elgg_register_plugin_hook_handler('unit_test', 'system', 'entities_test'); - // Allow a permission override for recursive entity deletion - // @todo Can this be done better? - elgg_register_plugin_hook_handler('permissions_check', 'all', 'recursive_delete_permissions_check'); - elgg_register_plugin_hook_handler('permissions_check:metadata', 'all', 'recursive_delete_permissions_check'); - elgg_register_plugin_hook_handler('gc', 'system', 'entities_gc'); } @@ -2281,3 +2587,4 @@ elgg_register_plugin_hook_handler('volatile', 'metadata', 'volatile_data_export_ /** Register init system event **/ elgg_register_event_handler('init', 'system', 'entities_init'); + |
