wayfinder.class.php 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. <?php
  2. /**
  3. * Wayfinder Class
  4. *
  5. * @package wayfinder
  6. */
  7. class Wayfinder {
  8. /**
  9. * The array of config parameters
  10. * @access private
  11. * @var array $_config
  12. */
  13. public $_config;
  14. public $_templates;
  15. public $_css;
  16. public $modx = null;
  17. public $docs = array ();
  18. public $parentTree = array ();
  19. public $hasChildren = array ();
  20. public $placeHolders = array (
  21. 'wrapperLevel' => array (
  22. '[[+wf.wrapper]]',
  23. '[[+wf.classes]]',
  24. '[[+wf.classnames]]'
  25. ),
  26. 'tvs' => array (),
  27. );
  28. public $tvList = array ();
  29. public $debugInfo = array ();
  30. private $_cached = false;
  31. private $_cachedTVs = array();
  32. private $_cacheKeys = array();
  33. private $_cacheOptions = array();
  34. function __construct(modX &$modx,array $config = array()) {
  35. $this->modx =& $modx;
  36. $this->_config = array_merge(array(
  37. 'id' => $this->modx->resource->get('id'),
  38. 'level' => 0,
  39. 'includeDocs' => '',
  40. 'excludeDocs' => '',
  41. 'ph' => false,
  42. 'debug' => false,
  43. 'ignoreHidden' =>false,
  44. 'hideSubMenus' => false,
  45. 'useWeblinkUrl' => true,
  46. 'fullLink' => false,
  47. 'sortOrder' => 'ASC',
  48. 'sortBy' => 'menuindex',
  49. 'limit' => 0,
  50. 'cssTpl' => false,
  51. 'jsTpl' => false,
  52. 'rowIdPrefix' => false,
  53. 'textOfLinks' => 'menutitle',
  54. 'titleOfLinks' => 'pagetitle',
  55. 'displayStart' => false,
  56. 'permissions' => 'list',
  57. 'previewUnpublished' => false,
  58. ),$config);
  59. if (empty($this->_config['hereId'])) {
  60. $this->_config['hereId'] = $this->modx->resource->get('id');
  61. }
  62. if (isset($config['sortOrder'])) {
  63. $this->_config['sortOrder'] = strtoupper($config['sortOrder']);
  64. }
  65. if (isset($config['startId'])) { $this->_config['id'] = $config['startId']; }
  66. if (isset($config['removeNewLines'])) { $this->_config['nl'] = ''; }
  67. if (isset($this->_config['contexts'])) {
  68. $this->_config['contexts'] = preg_replace('/, +/', ',', $this->_config['contexts']);
  69. }
  70. }
  71. /**
  72. * Main entry point to generate the menu
  73. *
  74. * @return string The menu HTML or relevant error message.
  75. */
  76. public function run() {
  77. /* setup here checking array */
  78. $this->parentTree = $this->modx->getParentIds($this->_config['hereId']);
  79. $this->parentTree[] = $this->_config['hereId'];
  80. if (!empty($this->_config['debug'])) {
  81. $this->addDebugInfo('settings', 'Settings', 'Settings', 'Settings used to create this menu.', $this->_config);
  82. $this->addDebugInfo('settings', 'CSS', 'CSS Settings', 'Available CSS options.', $this->_css);
  83. }
  84. /* load the templates */
  85. $this->checkTemplates();
  86. /* register any scripts */
  87. if ($this->_config['cssTpl'] || $this->_config['jsTpl']) {
  88. $this->regJsCss();
  89. }
  90. /* check for cached files */
  91. $cacheResults = $this->modx->getOption('cacheResults',$this->_config,true);
  92. if ($cacheResults) {
  93. $this->modx->getCacheManager();
  94. $cache = $this->getFromCache();
  95. if (!empty($cache) && !empty($cache['docs']) && !empty($cache['children'])) {
  96. /* cache files are set */
  97. $this->docs = $cache['docs'];
  98. $this->hasChildren = $cache['children'];
  99. $this->_cached = true;
  100. }
  101. }
  102. if (empty($this->_cached)) {
  103. /* cache files not set - get all of the resources */
  104. $this->docs = $this->getData();
  105. /* set cache files */
  106. if ($cacheResults) {
  107. $this->setToCache();
  108. }
  109. }
  110. if (!empty($this->docs)) {
  111. /* sort resources by level for proper wrapper substitution */
  112. ksort($this->docs);
  113. /* build the menu */
  114. return $this->buildMenu();
  115. } else {
  116. $noneReturn = $this->_config['debug'] ? '<p>No resources found for menu.</p>' : '';
  117. return $noneReturn;
  118. }
  119. }
  120. /**
  121. * Attempt to get the result set from the cache
  122. *
  123. * @return array Cached result set, if existent
  124. */
  125. public function getFromCache() {
  126. $cacheKeys = $this->getCacheKeys();
  127. /* check for cache */
  128. $cache = array();
  129. $cache['docs'] = $this->modx->cacheManager->get($cacheKeys['docs'],$this->_cacheOptions);
  130. $cache['children'] = $this->modx->cacheManager->get($cacheKeys['children'],$this->_cacheOptions);
  131. return $cache;
  132. }
  133. /**
  134. * Set result-set data to cache
  135. * @return boolean
  136. */
  137. public function setToCache() {
  138. $cacheKeys = $this->getCacheKeys();
  139. $cacheTime = $this->modx->getOption('cacheTime',$this->_config,3600);
  140. $this->modx->cacheManager->set($cacheKeys['docs'],$this->docs,$cacheTime,$this->_cacheOptions);
  141. $this->modx->cacheManager->set($cacheKeys['children'],$this->hasChildren,$cacheTime,$this->_cacheOptions);
  142. return true;
  143. }
  144. /**
  145. * Generate an array of cache keys used by wayfinder caching
  146. * @return array An array of cache keys
  147. */
  148. public function getCacheKeys() {
  149. if (!empty($this->_cacheKeys)) return $this->_cacheKeys;
  150. /* generate a UID based on the params passed to Wayfinder and the resource ID
  151. * and the User ID (so that permissions get correctly applied) */
  152. $cacheKey = 'wf-'.$this->modx->user->get('id').'-'.base64_encode(serialize($this->_config));
  153. $childrenCacheKey = $cacheKey.'-children';
  154. /* set cache keys to proper Resource cache so will sync with MODX core caching */
  155. $this->_cacheKeys = array(
  156. 'docs' => $this->modx->resource->getCacheKey().'/'.md5($cacheKey),
  157. 'children' => $this->modx->resource->getCacheKey().'/'.md5($childrenCacheKey),
  158. );
  159. $this->_cacheOptions = array(
  160. 'cache_key' => $this->modx->getOption('cache_resource_key',null, 'resource'),
  161. 'cache_handler' => $this->modx->getOption('cache_resource_handler', null, 'xPDOFileCache'),
  162. 'cache_expires' => (int)$this->modx->getOption('cache_expires', null, 0),
  163. );
  164. return $this->_cacheKeys;
  165. }
  166. /**
  167. * Constructs the menu HTML by looping through the document array
  168. *
  169. * @return string The HTML for the menu
  170. */
  171. public function buildMenu() {
  172. $output = '';
  173. /* loop through all of the menu levels */
  174. foreach ($this->docs as $level => $subDocs) {
  175. /* loop through each document group (grouped by parent resource) */
  176. foreach ($subDocs as $parentId => $docs) {
  177. //if ($this->_config['startId'] != 0 && $this->_config['hideSubMenus']) continue;
  178. /* only process resource group, if starting at root, hidesubmenus is off, or is in current parenttree */
  179. if ((!$this->_config['hideSubMenus'] || $this->isHere($parentId) || $parentId == 0)) {
  180. /* build the output for the group of resources */
  181. $menuPart = $this->buildSubMenu($docs,$level);
  182. /* if at the top of the menu start the output, otherwise replace the wrapper with the submenu */
  183. if (($level == 1 && (!$this->_config['displayStart'] || $this->_config['id'] == 0)) || ($level == 0 && $this->_config['displayStart'])) {
  184. $output = $menuPart;
  185. } else {
  186. $output = str_replace("[[+wf.wrapper.{$parentId}]]",$menuPart,$output);
  187. }
  188. }
  189. }
  190. }
  191. return $output;
  192. }
  193. /**
  194. * Constructs a sub menu for the menu
  195. *
  196. * @param array $menuDocs Array of documents for the menu
  197. * @param int $level The heirarchy level of the sub menu to be rendered
  198. * @return string The submenu HTML
  199. */
  200. public function buildSubMenu($menuDocs,$level) {
  201. $subMenuOutput = '';
  202. $firstItem = 1;
  203. $counter = 1;
  204. $numSubItems = count($menuDocs);
  205. /* loop through each resource to render output */
  206. foreach ($menuDocs as $docId => $docInfo) {
  207. $docInfo['level'] = $level;
  208. $docInfo['first'] = $firstItem;
  209. $firstItem = 0;
  210. /* determine if last item in group */
  211. if ($counter == ($numSubItems) && $numSubItems > 1) {
  212. $docInfo['last'] = 1;
  213. } else {
  214. $docInfo['last'] = 0;
  215. }
  216. /* determine if resource has children */
  217. $docInfo['hasChildren'] = in_array($docInfo['id'],$this->hasChildren) ? 1 : 0;
  218. $numChildren = $docInfo['hasChildren'] ? count($this->docs[$level+1][$docInfo['id']]) : 0;
  219. /* render the row output */
  220. $subMenuOutput .= $this->renderRow($docInfo,$numChildren);
  221. /* update counter for last check */
  222. $counter++;
  223. }
  224. if ($level > 0) {
  225. /* determine which wrapper template to use */
  226. if ($this->_templates['innerTpl'] && $level > 1) {
  227. $useChunk = $this->_templates['innerTpl'];
  228. $usedTemplate = 'innerTpl';
  229. } else {
  230. $useChunk = $this->_templates['outerTpl'];
  231. $usedTemplate = 'outerTpl';
  232. }
  233. /* determine wrapper class */
  234. if ($level > 1) {
  235. $wrapperClass = 'innercls';
  236. } else {
  237. $wrapperClass = 'outercls';
  238. }
  239. /* get the class names for the wrapper */
  240. $classNames = $this->setItemClass($wrapperClass);
  241. $useClass = $classNames ? ' class="' . $classNames . '"' : '';
  242. $phArray = array($subMenuOutput,$useClass,$classNames);
  243. /* process the wrapper */
  244. $subMenuOutput = str_replace($this->placeHolders['wrapperLevel'],$phArray,$useChunk);
  245. /* debug */
  246. if ($this->_config['debug']) {
  247. $debugParent = $docInfo['parent'];
  248. $debugDocInfo = array();
  249. $debugDocInfo['template'] = $usedTemplate;
  250. foreach ($this->placeHolders['wrapperLevel'] as $n => $v) {
  251. if ($v !== '[[+wf.wrapper]]') {
  252. $debugDocInfo[$v] = $phArray[$n];
  253. }
  254. }
  255. $this->addDebugInfo("wrapper","{$debugParent}","Wrapper for items with parent {$debugParent}.","These fields were used when processing the wrapper for the following resources: ",$debugDocInfo);
  256. }
  257. }
  258. return $subMenuOutput;
  259. }
  260. /**
  261. * Renders a row item for the menu
  262. *
  263. * @param array $resource An array containing the document information for the row
  264. * @param int $numChildren The number of children that the document contains
  265. * @return string The HTML for the row item
  266. */
  267. public function renderRow(&$resource,$numChildren) {
  268. $output = '';
  269. /* determine which template to use */
  270. if ($this->_config['displayStart'] && $resource['level'] == 0) {
  271. $usedTemplate = 'startItemTpl';
  272. } elseif ($resource['id'] == $this->_config['hereId'] && $resource['isfolder'] && $this->_templates['parentRowHereTpl'] && ($resource['level'] < $this->_config['level'] || $this->_config['level'] == 0) && $numChildren) {
  273. $usedTemplate = 'parentRowHereTpl';
  274. } elseif ($resource['id'] == $this->_config['hereId'] && $this->_templates['innerHereTpl'] && $resource['level'] > 1) {
  275. $usedTemplate = 'innerHereTpl';
  276. } elseif ($resource['id'] == $this->_config['hereId'] && $this->_templates['hereTpl']) {
  277. $usedTemplate = 'hereTpl';
  278. } elseif ($resource['isfolder'] && $this->_templates['activeParentRowTpl'] && ($resource['level'] < $this->_config['level'] || $this->_config['level'] == 0) && $this->isHere($resource['id'])) {
  279. $usedTemplate = 'activeParentRowTpl';
  280. } elseif ($resource['isfolder'] && ($resource['template']=="0" || is_numeric(strpos($resource['link_attributes'],'rel="category"'))) && $this->_templates['categoryFoldersTpl'] && ($resource['level'] < $this->_config['level'] || $this->_config['level'] == 0)) {
  281. $usedTemplate = 'categoryFoldersTpl';
  282. } elseif ($resource['isfolder'] && $this->_templates['parentRowTpl'] && ($resource['level'] < $this->_config['level'] || $this->_config['level'] == 0) && $numChildren) {
  283. $usedTemplate = 'parentRowTpl';
  284. } elseif ($resource['level'] > 1 && $this->_templates['innerRowTpl']) {
  285. $usedTemplate = 'innerRowTpl';
  286. } else {
  287. $usedTemplate = 'rowTpl';
  288. }
  289. /* get the template */
  290. $useChunk = $this->_templates[$usedTemplate];
  291. /* setup the new wrapper name and get the class names */
  292. $useSub = $resource['hasChildren'] ? "[[+wf.wrapper.{$resource['id']}]]" : "";
  293. $classNames = $this->setItemClass('rowcls',$resource['id'],$resource['first'],$resource['last'],$resource['level'],$resource['isfolder'],$resource['class_key']);
  294. $useClass = $classNames ? ' class="' . $classNames . '"' : '';
  295. /* setup the row id if a prefix is specified */
  296. if ($this->_config['rowIdPrefix']) {
  297. $useId = ' id="' . $this->_config['rowIdPrefix'] . $resource['id'] . '"';
  298. } else {
  299. $useId = '';
  300. }
  301. /* set placeholders for row */
  302. $placeholders = array();
  303. foreach ($resource as $k => $v) {
  304. $placeholders['wf.'.$k] = $v;
  305. }
  306. $placeholders['wf.wrapper'] = $useSub;
  307. $placeholders['wf.classes'] = $useClass;
  308. $placeholders['wf.classNames'] = $classNames;
  309. $placeholders['wf.classnames'] = $classNames;
  310. $placeholders['wf.id'] = $useId;
  311. $placeholders['wf.level'] = $resource['level'];
  312. $placeholders['wf.docid'] = $resource['id'];
  313. $placeholders['wf.subitemcount'] = $numChildren;
  314. $placeholders['wf.attributes'] = $resource['link_attributes'];
  315. if (!empty($this->tvList)) {
  316. $usePlaceholders = array_merge($placeholders,$this->placeHolders['tvs']);
  317. foreach ($this->tvList as $tvName) {
  318. $placeholders[$tvName]=$resource[$tvName];
  319. }
  320. } else {
  321. $usePlaceholders = $placeholders;
  322. }
  323. /* debug */
  324. if ($this->_config['debug']) {
  325. $debugDocInfo = array();
  326. $debugDocInfo['template'] = $usedTemplate;
  327. foreach ($usePlaceholders as $n => $v) {
  328. $debugDocInfo[$v] = $placeholders[$n];
  329. }
  330. $this->addDebugInfo("row","{$resource['parent']}:{$resource['id']}","Doc: #{$resource['id']}","The following fields were used when processing this document.",$debugDocInfo);
  331. $this->addDebugInfo("rowdata","{$resource['parent']}:{$resource['id']}","Doc: #{$resource['id']}","The following fields were retrieved from the database for this document.",$resource);
  332. }
  333. /* @var modChunk $chunk process content as chunk */
  334. $chunk = $this->modx->newObject('modChunk');
  335. $chunk->setCacheable(false);
  336. $output .= $chunk->process($placeholders, $useChunk);
  337. /* return the row */
  338. $separator = $this->modx->getOption('nl',$this->_config,"\n");
  339. return $output . $separator;
  340. }
  341. /**
  342. * Determine style class for current item being processed
  343. *
  344. * @param string $classType The type of class to be returned
  345. * @param int $docId The document ID of the item being processed
  346. * @param int $first Integer representing if the item is the first item (0 or 1)
  347. * @param int $last Integer representing if the item is the last item (0 or 1)
  348. * @param int $level The heirarchy level of the item being processed
  349. * @param int $isFolder Integer representing if the item is a container (0 or 1)
  350. * @param string $type Resource type of the item being processed
  351. * @return string The class string to use
  352. */
  353. public function setItemClass($classType, $docId = 0, $first = 0, $last = 0, $level = 0, $isFolder = 0, $type = 'modDocument') {
  354. $returnClass = '';
  355. $hasClass = 0;
  356. if ($classType === 'outercls' && !empty($this->_css['outer'])) {
  357. /* set outer class if specified */
  358. $returnClass .= $this->_css['outer'];
  359. $hasClass = 1;
  360. } elseif ($classType === 'innercls' && !empty($this->_css['inner'])) {
  361. /* set inner class if specified */
  362. $returnClass .= $this->_css['inner'];
  363. $hasClass = 1;
  364. } elseif ($classType === 'rowcls') {
  365. /* set row class if specified */
  366. if (!empty($this->_css['row'])) {
  367. $returnClass .= $this->_css['row'];
  368. $hasClass = 1;
  369. }
  370. /* set first class if specified */
  371. if ($first && !empty($this->_css['first'])) {
  372. $returnClass .= $hasClass ? ' ' . $this->_css['first'] : $this->_css['first'];
  373. $hasClass = 1;
  374. }
  375. /* set last class if specified */
  376. if ($last && !empty($this->_css['last'])) {
  377. $returnClass .= $hasClass ? ' ' . $this->_css['last'] : $this->_css['last'];
  378. $hasClass = 1;
  379. }
  380. /* set level class if specified */
  381. if (!empty($this->_css['level'])) {
  382. $returnClass .= $hasClass ? ' ' . $this->_css['level'] . $level : $this->_css['level'] . $level;
  383. $hasClass = 1;
  384. }
  385. /* set parentFolder class if specified */
  386. if ($isFolder && !empty($this->_css['parent']) && ($level < $this->_config['level'] || $this->_config['level'] == 0)) {
  387. $returnClass .= $hasClass ? ' ' . $this->_css['parent'] : $this->_css['parent'];
  388. $hasClass = 1;
  389. }
  390. /* set here class if specified */
  391. if (!empty($this->_css['here']) && $this->isHere($docId)) {
  392. $returnClass .= $hasClass ? ' ' . $this->_css['here'] : $this->_css['here'];
  393. $hasClass = 1;
  394. }
  395. /* set self class if specified */
  396. if (!empty($this->_css['self']) && $docId == $this->_config['hereId']) {
  397. $returnClass .= $hasClass ? ' ' . $this->_css['self'] : $this->_css['self'];
  398. $hasClass = 1;
  399. }
  400. /* set class for weblink */
  401. if (!empty($this->_css['weblink']) && $type == 'modWebLink') {
  402. $returnClass .= $hasClass ? ' ' . $this->_css['weblink'] : $this->_css['weblink'];
  403. $hasClass = 1;
  404. }
  405. }
  406. return $returnClass;
  407. }
  408. /**
  409. * Determine the "you are here" point in the menu
  410. *
  411. * @param $did Document ID to find
  412. * @return bool Returns true if the document ID was found
  413. */
  414. public function isHere($did) {
  415. return in_array($did,$this->parentTree);
  416. }
  417. /**
  418. * Add the specified CSS and Javascript chunks to the page
  419. *
  420. * @return void
  421. */
  422. public function regJsCss() {
  423. /* debug */
  424. if ($this->_config['debug']) {
  425. $jsCssDebug = array('js' => 'None Specified.', 'css' => 'None Specified.');
  426. }
  427. /* check and load the CSS */
  428. if (!empty($this->_config['cssTpl'])) {
  429. $cssChunk = $this->fetch($this->_config['cssTpl']);
  430. if ($cssChunk) {
  431. $this->modx->regClientCSS($cssChunk);
  432. if ($this->_config['debug']) {$jsCssDebug['css'] = "The CSS in {$this->_config['cssTpl']} was registered.";}
  433. } else {
  434. if ($this->_config['debug']) {$jsCssDebug['css'] = "The CSS in {$this->_config['cssTpl']} was not found.";}
  435. }
  436. }
  437. /* check and load the Javascript */
  438. if (!empty($this->_config['jsTpl'])) {
  439. $jsChunk = $this->fetch($this->_config['jsTpl']);
  440. if ($jsChunk) {
  441. $this->modx->regClientStartupScript($jsChunk);
  442. if ($this->_config['debug']) {$jsCssDebug['js'] = "The Javascript in {$this->_config['jsTpl']} was registered.";}
  443. } else {
  444. if ($this->_config['debug']) {$jsCssDebug['js'] = "The Javascript in {$this->_config['jsTpl']} was not found.";}
  445. }
  446. }
  447. /* debug */
  448. if ($this->_config['debug']) {$this->addDebugInfo('settings','JSCSS','JS/CSS Includes','Results of CSS & Javascript includes.',$jsCssDebug);}
  449. }
  450. /**
  451. * Smarter getChildIds that will iterate across Contexts if needed
  452. *
  453. * @param integer $startId The ID which to start at
  454. * @param integer $depth The depth in which to parse
  455. * @return array
  456. */
  457. public function getChildIds($startId = 0,$depth = 10) {
  458. $ids = array();
  459. if (!empty($this->_config['contexts'])) {
  460. $contexts = explode(',',$this->_config['contexts']);
  461. $contexts = array_unique($contexts);
  462. $currentContext = $this->modx->context->get('key');
  463. $activeContext = $currentContext;
  464. $switched = false;
  465. foreach ($contexts as $context) {
  466. if ($context != $currentContext) {
  467. $this->modx->switchContext($context);
  468. $switched = true;
  469. $currentContext = $context;
  470. }
  471. /* use modx->getChildIds here, since we dont need to switch contexts within resource children */
  472. $contextIds = $this->modx->getChildIds($startId,$depth);
  473. if (!empty($contextIds)) {
  474. $ids = array_merge($ids,$contextIds);
  475. }
  476. }
  477. $ids = array_unique($ids);
  478. if ($switched) { /* make sure to switch back to active context */
  479. $this->modx->switchContext($activeContext);
  480. }
  481. } else { /* much faster if not using contexts */
  482. $ids = $this->modx->getChildIds($startId,$depth);
  483. }
  484. return $ids;
  485. }
  486. /**
  487. * Get the required resources from the database to build the menu
  488. *
  489. * @return array The resource array of documents to be processed
  490. */
  491. public function getData() {
  492. $depth = !empty($this->_config['level']) ? $this->_config['level'] : 10;
  493. $ids = $this->getChildIds($this->_config['id'],$depth);
  494. $resourceArray = array();
  495. /* get all of the ids for processing */
  496. if ($this->_config['displayStart'] && $this->_config['id'] !== 0) {
  497. $ids[] = $this->_config['id'];
  498. }
  499. if (!empty($ids)) {
  500. $c = $this->modx->newQuery('modResource');
  501. $c->leftJoin('modResourceGroupResource','ResourceGroupResources');
  502. $c->query['distinct'] = 'DISTINCT';
  503. /* add the ignore hidden option to the where clause */
  504. if (!$this->_config['ignoreHidden']) {
  505. $c->where(array('hidemenu:=' => 0));
  506. }
  507. /* if set, limit results to specific resources */
  508. if (!empty($this->_config['includeDocs'])) {
  509. $c->where(array('modResource.id:IN' => explode(',',$this->_config['includeDocs'])));
  510. }
  511. /* add the exclude resources to the where clause */
  512. if (!empty($this->_config['contexts'])) {
  513. $c->where(array('modResource.context_key:IN' => explode(',',$this->_config['contexts'])));
  514. $c->sortby('context_key','DESC');
  515. }
  516. /* add the exclude resources to the where clause */
  517. if (!empty($this->_config['excludeDocs'])) {
  518. $c->where(array('modResource.id:NOT IN' => explode(',',$this->_config['excludeDocs'])));
  519. }
  520. /* add the limit to the query */
  521. if (!empty($this->_config['limit'])) {
  522. $offset = !empty($this->_config['offset']) ? $this->_config['offset'] : 0;
  523. $c->limit($this->_config['limit'], $offset);
  524. }
  525. /* JSON where ability */
  526. if (!empty($this->_config['where'])) {
  527. $where = $this->modx->fromJSON($this->_config['where']);
  528. if (!empty($where)) {
  529. $c->where($where);
  530. }
  531. }
  532. if (!empty($this->_config['templates'])) {
  533. $c->where(array(
  534. 'template:IN' => explode(',',$this->_config['templates']),
  535. ));
  536. }
  537. /* determine sorting */
  538. if (strtolower($this->_config['sortBy']) == 'random') {
  539. $c->sortby('rand()', '');
  540. } else {
  541. $c->sortby($this->_config['sortBy'],$this->_config['sortOrder']);
  542. }
  543. $c->where(array('modResource.id:IN' => $ids));
  544. if ($this->modx->user->hasSessionContext('mgr') && $this->modx->hasPermission('view_unpublished') && $this->_config['previewUnpublished']) {} else {
  545. $c->where(array('modResource.published:=' => 1));
  546. }
  547. $c->where(array('modResource.deleted:=' => 0));
  548. /* not sure why this groupby is here in the first place. removing for now as it causes
  549. * issues with the sortby clauses */
  550. //$c->groupby($this->modx->getSelectColumns('modResource','modResource','',array('id')));
  551. $c->select($this->modx->getSelectColumns('modResource','modResource'));
  552. $c->select(array(
  553. 'protected' => 'ResourceGroupResources.document_group',
  554. ));
  555. $result = $this->modx->getCollection('modResource', $c);
  556. $resourceArray = array();
  557. $level = 1;
  558. $prevParent = -1;
  559. /* setup start level for determining each items level */
  560. if ($this->_config['id'] == 0) {
  561. $startLevel = 0;
  562. } else {
  563. $activeContext = $this->modx->context->get('key');
  564. $contexts = !empty($this->_config['contexts']) ? explode(',',$this->_config['contexts']) : array();
  565. /* switching ctx, as this startId may not be in current Context */
  566. if (!empty($this->_config['startIdContext'])) {
  567. $this->modx->switchContext($this->_config['startIdContext']);
  568. $startLevel = count($this->modx->getParentIds($this->_config['id']));
  569. $this->modx->switchContext($activeContext);
  570. /* attempt to auto-find startId context if &contexts param only has one context */
  571. } else if (!empty($contexts) && !empty($contexts[0]) && $contexts[0] != $activeContext) {
  572. $this->modx->switchContext($contexts[0]);
  573. $startLevel = count($this->modx->getParentIds($this->_config['id']));
  574. $this->modx->switchContext($activeContext);
  575. } else {
  576. $startLevel = count($this->modx->getParentIds($this->_config['id']));
  577. }
  578. }
  579. $resultIds = array();
  580. $activeContext = $this->modx->context->get('key');
  581. $currentContext = $activeContext;
  582. $switchedContext = false;
  583. /** @var modResource $doc */
  584. foreach ($result as $doc) {
  585. $docContextKey = $doc->get('context_key');
  586. if (!empty($docContextKey) && $docContextKey != $currentContext) {
  587. $this->modx->switchContext($docContextKey);
  588. $switchedContext = true;
  589. $currentContext = $doc->get('context_key');
  590. }
  591. if ((!empty($this->_config['permissions'])) && (!$doc->checkPolicy($this->_config['permissions']))) continue;
  592. $tempDocInfo = $doc->toArray();
  593. $resultIds[] = $tempDocInfo['id'];
  594. $tempDocInfo['content'] = $tempDocInfo['class_key'] == 'modWebLink' ? $tempDocInfo['content'] : '';
  595. /* create the link */
  596. $linkScheme = $this->_config['fullLink'] ? 'full' : '';
  597. if (!empty($this->_config['scheme'])) $linkScheme = $this->_config['scheme'];
  598. if ($this->_config['useWeblinkUrl'] !== 'false' && $tempDocInfo['class_key'] == 'modWebLink') {
  599. if (is_numeric($tempDocInfo['content'])) {
  600. $tempDocInfo['link'] = $this->modx->makeUrl(intval($tempDocInfo['content']),'','',$linkScheme);
  601. } else {
  602. $tempDocInfo['link'] = $tempDocInfo['content'];
  603. }
  604. } elseif ($tempDocInfo['id'] == $this->modx->getOption('site_start')) {
  605. $tempDocInfo['link'] = $this->modx->getOption('site_url');
  606. } else {
  607. $tempDocInfo['link'] = $this->modx->makeUrl($tempDocInfo['id'],'','',$linkScheme);
  608. }
  609. /* determine the level, if parent has changed */
  610. if ($prevParent !== $tempDocInfo['parent']) {
  611. $level = count($this->modx->getParentIds($tempDocInfo['id'])) - $startLevel;
  612. }
  613. /* add parent to hasChildren array for later processing */
  614. if (($level > 1 || $this->_config['displayStart']) && !in_array($tempDocInfo['parent'],$this->hasChildren)) {
  615. $this->hasChildren[] = $tempDocInfo['parent'];
  616. }
  617. /* set the level */
  618. $tempDocInfo['level'] = $level;
  619. $prevParent = $tempDocInfo['parent'];
  620. /* determine other output options */
  621. $useTextField = (empty($tempDocInfo[$this->_config['textOfLinks']])) ? 'pagetitle' : $this->_config['textOfLinks'];
  622. $tempDocInfo['linktext'] = $tempDocInfo[$useTextField];
  623. $tempDocInfo['title'] = $tempDocInfo[$this->_config['titleOfLinks']];
  624. $tempDocInfo['protected'] = !empty($tempDocInfo['protected']);
  625. if (!empty($this->tvList)) {
  626. $tempResults[] = $tempDocInfo;
  627. } else {
  628. $resourceArray[$tempDocInfo['level']][$tempDocInfo['parent']][] = $tempDocInfo;
  629. }
  630. }
  631. /* process the tvs */
  632. if (!empty($this->tvList) && !empty($resultIds)) {
  633. $tvValues = array();
  634. /* loop through all tvs and get their values for each resource */
  635. foreach ($this->tvList as $tvName) {
  636. $tvValues = array_merge_recursive($this->appendTV($tvName,$resultIds),$tvValues);
  637. }
  638. /* loop through the document array and add the tvarpublic ues to each resource */
  639. foreach ($tempResults as $tempDocInfo) {
  640. if (array_key_exists("#{$tempDocInfo['id']}",$tvValues)) {
  641. foreach ($tvValues["#{$tempDocInfo['id']}"] as $tvName => $tvValue) {
  642. $tempDocInfo[$tvName] = $tvValue;
  643. }
  644. }
  645. $resourceArray[$tempDocInfo['level']][$tempDocInfo['parent']][] = $tempDocInfo;
  646. }
  647. }
  648. if (!empty($switchedContext)) {
  649. $this->modx->switchContext($activeContext);
  650. }
  651. }
  652. return $resourceArray;
  653. }
  654. /**
  655. * Append a TV to the resource array
  656. *
  657. * @param string $tvName Name of the Template Variable to append
  658. * @param array $docIds An array of document IDs to append the TV to
  659. * @return array A resource array with the TV information
  660. */
  661. public function appendTV($tvName,$docIds){
  662. $resourceArray = array();
  663. /** @var modTemplateVar $tv */
  664. if (empty($this->_cachedTVs[$tvName])) {
  665. $tv = $this->modx->getObject('modTemplateVar',array(
  666. 'name' => $tvName,
  667. ));
  668. } else {
  669. $tv =& $this->_cachedTVs[$tvName];
  670. }
  671. if ($tv) {
  672. foreach ($docIds as $docId) {
  673. $resourceArray["#{$docId}"][$tvName] = $tv->renderOutput($docId);
  674. }
  675. }
  676. return $resourceArray;
  677. }
  678. /**
  679. * Check that templates are valid
  680. *
  681. * @return void
  682. */
  683. public function checkTemplates() {
  684. $nonWayfinderFields = array();
  685. foreach ($this->_templates as $n => $v) {
  686. $templateCheck = $this->fetch($v);
  687. if (empty($v) || !$templateCheck) {
  688. if ($n === 'outerTpl') {
  689. $this->_templates[$n] = '<ul[[+wf.classes]]>[[+wf.wrapper]]</ul>';
  690. } elseif ($n === 'rowTpl') {
  691. $this->_templates[$n] = '<li[[+wf.id]][[+wf.classes]]><a href="[[+wf.link]]" title="[[+wf.title]]" [[+wf.attributes]]>[[+wf.linktext]]</a>[[+wf.wrapper]]</li>';
  692. } elseif ($n === 'startItemTpl') {
  693. $this->_templates[$n] = '<h2[[+wf.id]][[+wf.classes]]>[[+wf.linktext]]</h2>[[+wf.wrapper]]';
  694. } else {
  695. $this->_templates[$n] = false;
  696. }
  697. if ($this->_config['debug']) { $this->addDebugInfo('template',$n,$n,"No template found, using default.",array($n => $this->_templates[$n])); }
  698. } else {
  699. $this->_templates[$n] = $templateCheck;
  700. $check = $this->findTemplateVars($templateCheck);
  701. if (is_array($check)) {
  702. $nonWayfinderFields = array_merge($check, $nonWayfinderFields);
  703. }
  704. if ($this->_config['debug']) { $this->addDebugInfo('template',$n,$n,"Template Found.",array($n => $this->_templates[$n])); }
  705. }
  706. }
  707. if (!empty($nonWayfinderFields)) {
  708. $nonWayfinderFields = array_unique($nonWayfinderFields);
  709. foreach ($nonWayfinderFields as $field) {
  710. $this->placeHolders['tvs'][] = "{$field}";
  711. $this->tvList[] = $field;
  712. }
  713. if ($this->_config['debug']) { $this->addDebugInfo('tvars','tvs','Template Variables',"The following template variables were found in your templates.",$this->tvList); }
  714. }
  715. }
  716. /**
  717. * Fetch a template from the database or filesystem
  718. *
  719. * @param string $tpl Template to be fetched
  720. * @return string|bool Template HTML or false if no template was found
  721. */
  722. public function fetch($tpl) {
  723. /** @var modChunk $chunk */
  724. if ($chunk= $this->modx->getObject('modChunk', array ('name' => $tpl), true)) {
  725. $template = $chunk->getContent();
  726. } else if(substr($tpl, 0, 6) == "@FILE:") {
  727. $template = $this->get_file_contents(substr($tpl, 6));
  728. } else if(substr($tpl, 0, 6) == "@CODE:") {
  729. $template = substr($tpl, 6);
  730. } else if(substr($tpl, 0, 5) == "@FILE") {
  731. $template = $this->get_file_contents(trim(substr($tpl, 5)));
  732. } else if(substr($tpl, 0, 5) == "@CODE") {
  733. $template = trim(substr($tpl, 5));
  734. } else {
  735. $template = false;
  736. }
  737. return $template;
  738. }
  739. /**
  740. * Substitute function for file_get_contents()
  741. *
  742. * @param string $filename Name of file to be fetched
  743. * @return string The file contents
  744. */
  745. public function get_file_contents($filename) {
  746. if (!function_exists('file_get_contents')) {
  747. $fhandle = fopen($filename, "r");
  748. $fcontents = fread($fhandle, filesize($filename));
  749. fclose($fhandle);
  750. } else {
  751. $fcontents = file_get_contents($filename);
  752. }
  753. return $fcontents;
  754. }
  755. public function findTemplateVars($tpl) {
  756. preg_match_all('~\[\[\+(.*?)\]\]~', $tpl, $matches);
  757. $TVs = array();
  758. foreach($matches[1] as $tv) {
  759. if (strpos($tv, "wf.") === false) {
  760. $match = explode(":", $tv);
  761. $TVs[strtolower($match[0])] = $match[0];
  762. }
  763. }
  764. if (count($TVs) >= 1) {
  765. return array_values($TVs);
  766. } else {
  767. return false;
  768. }
  769. }
  770. /**
  771. * Add debug information to the debug array
  772. *
  773. * @param string $group Group to attach the message to
  774. * @param string $groupkey Group key to attach the message to
  775. * @param string $header Title for the debug message
  776. * @param string $message The debug message
  777. * @param array $info An array of information to be added to the message as $key=>$value pairs
  778. * @return void
  779. */
  780. public function addDebugInfo($group,$groupkey,$header,$message,$info) {
  781. $infoString = '<table border="1" cellpadding="3px">';
  782. $numInfo = count($info);
  783. $count = 0;
  784. foreach ($info as $key => $value) {
  785. $key = $this->modxPrep($key);
  786. if ($value === true || $value === false) {
  787. $value = $value ? 'true' : 'false';
  788. } else {
  789. $value = $this->modxPrep($value);
  790. }
  791. if ($count == 2) { $infoString .= '</tr>'; $count = 0; }
  792. if ($count == 0) { $infoString .= '<tr>'; }
  793. $value = empty($value) ? '&nbsp;' : $value;
  794. $infoString .= "<td><strong>{$key}</strong></td><td>{$value}</td>";
  795. $count++;
  796. }
  797. $infoString .= '</tr></table>';
  798. $this->debugInfo[$group][$groupkey] = array(
  799. 'header' => $this->modxPrep($header),
  800. 'message' => $this->modxPrep($message),
  801. 'info' => $infoString,
  802. );
  803. }
  804. /**
  805. * Render the debug array for display
  806. *
  807. * @return string HTML containing the rendered debug information
  808. */
  809. public function renderDebugOutput() {
  810. $output = '<table border="1" cellpadding="3px" width="100%">';
  811. foreach ($this->debugInfo as $group => $item) {
  812. switch ($group) {
  813. case 'template':
  814. $output .= "<tr><th style=\"background:#C3D9FF;font-size:200%;\">Template Processing</th></tr>";
  815. foreach ($item as $parentId => $info) {
  816. $output .= "
  817. <tr style=\"background:#336699;color:#fff;\"><th>{$info['header']} - <span style=\"font-weight:normal;\">{$info['message']}</span></th></tr>
  818. <tr><td>{$info['info']}</td></tr>";
  819. }
  820. break;
  821. case 'wrapper':
  822. $output .= "<tr><th style=\"background:#C3D9FF;font-size:200%;\">Document Processing</th></tr>";
  823. foreach ($item as $parentId => $info) {
  824. $output .= "<tr><table border=\"1\" cellpadding=\"3px\" style=\"margin-bottom: 10px;\" width=\"100%\">
  825. <tr style=\"background:#336699;color:#fff;\"><th>{$info['header']} - <span style=\"font-weight:normal;\">{$info['message']}</span></th></tr>
  826. <tr><td>{$info['info']}</td></tr>
  827. <tr style=\"background:#336699;color:#fff;\"><th>Documents included in this wrapper:</th></tr>";
  828. foreach ($this->debugInfo['row'] as $key => $value) {
  829. $keyParts = explode(':',$key);
  830. if ($parentId == $keyParts[0]) {
  831. $output .= "<tr style=\"background:#eee;\"><th>{$value['header']}</th></tr>
  832. <tr><td><div style=\"float:left;margin-right:1%;\">{$value['message']}<br />{$value['info']}</div><div style=\"float:left;\">{$this->debugInfo['rowdata'][$key]['message']}<br />{$this->debugInfo['rowdata'][$key]['info']}</div></td></tr>";
  833. }
  834. }
  835. $output .= '</table></tr>';
  836. }
  837. break;
  838. case 'settings':
  839. $output .= "<tr><th style=\"background:#C3D9FF;font-size:200%;\">Settings</th></tr>";
  840. foreach ($item as $parentId => $info) {
  841. $output .= "
  842. <tr style=\"background:#336699;color:#fff;\"><th>{$info['header']} - <span style=\"font-weight:normal;\">{$info['message']}</span></th></tr>
  843. <tr><td>{$info['info']}</td></tr>";
  844. }
  845. break;
  846. default:
  847. break;
  848. }
  849. }
  850. $output .= '</table>';
  851. return $output;
  852. }
  853. /**
  854. * Preprocess values for rendering in the debug information
  855. *
  856. * @param string $value The value to be processed
  857. * @return string The processed value
  858. */
  859. public function modxPrep($value) {
  860. $value = (strpos($value,"<") !== false) ? htmlentities($value) : $value;
  861. $value = str_replace("[","&#091;",$value);
  862. $value = str_replace("]","&#093;",$value);
  863. $value = str_replace("{","&#123;",$value);
  864. $value = str_replace("}","&#125;",$value);
  865. return $value;
  866. }
  867. }