Merge pull request #15650 from demeritcowboy/mode-value-missing
[civicrm-core.git] / CRM / Core / Menu.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * This file contains the various menus of the CiviCRM module
30 *
31 * @package CRM
32 * @copyright CiviCRM LLC (c) 2004-2019
33 */
34
35 require_once 'CRM/Core/I18n.php';
36
37 /**
38 * Class CRM_Core_Menu.
39 */
40 class CRM_Core_Menu {
41
42 /**
43 * The list of menu items.
44 *
45 * @var array
46 */
47 public static $_items = NULL;
48
49 /**
50 * The list of permissioned menu items.
51 *
52 * @var array
53 */
54 public static $_permissionedItems = NULL;
55
56 public static $_serializedElements = array(
57 'access_arguments',
58 'access_callback',
59 'page_arguments',
60 'page_callback',
61 'breadcrumb',
62 );
63
64 public static $_menuCache = NULL;
65 const MENU_ITEM = 1;
66
67 /**
68 * This function fetches the menu items from xml and xmlMenu hooks.
69 *
70 * @param bool $fetchFromXML
71 * Fetch the menu items from xml and not from cache.
72 *
73 * @return array
74 */
75 public static function &xmlItems($fetchFromXML = FALSE) {
76 if (!self::$_items || $fetchFromXML) {
77 $config = CRM_Core_Config::singleton();
78
79 // We needs this until Core becomes a component
80 $coreMenuFilesNamespace = 'CRM_Core_xml_Menu';
81 $coreMenuFilesPath = str_replace('_', DIRECTORY_SEPARATOR, $coreMenuFilesNamespace);
82 global $civicrm_root;
83 $files = CRM_Utils_File::getFilesByExtension($civicrm_root . DIRECTORY_SEPARATOR . $coreMenuFilesPath, 'xml');
84
85 // Grab component menu files
86 $files = array_merge($files,
87 CRM_Core_Component::xmlMenu()
88 );
89
90 // lets call a hook and get any additional files if needed
91 CRM_Utils_Hook::xmlMenu($files);
92
93 self::$_items = array();
94 foreach ($files as $file) {
95 self::read($file, self::$_items);
96 }
97
98 CRM_Utils_Hook::alterMenu(self::$_items);
99 }
100
101 return self::$_items;
102 }
103
104 /**
105 * Read menu.
106 *
107 * @param string $name
108 * File name
109 * @param array $menu
110 * An alterable list of menu items.
111 *
112 * @throws Exception
113 */
114 public static function read($name, &$menu) {
115 $xml = simplexml_load_file($name);
116 self::readXML($xml, $menu);
117 }
118
119 /**
120 * @param SimpleXMLElement $xml
121 * An XML document defining a list of menu items.
122 * @param array $menu
123 * An alterable list of menu items.
124 */
125 public static function readXML($xml, &$menu) {
126 $config = CRM_Core_Config::singleton();
127 foreach ($xml->item as $item) {
128 if (!(string ) $item->path) {
129 CRM_Core_Error::debug('i', $item);
130 CRM_Core_Error::fatal();
131 }
132 $path = (string ) $item->path;
133 $menu[$path] = array();
134 unset($item->path);
135
136 if ($item->ids_arguments) {
137 $ids = array();
138 foreach (array('json' => 'json', 'html' => 'html', 'exception' => 'exceptions') as $tag => $attr) {
139 $ids[$attr] = array();
140 foreach ($item->ids_arguments->{$tag} as $value) {
141 $ids[$attr][] = (string) $value;
142 }
143 }
144 $menu[$path]['ids_arguments'] = $ids;
145 unset($item->ids_arguments);
146 }
147
148 foreach ($item as $key => $value) {
149 $key = (string ) $key;
150 $value = (string ) $value;
151 if (strpos($key, '_callback') &&
152 strpos($value, '::')
153 ) {
154 // FIXME Remove the rewrite at this level. Instead, change downstream call_user_func*($value)
155 // to call_user_func*(Civi\Core\Resolver::singleton()->get($value)).
156 $value = explode('::', $value);
157 }
158 elseif ($key == 'access_arguments') {
159 // FIXME Move the permission parser to its own class (or *maybe* CRM_Core_Permission).
160 if (strpos($value, ',') ||
161 strpos($value, ';')
162 ) {
163 if (strpos($value, ',')) {
164 $elements = explode(',', $value);
165 $op = 'and';
166 }
167 else {
168 $elements = explode(';', $value);
169 $op = 'or';
170 }
171 $items = array();
172 foreach ($elements as $element) {
173 $items[] = $element;
174 }
175 $value = array($items, $op);
176 }
177 else {
178 $value = array(array($value), 'and');
179 }
180 }
181 elseif ($key == 'is_public' || $key == 'is_ssl') {
182 $value = ($value == 'true' || $value == 1) ? 1 : 0;
183 }
184 $menu[$path][$key] = $value;
185 }
186 }
187 }
188
189 /**
190 * This function defines information for various menu items.
191 *
192 * @param bool $fetchFromXML
193 * Fetch the menu items from xml and not from cache.
194 *
195 * @return array
196 */
197 public static function &items($fetchFromXML = FALSE) {
198 return self::xmlItems($fetchFromXML);
199 }
200
201 /**
202 * Is array true (whatever that means!).
203 *
204 * @param array $values
205 *
206 * @return bool
207 */
208 public static function isArrayTrue(&$values) {
209 foreach ($values as $name => $value) {
210 if (!$value) {
211 return FALSE;
212 }
213 }
214 return TRUE;
215 }
216
217 /**
218 * Fill menu values.
219 *
220 * @param array $menu
221 * @param string $path
222 *
223 * @throws Exception
224 */
225 public static function fillMenuValues(&$menu, $path) {
226 $fieldsToPropagate = array(
227 'access_callback',
228 'access_arguments',
229 'page_callback',
230 'page_arguments',
231 'is_ssl',
232 );
233 $fieldsPresent = array();
234 foreach ($fieldsToPropagate as $field) {
235 $fieldsPresent[$field] = CRM_Utils_Array::value($field, $menu[$path]) !== NULL ? TRUE : FALSE;
236 }
237
238 $args = explode('/', $path);
239 while (!self::isArrayTrue($fieldsPresent) && !empty($args)) {
240
241 array_pop($args);
242 $parentPath = implode('/', $args);
243
244 foreach ($fieldsToPropagate as $field) {
245 if (!$fieldsPresent[$field]) {
246 if (CRM_Utils_Array::value($field, CRM_Utils_Array::value($parentPath, $menu)) !== NULL) {
247 $fieldsPresent[$field] = TRUE;
248 $menu[$path][$field] = $menu[$parentPath][$field];
249 }
250 }
251 }
252 }
253
254 if (self::isArrayTrue($fieldsPresent)) {
255 return;
256 }
257
258 $messages = array();
259 foreach ($fieldsToPropagate as $field) {
260 if (!$fieldsPresent[$field]) {
261 $messages[] = ts("Could not find %1 in path tree",
262 array(1 => $field)
263 );
264 }
265 }
266 CRM_Core_Error::fatal("'$path': " . implode(', ', $messages));
267 }
268
269 /**
270 * We use this function to.
271 *
272 * 1. Compute the breadcrumb
273 * 2. Compute local tasks value if any
274 * 3. Propagate access argument, access callback, page callback to the menu item
275 * 4. Build the global navigation block
276 *
277 * @param array $menu
278 */
279 public static function build(&$menu) {
280 foreach ($menu as $path => $menuItems) {
281 self::buildBreadcrumb($menu, $path);
282 self::fillMenuValues($menu, $path);
283 self::fillComponentIds($menu, $path);
284 self::buildReturnUrl($menu, $path);
285
286 // add add page_type if not present
287 if (!isset($menu[$path]['page_type'])) {
288 $menu[$path]['page_type'] = 0;
289 }
290 }
291
292 self::buildAdminLinks($menu);
293 }
294
295 /**
296 * This function recomputes menu from xml and populates civicrm_menu.
297 *
298 * @param bool $truncate
299 */
300 public static function store($truncate = TRUE) {
301 // first clean up the db
302 if ($truncate) {
303 $query = 'TRUNCATE civicrm_menu';
304 CRM_Core_DAO::executeQuery($query);
305 }
306 $menuArray = self::items($truncate);
307
308 self::build($menuArray);
309
310 $daoFields = CRM_Core_DAO_Menu::fields();
311
312 foreach ($menuArray as $path => $item) {
313 $menu = new CRM_Core_DAO_Menu();
314 $menu->path = $path;
315 $menu->domain_id = CRM_Core_Config::domainID();
316
317 $menu->find(TRUE);
318
319 if (!CRM_Core_Config::isUpgradeMode() ||
320 CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_menu', 'module_data', FALSE)
321 ) {
322 // Move unrecognized fields to $module_data.
323 $module_data = array();
324 foreach (array_keys($item) as $key) {
325 if (!isset($daoFields[$key])) {
326 $module_data[$key] = $item[$key];
327 unset($item[$key]);
328 }
329 }
330
331 $menu->module_data = serialize($module_data);
332 }
333
334 $menu->copyValues($item);
335
336 foreach (self::$_serializedElements as $element) {
337 if (!isset($item[$element]) ||
338 $item[$element] == 'null'
339 ) {
340 $menu->$element = NULL;
341 }
342 else {
343 $menu->$element = serialize($item[$element]);
344 }
345 }
346
347 $menu->save();
348 }
349 }
350
351 /**
352 * Build admin links.
353 *
354 * @param array $menu
355 */
356 public static function buildAdminLinks(&$menu) {
357 $values = array();
358
359 foreach ($menu as $path => $item) {
360 if (empty($item['adminGroup'])) {
361 continue;
362 }
363
364 $query = !empty($item['path_arguments']) ? str_replace(',', '&', $item['path_arguments']) . '&reset=1' : 'reset=1';
365
366 $value = array(
367 'title' => $item['title'],
368 'desc' => CRM_Utils_Array::value('desc', $item),
369 'id' => strtr($item['title'], array(
370 '(' => '_',
371 ')' => '',
372 ' ' => '',
373 ',' => '_',
374 '/' => '_',
375 )),
376 'url' => CRM_Utils_System::url($path, $query,
377 FALSE,
378 NULL,
379 TRUE,
380 FALSE,
381 // forceBackend; CRM-14439 work-around; acceptable for now because we don't display breadcrumbs on frontend
382 TRUE
383 ),
384 'icon' => CRM_Utils_Array::value('icon', $item),
385 'extra' => CRM_Utils_Array::value('extra', $item),
386 );
387 if (!array_key_exists($item['adminGroup'], $values)) {
388 $values[$item['adminGroup']] = array();
389 $values[$item['adminGroup']]['fields'] = array();
390 }
391 $weight = CRM_Utils_Array::value('weight', $item, 0);
392 $values[$item['adminGroup']]['fields']["{weight}.{$item['title']}"] = $value;
393 $values[$item['adminGroup']]['component_id'] = $item['component_id'];
394 }
395
396 foreach ($values as $group => $dontCare) {
397 $values[$group]['perColumn'] = round(count($values[$group]['fields']) / 2);
398 ksort($values[$group]);
399 }
400
401 $menu['admin'] = array('breadcrumb' => $values);
402 }
403
404 /**
405 * Get admin links.
406 *
407 * @return null
408 */
409 public static function &getAdminLinks() {
410 $links = self::get('admin');
411
412 if (!$links ||
413 !isset($links['breadcrumb'])
414 ) {
415 return NULL;
416 }
417
418 $values = &$links['breadcrumb'];
419 return $values;
420 }
421
422 /**
423 * Get the breadcrumb for a given path.
424 *
425 * @param array $menu
426 * An array of all the menu items.
427 * @param string $path
428 * Path for which breadcrumb is to be build.
429 *
430 * @return array
431 * The breadcrumb for this path
432 */
433 public static function buildBreadcrumb(&$menu, $path) {
434 $crumbs = array();
435
436 $pathElements = explode('/', $path);
437 array_pop($pathElements);
438
439 $currentPath = NULL;
440 while ($newPath = array_shift($pathElements)) {
441 $currentPath = $currentPath ? ($currentPath . '/' . $newPath) : $newPath;
442
443 // when we come across breadcrumb which involves ids,
444 // we should skip now and later on append dynamically.
445 if (isset($menu[$currentPath]['skipBreadcrumb'])) {
446 continue;
447 }
448
449 // add to crumb, if current-path exists in params.
450 if (array_key_exists($currentPath, $menu) &&
451 isset($menu[$currentPath]['title'])
452 ) {
453 $urlVar = !empty($menu[$currentPath]['path_arguments']) ? '&' . $menu[$currentPath]['path_arguments'] : '';
454 $crumbs[] = array(
455 'title' => $menu[$currentPath]['title'],
456 'url' => CRM_Utils_System::url($currentPath,
457 'reset=1' . $urlVar,
458 // absolute
459 FALSE,
460 // fragment
461 NULL,
462 // htmlize
463 TRUE,
464 // frontend
465 FALSE,
466 // forceBackend; CRM-14439 work-around; acceptable for now because we don't display breadcrumbs on frontend
467 TRUE
468 ),
469 );
470 }
471 }
472 $menu[$path]['breadcrumb'] = $crumbs;
473
474 return $crumbs;
475 }
476
477 /**
478 * @param $menu
479 * @param $path
480 */
481 public static function buildReturnUrl(&$menu, $path) {
482 if (!isset($menu[$path]['return_url'])) {
483 list($menu[$path]['return_url'], $menu[$path]['return_url_args']) = self::getReturnUrl($menu, $path);
484 }
485 }
486
487 /**
488 * @param $menu
489 * @param $path
490 *
491 * @return array
492 */
493 public static function getReturnUrl(&$menu, $path) {
494 if (!isset($menu[$path]['return_url'])) {
495 $pathElements = explode('/', $path);
496 array_pop($pathElements);
497
498 if (empty($pathElements)) {
499 return array(NULL, NULL);
500 }
501 $newPath = implode('/', $pathElements);
502
503 return self::getReturnUrl($menu, $newPath);
504 }
505 else {
506 return array(
507 CRM_Utils_Array::value('return_url',
508 $menu[$path]
509 ),
510 CRM_Utils_Array::value('return_url_args',
511 $menu[$path]
512 ),
513 );
514 }
515 }
516
517 /**
518 * @param $menu
519 * @param $path
520 */
521 public static function fillComponentIds(&$menu, $path) {
522 static $cache = array();
523
524 if (array_key_exists('component_id', $menu[$path])) {
525 return;
526 }
527
528 $args = explode('/', $path);
529
530 if (count($args) > 1) {
531 $compPath = $args[0] . '/' . $args[1];
532 }
533 else {
534 $compPath = $args[0];
535 }
536
537 $componentId = NULL;
538
539 if (array_key_exists($compPath, $cache)) {
540 $menu[$path]['component_id'] = $cache[$compPath];
541 }
542 else {
543 if (CRM_Utils_Array::value('component', CRM_Utils_Array::value($compPath, $menu))) {
544 $componentId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Component',
545 $menu[$compPath]['component'],
546 'id', 'name'
547 );
548 }
549 $menu[$path]['component_id'] = $componentId ? $componentId : NULL;
550 $cache[$compPath] = $menu[$path]['component_id'];
551 }
552 }
553
554 /**
555 * @param $path string
556 * Path of menu item to retrieve.
557 *
558 * @return array
559 * Menu entry array.
560 */
561 public static function get($path) {
562 // return null if menu rebuild
563 $config = CRM_Core_Config::singleton();
564
565 $args = explode('/', $path);
566
567 $elements = array();
568 while (!empty($args)) {
569 $string = implode('/', $args);
570 $string = CRM_Core_DAO::escapeString($string);
571 $elements[] = "'{$string}'";
572 array_pop($args);
573 }
574
575 $queryString = implode(', ', $elements);
576 $domainID = CRM_Core_Config::domainID();
577
578 $query = "
579 (
580 SELECT *
581 FROM civicrm_menu
582 WHERE path in ( $queryString )
583 AND domain_id = $domainID
584 ORDER BY length(path) DESC
585 LIMIT 1
586 )
587 ";
588
589 if ($path != 'navigation') {
590 $query .= "
591 UNION (
592 SELECT *
593 FROM civicrm_menu
594 WHERE path IN ( 'navigation' )
595 AND domain_id = $domainID
596 )
597 ";
598 }
599
600 $menu = new CRM_Core_DAO_Menu();
601 $menu->query($query);
602
603 self::$_menuCache = array();
604 $menuPath = NULL;
605 while ($menu->fetch()) {
606 self::$_menuCache[$menu->path] = array();
607 CRM_Core_DAO::storeValues($menu, self::$_menuCache[$menu->path]);
608
609 // Move module_data into main item.
610 if (isset(self::$_menuCache[$menu->path]['module_data'])) {
611 CRM_Utils_Array::extend(self::$_menuCache[$menu->path],
612 unserialize(self::$_menuCache[$menu->path]['module_data']));
613 unset(self::$_menuCache[$menu->path]['module_data']);
614 }
615
616 // Unserialize other elements.
617 foreach (self::$_serializedElements as $element) {
618 self::$_menuCache[$menu->path][$element] = unserialize($menu->$element);
619
620 if (strpos($path, $menu->path) !== FALSE) {
621 $menuPath = &self::$_menuCache[$menu->path];
622 }
623 }
624 }
625
626 if (strstr($path, 'report/instance')) {
627 $args = explode('/', $path);
628 if (is_numeric(end($args))) {
629 $menuPath['path'] .= '/' . end($args);
630 }
631 }
632
633 // *FIXME* : hack for 4.1 -> 4.2 upgrades.
634 if (preg_match('/^civicrm\/(upgrade\/)?queue\//', $path)) {
635 CRM_Queue_Menu::alter($path, $menuPath);
636 }
637
638 // Part of upgrade framework but not run inside main upgrade because it deletes data
639 // Once we have another example of a 'cleanup' we should generalize the clause below so it grabs string
640 // which follows upgrade/ and checks for existence of a function in Cleanup class.
641 if ($path == 'civicrm/upgrade/cleanup425') {
642 $menuPath['page_callback'] = array('CRM_Upgrade_Page_Cleanup', 'cleanup425');
643 $menuPath['access_arguments'][0][] = 'administer CiviCRM';
644 $menuPath['access_callback'] = array('CRM_Core_Permission', 'checkMenu');
645 }
646
647 if (!empty($menuPath)) {
648 $i18n = CRM_Core_I18n::singleton();
649 $i18n->localizeTitles($menuPath);
650 }
651 return $menuPath;
652 }
653
654 /**
655 * @param $pathArgs
656 *
657 * @return mixed
658 */
659 public static function getArrayForPathArgs($pathArgs) {
660 if (!is_string($pathArgs)) {
661 return;
662 }
663 $args = array();
664
665 $elements = explode(',', $pathArgs);
666 foreach ($elements as $keyVal) {
667 list($key, $val) = explode('=', $keyVal, 2);
668 $arr[$key] = $val;
669 }
670
671 if (array_key_exists('urlToSession', $arr)) {
672 $urlToSession = array();
673
674 $params = explode(';', $arr['urlToSession']);
675 $count = 0;
676 foreach ($params as $keyVal) {
677 list($urlToSession[$count]['urlVar'],
678 $urlToSession[$count]['sessionVar'],
679 $urlToSession[$count]['type'],
680 $urlToSession[$count]['default']
681 ) = explode(':', $keyVal);
682 $count++;
683 }
684 $arr['urlToSession'] = $urlToSession;
685 }
686 return $arr;
687 }
688
689 }