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

/core/model/modx/modcategory.class.php

http://github.com/modxcms/revolution
PHP | 280 lines | 255 code | 2 blank | 23 comment | 0 complexity | 453d1faa54a06ed95fa1ddbea9027cfd MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, BSD-3-Clause, LGPL-2.1
  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. * Represents a category for organizing modElement instances.
  12. *
  13. * @property int $parent The parent category ID, if set. Otherwise defaults to 0.
  14. * @property string $category The name of the Category.
  15. * @package modx
  16. */
  17. class modCategory extends modAccessibleSimpleObject {
  18. /**
  19. * @var boolean Monitors whether parent has been changed.
  20. * @access protected
  21. */
  22. protected $_parentChanged = false;
  23. /**
  24. * @var array A list of invalid characters in the name of an Element.
  25. * @access protected
  26. */
  27. protected $_invalidCharacters = array('!','@','#','$','%','^','&','*',
  28. '(',')','+','=','[',']','{','}','\'','"',':',';','\\','/','<','>','?'
  29. ,',','`','~');
  30. /**
  31. * Overrides xPDOObject::set to strip invalid characters from element names.
  32. *
  33. * {@inheritDoc}
  34. */
  35. public function set($k, $v= null, $vType= '') {
  36. $set = false;
  37. switch ($k) {
  38. case 'category':
  39. $v = str_replace($this->_invalidCharacters,'',$v);
  40. default:
  41. $oldParentId = $this->get('parent');
  42. $set = parent::set($k,$v,$vType);
  43. if ($set && $k == 'parent' && $v != $oldParentId && !$this->isNew()) {
  44. $this->_parentChanged = true;
  45. }
  46. }
  47. return $set;
  48. }
  49. /**
  50. * Overrides xPDOObject::save to fire modX-specific events
  51. *
  52. * {@inheritDoc}
  53. */
  54. public function save($cacheFlag = null) {
  55. $isNew = $this->isNew();
  56. if ($this->xpdo instanceof modX) {
  57. $this->xpdo->invokeEvent('OnCategoryBeforeSave',array(
  58. 'mode' => $isNew ? modSystemEvent::MODE_NEW : modSystemEvent::MODE_UPD,
  59. 'category' => &$this,
  60. 'cacheFlag' => $cacheFlag,
  61. ));
  62. }
  63. $saved = parent :: save($cacheFlag);
  64. /* if a new board */
  65. if ($saved && $isNew) {
  66. $this->buildClosure();
  67. }
  68. /* if parent changed on existing object, rebuild closure table */
  69. if (!$isNew && $this->_parentChanged) {
  70. $this->rebuildClosure();
  71. }
  72. if ($saved && $this->xpdo instanceof modX) {
  73. $this->xpdo->invokeEvent('OnCategorySave',array(
  74. 'mode' => $isNew ? modSystemEvent::MODE_NEW : modSystemEvent::MODE_UPD,
  75. 'category' => &$this,
  76. 'cacheFlag' => $cacheFlag,
  77. ));
  78. }
  79. return $saved;
  80. }
  81. /**
  82. * Overrides xPDOObject::remove to reset all Element categories back to 0
  83. * and fire modX-specific events.
  84. *
  85. * {@inheritDoc}
  86. */
  87. public function remove(array $ancestors = array()) {
  88. if ($this->xpdo instanceof modX) {
  89. $this->xpdo->invokeEvent('OnCategoryBeforeRemove',array(
  90. 'category' => &$this,
  91. 'ancestors' => $ancestors,
  92. ));
  93. }
  94. $removed = parent :: remove($ancestors);
  95. if ($removed && $this->xpdo instanceof modX) {
  96. $elementClasses = array(
  97. 'modChunk',
  98. 'modPlugin',
  99. 'modSnippet',
  100. 'modTemplate',
  101. 'modTemplateVar',
  102. );
  103. foreach ($elementClasses as $classKey) {
  104. $elements = $this->xpdo->getCollection($classKey,array('category' => $this->get('id')));
  105. foreach ($elements as $element) {
  106. $element->set('category',0);
  107. $element->save();
  108. }
  109. }
  110. $this->xpdo->invokeEvent('OnCategoryRemove',array(
  111. 'category' => &$this,
  112. 'ancestors' => $ancestors,
  113. ));
  114. }
  115. return $removed;
  116. }
  117. /**
  118. * Loads the access control policies applicable to this category.
  119. *
  120. * {@inheritdoc}
  121. */
  122. public function findPolicy($context = '') {
  123. $policy = array();
  124. $enabled = true;
  125. $context = !empty($context) ? $context : $this->xpdo->context->get('key');
  126. if ($context === $this->xpdo->context->get('key')) {
  127. $enabled = (boolean) $this->xpdo->getOption('access_category_enabled', null, true);
  128. } elseif ($this->xpdo->getContext($context)) {
  129. $enabled = (boolean) $this->xpdo->contexts[$context]->getOption('access_category_enabled', true);
  130. }
  131. if ($enabled) {
  132. if (empty($this->_policies) || !isset($this->_policies[$context])) {
  133. $aclSelectColumns = $this->xpdo->getSelectColumns('modAccessCategory','modAccessCategory','',array('id','target','principal','authority','policy'));
  134. $c = $this->xpdo->newQuery('modAccessCategory');
  135. $c->select($aclSelectColumns);
  136. $c->select($this->xpdo->getSelectColumns('modAccessPolicy','Policy','',array('data')));
  137. $c->leftJoin('modAccessPolicy','Policy');
  138. $c->innerJoin('modCategoryClosure','CategoryClosure',array(
  139. 'CategoryClosure.descendant:=' => $this->get('id'),
  140. 'modAccessCategory.principal_class:=' => 'modUserGroup',
  141. 'CategoryClosure.ancestor = modAccessCategory.target',
  142. array(
  143. 'modAccessCategory.context_key:=' => $context,
  144. 'OR:modAccessCategory.context_key:=' => null,
  145. 'OR:modAccessCategory.context_key:=' => '',
  146. ),
  147. ));
  148. $c->groupby($aclSelectColumns);
  149. $c->sortby($this->xpdo->getSelectColumns('modCategoryClosure','CategoryClosure','',array('depth')).' DESC, '.$this->xpdo->getSelectColumns('modAccessCategory','modAccessCategory','',array('authority')).' ASC','');
  150. $acls = $this->xpdo->getIterator('modAccessCategory',$c);
  151. foreach ($acls as $acl) {
  152. $policy['modAccessCategory'][$acl->get('target')][] = array(
  153. 'principal' => $acl->get('principal'),
  154. 'authority' => $acl->get('authority'),
  155. 'policy' => $acl->get('data') ? $this->xpdo->fromJSON($acl->get('data'), true) : array(),
  156. );
  157. }
  158. $this->_policies[$context] = $policy;
  159. } else {
  160. $policy = $this->_policies[$context];
  161. }
  162. }
  163. return $policy;
  164. }
  165. /**
  166. * Build the closure table for this instance.
  167. *
  168. * @return boolean True unless building the closure fails and instance is removed.
  169. */
  170. public function buildClosure() {
  171. $id = $this->get('id');
  172. $parent = $this->get('parent');
  173. /* create self closure */
  174. $cl = $this->xpdo->newObject('modCategoryClosure');
  175. $cl->set('ancestor',$id);
  176. $cl->set('descendant',$id);
  177. if ($cl->save() === false) {
  178. $this->remove();
  179. return false;
  180. }
  181. /* create closures and calculate rank */
  182. $c = $this->xpdo->newQuery('modCategoryClosure');
  183. $c->where(array(
  184. 'descendant' => $parent,
  185. ));
  186. $c->sortby('depth','DESC');
  187. $gparents = $this->xpdo->getCollection('modCategoryClosure',$c);
  188. $cgps = count($gparents);
  189. $i = $cgps - 1;
  190. $gps = array();
  191. foreach ($gparents as $gparent) {
  192. $depth = 0;
  193. $ancestor = $gparent->get('ancestor');
  194. if ($ancestor != 0) $depth = $i;
  195. $obj = $this->xpdo->newObject('modCategoryClosure');
  196. $obj->set('ancestor',$ancestor);
  197. $obj->set('descendant',$id);
  198. $obj->set('depth',$depth);
  199. $obj->save();
  200. $i--;
  201. $gps[] = $ancestor;
  202. }
  203. /* handle 0 ancestor closure */
  204. $rootcl = $this->xpdo->getObject('modCategoryClosure',array(
  205. 'ancestor' => 0,
  206. 'descendant' => $id,
  207. ));
  208. if (!$rootcl) {
  209. $rootcl = $this->xpdo->newObject('modCategoryClosure');
  210. $rootcl->set('ancestor',0);
  211. $rootcl->set('descendant',$id);
  212. $rootcl->set('depth',0);
  213. $rootcl->save();
  214. }
  215. return true;
  216. }
  217. /**
  218. * Rebuild closure table records for this instance, i.e. parent changed.
  219. */
  220. public function rebuildClosure() {
  221. /* first remove old tree path */
  222. $this->xpdo->removeCollection('modCategoryClosure',array(
  223. 'descendant' => $this->get('id'),
  224. 'ancestor:!=' => $this->get('id'),
  225. ));
  226. /* now create new tree path from new parent */
  227. $newParentId = $this->get('parent');
  228. $c = $this->xpdo->newQuery('modCategoryClosure');
  229. $c->where(array(
  230. 'descendant' => $newParentId,
  231. ));
  232. $c->sortby('depth','DESC');
  233. $ancestors= $this->xpdo->getCollection('modCategoryClosure',$c);
  234. $grandParents = array();
  235. foreach ($ancestors as $ancestor) {
  236. $depth = $ancestor->get('depth');
  237. $grandParentId = $ancestor->get('ancestor');
  238. /* if already has a depth, inc by 1 */
  239. if ($depth > 0) $depth++;
  240. /* if is the new parent node, set depth to 1 */
  241. if ($grandParentId == $newParentId && $newParentId != 0) { $depth = 1; }
  242. if ($grandParentId != 0) {
  243. $grandParents[] = $grandParentId;
  244. }
  245. $cl = $this->xpdo->newObject('modCategoryClosure');
  246. $cl->set('ancestor',$ancestor->get('ancestor'));
  247. $cl->set('descendant',$this->get('id'));
  248. $cl->set('depth',$depth);
  249. $cl->save();
  250. }
  251. /* if parent is root, make sure to set the root closure */
  252. if ($newParentId == 0) {
  253. $cl = $this->xpdo->newObject('modCategoryClosure');
  254. $cl->set('ancestor',0);
  255. $cl->set('descendant',$this->get('id'));
  256. $cl->save();
  257. }
  258. }
  259. }