diff options
Diffstat (limited to 'engine/lib/entities.php')
-rw-r--r-- | engine/lib/entities.php | 424 |
1 files changed, 315 insertions, 109 deletions
diff --git a/engine/lib/entities.php b/engine/lib/entities.php index 66c1475b7..ce736ce05 100644 --- a/engine/lib/entities.php +++ b/engine/lib/entities.php @@ -17,20 +17,20 @@ global $ENTITY_CACHE; $ENTITY_CACHE = array(); /** - * Cache subtypes and related class names once loaded. + * Cache subtypes and related class names. * - * @global array $SUBTYPE_CACHE + * @global array|null $SUBTYPE_CACHE array once populated from DB, initially null * @access private */ global $SUBTYPE_CACHE; -$SUBTYPE_CACHE = NULL; +$SUBTYPE_CACHE = null; /** * Invalidate this class's entry in the cache. * * @param int $guid The entity guid * - * @return void + * @return null * @access private */ function invalidate_cache_for_entity($guid) { @@ -39,6 +39,8 @@ function invalidate_cache_for_entity($guid) { $guid = (int)$guid; unset($ENTITY_CACHE[$guid]); + + elgg_get_metadata_cache()->clear($guid); } /** @@ -48,14 +50,35 @@ function invalidate_cache_for_entity($guid) { * * @param ElggEntity $entity Entity to cache * - * @return void + * @return null * @see retrieve_cached_entity() * @see invalidate_cache_for_entity() * @access private + * TODO(evan): Use an ElggCache object */ function cache_entity(ElggEntity $entity) { global $ENTITY_CACHE; + // 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; + } + + // Don't store too many or we'll have memory problems + // TODO(evan): Pick a less arbitrary limit + if (count($ENTITY_CACHE) > 256) { + $random_guid = array_rand($ENTITY_CACHE); + + 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); + } + $ENTITY_CACHE[$entity->guid] = $entity; } @@ -64,7 +87,7 @@ function cache_entity(ElggEntity $entity) { * * @param int $guid The guid * - * @return void + * @return ElggEntity|bool false if entity not cached, or not fully loaded * @see cache_entity() * @see invalidate_cache_for_entity() * @access private @@ -72,8 +95,6 @@ function cache_entity(ElggEntity $entity) { function retrieve_cached_entity($guid) { global $ENTITY_CACHE; - $guid = (int)$guid; - if (isset($ENTITY_CACHE[$guid])) { if ($ENTITY_CACHE[$guid]->isFullyLoaded()) { return $ENTITY_CACHE[$guid]; @@ -133,29 +154,23 @@ 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 use the cache before hitting database - $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; } /** @@ -163,35 +178,67 @@ function get_subtype_id($type, $subtype) { * * @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; + } } /** @@ -210,25 +257,19 @@ 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 use the cache before going to the database - $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; } /** @@ -242,29 +283,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; } /** @@ -290,21 +323,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; @@ -346,22 +390,31 @@ function remove_subtype($type, $subtype) { function update_subtype($type, $subtype, $class = '') { 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); - - $result = 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 ($result && isset($SUBTYPE_CACHE[$id])) { - $SUBTYPE_CACHE[$id]->class = $class; + if ($success && isset($SUBTYPE_CACHE[$id])) { + $SUBTYPE_CACHE[$id]->class = $unescaped_class; } - return $result; + return $success; } /** @@ -593,12 +646,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)) { @@ -676,29 +731,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; + return false; + } + + // Check local cache first + $new_entity = retrieve_cached_entity($guid); + if ($new_entity) { + return $new_entity; } - if ((!$newentity_cache) && (is_memcache_available())) { - $newentity_cache = new ElggMemcache('new_entity_cache'); + // 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) { - $new_entity = $newentity_cache->load($guid); + // until ACLs in memcache, DB query is required to determine access + $entity_row = get_entity_as_row($guid); + if (!$entity_row) { + return false; } - if ($new_entity) { - return $new_entity; + if ($shared_cache) { + $cached_entity = $shared_cache->load($guid); + // @todo store ACLs in memcache http://trac.elgg.org/ticket/3018#comment:3 + if ($cached_entity) { + // @todo use ACL and cached entity access_id to determine if user can see it + return $cached_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) { + cache_entity($new_entity); + } + return $new_entity; } /** @@ -939,7 +1024,32 @@ function elgg_get_entities(array $options = array()) { $query .= " LIMIT $offset, $limit"; } - $dt = get_data($query, $options['callback']); + if ($options['callback'] === 'entity_row_to_elggstar') { + $dt = _elgg_fetch_entities_from_sql($query); + } 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) { + 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); @@ -948,6 +1058,97 @@ function elgg_get_entities(array $options = array()) { } /** + * Return entities from an SQL query generated by elgg_get_entities. + * + * @param string $sql + * @return ElggEntity[] + * + * @access private + * @throws LogicException + */ +function _elgg_fetch_entities_from_sql($sql) { + 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 = 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]); + } + } + } + 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 @@ -1113,7 +1314,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 */ @@ -1162,7 +1363,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 */ @@ -1264,7 +1465,7 @@ function elgg_list_entities(array $options = array(), $getter = 'elgg_get_entiti * @param string $subtype The subtype of entity * @param int $container_guid The container GUID that the entinties 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 */ @@ -1412,6 +1613,7 @@ function disable_entity($guid, $reason = "", $recursive = true) { $entity->disableMetadata(); $entity->disableAnnotations(); + invalidate_cache_for_entity($guid); $res = update_data("UPDATE {$CONFIG->dbprefix}entities SET enabled = 'no' @@ -1608,7 +1810,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 void + * @return ElggMetadata|null * @elgg_plugin_hook_handler volatile metadata * @todo investigate more. * @access private @@ -1653,6 +1855,8 @@ 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 @@ -1695,6 +1899,8 @@ 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'); @@ -1706,7 +1912,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(); @@ -1766,11 +1972,13 @@ function oddentity_to_elggentity(ODDEntity $element) { * @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); @@ -1812,8 +2020,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) { @@ -1937,7 +2143,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 @@ -1945,7 +2151,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; } @@ -1973,7 +2179,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 @@ -2010,7 +2216,7 @@ 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) { @@ -2077,7 +2283,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; @@ -2277,7 +2483,7 @@ function entities_gc() { /** * 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 |