<?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 '';
|
|
}
|
|
}
|
|
}
|