*/ protected $menuIcons = []; /** * Builds the menu, applies edits, renames * and orders the entries. * * @return array */ public function getMenu() { $this->buildMenu(); $this->applyEdits(); $this->applyRemoves(); $this->applyRenames(); $this->applyOrdering(); return $this->menu; } /** * lets you register a menu icon for a certain menu category to replace the default arrow icon. * * @param string $menuName The translation key of a main menu category, eg 'Dashboard_Dashboard' * @param string $iconCssClass The css class name of an icon, eg 'icon-user' * @return void */ public function registerMenuIcon($menuName, $iconCssClass) { $this->menuIcons[$menuName] = $iconCssClass; } /** * Returns a list of available plugin menu instances. * * @return \Piwik\Plugin\Menu[] */ protected function getAllMenus() { $cacheId = 'Menus.all'; $cache = Cache::getTransientCache(); if ($cache->contains($cacheId)) { return $cache->fetch($cacheId); } $components = PluginManager::getInstance()->findComponents('Menu', 'Piwik\\Plugin\\Menu'); $menus = []; foreach ($components as $component) { $menus[] = StaticContainer::get($component); } $cache->save($cacheId, $menus); return $menus; } /** * Adds a new entry to the menu. * * @param string $menuName The menu's category name. Can be a translation token. * @param null|string $subMenuName The menu item's name. Can be a translation token. * @param string|array $url The URL the admin menu entry should link to, or an array of query parameters * that can be used to build the URL. * @param int $order The order hint. * @param string|null|false $tooltip An optional tooltip to display or false to display the tooltip. * @param string|null|false $icon An icon classname, such as "icon-add". Only supported by admin menu * @param string|null|false $onclick Will execute the on click handler instead of executing the link. Only supported by admin menu. * @param string|null|false $attribute Will add this string as a link attribute. * @param string|null|false $help Will display a help icon that will pop a notification with help information. * @param int $badgeCount If non-zero then a badge will be overlaid on the icon showing the provided count * @param string $cssClass If a string is provided, it will be added as an extra CSS class to the menu item * @return void * @since 2.7.0 * @api */ public function addItem( string $menuName, ?string $subMenuName, $url, int $order = 50, $tooltip = false, $icon = false, $onclick = false, $attribute = false, $help = false, int $badgeCount = 0, string $cssClass = '' ) { // make sure the idSite value used is numeric (hack-y fix for #3426) if (isset($url['idSite']) && !is_numeric($url['idSite'])) { $idSites = API::getInstance()->getSitesIdWithAtLeastViewAccess(); $url['idSite'] = reset($idSites); } $this->menuEntries[] = [ $menuName, $subMenuName, $url, $order, $tooltip, $icon, $onclick, $attribute, $help, $badgeCount, $cssClass, ]; } /** * Removes an existing entry from the menu. * * @param string $menuName The menu's category name. Can be a translation token. * @param string|null|false $subMenuName The menu item's name. Can be a translation token. * @return void * @api */ public function remove($menuName, $subMenuName = false) { $this->menuEntriesToRemove[] = array( $menuName, $subMenuName, ); } /** * Builds a single menu item * * @param string|array $url * @param string|null|false $tooltip Tooltip to display. * @param string|null|false $icon * @param string|null|false $onclick * @param string|null|false $attribute * @param string|null|false $help */ private function buildMenuItem( string $menuName, ?string $subMenuName, $url, int $order = 50, $tooltip = false, $icon = false, $onclick = false, $attribute = false, $help = false, int $badgeCount = 0, string $cssClass = '' ): void { if (!isset($this->menu[$menuName])) { $this->menu[$menuName] = [ '_hasSubmenu' => false, '_order' => $order, ]; } if (empty($subMenuName)) { $this->menu[$menuName]['_url'] = $url; $this->menu[$menuName]['_order'] = $order; $this->menu[$menuName]['_name'] = $menuName; $this->menu[$menuName]['_tooltip'] = $tooltip; $this->menu[$menuName]['_attribute'] = $attribute; if (!empty($this->menuIcons[$menuName])) { $this->menu[$menuName]['_icon'] = $this->menuIcons[$menuName]; } else { $this->menu[$menuName]['_icon'] = ''; } if (!empty($onclick)) { $this->menu[$menuName]['_onclick'] = $onclick; } $this->menu[$menuName]['_help'] = $help ?: ''; $this->menu[$menuName]['_badgecount'] = $badgeCount; $this->menu[$menuName]['_cssClass'] = $cssClass; } if (!empty($subMenuName)) { $this->menu[$menuName][$subMenuName]['_url'] = $url; $this->menu[$menuName][$subMenuName]['_order'] = $order; $this->menu[$menuName][$subMenuName]['_name'] = $subMenuName; $this->menu[$menuName][$subMenuName]['_tooltip'] = $tooltip; $this->menu[$menuName][$subMenuName]['_attribute'] = $attribute; $this->menu[$menuName][$subMenuName]['_icon'] = $icon; $this->menu[$menuName][$subMenuName]['_onclick'] = $onclick; $this->menu[$menuName][$subMenuName]['_help'] = $help ?: ''; $this->menu[$menuName][$subMenuName]['_badgecount'] = $badgeCount; $this->menu[$menuName][$subMenuName]['_cssClass'] = $cssClass; $this->menu[$menuName]['_hasSubmenu'] = true; if (!array_key_exists('_tooltip', $this->menu[$menuName])) { $this->menu[$menuName]['_tooltip'] = $tooltip; } } } /** * Builds the menu from the $this->menuEntries variable. */ private function buildMenu(): void { foreach ($this->menuEntries as $menuEntry) { $this->buildMenuItem(...$menuEntry); } } /** * Renames a single menu entry. * * @param string $mainMenuOriginal * @param string|null $subMenuOriginal * @param string $mainMenuRenamed * @param string|null $subMenuRenamed * @phpstan-param ($subMenuOriginal is null ? null : string) $subMenuRenamed * @return void * @api */ public function rename($mainMenuOriginal, $subMenuOriginal, $mainMenuRenamed, $subMenuRenamed) { $this->renames[] = [ $mainMenuOriginal, $subMenuOriginal, $mainMenuRenamed, $subMenuRenamed, ]; } /** * Edits a URL of an existing menu entry. * * @param string $mainMenuToEdit * @param string|null $subMenuToEdit * @param string|array $newUrl * @return void * @api */ public function editUrl($mainMenuToEdit, $subMenuToEdit, $newUrl) { $this->edits[] = [$mainMenuToEdit, $subMenuToEdit, $newUrl]; } /** * Applies all edits to the menu. */ private function applyEdits(): void { foreach ($this->edits as $edit) { $mainMenuToEdit = $edit[0]; $subMenuToEdit = $edit[1]; $newUrl = $edit[2]; if ($subMenuToEdit === null) { if (isset($this->menu[$mainMenuToEdit])) { $menuDataToEdit = &$this->menu[$mainMenuToEdit]; } else { $menuDataToEdit = null; } } else { if (isset($this->menu[$mainMenuToEdit][$subMenuToEdit])) { $menuDataToEdit = &$this->menu[$mainMenuToEdit][$subMenuToEdit]; } else { $menuDataToEdit = null; } } if (empty($menuDataToEdit)) { $this->buildMenuItem($mainMenuToEdit, $subMenuToEdit, $newUrl); } else { $menuDataToEdit['_url'] = $newUrl; } } } private function applyRemoves(): void { foreach ($this->menuEntriesToRemove as $menuToDelete) { if (empty($menuToDelete[1])) { // Delete Main Menu if (isset($this->menu[$menuToDelete[0]])) { unset($this->menu[$menuToDelete[0]]); } } else { // Delete Sub Menu if (isset($this->menu[$menuToDelete[0]][$menuToDelete[1]])) { unset($this->menu[$menuToDelete[0]][$menuToDelete[1]]); } } } } /** * Applies renames to the menu. */ private function applyRenames(): void { foreach ($this->renames as $rename) { $mainMenuOriginal = $rename[0]; $subMenuOriginal = $rename[1]; $mainMenuRenamed = $rename[2]; $subMenuRenamed = $rename[3]; // Are we changing a submenu? if (!empty($subMenuOriginal)) { if (isset($this->menu[$mainMenuOriginal][$subMenuOriginal])) { $save = $this->menu[$mainMenuOriginal][$subMenuOriginal]; $save['_name'] = $subMenuRenamed; unset($this->menu[$mainMenuOriginal][$subMenuOriginal]); $this->menu[$mainMenuRenamed][$subMenuRenamed] = $save; } } elseif (isset($this->menu[$mainMenuOriginal])) { // Changing a first-level element $save = $this->menu[$mainMenuOriginal]; $save['_name'] = $mainMenuRenamed; unset($this->menu[$mainMenuOriginal]); $this->menu[$mainMenuRenamed] = $save; } } } /** * Orders the menu according to their order. */ private function applyOrdering(): void { if ( empty($this->menu) || $this->orderingApplied ) { return; } uasort($this->menu, [$this, 'menuCompare']); foreach ($this->menu as $key => &$element) { if (is_null($element)) { unset($this->menu[$key]); } elseif ($element['_hasSubmenu']) { uasort($element, [$this, 'menuCompare']); } } $this->orderingApplied = true; } /** * Compares two menu entries. Used for ordering. * * @param array|string $itemOne * @param array|string $itemTwo * @return int */ protected function menuCompare($itemOne, $itemTwo) { if (!is_array($itemOne) && !is_array($itemTwo)) { return 0; } if (!is_array($itemOne) && is_array($itemTwo)) { return -1; } if (is_array($itemOne) && !is_array($itemTwo)) { return 1; } if (!isset($itemOne['_order']) && !isset($itemTwo['_order'])) { return 0; } if (!isset($itemOne['_order']) && isset($itemTwo['_order'])) { return -1; } if (isset($itemOne['_order']) && !isset($itemTwo['_order'])) { return 1; } if ($itemOne['_order'] == $itemTwo['_order']) { return strcmp( $itemOne['_name'] ?? '', $itemTwo['_name'] ?? '' ); } return ($itemOne['_order'] < $itemTwo['_order']) ? -1 : 1; } }