modaccessibleobject.class.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. <?php
  2. /*
  3. * This file is part of MODX Revolution.
  4. *
  5. * Copyright (c) MODX, LLC. All Rights Reserved.
  6. *
  7. * For complete copyright and license information, see the COPYRIGHT and LICENSE
  8. * files found in the top-level directory of this distribution.
  9. */
  10. /**
  11. * Defines an interface to provide configurable access policies for principals.
  12. *
  13. * @property modX|xPDO $xpdo
  14. * @package modx
  15. */
  16. class modAccessibleObject extends xPDOObject {
  17. /**
  18. * A local cache of access policies for the instance.
  19. * @var array
  20. */
  21. protected $_policies = array();
  22. /**
  23. * Custom instance from row loader that respects policy checking
  24. *
  25. * @param xPDO|modX $xpdo A reference to the xPDO/modX object.
  26. * @param string $className The name of the class by which to grab the instance from
  27. * @param mixed $criteria A criteria to use when grabbing this instance
  28. * @param int $row The row to select
  29. * @return modAccessibleObject|null An instance of the object
  30. */
  31. public static function _loadInstance(& $xpdo, $className, $criteria, $row) {
  32. /** @var modAccessibleObject $instance */
  33. $instance = xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row);
  34. if ($instance instanceof modAccessibleObject && !$instance->checkPolicy('load')) {
  35. if ($xpdo instanceof modX) {
  36. $userid = $xpdo->getLoginUserID();
  37. if (!$userid) $userid = '0';
  38. $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) : ''));
  39. }
  40. $instance = null;
  41. }
  42. return $instance;
  43. }
  44. /**
  45. * Custom instance loader for collections that respects policy checking.
  46. *
  47. * {@inheritdoc}
  48. */
  49. public static function _loadCollectionInstance(xPDO & $xpdo, array & $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag=true) {
  50. $loaded = false;
  51. if ($obj= modAccessibleObject :: _loadInstance($xpdo, $className, $criteria, $row)) {
  52. if (($cacheKey= $obj->getPrimaryKey()) && !$obj->isLazy()) {
  53. if (is_array($cacheKey)) {
  54. $pkval= implode('-', $cacheKey);
  55. } else {
  56. $pkval= $cacheKey;
  57. }
  58. if ($xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1) == 2 && $xpdo->_cacheEnabled && $cacheFlag) {
  59. if (!$fromCache) {
  60. $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag);
  61. $xpdo->toCache($pkCriteria, $obj, $cacheFlag);
  62. } else {
  63. $obj->_cacheFlag= true;
  64. }
  65. }
  66. $objCollection[$pkval]= $obj;
  67. $loaded = true;
  68. } else {
  69. $objCollection[]= $obj;
  70. $loaded = true;
  71. }
  72. }
  73. return $loaded;
  74. }
  75. /**
  76. * Custom instance loader that forces access policy checking.
  77. *
  78. * {@inheritdoc}
  79. */
  80. public static function load(xPDO & $xpdo, $className, $criteria, $cacheFlag= true) {
  81. $instance= null;
  82. $fromCache= false;
  83. if ($className= $xpdo->loadClass($className)) {
  84. if (!is_object($criteria)) {
  85. $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
  86. }
  87. if (is_object($criteria)) {
  88. $row= null;
  89. if ($xpdo->_cacheEnabled && $criteria->cacheFlag && $cacheFlag) {
  90. $row= $xpdo->fromCache($criteria, $className);
  91. }
  92. if ($row === null || !is_array($row)) {
  93. if ($rows= xPDOObject :: _loadRows($xpdo, $className, $criteria)) {
  94. $row= $rows->fetch(PDO::FETCH_ASSOC);
  95. $rows->closeCursor();
  96. }
  97. } else {
  98. $fromCache= true;
  99. }
  100. if (!is_array($row)) {
  101. 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));
  102. } else {
  103. $instance= modAccessibleObject :: _loadInstance($xpdo, $className, $criteria, $row);
  104. if (is_object($instance)) {
  105. if (!$fromCache && $cacheFlag && $xpdo->_cacheEnabled) {
  106. $xpdo->toCache($criteria, $instance, $cacheFlag);
  107. if ($xpdo->getOption(xPDO::OPT_CACHE_DB_OBJECTS_BY_PK) && ($cacheKey= $instance->getPrimaryKey()) && !$instance->isLazy()) {
  108. $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag);
  109. $xpdo->toCache($pkCriteria, $instance, $cacheFlag);
  110. }
  111. }
  112. if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Loaded object instance: " . print_r($instance->toArray('', true), true));
  113. }
  114. }
  115. } else {
  116. $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No valid statement could be found in or generated from the given criteria.');
  117. }
  118. } else {
  119. $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Invalid class specified: ' . $className);
  120. }
  121. return $instance;
  122. }
  123. /**
  124. * Custom collection loader that forces access policy checking.
  125. *
  126. * {@inheritdoc}
  127. */
  128. public static function loadCollection(xPDO & $xpdo, $className, $criteria= null, $cacheFlag= true) {
  129. $objCollection= array ();
  130. $fromCache = false;
  131. if (!$className= $xpdo->loadClass($className)) return $objCollection;
  132. $rows= false;
  133. $fromCache= false;
  134. $collectionCaching = (integer) $xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1);
  135. if (!is_object($criteria)) {
  136. $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
  137. }
  138. if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag) {
  139. $rows= $xpdo->fromCache($criteria);
  140. $fromCache = (is_array($rows) && !empty($rows));
  141. }
  142. if (!$fromCache && is_object($criteria)) {
  143. $rows= xPDOObject :: _loadRows($xpdo, $className, $criteria);
  144. }
  145. if (is_array ($rows)) {
  146. foreach ($rows as $row) {
  147. modAccessibleObject :: _loadCollectionInstance($xpdo, $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag);
  148. }
  149. } elseif (is_object($rows)) {
  150. $cacheRows = array();
  151. while ($row = $rows->fetch(PDO::FETCH_ASSOC)) {
  152. modAccessibleObject :: _loadCollectionInstance($xpdo, $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag);
  153. if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag && !$fromCache) $cacheRows[] = $row;
  154. }
  155. if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag && !$fromCache) $rows =& $cacheRows;
  156. }
  157. if (!$fromCache && $xpdo->_cacheEnabled && $collectionCaching > 0 && $cacheFlag && !empty($rows)) {
  158. $xpdo->toCache($criteria, $rows, $cacheFlag);
  159. }
  160. return $objCollection;
  161. }
  162. /**
  163. * Custom save that respects access policies.
  164. *
  165. * {@inheritdoc}
  166. */
  167. public function save($cacheFlag = null) {
  168. $saved = false;
  169. if (!$this->checkPolicy('save')) {
  170. $this->xpdo->error->failure($this->xpdo->lexicon('permission_denied'));
  171. }
  172. $saved = parent :: save($cacheFlag);
  173. return $saved;
  174. }
  175. /**
  176. * Custom remove that respects access policies.
  177. *
  178. * {@inheritdoc}
  179. */
  180. public function remove(array $ancestors= array ()) {
  181. $removed = false;
  182. if (!$this->checkPolicy('remove')) {
  183. $this->xpdo->error->failure($this->xpdo->lexicon('permission_denied'));
  184. }
  185. $removed = parent :: remove($ancestors);
  186. return $removed;
  187. }
  188. /**
  189. * Determine if the current/specified user attributes satisfy the object policy.
  190. *
  191. * @param array|string $criteria An associative array providing a key and value to
  192. * search for within the matched policy attributes between policy and
  193. * principal, or the name of a permission to check.
  194. * @param string|array $targets A target modAccess class name or an array of
  195. * class names to limit the check. In most cases, this does not need to be
  196. * set; derivatives should typically determine what targets to include in
  197. * the findPolicy() implementation.
  198. * @param modUser $user
  199. * @return boolean Returns true if the policy is satisfied or no policy
  200. * exists.
  201. */
  202. public function checkPolicy($criteria, $targets = null, modUser $user = null) {
  203. if(!$user){
  204. $user = & $this->xpdo->user;
  205. }
  206. if ($criteria && $this->xpdo instanceof modX && $this->xpdo->getSessionState() == modX::SESSION_STATE_INITIALIZED) {
  207. if ($user->get('sudo')) return true;
  208. if (!is_array($criteria) && is_scalar($criteria)) {
  209. $criteria = array("{$criteria}" => true);
  210. }
  211. $policy = $this->findPolicy();
  212. if (!empty($policy)) {
  213. $principal = $user->getAttributes($targets);
  214. if (!empty($principal)) {
  215. foreach ($policy as $policyAccess => $access) {
  216. foreach ($access as $targetId => $targetPolicy) {
  217. foreach ($targetPolicy as $policyIndex => $applicablePolicy) {
  218. if ($this->xpdo->getDebug() === true)
  219. $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));
  220. $principalPolicyData = array();
  221. $principalAuthority = 9999;
  222. if (isset($principal[$policyAccess][$targetId]) && is_array($principal[$policyAccess][$targetId])) {
  223. foreach ($principal[$policyAccess][$targetId] as $acl) {
  224. $principalAuthority = intval($acl['authority']);
  225. $principalPolicyData = $acl['policy'];
  226. $principalId = $acl['principal'];
  227. if ($applicablePolicy['principal'] == $principalId) {
  228. if ($principalAuthority <= $applicablePolicy['authority']) {
  229. if (!$applicablePolicy['policy']) {
  230. return true;
  231. }
  232. if (empty($principalPolicyData)) $principalPolicyData = array();
  233. $matches = array_intersect_assoc($principalPolicyData, $applicablePolicy['policy']);
  234. if ($matches) {
  235. if ($this->xpdo->getDebug() === true)
  236. $this->xpdo->log(modX::LOG_LEVEL_DEBUG, 'Evaluating policy matches: ' . print_r($matches, 1));
  237. $matched = array_diff_assoc($criteria, $matches);
  238. if (empty($matched)) {
  239. return true;
  240. }
  241. }
  242. }
  243. }
  244. }
  245. }
  246. }
  247. }
  248. }
  249. }
  250. return false;
  251. }
  252. }
  253. return true;
  254. }
  255. /**
  256. * Find access policies applicable to this object in a specific context.
  257. *
  258. * @access protected
  259. * @param string $context A key identifying a specific context to use when
  260. * searching for the applicable policies. If not provided, the current
  261. * context is used.
  262. * @return array An array of access policies for this object; an empty
  263. * array is returned if no policies are assigned to the object.
  264. */
  265. public function findPolicy($context = '') {
  266. return array();
  267. }
  268. /**
  269. * Return the currently loaded array of policies.
  270. *
  271. * @return array
  272. */
  273. public function getPolicies() {
  274. return $this->_policies;
  275. }
  276. /**
  277. * Set the current object's policies.
  278. *
  279. * @param array $policies
  280. * @return void
  281. */
  282. public function setPolicies(array $policies = array()) {
  283. $this->_policies = $policies;
  284. }
  285. }