SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/AdminPerms.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Exception;
  17.  
  18. use karmabunny\pdb\Exceptions\QueryException;
  19. use karmabunny\pdb\Exceptions\RowMissingException;
  20.  
  21.  
  22. /**
  23. * Provides user permisison functions for the admin
  24. **/
  25. class AdminPerms
  26. {
  27. public static $access_flags;
  28. public static $subsites_permitted;
  29.  
  30. /**
  31.   * Checks whether the currently logged in operator can access the specified item.
  32.   * This method should be used for tree-based tables, like the 'pages' table,
  33.   * which may inherit their permissions from the parent record.
  34.   *
  35.   * @param string $table The table name of the item to check
  36.   * @param int $id The id of the record to check
  37.   * @returns boolean True if the operator has access, false otherwise
  38.   **/
  39. public static function checkPermissionsTree($table, $id)
  40. {
  41. Session::instance();
  42.  
  43. // Not logged in - nothing
  44. if (! $_SESSION['admin']['login_id']) return false;
  45.  
  46. // Super users can access everything
  47. if ($_SESSION['admin']['super']) return true;
  48.  
  49. // Standard users - find out which groups can access this page
  50. $access_groups = self::getAccessableGroups($table, $id);
  51. if (count($access_groups) == 0) return false;
  52.  
  53. // Check if the user is in any of those groups
  54. $params = [];
  55. $conditions = ['operator_id' => $_SESSION['admin']['login_id'], ['cat_id', 'IN', $access_groups]];
  56. $q = "SELECT 1
  57. FROM ~operators_cat_join
  58. WHERE " . Pdb::buildClause($conditions, $params);
  59. try {
  60. Pdb::query($q, $params, 'row');
  61. return true;
  62. } catch (QueryException $ex) {
  63. return false;
  64. }
  65. }
  66.  
  67.  
  68. /**
  69.   * Returns a list of groups (operator categories) which can access a specific page.
  70.   *
  71.   * Each page can either specify permissions, or inherit permissions from it's parent page.
  72.   *
  73.   * @example
  74.   * $cat_ids = AdminPerms::getAccessableGroups('pages', 10);
  75.   *
  76.   * @param string $table The table to get permissions for
  77.   * @param int $id The ID of the record to get permissions for
  78.   * @return array Integers, one per category of operator which has access
  79.   * @return false No operators have access
  80.   **/
  81. public static function getAccessableGroups($table, $id)
  82. {
  83. // The top level node always allows all categories
  84. if ($id == 0) {
  85. return array_keys(AdminAuth::getAllCategories());
  86. }
  87.  
  88. // Fetch the permissions for this page, along with some page details
  89. $single = Inflector::singular($table);
  90. $q = "SELECT tbl.parent_id, tbl.admin_perm_type, perms.category_id
  91. FROM ~{$table} AS tbl
  92. LEFT JOIN ~{$single}_admin_permissions AS perms ON perms.item_id = tbl.id
  93. WHERE tbl.id = ?";
  94. $res = Pdb::q($q, [$id], 'arr');
  95.  
  96. // No records found, so no permissions
  97. if (count($res) == 0) {
  98. return false;
  99. }
  100.  
  101. switch ($res[0]['admin_perm_type']) {
  102. case Constants::PERM_INHERIT:
  103. // Inherit from parent record
  104. return self::getAccessableGroups($table, $res[0]['parent_id']);
  105. break;
  106.  
  107. case Constants::PERM_SPECIFIC:
  108. // Grab the category IDs from the resultset fetched earlier
  109. $items = [];
  110. foreach ($res as $row) {
  111. if ($row['category_id']) {
  112. $items[] = $row['category_id'];
  113. }
  114. }
  115. return $items;
  116. break;
  117. }
  118. }
  119.  
  120.  
  121. /**
  122.   * Gets all 'access' flags for this user.
  123.   * Access flags are specified per operator-group.
  124.   * This function will return the values for all of the defined access flags,
  125.   * returning the greatest-permission flag available for the user.
  126.   * Will get data for the currently logged in user
  127.   **/
  128. public static function loadAccessFlags()
  129. {
  130. Session::instance();
  131.  
  132. self::$access_flags = array(
  133. 'access_operators' => 0,
  134. 'access_noapproval' => 0,
  135. 'access_reportemail' => 0,
  136. 'access_homepage' => 0,
  137. );
  138.  
  139. // Not logged in - nothing
  140. if (empty($_SESSION['admin']['login_id'])) return;
  141.  
  142. // Super users can access everything
  143. if ($_SESSION['admin']['super']) {
  144. foreach (self::$access_flags as $key => $val) {
  145. self::$access_flags[$key] = 1;
  146. }
  147. return;
  148. }
  149.  
  150.  
  151. // Get the values of the operator flags
  152. $flag_names = implode(', ', array_keys(self::$access_flags));
  153. $q = "SELECT {$flag_names}
  154. FROM ~operators_cat_list AS cat
  155. INNER JOIN ~operators_cat_join AS joiner ON cat.id = joiner.cat_id
  156. WHERE joiner.operator_id = ?";
  157. $res = Pdb::q($q, [$_SESSION['admin']['login_id']], 'pdo');
  158.  
  159. // Grab the highest value for each flag
  160. foreach ($res as $op) {
  161. foreach (self::$access_flags as $name => $value) {
  162. self::$access_flags[$name] = max($op[$name], $value);
  163. }
  164. }
  165. $res->closeCursor();
  166. }
  167.  
  168.  
  169. /**
  170.   * Returns true or false depending on if an access flag is available for this user or not
  171.   **/
  172. public static function canAccess($access_flag)
  173. {
  174. self::loadAccessFlags();
  175. return (boolean) self::$access_flags[$access_flag];
  176. }
  177.  
  178.  
  179. /**
  180.   * Gets all permitted subsite IDs for this user.
  181.   * Subsite permissions are specified per operator-group.
  182.   * This function will return an array of subsite IDs.
  183.   * Will get data for the currently logged in user
  184.   *
  185.   * @param array $operator_cats An array of categories the operator is permitted to work with
  186.   **/
  187. public static function loadSubsitesPermitted()
  188. {
  189. Session::instance();
  190.  
  191. try {
  192. $subsites = Pdb::lookup('subsites');
  193. } catch (QueryException $ex) {
  194. // Assume DB has no tables
  195. $subsites = [];
  196. }
  197.  
  198. self::$subsites_permitted = array();
  199.  
  200. // Not logged in - return all false
  201. if (empty($_SESSION['admin']['login_id'])) return self::$subsites_permitted;
  202.  
  203. // Super users can access everything
  204. if (isset($_SESSION['admin']['super'])) {
  205. // Pretend there's a subsite if DB has no tables
  206. if (count($subsites) == 0) $subsites = [1 => 'Site with no DB'];
  207.  
  208. foreach ($subsites as $key => $val) {
  209. self::$subsites_permitted[$key] = true;
  210. }
  211. return self::$subsites_permitted;
  212. }
  213.  
  214.  
  215. $cats = array(0);
  216. $cats = array_merge($cats, AdminAuth::getOperatorCategories());
  217.  
  218. // If set to default (ie all) on any category then grant all subsites
  219. $params = [];
  220. $where = Pdb::buildClause([['id', 'IN', $cats]], $params);
  221. $admin_id = AdminAuth::getId();
  222. $q = "SELECT access_all_subsites
  223. FROM ~operators_cat_list
  224. WHERE {$where}";
  225. $res = Pdb::q($q, $params, 'col');
  226.  
  227. foreach ($res as $access_all_subsites) {
  228. if ($access_all_subsites) {
  229. foreach ($subsites as $key => $val) {
  230. self::$subsites_permitted[$key] = true;
  231. }
  232. return self::$subsites_permitted;
  233. }
  234. }
  235.  
  236. // Get the values of the subsite settings
  237. $params = [];
  238. $where = Pdb::buildClause([['operatorcategory_id', 'IN', $cats]], $params);
  239. $q = "SELECT subsite_id
  240. FROM ~operatorcategory_subsites
  241. WHERE {$where}";
  242. $subs_ops = Pdb::q($q, $params, 'col');
  243.  
  244. // Grab the current settings and load into array
  245. foreach ($subs_ops as $subsite_id) {
  246. self::$subsites_permitted[$subsite_id] = true;
  247. }
  248.  
  249. return self::$subsites_permitted;
  250. }
  251.  
  252.  
  253. /**
  254.   * Returns true or false depending on if a subsite is available for this user or not
  255.   **/
  256. public static function canAccessSubsite($subsite_id)
  257. {
  258. self::loadSubsitesPermitted();
  259. return @self::$subsites_permitted[$subsite_id];
  260. }
  261.  
  262.  
  263. /**
  264.   * Get a list of all operators with a specific access flag
  265.   * @param string $access_flag One of the fields in the operators_cat_list table, e.g. 'access_homepage'
  266.   * @return array Matching rows from the operators table
  267.   * @throws QueryException
  268.   */
  269. public static function getOperatorsWithAccess($access_flag)
  270. {
  271. Pdb::validateIdentifier($access_flag);
  272.  
  273. $q = "SELECT DISTINCT op.*
  274. FROM ~operators AS op
  275. INNER JOIN ~operators_cat_join AS joiner ON joiner.operator_id = op.id
  276. INNER JOIN ~operators_cat_list AS cat ON joiner.cat_id = cat.id
  277. WHERE cat.{$access_flag} = 1
  278. GROUP BY op.id
  279. ORDER BY op.id";
  280. return Pdb::q($q, [], 'arr');
  281. }
  282.  
  283.  
  284. /**
  285.   * Returns true or false depending on if an access flag is available for this user or not
  286.   *
  287.   * @param string $controller The name of the controller to check an access flag for
  288.   * @param string $access_flag The flag to check (e.g. 'main', 'add', 'edit', etc)
  289.   **/
  290. public static function controllerAccess($controller, $access_flag)
  291. {
  292. Session::instance();
  293.  
  294. if (! $_SESSION['admin']['login_id']) return false;
  295. if ($_SESSION['admin']['super']) return true;
  296.  
  297.  
  298. // Home page - use the dedicated checkbox instead.
  299. if ($controller == 'home_page') {
  300. return AdminPerms::canAccess('access_homepage');
  301. }
  302.  
  303. // Operators have special logic at the controller level for edits
  304. // to allow operators to change their own passwords
  305. if ($controller == 'operator') return true;
  306.  
  307. // These tools are controlled by the 'operators' flag as well
  308. if ($controller == 'tools' or $controller == 'action_log' or $controller == 'subsites') {
  309. return AdminPerms::canAccess('access_operators');
  310. }
  311.  
  312.  
  313.  
  314. // Grab a list of categories this user is in
  315. $q = "SELECT cats.id, cats.default_allow
  316. FROM ~operators_cat_list AS cats
  317. INNER JOIN ~operators_cat_join AS joiner ON cats.id = joiner.cat_id
  318. WHERE joiner.operator_id = ?";
  319. $res = Pdb::q($q, [AdminAuth::getId()], 'arr');
  320.  
  321. // No categories? That's an error.
  322. if (count($res) == 0) {
  323. throw new Exception('The currently logged-in operator isn\'t in any categories');
  324. }
  325.  
  326. // Grab the ids, and also find the highest value for the default field
  327. $cat_ids = array();
  328. $default_allow = 0;
  329. foreach ($res as $row) {
  330. $cat_ids[] = $row['id'];
  331. $default_allow = max($default_allow, $row['default_allow']);
  332. }
  333.  
  334. // Prep for the query
  335. $access_flag = trim(strtolower($access_flag));
  336.  
  337. // The main query just finds the highest value access flag and returns it
  338. // If there isn't anything, use the default
  339. $params = [];
  340. $conditions = [['operatorcategory_id', 'IN', $cat_ids], 'controller' => $controller];
  341. $where = Pdb::buildClause($conditions, $params);
  342. $q = "SELECT MAX(access_{$access_flag}) AS flag
  343. FROM ~operatorcategory_permissions
  344. WHERE {$where}
  345. LIMIT 1";
  346. try {
  347. $flag = Pdb::q($q, $params, 'val');
  348. if ($flag === null) return ($default_allow == 1);
  349. return $flag;
  350. } catch (RowMissingException $ex) {
  351. return ($default_allow == 1);
  352. }
  353. }
  354.  
  355.  
  356. /**
  357.   * For a given list of controllers, return a list of controllers which the current user can actually access.
  358.   * This controls which tabs show in the navigation.
  359.   * It checks for the 'contents' access flag.
  360.   **/
  361. public static function controllerAccessMulti(array $controllers)
  362. {
  363. Session::instance();
  364.  
  365. if (! $_SESSION['admin']['login_id']) return array();
  366. if ($_SESSION['admin']['super']) return $controllers;
  367.  
  368.  
  369. // Grab a list of categories this user is in
  370. $q = "SELECT cats.id, cats.default_allow
  371. FROM ~operators_cat_list AS cats
  372. INNER JOIN ~operators_cat_join AS joiner ON joiner.cat_id = cats.id
  373. WHERE joiner.operator_id = ?";
  374. $res = Pdb::q($q, [AdminAuth::getId()], 'arr');
  375.  
  376. // No categories? That's an error.
  377. if (count($res) == 0) {
  378. throw new Exception('The currently logged-in operator isn\'t in any categories');
  379. }
  380.  
  381. // Grab the ids, and also find the highest value for the default field
  382. $cat_ids = array();
  383. $default_allow = 0;
  384. foreach ($res as $row) {
  385. $cat_ids[] = $row['id'];
  386. $default_allow = max($default_allow, $row['default_allow']);
  387. }
  388.  
  389. // The main query just finds the highest value access flag and returns it
  390. $params = [];
  391. $conditions = [['operatorcategory_id', 'IN', $cat_ids], ['controller', 'IN', $controllers]];
  392. $where = Pdb::buildClause($conditions, $params);
  393. $q = "SELECT controller, MAX(access_contents) AS flag
  394. FROM ~operatorcategory_permissions
  395. WHERE {$where}
  396. GROUP BY controller";
  397. $res = Pdb::q($q, $params, 'arr');
  398.  
  399. // Build the response array
  400. $out = array();
  401. foreach ($res as $row) {
  402. if ($row['flag']) $out[] = $row['controller'];
  403. unset($controllers[array_search($row['controller'], $controllers)]);
  404. }
  405.  
  406. // If the default is "allow", add any controllers which didn't return a result
  407. if ($default_allow == 1) {
  408. foreach ($controllers as $c) {
  409. $out[] = $c;
  410. }
  411. }
  412.  
  413. // These are managed elsewhere, so we always allow access here
  414. $out[] = 'home_page';
  415. $out[] = 'operator';
  416. $out[] = 'tools';
  417. $out[] = 'action_log';
  418. $out[] = 'subsites';
  419.  
  420. return $out;
  421. }
  422.  
  423.  
  424. /**
  425.   * Get the name of the first controller the user has access to.
  426.   * For most users, this will be 'pages', but it might be something else.
  427.   **/
  428. public static function getFirstAccessable()
  429. {
  430. $controller_names = array_keys(Register::getAdminControllers());
  431. array_unshift($controller_names, 'user');
  432. array_unshift($controller_names, 'file');
  433. array_unshift($controller_names, 'page');
  434.  
  435. $allowed_ctlrs = AdminPerms::controllerAccessMulti($controller_names);
  436.  
  437. return $allowed_ctlrs[0];
  438. }
  439.  
  440.  
  441. /**
  442.   * Remove controllers and tiles for which the user doesn't have access
  443.   *
  444.   * @param array Tile definitions, loaded from {@see Register::getAdminTiles}
  445.   * @return array Tile definitions sans unpermitted controllers
  446.   */
  447. public static function filterAdminTiles(array $tiles)
  448. {
  449. foreach ($tiles as $tile_index => &$tile) {
  450. foreach ($tile['controllers'] as $ctlr => $name) {
  451. if (!AdminPerms::controllerAccess($ctlr, 'contents')) {
  452. unset($tile['controllers'][$ctlr]);
  453. }
  454. }
  455. if (count($tile['controllers']) === 0) {
  456. unset($tiles[$tile_index]);
  457. }
  458. }
  459.  
  460. return $tiles;
  461. }
  462.  
  463.  
  464. /**
  465.   * Return the list of operator categories which the currently-logged in operator can manage
  466.   * @return array Categories as id => name
  467.   */
  468. public static function getManageOperatorCategories()
  469. {
  470. if (! AdminAuth::isLoggedIn()) {
  471. return array();
  472. }
  473.  
  474. $cats_table = Category::tableMain2cat('operators');
  475.  
  476. if (AdminPerms::canAccess('access_operators')) {
  477. try {
  478. return Pdb::lookup($cats_table);
  479. } catch (QueryException $ex) {
  480. // Assume DB has no tables; can't manage non-existent categories
  481. return [];
  482. }
  483. }
  484.  
  485. $joiner_table = Category::tableMain2joiner('operators');
  486.  
  487. // Get the list
  488. $q = "SELECT cat.id, cat.name
  489. FROM ~{$cats_table} AS cat
  490. INNER JOIN ~operatorcategory_manage_categories AS manage ON manage.manage_category_id = cat.id
  491. INNER JOIN ~{$joiner_table} AS joiner ON manage.operatorcategory_id = joiner.cat_id
  492. WHERE joiner.operator_id = ?
  493. ORDER BY cat.name";
  494. return Pdb::q($q, [AdminAuth::getId()], 'map');
  495. }
  496.  
  497.  
  498. /**
  499.   * Can the currently-logged in operator edit the operator in question?
  500.   **/
  501. public static function canEditOperator($operator_id)
  502. {
  503. if (! AdminAuth::isLoggedIn()) {
  504. return false;
  505. }
  506.  
  507. if (AdminPerms::canAccess('access_operators')) {
  508. return true;
  509. }
  510.  
  511. // Get list of categories the operator is in
  512. $record_cats = Category::categoryList('operators', $operator_id);
  513. if (! $record_cats) return false;
  514.  
  515. // Get list of categories the logged in operator is allowed to manage
  516. $manage_cats = self::getManageOperatorCategories();
  517. if (! $manage_cats) return false;
  518.  
  519. // Check for a match between them
  520. foreach ($manage_cats as $id => $name) {
  521. if (in_array($id, $record_cats)) return true;
  522. }
  523.  
  524. return false;
  525. }
  526.  
  527. }
  528.  
  529.  
  530.