Merge pull request #14793 from eileenmcnaughton/update_test
[civicrm-core.git] / CRM / Core / BAO / Navigation.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 *
30 * @package CRM
6b83d5bd 31 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
32 */
33class CRM_Core_BAO_Navigation extends CRM_Core_DAO_Navigation {
34
73538697
CW
35 // Number of characters in the menu js cache key
36 const CACHE_KEY_STRLEN = 8;
37
6a488035 38 /**
fe482240 39 * Class constructor.
6a488035 40 */
00be9182 41 public function __construct() {
6a488035
TO
42 parent::__construct();
43 }
44
45 /**
fe482240 46 * Update the is_active flag in the db.
6a488035 47 *
6a0b768e
TO
48 * @param int $id
49 * Id of the database record.
50 * @param bool $is_active
51 * Value we want to set the is_active field.
6a488035 52 *
8a4fede3 53 * @return bool
54 * true if we found and updated the object, else false
6a488035 55 */
00be9182 56 public static function setIsActive($id, $is_active) {
6a488035
TO
57 return CRM_Core_DAO::setFieldValue('CRM_Core_DAO_Navigation', $id, 'is_active', $is_active);
58 }
59
6a488035 60 /**
ffbff132 61 * Add/update navigation record.
6a488035 62 *
ffbff132 63 * @param array $params Submitted values
6a488035 64 *
ffbff132 65 * @return CRM_Core_DAO_Navigation
a6c01b45 66 * navigation object
6a488035 67 */
00be9182 68 public static function add(&$params) {
6a488035 69 $navigation = new CRM_Core_DAO_Navigation();
3523b615 70 if (empty($params['id'])) {
71 $params['is_active'] = CRM_Utils_Array::value('is_active', $params, FALSE);
72 $params['has_separator'] = CRM_Utils_Array::value('has_separator', $params, FALSE);
7a487c63 73 $params['domain_id'] = CRM_Utils_Array::value('domain_id', $params, CRM_Core_Config::domainID());
3523b615 74 }
6a488035
TO
75
76 if (!isset($params['id']) ||
481a74f4 77 (CRM_Utils_Array::value('parent_id', $params) != CRM_Utils_Array::value('current_parent_id', $params))
6a488035
TO
78 ) {
79 /* re/calculate the weight, if the Parent ID changed OR create new menu */
80
81 if ($navName = CRM_Utils_Array::value('name', $params)) {
82 $params['name'] = $navName;
83 }
c5e077b2
PJ
84 elseif ($navLabel = CRM_Utils_Array::value('label', $params)) {
85 $params['name'] = $navLabel;
6a488035
TO
86 }
87
88 $params['weight'] = self::calculateWeight(CRM_Utils_Array::value('parent_id', $params));
89 }
90
c5e077b2 91 if (array_key_exists('permission', $params) && is_array($params['permission'])) {
6a488035
TO
92 $params['permission'] = implode(',', $params['permission']);
93 }
94
95 $navigation->copyValues($params);
96
6a488035
TO
97 $navigation->save();
98 return $navigation;
99 }
100
101 /**
fe482240 102 * Fetch object based on array of properties.
6a488035 103 *
6a0b768e
TO
104 * @param array $params
105 * (reference ) an assoc array of name/value pairs.
106 * @param array $defaults
107 * (reference ) an assoc array to hold the flattened values.
6a488035 108 *
16b10e64
CW
109 * @return CRM_Core_BAO_Navigation|null
110 * object on success, NULL otherwise
6a488035 111 */
00be9182 112 public static function retrieve(&$params, &$defaults) {
6a488035
TO
113 $navigation = new CRM_Core_DAO_Navigation();
114 $navigation->copyValues($params);
115
116 $navigation->domain_id = CRM_Core_Config::domainID();
117
118 if ($navigation->find(TRUE)) {
119 CRM_Core_DAO::storeValues($navigation, $defaults);
120 return $navigation;
121 }
122 return NULL;
123 }
124
125 /**
fe482240 126 * Calculate navigation weight.
6a488035 127 *
5a4f6742 128 * @param int $parentID
6a0b768e 129 * Parent_id of a menu.
5a4f6742 130 * @param int $menuID
6a0b768e 131 * Menu id.
6a488035 132 *
a6c01b45
CW
133 * @return int
134 * $weight string
6a488035 135 */
00be9182 136 public static function calculateWeight($parentID = NULL, $menuID = NULL) {
6a488035
TO
137 $domainID = CRM_Core_Config::domainID();
138
139 $weight = 1;
140 // we reset weight for each parent, i.e we start from 1 to n
141 // calculate max weight for top level menus, if parent id is absent
142 if (!$parentID) {
143 $query = "SELECT max(weight) as weight FROM civicrm_navigation WHERE parent_id IS NULL AND domain_id = $domainID";
144 }
145 else {
146 // if parent is passed, we need to get max weight for that particular parent
147 $query = "SELECT max(weight) as weight FROM civicrm_navigation WHERE parent_id = {$parentID} AND domain_id = $domainID";
148 }
149
150 $dao = CRM_Core_DAO::executeQuery($query);
151 $dao->fetch();
152 return $weight = $weight + $dao->weight;
153 }
154
155 /**
fe482240 156 * Get formatted menu list.
6a488035 157 *
a6c01b45
CW
158 * @return array
159 * returns associated array
6a488035 160 */
00be9182 161 public static function getNavigationList() {
f4de748f 162 $cacheKeyString = "navigationList_" . CRM_Core_Config::domainID();
6a488035
TO
163 $whereClause = '';
164
165 $config = CRM_Core_Config::singleton();
166
167 // check if we can retrieve from database cache
168 $navigations = CRM_Core_BAO_Cache::getItem('navigation', $cacheKeyString);
169
170 if (!$navigations) {
171 $domainID = CRM_Core_Config::domainID();
172 $query = "
173SELECT id, label, parent_id, weight, is_active, name
a8911fe3 174FROM civicrm_navigation WHERE domain_id = $domainID";
6a488035
TO
175 $result = CRM_Core_DAO::executeQuery($query);
176
be2fb01f 177 $pidGroups = [];
6a488035
TO
178 while ($result->fetch()) {
179 $pidGroups[$result->parent_id][$result->label] = $result->id;
180 }
181
182 foreach ($pidGroups[''] as $label => $val) {
183 $pidGroups[''][$label] = self::_getNavigationValue($val, $pidGroups);
184 }
185
be2fb01f 186 $navigations = [];
6a488035
TO
187 self::_getNavigationLabel($pidGroups[''], $navigations);
188
189 CRM_Core_BAO_Cache::setItem($navigations, 'navigation', $cacheKeyString);
190 }
191 return $navigations;
192 }
193
194 /**
ffbff132 195 * Helper function for getNavigationList().
6a488035 196 *
6a0b768e
TO
197 * @param array $list
198 * Menu info.
199 * @param array $navigations
200 * Navigation menus.
201 * @param string $separator
202 * Menu separator.
6a488035 203 */
00be9182 204 public static function _getNavigationLabel($list, &$navigations, $separator = '') {
6a488035
TO
205 $i18n = CRM_Core_I18n::singleton();
206 foreach ($list as $label => $val) {
207 if ($label == 'navigation_id') {
208 continue;
209 }
be2fb01f 210 $translatedLabel = $i18n->crm_translate($label, ['context' => 'menu']);
6a488035
TO
211 $navigations[is_array($val) ? $val['navigation_id'] : $val] = "{$separator}{$translatedLabel}";
212 if (is_array($val)) {
213 self::_getNavigationLabel($val, $navigations, $separator . '&nbsp;&nbsp;&nbsp;&nbsp;');
214 }
215 }
216 }
217
218 /**
ffbff132 219 * Helper function for getNavigationList().
6a488035 220 *
6a0b768e
TO
221 * @param string $val
222 * Menu name.
223 * @param array $pidGroups
224 * Parent menus.
ffbff132 225 *
6a488035
TO
226 * @return array
227 */
00be9182 228 public static function _getNavigationValue($val, &$pidGroups) {
6a488035 229 if (array_key_exists($val, $pidGroups)) {
be2fb01f 230 $list = ['navigation_id' => $val];
6a488035
TO
231 foreach ($pidGroups[$val] as $label => $id) {
232 $list[$label] = self::_getNavigationValue($id, $pidGroups);
233 }
234 unset($pidGroups[$val]);
235 return $list;
236 }
237 else {
238 return $val;
239 }
240 }
241
242 /**
fe482240 243 * Build navigation tree.
6a488035 244 *
a6c01b45
CW
245 * @return array
246 * nested array of menus
6a488035 247 */
656e5c5b 248 public static function buildNavigationTree() {
6a488035 249 $domainID = CRM_Core_Config::domainID();
be2fb01f 250 $navigationTree = [];
6a488035 251
c88f9a4d
O
252 $navigationMenu = new self();
253 $navigationMenu->domain_id = $domainID;
254 $navigationMenu->orderBy('parent_id, weight');
255 $navigationMenu->find();
256
257 while ($navigationMenu->fetch()) {
be2fb01f
CW
258 $navigationTree[$navigationMenu->id] = [
259 'attributes' => [
c88f9a4d
O
260 'label' => $navigationMenu->label,
261 'name' => $navigationMenu->name,
262 'url' => $navigationMenu->url,
263 'icon' => $navigationMenu->icon,
9f51bd16 264 'weight' => $navigationMenu->weight,
c88f9a4d
O
265 'permission' => $navigationMenu->permission,
266 'operator' => $navigationMenu->permission_operator,
267 'separator' => $navigationMenu->has_separator,
268 'parentID' => $navigationMenu->parent_id,
269 'navID' => $navigationMenu->id,
270 'active' => $navigationMenu->is_active,
be2fb01f
CW
271 ],
272 ];
6a488035
TO
273 }
274
656e5c5b 275 return self::buildTree($navigationTree);
6a488035
TO
276 }
277
278 /**
656e5c5b 279 * Convert flat array to nested.
6a488035 280 *
656e5c5b
CW
281 * @param array $elements
282 * @param int|null $parentId
283 *
284 * @return array
285 */
286 private static function buildTree($elements, $parentId = NULL) {
be2fb01f 287 $branch = [];
656e5c5b
CW
288
289 foreach ($elements as $id => $element) {
290 if ($element['attributes']['parentID'] == $parentId) {
291 $children = self::buildTree($elements, $id);
292 if ($children) {
293 $element['child'] = $children;
294 }
295 $branch[$id] = $element;
296 }
297 }
298
299 return $branch;
300 }
301
9f51bd16
MM
302 /**
303 * buildNavigationTree retreives items in order. We call this function to
304 * ensure that any items added by the hook are also in the correct order.
305 */
c7e39a79 306 public static function orderByWeight(&$navigations) {
9f51bd16
MM
307 // sort each item in navigations by weight
308 usort($navigations, function($a, $b) {
309
310 // If no weight have been defined for an item put it at the end of the list
311 if (!isset($a['attributes']['weight'])) {
312 $a['attributes']['weight'] = 1000;
313 }
314 if (!isset($b['attributes']['weight'])) {
315 $b['attributes']['weight'] = 1000;
316 }
317 return $a['attributes']['weight'] - $b['attributes']['weight'];
318 });
319
320 // If any of the $navigations have children, recurse
321 foreach ($navigations as $navigation) {
322 if (isset($navigation['child'])) {
323 self::orderByWeight($navigation['child']);
324 }
325 }
326 }
327
6a488035 328 /**
fe482240 329 * Recursively check child menus.
6a488035
TO
330 *
331 * @param array $value
332 * @param string $navigationString
656e5c5b 333 * @param array $skipMenuItems
ffbff132 334 *
6a488035
TO
335 * @return string
336 */
656e5c5b
CW
337 public static function recurseNavigation(&$value, &$navigationString, $skipMenuItems) {
338 if (!empty($value['child'])) {
339 $navigationString .= '<ul>';
6a488035
TO
340 }
341 else {
656e5c5b
CW
342 $navigationString .= '</li>';
343 //locate separator after
344 if (isset($value['attributes']['separator']) && $value['attributes']['separator'] == 1) {
345 $navigationString .= '<li class="menu-separator"></li>';
6a488035 346 }
656e5c5b 347 }
6a488035 348
656e5c5b
CW
349 if (!empty($value['child'])) {
350 foreach ($value['child'] as $val) {
351 $name = self::getMenuName($val, $skipMenuItems);
352 if ($name) {
353 //locate separator before
354 if (isset($val['attributes']['separator']) && $val['attributes']['separator'] == 2) {
355 $navigationString .= '<li class="menu-separator"></li>';
6a488035 356 }
be2fb01f 357 $removeCharacters = ['/', '!', '&', '*', ' ', '(', ')', '.'];
656e5c5b
CW
358 $navigationString .= '<li class="crm-' . str_replace($removeCharacters, '_', $val['attributes']['label']) . '">' . $name;
359 self::recurseNavigation($val, $navigationString, $skipMenuItems);
6a488035
TO
360 }
361 }
656e5c5b
CW
362 }
363 if (!empty($value['child'])) {
364 $navigationString .= '</ul></li>';
365 if (isset($value['attributes']['separator']) && $value['attributes']['separator'] == 1) {
366 $navigationString .= '<li class="menu-separator"></li>';
6a488035
TO
367 }
368 }
369 return $navigationString;
370 }
371
b774b065
TO
372 /**
373 * Given a navigation menu, generate navIDs for any items which are
374 * missing them.
375 *
376 * @param array $nodes
377 * Each key is a numeral; each value is a node in
378 * the menu tree (with keys "child" and "attributes").
379 */
380 public static function fixNavigationMenu(&$nodes) {
381 $maxNavID = 1;
382 array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) {
383 if ($key === 'navID') {
384 $maxNavID = max($maxNavID, $item);
385 }
386 });
387 self::_fixNavigationMenu($nodes, $maxNavID, NULL);
388 }
389
390 /**
391 * @param array $nodes
392 * Each key is a numeral; each value is a node in
393 * the menu tree (with keys "child" and "attributes").
ea3ddccf 394 * @param int $maxNavID
395 * @param int $parentID
b774b065
TO
396 */
397 private static function _fixNavigationMenu(&$nodes, &$maxNavID, $parentID) {
398 $origKeys = array_keys($nodes);
399 foreach ($origKeys as $origKey) {
400 if (!isset($nodes[$origKey]['attributes']['parentID']) && $parentID !== NULL) {
401 $nodes[$origKey]['attributes']['parentID'] = $parentID;
402 }
403 // If no navID, then assign navID and fix key.
404 if (!isset($nodes[$origKey]['attributes']['navID'])) {
405 $newKey = ++$maxNavID;
406 $nodes[$origKey]['attributes']['navID'] = $newKey;
f5d3c41c
JM
407 if ($origKey != $newKey) {
408 // If the keys are different, reset the array index to match.
409 $nodes[$newKey] = $nodes[$origKey];
410 unset($nodes[$origKey]);
411 $origKey = $newKey;
412 }
b774b065
TO
413 }
414 if (isset($nodes[$origKey]['child']) && is_array($nodes[$origKey]['child'])) {
415 self::_fixNavigationMenu($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']);
416 }
417 }
418 }
419
6a488035 420 /**
c7e39a79 421 * Check permissions and format menu item as html.
6a488035
TO
422 *
423 * @param $value
100fef9d 424 * @param array $skipMenuItems
ffbff132 425 *
6a488035
TO
426 * @return bool|string
427 */
00be9182 428 public static function getMenuName(&$value, &$skipMenuItems) {
6a488035
TO
429 // we need to localise the menu labels (CRM-5456) and don’t
430 // want to use ts() as it would throw the ts-extractor off
431 $i18n = CRM_Core_I18n::singleton();
432
be2fb01f 433 $name = $i18n->crm_translate($value['attributes']['label'], ['context' => 'menu']);
29c4b4bf 434 $url = CRM_Utils_Array::value('url', $value['attributes']);
29c4b4bf
WA
435 $parentID = CRM_Utils_Array::value('parentID', $value['attributes']);
436 $navID = CRM_Utils_Array::value('navID', $value['attributes']);
437 $active = CRM_Utils_Array::value('active', $value['attributes']);
353ffa53 438 $target = CRM_Utils_Array::value('target', $value['attributes']);
6a488035 439
c7e39a79 440 if (in_array($parentID, $skipMenuItems) || !$active || !self::checkPermission($value['attributes'])) {
6a488035
TO
441 $skipMenuItems[] = $navID;
442 return FALSE;
443 }
444
6a488035 445 $makeLink = FALSE;
45d70ef5 446 if (!empty($url)) {
0f1c7462 447 $url = self::makeFullyFormedUrl($url);
6a488035
TO
448 $makeLink = TRUE;
449 }
450
c7e39a79
CW
451 if (!empty($value['attributes']['icon'])) {
452 $menuIcon = sprintf('<i class="%s"></i>', $value['attributes']['icon']);
453 $name = $menuIcon . $name;
6a488035
TO
454 }
455
c7e39a79
CW
456 if ($makeLink) {
457 $url = CRM_Utils_System::evalUrl($url);
458 if ($target) {
459 $name = "<a href=\"{$url}\" target=\"{$target}\">{$name}</a>";
460 }
461 else {
462 $name = "<a href=\"{$url}\">{$name}</a>";
463 }
464 }
6a488035 465
c7e39a79
CW
466 return $name;
467 }
468
469 /**
470 * Check if a menu item should be visible based on permissions and component.
471 *
472 * @param $item
473 * @return bool
474 */
475 public static function checkPermission($item) {
476 if (!empty($item['permission'])) {
477 $permissions = explode(',', $item['permission']);
478 $operator = CRM_Utils_Array::value('operator', $item);
6a488035
TO
479 $hasPermission = FALSE;
480 foreach ($permissions as $key) {
481 $key = trim($key);
482 $showItem = TRUE;
483
484 //get the component name from permission.
485 $componentName = CRM_Core_Permission::getComponentName($key);
486
487 if ($componentName) {
c7e39a79 488 if (!in_array($componentName, CRM_Core_Config::singleton()->enableComponents) ||
6a488035
TO
489 !CRM_Core_Permission::check($key)
490 ) {
491 $showItem = FALSE;
492 if ($operator == 'AND') {
c7e39a79 493 return FALSE;
6a488035
TO
494 }
495 }
496 else {
497 $hasPermission = TRUE;
498 }
499 }
500 elseif (!CRM_Core_Permission::check($key)) {
501 $showItem = FALSE;
502 if ($operator == 'AND') {
c7e39a79 503 return FALSE;
6a488035
TO
504 }
505 }
506 else {
507 $hasPermission = TRUE;
508 }
509 }
510
c7e39a79 511 if (empty($showItem) && !$hasPermission) {
6a488035
TO
512 return FALSE;
513 }
514 }
c7e39a79 515 return TRUE;
6a488035
TO
516 }
517
0f1c7462
DA
518 /**
519 * Turns relative URLs (like civicrm/foo/bar) into fully-formed
520 * ones (i.e. example.com/wp-admin?q=civicrm/dashboard).
521 *
522 * If the URL is already fully-formed, nothing will be done.
523 *
524 * @param string $url
525 *
526 * @return string
527 */
c7e39a79 528 public static function makeFullyFormedUrl($url) {
0f1c7462
DA
529 if (self::isNotFullyFormedUrl($url)) {
530 //CRM-7656 --make sure to separate out url path from url params,
531 //as we'r going to validate url path across cross-site scripting.
532 $path = parse_url($url, PHP_URL_PATH);
533 $q = parse_url($url, PHP_URL_QUERY);
534 $fragment = parse_url($url, PHP_URL_FRAGMENT);
535 return CRM_Utils_System::url($path, $q, FALSE, $fragment);
536 }
537
538 if (strpos($url, '&amp;') === FALSE) {
539 return htmlspecialchars($url);
540 }
541
542 return $url;
543 }
544
545 /**
546 * Checks if the given URL is not fully-formed
547 *
548 * @param string $url
549 *
550 * @return bool
551 */
552 private static function isNotFullyFormedUrl($url) {
553 return substr($url, 0, 4) !== 'http' && $url[0] !== '/' && $url[0] !== '#';
554 }
555
73538697 556 /**
fe482240 557 * Reset navigation for all contacts or a specified contact.
73538697 558 *
6a0b768e
TO
559 * @param int $contactID
560 * Reset only entries belonging to that contact ID.
ffbff132 561 *
73538697
CW
562 * @return string
563 */
00be9182 564 public static function resetNavigation($contactID = NULL) {
73538697
CW
565 $newKey = CRM_Utils_String::createRandom(self::CACHE_KEY_STRLEN, CRM_Utils_String::ALPHANUMERIC);
566 if (!$contactID) {
290110ef
TO
567 $ser = serialize($newKey);
568 $query = "UPDATE civicrm_setting SET value = '$ser' WHERE name='navigation' AND contact_id IS NOT NULL";
73538697
CW
569 CRM_Core_DAO::executeQuery($query);
570 CRM_Core_BAO_Cache::deleteGroup('navigation');
571 }
572 else {
6a488035 573 // before inserting check if contact id exists in db
73538697 574 // this is to handle weird case when contact id is in session but not in db
6a488035
TO
575 $contact = new CRM_Contact_DAO_Contact();
576 $contact->id = $contactID;
577 if ($contact->find(TRUE)) {
578 CRM_Core_BAO_Setting::setItem(
73538697 579 $newKey,
6a488035
TO
580 CRM_Core_BAO_Setting::PERSONAL_PREFERENCES_NAME,
581 'navigation',
582 NULL,
583 $contactID,
584 $contactID
585 );
586 }
587 }
73538697
CW
588
589 return $newKey;
6a488035
TO
590 }
591
592 /**
fe482240 593 * Process navigation.
6a488035 594 *
6a0b768e
TO
595 * @param array $params
596 * Associated array, $_GET.
6a488035 597 */
00be9182 598 public static function processNavigation(&$params) {
353ffa53
TO
599 $nodeID = (int) str_replace("node_", "", $params['id']);
600 $referenceID = (int) str_replace("node_", "", $params['ref_id']);
601 $position = $params['ps'];
602 $type = $params['type'];
603 $label = CRM_Utils_Array::value('data', $params);
6a488035
TO
604
605 switch ($type) {
606 case "move":
607 self::processMove($nodeID, $referenceID, $position);
608 break;
609
610 case "rename":
611 self::processRename($nodeID, $label);
612 break;
613
614 case "delete":
615 self::processDelete($nodeID);
616 break;
617 }
618
619 //reset navigation menus
620 self::resetNavigation();
621 CRM_Utils_System::civiExit();
622 }
623
624 /**
fe482240 625 * Process move action.
6a488035 626 *
6a0b768e
TO
627 * @param $nodeID
628 * Node that is being moved.
629 * @param $referenceID
630 * Parent id where node is moved. 0 mean no parent.
631 * @param $position
632 * New position of the nod, it starts with 0 - n.
6a488035 633 */
00be9182 634 public static function processMove($nodeID, $referenceID, $position) {
6a488035
TO
635 // based on the new position we need to get the weight of the node after moved node
636 // 1. update the weight of $position + 1 nodes to weight + 1
637 // 2. weight of the ( $position -1 ) node - 1 is the new weight of the node being moved
638
639 // check if there is parent id, which means node is moved inside existing parent container, so use parent id
640 // to find the correct position else use NULL to get the weights of parent ( $position - 1 )
641 // accordingly set the new parent_id
642 if ($referenceID) {
643 $newParentID = $referenceID;
644 $parentClause = "parent_id = {$referenceID} ";
645 }
646 else {
647 $newParentID = 'NULL';
648 $parentClause = 'parent_id IS NULL';
649 }
650
4eeb9a5b 651 $incrementOtherNodes = TRUE;
353ffa53 652 $sql = "SELECT weight from civicrm_navigation WHERE {$parentClause} ORDER BY weight LIMIT %1, 1";
be2fb01f 653 $params = [1 => [$position, 'Positive']];
6a488035
TO
654 $newWeight = CRM_Core_DAO::singleValueQuery($sql, $params);
655
656 // this means node is moved to last position, so you need to get the weight of last element + 1
657 if (!$newWeight) {
7cc5305f
CW
658 // If this is not the first item being added to a parent
659 if ($position) {
660 $lastPosition = $position - 1;
661 $sql = "SELECT weight from civicrm_navigation WHERE {$parentClause} ORDER BY weight LIMIT %1, 1";
be2fb01f 662 $params = [1 => [$lastPosition, 'Positive']];
7cc5305f
CW
663 $newWeight = CRM_Core_DAO::singleValueQuery($sql, $params);
664
665 // since last node increment + 1
666 $newWeight = $newWeight + 1;
667 }
668 else {
669 $newWeight = '0';
670 }
6a488035
TO
671
672 // since this is a last node we don't need to increment other nodes
ab8a593e 673 $incrementOtherNodes = FALSE;
6a488035
TO
674 }
675
676 $transaction = new CRM_Core_Transaction();
677
678 // now update the existing nodes to weight + 1, if required.
481a74f4 679 if ($incrementOtherNodes) {
6a488035
TO
680 $query = "UPDATE civicrm_navigation SET weight = weight + 1
681 WHERE {$parentClause} AND weight >= {$newWeight}";
682
683 CRM_Core_DAO::executeQuery($query);
684 }
685
686 // finally set the weight of current node
687 $query = "UPDATE civicrm_navigation SET weight = {$newWeight}, parent_id = {$newParentID} WHERE id = {$nodeID}";
688 CRM_Core_DAO::executeQuery($query);
689
690 $transaction->commit();
691 }
692
693 /**
ffbff132 694 * Function to process rename action for tree.
6a488035 695 *
100fef9d 696 * @param int $nodeID
6a488035
TO
697 * @param $label
698 */
00be9182 699 public static function processRename($nodeID, $label) {
6a488035
TO
700 CRM_Core_DAO::setFieldValue('CRM_Core_DAO_Navigation', $nodeID, 'label', $label);
701 }
702
703 /**
fe482240 704 * Process delete action for tree.
6a488035 705 *
100fef9d 706 * @param int $nodeID
6a488035 707 */
00be9182 708 public static function processDelete($nodeID) {
6a488035
TO
709 $query = "DELETE FROM civicrm_navigation WHERE id = {$nodeID}";
710 CRM_Core_DAO::executeQuery($query);
711 }
712
6a488035 713 /**
fe482240 714 * Update menu.
6a488035 715 *
6a0b768e
TO
716 * @param array $params
717 * @param array $newParams
718 * New value of params.
6a488035 719 */
00be9182 720 public static function processUpdate($params, $newParams) {
6a488035
TO
721 $dao = new CRM_Core_DAO_Navigation();
722 $dao->copyValues($params);
723 if ($dao->find(TRUE)) {
724 $dao->copyValues($newParams);
725 $dao->save();
726 }
727 }
73538697 728
31cb1898
AN
729 /**
730 * Rebuild reports menu.
731 *
732 * All Contact reports will become sub-items of 'Contact Reports' and so on.
733 *
734 * @param int $domain_id
735 */
736 public static function rebuildReportsNavigation($domain_id) {
be2fb01f 737 $component_to_nav_name = [
31cb1898
AN
738 'CiviContact' => 'Contact Reports',
739 'CiviContribute' => 'Contribution Reports',
740 'CiviMember' => 'Membership Reports',
741 'CiviEvent' => 'Event Reports',
742 'CiviPledge' => 'Pledge Reports',
743 'CiviGrant' => 'Grant Reports',
744 'CiviMail' => 'Mailing Reports',
745 'CiviCampaign' => 'Campaign Reports',
be2fb01f 746 ];
31cb1898
AN
747
748 // Create or update the top level Reports link.
749 $reports_nav = self::createOrUpdateTopLevelReportsNavItem($domain_id);
750
751 // Get all active report instances grouped by component.
752 $components = self::getAllActiveReportsByComponent($domain_id);
753 foreach ($components as $component_id => $component) {
754 // Create or update the per component reports links.
755 $component_nav_name = $component['name'];
756 if (isset($component_to_nav_name[$component_nav_name])) {
757 $component_nav_name = $component_to_nav_name[$component_nav_name];
758 }
759 $permission = "access {$component['name']}";
760 if ($component['name'] === 'CiviContact') {
761 $permission = "administer CiviCRM";
762 }
763 elseif ($component['name'] === 'CiviCampaign') {
764 $permission = "access CiviReport";
765 }
0087242f 766 $component_nav = self::createOrUpdateReportNavItem($component_nav_name, 'civicrm/report/list',
767 "compid={$component_id}&reset=1", $reports_nav->id, $permission, $domain_id, TRUE);
31cb1898
AN
768 foreach ($component['reports'] as $report_id => $report) {
769 // Create or update the report instance links.
51704c4d 770 $report_nav = self::createOrUpdateReportNavItem($report['title'], $report['url'], 'reset=1', $component_nav->id, $report['permission'], $domain_id, FALSE, TRUE);
31cb1898
AN
771 // Update the report instance to include the navigation id.
772 $query = "UPDATE civicrm_report_instance SET navigation_id = %1 WHERE id = %2";
be2fb01f
CW
773 $params = [
774 1 => [$report_nav->id, 'Integer'],
775 2 => [$report_id, 'Integer'],
776 ];
31cb1898
AN
777 CRM_Core_DAO::executeQuery($query, $params);
778 }
779 }
780
781 // Create or update the All Reports link.
dfbeefd8 782 self::createOrUpdateReportNavItem('All Reports', 'civicrm/report/list', 'reset=1', $reports_nav->id, 'access CiviReport', $domain_id, TRUE);
e24131b6 783 // Create or update the My Reports link.
784 self::createOrUpdateReportNavItem('My Reports', 'civicrm/report/list', 'myreports=1&reset=1', $reports_nav->id, 'access CiviReport', $domain_id, TRUE);
785
31cb1898
AN
786 }
787
788 /**
789 * Create the top level 'Reports' item in the navigation tree.
790 *
791 * @param int $domain_id
792 *
793 * @return bool|\CRM_Core_DAO
794 */
518fa0ee 795 public static function createOrUpdateTopLevelReportsNavItem($domain_id) {
31cb1898
AN
796 $id = NULL;
797
798 $dao = new CRM_Core_BAO_Navigation();
799 $dao->name = 'Reports';
800 $dao->domain_id = $domain_id;
801 // The first selectAdd clears it - so that we only retrieve the one field.
802 $dao->selectAdd();
803 $dao->selectAdd('id');
804 if ($dao->find(TRUE)) {
805 $id = $dao->id;
806 }
807
808 $nav = self::createReportNavItem('Reports', NULL, NULL, NULL, 'access CiviReport', $id, $domain_id);
809 return $nav;
810 }
811
812 /**
813 * Retrieve a navigation item using it's url.
814 *
51704c4d 815 * Note that we use LIKE to permit a wildcard as the calling code likely doesn't
816 * care about output params appended.
817 *
31cb1898
AN
818 * @param string $url
819 * @param array $url_params
820 *
0087242f 821 * @param int|null $parent_id
822 * Optionally restrict to one parent.
823 *
824 * @return bool|\CRM_Core_BAO_Navigation
31cb1898 825 */
0087242f 826 public static function getNavItemByUrl($url, $url_params, $parent_id = NULL) {
827 $nav = new CRM_Core_BAO_Navigation();
0087242f 828 $nav->parent_id = $parent_id;
51704c4d 829 $nav->whereAdd("url LIKE '{$url}?{$url_params}'");
830
0087242f 831 if ($nav->find(TRUE)) {
832 return $nav;
31cb1898
AN
833 }
834 return FALSE;
835 }
836
837 /**
838 * Get all active reports, organised by component.
839 *
840 * @param int $domain_id
841 *
842 * @return array
843 */
844 public static function getAllActiveReportsByComponent($domain_id) {
845 $sql = "
846 SELECT
847 civicrm_report_instance.id, civicrm_report_instance.title, civicrm_report_instance.permission, civicrm_component.name, civicrm_component.id AS component_id
848 FROM
849 civicrm_option_group
850 LEFT JOIN
851 civicrm_option_value ON civicrm_option_value.option_group_id = civicrm_option_group.id AND civicrm_option_group.name = 'report_template'
852 LEFT JOIN
853 civicrm_report_instance ON civicrm_option_value.value = civicrm_report_instance.report_id
854 LEFT JOIN
855 civicrm_component ON civicrm_option_value.component_id = civicrm_component.id
856 WHERE
857 civicrm_option_value.is_active = 1
858 AND
859 civicrm_report_instance.domain_id = %1
860 ORDER BY civicrm_option_value.weight";
861
be2fb01f
CW
862 $dao = CRM_Core_DAO::executeQuery($sql, [
863 1 => [$domain_id, 'Integer'],
864 ]);
865 $rows = [];
31cb1898
AN
866 while ($dao->fetch()) {
867 $component_name = is_null($dao->name) ? 'CiviContact' : $dao->name;
868 $component_id = is_null($dao->component_id) ? 99 : $dao->component_id;
869 $rows[$component_id]['name'] = $component_name;
be2fb01f 870 $rows[$component_id]['reports'][$dao->id] = [
31cb1898
AN
871 'title' => $dao->title,
872 'url' => "civicrm/report/instance/{$dao->id}",
873 'permission' => $dao->permission,
be2fb01f 874 ];
31cb1898
AN
875 }
876 return $rows;
877 }
878
879 /**
880 * Create or update a navigation item for a report instance.
881 *
882 * The function will check whether create or update is required.
883 *
884 * @param string $name
885 * @param string $url
886 * @param string $url_params
887 * @param int $parent_id
888 * @param string $permission
889 * @param int $domain_id
890 *
0087242f 891 * @param bool $onlyMatchParentID
892 * If True then do not match with a url that has a different parent
893 * (This is because for top level items there is a risk of 'stealing' rows that normally
894 * live under 'Contact' and intentionally duplicate the report examples.)
518fa0ee 895 * @param bool $useWildcard
31cb1898
AN
896 * @return \CRM_Core_DAO_Navigation
897 */
0087242f 898 protected static function createOrUpdateReportNavItem($name, $url, $url_params, $parent_id, $permission,
51704c4d 899 $domain_id, $onlyMatchParentID = FALSE, $useWildcard = TRUE) {
31cb1898 900 $id = NULL;
51704c4d 901 $existing_url_params = $useWildcard ? $url_params . '%' : $url_params;
902 $existing_nav = CRM_Core_BAO_Navigation::getNavItemByUrl($url, $existing_url_params, ($onlyMatchParentID ? $parent_id : NULL));
31cb1898
AN
903 if ($existing_nav) {
904 $id = $existing_nav->id;
905 }
0087242f 906
31cb1898
AN
907 $nav = self::createReportNavItem($name, $url, $url_params, $parent_id, $permission, $id, $domain_id);
908 return $nav;
909 }
910
911 /**
912 * Create a navigation item for a report instance.
913 *
914 * @param string $name
915 * @param string $url
916 * @param string $url_params
917 * @param int $parent_id
918 * @param string $permission
919 * @param int $id
920 * @param int $domain_id
921 * ID of domain to create item in.
922 *
923 * @return \CRM_Core_DAO_Navigation
924 */
925 public static function createReportNavItem($name, $url, $url_params, $parent_id, $permission, $id, $domain_id) {
926 if ($url !== NULL) {
927 $url = "{$url}?{$url_params}";
928 }
be2fb01f 929 $params = [
31cb1898
AN
930 'name' => $name,
931 'label' => ts($name),
932 'url' => $url,
933 'parent_id' => $parent_id,
934 'is_active' => TRUE,
be2fb01f 935 'permission' => [
31cb1898 936 $permission,
be2fb01f 937 ],
31cb1898 938 'domain_id' => $domain_id,
be2fb01f 939 ];
0087242f 940 if ($id) {
31cb1898
AN
941 $params['id'] = $id;
942 }
0087242f 943 return CRM_Core_BAO_Navigation::add($params);
31cb1898
AN
944 }
945
b5c2afd0 946 /**
ffbff132
EM
947 * Get cache key.
948 *
100fef9d 949 * @param int $cid
b5c2afd0
EM
950 *
951 * @return object|string
952 */
00be9182 953 public static function getCacheKey($cid) {
aaffa79f 954 $key = Civi::service('settings_manager')
955 ->getBagByContact(NULL, $cid)
956 ->get('navigation');
73538697
CW
957 if (strlen($key) !== self::CACHE_KEY_STRLEN) {
958 $key = self::resetNavigation($cid);
959 }
960 return $key;
961 }
96025800 962
b30809e4
CW
963 /**
964 * Unset menu items for disabled components and non-permissioned users
965 *
966 * @param $menu
967 */
968 public static function filterByPermission(&$menu) {
969 foreach ($menu as $key => $item) {
970 if (
971 (array_key_exists('active', $item['attributes']) && !$item['attributes']['active']) ||
972 !CRM_Core_BAO_Navigation::checkPermission($item['attributes'])
973 ) {
974 unset($menu[$key]);
975 continue;
976 }
977 if (!empty($item['child'])) {
978 self::filterByPermission($menu[$key]['child']);
979 }
980 }
981 }
982
983 /**
984 * @param array $menu
985 */
986 public static function buildHomeMenu(&$menu) {
987 foreach ($menu as &$item) {
988 if (CRM_Utils_Array::value('name', $item['attributes']) === 'Home') {
989 unset($item['attributes']['label'], $item['attributes']['url']);
990 $item['attributes']['icon'] = 'crm-logo-sm';
991 $item['attributes']['attr']['accesskey'] = 'm';
992 $item['child'] = [
993 [
994 'attributes' => [
995 'label' => 'CiviCRM Home',
996 'name' => 'CiviCRM Home',
997 'url' => 'civicrm/dashboard?reset=1',
998 'weight' => 1,
518fa0ee 999 ],
b30809e4
CW
1000 ],
1001 [
1002 'attributes' => [
1003 'label' => 'Hide Menu',
1004 'name' => 'Hide Menu',
1005 'url' => '#hidemenu',
1006 'weight' => 2,
518fa0ee 1007 ],
b30809e4
CW
1008 ],
1009 [
1010 'attributes' => [
1011 'label' => 'Log out',
1012 'name' => 'Log out',
1013 'url' => 'civicrm/logout?reset=1',
1014 'weight' => 3,
518fa0ee 1015 ],
b30809e4
CW
1016 ],
1017 ];
1018 return;
1019 }
1020 }
1021 }
1022
6a488035 1023}