core/model/modx/modaccessibleobject.class.php
<?php
/*
* This file is part of MODX Revolution.
*
* Copyright (c) MODX, LLC. All Rights Reserved.
*
* For complete copyright and license information, see the COPYRIGHT and LICENSE
* files found in the top-level directory of this distribution.
*/
/**
* Defines an interface to provide configurable access policies for principals.
*
* @property modX|xPDO $xpdo
* @package modx
*/
class modAccessibleObject extends xPDOObject {
/**
* A local cache of access policies for the instance.
* @var array
*/
protected $_policies = array();
/**
* Custom instance from row loader that respects policy checking
*
* @param xPDO|modX $xpdo A reference to the xPDO/modX object.
* @param string $className The name of the class by which to grab the instance from
* @param mixed $criteria A criteria to use when grabbing this instance
* @param int $row The row to select
* @return modAccessibleObject|null An instance of the object
*/
public static function _loadInstance(& $xpdo, $className, $criteria, $row) {
/** @var modAccessibleObject $instance */
$instance = xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row);
if ($instance instanceof modAccessibleObject && !$instance->checkPolicy('load')) {
if ($xpdo instanceof modX) {
$userid = $xpdo->getLoginUserID();
if (!$userid) $userid = '0';
$xpdo->log(xPDO::LOG_LEVEL_INFO, "Principal {$userid} does not have permission to load object of class {$instance->_class} with primary key: " . (is_object($instance) && method_exists($instance,'getPrimaryKey') ? print_r($instance->getPrimaryKey(), true) : ''));
}
$instance = null;
}
return $instance;
}
/**
* Custom instance loader for collections that respects policy checking.
*
* {@inheritdoc}
*/
public static function _loadCollectionInstance(xPDO & $xpdo, array & $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag=true) {
$loaded = false;
if ($obj= modAccessibleObject :: _loadInstance($xpdo, $className, $criteria, $row)) {
if (($cacheKey= $obj->getPrimaryKey()) && !$obj->isLazy()) {
if (is_array($cacheKey)) {
$pkval= implode('-', $cacheKey);
} else {
$pkval= $cacheKey;
}
if ($xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1) == 2 && $xpdo->_cacheEnabled && $cacheFlag) {
if (!$fromCache) {
$pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag);
$xpdo->toCache($pkCriteria, $obj, $cacheFlag);
} else {
$obj->_cacheFlag= true;
}
}
$objCollection[$pkval]= $obj;
$loaded = true;
} else {
$objCollection[]= $obj;
$loaded = true;
}
}
return $loaded;
}
/**
* Custom instance loader that forces access policy checking.
*
* {@inheritdoc}
*/
public static function load(xPDO & $xpdo, $className, $criteria, $cacheFlag= true) {
$instance= null;
$fromCache= false;
if ($className= $xpdo->loadClass($className)) {
if (!is_object($criteria)) {
$criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
}
if (is_object($criteria)) {
$row= null;
if ($xpdo->_cacheEnabled && $criteria->cacheFlag && $cacheFlag) {
$row= $xpdo->fromCache($criteria, $className);
}
if ($row === null || !is_array($row)) {
if ($rows= xPDOObject :: _loadRows($xpdo, $className, $criteria)) {
$row= $rows->fetch(PDO::FETCH_ASSOC);
$rows->closeCursor();
}
} else {
$fromCache= true;
}
if (!is_array($row)) {
if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Fetched empty result set from statement: " . print_r($criteria->sql, true) . " with bindings: " . print_r($criteria->bindings, true));
} else {
$instance= modAccessibleObject :: _loadInstance($xpdo, $className, $criteria, $row);
if (is_object($instance)) {
if (!$fromCache && $cacheFlag && $xpdo->_cacheEnabled) {
$xpdo->toCache($criteria, $instance, $cacheFlag);
if ($xpdo->getOption(xPDO::OPT_CACHE_DB_OBJECTS_BY_PK) && ($cacheKey= $instance->getPrimaryKey()) && !$instance->isLazy()) {
$pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag);
$xpdo->toCache($pkCriteria, $instance, $cacheFlag);
}
}
if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Loaded object instance: " . print_r($instance->toArray('', true), true));
}
}
} else {
$xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No valid statement could be found in or generated from the given criteria.');
}
} else {
$xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Invalid class specified: ' . $className);
}
return $instance;
}
/**
* Custom collection loader that forces access policy checking.
*
* {@inheritdoc}
*/
public static function loadCollection(xPDO & $xpdo, $className, $criteria= null, $cacheFlag= true) {
$objCollection= array ();
$fromCache = false;
if (!$className= $xpdo->loadClass($className)) return $objCollection;
$rows= false;
$fromCache= false;
$collectionCaching = (integer) $xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1);
if (!is_object($criteria)) {
$criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
}
if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag) {
$rows= $xpdo->fromCache($criteria);
$fromCache = (is_array($rows) && !empty($rows));
}
if (!$fromCache && is_object($criteria)) {
$rows= xPDOObject :: _loadRows($xpdo, $className, $criteria);
}
if (is_array ($rows)) {
foreach ($rows as $row) {
modAccessibleObject :: _loadCollectionInstance($xpdo, $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag);
}
} elseif (is_object($rows)) {
$cacheRows = array();
while ($row = $rows->fetch(PDO::FETCH_ASSOC)) {
modAccessibleObject :: _loadCollectionInstance($xpdo, $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag);
if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag && !$fromCache) $cacheRows[] = $row;
}
if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag && !$fromCache) $rows =& $cacheRows;
}
if (!$fromCache && $xpdo->_cacheEnabled && $collectionCaching > 0 && $cacheFlag && !empty($rows)) {
$xpdo->toCache($criteria, $rows, $cacheFlag);
}
return $objCollection;
}
/**
* Custom save that respects access policies.
*
* {@inheritdoc}
*/
public function save($cacheFlag = null) {
$saved = false;
if (!$this->checkPolicy('save')) {
$this->xpdo->error->failure($this->xpdo->lexicon('permission_denied'));
}
$saved = parent :: save($cacheFlag);
return $saved;
}
/**
* Custom remove that respects access policies.
*
* {@inheritdoc}
*/
public function remove(array $ancestors= array ()) {
$removed = false;
if (!$this->checkPolicy('remove')) {
$this->xpdo->error->failure($this->xpdo->lexicon('permission_denied'));
}
$removed = parent :: remove($ancestors);
return $removed;
}
/**
* Determine if the current/specified user attributes satisfy the object policy.
*
* @param array|string $criteria An associative array providing a key and value to
* search for within the matched policy attributes between policy and
* principal, or the name of a permission to check.
* @param string|array $targets A target modAccess class name or an array of
* class names to limit the check. In most cases, this does not need to be
* set; derivatives should typically determine what targets to include in
* the findPolicy() implementation.
* @param modUser $user
* @return boolean Returns true if the policy is satisfied or no policy
* exists.
*/
public function checkPolicy($criteria, $targets = null, modUser $user = null) {
if(!$user){
$user = & $this->xpdo->user;
}
if ($criteria && $this->xpdo instanceof modX && $this->xpdo->getSessionState() == modX::SESSION_STATE_INITIALIZED) {
if ($user->get('sudo')) return true;
if (!is_array($criteria) && is_scalar($criteria)) {
$criteria = array("{$criteria}" => true);
}
$policy = $this->findPolicy();
if (!empty($policy)) {
$principal = $user->getAttributes($targets);
if (!empty($principal)) {
foreach ($policy as $policyAccess => $access) {
foreach ($access as $targetId => $targetPolicy) {
foreach ($targetPolicy as $policyIndex => $applicablePolicy) {
if ($this->xpdo->getDebug() === true)
$this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, 'target pk='. $this->getPrimaryKey() .'; evaluating policy: ' . print_r($applicablePolicy, 1) . ' against principal for user id=' . $user->id .': ' . print_r($principal[$policyAccess], 1));
$principalPolicyData = array();
$principalAuthority = 9999;
if (isset($principal[$policyAccess][$targetId]) && is_array($principal[$policyAccess][$targetId])) {
foreach ($principal[$policyAccess][$targetId] as $acl) {
$principalAuthority = intval($acl['authority']);
$principalPolicyData = $acl['policy'];
$principalId = $acl['principal'];
if ($applicablePolicy['principal'] == $principalId) {
if ($principalAuthority <= $applicablePolicy['authority']) {
if (!$applicablePolicy['policy']) {
return true;
}
if (empty($principalPolicyData)) $principalPolicyData = array();
$matches = array_intersect_assoc($principalPolicyData, $applicablePolicy['policy']);
if ($matches) {
if ($this->xpdo->getDebug() === true)
$this->xpdo->log(modX::LOG_LEVEL_DEBUG, 'Evaluating policy matches: ' . print_r($matches, 1));
$matched = array_diff_assoc($criteria, $matches);
if (empty($matched)) {
return true;
}
}
}
}
}
}
}
}
}
}
return false;
}
}
return true;
}
/**
* Find access policies applicable to this object in a specific context.
*
* @access protected
* @param string $context A key identifying a specific context to use when
* searching for the applicable policies. If not provided, the current
* context is used.
* @return array An array of access policies for this object; an empty
* array is returned if no policies are assigned to the object.
*/
public function findPolicy($context = '') {
return array();
}
/**
* Return the currently loaded array of policies.
*
* @return array
*/
public function getPolicies() {
return $this->_policies;
}
/**
* Set the current object's policies.
*
* @param array $policies
* @return void
*/
public function setPolicies(array $policies = array()) {
$this->_policies = $policies;
}
}