diff options
Diffstat (limited to 'engine')
24 files changed, 720 insertions, 290 deletions
diff --git a/engine/classes/ElggAttributeLoader.php b/engine/classes/ElggAttributeLoader.php new file mode 100644 index 000000000..602bb8bae --- /dev/null +++ b/engine/classes/ElggAttributeLoader.php @@ -0,0 +1,199 @@ +<?php + +/** + * Loads ElggEntity attributes from DB or validates those passed in via constructor + * + * @access private + */ +class ElggAttributeLoader { + + /** + * @var array names of attributes in all entities + */ + protected static $primary_attr_names = array( + 'guid', + 'type', + 'subtype', + 'owner_guid', + 'container_guid', + 'site_guid', + 'access_id', + 'time_created', + 'time_updated', + 'last_action', + 'enabled' + ); + + /** + * @var array names of secondary attributes required for the entity + */ + protected $secondary_attr_names = array(); + + /** + * @var string entity type (not class) required for fetched primaries + */ + protected $required_type; + + /** + * @var array + */ + protected $initialized_attributes; + + /** + * @var string class of object being loaded + */ + protected $class; + + /** + * @var bool should access control be considered when fetching entity? + */ + public $requires_access_control = true; + + /** + * @var callable function used to load attributes from {prefix}entities table + */ + public $primary_loader = 'get_entity_as_row'; + + /** + * @var callable function used to load attributes from secondary table + */ + public $secondary_loader = ''; + + /** + * @var callable function used to load all necessary attributes + */ + public $full_loader = ''; + + /** + * @param string $class class of object being loaded + * @param string $required_type entity type this is being used to populate + * @param array $initialized_attrs attributes after initializeAttributes() has been run + * @throws InvalidArgumentException + */ + public function __construct($class, $required_type, array $initialized_attrs) { + if (!is_string($class)) { + throw new InvalidArgumentException('$class must be a class name.'); + } + $this->class = $class; + + if (!is_string($required_type)) { + throw new InvalidArgumentException('$requiredType must be a system entity type.'); + } + $this->required_type = $required_type; + + $this->initialized_attributes = $initialized_attrs; + unset($initialized_attrs['tables_split'], $initialized_attrs['tables_loaded']); + $all_attr_names = array_keys($initialized_attrs); + $this->secondary_attr_names = array_diff($all_attr_names, self::$primary_attr_names); + } + + protected function isMissingPrimaries($row) { + return array_diff(self::$primary_attr_names, array_keys($row)) !== array(); + } + + protected function isMissingSecondaries($row) { + return array_diff($this->secondary_attr_names, array_keys($row)) !== array(); + } + + protected function checkType($row) { + if ($row['type'] !== $this->required_type) { + $msg = elgg_echo('InvalidClassException:NotValidElggStar', array($row['guid'], $this->class)); + throw new InvalidClassException($msg); + } + } + + /** + * Get all required attributes for the entity, validating any that are passed in. Returns empty array + * if can't be loaded (Check $failure_reason). + * + * This function splits loading between "primary" attributes (those in {prefix}entities table) and + * "secondary" attributes (e.g. those in {prefix}objects_entity), but can load all at once if a + * combined loader is available. + * + * @param mixed $row a row loaded from DB (array or stdClass) or a GUID + * @return array will be empty if failed to load all attributes (access control or entity doesn't exist) + * + * @throws InvalidArgumentException|LogicException|IncompleteEntityException + */ + public function getRequiredAttributes($row) { + if (!is_array($row) && !($row instanceof stdClass)) { + // assume row is the GUID + $row = array('guid' => $row); + } + $row = (array) $row; + if (empty($row['guid'])) { + throw new InvalidArgumentException('$row must be or contain a GUID'); + } + + // these must be present to support isFullyLoaded() + foreach (array('tables_split', 'tables_loaded') as $key) { + if (isset($this->initialized_attributes[$key])) { + $row[$key] = $this->initialized_attributes[$key]; + } + } + + $was_missing_primaries = $this->isMissingPrimaries($row); + $was_missing_secondaries = $this->isMissingSecondaries($row); + + // some types have a function to load all attributes at once, it should be faster + if (($was_missing_primaries || $was_missing_secondaries) && is_callable($this->full_loader)) { + $fetched = (array) call_user_func($this->full_loader, $row['guid']); + if (!$fetched) { + return array(); + } + $row = array_merge($row, $fetched); + $this->checkType($row); + } else { + if ($was_missing_primaries) { + if (!is_callable($this->primary_loader)) { + throw new LogicException('Primary attribute loader must be callable'); + } + if (!$this->requires_access_control) { + $ignoring_access = elgg_set_ignore_access(); + } + $fetched = (array) call_user_func($this->primary_loader, $row['guid']); + if (!$this->requires_access_control) { + elgg_set_ignore_access($ignoring_access); + } + if (!$fetched) { + return array(); + } + $row = array_merge($row, $fetched); + } + + // We must test type before trying to load the secondaries so that InvalidClassException + // gets thrown. Otherwise the secondary loader will fail and return false. + $this->checkType($row); + + if ($was_missing_secondaries) { + if (!is_callable($this->secondary_loader)) { + throw new LogicException('Secondary attribute loader must be callable'); + } + $fetched = (array) call_user_func($this->secondary_loader, $row['guid']); + if (!$fetched) { + if ($row['type'] === 'site') { + // A special case is needed for sites: When vanilla ElggEntities are created and + // saved, these are stored w/ type "site", but with no sites_entity row. These + // are probably only created in the unit tests. + // @todo Don't save vanilla ElggEntities with type "site" + $row['guid'] = (int) $row['guid']; + return $row; + } + throw new IncompleteEntityException("Secondary loader failed to return row for {$row['guid']}"); + } + $row = array_merge($row, $fetched); + } + } + + // loading complete: re-check missing and check type + if (($was_missing_primaries && $this->isMissingPrimaries($row)) + || ($was_missing_secondaries && $this->isMissingSecondaries($row))) { + throw new LogicException('Attribute loaders failed to return proper attributes'); + } + + // guid needs to be an int http://trac.elgg.org/ticket/4111 + $row['guid'] = (int) $row['guid']; + + return $row; + } +} diff --git a/engine/classes/ElggDiskFilestore.php b/engine/classes/ElggDiskFilestore.php index f00376481..7aace43ba 100644 --- a/engine/classes/ElggDiskFilestore.php +++ b/engine/classes/ElggDiskFilestore.php @@ -200,18 +200,18 @@ class ElggDiskFilestore extends ElggFilestore { * @return string The full path of where the file is stored */ public function getFilenameOnFilestore(ElggFile $file) { - $owner = $file->getOwnerEntity(); - if (!$owner) { - $owner = elgg_get_logged_in_user_entity(); + $owner_guid = $file->getOwnerGuid(); + if (!$owner_guid) { + $owner_guid = elgg_get_logged_in_user_guid(); } - if (!$owner) { + if (!$owner_guid) { $msg = elgg_echo('InvalidParameterException:MissingOwner', array($file->getFilename(), $file->guid)); throw new InvalidParameterException($msg); } - return $this->dir_root . $this->makefileMatrix($owner->guid) . $file->getFilename(); + return $this->dir_root . $this->makefileMatrix($owner_guid) . $file->getFilename(); } /** diff --git a/engine/classes/ElggGroup.php b/engine/classes/ElggGroup.php index 121186196..ea257f368 100644 --- a/engine/classes/ElggGroup.php +++ b/engine/classes/ElggGroup.php @@ -324,37 +324,18 @@ class ElggGroup extends ElggEntity * @return bool */ protected function load($guid) { - // Test to see if we have the generic stuff - if (!parent::load($guid)) { - return false; - } + $attr_loader = new ElggAttributeLoader(get_class(), 'group', $this->attributes); + $attr_loader->requires_access_control = !($this instanceof ElggPlugin); + $attr_loader->secondary_loader = 'get_group_entity_as_row'; - // Only work with GUID from here - if ($guid instanceof stdClass) { - $guid = $guid->guid; - } - - // Check the type - if ($this->attributes['type'] != 'group') { - $msg = elgg_echo('InvalidClassException:NotValidElggStar', array($guid, get_class())); - throw new InvalidClassException($msg); - } - - // Load missing data - $row = get_group_entity_as_row($guid); - if (($row) && (!$this->isFullyLoaded())) { - // If $row isn't a cached copy then increment the counter - $this->attributes['tables_loaded']++; - } - - // Now put these into the attributes array as core values - $objarray = (array) $row; - foreach ($objarray as $key => $value) { - $this->attributes[$key] = $value; + $attrs = $attr_loader->getRequiredAttributes($guid); + if (!$attrs) { + return false; } - // guid needs to be an int http://trac.elgg.org/ticket/4111 - $this->attributes['guid'] = (int)$this->attributes['guid']; + $this->attributes = $attrs; + $this->attributes['tables_loaded'] = 2; + cache_entity($this); return true; } diff --git a/engine/classes/ElggGroupItemVisibility.php b/engine/classes/ElggGroupItemVisibility.php new file mode 100644 index 000000000..2c7e2abb4 --- /dev/null +++ b/engine/classes/ElggGroupItemVisibility.php @@ -0,0 +1,93 @@ +<?php + +/** + * Determines if otherwise visible items should be hidden from a user due to group + * policy or visibility. + * + * @class ElggGroupItemVisibility + * @package Elgg.Core + * @subpackage Groups + * + * @access private + */ +class ElggGroupItemVisibility { + + const REASON_MEMBERSHIP = 'membershiprequired'; + const REASON_LOGGEDOUT = 'loggedinrequired'; + const REASON_NOACCESS = 'noaccess'; + + /** + * @var bool + */ + public $shouldHideItems = false; + + /** + * @var string + */ + public $reasonHidden = ''; + + /** + * Determine visibility of items within a container for the current user + * + * @param int $container_guid GUID of a container (may/may not be a group) + * + * @return ElggGroupItemVisibility + * + * @todo Make this faster, considering it must run for every river item. + */ + static public function factory($container_guid) { + // cache because this may be called repeatedly during river display, and + // due to need to check group visibility, cache will be disabled for some + // get_entity() calls + static $cache = array(); + + $ret = new ElggGroupItemVisibility(); + + if (!$container_guid) { + return $ret; + } + + $user = elgg_get_logged_in_user_entity(); + $user_guid = $user ? $user->guid : 0; + + $container_guid = (int) $container_guid; + + $cache_key = "$container_guid|$user_guid"; + if (empty($cache[$cache_key])) { + // compute + + $container = get_entity($container_guid); + $is_visible = (bool) $container; + + if (!$is_visible) { + // see if it *really* exists... + $prev_access = elgg_set_ignore_access(); + $container = get_entity($container_guid); + elgg_set_ignore_access($prev_access); + } + + if ($container && $container instanceof ElggGroup) { + /* @var ElggGroup $container */ + + if ($is_visible) { + if (!$container->isPublicMembership()) { + if ($user) { + if (!$container->isMember($user) && !$user->isAdmin()) { + $ret->shouldHideItems = true; + $ret->reasonHidden = self::REASON_MEMBERSHIP; + } + } else { + $ret->shouldHideItems = true; + $ret->reasonHidden = self::REASON_LOGGEDOUT; + } + } + } else { + $ret->shouldHideItems = true; + $ret->reasonHidden = self::REASON_NOACCESS; + } + } + $cache[$cache_key] = $ret; + } + return $cache[$cache_key]; + } +} diff --git a/engine/classes/ElggMenuBuilder.php b/engine/classes/ElggMenuBuilder.php index df0f9147f..d7f85685c 100644 --- a/engine/classes/ElggMenuBuilder.php +++ b/engine/classes/ElggMenuBuilder.php @@ -205,7 +205,7 @@ class ElggMenuBuilder { // sort each section foreach ($this->menu as $index => $section) { foreach ($section as $key => $node) { - $section[$key]->original_order = $key; + $section[$key]->setData('original_order', $key); } usort($section, $sort_callback); $this->menu[$index] = $section; @@ -240,7 +240,7 @@ class ElggMenuBuilder { $result = strnatcmp($at, $bt); if ($result === 0) { - return $a->original_order - $b->original_order; + return $a->getData('original_order') - $b->getData('original_order'); } return $result; } @@ -258,7 +258,7 @@ class ElggMenuBuilder { $result = strcmp($an, $bn); if ($result === 0) { - return $a->original_order - $b->original_order; + return $a->getData('original_order') - $b->getData('original_order'); } return $result; } @@ -275,7 +275,7 @@ class ElggMenuBuilder { $bw = $b->getWeight(); if ($aw == $bw) { - return $a->original_order - $b->original_order; + return $a->getData('original_order') - $b->getData('original_order'); } return $aw - $bw; } diff --git a/engine/classes/ElggMenuItem.php b/engine/classes/ElggMenuItem.php index fe25f3ddd..81ce6c099 100644 --- a/engine/classes/ElggMenuItem.php +++ b/engine/classes/ElggMenuItem.php @@ -543,7 +543,7 @@ class ElggMenuItem { */ public function sortChildren($sortFunction) { foreach ($this->data['children'] as $key => $node) { - $this->data['children'][$key]->original_order = $key; + $this->data['children'][$key]->data['original_order'] = $key; } usort($this->data['children'], $sortFunction); } diff --git a/engine/classes/ElggObject.php b/engine/classes/ElggObject.php index fa6296c8c..6263f84f6 100644 --- a/engine/classes/ElggObject.php +++ b/engine/classes/ElggObject.php @@ -99,37 +99,18 @@ class ElggObject extends ElggEntity { * @throws InvalidClassException */ protected function load($guid) { - // Load data from entity table if needed - if (!parent::load($guid)) { - return false; - } + $attr_loader = new ElggAttributeLoader(get_class(), 'object', $this->attributes); + $attr_loader->requires_access_control = !($this instanceof ElggPlugin); + $attr_loader->secondary_loader = 'get_object_entity_as_row'; - // Only work with GUID from here - if ($guid instanceof stdClass) { - $guid = $guid->guid; - } - - // Check the type - if ($this->attributes['type'] != 'object') { - $msg = elgg_echo('InvalidClassException:NotValidElggStar', array($guid, get_class())); - throw new InvalidClassException($msg); - } - - // Load missing data - $row = get_object_entity_as_row($guid); - if (($row) && (!$this->isFullyLoaded())) { - // If $row isn't a cached copy then increment the counter - $this->attributes['tables_loaded']++; - } - - // Now put these into the attributes array as core values - $objarray = (array) $row; - foreach ($objarray as $key => $value) { - $this->attributes[$key] = $value; + $attrs = $attr_loader->getRequiredAttributes($guid); + if (!$attrs) { + return false; } - // guid needs to be an int http://trac.elgg.org/ticket/4111 - $this->attributes['guid'] = (int)$this->attributes['guid']; + $this->attributes = $attrs; + $this->attributes['tables_loaded'] = 2; + cache_entity($this); return true; } @@ -149,7 +130,7 @@ class ElggObject extends ElggEntity { // Save ElggObject-specific attributes return create_object_entity($this->get('guid'), $this->get('title'), - $this->get('description'), $this->get('container_guid')); + $this->get('description')); } /** diff --git a/engine/classes/ElggPlugin.php b/engine/classes/ElggPlugin.php index c6ce2905f..32b5f952a 100644 --- a/engine/classes/ElggPlugin.php +++ b/engine/classes/ElggPlugin.php @@ -82,64 +82,6 @@ class ElggPlugin extends ElggObject { } /** - * Overridden from ElggEntity and ElggObject::load(). Core always inits plugins with - * a query joined to the objects_entity table, so all the info is there. - * - * @param mixed $guid GUID of an ElggObject or the stdClass object from entities table - * - * @return bool - * @throws InvalidClassException - */ - protected function load($guid) { - - $expected_attributes = $this->attributes; - unset($expected_attributes['tables_split']); - unset($expected_attributes['tables_loaded']); - - // this was loaded with a full join - $needs_loaded = false; - - if ($guid instanceof stdClass) { - $row = (array) $guid; - $missing_attributes = array_diff_key($expected_attributes, $row); - if ($missing_attributes) { - $needs_loaded = true; - $guid = $row['guid']; - } else { - $this->attributes = $row; - } - } else { - $needs_loaded = true; - } - - if ($needs_loaded) { - $entity = (array) get_entity_as_row($guid); - $object = (array) get_object_entity_as_row($guid); - - if (!$entity || !$object) { - return false; - } - - $this->attributes = array_merge($this->attributes, $entity, $object); - } - - $this->attributes['tables_loaded'] = 2; - - // Check the type - if ($this->attributes['type'] != 'object') { - $msg = elgg_echo('InvalidClassException:NotValidElggStar', array($guid, get_class())); - throw new InvalidClassException($msg); - } - - // guid needs to be an int http://trac.elgg.org/ticket/4111 - $this->attributes['guid'] = (int)$this->attributes['guid']; - - cache_entity($this); - - return true; - } - - /** * Save the plugin object. Make sure required values exist. * * @see ElggObject::save() diff --git a/engine/classes/ElggSession.php b/engine/classes/ElggSession.php index 13a33736c..9750f063e 100644 --- a/engine/classes/ElggSession.php +++ b/engine/classes/ElggSession.php @@ -54,7 +54,7 @@ class ElggSession implements ArrayAccess { * * @param mixed $key Name * - * @return void + * @return mixed */ function offsetGet($key) { if (!ElggSession::$__localcache) { @@ -98,7 +98,7 @@ class ElggSession implements ArrayAccess { * * @param int $offset Offset * - * @return int + * @return bool */ function offsetExists($offset) { if (isset(ElggSession::$__localcache[$offset])) { @@ -112,6 +112,8 @@ class ElggSession implements ArrayAccess { if ($this->offsetGet($offset)) { return true; } + + return false; } @@ -132,10 +134,10 @@ class ElggSession implements ArrayAccess { * @param string $key Name * @param mixed $value Value * - * @return mixed + * @return void */ function set($key, $value) { - return $this->offsetSet($key, $value); + $this->offsetSet($key, $value); } /** @@ -143,9 +145,9 @@ class ElggSession implements ArrayAccess { * * @param string $key Name * - * @return bool + * @return void */ function del($key) { - return $this->offsetUnset($key); + $this->offsetUnset($key); } } diff --git a/engine/classes/ElggSite.php b/engine/classes/ElggSite.php index 401939005..f7f5b68ea 100644 --- a/engine/classes/ElggSite.php +++ b/engine/classes/ElggSite.php @@ -117,37 +117,18 @@ class ElggSite extends ElggEntity { * @throws InvalidClassException */ protected function load($guid) { - // Test to see if we have the generic stuff - if (!parent::load($guid)) { - return false; - } + $attr_loader = new ElggAttributeLoader(get_class(), 'site', $this->attributes); + $attr_loader->requires_access_control = !($this instanceof ElggPlugin); + $attr_loader->secondary_loader = 'get_site_entity_as_row'; - // Only work with GUID from here - if ($guid instanceof stdClass) { - $guid = $guid->guid; - } - - // Check the type - if ($this->attributes['type'] != 'site') { - $msg = elgg_echo('InvalidClassException:NotValidElggStar', array($guid, get_class())); - throw new InvalidClassException($msg); - } - - // Load missing data - $row = get_site_entity_as_row($guid); - if (($row) && (!$this->isFullyLoaded())) { - // If $row isn't a cached copy then increment the counter - $this->attributes['tables_loaded']++; - } - - // Now put these into the attributes array as core values - $objarray = (array) $row; - foreach ($objarray as $key => $value) { - $this->attributes[$key] = $value; + $attrs = $attr_loader->getRequiredAttributes($guid); + if (!$attrs) { + return false; } - // guid needs to be an int http://trac.elgg.org/ticket/4111 - $this->attributes['guid'] = (int)$this->attributes['guid']; + $this->attributes = $attrs; + $this->attributes['tables_loaded'] = 2; + cache_entity($this); return true; } diff --git a/engine/classes/ElggStaticVariableCache.php b/engine/classes/ElggStaticVariableCache.php index 787d35a32..17d849400 100644 --- a/engine/classes/ElggStaticVariableCache.php +++ b/engine/classes/ElggStaticVariableCache.php @@ -21,8 +21,8 @@ class ElggStaticVariableCache extends ElggSharedMemoryCache { * This function creates a variable cache in a static variable in * memory, optionally with a given namespace (to avoid overlap). * - * @param string $namespace The namespace for this cache to write to - * note, namespaces of the same name are shared! + * @param string $namespace The namespace for this cache to write to. + * @note namespaces of the same name are shared! */ function __construct($namespace = 'default') { $this->setNamespace($namespace); diff --git a/engine/classes/ElggUser.php b/engine/classes/ElggUser.php index d7bb89265..6c1cdc1de 100644 --- a/engine/classes/ElggUser.php +++ b/engine/classes/ElggUser.php @@ -106,37 +106,17 @@ class ElggUser extends ElggEntity * @return bool */ protected function load($guid) { - // Test to see if we have the generic stuff - if (!parent::load($guid)) { - return false; - } + $attr_loader = new ElggAttributeLoader(get_class(), 'user', $this->attributes); + $attr_loader->secondary_loader = 'get_user_entity_as_row'; - // Only work with GUID from here - if ($guid instanceof stdClass) { - $guid = $guid->guid; - } - - // Check the type - if ($this->attributes['type'] != 'user') { - $msg = elgg_echo('InvalidClassException:NotValidElggStar', array($guid, get_class())); - throw new InvalidClassException($msg); - } - - // Load missing data - $row = get_user_entity_as_row($guid); - if (($row) && (!$this->isFullyLoaded())) { - // If $row isn't a cached copy then increment the counter - $this->attributes['tables_loaded']++; - } - - // Now put these into the attributes array as core values - $objarray = (array) $row; - foreach ($objarray as $key => $value) { - $this->attributes[$key] = $value; + $attrs = $attr_loader->getRequiredAttributes($guid); + if (!$attrs) { + return false; } - // guid needs to be an int http://trac.elgg.org/ticket/4111 - $this->attributes['guid'] = (int)$this->attributes['guid']; + $this->attributes = $attrs; + $this->attributes['tables_loaded'] = 2; + cache_entity($this); return true; } diff --git a/engine/classes/IncompleteEntityException.php b/engine/classes/IncompleteEntityException.php new file mode 100644 index 000000000..8c86edcc6 --- /dev/null +++ b/engine/classes/IncompleteEntityException.php @@ -0,0 +1,10 @@ +<?php +/** + * IncompleteEntityException + * Thrown when constructing an entity that is missing its secondary entity table + * + * @package Elgg.Core + * @subpackage Exception + * @access private + */ +class IncompleteEntityException extends Exception {} diff --git a/engine/lib/access.php b/engine/lib/access.php index 3b2b7aeaa..f7d3bf7ea 100644 --- a/engine/lib/access.php +++ b/engine/lib/access.php @@ -12,6 +12,26 @@ */ /** + * Return an ElggCache static variable cache for the access caches + * + * @staticvar ElggStaticVariableCache $access_cache + * @return \ElggStaticVariableCache + * @access private + */ +function _elgg_get_access_cache() { + /** + * A default filestore cache using the dataroot. + */ + static $access_cache; + + if (!$access_cache) { + $access_cache = new ElggStaticVariableCache('access'); + } + + return $access_cache; +} + +/** * Return a string of access_ids for $user_id appropriate for inserting into an SQL IN clause. * * @uses get_access_array @@ -29,10 +49,10 @@ */ function get_access_list($user_id = 0, $site_id = 0, $flush = false) { global $CONFIG, $init_finished; - static $access_list; - - if (!isset($access_list)) { - $access_list = array(); + $cache = _elgg_get_access_cache(); + + if ($flush) { + $cache->clear(); } if ($user_id == 0) { @@ -45,20 +65,20 @@ function get_access_list($user_id = 0, $site_id = 0, $flush = false) { $user_id = (int) $user_id; $site_id = (int) $site_id; - if (isset($access_list[$user_id]) && $flush == false) { - return $access_list[$user_id]; - } + $hash = $user_id . $site_id . 'get_access_list'; - $access = "(" . implode(",", get_access_array($user_id, $site_id, $flush)) . ")"; + if ($cache[$hash]) { + return $cache[$hash]; + } + + $access_array = get_access_array($user_id, $site_id, $flush); + $access = "(" . implode(",", $access_array) . ")"; - // only cache if done with init and access is enabled (unless admin user) - // session is loaded before init is finished, so don't need to check for user session - if ($init_finished && (elgg_is_admin_logged_in() || !elgg_get_ignore_access())) { - $access_list[$user_id] = $access; - return $access_list[$user_id]; - } else { - return $access; + if ($init_finished) { + $cache[$hash] = $access; } + + return $access; } /** @@ -86,9 +106,11 @@ function get_access_list($user_id = 0, $site_id = 0, $flush = false) { function get_access_array($user_id = 0, $site_id = 0, $flush = false) { global $CONFIG, $init_finished; - // @todo everything from the db is cached. - // this cache might be redundant. But db cache is flushed on every db write. - static $access_array = array(); + $cache = _elgg_get_access_cache(); + + if ($flush) { + $cache->clear(); + } if ($user_id == 0) { $user_id = elgg_get_logged_in_user_guid(); @@ -101,35 +123,41 @@ function get_access_array($user_id = 0, $site_id = 0, $flush = false) { $user_id = (int) $user_id; $site_id = (int) $site_id; - if (empty($access_array[$user_id]) || $flush == true) { - $tmp_access_array = array(ACCESS_PUBLIC); + $hash = $user_id . $site_id . 'get_access_array'; + + if ($cache[$hash]) { + $access_array = $cache[$hash]; + } else { + $access_array = array(ACCESS_PUBLIC); // The following can only return sensible data if the user is logged in. if (elgg_is_logged_in()) { - $tmp_access_array[] = ACCESS_LOGGED_IN; + $access_array[] = ACCESS_LOGGED_IN; // Get ACL memberships $query = "SELECT am.access_collection_id" . " FROM {$CONFIG->dbprefix}access_collection_membership am" . " LEFT JOIN {$CONFIG->dbprefix}access_collections ag ON ag.id = am.access_collection_id" - . " WHERE am.user_guid = {$user_id} AND (ag.site_guid = {$site_id} OR ag.site_guid = 0)"; + . " WHERE am.user_guid = $user_id AND (ag.site_guid = $site_id OR ag.site_guid = 0)"; - if ($collections = get_data($query)) { + $collections = get_data($query); + if ($collections) { foreach ($collections as $collection) { if (!empty($collection->access_collection_id)) { - $tmp_access_array[] = (int)$collection->access_collection_id; + $access_array[] = (int)$collection->access_collection_id; } } } // Get ACLs owned. $query = "SELECT ag.id FROM {$CONFIG->dbprefix}access_collections ag "; - $query .= "WHERE ag.owner_guid = {$user_id} AND (ag.site_guid = {$site_id} OR ag.site_guid = 0)"; + $query .= "WHERE ag.owner_guid = $user_id AND (ag.site_guid = $site_id OR ag.site_guid = 0)"; - if ($collections = get_data($query)) { + $collections = get_data($query); + if ($collections) { foreach ($collections as $collection) { if (!empty($collection->id)) { - $tmp_access_array[] = (int)$collection->id; + $access_array[] = (int)$collection->id; } } } @@ -137,21 +165,21 @@ function get_access_array($user_id = 0, $site_id = 0, $flush = false) { $ignore_access = elgg_check_access_overrides($user_id); if ($ignore_access == true) { - $tmp_access_array[] = ACCESS_PRIVATE; + $access_array[] = ACCESS_PRIVATE; } + } - // only cache if done with init and access is enabled (unless admin user) - // session is loaded before init is finished, so don't need to check for user session - if ($init_finished && (elgg_is_admin_logged_in() || !elgg_get_ignore_access())) { - $access_array[$user_id] = $tmp_access_array; - } + if ($init_finished) { + $cache[$hash] = $access_array; } - } else { - $tmp_access_array = $access_array[$user_id]; } - $options = array('user_id' => $user_id, 'site_id' => $site_id); - return elgg_trigger_plugin_hook('access:collections:read', 'user', $options, $tmp_access_array); + $options = array( + 'user_id' => $user_id, + 'site_id' => $site_id + ); + + return elgg_trigger_plugin_hook('access:collections:read', 'user', $options, $access_array); } /** @@ -397,9 +425,12 @@ function has_access_to_entity($entity, $user = null) { * @link http://docs.elgg.org/Access */ function get_write_access_array($user_id = 0, $site_id = 0, $flush = false) { - global $CONFIG; - //@todo this is probably not needed since caching happens at the DB level. - static $access_array; + global $CONFIG, $init_finished; + $cache = _elgg_get_access_cache(); + + if ($flush) { + $cache->clear(); + } if ($user_id == 0) { $user_id = elgg_get_logged_in_user_guid(); @@ -412,37 +443,41 @@ function get_write_access_array($user_id = 0, $site_id = 0, $flush = false) { $user_id = (int) $user_id; $site_id = (int) $site_id; - if (empty($access_array[$user_id]) || $flush == true) { - $query = "SELECT ag.* FROM {$CONFIG->dbprefix}access_collections ag "; - $query .= " WHERE (ag.site_guid = {$site_id} OR ag.site_guid = 0)"; - $query .= " AND (ag.owner_guid = {$user_id})"; - // ACCESS_PRIVATE through ACCESS_PUBLIC take 0 through 2 - // @todo this AND clause is unnecessary because of id starts at 3 for table - $query .= " AND ag.id >= 3"; + $hash = $user_id . $site_id . 'get_write_access_array'; - $tmp_access_array = array( + if ($cache[$hash]) { + $access_array = $cache[$hash]; + } else { + // @todo is there such a thing as public write access? + $access_array = array( ACCESS_PRIVATE => elgg_echo("PRIVATE"), ACCESS_FRIENDS => elgg_echo("access:friends:label"), ACCESS_LOGGED_IN => elgg_echo("LOGGED_IN"), ACCESS_PUBLIC => elgg_echo("PUBLIC") ); + + $query = "SELECT ag.* FROM {$CONFIG->dbprefix}access_collections ag "; + $query .= " WHERE (ag.site_guid = $site_id OR ag.site_guid = 0)"; + $query .= " AND (ag.owner_guid = $user_id)"; + $collections = get_data($query); if ($collections) { foreach ($collections as $collection) { - $tmp_access_array[$collection->id] = $collection->name; + $access_array[$collection->id] = $collection->name; } } - $access_array[$user_id] = $tmp_access_array; - } else { - $tmp_access_array = $access_array[$user_id]; + if ($init_finished) { + $cache[$hash] = $access_array; + } } - $options = array('user_id' => $user_id, 'site_id' => $site_id); - $tmp_access_array = elgg_trigger_plugin_hook('access:collections:write', 'user', - $options, $tmp_access_array); - - return $tmp_access_array; + $options = array( + 'user_id' => $user_id, + 'site_id' => $site_id + ); + return elgg_trigger_plugin_hook('access:collections:write', 'user', + $options, $access_array); } /** @@ -871,6 +906,8 @@ function get_readable_access_level($entity_access_id) { * @tip Use this to access entities in automated scripts * when no user is logged in. * + * @note This clears the access cache. + * * @warning This will not show disabled entities. * Use {@link access_show_hidden_entities()} to access disabled entities. * @@ -882,6 +919,8 @@ function get_readable_access_level($entity_access_id) { * @see elgg_get_ignore_access() */ function elgg_set_ignore_access($ignore = true) { + $cache = _elgg_get_access_cache(); + $cache->clear(); $elgg_access = elgg_get_access_object(); return $elgg_access->setIgnoreAccess($ignore); } diff --git a/engine/lib/admin.php b/engine/lib/admin.php index 3f23f079c..35ab5599d 100644 --- a/engine/lib/admin.php +++ b/engine/lib/admin.php @@ -233,6 +233,7 @@ function admin_init() { elgg_register_action('admin/site/update_basic', '', 'admin'); elgg_register_action('admin/site/update_advanced', '', 'admin'); elgg_register_action('admin/site/flush_cache', '', 'admin'); + elgg_register_action('admin/site/unlock_upgrade', '', 'admin'); elgg_register_action('admin/menu/save', '', 'admin'); @@ -422,7 +423,7 @@ function admin_pagesetup() { elgg_register_menu_item('admin_footer', array( 'name' => 'community_forums', 'text' => elgg_echo('admin:footer:community_forums'), - 'href' => 'http://community.elgg.org/pg/groups/world/', + 'href' => 'http://community.elgg.org/groups/all/', )); elgg_register_menu_item('admin_footer', array( diff --git a/engine/lib/annotations.php b/engine/lib/annotations.php index 2036ccd61..3b9f84703 100644 --- a/engine/lib/annotations.php +++ b/engine/lib/annotations.php @@ -316,8 +316,6 @@ function elgg_list_annotations($options) { * * annotation_owner_guids => NULL|ARR guids for annotaiton owners * - * annotation_ids => NULL|ARR Annotation IDs - * * @return mixed If count, int. If not count, array. false on errors. * @since 1.7.0 */ @@ -336,8 +334,6 @@ function elgg_get_entities_from_annotations(array $options = array()) { 'annotation_owner_guids' => ELGG_ENTITIES_ANY_VALUE, - 'annotation_ids' => ELGG_ENTITIES_ANY_VALUE, - 'order_by' => 'maxtime desc', 'group_by' => 'a.entity_guid' ); @@ -345,12 +341,13 @@ function elgg_get_entities_from_annotations(array $options = array()) { $options = array_merge($defaults, $options); $singulars = array('annotation_name', 'annotation_value', - 'annotation_name_value_pair', 'annotation_owner_guid', 'annotation_id'); + 'annotation_name_value_pair', 'annotation_owner_guid'); $options = elgg_normalise_plural_options_array($options, $singulars); + $options = elgg_entities_get_metastrings_options('annotation', $options); - if (!$options = elgg_entities_get_metastrings_options('annotation', $options)) { - return FALSE; + if (!$options) { + return false; } // special sorting for annotations diff --git a/engine/lib/entities.php b/engine/lib/entities.php index fda554388..ce736ce05 100644 --- a/engine/lib/entities.php +++ b/engine/lib/entities.php @@ -773,7 +773,13 @@ function get_entity($guid) { } } - $new_entity = entity_row_to_elggstar($entity_row); + // don't let incomplete entities cause fatal exceptions + try { + $new_entity = entity_row_to_elggstar($entity_row); + } catch (IncompleteEntityException $e) { + return false; + } + if ($new_entity) { cache_entity($new_entity); } @@ -1018,7 +1024,12 @@ 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(); @@ -1047,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 diff --git a/engine/lib/group.php b/engine/lib/group.php index 852c08de9..5a38e1ea6 100644 --- a/engine/lib/group.php +++ b/engine/lib/group.php @@ -248,48 +248,42 @@ function get_users_membership($user_guid) { } /** - * Checks access to a group. + * May the current user access item(s) on this page? If the page owner is a group, + * membership, visibility, and logged in status are taken into account. * * @param boolean $forward If set to true (default), will forward the page; * if set to false, will return true or false. * - * @return true|false If $forward is set to false. + * @return bool If $forward is set to false. */ function group_gatekeeper($forward = true) { - $allowed = true; - $url = ''; - - if ($group = elgg_get_page_owner_entity()) { - if ($group instanceof ElggGroup) { - $url = $group->getURL(); - if (!$group->isPublicMembership()) { - // closed group so must be member or an admin - - if (!elgg_is_logged_in()) { - $allowed = false; - if ($forward == true) { - $_SESSION['last_forward_from'] = current_page_url(); - register_error(elgg_echo('loggedinrequired')); - forward('', 'login'); - } - } else if (!$group->isMember(elgg_get_logged_in_user_entity())) { - $allowed = false; - } - // Admin override - if (elgg_is_admin_logged_in()) { - $allowed = true; - } - } - } + $page_owner_guid = elgg_get_page_owner_guid(); + if (!$page_owner_guid) { + return true; } + $visibility = ElggGroupItemVisibility::factory($page_owner_guid); - if ($forward && $allowed == false) { - register_error(elgg_echo('membershiprequired')); - forward($url, 'member'); + if (!$visibility->shouldHideItems) { + return true; } + if ($forward) { + // only forward to group if user can see it + $group = get_entity($page_owner_guid); + $forward_url = $group ? $group->getURL() : ''; + + if (!elgg_is_logged_in()) { + $_SESSION['last_forward_from'] = current_page_url(); + $forward_reason = 'login'; + } else { + $forward_reason = 'member'; + } - return $allowed; + register_error(elgg_echo($visibility->reasonHidden)); + forward($forward_url, $forward_reason); + } + + return false; } /** diff --git a/engine/lib/navigation.php b/engine/lib/navigation.php index 8c3952594..86624cd7c 100644 --- a/engine/lib/navigation.php +++ b/engine/lib/navigation.php @@ -308,6 +308,32 @@ function elgg_site_menu_setup($hook, $type, $return, $params) { $return['more'] = array_splice($return['default'], $max_display_items); } } + + // check if we have anything selected + $selected = false; + foreach ($return as $section_name => $section) { + foreach ($section as $key => $item) { + if ($item->getSelected()) { + $selected = true; + break 2; + } + } + } + + if (!$selected) { + // nothing selected, match name to context + foreach ($return as $section_name => $section) { + foreach ($section as $key => $item) { + // only highlight internal links + if (strpos($item->getHref(), elgg_get_site_url()) === 0) { + if ($item->getName() == elgg_get_context()) { + $return[$section_name][$key]->setSelected(true); + break 2; + } + } + } + } + } return $return; } diff --git a/engine/lib/pageowner.php b/engine/lib/pageowner.php index 0cf0e0625..94765feee 100644 --- a/engine/lib/pageowner.php +++ b/engine/lib/pageowner.php @@ -37,6 +37,8 @@ function elgg_get_page_owner_guid($guid = 0) { /** * Gets the owner entity for the current page. * + * @note Access is disabled when getting the page owner entity. + * * @return ElggEntity|false The current page owner or false if none. * * @since 1.8.0 @@ -44,10 +46,14 @@ function elgg_get_page_owner_guid($guid = 0) { function elgg_get_page_owner_entity() { $guid = elgg_get_page_owner_guid(); if ($guid > 0) { - return get_entity($guid); + $ia = elgg_set_ignore_access(true); + $owner = get_entity($guid); + elgg_set_ignore_access($ia); + + return $owner; } - return FALSE; + return false; } /** @@ -75,6 +81,8 @@ function elgg_set_page_owner_guid($guid) { * <handler>/edit/<entity guid> * <handler>/group/<group guid> * + * @note Access is disabled while finding the page owner for the group gatekeeper functions. + * * * @param string $hook 'page_owner' * @param string $entity_type 'system' @@ -90,6 +98,8 @@ function default_page_owner_handler($hook, $entity_type, $returnvalue, $params) return $returnvalue; } + $ia = elgg_set_ignore_access(true); + $username = get_input("username"); if ($username) { // @todo using a username of group:<guid> is deprecated @@ -97,6 +107,7 @@ function default_page_owner_handler($hook, $entity_type, $returnvalue, $params) preg_match('/group\:([0-9]+)/i', $username, $matches); $guid = $matches[1]; if ($entity = get_entity($guid)) { + elgg_set_ignore_access($ia); return $entity->getGUID(); } } @@ -109,6 +120,7 @@ function default_page_owner_handler($hook, $entity_type, $returnvalue, $params) $owner = get_input("owner_guid"); if ($owner) { if ($user = get_entity($owner)) { + elgg_set_ignore_access($ia); return $user->getGUID(); } } @@ -130,6 +142,7 @@ function default_page_owner_handler($hook, $entity_type, $returnvalue, $params) case 'friends': $user = get_user_by_username($segments[2]); if ($user) { + elgg_set_ignore_access($ia); return $user->getGUID(); } break; @@ -137,6 +150,7 @@ function default_page_owner_handler($hook, $entity_type, $returnvalue, $params) case 'edit': $entity = get_entity($segments[2]); if ($entity) { + elgg_set_ignore_access($ia); return $entity->getContainerGUID(); } break; @@ -144,6 +158,7 @@ function default_page_owner_handler($hook, $entity_type, $returnvalue, $params) case 'group': $entity = get_entity($segments[2]); if ($entity) { + elgg_set_ignore_access($ia); return $entity->getGUID(); } break; @@ -151,7 +166,7 @@ function default_page_owner_handler($hook, $entity_type, $returnvalue, $params) } } - return $returnvalue; + elgg_set_ignore_access($ia); } /** diff --git a/engine/lib/plugins.php b/engine/lib/plugins.php index ca4a957f4..94aff277e 100644 --- a/engine/lib/plugins.php +++ b/engine/lib/plugins.php @@ -208,6 +208,7 @@ function elgg_get_plugin_from_id($plugin_id) { 'type' => 'object', 'subtype' => 'plugin', 'joins' => array("JOIN {$db_prefix}objects_entity oe on oe.guid = e.guid"), + 'selects' => array("oe.title", "oe.description"), 'wheres' => array("oe.title = '$plugin_id'"), 'limit' => 1 ); diff --git a/engine/lib/upgrade.php b/engine/lib/upgrade.php index f0874a483..f4f4b16f5 100644 --- a/engine/lib/upgrade.php +++ b/engine/lib/upgrade.php @@ -311,3 +311,58 @@ function elgg_upgrade_bootstrap_17_to_18() { return elgg_set_processed_upgrades($processed_upgrades); } + +/** + * Creates a table {prefix}upgrade_lock that is used as a mutex for upgrades. + * + * @see _elgg_upgrade_lock() + * + * @return bool + * @access private + */ +function _elgg_upgrade_lock() { + global $CONFIG; + + if (!_elgg_upgrade_is_locked()) { + // lock it + insert_data("create table {$CONFIG->dbprefix}upgrade_lock (id INT)"); + elgg_log('Locked for upgrade.', 'NOTICE'); + return true; + } + + elgg_log('Cannot lock for upgrade: already locked.', 'WARNING'); + return false; +} + +/** + * Unlocks upgrade. + * + * @see _elgg_upgrade_lock() + * + * @access private + */ +function _elgg_upgrade_unlock() { + global $CONFIG; + delete_data("drop table {$CONFIG->dbprefix}upgrade_lock"); + elgg_log('Upgrade unlocked.', 'NOTICE'); +} + +/** + * Checks if upgrade is locked + * + * @return bool + * @access private + */ +function _elgg_upgrade_is_locked() { + global $CONFIG, $DB_QUERY_CACHE; + + $is_locked = count(get_data("show tables like '{$CONFIG->dbprefix}upgrade_lock'")); + + // Invalidate query cache + if ($DB_QUERY_CACHE) { + $DB_QUERY_CACHE->clear(); + elgg_log("Query cache invalidated", 'NOTICE'); + } + + return $is_locked; +} diff --git a/engine/lib/views.php b/engine/lib/views.php index 69bceabf5..8618c2997 100644 --- a/engine/lib/views.php +++ b/engine/lib/views.php @@ -1236,6 +1236,15 @@ function elgg_view_river_item($item, array $vars = array()) { // subject is disabled or subject/object deleted return ''; } + // Don't hide objects in closed groups that a user can see. + // see http://trac.elgg.org/ticket/4789 +// else { +// // hide based on object's container +// $visibility = ElggGroupItemVisibility::factory($object->container_guid); +// if ($visibility->shouldHideItems) { +// return ''; +// } +// } $vars['item'] = $item; diff --git a/engine/tests/api/access_collections.php b/engine/tests/api/access_collections.php index bea995a6e..ebcd7d318 100644 --- a/engine/tests/api/access_collections.php +++ b/engine/tests/api/access_collections.php @@ -268,4 +268,26 @@ class ElggCoreAccessCollectionsTest extends ElggCoreUnitTest { $group->delete(); } + + public function testAccessCaching() { + // create a new user to check against + $user = new ElggUser(); + $user->username = 'access_test_user'; + $user->save(); + + foreach (array('get_access_list', 'get_access_array') as $func) { + $cache = _elgg_get_access_cache(); + $cache->clear(); + + // admin users run tests, so disable access + elgg_set_ignore_access(true); + $access = $func($user->getGUID()); + + elgg_set_ignore_access(false); + $access2 = $func($user->getGUID()); + $this->assertNotEqual($access, $access2, "Access test for $func"); + } + + $user->delete(); + } } |