<?php
/*
* Copyright (C) 2017 Karmabunny Pty Ltd.
*
* This file is a part of SproutCMS.
*
* SproutCMS is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either
* version 2 of the License, or (at your option) any later version.
*
* For more information, visit <http://getsproutcms.com>.
*/
namespace Sprout\Helpers;
use Exception;
use karmabunny\pdb\Exceptions\QueryException;
use karmabunny\pdb\Exceptions\RowMissingException;
/**
* Provides user permisison functions for the admin
**/
class AdminPerms
{
public static $access_flags;
public static $subsites_permitted;
/**
* Checks whether the currently logged in operator can access the specified item.
* This method should be used for tree-based tables, like the 'pages' table,
* which may inherit their permissions from the parent record.
*
* @param string $table The table name of the item to check
* @param int $id The id of the record to check
* @returns boolean True if the operator has access, false otherwise
**/
public static function checkPermissionsTree($table, $id)
{
Session::instance();
// Not logged in - nothing
if (! $_SESSION['admin']['login_id']) return false;
// Super users can access everything
if ($_SESSION['admin']['super']) return true;
// Standard users - find out which groups can access this page
$access_groups = self::getAccessableGroups($table, $id);
if (count($access_groups) == 0) return false;
// Check if the user is in any of those groups
$params = [];
$conditions = ['operator_id' => $_SESSION['admin']['login_id'], ['cat_id', 'IN', $access_groups]];
$q = "SELECT 1
FROM ~operators_cat_join
WHERE " . Pdb::buildClause($conditions, $params);
try {
Pdb::query($q, $params, 'row');
return true;
} catch (QueryException $ex) {
return false;
}
}
/**
* Returns a list of groups (operator categories) which can access a specific page.
*
* Each page can either specify permissions, or inherit permissions from it's parent page.
*
* @example
* $cat_ids = AdminPerms::getAccessableGroups('pages', 10);
*
* @param string $table The table to get permissions for
* @param int $id The ID of the record to get permissions for
* @return array Integers, one per category of operator which has access
* @return false No operators have access
**/
public static function getAccessableGroups($table, $id)
{
// The top level node always allows all categories
if ($id == 0) {
}
// Fetch the permissions for this page, along with some page details
$single = Inflector::singular($table);
$q = "SELECT tbl.parent_id, tbl.admin_perm_type, perms.category_id
FROM ~{$table} AS tbl
LEFT JOIN ~{$single}_admin_permissions AS perms ON perms.item_id = tbl.id
WHERE tbl.id = ?";
$res = Pdb::q($q, [$id], 'arr');
// No records found, so no permissions
return false;
}
switch ($res[0]['admin_perm_type']) {
case Constants::PERM_INHERIT:
// Inherit from parent record
return self::getAccessableGroups($table, $res[0]['parent_id']);
break;
case Constants::PERM_SPECIFIC:
// Grab the category IDs from the resultset fetched earlier
$items = [];
foreach ($res as $row) {
if ($row['category_id']) {
$items[] = $row['category_id'];
}
}
return $items;
break;
}
}
/**
* Gets all 'access' flags for this user.
* Access flags are specified per operator-group.
* This function will return the values for all of the defined access flags,
* returning the greatest-permission flag available for the user.
* Will get data for the currently logged in user
**/
public static function loadAccessFlags()
{
Session::instance();
self::$access_flags = array( 'access_operators' => 0,
'access_noapproval' => 0,
'access_reportemail' => 0,
'access_homepage' => 0,
);
// Not logged in - nothing
if (empty($_SESSION['admin']['login_id'])) return;
// Super users can access everything
if ($_SESSION['admin']['super']) {
foreach (self::$access_flags as $key => $val) {
self::$access_flags[$key] = 1;
}
return;
}
// Get the values of the operator flags
$q = "SELECT {$flag_names}
FROM ~operators_cat_list AS cat
INNER JOIN ~operators_cat_join AS joiner ON cat.id = joiner.cat_id
WHERE joiner.operator_id = ?";
$res = Pdb::q($q, [$_SESSION['admin']['login_id']], 'pdo');
// Grab the highest value for each flag
foreach ($res as $op) {
foreach (self::$access_flags as $name => $value) {
self::$access_flags[$name] = max($op[$name], $value); }
}
$res->closeCursor();
}
/**
* Returns true or false depending on if an access flag is available for this user or not
**/
public static function canAccess($access_flag)
{
self::loadAccessFlags();
return (boolean) self::$access_flags[$access_flag];
}
/**
* Gets all permitted subsite IDs for this user.
* Subsite permissions are specified per operator-group.
* This function will return an array of subsite IDs.
* Will get data for the currently logged in user
*
* @param array $operator_cats An array of categories the operator is permitted to work with
**/
public static function loadSubsitesPermitted()
{
Session::instance();
try {
$subsites = Pdb::lookup('subsites');
} catch (QueryException $ex) {
// Assume DB has no tables
$subsites = [];
}
self::$subsites_permitted = array();
// Not logged in - return all false
if (empty($_SESSION['admin']['login_id'])) return self::$subsites_permitted;
// Super users can access everything
if (isset($_SESSION['admin']['super'])) { // Pretend there's a subsite if DB has no tables
if (count($subsites) == 0) $subsites = [1 => 'Site with no DB'];
foreach ($subsites as $key => $val) {
self::$subsites_permitted[$key] = true;
}
return self::$subsites_permitted;
}
$cats = array_merge($cats, AdminAuth
::getOperatorCategories());
// If set to default (ie all) on any category then grant all subsites
$params = [];
$where = Pdb::buildClause([['id', 'IN', $cats]], $params);
$admin_id = AdminAuth::getId();
$q = "SELECT access_all_subsites
FROM ~operators_cat_list
WHERE {$where}";
$res = Pdb::q($q, $params, 'col');
foreach ($res as $access_all_subsites) {
if ($access_all_subsites) {
foreach ($subsites as $key => $val) {
self::$subsites_permitted[$key] = true;
}
return self::$subsites_permitted;
}
}
// Get the values of the subsite settings
$params = [];
$where = Pdb::buildClause([['operatorcategory_id', 'IN', $cats]], $params);
$q = "SELECT subsite_id
FROM ~operatorcategory_subsites
WHERE {$where}";
$subs_ops = Pdb::q($q, $params, 'col');
// Grab the current settings and load into array
foreach ($subs_ops as $subsite_id) {
self::$subsites_permitted[$subsite_id] = true;
}
return self::$subsites_permitted;
}
/**
* Returns true or false depending on if a subsite is available for this user or not
**/
public static function canAccessSubsite($subsite_id)
{
self::loadSubsitesPermitted();
return @self::$subsites_permitted[$subsite_id];
}
/**
* Get a list of all operators with a specific access flag
* @param string $access_flag One of the fields in the operators_cat_list table, e.g. 'access_homepage'
* @return array Matching rows from the operators table
* @throws QueryException
*/
public static function getOperatorsWithAccess($access_flag)
{
Pdb::validateIdentifier($access_flag);
$q = "SELECT DISTINCT op.*
FROM ~operators AS op
INNER JOIN ~operators_cat_join AS joiner ON joiner.operator_id = op.id
INNER JOIN ~operators_cat_list AS cat ON joiner.cat_id = cat.id
WHERE cat.{$access_flag} = 1
GROUP BY op.id
ORDER BY op.id";
return Pdb::q($q, [], 'arr');
}
/**
* Returns true or false depending on if an access flag is available for this user or not
*
* @param string $controller The name of the controller to check an access flag for
* @param string $access_flag The flag to check (e.g. 'main', 'add', 'edit', etc)
**/
public static function controllerAccess($controller, $access_flag)
{
Session::instance();
if (! $_SESSION['admin']['login_id']) return false;
if ($_SESSION['admin']['super']) return true;
// Home page - use the dedicated checkbox instead.
if ($controller == 'home_page') {
return AdminPerms::canAccess('access_homepage');
}
// Operators have special logic at the controller level for edits
// to allow operators to change their own passwords
if ($controller == 'operator') return true;
// These tools are controlled by the 'operators' flag as well
if ($controller == 'tools' or $controller == 'action_log' or $controller == 'subsites') {
return AdminPerms::canAccess('access_operators');
}
// Grab a list of categories this user is in
$q = "SELECT cats.id, cats.default_allow
FROM ~operators_cat_list AS cats
INNER JOIN ~operators_cat_join AS joiner ON cats.id = joiner.cat_id
WHERE joiner.operator_id = ?";
$res = Pdb::q($q, [AdminAuth::getId()], 'arr');
// No categories? That's an error.
throw new Exception('The currently logged-in operator isn\'t in any categories');
}
// Grab the ids, and also find the highest value for the default field
$default_allow = 0;
foreach ($res as $row) {
$cat_ids[] = $row['id'];
$default_allow = max($default_allow, $row['default_allow']); }
// Prep for the query
// The main query just finds the highest value access flag and returns it
// If there isn't anything, use the default
$params = [];
$conditions = [['operatorcategory_id', 'IN', $cat_ids], 'controller' => $controller];
$where = Pdb::buildClause($conditions, $params);
$q = "SELECT MAX(access_{$access_flag}) AS flag
FROM ~operatorcategory_permissions
WHERE {$where}
LIMIT 1";
try {
$flag = Pdb::q($q, $params, 'val');
if ($flag === null) return ($default_allow == 1);
return $flag;
} catch (RowMissingException $ex) {
return ($default_allow == 1);
}
}
/**
* For a given list of controllers, return a list of controllers which the current user can actually access.
* This controls which tabs show in the navigation.
* It checks for the 'contents' access flag.
**/
public static
function controllerAccessMulti
(array $controllers) {
Session::instance();
if (! $_SESSION['admin']['login_id']) return array(); if ($_SESSION['admin']['super']) return $controllers;
// Grab a list of categories this user is in
$q = "SELECT cats.id, cats.default_allow
FROM ~operators_cat_list AS cats
INNER JOIN ~operators_cat_join AS joiner ON joiner.cat_id = cats.id
WHERE joiner.operator_id = ?";
$res = Pdb::q($q, [AdminAuth::getId()], 'arr');
// No categories? That's an error.
throw new Exception('The currently logged-in operator isn\'t in any categories');
}
// Grab the ids, and also find the highest value for the default field
$default_allow = 0;
foreach ($res as $row) {
$cat_ids[] = $row['id'];
$default_allow = max($default_allow, $row['default_allow']); }
// The main query just finds the highest value access flag and returns it
$params = [];
$conditions = [['operatorcategory_id', 'IN', $cat_ids], ['controller', 'IN', $controllers]];
$where = Pdb::buildClause($conditions, $params);
$q = "SELECT controller, MAX(access_contents) AS flag
FROM ~operatorcategory_permissions
WHERE {$where}
GROUP BY controller";
$res = Pdb::q($q, $params, 'arr');
// Build the response array
foreach ($res as $row) {
if ($row['flag']) $out[] = $row['controller'];
}
// If the default is "allow", add any controllers which didn't return a result
if ($default_allow == 1) {
foreach ($controllers as $c) {
$out[] = $c;
}
}
// These are managed elsewhere, so we always allow access here
$out[] = 'home_page';
$out[] = 'operator';
$out[] = 'tools';
$out[] = 'action_log';
$out[] = 'subsites';
return $out;
}
/**
* Get the name of the first controller the user has access to.
* For most users, this will be 'pages', but it might be something else.
**/
public static function getFirstAccessable()
{
$controller_names = array_keys(Register
::getAdminControllers());
$allowed_ctlrs = AdminPerms::controllerAccessMulti($controller_names);
return $allowed_ctlrs[0];
}
/**
* Remove controllers and tiles for which the user doesn't have access
*
* @param array Tile definitions, loaded from {@see Register::getAdminTiles}
* @return array Tile definitions sans unpermitted controllers
*/
public static
function filterAdminTiles
(array $tiles) {
foreach ($tiles as $tile_index => &$tile) {
foreach ($tile['controllers'] as $ctlr => $name) {
if (!AdminPerms::controllerAccess($ctlr, 'contents')) {
unset($tile['controllers'][$ctlr]); }
}
if (count($tile['controllers']) === 0) { unset($tiles[$tile_index]); }
}
return $tiles;
}
/**
* Return the list of operator categories which the currently-logged in operator can manage
* @return array Categories as id => name
*/
public static function getManageOperatorCategories()
{
if (! AdminAuth::isLoggedIn()) {
}
$cats_table = Category::tableMain2cat('operators');
if (AdminPerms::canAccess('access_operators')) {
try {
return Pdb::lookup($cats_table);
} catch (QueryException $ex) {
// Assume DB has no tables; can't manage non-existent categories
return [];
}
}
$joiner_table = Category::tableMain2joiner('operators');
// Get the list
$q = "SELECT cat.id, cat.name
FROM ~{$cats_table} AS cat
INNER JOIN ~operatorcategory_manage_categories AS manage ON manage.manage_category_id = cat.id
INNER JOIN ~{$joiner_table} AS joiner ON manage.operatorcategory_id = joiner.cat_id
WHERE joiner.operator_id = ?
ORDER BY cat.name";
return Pdb::q($q, [AdminAuth::getId()], 'map');
}
/**
* Can the currently-logged in operator edit the operator in question?
**/
public static function canEditOperator($operator_id)
{
if (! AdminAuth::isLoggedIn()) {
return false;
}
if (AdminPerms::canAccess('access_operators')) {
return true;
}
// Get list of categories the operator is in
$record_cats = Category::categoryList('operators', $operator_id);
if (! $record_cats) return false;
// Get list of categories the logged in operator is allowed to manage
$manage_cats = self::getManageOperatorCategories();
if (! $manage_cats) return false;
// Check for a match between them
foreach ($manage_cats as $id => $name) {
if (in_array($id, $record_cats)) return true; }
return false;
}
}