# Functions that build the left menu: in the left frame of the frameset, or
# at the side of every page in the single page layout. theme.pl loads this
# file into its own package when a menu is needed.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
# $main::session_id, set by the core, is used only once in this file
no warnings 'once';
# Globals of the core, in the package this file is loaded into
our (%in, %gconfig, $remote_user, $base_remote_user, $current_theme);
# Pages that show a domain or a managed system. menu.cgi opens one when a
# domain or system is chosen in a menu field that names no page of its own.
our %theme_menu_pages = (
'dom' => '/virtual-server/summary_domain.cgi?dom=',
'sid' => '/server-manager/edit_serv.cgi?id=',
);
# theme_menu_html([&options])
# Returns the HTML of the menu panel. The options are :
# in - Menu inputs, as the left frame gets them: the mode, the dom,
# dname or sid of the chosen domain or system, and the IDs of
# categories to open
# single - Build the menu for a page of the single page layout
# fragment - Return the panel alone, without the scripts that follow
# it, for menu.cgi to send to a page that loads its menu
# In the single page layout the current page picks the mode, domain and
# system when its module has a menu of its own, and the choices remembered
# in the cookie fill in the rest. The link to the current page, or to the
# page the browser came from, is marked and its category opened.
sub theme_menu_html
{
my ($opts) = @_;
$opts ||= { };
# The settings and language strings, and in the single page layout the
# remembered choices and what the current page implies
my $single = $opts->{'single'};
my %min = %{$opts->{'in'} || { }};
my %mtext = &load_language($current_theme);
my $sects = &theme_settings();
my $state = $single ? &theme_menu_state() : { };
my %page = $single ? &theme_menu_page() : ( );
if ($single) {
# The page's domain or system wins over the remembered one
foreach my $k ('dom', 'sid') {
my $v = defined($page{$k}) ? $page{$k} : $state->{$k};
$min{$k} = $v if ($v ne '');
}
}
# Find all left-side items from Webmin
my @allitems = &list_combined_webmin_menu($sects, \%min);
my $sel = &theme_menu_selection(\@allitems);
if ($single) {
# If the menu cannot show the domain or system the page names, for
# example because the ID is of some other object, use the remembered
# one instead
my $retry = 0;
foreach my $k ('dom', 'sid') {
next if (!defined($page{$k}) || !exists($sel->{$k}) ||
$sel->{$k} eq $page{$k} ||
$state->{$k} eq '' || $state->{$k} eq $page{$k});
$min{$k} = $state->{$k};
$retry++;
}
if ($retry) {
# Build the menu again with the remembered choices
@allitems = &list_combined_webmin_menu($sects, \%min);
$sel = &theme_menu_selection(\@allitems);
}
}
# The titles of module menus, such as Virtualmin's, name the modes of the
# product switch
my @leftitems = @allitems;
my @lefttitles = grep { $_->{'type'} eq 'title' } @leftitems;
# Work out what the mode selector contains
my @has = ( );
my %modmenu;
foreach my $title (@lefttitles) {
push(@has, { 'id' => $title->{'module'},
'desc' => $title->{'desc'} });
$modmenu{$title->{'module'}}++;
}
my $nw = $sects->{'nowebmin'} || 0;
if ($nw == 0 || $nw == 2 && &theme_menu_master()) {
# Offer the Webmin modules mode, unless the settings hide it from
# everyone, or from everyone but the master admin
push(@has, { 'id' => 'modules',
'desc' => $mtext{'has_'.&get_product_name()} });
}
my %hasmode = map { $_->{'id'}, 1 } @has;
# In the left frame, use the mode asked for. In the single page layout,
# use the mode of the current page's module if it has one, else the
# remembered mode. Failing that, the settings or the first menu decide.
my $mode = !$single ? $min{'mode'} :
$hasmode{$page{'mode'}} ? $page{'mode'} : $state->{'mode'};
$mode ||= $sects->{'tab'} && $sects->{'tab'} =~ /vm2/ ? "server-manager" :
$sects->{'tab'} && $sects->{'tab'} =~ /virtualmin/ ? "virtual-server" :
$sects->{'tab'} && $sects->{'tab'} =~ /mail/ ? "mailboxes" :
$sects->{'tab'} && $sects->{'tab'} =~ /webmin/ ? "modules" :
@leftitems ? $has[0]->{'id'} : "modules";
if (!$hasmode{$mode}) {
# A mode that is not offered falls back to the first one
$mode = $has[0]->{'id'};
}
if ($mode eq "modules") {
# The Webmin modules by category, without the modules that have a
# mode of their own
@leftitems = &list_modules_webmin_menu();
foreach my $l (@leftitems) {
$l->{'members'} = [ grep { !$modmenu{$_->{'id'}} }
@{$l->{'members'}} ];
}
push(@leftitems, { 'type' => 'hr' });
}
else {
# The menu of the mode's module, plus items of menus that have no
# title and so no mode
my %titlemods = map { $_->{'module'}, $_ } @lefttitles;
@leftitems = grep { $_->{'module'} eq $mode ||
!$titlemods{$_->{'module'}} } @leftitems;
}
# The titles only name the modes, and are not shown in the menu
@leftitems = grep { $_->{'type'} ne 'title' } @leftitems;
# Lines of text at the start of the menu, such as the login and its level,
# go into the brand block instead of the menu
my @userlines;
while(@leftitems && $leftitems[0]->{'type'} eq 'text') {
my $t = shift(@leftitems);
if ($t->{'json'} && $t->{'json'}->{'label'}) {
# Login line, shown as the username and its level
push(@userlines, "".&html_escape($remote_user)."".
($t->{'json'}->{'level'} ?
" · ".$t->{'json'}->{'level'} : ""));
}
else {
# Any other line of text, as it is
push(@userlines, &html_escape($t->{'desc'}));
}
}
if (@userlines && $leftitems[0]->{'type'} eq 'hr') {
# The separator after the text lines goes with them
shift(@leftitems);
}
if (!@userlines) {
# Without a login line, show the username alone
push(@userlines, "".&html_escape($remote_user || $base_remote_user).
"");
}
# Add the search box and the links every menu ends with
push(@leftitems, &theme_menu_extra_items($mode, $sects, \%mtext));
# The page the mode switch and the menu fields come back to: the index of
# the current module, or the system information page on a theme page. The
# current page itself could repeat an action it ran, such as a module
# refresh, and would lose what was typed into it.
my $mod = &theme_menu_module();
my $return = $mod ? "/$mod/" : "/right.cgi";
# What the items need to build their HTML
my %ctx = ( 'single' => $single,
'mode' => $mode,
'in' => \%min,
'return' => $return,
'text' => \%mtext );
if ($single) {
# In the single page layout, the link to the current page is marked
# and its categories opened
my ($active, $cats) = &theme_menu_active(\@leftitems, $mode);
$ctx{'active'} = $active;
$ctx{'open'} = { map { $_, 1 } @$cats };
}
# In the single page layout, remember what the menu shows, so pages of
# other modules keep it. The script that does it follows the panel, and
# the panel carries the cookie for the menu script.
my ($remember, $cookie);
if ($single) {
my %new = ( %$state, 'mode' => $mode );
foreach my $k ('dom', 'sid') {
$new{$k} = $sel->{$k} if ($sel->{$k} ne '');
}
$remember = &theme_menu_remember(\%new);
$cookie = &theme_menu_cookie_string(\%new);
}
# The product for the logo, and the URL prefix for links
my ($prod, $prodname) = &theme_menu_product(\%mtext);
my $pfx = &get_webprefix();
my $h = "";
# The whole menu sits in one panel that floats on the page ground
if ($single) {
# An aside element, labelled with the product name for screen readers.
# The script keeps a scroll position for each mode.
$h .= "