diff --git a/plist b/plist index 79422cf42b0..1ebe2d5b351 100644 --- a/plist +++ b/plist @@ -656,6 +656,7 @@ /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/UpdateOnlyTextField.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/UrlField.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/VirtualIPField.php +/usr/local/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuContainer.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuInitException.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuItem.php /usr/local/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php @@ -768,6 +769,7 @@ /usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Filter.xml /usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Group.php /usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Group.xml +/usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.php /usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.xml /usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Migrations/M1_0_0.php /usr/local/opnsense/mvc/app/models/OPNsense/Firewall/Migrations/MFP1_0_0.php @@ -828,6 +830,7 @@ /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Lagg.xml /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Loopback.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Loopback.xml +/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.xml /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Migrations/SET1_0_0.php /usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Neighbor.php diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuContainer.php b/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuContainer.php new file mode 100644 index 00000000000..fee7eadaba9 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuContainer.php @@ -0,0 +1,49 @@ +menusystem = $menusystem; + } + + public function appendItem($root, $id, $properties) + { + return $this->menusystem->appendItem($root, $id, $properties); + } + + public function collect() + { + return; + } +} \ No newline at end of file diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuItem.php b/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuItem.php index 6ffe702b8d7..873bbd71883 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuItem.php +++ b/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuItem.php @@ -64,6 +64,13 @@ class MenuItem */ private $CssClass = ""; + /** + * Classes to add to the link + * @var string + */ + private $LinkClass = ""; + + /** * link to url location * @var string @@ -94,6 +101,12 @@ class MenuItem */ private $selected = false; + /** + * Tree depth + * @var int + */ + private $depth = 0; + /** * class method getters * @var array @@ -141,6 +154,9 @@ public function __construct($id, $parent = null) $this->id = $id; $this->visibleName = gettext($id); $this->parent = $parent; + if ($parent !== null) { + $this->depth = $parent->getDepth() + 1; + } $prop_exclude_list = ['getXmlPropertySetterName' => true]; if (self::$internalClassMethodAliases === null) { self::$internalClassMethodAliases = []; @@ -169,6 +185,14 @@ public function getId() return $this->id; } + /** + * return this nodes depth in the menu + */ + public function getDepth() + { + return $this->depth; + } + /** * set sort order @@ -242,6 +266,40 @@ public function getCssClass() return $this->CssClass; } + /** + * setter for default link class + * @param $value + */ + public function setLinkClass($value) + { + $this->LinkClass = $value; + } + + + /** + * getter for css class set on the actual link + */ + public function getLinkClass() + { + $css = ['list-group-item']; + if (count($this->children) >= 1 && $this->depth < 3) { + if ($this->selected) { + $css[] = 'active-menu-title'; + } + } else { + if ($this->depth == 3) { + $css[] = 'menu-level-3-item'; + } + if ($this->selected) { + $css[] = 'active'; + } + } + if ($this->Url != '') { + $css[] = 'menu_ref_'.md5($this->Url); + } + return implode(' ', $css) . " " . $this->LinkClass; + } + /** * setter for url field * @param $value diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php b/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php index a4d21fbc48d..99563a168a8 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php +++ b/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php @@ -29,6 +29,7 @@ namespace OPNsense\Base\Menu; +use ReflectionClass; use OPNsense\Core\AppConfig; use OPNsense\Core\Config; @@ -48,6 +49,11 @@ class MenuSystem */ private $menuCacheFilename = null; + /** + * @var array model directories + */ + private $modelDirs = []; + /** * @var int time to live for merged menu xml */ @@ -106,6 +112,20 @@ public function invalidateCache() @unlink($this->menuCacheFilename); } + private function iterateMenuPaths() + { + foreach ($this->modelDirs as $modelDir) { + foreach (glob(preg_replace('#/+#', '/', "{$modelDir}/*")) as $vendor) { + foreach (glob($vendor . '/*') as $module) { + if (is_dir($module . '/Menu')) { + $path = $module . '/Menu/'; + yield ['path' => $path, 'base' => substr($path, strlen($modelDir))]; + } + } + } + } + } + /** * Load and persist Menu configuration to disk. * @param bool $nowait when the cache is locked, skip waiting for it to become available. @@ -113,33 +133,19 @@ public function invalidateCache() */ public function persist($nowait = true) { - // fetch our model locations - $appconfig = new AppConfig(); - if (!empty($appconfig->application->modelsDir)) { - $modelDirs = $appconfig->application->modelsDir; - if (!is_array($modelDirs) && !is_object($modelDirs)) { - $modelDirs = array($modelDirs); - } - } - // collect all XML menu definitions into a single file $menuXml = new \DOMDocument('1.0'); $root = $menuXml->createElement('menu'); $menuXml->appendChild($root); // crawl all vendors and modules and add menu definitions - foreach ($modelDirs as $modelDir) { - foreach (glob(preg_replace('#/+#', '/', "{$modelDir}/*")) as $vendor) { - foreach (glob($vendor . '/*') as $module) { - $menu_cfg_xml = $module . '/Menu/Menu.xml'; - if (file_exists($menu_cfg_xml)) { - try { - $domNode = dom_import_simplexml($this->addXML($menu_cfg_xml)); - $domNode = $root->ownerDocument->importNode($domNode, true); - $root->appendChild($domNode); - } catch (MenuInitException $e) { - error_log($e); - } - } + foreach ($this->iterateMenuPaths() as $menu_dir) { + if (file_exists($menu_dir['path'] . 'Menu.xml')) { + try { + $domNode = dom_import_simplexml($this->addXML($menu_dir['path'] . 'Menu.xml')); + $domNode = $root->ownerDocument->importNode($domNode, true); + $root->appendChild($domNode); + } catch (MenuInitException $e) { + error_log($e); } } } @@ -179,8 +185,16 @@ public function isExpired() */ public function __construct() { + $appconfig = new AppConfig(); + if (!empty($appconfig->application->modelsDir)) { + $this->modelDirs = $appconfig->application->modelsDir; + if (!is_array($this->modelDirs) && !is_object($this->modelDirs)) { + $this->modelDirs = [$this->modelDirs]; + } + } + // set cache location - $this->menuCacheFilename = (new AppConfig())->application->tempDir . '/opnsense_menu_cache.xml'; + $this->menuCacheFilename = $appconfig->application->tempDir . '/opnsense_menu_cache.xml'; // load menu xml's $menuxml = null; @@ -199,47 +213,33 @@ public function __construct() } } - $config = Config::getInstance()->object(); - - // collect interfaces for dynamic (interface) menu tabs... - $iftargets = ['if' => [], 'gr' => [], 'wl' => [], 'fw' => [], 'dhcp4' => [], 'dhcp6' => []]; - $ifgroups = []; - $ifgroups_seq = []; - - if ($config->interfaces->count() > 0) { - if ($config->ifgroups->count() > 0) { - foreach ($config->ifgroups->children() as $key => $node) { - if (empty($node->members) || !empty($node->nogroup)) { - continue; - } - if (!empty((string)$node->sequence)) { - $ifgroups_seq[(string)$node->ifname] = (int)((string)$node->sequence); - } - /* we need both if and gr reference */ - $iftargets['if'][(string)$node->ifname] = (string)$node->ifname; - $iftargets['gr'][(string)$node->ifname] = (string)$node->ifname; - foreach (preg_split('/[ |,]+/', (string)$node->members) as $member) { - if (!array_key_exists($member, $ifgroups)) { - $ifgroups[$member] = []; - } - array_push($ifgroups[$member], (string)$node->ifname); + // collect and insert dynamic entries + foreach ($this->iterateMenuPaths() as $menu_dir) { + if (file_exists($menu_dir['path'] . 'Menu.php')) { + $classname = str_replace('/', '\\', $menu_dir['base']) . 'Menu'; + try { + $cls = new ReflectionClass($classname); + if (!$cls->isInstantiable() || !$cls->isSubclassOf('OPNsense\\Base\\Menu\\MenuContainer')) { + continue; /* ignore, not ours */ } + } catch (\ReflectionException) { + continue; /* ignore, can't construct */ } + $cls->newInstance($this)->collect(); } + } + + /* XXX: move to ISC plugin */ + if (!file_exists('/usr/local/www/services_dhcp.php') && !file_exists('/usr/local/www/services_dhcpv6.php')) { + return; + } + $config = Config::getInstance()->object(); + + // collect interfaces for dynamic (interface) menu tabs... + $iftargets = ['dhcp4' => [], 'dhcp6' => []]; + if ($config->interfaces->count() > 0) { foreach ($config->interfaces->children() as $key => $node) { - // Interfaces tab - if (empty($node->virtual)) { - $iftargets['if'][$key] = !empty($node->descr) ? (string)$node->descr : strtoupper($key); - } - // Wireless status tab - if (isset($node->wireless)) { - $iftargets['wl'][$key] = !empty($node->descr) ? (string)$node->descr : strtoupper($key); - } - // "Firewall: Rules" menu tab... - if (isset($node->enable) && $node->if != 'lo0') { - $iftargets['fw'][$key] = !empty($node->descr) ? (string)$node->descr : strtoupper($key); - } // "Services: DHCPv[46]" menu tab: if (empty($node->virtual) && isset($node->enable)) { if (!empty(filter_var($node->ipaddr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4))) { @@ -256,110 +256,9 @@ public function __construct() natcasesort($iftargets[$tab]); } - // add groups and interfaces to "Interfaces" menu tab... - $ordid = count($ifgroups_seq) > 0 ? max($ifgroups_seq) : 0; - foreach ($iftargets['if'] as $key => $descr) { - if (array_key_exists($key, $iftargets['gr'])) { - $this->appendItem('Interfaces', $key, [ - 'fixedname' => '[' . $descr . ']', - 'cssclass' => 'fa fa-sitemap', - 'order' => isset($ifgroups_seq[$key]) ? $ifgroups_seq[$key] : $ordid++, - ]); - } elseif (!array_key_exists($key, $ifgroups)) { - $this->appendItem('Interfaces', $key, [ - 'url' => '/interfaces.php?if=' . $key, - 'fixedname' => '[' . $descr . ']', - 'cssclass' => 'fa fa-sitemap', - 'order' => $ordid++, - ]); - } - } - - foreach ($ifgroups as $key => $groupings) { - $first = true; - foreach ($groupings as $grouping) { - if (empty($iftargets['if'][$key])) { - // referential integrity between ifgroups and interfaces isn't assured, skip when interface doesn't exist - continue; - } - $this->appendItem('Interfaces.' . $grouping, $key, [ - 'url' => '/interfaces.php?if=' . $key . '&group=' . $grouping, - 'fixedname' => '[' . $iftargets['if'][$key] . ']', - 'order' => array_search($key, array_keys($iftargets['if'])) - ]); - if ($first) { - $this->appendItem('Interfaces.' . $grouping . '.' . $key, 'Origin', [ - 'url' => '/interfaces.php?if=' . $key, - 'visibility' => 'hidden', - ]); - $first = false; - } - } - } - - $ordid = 100; - foreach ($iftargets['wl'] as $key => $descr) { - $this->appendItem('Interfaces.Wireless', $key, [ - 'fixedname' => sprintf(gettext('%s Status'), $descr), - 'url' => '/status_wireless.php?if=' . $key, - 'order' => $ordid++, - ]); - } - - // add interfaces to "Firewall: Rules" menu tab... - $has_legacy_fw = !empty($config->filter?->rule?->count()); - $has_mvc_fw = !empty($config->OPNsense?->Firewall?->Filter?->rules?->count()); - if ($has_legacy_fw) { - $this->appendItem('Firewall.Rules', 'Migration', [ - 'url' => '/ui/firewall/migration', - 'fixedname' => sprintf(" %s", gettext('Migration assistant')), - 'order' => 0, - ]); - $iftargets['fw'] = array_merge(['FloatingRules' => gettext('Floating')], $iftargets['fw']); - } - $ordid = 1; - foreach ($iftargets['fw'] as $key => $descr) { - if ($has_mvc_fw && !$has_legacy_fw) { - /* only search */ - $this->appendItem('Firewall.Rule', $key, [ - 'url' => '/ui/firewall/filter/#interface=' . $key, - 'fixedname' => $descr, - 'order' => $ordid++, - ]); - continue; - } - /* legacy rules */ - $this->appendItem('Firewall.Rules', $key, [ - 'url' => '/firewall_rules.php?if=' . $key, - 'fixedname' => $descr, - 'order' => $ordid++, - ]); - $this->appendItem('Firewall.Rules.' . $key, 'Select' . $key, [ - 'url' => '/firewall_rules.php?if=' . $key . '&*', - 'visibility' => 'hidden', - ]); - if ($key == 'FloatingRules') { - $this->appendItem('Firewall.Rules.' . $key, 'Top' . $key, [ - 'url' => '/firewall_rules.php', - 'visibility' => 'hidden', - ]); - } - $this->appendItem('Firewall.Rules.' . $key, 'Add' . $key, [ - 'url' => '/firewall_rules_edit.php?if=' . $key, - 'visibility' => 'hidden', - ]); - $this->appendItem('Firewall.Rules.' . $key, 'Edit' . $key, [ - 'url' => '/firewall_rules_edit.php?if=' . $key . '&*', - 'visibility' => 'hidden', - ]); - } - // add interfaces to "Services: DHCPv[46]" menu tab: $ordid = 0; foreach ($iftargets['dhcp4'] as $key => $descr) { - if (!file_exists('/usr/local/www/services_dhcp.php')) { - break; - } $this->appendItem('Services.ISC_DHCPv4', $key, [ 'url' => '/services_dhcp.php?if=' . $key, 'fixedname' => "[$descr]", @@ -380,9 +279,6 @@ public function __construct() } $ordid = 0; foreach ($iftargets['dhcp6'] as $key => $descr) { - if (!file_exists('/usr/local/www/services_dhcpv6.php')) { - break; - } $this->appendItem('Services.ISC_DHCPv6', $key, [ 'url' => '/services_dhcpv6.php?if=' . $key, 'fixedname' => "[$descr]", diff --git a/src/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.php b/src/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.php new file mode 100644 index 00000000000..2398ac13b13 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/Menu/Menu.php @@ -0,0 +1,98 @@ +object(); + $iftargets = []; + if ($config->interfaces->count() > 0) { + foreach ($config->interfaces->children() as $key => $node) { + // "Firewall: Rules" menu tab... + if (isset($node->enable) && $node->if != 'lo0') { + $iftargets[$key] = !empty($node->descr) ? (string)$node->descr : strtoupper($key); + } + } + } + natcasesort($iftargets); + + // add interfaces to "Firewall: Rules" menu tab... + $has_legacy_fw = !empty($config->filter?->rule?->count()); + $has_mvc_fw = !empty($config->OPNsense?->Firewall?->Filter?->rules?->count()); + if ($has_legacy_fw) { + $this->appendItem('Firewall.Rules', 'Migration', [ + 'url' => '/ui/firewall/migration', + 'fixedname' => sprintf(" %s", gettext('Migration assistant')), + 'order' => 0, + ]); + $iftargets = array_merge(['FloatingRules' => gettext('Floating')], $iftargets); + } + $ordid = 1; + foreach ($iftargets as $key => $descr) { + if ($has_mvc_fw && !$has_legacy_fw) { + /* only search */ + $this->appendItem('Firewall.Rule', $key, [ + 'url' => '/ui/firewall/filter/#interface=' . $key, + 'fixedname' => $descr, + 'order' => $ordid++, + ]); + continue; + } + /* legacy rules */ + $this->appendItem('Firewall.Rules', $key, [ + 'url' => '/firewall_rules.php?if=' . $key, + 'fixedname' => $descr, + 'order' => $ordid++, + ]); + $this->appendItem('Firewall.Rules.' . $key, 'Select' . $key, [ + 'url' => '/firewall_rules.php?if=' . $key . '&*', + 'visibility' => 'hidden', + ]); + if ($key == 'FloatingRules') { + $this->appendItem('Firewall.Rules.' . $key, 'Top' . $key, [ + 'url' => '/firewall_rules.php', + 'visibility' => 'hidden', + ]); + } + $this->appendItem('Firewall.Rules.' . $key, 'Add' . $key, [ + 'url' => '/firewall_rules_edit.php?if=' . $key, + 'visibility' => 'hidden', + ]); + $this->appendItem('Firewall.Rules.' . $key, 'Edit' . $key, [ + 'url' => '/firewall_rules_edit.php?if=' . $key . '&*', + 'visibility' => 'hidden', + ]); + } + } +} \ No newline at end of file diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.php new file mode 100644 index 00000000000..944048b873e --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.php @@ -0,0 +1,129 @@ +object(); + $iftargets = ['if' => [], 'gr' => [], 'wl' => []]; + $ifgroups = []; + $ifgroups_seq = []; + + if ($config->interfaces->count() > 0) { + if ($config->ifgroups->count() > 0) { + foreach ($config->ifgroups->children() as $key => $node) { + if (empty($node->members) || !empty($node->nogroup)) { + continue; + } + if (!empty((string)$node->sequence)) { + $ifgroups_seq[(string)$node->ifname] = (int)((string)$node->sequence); + } + /* we need both if and gr reference */ + $iftargets['if'][(string)$node->ifname] = (string)$node->ifname; + $iftargets['gr'][(string)$node->ifname] = (string)$node->ifname; + foreach (preg_split('/[ |,]+/', (string)$node->members) as $member) { + if (!array_key_exists($member, $ifgroups)) { + $ifgroups[$member] = []; + } + array_push($ifgroups[$member], (string)$node->ifname); + } + } + } + foreach ($config->interfaces->children() as $key => $node) { + // Interfaces tab + if (empty($node->virtual)) { + $iftargets['if'][$key] = !empty($node->descr) ? (string)$node->descr : strtoupper($key); + } + // Wireless status tab + if (isset($node->wireless)) { + $iftargets['wl'][$key] = !empty($node->descr) ? (string)$node->descr : strtoupper($key); + } + } + } + foreach (array_keys($iftargets) as $tab) { + natcasesort($iftargets[$tab]); + } + + // add groups and interfaces to "Interfaces" menu tab... + $ordid = count($ifgroups_seq) > 0 ? max($ifgroups_seq) : 0; + foreach ($iftargets['if'] as $key => $descr) { + if (array_key_exists($key, $iftargets['gr'])) { + $this->appendItem('Interfaces', $key, [ + 'fixedname' => '[' . $descr . ']', + 'cssclass' => 'fa fa-sitemap', + 'order' => isset($ifgroups_seq[$key]) ? $ifgroups_seq[$key] : $ordid++, + ]); + } elseif (!array_key_exists($key, $ifgroups)) { + $this->appendItem('Interfaces', $key, [ + 'url' => '/interfaces.php?if=' . $key, + 'fixedname' => '[' . $descr . ']', + 'cssclass' => 'fa fa-sitemap', + 'order' => $ordid++, + ]); + } + } + + foreach ($ifgroups as $key => $groupings) { + $first = true; + foreach ($groupings as $grouping) { + if (empty($iftargets['if'][$key])) { + // referential integrity between ifgroups and interfaces isn't assured, skip when interface doesn't exist + continue; + } + $this->appendItem('Interfaces.' . $grouping, $key, [ + 'url' => '/interfaces.php?if=' . $key . '&group=' . $grouping, + 'fixedname' => '[' . $iftargets['if'][$key] . ']', + 'order' => array_search($key, array_keys($iftargets['if'])) + ]); + if ($first) { + $this->appendItem('Interfaces.' . $grouping . '.' . $key, 'Origin', [ + 'url' => '/interfaces.php?if=' . $key, + 'visibility' => 'hidden', + ]); + $first = false; + } + } + } + + $ordid = 100; + foreach ($iftargets['wl'] as $key => $descr) { + $this->appendItem('Interfaces.Wireless', $key, [ + 'fixedname' => sprintf(gettext('%s Status'), $descr), + 'url' => '/status_wireless.php?if=' . $key, + 'order' => $ordid++, + ]); + } + + } +} \ No newline at end of file diff --git a/src/opnsense/mvc/app/views/layout_partials/base_menu_system.volt b/src/opnsense/mvc/app/views/layout_partials/base_menu_system.volt index 67cde41f078..b29fd18518c 100644 --- a/src/opnsense/mvc/app/views/layout_partials/base_menu_system.volt +++ b/src/opnsense/mvc/app/views/layout_partials/base_menu_system.volt @@ -5,14 +5,14 @@
{% for topMenuItem in menuSystem %} {% if topMenuItem.Children|length >= 1 %} - + {{ topMenuItem.VisibleName }}
{% for subMenuItem in topMenuItem.Children %} {% if subMenuItem.Url == '' %} {# next level items, submenu is a container #} -
@@ -25,13 +25,13 @@ {% elseif subMenuItem.IsExternal == "Y" %} -
@@ -43,7 +43,7 @@
{% elseif acl.isPageAccessible(session.get('Username'),subMenuItem.Url) %} - +
{{ subMenuItem.VisibleName }}
@@ -59,11 +59,11 @@ {% else %} {# parent level link menu items that pivot #} {% if topMenuItem.IsExternal == "Y" %} -
+ {{ topMenuItem.VisibleName }} {% elseif acl.isPageAccessible(session.get('Username'),topMenuItem.Url) %} - + {{ topMenuItem.VisibleName }} {% endif %} diff --git a/src/www/fbegin.inc b/src/www/fbegin.inc index e83e1a899b5..8e4f17849b7 100644 --- a/src/www/fbegin.inc +++ b/src/www/fbegin.inc @@ -89,13 +89,13 @@ $aclObj = new \OPNsense\Core\ACL(); foreach($menuSystem as $topMenuItem): ?> Children) >= 1): ?> - VisibleName) ?> + VisibleName) ?>