PageRenderTime 27ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/core/Perms.php

https://gitlab.com/ElvisAns/tiki
PHP | 416 lines | 218 code | 54 blank | 144 comment | 23 complexity | 03a783e8a91c915ee2e6d53827ee0aef MD5 | raw file
  1. <?php
  2. // (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
  3. //
  4. // All Rights Reserved. See copyright.txt for details and a complete list of authors.
  5. // Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
  6. // $Id$
  7. /**
  8. * Facade class of the permission subsystem. Once configured, the ::get()
  9. * static method can be used to obtain accessors for specific objects.
  10. * The accessor will contain all the rules applicable to the object.
  11. *
  12. * Sample usage:
  13. * $perms = Perms::get( array(
  14. * 'type' => 'wiki page',
  15. * 'object' => 'HomePage',
  16. * ) );
  17. *
  18. * if ( $perms->view_calendar ) {
  19. * // ...
  20. * }
  21. *
  22. * Global permissions may be obtained using Perms::get() without a context.
  23. *
  24. * Please note that the Perms will now be correct for checking trackeritem
  25. * context and permissions assigned to parent tracker. If no trackeritem
  26. * specific permissions are set on the object or category level, system will
  27. * check parent tracker permissions before continuing to the global level.
  28. *
  29. * The facade also provides a convenient way to filter lists based on
  30. * permissions. Using the method will also used the underlying::bulk()
  31. * method to charge permissions for multiple objects at once and reduce
  32. * the amount of queries required.
  33. *
  34. * Sample usage:
  35. * $pages = $tikilib->listpages();
  36. *
  37. * $filtered = Perms::filter(
  38. * array( 'type' => 'wiki page' ),
  39. * 'object',
  40. * $pages,
  41. * array( 'object' => 'pageName' ),
  42. * 'view' );
  43. *
  44. * The sample above would return the data without elements not visible,
  45. * assuming tiki_p_view is required, in the same format as provided.
  46. * In a standard configuration, this filter would use a maximum of
  47. * 4 queries to the database, less if some elements were previously
  48. * loaded.
  49. *
  50. * The permission facade handles local caching of the decision rules,
  51. * meaning that calling the facade for the same object twice will not
  52. * cause multiple queries to the database. Rather, the same object will
  53. * be provided. Moreover, if two objects use the same rules, like two
  54. * objects with the same set of categories, the rules will be shared
  55. * between the two accessors.
  56. *
  57. * Configuration of the facade is required only once. Configuration
  58. * includes indicating which rules apply, which are the active groups
  59. * for the current user and a prefix for backwards compatibility. The
  60. * rules are provided as a list of ResolverFactory objects. Each of
  61. * these objects will fetch the permissions applicable for the given
  62. * context. The first factory providing a Resolver for the context
  63. * will be the applicable set of rules. For example, when configured
  64. * with ObjectFactory, CategoryFactory and GlobalFactory, the facade
  65. * would first search for object permissions, if none are found, it
  66. * would fall back to categories and finally to globals. Global
  67. * guarentees a basic set of rules.
  68. *
  69. * The context is provided as an array for extensibility. Currently,
  70. * type and object are the only two known keys.
  71. *
  72. * Resolvers are group agnostic, meaning the same resolver will be
  73. * provided no matter which groups have been configured. This allows
  74. * for more caching possible. As a general rule, the permission sub-
  75. * system fetches all the information it may require and counts on
  76. * caching to have the extra cost diminished over multiple requests.
  77. *
  78. * The accessors are simply a binding between the groups and the
  79. * resolver that provides a convenient access to the permissions.
  80. * The introduction paragraphs mentionned accessors were build for
  81. * specific objects and shared when multiple requests were made. This
  82. * is in fact incorrect. A new accessor is built every time, however
  83. * those are very thin and they share a common resolver. These separate
  84. * instances allow to reconfigure the accessors depending on the
  85. * environment in which they are used. For example, the accessors are
  86. * configured with the global groups by default. However, they can be
  87. * replaced to evaluate the permissions for a different user
  88. * by creating a new Perms_Context object before accessing the perms,
  89. * e.g.
  90. * $permissionContext = new Perms_Context($aUserName);
  91. *
  92. * Each ResolverFactory will generate a hash from the context which
  93. * represents a unique key to the matching resolver it would provide.
  94. * The hash provided by the global factory is a constant key, the one
  95. * provided by the object factory is straightforward and the one
  96. * provided for categories is a list of all categories applicable to
  97. * the object. These hashes are used to shortcut the amount of
  98. * database queries executed by reusing as much data as possible.
  99. */
  100. class Perms
  101. {
  102. private static $instance;
  103. private $prefix = '';
  104. private $groups = [];
  105. private $factories = [];
  106. private $checkSequence = null;
  107. private $hashes = [];
  108. private $filterCache = [];
  109. /**
  110. * Provides a new accessor configured with the global settings and
  111. * a resolver appropriate to the context requested.
  112. */
  113. public static function get($context = [])
  114. {
  115. if (! is_array($context)) {
  116. $args = func_get_args();
  117. $context = [
  118. 'type' => $args[0],
  119. 'object' => $args[1],
  120. 'parentId' => isset($args[2]) ? $args[2] : null,
  121. ];
  122. }
  123. if (self::$instance) {
  124. return self::$instance->getAccessor($context);
  125. } else {
  126. $accessor = new Perms_Accessor();
  127. $accessor->setContext($context);
  128. return $accessor;
  129. }
  130. }
  131. public function getAccessor(array $context = [])
  132. {
  133. $accessor = new Perms_Accessor();
  134. $accessor->setContext($context);
  135. $accessor->setPrefix($this->prefix);
  136. $accessor->setGroups($this->groups);
  137. if ($this->checkSequence) {
  138. $accessor->setCheckSequence($this->checkSequence);
  139. }
  140. if ($resolver = $this->getResolver($context)) {
  141. $accessor->setResolver($resolver);
  142. }
  143. return $accessor;
  144. }
  145. public static function getInstance()
  146. {
  147. return self::$instance;
  148. }
  149. /**
  150. * Sets the global Perms instance to use when obtaining accessors.
  151. */
  152. public static function set(self $perms)
  153. {
  154. self::$instance = $perms;
  155. }
  156. /**
  157. * Loads the data for multiple contexts at the same time. This method
  158. * can be used to reduce the amount of queries performed to request
  159. * multiple accessors. The method simply forwards the bulk call to
  160. * each of the ResolverFactory object in sequence, which is then
  161. * responsible to handle the call in an efficient manner and return
  162. * the list of objects which are left to be handled. Only the remaining
  163. * objects are sent to the subsequent factories.
  164. *
  165. * @param $baseContext array The part of the context common to all
  166. * objects.
  167. * @param $bulkKey string The key added for each of the objects in bulk
  168. * loading.
  169. * @param $data array A simple list of values to be loaded (such as a
  170. * list of page names) or a list of records. When
  171. * a list of records is provided, the $dataKey
  172. * parameter is required.
  173. * @param $dataKey mixed The key to fetch from each record when a dataset
  174. * is used.
  175. */
  176. public static function bulk(array $baseContext, $bulkKey, array $data, $dataKey = null)
  177. {
  178. $remaining = [];
  179. foreach ($data as $entry) {
  180. if ($dataKey) {
  181. $value = $entry[$dataKey];
  182. } else {
  183. $value = $entry;
  184. }
  185. $remaining[] = $value;
  186. }
  187. if (count($remaining)) {
  188. self::$instance->loadBulk($baseContext, $bulkKey, $remaining);
  189. }
  190. }
  191. /**
  192. * Filters a dataset based on a permission. The method will perform bulk
  193. * loading of the permissions on all objects in the dataset and then
  194. * filter the dataset with a single permission.
  195. *
  196. * Filters are now cached on the instance level due to performance issues.
  197. *
  198. * @param $baseContext array The part of the context common to all
  199. * objects.
  200. * @param $bulkKey string The key added for each of the objects in bulk
  201. * loading.
  202. * @param $data array A list of records.
  203. * @param $contextMap mixed The key to fetch from each record as the object.
  204. * @param $permission string The permission name to validate on each record.
  205. * @return array What remains of the dataset after filtering.
  206. */
  207. public static function filter(array $baseContext, $bulkKey, array $data, array $contextMap, $permission)
  208. {
  209. $cacheKey = md5(serialize($baseContext) . serialize($bulkKey) . serialize($data) . serialize($contextMap) . $permission);
  210. if (isset(self::$instance->filterCache[$cacheKey])) {
  211. return self::$instance->filterCache[$cacheKey];
  212. }
  213. self::bulk($baseContext, $bulkKey, $data, $contextMap[$bulkKey]);
  214. $valid = [];
  215. foreach ($data as $entry) {
  216. if (self::hasPerm($baseContext, $contextMap, $entry, $permission)) {
  217. $valid[] = $entry;
  218. }
  219. }
  220. if (! isset(self::$instance->filterCache[$cacheKey])) {
  221. self::$instance->filterCache[$cacheKey] = $valid;
  222. }
  223. return $valid;
  224. }
  225. public static function simpleFilter($type, $key, $permission, array $data)
  226. {
  227. return self::filter(
  228. ['type' => $type],
  229. 'object',
  230. $data,
  231. ['object' => $key],
  232. $permission
  233. );
  234. }
  235. private static function hasPerm($baseContext, $contextMap, $entry, $permission)
  236. {
  237. $context = $baseContext;
  238. foreach ($contextMap as $to => $from) {
  239. $context[$to] = $entry[$from];
  240. }
  241. $accessor = self::get($context);
  242. if (is_array($permission)) {
  243. foreach ($permission as $perm) {
  244. if ($accessor->$perm) {
  245. return true;
  246. }
  247. }
  248. } else {
  249. return $accessor->$permission;
  250. }
  251. }
  252. public static function mixedFilter(array $baseContext, $discriminator, $bulkKey, $data, $contextMapMap, $permissionMap)
  253. {
  254. //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>';
  255. $perType = [];
  256. foreach ($data as $row) {
  257. $type = $row[$discriminator];
  258. if (! isset($perType[$type])) {
  259. $perType[$type] = [];
  260. }
  261. $key = $contextMapMap[$type][$bulkKey];
  262. $perType[$type][] = $row[$key];
  263. }
  264. foreach ($perType as $type => $values) {
  265. $context = $baseContext;
  266. $context[ $contextMapMap[$type][$discriminator] ] = $type;
  267. self::$instance->loadBulk($context, $bulkKey, $values);
  268. }
  269. $valid = [];
  270. foreach ($data as $entry) {
  271. $type = $entry[$discriminator];
  272. if (self::hasPerm($baseContext, $contextMapMap[$type], $entry, $permissionMap[$type])) {
  273. $valid[] = $entry;
  274. }
  275. }
  276. return $valid;
  277. }
  278. public function setGroups(array $groups)
  279. {
  280. $this->groups = $groups;
  281. }
  282. public function getGroups()
  283. {
  284. return $this->groups;
  285. }
  286. public function setPrefix($prefix)
  287. {
  288. $this->prefix = $prefix;
  289. }
  290. public function setResolverFactories(array $factories)
  291. {
  292. $this->factories = $factories;
  293. }
  294. public function setCheckSequence(array $sequence)
  295. {
  296. $this->checkSequence = $sequence;
  297. }
  298. private function getResolver(array $context)
  299. {
  300. $toSet = [];
  301. $finalResolver = false;
  302. foreach ($this->factories as $factory) {
  303. $hash = $factory->getHash($context);
  304. // no hash returned by factory means factory does not support that context
  305. if (! $hash) {
  306. continue;
  307. }
  308. if (isset($this->hashes[$hash])) {
  309. $finalResolver = $this->hashes[$hash];
  310. } else {
  311. $finalResolver = $factory->getResolver($context);
  312. $toSet[$hash] = $finalResolver;
  313. }
  314. if ($finalResolver) {
  315. break;
  316. }
  317. }
  318. // Limit the amount of hashes preserved to reduce memory consumption
  319. if (count($this->hashes) > 1024) {
  320. $this->hashes = [];
  321. }
  322. foreach ($toSet as $hash => $resolver) {
  323. $this->hashes[$hash] = $resolver;
  324. }
  325. return $finalResolver;
  326. }
  327. private function loadBulk($baseContext, $bulkKey, $data)
  328. {
  329. foreach ($this->factories as $factory) {
  330. $data = $factory->bulk($baseContext, $bulkKey, $data);
  331. }
  332. }
  333. public function clear()
  334. {
  335. $this->hashes = [];
  336. foreach ($this->factories as $factory) {
  337. if (method_exists($factory, 'clear')) {
  338. $factory->clear();
  339. }
  340. }
  341. }
  342. public static function parentType($type)
  343. {
  344. switch ($type) {
  345. case 'trackeritem':
  346. return 'tracker';
  347. case 'file':
  348. return 'file gallery';
  349. case 'article':
  350. return 'topic';
  351. case 'blog post':
  352. return 'blog';
  353. case 'thread':
  354. return 'forum';
  355. case 'calendaritem':
  356. case 'event':
  357. return 'calendar';
  358. default:
  359. return '';
  360. }
  361. }
  362. }