Merge pull request #15168 from MegaphoneJon/class-fixes
[civicrm-core.git] / CRM / Core / Menu.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
6a488035
TO
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 * This file contains the various menus of the CiviCRM module
30 *
31 * @package CRM
6b83d5bd 32 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
33 */
34
35require_once 'CRM/Core/I18n.php';
28518c90
EM
36
37/**
ad37ac8e 38 * Class CRM_Core_Menu.
28518c90 39 */
6a488035
TO
40class CRM_Core_Menu {
41
42 /**
fe482240 43 * The list of menu items.
6a488035
TO
44 *
45 * @var array
6a488035 46 */
518fa0ee 47 public static $_items = NULL;
6a488035
TO
48
49 /**
fe482240 50 * The list of permissioned menu items.
6a488035
TO
51 *
52 * @var array
6a488035 53 */
518fa0ee 54 public static $_permissionedItems = NULL;
6a488035 55
518fa0ee 56 public static $_serializedElements = array(
6a488035
TO
57 'access_arguments',
58 'access_callback',
59 'page_arguments',
60 'page_callback',
61 'breadcrumb',
62 );
63
518fa0ee 64 public static $_menuCache = NULL;
7da04cde 65 const MENU_ITEM = 1;
6a488035 66
a0ee3941 67 /**
fe482240 68 * This function fetches the menu items from xml and xmlMenu hooks.
49d97c48 69 *
ad37ac8e 70 * @param bool $fetchFromXML
6a0b768e 71 * Fetch the menu items from xml and not from cache.
49d97c48 72 *
a0ee3941
EM
73 * @return array
74 */
00be9182 75 public static function &xmlItems($fetchFromXML = FALSE) {
49d97c48 76 if (!self::$_items || $fetchFromXML) {
6a488035
TO
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 }
7dc34fb8
TO
97
98 CRM_Utils_Hook::alterMenu(self::$_items);
6a488035
TO
99 }
100
101 return self::$_items;
102 }
103
a0ee3941 104 /**
ad37ac8e 105 * Read menu.
106 *
100fef9d 107 * @param string $name
5bfe764c
TO
108 * File name
109 * @param array $menu
110 * An alterable list of menu items.
a0ee3941
EM
111 *
112 * @throws Exception
113 */
00be9182 114 public static function read($name, &$menu) {
5bfe764c
TO
115 $xml = simplexml_load_file($name);
116 self::readXML($xml, $menu);
117 }
6a488035 118
5bfe764c
TO
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) {
6a488035 126 $config = CRM_Core_Config::singleton();
6a488035
TO
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);
4535c1f5
TO
135
136 if ($item->ids_arguments) {
137 $ids = array();
36d4fa1b
TO
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;
4535c1f5
TO
142 }
143 }
144 $menu[$path]['ids_arguments'] = $ids;
145 unset($item->ids_arguments);
146 }
147
6a488035
TO
148 foreach ($item as $key => $value) {
149 $key = (string ) $key;
150 $value = (string ) $value;
151 if (strpos($key, '_callback') &&
152 strpos($value, '::')
153 ) {
c8074a93
TO
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)).
6a488035
TO
156 $value = explode('::', $value);
157 }
158 elseif ($key == 'access_arguments') {
c8074a93 159 // FIXME Move the permission parser to its own class (or *maybe* CRM_Core_Permission).
6a488035
TO
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 /**
fe482240 190 * This function defines information for various menu items.
6a488035 191 *
ad37ac8e 192 * @param bool $fetchFromXML
6a0b768e 193 * Fetch the menu items from xml and not from cache.
49d97c48 194 *
7a9ab499 195 * @return array
6a488035 196 */
00be9182 197 public static function &items($fetchFromXML = FALSE) {
49d97c48 198 return self::xmlItems($fetchFromXML);
6a488035
TO
199 }
200
a0ee3941 201 /**
ad37ac8e 202 * Is array true (whatever that means!).
203 *
204 * @param array $values
a0ee3941
EM
205 *
206 * @return bool
207 */
00be9182 208 public static function isArrayTrue(&$values) {
6a488035
TO
209 foreach ($values as $name => $value) {
210 if (!$value) {
211 return FALSE;
212 }
213 }
214 return TRUE;
215 }
216
a0ee3941 217 /**
ad37ac8e 218 * Fill menu values.
219 *
220 * @param array $menu
221 * @param string $path
a0ee3941
EM
222 *
223 * @throws Exception
224 */
00be9182 225 public static function fillMenuValues(&$menu, $path) {
6a488035
TO
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 /**
fe482240 270 * We use this function to.
6a488035
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
ea3ddccf 276 *
277 * @param array $menu
6a488035 278 */
00be9182 279 public static function build(&$menu) {
6a488035
TO
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
a0ee3941 295 /**
fe482240 296 * This function recomputes menu from xml and populates civicrm_menu.
ad37ac8e 297 *
a0ee3941
EM
298 * @param bool $truncate
299 */
00be9182 300 public static function store($truncate = TRUE) {
6a488035
TO
301 // first clean up the db
302 if ($truncate) {
303 $query = 'TRUNCATE civicrm_menu';
304 CRM_Core_DAO::executeQuery($query);
305 }
49d97c48 306 $menuArray = self::items($truncate);
6a488035
TO
307
308 self::build($menuArray);
309
b44dc91e 310 $daoFields = CRM_Core_DAO_Menu::fields();
6a488035
TO
311
312 foreach ($menuArray as $path => $item) {
353ffa53
TO
313 $menu = new CRM_Core_DAO_Menu();
314 $menu->path = $path;
6a488035
TO
315 $menu->domain_id = CRM_Core_Config::domainID();
316
317 $menu->find(TRUE);
318
d6f1a16c 319 if (!CRM_Core_Config::isUpgradeMode() ||
eed7e803 320 CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_menu', 'module_data', FALSE)
d6f1a16c
NM
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 }
b44dc91e 329 }
b44dc91e 330
d6f1a16c
NM
331 $menu->module_data = serialize($module_data);
332 }
6a488035 333
c09129a5
NM
334 $menu->copyValues($item);
335
6a488035
TO
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
a0ee3941 351 /**
ad37ac8e 352 * Build admin links.
353 *
354 * @param array $menu
a0ee3941 355 */
00be9182 356 public static function buildAdminLinks(&$menu) {
6a488035
TO
357 $values = array();
358
359 foreach ($menu as $path => $item) {
a7488080 360 if (empty($item['adminGroup'])) {
6a488035
TO
361 continue;
362 }
363
0d8afee2 364 $query = !empty($item['path_arguments']) ? str_replace(',', '&', $item['path_arguments']) . '&reset=1' : 'reset=1';
6a488035
TO
365
366 $value = array(
367 'title' => $item['title'],
368 'desc' => CRM_Utils_Array::value('desc', $item),
369 'id' => strtr($item['title'], array(
518fa0ee
SL
370 '(' => '_',
371 ')' => '',
372 ' ' => '',
373 ',' => '_',
374 '/' => '_',
375 )),
d75f2f47 376 'url' => CRM_Utils_System::url($path, $query,
ad37ac8e 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
f4bdec6a 383 ),
6a488035
TO
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
a0ee3941 404 /**
ad37ac8e 405 * Get admin links.
406 *
a0ee3941
EM
407 * @return null
408 */
00be9182 409 public static function &getAdminLinks() {
6a488035
TO
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 *
6a0b768e
TO
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.
6a488035 429 *
a6c01b45
CW
430 * @return array
431 * The breadcrumb for this path
6a488035 432 */
00be9182 433 public static function buildBreadcrumb(&$menu, $path) {
6a488035
TO
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
b44e3f84 443 // when we come across breadcrumb which involves ids,
6a488035
TO
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 ) {
0d8afee2 453 $urlVar = !empty($menu[$currentPath]['path_arguments']) ? '&' . $menu[$currentPath]['path_arguments'] : '';
6a488035
TO
454 $crumbs[] = array(
455 'title' => $menu[$currentPath]['title'],
456 'url' => CRM_Utils_System::url($currentPath,
38a0e912 457 'reset=1' . $urlVar,
518fa0ee
SL
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
6a488035
TO
468 ),
469 );
470 }
471 }
472 $menu[$path]['breadcrumb'] = $crumbs;
473
474 return $crumbs;
475 }
476
a0ee3941
EM
477 /**
478 * @param $menu
479 * @param $path
480 */
00be9182 481 public static function buildReturnUrl(&$menu, $path) {
6a488035
TO
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
a0ee3941
EM
487 /**
488 * @param $menu
489 * @param $path
490 *
491 * @return array
492 */
00be9182 493 public static function getReturnUrl(&$menu, $path) {
6a488035
TO
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
a0ee3941
EM
517 /**
518 * @param $menu
519 * @param $path
520 */
00be9182 521 public static function fillComponentIds(&$menu, $path) {
6a488035
TO
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
a0ee3941 554 /**
35146d69 555 * @param $path string
2bc65854 556 * Path of menu item to retrieve.
a0ee3941 557 *
35146d69 558 * @return array
2bc65854 559 * Menu entry array.
a0ee3941 560 */
00be9182 561 public static function get($path) {
6a488035
TO
562 // return null if menu rebuild
563 $config = CRM_Core_Config::singleton();
564
6a488035
TO
565 $args = explode('/', $path);
566
567 $elements = array();
568 while (!empty($args)) {
353ffa53
TO
569 $string = implode('/', $args);
570 $string = CRM_Core_DAO::escapeString($string);
6a488035
TO
571 $elements[] = "'{$string}'";
572 array_pop($args);
573 }
574
353ffa53
TO
575 $queryString = implode(', ', $elements);
576 $domainID = CRM_Core_Config::domainID();
6a488035
TO
577
578 $query = "
579(
580 SELECT *
581 FROM civicrm_menu
582 WHERE path in ( $queryString )
99a2c00a 583 AND domain_id = $domainID
6a488035
TO
584 ORDER BY length(path) DESC
585 LIMIT 1
586)
587";
588
589 if ($path != 'navigation') {
590 $query .= "
591UNION (
592 SELECT *
593 FROM civicrm_menu
594 WHERE path IN ( 'navigation' )
99a2c00a 595 AND domain_id = $domainID
6a488035
TO
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
b44dc91e
TO
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.
6a488035
TO
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
6a488035
TO
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') {
2aa397bc 642 $menuPath['page_callback'] = array('CRM_Upgrade_Page_Cleanup', 'cleanup425');
6a488035
TO
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
a0ee3941
EM
654 /**
655 * @param $pathArgs
656 *
657 * @return mixed
658 */
00be9182 659 public static function getArrayForPathArgs($pathArgs) {
6a488035
TO
660 if (!is_string($pathArgs)) {
661 return;
662 }
663 $args = array();
664
665 $elements = explode(',', $pathArgs);
6a488035 666 foreach ($elements as $keyVal) {
3b4339fd 667 list($key, $val) = explode('=', $keyVal, 2);
6a488035
TO
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']
353ffa53 681 ) = explode(':', $keyVal);
6a488035
TO
682 $count++;
683 }
684 $arr['urlToSession'] = $urlToSession;
685 }
686 return $arr;
687 }
96025800 688
6a488035 689}