/lib/core/Perms.php
PHP | 416 lines | 218 code | 54 blank | 144 comment | 23 complexity | 03a783e8a91c915ee2e6d53827ee0aef MD5 | raw file
- <?php
- // (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
- //
- // All Rights Reserved. See copyright.txt for details and a complete list of authors.
- // Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
- // $Id$
- /**
- * Facade class of the permission subsystem. Once configured, the ::get()
- * static method can be used to obtain accessors for specific objects.
- * The accessor will contain all the rules applicable to the object.
- *
- * Sample usage:
- * $perms = Perms::get( array(
- * 'type' => 'wiki page',
- * 'object' => 'HomePage',
- * ) );
- *
- * if ( $perms->view_calendar ) {
- * // ...
- * }
- *
- * Global permissions may be obtained using Perms::get() without a context.
- *
- * Please note that the Perms will now be correct for checking trackeritem
- * context and permissions assigned to parent tracker. If no trackeritem
- * specific permissions are set on the object or category level, system will
- * check parent tracker permissions before continuing to the global level.
- *
- * The facade also provides a convenient way to filter lists based on
- * permissions. Using the method will also used the underlying::bulk()
- * method to charge permissions for multiple objects at once and reduce
- * the amount of queries required.
- *
- * Sample usage:
- * $pages = $tikilib->listpages();
- *
- * $filtered = Perms::filter(
- * array( 'type' => 'wiki page' ),
- * 'object',
- * $pages,
- * array( 'object' => 'pageName' ),
- * 'view' );
- *
- * The sample above would return the data without elements not visible,
- * assuming tiki_p_view is required, in the same format as provided.
- * In a standard configuration, this filter would use a maximum of
- * 4 queries to the database, less if some elements were previously
- * loaded.
- *
- * The permission facade handles local caching of the decision rules,
- * meaning that calling the facade for the same object twice will not
- * cause multiple queries to the database. Rather, the same object will
- * be provided. Moreover, if two objects use the same rules, like two
- * objects with the same set of categories, the rules will be shared
- * between the two accessors.
- *
- * Configuration of the facade is required only once. Configuration
- * includes indicating which rules apply, which are the active groups
- * for the current user and a prefix for backwards compatibility. The
- * rules are provided as a list of ResolverFactory objects. Each of
- * these objects will fetch the permissions applicable for the given
- * context. The first factory providing a Resolver for the context
- * will be the applicable set of rules. For example, when configured
- * with ObjectFactory, CategoryFactory and GlobalFactory, the facade
- * would first search for object permissions, if none are found, it
- * would fall back to categories and finally to globals. Global
- * guarentees a basic set of rules.
- *
- * The context is provided as an array for extensibility. Currently,
- * type and object are the only two known keys.
- *
- * Resolvers are group agnostic, meaning the same resolver will be
- * provided no matter which groups have been configured. This allows
- * for more caching possible. As a general rule, the permission sub-
- * system fetches all the information it may require and counts on
- * caching to have the extra cost diminished over multiple requests.
- *
- * The accessors are simply a binding between the groups and the
- * resolver that provides a convenient access to the permissions.
- * The introduction paragraphs mentionned accessors were build for
- * specific objects and shared when multiple requests were made. This
- * is in fact incorrect. A new accessor is built every time, however
- * those are very thin and they share a common resolver. These separate
- * instances allow to reconfigure the accessors depending on the
- * environment in which they are used. For example, the accessors are
- * configured with the global groups by default. However, they can be
- * replaced to evaluate the permissions for a different user
- * by creating a new Perms_Context object before accessing the perms,
- * e.g.
- * $permissionContext = new Perms_Context($aUserName);
- *
- * Each ResolverFactory will generate a hash from the context which
- * represents a unique key to the matching resolver it would provide.
- * The hash provided by the global factory is a constant key, the one
- * provided by the object factory is straightforward and the one
- * provided for categories is a list of all categories applicable to
- * the object. These hashes are used to shortcut the amount of
- * database queries executed by reusing as much data as possible.
- */
- class Perms
- {
- private static $instance;
- private $prefix = '';
- private $groups = [];
- private $factories = [];
- private $checkSequence = null;
- private $hashes = [];
- private $filterCache = [];
- /**
- * Provides a new accessor configured with the global settings and
- * a resolver appropriate to the context requested.
- */
- public static function get($context = [])
- {
- if (! is_array($context)) {
- $args = func_get_args();
- $context = [
- 'type' => $args[0],
- 'object' => $args[1],
- 'parentId' => isset($args[2]) ? $args[2] : null,
- ];
- }
- if (self::$instance) {
- return self::$instance->getAccessor($context);
- } else {
- $accessor = new Perms_Accessor();
- $accessor->setContext($context);
- return $accessor;
- }
- }
- public function getAccessor(array $context = [])
- {
- $accessor = new Perms_Accessor();
- $accessor->setContext($context);
- $accessor->setPrefix($this->prefix);
- $accessor->setGroups($this->groups);
- if ($this->checkSequence) {
- $accessor->setCheckSequence($this->checkSequence);
- }
- if ($resolver = $this->getResolver($context)) {
- $accessor->setResolver($resolver);
- }
- return $accessor;
- }
- public static function getInstance()
- {
- return self::$instance;
- }
- /**
- * Sets the global Perms instance to use when obtaining accessors.
- */
- public static function set(self $perms)
- {
- self::$instance = $perms;
- }
- /**
- * Loads the data for multiple contexts at the same time. This method
- * can be used to reduce the amount of queries performed to request
- * multiple accessors. The method simply forwards the bulk call to
- * each of the ResolverFactory object in sequence, which is then
- * responsible to handle the call in an efficient manner and return
- * the list of objects which are left to be handled. Only the remaining
- * objects are sent to the subsequent factories.
- *
- * @param $baseContext array The part of the context common to all
- * objects.
- * @param $bulkKey string The key added for each of the objects in bulk
- * loading.
- * @param $data array A simple list of values to be loaded (such as a
- * list of page names) or a list of records. When
- * a list of records is provided, the $dataKey
- * parameter is required.
- * @param $dataKey mixed The key to fetch from each record when a dataset
- * is used.
- */
- public static function bulk(array $baseContext, $bulkKey, array $data, $dataKey = null)
- {
- $remaining = [];
- foreach ($data as $entry) {
- if ($dataKey) {
- $value = $entry[$dataKey];
- } else {
- $value = $entry;
- }
- $remaining[] = $value;
- }
- if (count($remaining)) {
- self::$instance->loadBulk($baseContext, $bulkKey, $remaining);
- }
- }
- /**
- * Filters a dataset based on a permission. The method will perform bulk
- * loading of the permissions on all objects in the dataset and then
- * filter the dataset with a single permission.
- *
- * Filters are now cached on the instance level due to performance issues.
- *
- * @param $baseContext array The part of the context common to all
- * objects.
- * @param $bulkKey string The key added for each of the objects in bulk
- * loading.
- * @param $data array A list of records.
- * @param $contextMap mixed The key to fetch from each record as the object.
- * @param $permission string The permission name to validate on each record.
- * @return array What remains of the dataset after filtering.
- */
- public static function filter(array $baseContext, $bulkKey, array $data, array $contextMap, $permission)
- {
- $cacheKey = md5(serialize($baseContext) . serialize($bulkKey) . serialize($data) . serialize($contextMap) . $permission);
- if (isset(self::$instance->filterCache[$cacheKey])) {
- return self::$instance->filterCache[$cacheKey];
- }
- self::bulk($baseContext, $bulkKey, $data, $contextMap[$bulkKey]);
- $valid = [];
- foreach ($data as $entry) {
- if (self::hasPerm($baseContext, $contextMap, $entry, $permission)) {
- $valid[] = $entry;
- }
- }
- if (! isset(self::$instance->filterCache[$cacheKey])) {
- self::$instance->filterCache[$cacheKey] = $valid;
- }
- return $valid;
- }
- public static function simpleFilter($type, $key, $permission, array $data)
- {
- return self::filter(
- ['type' => $type],
- 'object',
- $data,
- ['object' => $key],
- $permission
- );
- }
- private static function hasPerm($baseContext, $contextMap, $entry, $permission)
- {
- $context = $baseContext;
- foreach ($contextMap as $to => $from) {
- $context[$to] = $entry[$from];
- }
- $accessor = self::get($context);
- if (is_array($permission)) {
- foreach ($permission as $perm) {
- if ($accessor->$perm) {
- return true;
- }
- }
- } else {
- return $accessor->$permission;
- }
- }
- public static function mixedFilter(array $baseContext, $discriminator, $bulkKey, $data, $contextMapMap, $permissionMap)
- {
- //echo '<pre>BASECONTEXT'; print_r($baseContext); echo 'DISCRIMATOR';print_r($discriminator); echo 'BULKEY';print_r($bulkKey); echo 'DATA';print_r($data); echo 'CONTEXTMAPMAP';print_r($contextMapMap); echo 'PERMISSIONMAP';print_r($permissionMap); echo '</pre>';
- $perType = [];
- foreach ($data as $row) {
- $type = $row[$discriminator];
- if (! isset($perType[$type])) {
- $perType[$type] = [];
- }
- $key = $contextMapMap[$type][$bulkKey];
- $perType[$type][] = $row[$key];
- }
- foreach ($perType as $type => $values) {
- $context = $baseContext;
- $context[ $contextMapMap[$type][$discriminator] ] = $type;
- self::$instance->loadBulk($context, $bulkKey, $values);
- }
- $valid = [];
- foreach ($data as $entry) {
- $type = $entry[$discriminator];
- if (self::hasPerm($baseContext, $contextMapMap[$type], $entry, $permissionMap[$type])) {
- $valid[] = $entry;
- }
- }
- return $valid;
- }
- public function setGroups(array $groups)
- {
- $this->groups = $groups;
- }
- public function getGroups()
- {
- return $this->groups;
- }
- public function setPrefix($prefix)
- {
- $this->prefix = $prefix;
- }
- public function setResolverFactories(array $factories)
- {
- $this->factories = $factories;
- }
- public function setCheckSequence(array $sequence)
- {
- $this->checkSequence = $sequence;
- }
- private function getResolver(array $context)
- {
- $toSet = [];
- $finalResolver = false;
- foreach ($this->factories as $factory) {
- $hash = $factory->getHash($context);
- // no hash returned by factory means factory does not support that context
- if (! $hash) {
- continue;
- }
- if (isset($this->hashes[$hash])) {
- $finalResolver = $this->hashes[$hash];
- } else {
- $finalResolver = $factory->getResolver($context);
- $toSet[$hash] = $finalResolver;
- }
- if ($finalResolver) {
- break;
- }
- }
- // Limit the amount of hashes preserved to reduce memory consumption
- if (count($this->hashes) > 1024) {
- $this->hashes = [];
- }
- foreach ($toSet as $hash => $resolver) {
- $this->hashes[$hash] = $resolver;
- }
- return $finalResolver;
- }
- private function loadBulk($baseContext, $bulkKey, $data)
- {
- foreach ($this->factories as $factory) {
- $data = $factory->bulk($baseContext, $bulkKey, $data);
- }
- }
- public function clear()
- {
- $this->hashes = [];
- foreach ($this->factories as $factory) {
- if (method_exists($factory, 'clear')) {
- $factory->clear();
- }
- }
- }
- public static function parentType($type)
- {
- switch ($type) {
- case 'trackeritem':
- return 'tracker';
- case 'file':
- return 'file gallery';
- case 'article':
- return 'topic';
- case 'blog post':
- return 'blog';
- case 'thread':
- return 'forum';
- case 'calendaritem':
- case 'event':
- return 'calendar';
- default:
- return '';
- }
- }
- }