mirror of
https://github.com/webmin/webmin.git
synced 2026-10-21 19:50:40 +01:00
Compare commits
1 Commits
dev/fix-rp
...
dev/framed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbe1f7f17e |
@@ -16,3 +16,4 @@ applet_light_bg=ffffff
|
||||
noindex=1
|
||||
inframe=1
|
||||
framed=1
|
||||
oofunctions=theme-init.pl
|
||||
|
||||
@@ -50,6 +50,12 @@ if ($hasvirt || $hasvm2) {
|
||||
[ "webmin", $text{'edright_webmin'} ] ]));
|
||||
}
|
||||
|
||||
# Menu in a frame of its own, or on every page
|
||||
print &ui_table_row($text{'edright_layout'},
|
||||
&ui_select("layout", $sects->{'layout'},
|
||||
[ [ "", $text{'edright_layout_frames'} ],
|
||||
[ "single", $text{'edright_layout_single'} ] ]));
|
||||
|
||||
# Left frame size
|
||||
print &ui_table_row($text{'edright_fsize'},
|
||||
&ui_opt_textbox("fsize", $sects->{'fsize'}, 6, $text{'edright_fsizedef'}).
|
||||
|
||||
@@ -12,14 +12,11 @@ $right_frame_sections_file = "$config_directory/$current_theme/sections";
|
||||
$default_domains_to_show = 10;
|
||||
|
||||
# get_left_frame_width()
|
||||
# Returns the width of the left frame in pixels
|
||||
# Returns the width of the left frame in pixels, the same as the width of
|
||||
# the menu in the single page layout
|
||||
sub get_left_frame_width
|
||||
{
|
||||
local $sects = &get_right_frame_sections();
|
||||
return $sects->{'fsize'} ? $sects->{'fsize'} :
|
||||
&get_product_name() eq 'usermin' ? 200 :
|
||||
&foreign_available("server-manager") &&
|
||||
&foreign_available("virtual-server") ? 280 : 260;
|
||||
return &theme_menu_width();
|
||||
}
|
||||
|
||||
# list_virtualmin_theme_overlays()
|
||||
|
||||
@@ -46,6 +46,13 @@ if (!$goto) {
|
||||
}
|
||||
}
|
||||
|
||||
if (&theme_single_page()) {
|
||||
# The single page layout has no frameset, so the first page opens by
|
||||
# itself and brings its own menu
|
||||
&theme_local_redirect("/$goto");
|
||||
exit;
|
||||
}
|
||||
|
||||
# Work out the title that includes the version
|
||||
if ($hasvirt) {
|
||||
%minfo = &get_module_info("virtual-server");
|
||||
@@ -107,20 +114,7 @@ print "<link rel='stylesheet' type='text/css' href='".&get_webprefix().
|
||||
&theme_asset_key("gray-theme.css")."'>\n";
|
||||
print &ui_switch_theme_javascript();
|
||||
print "<title>$title</title>\n";
|
||||
my $imgdir = "@{[&get_webprefix()]}/images";
|
||||
my $prod = 'webmin';
|
||||
if (foreign_available("server-manager")) {
|
||||
$prod = 'cloudmin';
|
||||
}
|
||||
elsif (foreign_available("virtual-server")) {
|
||||
$prod = 'virtualmin';
|
||||
}
|
||||
elsif (get_product_name() eq 'usermin') {
|
||||
$prod = 'usermin';
|
||||
}
|
||||
print "<link rel='icon' type='image/png' sizes='16x16' href='$imgdir/favicons/$prod/favicon-16x16.png'>\n";
|
||||
print "<link rel='icon' type='image/png' sizes='32x32' href='$imgdir/favicons/$prod/favicon-32x32.png'>\n";
|
||||
print "<link rel='icon' type='image/png' sizes='192x192' href='$imgdir/favicons/$prod/favicon-192x192.png'>\n";
|
||||
print &theme_favicons();
|
||||
print "</head>\n";
|
||||
|
||||
# Upper custom frame
|
||||
|
||||
@@ -3,6 +3,8 @@ left_search=Search
|
||||
left_dom=Virtual server
|
||||
left_sid=Managed system
|
||||
left_dname=Virtual server
|
||||
left_menu=Menu
|
||||
left_elib=Failed to load the theme menu : $1
|
||||
|
||||
right_virtver=Virtualmin version $1 or higher is now required by this theme!
|
||||
right_vm2ver=Cloudmin version $1 or higher is now required by this theme!
|
||||
@@ -57,11 +59,15 @@ edright_tab1=Whichever is first
|
||||
edright_virtualmin=Virtualmin
|
||||
edright_vm2=Cloudmin
|
||||
edright_webmin=Webmin
|
||||
edright_fsize=Left frame width
|
||||
edright_fsize=Left menu width
|
||||
edright_search=Show search box below left menu?
|
||||
edright_fsizedef=Decide automatically
|
||||
edright_pixels=pixels
|
||||
edright_efsize=Missing or invalid left frame width in pixels
|
||||
edright_efsize=Missing or invalid left menu width in pixels
|
||||
edright_layout=Page layout
|
||||
edright_layout_frames=Menu in its own frame
|
||||
edright_layout_single=Menu on every page, without frames
|
||||
edright_elayout=Invalid page layout
|
||||
edright_scheme=Color scheme
|
||||
edright_scheme_auto=Follow the browser setting
|
||||
edright_scheme_light=Light
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/local/bin/perl
|
||||
# Show the left-side menu of Virtualmin domains, plus modules
|
||||
# Show the menu in the left frame of the frameset: the Virtualmin or Cloudmin
|
||||
# menu, or the Webmin modules
|
||||
use strict;
|
||||
use warnings;
|
||||
no warnings 'redefine';
|
||||
@@ -8,238 +9,25 @@ no warnings 'uninitialized';
|
||||
# Globals
|
||||
our %in;
|
||||
our %text;
|
||||
our $base_remote_user;
|
||||
our $remote_user;
|
||||
our %miniserv;
|
||||
our %gaccess;
|
||||
our $session_id;
|
||||
|
||||
# The menu only shows links and changes nothing, so it loads without the
|
||||
# referer check
|
||||
our $trust_unknown_referers = 1;
|
||||
|
||||
# Load the theme and the functions that build the menu
|
||||
require "gray-theme/gray-theme-lib.pl";
|
||||
require "gray-theme/theme.pl";
|
||||
ReadParse();
|
||||
theme_menu_lib() || error(text('left_elib', html_escape($@)));
|
||||
|
||||
# The body class selects the left frame styles
|
||||
our $gray_theme_body_class = 'left-frame';
|
||||
|
||||
my $is_master;
|
||||
# Is this user the master administrator? Virtualmin and Cloudmin each
|
||||
# have their own test for it
|
||||
if (foreign_available("virtual-server")) {
|
||||
# Virtualmin's master admin
|
||||
foreign_require("virtual-server");
|
||||
$is_master = virtual_server::master_admin();
|
||||
}
|
||||
elsif (foreign_available("server-manager")) {
|
||||
# Cloudmin's user with global permissions
|
||||
foreign_require("server-manager");
|
||||
$is_master = server_manager::can_action(undef, "global");
|
||||
}
|
||||
|
||||
# Find all left-side items from Webmin
|
||||
my $sects = get_right_frame_sections();
|
||||
my @leftitems = list_combined_webmin_menu($sects, \%in);
|
||||
my @lefttitles = grep { $_->{'type'} eq 'title' } @leftitems;
|
||||
|
||||
# Work out what 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 && $is_master) {
|
||||
# The Webmin modules mode is offered unless the settings hide it,
|
||||
# from everyone or from all but the master
|
||||
my $p = get_product_name();
|
||||
push(@has, { 'id' => 'modules',
|
||||
'desc' => $text{'has_'.$p} });
|
||||
}
|
||||
|
||||
# Default left-side mode
|
||||
my $mode = $in{'mode'} ? $in{'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 (indexof($mode, (map { $_->{'id'} } @has)) < 0) {
|
||||
# A mode that is not offered falls back to the first one
|
||||
$mode = $has[0]->{'id'};
|
||||
}
|
||||
|
||||
# Product shown in the brand block, and its name
|
||||
my $prod = foreign_available("server-manager") ? 'cloudmin' :
|
||||
foreign_available("virtual-server") ? 'virtualmin' :
|
||||
get_product_name() eq 'usermin' ? 'usermin' : 'webmin';
|
||||
my %prodnames = ( 'cloudmin' => $text{'has_vm2'},
|
||||
'virtualmin' => $text{'has_virtualmin'},
|
||||
'usermin' => $text{'has_usermin'},
|
||||
'webmin' => $text{'has_webmin'} );
|
||||
|
||||
popup_header($prodnames{$prod});
|
||||
|
||||
# The whole menu sits in one panel that floats on the page ground
|
||||
print "<div class='menu-panel'>\n";
|
||||
|
||||
if ($mode eq "modules") {
|
||||
# Only showing Webmin modules
|
||||
@leftitems = &list_modules_webmin_menu();
|
||||
foreach my $l (@leftitems) {
|
||||
$l->{'members'} = [ grep { !$modmenu{$_->{'id'}} } @{$l->{'members'}} ];
|
||||
}
|
||||
push(@leftitems, { 'type' => 'hr' });
|
||||
}
|
||||
else {
|
||||
# Only show items under some title OR items that have no title
|
||||
my ($lefttitle) = grep { $_->{'id'} eq $mode } @lefttitles;
|
||||
my %titlemods = map { $_->{'module'}, $_ } @lefttitles;
|
||||
@leftitems = grep { $_->{'module'} eq $mode ||
|
||||
!$titlemods{$_->{'module'}} } @leftitems;
|
||||
}
|
||||
@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, "<b>".html_escape($remote_user)."</b>".
|
||||
($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, "<b>".html_escape($remote_user || $base_remote_user).
|
||||
"</b>");
|
||||
}
|
||||
|
||||
# The brand block and the product switch stay at the top of the panel
|
||||
# while the menu scrolls under them. The loader is the bar that bounces
|
||||
# along the top edge while the right frame loads a page.
|
||||
print "<div class='menu-top'>\n";
|
||||
print "<div class='menu-loader'></div>\n";
|
||||
|
||||
# Show the brand block, with the product logo in a light and a dark
|
||||
# version for the stylesheet to pick from
|
||||
my $imgdir = add_webprefix("/images");
|
||||
my $alt = html_escape($prodnames{$prod});
|
||||
print "<div class='menu-brand'>\n";
|
||||
print "<a class='menu-brand-name' href='".add_webprefix("/right.cgi").
|
||||
"' target='right' title='$alt'>";
|
||||
print "<img class='menu-brand-logo' src='$imgdir/logos/$prod.svg' ".
|
||||
"alt='$alt'>";
|
||||
print "<img class='menu-brand-logo menu-brand-logo-dark' ".
|
||||
"src='$imgdir/logos/$prod-dark.svg' alt='$alt'>";
|
||||
print "</a>\n";
|
||||
# The host and the login, as small lines with icons
|
||||
print "<div class='menu-meta'>\n";
|
||||
print "<div class='menu-host'>".ui_svg_icon('server', { 'size' => 13 }).
|
||||
"<span>".html_escape(get_display_hostname())."</span></div>\n";
|
||||
foreach my $l (@userlines) {
|
||||
print "<div class='menu-user'>".ui_svg_icon('user', { 'size' => 13 }).
|
||||
"<span>$l</span></div>\n";
|
||||
}
|
||||
print "</div>\n";
|
||||
print "</div>\n";
|
||||
|
||||
# Show mode selector
|
||||
if (@has > 1) {
|
||||
print "<div class='mode'>";
|
||||
foreach my $m (@has) {
|
||||
print "<b data-mode='$m->{'id'}'>";
|
||||
if ($m->{'id'} ne $mode) {
|
||||
# The click marks the switch as loading until the
|
||||
# menu has been replaced
|
||||
print "<a href='left.cgi?mode=$m->{'id'}' ".
|
||||
"onclick=\"this.parentNode.parentNode.".
|
||||
"classList.add('loading');".
|
||||
"this.parentNode.classList.add('active')\">";
|
||||
}
|
||||
print $m->{'desc'};
|
||||
if ($m->{'id'} ne $mode) {
|
||||
# Close the link of an unselected mode
|
||||
print "</a>";
|
||||
}
|
||||
print "</b>\n";
|
||||
}
|
||||
print "</div>\n";
|
||||
}
|
||||
print "</div>\n";
|
||||
# Print the frame page, titled with the product name, and the menu
|
||||
my (undef, $prodname) = theme_menu_product(\%text);
|
||||
popup_header($prodname);
|
||||
print &ui_switch_theme_javascript();
|
||||
print "<div class='leftmenu'>\n";
|
||||
|
||||
# Show Webmin search form
|
||||
my $cansearch = ($gaccess{'webminsearch'} || '') ne '0' &&
|
||||
!$sects->{'nosearch'};
|
||||
if ($mode eq "modules" && $cansearch) {
|
||||
push(@leftitems, { 'type' => 'input',
|
||||
'desc' => ' ',
|
||||
'tags' => " placeholder='$text{'left_search'}'",
|
||||
'size' => 10,
|
||||
'name' => 'search',
|
||||
'cgi' => '/webmin_search.cgi', });
|
||||
push(@leftitems, { 'type' => 'hr' });
|
||||
}
|
||||
# Show system information link
|
||||
push(@leftitems, { 'type' => 'item',
|
||||
'id' => 'home',
|
||||
'desc' => $text{'left_home'},
|
||||
'link' => '/right.cgi' });
|
||||
|
||||
# Show refresh modules link
|
||||
if ($mode eq "modules" && foreign_available("webmin")) {
|
||||
push(@leftitems, { 'type' => 'item',
|
||||
'id' => 'refresh',
|
||||
'desc' => $text{'main_refreshmods'},
|
||||
'link' => '/webmin/refresh_modules.cgi' });
|
||||
}
|
||||
|
||||
# Show logout link
|
||||
get_miniserv_config(\%miniserv);
|
||||
if ($miniserv{'logout'} && !$ENV{'SSL_USER'} && !$ENV{'LOCAL_USER'} &&
|
||||
$ENV{'HTTP_USER_AGENT'} !~ /webmin/i) {
|
||||
my $logout = { 'type' => 'item',
|
||||
'id' => 'logout',
|
||||
'target' => 'window' };
|
||||
if ($main::session_id) {
|
||||
# Session logins can log out
|
||||
$logout->{'desc'} = $text{'main_logout'};
|
||||
$logout->{'link'} = '/session_login.cgi?logout=1';
|
||||
}
|
||||
else {
|
||||
# Other logins can only switch to another user
|
||||
$logout->{'desc'} = $text{'main_switch'};
|
||||
$logout->{'link'} = '/switch_user.cgi';
|
||||
}
|
||||
push(@leftitems, $logout);
|
||||
}
|
||||
|
||||
# Show link back to original Webmin server
|
||||
if ($ENV{'HTTP_WEBMIN_SERVERS'}) {
|
||||
push(@leftitems, { 'type' => 'item',
|
||||
'desc' => $text{'header_servers'},
|
||||
'link' => $ENV{'HTTP_WEBMIN_SERVERS'},
|
||||
'target' => 'window' });
|
||||
}
|
||||
|
||||
show_menu_items_list(\@leftitems, 0);
|
||||
|
||||
print "</div>\n";
|
||||
print "</div>\n";
|
||||
print theme_menu_html({ 'in' => \%in });
|
||||
|
||||
# The loader starts on a click or a form submission aimed at the right
|
||||
# frame and stops when the new page there reports, through the functions
|
||||
@@ -272,133 +60,3 @@ document.addEventListener('submit', function(e) {
|
||||
</script>
|
||||
EOF
|
||||
popup_footer();
|
||||
|
||||
# show_menu_items_list(&list, indent)
|
||||
# Actually prints the HTML for menu items
|
||||
sub show_menu_items_list
|
||||
{
|
||||
my ($items, $indent) = @_;
|
||||
foreach my $item (@$items) {
|
||||
if ($item->{'type'} eq 'item') {
|
||||
# Link to some page
|
||||
my $it = $item->{'target'} || '';
|
||||
my $t = $it eq 'new' ? '_blank' :
|
||||
$it eq 'window' ? '_top' : 'right';
|
||||
my $link = add_webprefix($item->{'link'});
|
||||
if ($item->{'link'} =~ /^(https?):\/\//) {
|
||||
# Links to other sites open in a new window
|
||||
$t = '_blank';
|
||||
$link = $item->{'link'};
|
||||
}
|
||||
my $cls;
|
||||
if ($item->{'format'} eq 'link-new') {
|
||||
# Creation link shown as a button with a plus
|
||||
$cls = 'menu-create';
|
||||
$item->{'desc'} = ui_svg_icon('plus', { 'size' => 15 }).
|
||||
" ".$item->{'desc'};
|
||||
}
|
||||
else {
|
||||
# Ordinary link, indented inside a category and muted
|
||||
# when inactive
|
||||
$cls = 'menu-link';
|
||||
$cls .= ' menu-sub' if ($indent);
|
||||
$cls .= ' inactive' if ($item->{'inactive'});
|
||||
if ($item->{'id'} eq 'logout') {
|
||||
# Logout gets an icon and a muted look
|
||||
$cls .= ' menu-logout';
|
||||
$item->{'desc'} = ui_svg_icon('power', { 'size' => 14 }).
|
||||
" ".$item->{'desc'};
|
||||
}
|
||||
}
|
||||
print "<a class='$cls' href='$link' target='$t'>".
|
||||
"$item->{'desc'}</a>\n";
|
||||
}
|
||||
elsif ($item->{'type'} eq 'cat') {
|
||||
# Start of a new category, opened when requested by the
|
||||
# frameset page
|
||||
my $c = $item->{'id'};
|
||||
print "<details class='menu-cat'".($in{$c} ? " open" : "").">";
|
||||
print "<summary><span>$item->{'desc'}</span></summary>\n";
|
||||
show_menu_items_list($item->{'members'}, $indent+1);
|
||||
print "</details>\n";
|
||||
}
|
||||
elsif ($item->{'type'} eq 'html') {
|
||||
# Some HTML block
|
||||
print "<div class='menu-html'>",$item->{'html'},"</div>\n";
|
||||
}
|
||||
elsif ($item->{'type'} eq 'text') {
|
||||
# A line of text
|
||||
print "<div class='menu-text'>",
|
||||
html_escape($item->{'desc'}),"</div>\n";
|
||||
}
|
||||
elsif ($item->{'type'} eq 'hr') {
|
||||
# Separator line
|
||||
print "<hr class='menu-divider'>\n";
|
||||
}
|
||||
elsif ($item->{'type'} eq 'menu' || $item->{'type'} eq 'input') {
|
||||
# Form with an input of some kind
|
||||
if ($item->{'cgi'}) {
|
||||
# The form submits to the item's CGI in the right frame
|
||||
my $cgi = add_webprefix($item->{'cgi'});
|
||||
print "<form class='menu-form' action='$cgi' ".
|
||||
"target='right'>\n";
|
||||
}
|
||||
else {
|
||||
# Without a CGI, the form reloads this menu with the
|
||||
# new value
|
||||
print "<form class='menu-form'>\n";
|
||||
}
|
||||
foreach my $h (@{$item->{'hidden'}}) {
|
||||
print ui_hidden(@$h);
|
||||
}
|
||||
print ui_hidden("mode", $mode);
|
||||
my $label = $item->{'desc'} =~ /\S/ ? $item->{'desc'}
|
||||
: $text{'left_'.$item->{'name'}};
|
||||
if ($label) {
|
||||
# Small caps label above the field
|
||||
print "<label class='menu-label' for='".
|
||||
html_escape($item->{'name'})."'>$label</label>\n";
|
||||
}
|
||||
print "<div class='menu-field'>\n";
|
||||
if ($item->{'type'} eq 'menu') {
|
||||
# A drop-down that submits as soon as it changes
|
||||
my $sel = "";
|
||||
if ($item->{'onchange'}) {
|
||||
# Some menus also load a page for the chosen
|
||||
# value in the right frame
|
||||
$sel = "window.parent.frames[1].location = ".
|
||||
"\"$item->{'onchange'}\" + this.value";
|
||||
}
|
||||
print ui_select($item->{'name'}, $item->{'value'},
|
||||
$item->{'menu'}, 1, 0, 0, 0,
|
||||
"onChange='form.submit(); $sel'");
|
||||
}
|
||||
elsif ($item->{'type'} eq 'input') {
|
||||
# A text field, such as the search box
|
||||
print ui_textbox($item->{'name'}, $item->{'value'},
|
||||
$item->{'size'}, undef, undef, $item->{'tags'});
|
||||
}
|
||||
print "</div>\n";
|
||||
print "</form>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# module_to_menu_item(&module)
|
||||
# Converts a module to the hash ref format expected by show_menu_items_list
|
||||
sub module_to_menu_item
|
||||
{
|
||||
my ($minfo) = @_;
|
||||
return { 'type' => 'item',
|
||||
'id' => $minfo->{'dir'},
|
||||
'desc' => $minfo->{'desc'},
|
||||
'link' => '/'.$minfo->{'dir'}.'/' };
|
||||
}
|
||||
|
||||
# add_webprefix(link)
|
||||
# If a URL starts with a / , add webprefix
|
||||
sub add_webprefix
|
||||
{
|
||||
my ($link) = @_;
|
||||
return $link =~ /^\// ? &get_webprefix().$link : $link;
|
||||
}
|
||||
|
||||
1059
gray-theme/menu-lib.pl
Normal file
1059
gray-theme/menu-lib.pl
Normal file
File diff suppressed because it is too large
Load Diff
101
gray-theme/menu.cgi
Executable file
101
gray-theme/menu.cgi
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/local/bin/perl
|
||||
# Remember a choice made in the menu of the single page layout, such as the
|
||||
# menu mode, a domain or a managed system, and open a page for it. Also
|
||||
# sends the menu panel to a page that loads it.
|
||||
use strict;
|
||||
use warnings;
|
||||
no warnings 'redefine';
|
||||
no warnings 'uninitialized';
|
||||
|
||||
# Globals; %theme_menu_pages comes from menu-lib.pl
|
||||
our %in;
|
||||
our %text;
|
||||
our %theme_menu_pages;
|
||||
|
||||
# The request only sets a cookie and redirects to a page on this server
|
||||
our $trust_unknown_referers = 1;
|
||||
|
||||
# Load the theme and the functions that build the menu
|
||||
require "gray-theme/gray-theme-lib.pl";
|
||||
require "gray-theme/theme.pl";
|
||||
ReadParse();
|
||||
theme_menu_lib() || error(text('left_elib', html_escape($@)));
|
||||
|
||||
if ($in{'fragment'}) {
|
||||
# The menu panel of a page that runs as a Unix user and loads its menu
|
||||
# from here: built for that page's address, module and the page the
|
||||
# browser came from, with the choices it shows stored in the cookie
|
||||
$ENV{'REQUEST_URI'} = theme_menu_local_url($in{'page'}) ? $in{'page'}
|
||||
: "";
|
||||
$ENV{'HTTP_REFERER'} = $in{'ref'};
|
||||
$main::gray_theme_menu_module = $in{'module'} =~ /^[\w\-]+$/ ?
|
||||
$in{'module'} : "";
|
||||
my $html = theme_menu_html({ 'single' => 1, 'fragment' => 1 });
|
||||
print "Set-Cookie: ".theme_menu_cookie_string(theme_menu_state())."\n";
|
||||
PrintHeader();
|
||||
print $html;
|
||||
exit;
|
||||
}
|
||||
|
||||
# Start from the remembered choices
|
||||
my %state = %{theme_menu_state()};
|
||||
if ($in{'mode'} =~ /^[\w.\-]+$/) {
|
||||
# A new menu mode
|
||||
$state{'mode'} = $in{'mode'};
|
||||
}
|
||||
|
||||
# Look up a domain or system chosen in a menu field the same way the menu
|
||||
# does, so only one the menu can show is remembered. Build the menus with
|
||||
# the remembered value for the other one, so choosing a system leaves the
|
||||
# remembered domain alone, and the other way around.
|
||||
my %ask = map { $_, $state{$_} } grep { $state{$_} ne '' } ('dom', 'sid');
|
||||
my %asked;
|
||||
foreach my $k ('dom', 'dname', 'sid') {
|
||||
next if ($in{$k} eq '');
|
||||
$ask{$k} = $in{$k};
|
||||
$asked{$k eq 'dname' ? 'dom' : $k}++;
|
||||
}
|
||||
if ($in{'dname'} ne '' && $in{'dom'} eq '') {
|
||||
# The Virtualmin menu looks up a name only when no domain ID is given
|
||||
delete($ask{'dom'});
|
||||
}
|
||||
my %changed;
|
||||
if (%asked) {
|
||||
# Build the menus with the new value, and keep what they show for it
|
||||
my @items = list_combined_webmin_menu(theme_settings(), \%ask);
|
||||
my $sel = theme_menu_selection(\@items);
|
||||
foreach my $k (keys %asked) {
|
||||
next if ($sel->{$k} eq '');
|
||||
$changed{$k}++ if ($sel->{$k} ne $state{$k});
|
||||
$state{$k} = $sel->{$k};
|
||||
}
|
||||
}
|
||||
|
||||
# Work out the page to open next
|
||||
my $url;
|
||||
my $field = $in{'field'};
|
||||
if (theme_menu_local_url($in{'goto'}) && $field =~ /^\w+$/) {
|
||||
# The field names a page for its value, such as a domain summary
|
||||
my $value = $field =~ /^(dom|sid)$/ && $state{$field} ne '' ?
|
||||
$state{$field} : $in{$field};
|
||||
$url = $in{'goto'}.urlize($value);
|
||||
}
|
||||
elsif ($changed{'dom'} || $changed{'sid'}) {
|
||||
# A page showing the new domain or system
|
||||
my $k = $changed{'dom'} ? 'dom' : 'sid';
|
||||
$url = $theme_menu_pages{$k}.urlize($state{$k});
|
||||
}
|
||||
elsif (theme_menu_local_url($in{'return'})) {
|
||||
# Back to the index of the module the menu was on, or to the system
|
||||
# information page
|
||||
$url = $in{'return'};
|
||||
}
|
||||
else {
|
||||
# The system information page
|
||||
$url = "/right.cgi";
|
||||
}
|
||||
|
||||
# Store the choices and redirect with a path only, so the browser stays on
|
||||
# the host it used
|
||||
print "Set-Cookie: ".theme_menu_cookie_string(\%state)."\n";
|
||||
theme_local_redirect($url);
|
||||
@@ -7,6 +7,10 @@ no warnings 'redefine';
|
||||
no warnings 'uninitialized';
|
||||
# The language table of the system status module is read once below
|
||||
no warnings 'once';
|
||||
# The page is the start page, which the browser opens without a Referer
|
||||
# when the address was typed, as / redirects to it in the single page
|
||||
# layout. A request with a Referer from another site is still refused.
|
||||
our $trust_unknown_referers = 2;
|
||||
require "gray-theme/gray-theme-lib.pl";
|
||||
require "gray-theme/theme.pl";
|
||||
&ReadParse();
|
||||
@@ -25,8 +29,10 @@ if ($redir) {
|
||||
return;
|
||||
}
|
||||
|
||||
# The body class selects the right frame styles
|
||||
# The body class selects the right frame styles. In the single page layout
|
||||
# this page shows the menu, even though it uses the popup header.
|
||||
our $gray_theme_body_class = 'right-frame';
|
||||
our $gray_theme_menu_page = 1;
|
||||
&popup_header($text{'left_home'});
|
||||
|
||||
# Links from modules become the page actions, with an icon where the
|
||||
|
||||
@@ -27,6 +27,9 @@ else {
|
||||
}
|
||||
$sect->{'list'} = $in{'list'};
|
||||
$sect->{'tab'} = $in{'tab'};
|
||||
# Page layout, framed or a single page
|
||||
$in{'layout'} =~ /^(single|)$/ || &error($text{'edright_elayout'});
|
||||
$sect->{'layout'} = $in{'layout'};
|
||||
if ($in{'fsize_def'}) {
|
||||
delete($sect->{'fsize'});
|
||||
}
|
||||
@@ -61,5 +64,16 @@ if ($hasvirt && &virtual_server::master_admin() ||
|
||||
|
||||
# Save config
|
||||
&save_right_frame_sections($sect);
|
||||
&redirect("right.cgi");
|
||||
if ($sect->{'layout'} ne $sects->{'layout'}) {
|
||||
# The layout changed, so reload the whole window to drop the frameset
|
||||
# or bring it back
|
||||
$main::gray_theme_settings = undef;
|
||||
&popup_header();
|
||||
print &js_redirect($sect->{'layout'} ? "/right.cgi" : "/", "top");
|
||||
&popup_footer();
|
||||
}
|
||||
else {
|
||||
# Back to the system information page
|
||||
&redirect("right.cgi");
|
||||
}
|
||||
|
||||
|
||||
34
gray-theme/theme-init.pl
Normal file
34
gray-theme/theme-init.pl
Normal file
@@ -0,0 +1,34 @@
|
||||
# Functions that read the theme settings. The core loads this file while it
|
||||
# sets up every page, as the oofunctions file named in the theme config,
|
||||
# and theme.pl loads it too. Reading the settings that early matters on
|
||||
# pages that then switch to a Unix user, such as File Manager for a domain
|
||||
# owner: that user cannot read the root-only settings files.
|
||||
|
||||
# theme_post_init_config()
|
||||
# Called by the core at the end of init_config, while the page still runs
|
||||
# as root. Reads the theme settings, which the page then keeps.
|
||||
sub theme_post_init_config
|
||||
{
|
||||
&theme_settings();
|
||||
}
|
||||
|
||||
# theme_settings()
|
||||
# Returns the theme settings as a hash ref: the user's own, or the global
|
||||
# ones when those apply to everyone or the user has none. They are stored
|
||||
# with the system information page settings and read once per page.
|
||||
sub theme_settings
|
||||
{
|
||||
return $main::gray_theme_settings if ($main::gray_theme_settings);
|
||||
my $file = "$config_directory/$current_theme/sections";
|
||||
my %sects;
|
||||
&read_file($file, \%sects);
|
||||
if (!$sects{'global'}) {
|
||||
# The user's own file replaces the global settings
|
||||
my %usects;
|
||||
%sects = %usects if (&read_file("$file.$remote_user", \%usects));
|
||||
}
|
||||
$main::gray_theme_settings = \%sects;
|
||||
return $main::gray_theme_settings;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -16,6 +16,10 @@ $main::nosingledomain_virtualmin_mode = 1;
|
||||
|
||||
our $ui_formcount;
|
||||
|
||||
# The functions that read the theme settings, unless the core has already
|
||||
# loaded them into this package
|
||||
do "$theme_root_directory/theme-init.pl" if (!defined(&theme_settings));
|
||||
|
||||
# Global state for wrapper
|
||||
# if 0, wrapper isn't on, add one and open it, if 1 close it, if 2+, subtract
|
||||
# but don't close
|
||||
@@ -131,10 +135,17 @@ else {
|
||||
}
|
||||
|
||||
# theme_post_save_domain(&domain, action)
|
||||
# Called by Virtualmin after a domain is updated, to refresh the left menu
|
||||
# Called by Virtualmin after a domain is updated, to refresh the left menu.
|
||||
# In the single page layout, a new domain becomes the one the menu shows.
|
||||
sub theme_post_save_domain
|
||||
{
|
||||
local ($d, $action) = @_;
|
||||
if (&theme_single_page()) {
|
||||
# The next page builds the menu again, showing a new domain
|
||||
print &theme_menu_remember({ 'dom' => $d->{'id'} })
|
||||
if ($action eq 'create');
|
||||
return;
|
||||
}
|
||||
# Refresh left side, in case options have changed
|
||||
print "<script>\n";
|
||||
if ($action eq 'create') {
|
||||
@@ -152,6 +163,8 @@ print "</script>\n";
|
||||
# Called after multiple domains are updated, to refresh the left menu
|
||||
sub theme_post_save_domains
|
||||
{
|
||||
# The single page layout builds the menu again on the next page
|
||||
return if (&theme_single_page());
|
||||
print "<script>\n";
|
||||
print "top.left.location = top.left.location;\n";
|
||||
print "</script>\n";
|
||||
@@ -162,8 +175,12 @@ print "</script>\n";
|
||||
sub theme_post_save_server
|
||||
{
|
||||
local ($s, $action) = @_;
|
||||
# The single page layout builds the menu again on the next page
|
||||
return if (&theme_single_page());
|
||||
if ($action eq 'create' || $action eq 'delete' ||
|
||||
!$done_theme_post_save_server++) {
|
||||
# Refresh the left frame after a system is added or removed, and once
|
||||
# per page otherwise
|
||||
print "<script>\n";
|
||||
print "top.left.location = top.left.location;\n";
|
||||
print "</script>\n";
|
||||
@@ -171,11 +188,18 @@ if ($action eq 'create' || $action eq 'delete' ||
|
||||
}
|
||||
|
||||
# theme_select_server(&server)
|
||||
# Called by Cloudmin when a page for a server is displayed, to select it on the
|
||||
# left menu.
|
||||
# Called by Cloudmin when a page for a server is displayed, to select it on
|
||||
# the left menu. In the single page layout, the menu shows it from the next
|
||||
# page on.
|
||||
sub theme_select_server
|
||||
{
|
||||
local ($server) = @_;
|
||||
if (&theme_single_page()) {
|
||||
# This page's menu is already built, so only remember the system for
|
||||
# the next pages
|
||||
print &theme_menu_remember({ 'sid' => $server->{'id'} });
|
||||
return;
|
||||
}
|
||||
print <<EOF;
|
||||
<script>
|
||||
if (window.parent && window.parent.frames[0]) {
|
||||
@@ -198,11 +222,18 @@ EOF
|
||||
}
|
||||
|
||||
# theme_select_domain(&domain)
|
||||
# Called by Virtualmin when a page for a server is displayed, to select it on
|
||||
# the left menu.
|
||||
# Called by Virtualmin when a page for a domain is displayed, to select it
|
||||
# on the left menu. In the single page layout, the menu shows it from the
|
||||
# next page on.
|
||||
sub theme_select_domain
|
||||
{
|
||||
local ($d) = @_;
|
||||
if (&theme_single_page()) {
|
||||
# This page's menu is already built, so only remember the domain for
|
||||
# the next pages
|
||||
print &theme_menu_remember({ 'dom' => $d->{'id'} });
|
||||
return;
|
||||
}
|
||||
print <<EOF;
|
||||
<script>
|
||||
if (window.parent && window.parent.frames[0]) {
|
||||
@@ -227,6 +258,8 @@ EOF
|
||||
sub theme_post_save_folder
|
||||
{
|
||||
local ($folder, $action) = @_;
|
||||
# The single page layout builds the menu again on the next page
|
||||
return if (&theme_single_page());
|
||||
my $ref;
|
||||
if ($action eq 'create' || $action eq 'delete' || $action eq 'modify') {
|
||||
# Always refresh
|
||||
@@ -240,17 +273,24 @@ else {
|
||||
}
|
||||
}
|
||||
if ($ref) {
|
||||
# Reload the left frame
|
||||
print "<script>\n";
|
||||
print "top.frames[0].document.location = top.frames[0].document.location;\n";
|
||||
print "</script>\n";
|
||||
}
|
||||
}
|
||||
|
||||
# theme_post_change_modules()
|
||||
# Called after modules are installed, removed or refreshed, to reload the
|
||||
# left frame when it lists the Webmin modules
|
||||
sub theme_post_change_modules
|
||||
{
|
||||
# The single page layout builds the menu again on the next page
|
||||
return if (&theme_single_page());
|
||||
print <<EOF;
|
||||
<script>
|
||||
var url = '' + top.left.location;
|
||||
// Only the menu of the Webmin modules lists them
|
||||
if (url.indexOf('mode=modules') > 0) {
|
||||
top.left.location = url;
|
||||
}
|
||||
@@ -260,20 +300,31 @@ EOF
|
||||
|
||||
# theme_prebody(header-args...)
|
||||
# Hides the module index link on Virtualmin pages, whose menu is already
|
||||
# in the left frame
|
||||
# shown beside the page, and prints the menu of the single page layout
|
||||
sub theme_prebody
|
||||
{
|
||||
if (get_module_name() eq "virtual-server") {
|
||||
# No need for Module Index link, as we have the left-side frame
|
||||
# No need for Module Index link, as the menu already shows Virtualmin
|
||||
$tconfig{'nomoduleindex'} = 1;
|
||||
}
|
||||
# Print the menu if theme_prehead chose to show it on this page
|
||||
print &theme_page_menu() if ($main::gray_theme_page_menu);
|
||||
}
|
||||
|
||||
# theme_popup_prebody(popup-header-args...)
|
||||
# Prints the menu of the single page layout on a theme page that asks for
|
||||
# it, such as the system information page. Real popups never get it.
|
||||
sub theme_popup_prebody
|
||||
{
|
||||
print &theme_page_menu() if ($main::gray_theme_page_menu);
|
||||
}
|
||||
|
||||
# theme_prehead()
|
||||
# Prints the head parts of every page: the color scheme meta, the scheme
|
||||
# and frame scripts, and the links to the stylesheet and the table sorting
|
||||
# script. Also sets the body attributes the stylesheet keys on: the module
|
||||
# name as a class and the forced color scheme, if any.
|
||||
# name as a class and the forced color scheme, if any. In the single page
|
||||
# layout it also decides whether the page gets the menu.
|
||||
sub theme_prehead
|
||||
{
|
||||
my $pfx = &get_webprefix();
|
||||
@@ -286,10 +337,205 @@ print "<link rel='stylesheet' type='text/css' href='$pfx/unauthenticated/".
|
||||
print "<script type='text/javascript' src='$pfx/unauthenticated/".
|
||||
"sorttable.js?".&theme_asset_key("sorttable.js")."'></script>\n";
|
||||
my @cls = grep { $_ } ( $main::gray_theme_body_class, &get_module_name() );
|
||||
|
||||
# Decide whether this page shows the menu of the single page layout. The
|
||||
# body hooks read the result.
|
||||
my $menu = &theme_page_menu_wanted();
|
||||
if ($menu && &theme_one_module()) {
|
||||
# Webmin opens this user's only module directly, so a menu would
|
||||
# list just that module. Skip it, and let the core header show its
|
||||
# logout link, which the theme otherwise hides with the index link.
|
||||
$menu = 0;
|
||||
$tconfig{'noindex'} = 0;
|
||||
}
|
||||
$main::gray_theme_page_menu = $menu;
|
||||
if ($menu) {
|
||||
# Leave room for the menu, as wide as the left frame. A page that
|
||||
# still loads in a frame, such as in the old frameset right after the
|
||||
# layout changes, hides the menu.
|
||||
push(@cls, 'single-page');
|
||||
print "<style>:root { --menu-width: ".&theme_menu_width()."px; }".
|
||||
"</style>\n";
|
||||
# A window that a script opened as a popup, with no menu bar of its
|
||||
# own, such as a chooser, hides the menu too
|
||||
print "<script type='text/javascript'>if (self != top || ".
|
||||
"window.opener && window.menubar && !window.menubar.visible) ".
|
||||
"document.documentElement.classList.add('menu-framed');".
|
||||
"</script>\n";
|
||||
# Phones lay the page out at their own width, so the menu folds into
|
||||
# its bar there
|
||||
print "<meta name='viewport' ".
|
||||
"content='width=device-width, initial-scale=1'>\n";
|
||||
# The product's icons, as the frameset page has them
|
||||
print &theme_favicons();
|
||||
}
|
||||
$tconfig{'inbody'} = (@cls ? "class='".join(" ", @cls)."'" : "").
|
||||
($scheme ? " data-scheme='$scheme'" : "");
|
||||
}
|
||||
|
||||
# theme_page_menu_wanted()
|
||||
# Returns 1 if this page should show the menu of the single page layout:
|
||||
# the layout is on, a user is logged in, and the page is not a popup. Pages
|
||||
# loaded into a frame, an iframe or by a script get no menu either; browsers
|
||||
# report this in the Sec-Fetch-Dest header. If the menu code cannot be
|
||||
# loaded, the page shows no menu instead of an empty space.
|
||||
sub theme_page_menu_wanted
|
||||
{
|
||||
return 0 if (!&theme_single_page());
|
||||
return 0 if ($main::gray_theme_popup);
|
||||
return 0 if (!$remote_user || $ENV{'ANONYMOUS_USER'});
|
||||
my $dest = lc($ENV{'HTTP_SEC_FETCH_DEST'});
|
||||
return 0 if ($dest && $dest ne 'document');
|
||||
return &theme_menu_lib();
|
||||
}
|
||||
|
||||
# theme_one_module()
|
||||
# Returns 1 if Webmin is set to open a user's only module directly, the
|
||||
# user has exactly one module, and it has no menu of its own. The modules
|
||||
# are counted the way the core header counts them when it decides to show
|
||||
# its logout link, hidden ones included, so the page never loses both the
|
||||
# menu and that link. A user with a hidden module as well keeps the menu,
|
||||
# which has its own logout link.
|
||||
sub theme_one_module
|
||||
{
|
||||
return 0 if (!$gconfig{'gotoone'});
|
||||
my @avail = &get_available_module_infos(1);
|
||||
return 0 if (@avail != 1);
|
||||
my $dir = &module_root_directory($avail[0]->{'dir'});
|
||||
return -r "$dir/webmin_menu.pl" ? 0 : 1;
|
||||
}
|
||||
|
||||
# theme_page_menu()
|
||||
# Returns the menu of the single page layout. theme_prehead has already
|
||||
# decided to show it and loaded the menu code. A page that switched to a
|
||||
# Unix user other than root gets a panel that loads its menu instead.
|
||||
sub theme_page_menu
|
||||
{
|
||||
return $> ? &theme_menu_placeholder() : &theme_menu_html({ 'single' => 1 });
|
||||
}
|
||||
|
||||
# theme_menu_lib()
|
||||
# Loads the functions that build the menu, once. Returns 1 when they are
|
||||
# available.
|
||||
sub theme_menu_lib
|
||||
{
|
||||
if (!defined(&theme_menu_html)) {
|
||||
# do loads it into the package of this copy of theme.pl: WebminCore
|
||||
# when the core calls the theme's hooks, as on module pages and the
|
||||
# system information page, and main when a theme CGI such as left.cgi
|
||||
# or menu.cgi calls it directly
|
||||
do "$theme_root_directory/menu-lib.pl";
|
||||
}
|
||||
return defined(&theme_menu_html) ? 1 : 0;
|
||||
}
|
||||
|
||||
# theme_single_page()
|
||||
# Returns 1 if the theme settings choose the single page layout, with the
|
||||
# menu on every page instead of in a frame of its own. A server opened
|
||||
# through Webmin Servers Index keeps the frameset, as that index rewrites
|
||||
# only the links and redirects of framed pages.
|
||||
sub theme_single_page
|
||||
{
|
||||
return 0 if ($ENV{'HTTP_WEBMIN_SERVERS'} || $ENV{'HTTP_WEBMIN_PATH'});
|
||||
return &theme_settings()->{'layout'} eq 'single' ? 1 : 0;
|
||||
}
|
||||
|
||||
# theme_favicons()
|
||||
# Returns the links to the icons of the product, Cloudmin, Virtualmin,
|
||||
# Usermin or Webmin, in three sizes
|
||||
sub theme_favicons
|
||||
{
|
||||
my $prod = &foreign_available("server-manager") ? 'cloudmin' :
|
||||
&foreign_available("virtual-server") ? 'virtualmin' :
|
||||
&get_product_name() eq 'usermin' ? 'usermin' : 'webmin';
|
||||
my $dir = &get_webprefix()."/images/favicons/$prod";
|
||||
return join("", map { "<link rel='icon' type='image/png' sizes='${_}x$_' ".
|
||||
"href='$dir/favicon-${_}x$_.png'>\n" } (16, 32, 192));
|
||||
}
|
||||
|
||||
# theme_menu_width()
|
||||
# Returns the menu width in pixels: the width of the left frame, or of the
|
||||
# space the menu takes in the single page layout
|
||||
sub theme_menu_width
|
||||
{
|
||||
my $fsize = &theme_settings()->{'fsize'};
|
||||
return $fsize =~ /^(\d+)$/ ? $1 :
|
||||
&get_product_name() eq 'usermin' ? 200 :
|
||||
&foreign_available("server-manager") &&
|
||||
&foreign_available("virtual-server") ? 280 : 260;
|
||||
}
|
||||
|
||||
# theme_menu_cookie()
|
||||
# Returns the name of the cookie that remembers the menu choices of the
|
||||
# single page layout, and the path it is set for. The name holds the port,
|
||||
# so that Webmin and Usermin on one host keep their own choices.
|
||||
sub theme_menu_cookie
|
||||
{
|
||||
my $port = $ENV{'SERVER_PORT'} =~ /^(\d+)$/ ? $1 : "";
|
||||
# Use the URL prefix as the path, or / when there is none or it has
|
||||
# characters that are unsafe in a cookie
|
||||
my $path = &get_webprefix();
|
||||
$path = "/" if ($path !~ /^\/[\w\/.~\-]*$/);
|
||||
return ("gray_theme_menu$port", $path);
|
||||
}
|
||||
|
||||
# theme_menu_state()
|
||||
# Returns the menu choices of the single page layout as a hash ref: the
|
||||
# menu mode, the Virtualmin domain ID and the Cloudmin system ID. They
|
||||
# come from the cookie, or from a later change on this page.
|
||||
sub theme_menu_state
|
||||
{
|
||||
return $main::gray_theme_menu_state if ($main::gray_theme_menu_state);
|
||||
my ($name) = &theme_menu_cookie();
|
||||
my %state;
|
||||
if ($ENV{'HTTP_COOKIE'} =~ /(?:^|;\s*)\Q$name\E=([^;\s]*)/) {
|
||||
# The value holds key=value pairs joined with &, all URL-encoded
|
||||
foreach my $kv (split(/&/, &un_urlize($1))) {
|
||||
my ($k, $v) = split(/=/, $kv, 2);
|
||||
$state{$k} = $v if ($k =~ /^(mode|dom|sid)$/ &&
|
||||
$v =~ /^[\w.\-]+$/);
|
||||
}
|
||||
}
|
||||
$main::gray_theme_menu_state = \%state;
|
||||
return $main::gray_theme_menu_state;
|
||||
}
|
||||
|
||||
# theme_menu_cookie_string(&state)
|
||||
# Returns the cookie that stores the menu choices, with its attributes, in
|
||||
# the form both a Set-Cookie header and document.cookie take. It lasts
|
||||
# until the browser closes.
|
||||
sub theme_menu_cookie_string
|
||||
{
|
||||
my ($state) = @_;
|
||||
my ($name, $path) = &theme_menu_cookie();
|
||||
# Only the known choices, with values that need no quoting
|
||||
my $value = join("&", map { "$_=$state->{$_}" }
|
||||
grep { $state->{$_} =~ /^[\w.\-]+$/ }
|
||||
('mode', 'dom', 'sid'));
|
||||
return "$name=".&urlize($value)."; path=$path; SameSite=Lax".
|
||||
(uc($ENV{'HTTPS'}) eq 'ON' ? "; Secure" : "");
|
||||
}
|
||||
|
||||
# theme_menu_remember(&choices)
|
||||
# Returns a script that makes menu choices, such as a domain ID, the ones
|
||||
# of this page, and stores them in the cookie of the single page layout
|
||||
# when it does not hold them yet. The menu script puts the page's choices
|
||||
# back into the cookie whenever its tab comes back into use, so that tabs
|
||||
# keep their own choices.
|
||||
sub theme_menu_remember
|
||||
{
|
||||
my ($choices) = @_;
|
||||
my $state = &theme_menu_state();
|
||||
my %new = ( %$state, %$choices );
|
||||
my $cookie = &theme_menu_cookie_string(\%new);
|
||||
my $changed = $cookie ne &theme_menu_cookie_string($state);
|
||||
$main::gray_theme_menu_state = \%new;
|
||||
return "<script type='text/javascript'>".
|
||||
"window.grayThemeMenuCookie = '$cookie';".
|
||||
($changed ? " document.cookie = '$cookie';" : "").
|
||||
"</script>\n";
|
||||
}
|
||||
|
||||
# theme_scheme_script()
|
||||
# Returns a script that keeps the color scheme steady when none is forced.
|
||||
# A browser can report the system scheme for a moment before settling on
|
||||
@@ -355,18 +601,8 @@ EOF
|
||||
# system information page settings, per user unless they are global.
|
||||
sub theme_color_scheme
|
||||
{
|
||||
return $main::gray_theme_scheme if (defined($main::gray_theme_scheme));
|
||||
my $file = "$config_directory/$current_theme/sections";
|
||||
my %sects;
|
||||
&read_file($file, \%sects);
|
||||
if (!$sects{'global'}) {
|
||||
# Unless the global settings are forced on everyone, the user's own
|
||||
# file replaces them
|
||||
my %usects;
|
||||
%sects = %usects if (&read_file("$file.$remote_user", \%usects));
|
||||
}
|
||||
$main::gray_theme_scheme = $sects{'scheme'} =~ /^(light|dark)$/ ? $1 : "";
|
||||
return $main::gray_theme_scheme;
|
||||
my $scheme = &theme_settings()->{'scheme'};
|
||||
return $scheme =~ /^(light|dark)$/ ? $1 : "";
|
||||
}
|
||||
|
||||
# theme_asset_key(file)
|
||||
@@ -391,10 +627,19 @@ return "$tags class='$class'";
|
||||
}
|
||||
|
||||
# theme_popup_prehead(title, ...)
|
||||
# Popup windows and the frames get the same head as ordinary pages
|
||||
# Popup windows and frames get the same head as ordinary pages. A theme
|
||||
# page that uses the popup header but asks for the menu, such as the system
|
||||
# information page, is not treated as a popup.
|
||||
sub theme_popup_prehead
|
||||
{
|
||||
return &theme_prehead();
|
||||
local $main::gray_theme_popup = !$main::gray_theme_menu_page;
|
||||
&theme_prehead();
|
||||
if ($main::gray_theme_page_menu &&
|
||||
$current_lang_info->{'dir'} =~ /^(rtl|ltr)$/) {
|
||||
# The popup header sets no text direction on the body, but the menu
|
||||
# needs one to stay on the same side as on other pages
|
||||
$tconfig{'inbody'} .= " dir='$1'";
|
||||
}
|
||||
}
|
||||
|
||||
# theme_ui_table_start(heading, [tabletags], [cols], [&default-tds],
|
||||
@@ -1038,6 +1283,25 @@ if (get_module_name() eq "virtual-server" && $orig eq "" &&
|
||||
print "Location: $url\n\n";
|
||||
}
|
||||
|
||||
# theme_local_redirect(path)
|
||||
# Prints a redirect to a path on this server, such as /right.cgi, with the
|
||||
# path alone in the Location header. The browser then stays on the host and
|
||||
# port it used. The core redirect builds a full URL from the redirect host
|
||||
# in the Webmin settings, which a browser using another address may not
|
||||
# reach, so the page hangs.
|
||||
sub theme_local_redirect
|
||||
{
|
||||
my ($path) = @_;
|
||||
if ($gconfig{'webprefixnoredir'}) {
|
||||
# Webmin is set not to add the URL prefix to redirects, because a
|
||||
# proxy rewrites the full URLs of its redirects; the core redirect
|
||||
# gives it those
|
||||
&redirect($path);
|
||||
return;
|
||||
}
|
||||
print "Location: ".&get_webprefix()."$path\n\n";
|
||||
}
|
||||
|
||||
# theme_ui_hidden_javascript()
|
||||
# Returns the script that switches tabs and opens hidden sections. Both
|
||||
# only change classes; the stylesheet does the rest.
|
||||
|
||||
@@ -406,7 +406,8 @@ details.section > summary::after {
|
||||
details.menu-cat > summary::after {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin: 0 5px 1px 0;
|
||||
margin-block: 0 1px;
|
||||
margin-inline: 0 5px;
|
||||
}
|
||||
details[open].ui_hidden_table_start > summary::before,
|
||||
details[open].section > summary::after,
|
||||
@@ -651,11 +652,12 @@ table.ui_columns td > .ui_links_row { margin: 0; }
|
||||
/* ---- Module page header ---- */
|
||||
/* The header table printed by header() becomes the page title with the
|
||||
* module links beside it, on the page ground. It bleeds over the page
|
||||
* padding and stays at the top while the page scrolls. */
|
||||
* padding and stays at the top while the page scrolls, or under the menu
|
||||
* bar of the single page layout on a narrow screen. */
|
||||
|
||||
table.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: var(--sticky-top, 0);
|
||||
z-index: 5;
|
||||
display: block;
|
||||
width: auto;
|
||||
@@ -1710,8 +1712,8 @@ html.loading-right .menu-loader { display: block; }
|
||||
.mode.loading { pointer-events: none; }
|
||||
.mode.loading > b:not(.active) { opacity: 0.5; }
|
||||
.mode.loading > b.active { color: var(--text-color); }
|
||||
/* The ring sits 6px left of the name. Its right margin and its negative
|
||||
* left margin cancel its own width, so it takes no room in the centered
|
||||
/* The ring sits 6px before the name. Its end margin and its negative
|
||||
* start margin cancel its own width, so it takes no room in the centered
|
||||
* row and the name does not move when it appears */
|
||||
.mode.loading > b.active > a::before {
|
||||
content: "";
|
||||
@@ -1719,7 +1721,8 @@ html.loading-right .menu-loader { display: block; }
|
||||
box-sizing: border-box;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 0 6px 0 -16px;
|
||||
margin-block: 0;
|
||||
margin-inline: -16px 6px;
|
||||
border: 2px solid var(--border-color-darker);
|
||||
border-top-color: var(--accent-color);
|
||||
border-radius: 50%;
|
||||
@@ -1773,7 +1776,8 @@ html.loading-right .menu-loader { display: block; }
|
||||
/* Menu rows: 26px links that highlight under the mouse */
|
||||
.menu-link {
|
||||
display: block;
|
||||
padding: 4px 8px 4px 12px;
|
||||
padding-block: 4px;
|
||||
padding-inline: 12px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 18px;
|
||||
color: var(--text-color);
|
||||
@@ -1785,7 +1789,7 @@ html.loading-right .menu-loader { display: block; }
|
||||
text-decoration: none;
|
||||
}
|
||||
/* Links inside a category are indented under its name */
|
||||
.menu-link.menu-sub { padding-left: 24px; }
|
||||
.menu-link.menu-sub { padding-inline-start: 24px; }
|
||||
/* The logout link stands a little apart, muted, with its icon before
|
||||
* the text like any other icon in a link */
|
||||
.menu-link.menu-logout {
|
||||
@@ -1793,7 +1797,7 @@ html.loading-right .menu-loader { display: block; }
|
||||
color: var(--text-color-muted);
|
||||
}
|
||||
.menu-link svg {
|
||||
margin-right: 6px;
|
||||
margin-inline-end: 6px;
|
||||
vertical-align: -3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
@@ -1815,7 +1819,8 @@ details.menu-cat > summary {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
height: 28px;
|
||||
padding: 0 10px 0 12px;
|
||||
padding-block: 0;
|
||||
padding-inline: 12px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-color);
|
||||
list-style: none;
|
||||
@@ -1842,6 +1847,134 @@ details.menu-cat[open] { margin-bottom: 4px; }
|
||||
color: var(--text-color-muted);
|
||||
}
|
||||
.menu-html img { max-width: 100%; }
|
||||
/* The link to the current page, marked in the single page layout */
|
||||
.menu-link.active,
|
||||
.menu-link.active:hover {
|
||||
background: var(--element-highlight-bg-color);
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- Single page layout ---- */
|
||||
/* Without frames, every page carries the menu panel at the start of its
|
||||
* body. The panel is fixed at the left with the same 7px gap as in
|
||||
* the left frame, and the body leaves as much room for it as the left
|
||||
* frame would take, so a page looks just as it does in the frameset. The
|
||||
* page and the menu scroll separately. The logical properties put the
|
||||
* menu at the right for right-to-left languages. */
|
||||
body.single-page {
|
||||
padding-inline-start: calc(var(--menu-width, 260px) + var(--page-pad-x));
|
||||
}
|
||||
/* The system information page pads its own content */
|
||||
body.right-frame.single-page {
|
||||
padding-inline-start: var(--menu-width, 260px);
|
||||
}
|
||||
.page-menu {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
inset-inline-start: 7px;
|
||||
width: calc(var(--menu-width, 260px) - 7px);
|
||||
height: auto;
|
||||
}
|
||||
/* The button that opens the menu on a narrow screen, square and with an
|
||||
* icon only; it shows a cross while the menu is open */
|
||||
.page-menu .menu-toggle {
|
||||
display: none;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
min-height: 0;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.page-menu .menu-toggle svg { display: block; }
|
||||
.page-menu .menu-toggle .menu-toggle-close,
|
||||
.page-menu.open .menu-toggle .menu-toggle-open { display: none; }
|
||||
.page-menu.open .menu-toggle .menu-toggle-close { display: block; }
|
||||
/* A page that still ends up in a frame, such as in the old frameset right
|
||||
* after the layout was changed, hides its menu */
|
||||
html.menu-framed .page-menu { display: none; }
|
||||
html.menu-framed body.single-page {
|
||||
--menu-bar-height: 0px;
|
||||
padding-inline-start: var(--page-pad-x);
|
||||
}
|
||||
html.menu-framed body.right-frame.single-page { padding-inline-start: 0; }
|
||||
|
||||
/* On a narrow screen the menu is a bar across the top with the logo and
|
||||
* the button, and the pages start under it; their sticky headers stop
|
||||
* under it too. The button unfolds the bar into the whole menu, which
|
||||
* covers the screen and scrolls, while the page behind it stays put. */
|
||||
@media (max-width: 800px) {
|
||||
body.single-page {
|
||||
--menu-bar-height: 52px;
|
||||
--sticky-top: var(--menu-bar-height);
|
||||
padding-top: calc(var(--menu-bar-height) + var(--page-pad-top));
|
||||
padding-inline-start: var(--page-pad-x);
|
||||
}
|
||||
body.right-frame.single-page {
|
||||
padding-top: var(--menu-bar-height);
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
body.single-page:has(> .page-menu.open) { overflow: hidden; }
|
||||
/* The bar has no rounded corners, and a hairline and a soft shadow
|
||||
* along its bottom edge instead of a border, so that it is exactly
|
||||
* as tall as its brand row */
|
||||
.page-menu {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
inset-inline: 0;
|
||||
width: auto;
|
||||
max-height: 100vh;
|
||||
max-height: 100dvh;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 1px 0 var(--border-color-panel),
|
||||
0 2px 6px var(--shadow-color);
|
||||
}
|
||||
.page-menu.open {
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* The brand row: the logo at the start, the button at the end, and
|
||||
* the host and login lines under them while the menu is open */
|
||||
.page-menu .menu-brand {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
box-sizing: border-box;
|
||||
min-height: var(--menu-bar-height);
|
||||
padding-block: 0;
|
||||
padding-inline: 16px 10px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.page-menu .menu-brand-name {
|
||||
flex: 1 1 auto;
|
||||
margin: 0;
|
||||
}
|
||||
.page-menu .menu-brand-logo { height: 28px; }
|
||||
.page-menu .menu-toggle { display: inline-flex; }
|
||||
.page-menu .menu-meta {
|
||||
flex: 1 0 100%;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
.page-menu.open .menu-brand {
|
||||
border-bottom: 1px solid var(--border-color-frame);
|
||||
}
|
||||
/* The folded bar shows the brand row alone */
|
||||
.page-menu:not(.open) :is(.menu-meta, .mode, .leftmenu) {
|
||||
display: none;
|
||||
}
|
||||
.page-menu:not(.open) .menu-top {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- System information page (right.cgi) ---- */
|
||||
/* The page is a widget page: its header holds the title, the operating
|
||||
@@ -1858,11 +1991,12 @@ body.right-frame { padding: 0; }
|
||||
}
|
||||
.right-frame .ui_page.page > * { margin: 0; }
|
||||
.right-frame .ui_page.page > * + * { margin-top: var(--ui-gap); }
|
||||
/* The header bleeds over the page padding; its selector outranks the
|
||||
/* The header bleeds over the page padding and stays at the top, or under
|
||||
* the menu bar of the single page layout; its selector outranks the
|
||||
* margin reset above */
|
||||
.right-frame .ui_page.page > .ui_page_head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: var(--sticky-top, 0);
|
||||
z-index: 5;
|
||||
align-items: center;
|
||||
margin: 0 calc(-1 * var(--page-pad-x)) 12px;
|
||||
@@ -2319,14 +2453,15 @@ form.ui_form[action="uconfig.cgi"] {
|
||||
* scroll. The module sizes the terminal from the whole viewport with no
|
||||
* room for the header, so the page scrolled, and the scrollbar widened
|
||||
* the terminal past the frame. The 66px are the sticky header and its
|
||||
* margin; the rules outrank the module's own by specificity. */
|
||||
* margin, plus the menu bar of the single page layout on a narrow screen;
|
||||
* the rules outrank the module's own by specificity. */
|
||||
body.xterm { overflow: hidden; }
|
||||
body.xterm #terminal {
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
height: calc(100vh - 66px - var(--card-gap));
|
||||
height: calc(100vh - 66px - var(--card-gap) - var(--menu-bar-height, 0px));
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
@@ -2510,6 +2645,11 @@ form[action="config_save.cgi"] #sortableTable2 > thead > tr > td {
|
||||
|
||||
@media print {
|
||||
body { background: #fff; color: #000; }
|
||||
.page-menu { display: none; }
|
||||
body.single-page, body.right-frame.single-page {
|
||||
padding-top: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
body > .header,
|
||||
body > .header ~ center,
|
||||
.ui_form ~ a,
|
||||
|
||||
Reference in New Issue
Block a user