=head1 web-lib-funcs.pl
Common functions for Webmin CGI scripts. This file gets in-directly included
by all scripts that use web-lib.pl.
Example code:
use WebminCore;
init_config();
ui_print_header(undef, 'My Module', '');
print 'This is Webmin version ',get_webmin_version(),'
\n';
ui_print_footer();
=cut
##use warnings;
use Socket;
use POSIX;
eval "use Socket6";
$ipv6_module_error = $@;
our $error_handler_funcs = [ ];
use vars qw($loaded_theme_library $wait_for_input
$done_webmin_header $trust_unknown_referers $unsafe_index_cgi
%done_foreign_require $webmin_feedback_address
$pragma_no_cache $foreign_args);
# Globals
use vars qw($module_index_name $number_to_month_map $month_to_number_map
$umask_already $default_charset $licence_status $os_type
$licence_message $script_name $loaded_theme_oo_library
$done_web_lib_funcs $os_version $module_index_link
$called_from_webmin_core $ipv6_module_error);
=head2 read_file(file, &hash, [&order], [lowercase], [split-char])
Fill the given hash reference with name=value pairs from a file. The required
parameters are :
=item file - The file to head, which must be text with each line like name=value
=item hash - The hash reference to add values read from the file to.
=item order - If given, an array reference to add names to in the order they were read
=item lowercase - If set to 1, names are converted to lower case
=item split-char - If set, names and values are split on this character instead of =
=cut
sub read_file
{
local $_;
my $split = defined($_[4]) ? $_[4] : "=";
my $realfile = &translate_filename($_[0]);
&open_readfile(ARFILE, $_[0]) || return 0;
while() {
chomp;
my $hash = index($_, "#");
my $eq = index($_, $split);
if ($hash != 0 && $eq >= 0) {
my $n = substr($_, 0, $eq);
my $v = substr($_, $eq+1);
chomp($v);
$_[1]->{$_[3] ? lc($n) : $n} = $v;
push(@{$_[2]}, $n) if ($_[2]);
}
}
close(ARFILE);
$main::read_file_missing{$realfile} = 0; # It exists now
if (defined($main::read_file_cache{$realfile})) {
%{$main::read_file_cache{$realfile}} = %{$_[1]};
}
return 1;
}
=head2 read_file_cached(file, &hash, [&order], [lowercase], [split-char])
Like read_file, but reads from an in-memory cache if the file has already been
read in this Webmin script. Recommended, as it behaves exactly the same as
read_file, but can be much faster.
=cut
sub read_file_cached
{
my $realfile = &translate_filename($_[0]);
if (defined($main::read_file_cache{$realfile})) {
# Use cached data
%{$_[1]} = ( %{$_[1]}, %{$main::read_file_cache{$realfile}} );
return 1;
}
elsif ($main::read_file_missing{$realfile}) {
# Doesn't exist, so don't re-try read
return 0;
}
else {
# Actually read the file
my %d;
if (&read_file($_[0], \%d, $_[2], $_[3], $_[4])) {
%{$main::read_file_cache{$realfile}} = %d;
%{$_[1]} = ( %{$_[1]}, %d );
return 1;
}
else {
# Flag as non-existant
$main::read_file_missing{$realfile} = 1;
return 0;
}
}
}
=head2 read_file_cached_with_stat(file, &hash, [&order], [lowercase], [split-char])
Like read_file, but reads from an in-memory cache if the file has already been
read in this Webmin script AND has not changed.
=cut
sub read_file_cached_with_stat
{
my $realfile = &translate_filename($_[0]);
my $t = $main::read_file_cache_time{$realfile};
my @st = stat($realfile);
if ($t && $st[9] != $t) {
# Changed, invalidate cache
delete($main::read_file_cache{$realfile});
}
my $rv = &read_file_cached(@_);
$main::read_file_cache_time{$realfile} = $st[9];
return $rv;
}
=head2 write_file(file, &data-hash, [join-char], [sort], [sorted-by], [sorted-by-preserved])
Write out the contents of a hash as name=value lines. The parameters are :
=item file - Full path to write to
=item data-hash - A hash reference containing names and values to output
=item join-char - If given, names and values are separated by this instead of =
=item sort - If given, passed hash reference will be sorted by its keys
=item sorted-by - If given, hash reference that is being saved will be sorted by the keys of sorted-by hashref
=item sorted-by-sectioning-preserved - If sorted-by is used, then preserve the sectioning (line-breaks), and section comment as in hash reference
=cut
sub write_file
{
my ($file,
$data_hash,
$join_char,
$sort,
$sorted_by,
$sorted_by_sectioning_preserved) = @_;
my (%old, @order);
my $join = defined($join_char) ? $join_char : "=";
my $realfile = &translate_filename($file);
&read_file($sorted_by || $file, \%old, \@order);
&open_tempfile(ARFILE, ">$file");
if ($sort || $gconfig{'sortconfigs'}) {
foreach $k (sort keys %{$data_hash}) {
(print ARFILE $k,$join,$data_hash->{$k},"\n") ||
&error(&text("efilewrite", $realfile, $!));
}
}
else {
my %done;
foreach $k (@order) {
if (exists($data_hash->{$k}) && !$done{$k}++) {
(print ARFILE $k,$join,$data_hash->{$k},"\n") ||
&error(&text("efilewrite", $realfile, $!));
}
}
foreach $k (keys %{$data_hash}) {
if (!exists($old{$k}) && !$done{$k}++) {
(print ARFILE $k,$join,$data_hash->{$k},"\n") ||
&error(&text("efilewrite", $realfile, $!));
}
}
}
&close_tempfile(ARFILE);
if (defined($main::read_file_cache{$realfile})) {
%{$main::read_file_cache{$realfile}} = %{$data_hash};
}
if (defined($main::read_file_missing{$realfile})) {
$main::read_file_missing{$realfile} = 0;
}
if ($sorted_by && $sorted_by_sectioning_preserved) {
my $target = read_file_contents($file);
my $model = read_file_contents($sorted_by);
# Extract version related comments for a block, e.g. #1.962
my %comments = reverse ($model =~ m/(#\s*[\d\.]+)[\n\s]+(.*?)=/gm);
# Build blocks of line's key separated with a new line break
my @lines = (($model =~ m/(.*?)$join|(^\s*$)/gm), undef, undef);
my @blocks;
my @block;
for (my $line = 0; $line < scalar(@lines) - 1; $line += 2) {
if ($lines[$line] =~ /\S+/) {
push(@block, $lines[$line]);
}
else {
push(@blocks, [@block]);
@block = ();
}
}
for (my $block = 0; $block <= scalar(@blocks) - 1; $block++) {
foreach my $line (@{$blocks[$block]}) {
# Add a comment to the first block element
if ($target =~ /(\Q$line\E)=(.*)/) {
foreach my $comment (keys %comments) {
if (grep(/^\Q$comment\E$/, @{$blocks[$block]})) {
$target =~ s/(\Q$line\E)=(.*)/$comments{$comment}\n$1=$2/;
last;
}
}
last;
}
}
foreach my $line (reverse @{$blocks[$block]}) {
if (
# Go to another block immediately
# if new line already exists
$target =~ /(\Q$line\E)$join.*?(\r?\n|\r\n?)+$/m ||
# Add new line to the last element of
# the block and go to another block
$target =~ s/(\Q$line\E)$join(.*)/$1=$2\n/) {
last;
}
}
}
write_file_contents($file, $target);
}
}
=head2 html_escape(string)
Converts &, < and > codes in text to HTML entities, and returns the new string.
This should be used when including data read from other sources in HTML pages.
=cut
sub html_escape
{
my ($tmp) = @_;
if (!defined $tmp) {
return ''; # empty string
};
$tmp =~ s/&/&/g;
$tmp =~ s/</g;
$tmp =~ s/>/>/g;
$tmp =~ s/\"/"/g;
$tmp =~ s/\'/'/g;
$tmp =~ s/=/=/g;
return $tmp;
}
=head2 quote_escape(string, [only-quote])
Converts ' and " characters in a string into HTML entities, and returns it.
Useful for outputing HTML tag values.
=cut
sub quote_escape
{
my ($tmp, $only) = @_;
if (!defined $tmp) {
return ''; # empty string
};
if ($tmp !~ /\&[a-zA-Z]+;/ && $tmp !~ /\/) {
# convert &, unless it is part of nnn; or &foo;
$tmp =~ s/&([^#])/&$1/g;
}
$tmp =~ s/&$/&/g;
$tmp =~ s/\"/"/g if (!$only || $only eq '"');
$tmp =~ s/\'/'/g if (!$only || $only eq "'");
return $tmp;
}
=head2 quote_javascript(string)
Quote all characters that are unsafe for inclusion in javascript strings in HTML
=cut
sub quote_javascript
{
my ($str) = @_;
$str =~ s/["'<>&\\]/sprintf('\x%02x', ord $&)/ge;
return $str;
}
=head2 tempname_dir()
Returns the base directory under which temp files can be created.
=cut
sub tempname_dir
{
my $tmp_base = $gconfig{'tempdir_'.&get_module_name()} ?
$gconfig{'tempdir_'.&get_module_name()} :
$gconfig{'tempdir'} ? $gconfig{'tempdir'} :
$ENV{'TEMP'} && $ENV{'TEMP'} ne "/tmp" ? $ENV{'TEMP'} :
$ENV{'TMP'} && $ENV{'TMP'} ne "/tmp" ? $ENV{'TMP'} :
-d "c:/temp" ? "c:/temp" : "/tmp/.webmin";
my $tmp_dir;
if (-d $remote_user_info[7] && !$gconfig{'nohometemp'}) {
$tmp_dir = "$remote_user_info[7]/.tmp";
}
elsif (@remote_user_info) {
$tmp_dir = $tmp_base."-".$remote_user_info[2]."-".$remote_user;
}
elsif ($< != 0) {
my $u = getpwuid($<);
if ($u) {
$tmp_dir = $tmp_base."-".$<."-".$u;
}
else {
$tmp_dir = $tmp_base."-".$<;
}
}
else {
$tmp_dir = $tmp_base;
}
return $tmp_dir;
}
=head2 tempname([filename])
Returns a mostly random temporary file name, typically under the /tmp/.webmin
directory. If filename is given, this will be the base name used. Otherwise
a unique name is selected randomly.
=cut
sub tempname
{
my ($filename) = @_;
my $tmp_dir = &tempname_dir();
if ($gconfig{'os_type'} eq 'windows' || $tmp_dir =~ /^[a-z]:/i) {
# On Windows system, just create temp dir if missing
if (!-d $tmp_dir) {
mkdir($tmp_dir, 0755) ||
&error("Failed to create temp directory $tmp_dir : $!");
}
}
else {
# On Unix systems, need to make sure temp dir is valid
my $tries = 0;
while($tries++ < 10) {
my @st = lstat($tmp_dir);
last if ($st[4] == $< && (-d _) && ($st[2] & 0777) == 0755);
if (@st) {
unlink($tmp_dir) || rmdir($tmp_dir) ||
system("/bin/rm -rf ".quotemeta($tmp_dir));
}
mkdir($tmp_dir, 0755) || next;
chown($<, $(, $tmp_dir);
chmod(0755, $tmp_dir);
}
if ($tries >= 10) {
my @st = lstat($tmp_dir);
&error("Failed to create temp directory $tmp_dir");
}
# If running as root, check parent dir (usually /tmp) to make sure it's
# world-writable and owned by root
my $tmp_parent = $tmp_dir;
$tmp_parent =~ s/\/[^\/]+$//;
if ($tmp_parent eq "/tmp") {
my @st = stat($tmp_parent);
if (($st[2] & 0555) != 0555) {
&error("Base temp directory $tmp_parent is not world readable and listable");
}
}
}
my $rv;
if (defined($filename) && $filename !~ /\.\./) {
$rv = "$tmp_dir/$filename";
}
else {
$main::tempfilecount++;
&seed_random();
$rv = $tmp_dir."/".int(rand(1000000))."_".$$."_".
$main::tempfilecount."_".$scriptname;
}
return $rv;
}
=head2 transname([filename])
Behaves exactly like tempname, but records the temp file for deletion when the
current Webmin script process exits.
=cut
sub transname
{
my $rv = &tempname(@_);
push(@main::temporary_files, $rv);
return $rv;
}
=head2 transname_timestamped([filename], [extension])
Behaves exactly like transname, but returns a filename with current timestamp
=item filename - Optional filename prefix to preppend
=item extension - Optional extension for a filename to append
=cut
sub transname_timestamped
{
my ($fname, $fextension) = @_;
my $fdate = strftime('%Y%m%d_%H%M%S', localtime());
$fname = "${fname}-" if ($fname);
return &transname("$fname$fdate$fextension");
}
=head2 trunc(string, maxlen)
Truncates a string to the shortest whole word less than or equal to the
given width. Useful for word wrapping.
=cut
sub trunc
{
if (length($_[0]) <= $_[1]) {
return $_[0];
}
my $str = substr($_[0],0,$_[1]);
my $c;
do {
$c = chop($str);
} while($c !~ /\S/);
$str =~ s/\s+$//;
return $str;
}
=head2 indexof(string, value, ...)
Returns the index of some value in an array of values, or -1 if it was not
found.
=cut
sub indexof
{
for(my $i=1; $i <= $#_; $i++) {
if ($_[$i] eq $_[0]) { return $i - 1; }
}
return -1;
}
=head2 indexoflc(string, value, ...)
Like indexof, but does a case-insensitive match
=cut
sub indexoflc
{
my $str = lc(shift(@_));
my @arr = map { lc($_) } @_;
return &indexof($str, @arr);
}
=head2 sysprint(handle, [string]+)
Outputs some strings to a file handle, but bypassing IO buffering. Can be used
as a replacement for print when writing to pipes or sockets.
=cut
sub sysprint
{
my $fh = &callers_package($_[0]);
my $str = join('', @_[1..$#_]);
syswrite $fh, $str, length($str);
}
=head2 check_ipaddress(ip)
Check if some IPv4 address is properly formatted, returning 1 if so or 0 if not.
=cut
sub check_ipaddress
{
return $_[0] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ &&
$1 >= 0 && $1 <= 255 &&
$2 >= 0 && $2 <= 255 &&
$3 >= 0 && $3 <= 255 &&
$4 >= 0 && $4 <= 255;
}
=head2 check_ip6address(ip)
Check if some IPv6 address is properly formatted, and returns 1 if so.
=cut
sub check_ip6address
{
my @blocks = split(/:/, $_[0]);
return 0 if (@blocks == 0 || @blocks > 8);
# The address/netmask format is accepted. So we're looking for a "/" to isolate a possible netmask.
# After that, we delete the netmask to control the address only format, but we verify whether the netmask
# value is in [0;128].
my $ib = $#blocks;
my $where = index($blocks[$ib],"/");
my $m = 0;
if ($where != -1) {
my $b = substr($blocks[$ib],0,$where);
$m = substr($blocks[$ib],$where+1,length($blocks[$ib])-($where+1));
$blocks[$ib]=$b;
}
# The netmask must take its value in [0;128]
return 0 if ($m <0 || $m >128);
# Check the different blocks of the address : 16 bits block in hexa notation.
# Possibility of 1 empty block or 2 if the address begins with "::".
my $b;
my $empty = 0;
foreach $b (@blocks) {
return 0 if ($b ne "" && $b !~ /^[0-9a-f]{1,4}$/i);
$empty++ if ($b eq "");
}
return 0 if ($empty > 1 && !($_[0] =~ /^::/ && $empty == 2));
return 1;
}
=head2 generate_icon(image, title, link, [href], [width], [height], [before-title], [after-title])
Prints HTML for an icon image. The parameters are :
=item image - URL for the image, like images/foo.gif
=item title - Text to appear under the icon
=item link - Optional destination for the icon's link
=item href - Other HTML attributes to be added to the for the link
=item width - Optional width of the icon
=item height - Optional height of the icon
=item before-title - HTML to appear before the title link, but which is not actually in the link
=item after-title - HTML to appear after the title link, but which is not actually in the link
=cut
sub generate_icon
{
&load_theme_library();
if (defined(&theme_generate_icon)) {
&theme_generate_icon(@_);
return;
}
my $w = !defined($_[4]) ? "width='48'" : $_[4] ? "width='$_[4]'" : "";
my $h = !defined($_[5]) ? "height='48'" : $_[5] ? "height='$_[5]'" : "";
if ($tconfig{'noicons'}) {
if ($_[2]) {
print "$_[6]$_[1]$_[7]\n";
}
else {
print "$_[6]$_[1]$_[7]\n";
}
}
elsif ($_[2]) {
print "
\n$_[6]$_[1]$_[7]\n";
}
}
=head2 urlize
Converts a string to a form ok for putting in a URL, using % escaping.
=cut
sub urlize
{
my ($rv) = @_;
$rv =~ s/([^A-Za-z0-9])/sprintf("%%%2.2X", ord($1))/ge;
return $rv;
}
=head2 un_urlize(string)
Converts a URL-encoded string to it's original contents - the reverse of the
urlize function.
=cut
sub un_urlize
{
my ($rv, $plus) = @_;
if (!$plus) {
$rv =~ s/\+/ /g;
}
$rv =~ s/%(..)/pack("c",hex($1))/ge;
return $rv;
}
=head2 include(filename)
Read and output the contents of the given file.
=cut
sub include
{
local $_;
open(INCLUDE, "<".&translate_filename($_[0])) || return 0;
while() {
print;
}
close(INCLUDE);
return 1;
}
=head2 copydata(in-handle, out-handle)
Read from one file handle and write to another, until there is no more to read.
=cut
sub copydata
{
my ($in, $out) = @_;
$in = &callers_package($in);
$out = &callers_package($out);
my $buf;
while(read($in, $buf, 32768) > 0) {
(print $out $buf) || return 0;
}
return 1;
}
=head2 ReadParseMime([maximum], [&cbfunc, &cbargs], [array-mode])
Read data submitted via a POST request using the multipart/form-data coding,
and store it in the global %in hash. The optional parameters are :
=item maximum - If the number of bytes of input exceeds this number, stop reading and call error.
=item cbfunc - A function reference to call after reading each block of data.
=item cbargs - Additional parameters to the callback function.
=item array-mode - If set to 1, values in %in are arrays. If set to 0, multiple values are joined with \0. If set to 2, only the first value is used.
=cut
sub ReadParseMime
{
my ($max, $cbfunc, $cbargs, $arrays) = @_;
my ($boundary, $line, $name, $got, $file, $count_lines, $max_lines);
my $err = &text('readparse_max', $max);
$ENV{'CONTENT_TYPE'} =~ /boundary=(.*)$/ || &error($text{'readparse_enc'});
if ($ENV{'CONTENT_LENGTH'} && $max && $ENV{'CONTENT_LENGTH'} > $max) {
&error($err);
}
&$cbfunc(0, $ENV{'CONTENT_LENGTH'}, $file, @$cbargs) if ($cbfunc);
$boundary = $1;
$count_lines = 0;
$max_lines = 1000;
; # skip first boundary
while(1) {
$name = "";
# Read section headers
my $lastheader;
while(1) {
$line = ;
$got += length($line);
&$cbfunc($got, $ENV{'CONTENT_LENGTH'}, @$cbargs) if ($cbfunc);
if ($max && $got > $max) {
&error($err)
}
$line =~ tr/\r\n//d;
last if (!$line);
if ($line =~ /^(\S+):\s*(.*)$/) {
$header{$lastheader = lc($1)} = $2;
}
elsif ($line =~ /^\s+(.*)$/) {
$header{$lastheader} .= $line;
}
}
# Parse out filename and type
my $file;
if ($header{'content-disposition'} =~ /^form-data(.*)/) {
$rest = $1;
while ($rest =~ /([a-zA-Z]*)=\"([^\"]*)\"(.*)/) {
if ($1 eq 'name') {
$name = $2;
}
else {
my $foo = $name."_".$1;
if ($1 eq "filename") {
$file = $2;
}
if ($arrays == 1) {
$in{$foo} ||= [];
push(@{$in{$foo}}, $2);
}
elsif ($arrays == 2) {
$in{$foo} ||= $2;
}
else {
$in{$foo} .= "\0"
if (defined($in{$foo}));
$in{$foo} .= $2;
}
}
$rest = $3;
}
}
else {
&error($text{'readparse_cdheader'});
}
# Save content type separately
if ($header{'content-type'} =~ /^([^\s;]+)/) {
my $foo = $name."_content_type";
if ($arrays == 1) {
$in{$foo} ||= [];
push(@{$in{$foo}}, $1);
}
elsif ($arrays == 2) {
$in{$foo} ||= $1;
}
else {
$in{$foo} .= "\0" if (defined($in{$foo}));
$in{$foo} .= $1;
}
}
# Read data
my $data = "";
while(1) {
$line = ;
$got += length($line);
$count_lines++;
if ($count_lines == $max_lines) {
&$cbfunc($got, $ENV{'CONTENT_LENGTH'}, $file, @$cbargs)
if ($cbfunc);
$count_lines = 0;
}
if ($max && $got > $max) {
#print STDERR "over limit of $max\n";
#&error($err);
}
if (!$line) {
# Unexpected EOF?
&$cbfunc(-1, $ENV{'CONTENT_LENGTH'}, $file, @$cbargs)
if ($cbfunc);
return;
}
if (index($line, $boundary) != -1) { last; }
$data .= $line;
}
chop($data); chop($data);
if ($arrays == 1) {
$in{$name} ||= [];
push(@{$in{$name}}, $data);
}
elsif ($arrays == 2) {
$in{$name} ||= $data;
}
else {
$in{$name} .= "\0" if (defined($in{$name}));
$in{$name} .= $data;
}
if (index($line,"$boundary--") != -1) { last; }
}
&$cbfunc(-1, $ENV{'CONTENT_LENGTH'}, $file, @$cbargs) if ($cbfunc);
}
=head2 ReadParse([&hash], [method], [noplus], [array-mode])
Fills the given hash reference with CGI parameters, or uses the global hash
%in if none is given. Also sets the global variables $in and @in. The other
parameters are :
=item method - For use of this HTTP method, such as GET
=item noplus - Don't convert + in parameters to spaces.
=item array-mode - If set to 1, values in %in are arrays. If set to 0, multiple values are joined with \0. If set to 2, only the first value is used.
=cut
sub ReadParse
{
my $a = $_[0] || \%in;
%$a = ( );
my $meth = $_[1] ? $_[1] : $ENV{'REQUEST_METHOD'};
undef($in);
if ($meth eq 'POST') {
my $clen = $ENV{'CONTENT_LENGTH'};
&read_fully(STDIN, \$in, $clen) == $clen ||
&error("Failed to read POST input : $!");
}
if ($ENV{'QUERY_STRING'}) {
if ($in) { $in .= "&".$ENV{'QUERY_STRING'}; }
else { $in = $ENV{'QUERY_STRING'}; }
}
@in = split(/\&/, $in);
foreach my $i (@in) {
$i =~ /\0/ && &error("Null byte in query string");
my ($k, $v) = split(/=/, $i, 2);
if (!$_[2]) {
$k =~ tr/\+/ /;
$v =~ tr/\+/ /;
}
$k =~ s/%(..)/pack("c",hex($1))/ge;
$v =~ s/%(..)/pack("c",hex($1))/ge;
if ($_[3] == 1) {
$a->{$k} ||= [];
push(@{$a->{$k}}, $v);
}
elsif ($_[3] == 2) {
$a->{$k} ||= $v;
}
else {
$a->{$k} = defined($a->{$k}) ? $a->{$k}."\0".$v : $v;
}
}
}
=head2 read_fully(fh, &buffer, length)
Read data from some file handle up to the given length, even in the face
of partial reads. Reads the number of bytes read. Stores received data in the
string pointed to be the buffer reference.
=cut
sub read_fully
{
my ($fh, $buf, $len) = @_;
$fh = &callers_package($fh);
my $got = 0;
while($got < $len) {
my $r = read(STDIN, $$buf, $len-$got, $got);
last if ($r <= 0);
$got += $r;
}
return $got;
}
=head2 read_parse_mime_callback(size, totalsize, upload-id)
Called by ReadParseMime as new data arrives from a form-data POST. Only updates
the file on every 1% change though. For internal use by the upload progress
tracker.
=cut
sub read_parse_mime_callback
{
my ($size, $totalsize, $filename, $id) = @_;
return if ($gconfig{'no_upload_tracker'});
return if (!$id);
# Create the upload tracking directory - if running as non-root, this has to
# be under the user's home
my $vardir;
if ($<) {
my @uinfo = @remote_user_info ? @remote_user_info : getpwuid($<);
$vardir = "$uinfo[7]/.tmp";
}
else {
$vardir = $ENV{'WEBMIN_VAR'};
}
if (!-d $vardir) {
&make_dir($vardir, 0755);
}
# Remove any upload.* files more than 1 hour old
if (!$main::read_parse_mime_callback_flushed) {
my $now = time();
opendir(UPDIR, $vardir);
foreach my $f (readdir(UPDIR)) {
next if ($f !~ /^upload\./);
my @st = stat("$vardir/$f");
if ($st[9] < $now-3600) {
unlink("$vardir/$f");
}
}
closedir(UPDIR);
$main::read_parse_mime_callback_flushed++;
}
# Only update file once per percent
my $upfile = "$vardir/upload.$id";
if ($totalsize && $size >= 0) {
my $pc = int(100 * $size / $totalsize);
if ($pc <= $main::read_parse_mime_callback_pc{$upfile}) {
return;
}
$main::read_parse_mime_callback_pc{$upfile} = $pc;
}
# Write to the file
&open_tempfile(UPFILE, ">$upfile");
print UPFILE $size,"\n";
print UPFILE $totalsize,"\n";
print UPFILE $filename,"\n";
&close_tempfile(UPFILE);
}
=head2 read_parse_mime_javascript(upload-id, [&fields])
Returns an onSubmit= Javascript statement to popup a window for tracking
an upload with the given ID. For internal use by the upload progress tracker.
=cut
sub read_parse_mime_javascript
{
my ($id, $fields) = @_;
return "" if ($gconfig{'no_upload_tracker'});
my $opener = "window.open(\"$gconfig{'webprefix'}/uptracker.cgi?id=$id&uid=$<\", \"uptracker\", \"toolbar=no,menubar=no,scrollbars=no,width=500,height=128\");";
if ($fields) {
my $if = join(" || ", map { "typeof($_) != \"undefined\" && $_.value != \"\"" } @$fields);
return "onSubmit='if ($if) { $opener }'";
}
else {
return "onSubmit='$opener'";
}
}
=head2 PrintHeader(charset, [mime-type])
Outputs the HTTP headers for an HTML page. The optional charset parameter
can be used to set a character set. Normally this function is not called
directly, but is rather called by ui_print_header or header.
=cut
sub PrintHeader
{
my ($cs, $mt) = @_;
$mt ||= "text/html";
if ($pragma_no_cache || $gconfig{'pragma_no_cache'}) {
print "pragma: no-cache\n";
print "Expires: Thu, 1 Jan 1970 00:00:00 GMT\n";
print "Cache-Control: no-store, no-cache, must-revalidate\n";
print "Cache-Control: post-check=0, pre-check=0\n";
}
if ($gconfig{'extra_headers'}) {
foreach my $l (split(/\t+/, $gconfig{'extra_headers'})) {
print $l."\n";
}
}
if (!$gconfig{'no_frame_options'}) {
print "X-Frame-Options: SAMEORIGIN\n";
}
if (!$gconfig{'no_content_security_policy'}) {
print "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval'; frame-src 'self'; child-src 'self'\n";
}
print "X-Content-Type-Options: nosniff\n";
if (defined($cs)) {
print "Content-type: $mt; Charset=$cs\n\n";
}
else {
print "Content-type: $mt\n\n";
}
$main::header_content_type = $mt;
}
=head2 header(title, image, [help], [config], [nomodule], [nowebmin], [rightside], [head-stuff], [body-stuff], [below])
Outputs a Webmin HTML page header with a title, including HTTP headers. The
parameters are :
=item title - The text to show at the top of the page
=item image - An image to show instead of the title text. This is typically left blank.
=item help - If set, this is the name of a help page that will be linked to in the title.
=item config - If set to 1, the title will contain a link to the module's config page.
=item nomodule - If set to 1, there will be no link in the title section to the module's index.
=item nowebmin - If set to 1, there will be no link in the title section to the Webmin index.
=item rightside - HTML to be shown on the right-hand side of the title. Can contain multiple lines, separated by . Typically this is used for links to stop, start or restart servers.
=item head-stuff - HTML to be included in the section of the page.
=item body-stuff - HTML attributes to be include in the tag.
=item below - HTML to be displayed below the title. Typically this is used for application or server version information.
=cut
sub header
{
return if ($main::done_webmin_header++);
my $ll;
my $charset = defined($main::force_charset) ? $main::force_charset
: &get_charset();
&PrintHeader($charset);
&load_theme_library();
if (defined(&theme_header)) {
$module_name = &get_module_name();
&theme_header(@_);
$miniserv::page_capture = 1;
return;
}
print "\n";
print "\n";
print "\n";
if (defined(&theme_prehead)) {
&theme_prehead(@_);
}
if ($charset) {
print "\n";
}
if (@_ > 0) {
my $title = &get_html_title($_[0]);
print "$title\n" if ($_[7] !~ //i);
print $_[7] if ($_[7]);
print &get_html_status_line(0);
}
print "$tconfig{'headhtml'}\n" if ($tconfig{'headhtml'});
if ($tconfig{'headinclude'}) {
my ($theme, $overlay) = split(' ', $gconfig{'theme'});
my $file_contents = read_file_contents("$root_directory/$overlay/$tconfig{'headinclude'}");;
$file_contents = replace_meta($file_contents);
print $file_contents;
}
print "\n";
my $bgcolor = defined($tconfig{'cs_page'}) ? $tconfig{'cs_page'} :
defined($gconfig{'cs_page'}) ? $gconfig{'cs_page'} : "ffffff";
my $link = defined($tconfig{'cs_link'}) ? $tconfig{'cs_link'} :
defined($gconfig{'cs_link'}) ? $gconfig{'cs_link'} : "0000ee";
my $text = defined($tconfig{'cs_text'}) ? $tconfig{'cs_text'} :
defined($gconfig{'cs_text'}) ? $gconfig{'cs_text'} : "000000";
my $bgimage = defined($tconfig{'bgimage'}) ? "background=$tconfig{'bgimage'}" : "";
my $dir = $current_lang_info->{'dir'} ? "dir=\"$current_lang_info->{'dir'}\"" : "";
my $html_body = "\n";
$html_body =~ s/\s+\>/>/g;
print $html_body;
if (defined(&theme_prebody)) {
&theme_prebody(@_);
}
my $prebody = $tconfig{'prebody'};
if ($prebody) {
$prebody = replace_meta($prebody);
print "$prebody\n";
}
if ($tconfig{'prebodyinclude'}) {
my ($theme, $overlay) = split(' ', $gconfig{'theme'});
my $file_contents = read_file_contents("$root_directory/$overlay/$tconfig{'prebodyinclude'}");
$file_contents = replace_meta($file_contents);
print $file_contents;
}
if (@_ > 1) {
print $tconfig{'preheader'};
my %this_module_info = &get_module_info(&get_module_name());
print "
\n";
if ($gconfig{'sysinfo'} == 2 && $remote_user) {
print "
\n";
print $tconfig{'postheader'};
}
$miniserv::page_capture = 1;
}
=head2 get_html_title(title)
Returns the full string to appear in the HTML block.
=cut
sub get_html_title
{
my ($msg) = @_;
my $title;
my $os_type = $gconfig{'real_os_type'} || $gconfig{'os_type'};
my $os_version = $gconfig{'real_os_version'} || $gconfig{'os_version'};
my $host = &get_display_hostname();
if ($gconfig{'sysinfo'} == 1 && $remote_user) {
$title = sprintf "%s : %s on %s (%s %s)\n",
$msg, $remote_user, $host,
$os_type, $os_version;
}
elsif ($gconfig{'sysinfo'} == 4 && $remote_user) {
$title = sprintf "%s on %s (%s %s)\n",
$remote_user, $host,
$os_type, $os_version;
}
else {
$title = $msg;
}
if ($gconfig{'showlogin'} && $remote_user) {
$title = $remote_user.($title ? " : ".$title : "");
}
if ($gconfig{'showhost'}) {
$title = $host.($title ? " : ".$title : "");
}
return $title;
}
=head2 get_html_framed_title
Returns the title text for a framed theme main page.
=cut
sub get_html_framed_title
{
my $ostr;
my $os_type = $gconfig{'real_os_type'} || $gconfig{'os_type'};
my $os_version = $gconfig{'real_os_version'} || $gconfig{'os_version'};
my $title;
if (($gconfig{'sysinfo'} == 4 || $gconfig{'sysinfo'} == 1) && $remote_user) {
# Alternate title mode requested
$title = sprintf "%s on %s (%s %s)\n",
$remote_user, &get_display_hostname(),
$os_type, $os_version;
}
else {
# Title like 'Webmin x.yy on hostname (Linux 6)'
if ($os_version eq "*") {
$ostr = $os_type;
}
else {
$ostr = "$os_type $os_version";
}
my $host = &get_display_hostname();
my $ver = &get_webmin_version();
$title = $gconfig{'nohostname'} ? $text{'main_title2'} :
$gconfig{'showhost'} ? &text('main_title3', $ver, $ostr) :
&text('main_title', $ver, $host, $ostr);
if ($gconfig{'showlogin'}) {
$title = $remote_user.($title ? " : ".$title : "");
}
if ($gconfig{'showhost'}) {
$title = $host.($title ? " : ".$title : "");
}
}
return $title;
}
=head2 get_html_status_line(text-only)
Returns HTML for a script block that sets the status line, or if text-only
is set to 1, just return the status line text.
=cut
sub get_html_status_line
{
my ($textonly) = @_;
if (($gconfig{'sysinfo'} != 0 || !$remote_user) && !$textonly) {
# Disabled in this mode
return undef;
}
my $os_type = $gconfig{'real_os_type'} || $gconfig{'os_type'};
my $os_version = $gconfig{'real_os_version'} || $gconfig{'os_version'};
my $line = &text('header_statusmsg',
($ENV{'ANONYMOUS_USER'} ? "Anonymous user"
: $remote_user).
($ENV{'SSL_USER'} ? " (SSL certified)" :
$ENV{'LOCAL_USER'} ? " (Local user)" : ""),
$text{'programname'},
&get_webmin_version(),
&get_display_hostname(),
$os_type.($os_version eq "*" ? "" :" $os_version"));
if ($textonly) {
return $line;
}
else {
$line =~ s/\r|\n//g;
return "\n";
}
}
=head2 popup_header([title], [head-stuff], [body-stuff], [no-body])
Outputs a page header, suitable for a popup window. If no title is given,
absolutely no decorations are output. Also useful in framesets. The parameters
are :
=item title - Title text for the popup window.
=item head-stuff - HTML to appear in the section.
=item body-stuff - HTML attributes to be include in the tag.
=item no-body - If set to 1, don't generate a body tag
=cut
sub popup_header
{
return if ($main::done_webmin_header++);
my $ll;
my $charset = defined($main::force_charset) ? $main::force_charset
: &get_charset();
&PrintHeader($charset);
&load_theme_library();
if (defined(&theme_popup_header)) {
&theme_popup_header(@_);
$miniserv::page_capture = 1;
return;
}
print "\n";
print "\n";
print "\n";
if (defined(&theme_popup_prehead)) {
&theme_popup_prehead(@_);
}
print "$_[0]\n";
print $_[1];
print "$tconfig{'headhtml'}\n" if ($tconfig{'headhtml'});
if ($tconfig{'headinclude'}) {
print &read_file_contents(
"$theme_root_directory/$tconfig{'headinclude'}");
}
print "\n";
my $bgcolor = defined($tconfig{'cs_page'}) ? $tconfig{'cs_page'} :
defined($gconfig{'cs_page'}) ? $gconfig{'cs_page'} : "ffffff";
my $link = defined($tconfig{'cs_link'}) ? $tconfig{'cs_link'} :
defined($gconfig{'cs_link'}) ? $gconfig{'cs_link'} : "0000ee";
my $text = defined($tconfig{'cs_text'}) ? $tconfig{'cs_text'} :
defined($gconfig{'cs_text'}) ? $gconfig{'cs_text'} : "000000";
my $bgimage = defined($tconfig{'bgimage'}) ? "background='$tconfig{'bgimage'}'"
: "";
if (!$_[3]) {
print "\n";
if (defined(&theme_popup_prebody)) {
&theme_popup_prebody(@_);
}
}
$miniserv::page_capture = 1;
}
=head2 footer([page, name]+, [noendbody])
Outputs the footer for a Webmin HTML page, possibly with links back to other
pages. The links are specified by pairs of parameters, the first of which is
a link destination, and the second the link text. For example :
footer('/', 'Webmin index', '', 'Module menu');
=cut
sub footer
{
$miniserv::page_capture = 0;
&load_theme_library();
my %this_module_info = &get_module_info(&get_module_name());
if (defined(&theme_footer)) {
$module_name = &get_module_name(); # Old themes use these
%module_info = %this_module_info;
&theme_footer(@_);
return;
}
for(my $i=0; $i+1<@_; $i+=2) {
my $url = $_[$i];
if ($url ne '/' || !$tconfig{'noindex'}) {
if ($url eq '/') {
$url = "/?cat=$this_module_info{'category'}";
}
elsif ($url eq '' && &get_module_name()) {
$url = "/".&get_module_name()."/".
$this_module_info{'index_link'};
}
elsif ($url =~ /^\?/ && &get_module_name()) {
$url = "/".&get_module_name()."/$url";
}
$url = "$gconfig{'webprefix'}$url" if ($url =~ /^\//);
if ($i == 0) {
print "\n";
}
else {
print " |\n";
}
print " ",&text('main_return', $_[$i+1]),"\n";
}
}
print " \n";
if (!$_[$i]) {
my $postbody = $tconfig{'postbody'};
if ($postbody) {
$postbody = replace_meta($postbody);
print "$postbody\n";
}
if ($tconfig{'postbodyinclude'}) {
my ($theme, $overlay) = split(' ', $gconfig{'theme'});
my $file_contents = read_file_contents("$root_directory/$overlay/$tconfig{'postbodyinclude'}");
$file_contents = replace_meta($file_contents);
print $file_contents;
}
if (defined(&theme_postbody)) {
&theme_postbody(@_);
}
print "\n";
}
}
=head2 popup_footer([no-body])
Outputs html for a footer for a popup window, started by popup_header.
=cut
sub popup_footer
{
$miniserv::page_capture = 0;
&load_theme_library();
if (defined(&theme_popup_footer)) {
&theme_popup_footer(@_);
return;
}
if (!$_[0]) {
print "\n";
}
print "\n";
}
=head2 load_module_preferences(module, &config)
Check if user preferences can be loaded for given
module based on module's prefs.info special file.
=cut
sub load_module_preferences
{
my ($module, $curr_config) = @_;
my $module_prefs_acls = &get_module_preferences_acl($module, 'allowed');
my $current_user_prefs = "$config_directory/$module/prefs.$remote_user";
if ($module_prefs_acls && -r $current_user_prefs) {
if ($module_prefs_acls eq "*") {
&read_file($current_user_prefs, \%$curr_config);
}
else {
my %newconfigtmp;
&read_file($current_user_prefs, \%newconfigtmp);
foreach my $key (keys %newconfigtmp) {
if (grep(/^$key$/, split(",", $module_prefs_acls))) {
$curr_config->{$key} = $newconfigtmp{$key};
}
}
}
}
}
=head2 get_module_preferences_acl(module, type)
Return one of module's prefs params (if described by module in prefs.info).
=cut
sub get_module_preferences_acl
{
my ($module, $type) = @_;
my $module_dir = &module_root_directory($module);
my $module_prefs_conf_file = "$module_dir/prefs.info";
if (-r $module_prefs_conf_file) {
my %module_prefs_conf;
&read_file($module_prefs_conf_file, \%module_prefs_conf);
return $module_prefs_conf{$type};
}
return undef;
}
=head2 load_theme_library
Immediately loads the current theme's theme.pl file. Not generally useful for
most module developers, as this is called automatically by the header function.
=cut
sub load_theme_library
{
return if (!$current_theme || $loaded_theme_library++);
for(my $i=0; $i<@theme_root_directories; $i++) {
if ($theme_configs[$i]->{'functions'}) {
my @theme_funcs = split(/\s+/, $theme_configs[$i]->{'functions'});
foreach my $theme_func (@theme_funcs) {
do "$theme_root_directories[$i]/$theme_func";
}
}
}
}
=head2 redirect(url)
Output HTTP headers to redirect the browser to some page. The url parameter is
typically a relative URL like index.cgi or list_users.cgi.
=cut
sub redirect
{
my %miniserv;
&get_miniserv_config(\%miniserv);
my $redirhost = $miniserv{'redirect_host'} || $ENV{'SERVER_NAME'};
my $redirport = $miniserv{'redirect_port'} || $ENV{'SERVER_PORT'};
my $redirssl = $miniserv{'redirect_ssl'} ne '' ? $miniserv{'redirect_ssl'} :
uc($ENV{'HTTPS'}) eq "ON" ? 1 : 0;
my $port = $redirport == 443 && $redirssl ? "" :
$redirport == 80 && !$redirssl ? "" : ":".$redirport;
my $prot = $redirssl ? "https" : "http";
my $wp = $gconfig{'webprefixnoredir'} ? undef : $gconfig{'webprefix'};
my $url;
if ($_[0] =~ /^(http|https|ftp|gopher):/) {
# Absolute URL (like http://...)
$url = $_[0];
}
elsif ($_[0] =~ /^\//) {
# Absolute path (like /foo/bar.cgi)
if ($gconfig{'relative_redir'}) {
$url = "$wp$_[0]";
}
else {
$url = "$prot://$redirhost$port$wp$_[0]";
}
}
elsif ($ENV{'SCRIPT_NAME'} =~ /^(.*)\/[^\/]*$/) {
# Relative URL (like foo.cgi)
if ($gconfig{'relative_redir'}) {
$url = "$wp$1/$_[0]";
}
else {
$url = "$prot://$redirhost$port$wp$1/$_[0]";
}
}
else {
if ($gconfig{'relative_redir'}) {
$url = "$wp$_[0]";
}
else {
$url = "$prot://$redirhost$port/$wp$_[0]";
}
}
&load_theme_library();
if (defined(&theme_redirect)) {
$module_name = &get_module_name(); # Old themes use these
%module_info = &get_module_info($module_name);
&theme_redirect($_[0], $url);
}
else {
print "Location: $url\n\n";
}
}
=head2 kill_byname(name, signal)
Finds a process whose command line contains the given name (such as httpd), and
sends some signal to it. The signal can be numeric (like 9) or named
(like KILL).
=cut
sub kill_byname
{
my @pids = &find_byname($_[0]);
return scalar(@pids) if (&is_readonly_mode());
&webmin_debug_log('KILL', "signal=$_[1] name=$_[0]")
if ($gconfig{'debug_what_procs'});
if (@pids) { kill($_[1], @pids); return scalar(@pids); }
else { return 0; }
}
=head2 kill_byname_logged(name, signal)
Like kill_byname, but also logs the killing.
=cut
sub kill_byname_logged
{
my @pids = &find_byname($_[0]);
return scalar(@pids) if (&is_readonly_mode());
if (@pids) { &kill_logged($_[1], @pids); return scalar(@pids); }
else { return 0; }
}
=head2 find_byname(name)
Finds processes searching for the given name in their command lines, and
returns a list of matching PIDs.
=cut
sub find_byname
{
if ($gconfig{'os_type'} =~ /-linux$/ && -r "/proc/$$/cmdline") {
# Linux with /proc filesystem .. use cmdline files, as this is
# faster than forking
my @pids;
opendir(PROCDIR, "/proc");
foreach my $f (readdir(PROCDIR)) {
if ($f eq int($f) && $f != $$) {
my $line = &read_file_contents("/proc/$f/cmdline");
if ($line =~ /$_[0]/) {
push(@pids, $f);
}
}
}
closedir(PROCDIR);
return @pids;
}
if (&foreign_check("proc")) {
# Call the proc module
&foreign_require("proc", "proc-lib.pl");
if (defined(&proc::list_processes)) {
my @procs = &proc::list_processes();
my @pids;
foreach my $p (@procs) {
if ($p->{'args'} =~ /$_[0]/) {
push(@pids, $p->{'pid'});
}
}
@pids = grep { $_ != $$ } @pids;
return @pids;
}
}
# Fall back to running a command
my ($cmd, @pids);
$cmd = $gconfig{'find_pid_command'};
$cmd =~ s/NAME/"$_[0]"/g;
$cmd = &translate_command($cmd);
@pids = split(/\n/, `($cmd) <$null_file 2>$null_file`);
@pids = grep { $_ != $$ } @pids;
return @pids;
}
=head2 error([message]+)
Display an error message and exit. This should be used by CGI scripts that
encounter a fatal error or invalid user input to notify users of the problem.
If error_setup has been called, the displayed error message will be prefixed
by the message setup using that function.
=cut
sub error
{
$main::no_miniserv_userdb = 1;
my $msg = join("", @_);
$msg =~ s/<[^>]*>//g;
my $error_details = (($ENV{'WEBMIN_DEBUG'} || $gconfig{'debug_enabled'}) ? "" : "\n");
my $error_output_right = sub {
my ($err_msg) = @_;
return $main::webmin_script_type eq 'cmd' ? entities_to_ascii($err_msg) : $err_msg;
};
if (!$main::error_must_die) {
print STDERR "Error: ", &$error_output_right($msg), "\n";
}
&load_theme_library();
if ($main::error_must_die) {
die "@_$error_details";
}
&call_error_handlers();
if (!$ENV{'REQUEST_METHOD'}) {
# Show text-only error
print STDERR "$text{'error'}\n";
print STDERR "-----\n";
print STDERR ($main::whatfailed ? "$main::whatfailed : " : ""),
&$error_output_right($msg),"\n";
print STDERR "-----\n";
if ($gconfig{'error_stack'}) {
# Show call stack
print STDERR $text{'error_stack'},"\n";
for(my $i=0; my @stack = caller($i); $i++) {
print STDERR &text('error_stackline',
$stack[1], $stack[2], $stack[3]),"\n";
}
}
}
elsif (defined(&theme_error)) {
&theme_error(@_);
}
elsif ($ENV{'REQUEST_URI'} =~ /json-error=1/) {
my %jerror;
my $error_what = ($main::whatfailed ? "$main::whatfailed: " : "");
my $error_message = join(",", @_);
my $error = ($error_what . $error_message);
%jerror = (error => $error,
error_fatal => 1,
error_what => $error_what,
error_message => $error_message
);
print_json(\%jerror);
}
else {
&header($text{'error'}, "");
my $hh = $miniserv::page_capture;
print "\n" if ($hh);
if ($hh) {
print "
\n";
}
else {
my $error_what = ($main::whatfailed ? "$main::whatfailed: " : "");
my $error_html = join(",", @_);
my $error_text;
if ($error_html !~ //) {
$error_text = " — $error_html";
$error_html = undef;
}
print "$text{'error'}
$text{'error'}$error_text
$error_html \n";
}
if ($gconfig{'error_stack'}) {
# Show call stack
print "\n";
my $caption_no_header_style =
&miniserv::get_error_style('heading', "padding:0 0 10px 0;" . ($hh ? "color: #222;font-size:98%;" : "") . "");
print "\n" if ($hh);
print "
$text{'error_stack'}
\n";
print "
$text{'error_file'}
",
"
$text{'error_line'}
",
"
$text{'error_sub'}
\n";
for($i=0; my @stack = caller($i); $i++) {
print "
\n";
print "
$stack[1]
\n";
print "
$stack[2]
\n";
print "
$stack[3]
\n";
print "
\n";
}
print "
\n";
}
print "\n" if ($hh);
if ($ENV{'HTTP_REFERER'} && $main::completed_referers_check) {
&footer("javascript:history.back()", $text{'error_previous'});
}
else {
&footer();
}
}
&unlock_all_files();
&cleanup_tempnames();
exit(1);
}
=head2 popup_error([message]+)
This function is almost identical to error, but displays the message with HTML
headers suitable for a popup window.
=cut
sub popup_error
{
$main::no_miniserv_userdb = 1;
&load_theme_library();
if ($main::error_must_die) {
die @_;
}
&call_error_handlers();
if (defined(&theme_popup_error)) {
&theme_popup_error(@_);
}
else {
&popup_header($text{'error'}, "");
print "
\n"; $need_tr++; }
print "\n" if ($need_tr);
print "
\n";
}
=head2 replace_meta($string)
Replaces all occurrences of meta words
=item string - String value to search/replace in
=cut
sub replace_meta
{
my ($string) = @_;
my $hostname = &get_display_hostname();
my $version = &get_webmin_version();
my $os_type = $gconfig{'real_os_type'} || $gconfig{'os_type'};
my $os_version = $gconfig{'real_os_version'} || $gconfig{'os_version'};
$string =~ s/%HOSTNAME%/$hostname/g;
$string =~ s/%VERSION%/$version/g;
$string =~ s/%USER%/$remote_user/g;
$string =~ s/%OS%/$os_type $os_version/g;
return $string;
}
=head2 replace_file_line(file, line, [newline]*)
Replaces one line in some file with 0 or more new lines. The parameters are :
=item file - Full path to some file, like /etc/hosts.
=item line - Line number to replace, starting from 0.
=item newline - Zero or more lines to put into the file at the given line number. These must be newline-terminated strings.
=cut
sub replace_file_line
{
my @lines;
my $realfile = &translate_filename($_[0]);
open(FILE, "<".$realfile);
@lines = ;
close(FILE);
if (@_ > 2) { splice(@lines, $_[1], 1, @_[2..$#_]); }
else { splice(@lines, $_[1], 1); }
&open_tempfile(FILE, ">$realfile");
&print_tempfile(FILE, @lines);
&close_tempfile(FILE);
}
=head2 read_file_lines(file, [readonly])
Returns a reference to an array containing the lines from some file. This
array can be modified, and will be written out when flush_file_lines()
is called. The parameters are :
=item file - Full path to the file to read.
=item readonly - Should be set 1 if the caller is only going to read the lines, and never write it out.
Example code :
$lref = read_file_lines("/etc/hosts");
push(@$lref, "127.0.0.1 localhost");
flush_file_lines("/etc/hosts");
=cut
sub read_file_lines
{
my ($file, $readonly) = @_;
if (!$file) {
my ($package, $filename, $line) = caller;
&error("Missing file to read at ${package}::${filename} line $line");
}
my $realfile = &translate_filename($file);
if (!$main::file_cache{$realfile}) {
my (@lines, $eol);
local $_;
&webmin_debug_log('READ', $file) if ($gconfig{'debug_what_read'});
open(READFILE, "<".$realfile);
while() {
if (!$eol) {
$eol = /\r\n$/ ? "\r\n" : "\n";
}
tr/\r\n//d;
push(@lines, $_);
}
close(READFILE);
$main::file_cache{$realfile} = \@lines;
$main::file_cache_noflush{$realfile} = $readonly;
$main::file_cache_eol{$realfile} = $eol || "\n";
}
else {
# Make read-write if currently readonly
if (!$readonly) {
$main::file_cache_noflush{$realfile} = 0;
}
}
return $main::file_cache{$realfile};
}
=head2 flush_file_lines([file], [eol], [ignore-unloaded])
Write out to a file previously read by read_file_lines to disk (except
for those marked readonly). The parameters are :
=item file - The file to flush out.
=item eof - End-of-line character for each line. Defaults to \n.
=item ignore-unloaded - Don't fail if the file isn't loaded
=cut
sub flush_file_lines
{
my ($file, $eof, $ignore) = @_;
my @files;
if ($file) {
local $trans = &translate_filename($file);
if (!$main::file_cache{$trans}) {
if ($ignore) {
return 0;
}
else {
&error("flush_file_lines called on non-loaded file $trans");
}
}
push(@files, $trans);
}
else {
@files = ( keys %main::file_cache );
}
foreach my $f (@files) {
my $eol = $eof || $main::file_cache_eol{$f} || "\n";
if (!$main::file_cache_noflush{$f}) {
no warnings; # XXX Bareword file handles should go away
&open_tempfile(FLUSHFILE, ">$f");
foreach my $line (@{$main::file_cache{$f}}) {
(print FLUSHFILE $line,$eol) ||
&error(&text("efilewrite", $f, $!));
}
&close_tempfile(FLUSHFILE);
}
delete($main::file_cache{$f});
delete($main::file_cache_noflush{$f});
}
return scalar(@files);
}
=head2 unflush_file_lines(file)
Clear the internal cache of some given file, previously read by read_file_lines.
=cut
sub unflush_file_lines
{
my $realfile = &translate_filename($_[0]);
delete($main::file_cache{$realfile});
delete($main::file_cache_noflush{$realfile});
}
=head2 unix_user_input(fieldname, user, [form])
Returns HTML for an input to select a Unix user. By default this is a text
box with a user popup button next to it.
=cut
sub unix_user_input
{
if (defined(&theme_unix_user_input)) {
return &theme_unix_user_input(@_);
}
return " ".
&user_chooser_button($_[0], 0, $_[2] || 0)."\n";
}
=head2 unix_group_input(fieldname, user, [form])
Returns HTML for an input to select a Unix group. By default this is a text
box with a group popup button next to it.
=cut
sub unix_group_input
{
if (defined(&theme_unix_group_input)) {
return &theme_unix_group_input(@_);
}
return " ".
&group_chooser_button($_[0], 0, $_[2] || 0)."\n";
}
=head2 hlink(text, page, [module], [width], [height])
Returns HTML for a link that when clicked on pops up a window for a Webmin
help page. The parameters are :
=item text - Text for the link.
=item page - Help page code, such as 'intro'.
=item module - Module the help page is in. Defaults to the current module.
=item width - Width of the help popup window. Defaults to 600 pixels.
=item height - Height of the help popup window. Defaults to 400 pixels.
The actual help pages are in each module's help sub-directory, in files with
.html extensions.
=cut
sub hlink
{
if (defined(&theme_hlink)) {
return &theme_hlink(@_);
}
my $mod = $_[2] ? $_[2] : &get_module_name();
my $width = $_[3] || $tconfig{'help_width'} || $gconfig{'help_width'} || 600;
my $height = $_[4] || $tconfig{'help_height'} || $gconfig{'help_height'} || 400;
return "$_[0]";
}
=head2 user_chooser_button(field, multiple, [form])
Returns HTML for a javascript button for choosing a Unix user or users.
The parameters are :
=item field - Name of the HTML field to place the username into.
=item multiple - Set to 1 if multiple users can be selected.
=item form - Index of the form on the page.
=cut
sub user_chooser_button
{
return undef if (!&supports_users());
return &theme_user_chooser_button(@_)
if (defined(&theme_user_chooser_button));
my $form = defined($_[2]) ? $_[2] : 0;
my $w = $_[1] ? 500 : 300;
my $h = 200;
if ($_[1] && $gconfig{'db_sizeusers'}) {
($w, $h) = split(/x/, $gconfig{'db_sizeusers'});
}
elsif (!$_[1] && $gconfig{'db_sizeuser'}) {
($w, $h) = split(/x/, $gconfig{'db_sizeuser'});
}
return "\n";
}
=head2 group_chooser_button(field, multiple, [form])
Returns HTML for a javascript button for choosing a Unix group or groups
The parameters are :
=item field - Name of the HTML field to place the group name into.
=item multiple - Set to 1 if multiple groups can be selected.
=item form - Index of the form on the page.
=cut
sub group_chooser_button
{
return undef if (!&supports_users());
return &theme_group_chooser_button(@_)
if (defined(&theme_group_chooser_button));
my $form = defined($_[2]) ? $_[2] : 0;
my $w = $_[1] ? 500 : 300;
my $h = 200;
if ($_[1] && $gconfig{'db_sizeusers'}) {
($w, $h) = split(/x/, $gconfig{'db_sizeusers'});
}
elsif (!$_[1] && $gconfig{'db_sizeuser'}) {
($w, $h) = split(/x/, $gconfig{'db_sizeuser'});
}
return "\n";
}
=head2 foreign_check(module, [api-only])
Checks if some other module exists and is supported on this OS. The parameters
are :
=item module - Name of the module to check.
=item api-only - Set to 1 if you just want to check if the module provides an API that others can call, instead of the full web UI.
=cut
sub foreign_check
{
my ($mod, $api) = @_;
my %minfo;
my $mdir = &module_root_directory($mod);
&read_file_cached("$mdir/module.info", \%minfo) || return 0;
return &check_os_support(\%minfo, undef, undef, $api);
}
=head2 foreign_exists(module)
Checks if some other module exists. The module parameter is the short module
name.
=cut
sub foreign_exists
{
my $mdir = &module_root_directory($_[0]);
return -r "$mdir/module.info";
}
=head2 foreign_available(module)
Returns 1 if some module is installed, and acessible to the current user. The
module parameter is the module directory name.
=cut
sub foreign_available
{
return 0 if (!&foreign_check($_[0]) &&
!$gconfig{'available_even_if_no_support'});
my %foreign_module_info = &get_module_info($_[0]);
# Check list of allowed modules
my %acl;
&read_acl(\%acl, undef, [ $base_remote_user ]);
return 0 if (!$acl{$base_remote_user,$_[0]} &&
!$acl{$base_remote_user,'*'});
# Check for usermod restrictions
my @usermods = &list_usermods();
return 0 if (!&available_usermods( [ \%foreign_module_info ], \@usermods));
if (&get_product_name() eq "webmin") {
# Check if the user has any RBAC privileges in this module
if (&supports_rbac($_[0]) &&
&use_rbac_module_acl(undef, $_[0])) {
# RBAC is enabled for this user and module - check if he
# has any rights
my $rbacs = &get_rbac_module_acl($remote_user, $_[0]);
return 0 if (!$rbacs);
}
elsif ($gconfig{'rbacdeny_'.$base_remote_user}) {
# If denying access to modules not specifically allowed by
# RBAC, then prevent access
return 0;
}
}
# Check readonly support
if (&is_readonly_mode()) {
return 0 if (!$foreign_module_info{'readonly'});
}
# Check if theme vetos
if (defined(&theme_foreign_available)) {
return 0 if (!&theme_foreign_available($_[0]));
}
# Check if licence module vetos
if ($main::licence_module) {
return 0 if (!&foreign_call($main::licence_module,
"check_module_licence", $_[0]));
}
return 1;
}
=head2 foreign_require(module, [file], [package])
Brings in functions from another module, and places them in the Perl namespace
with the same name as the module. The parameters are :
=item module - The source module's directory name, like sendmail.
=item file - The API file in that module, like sendmail-lib.pl. If missing, all API files are loaded.
=item package - Perl package to place the module's functions and global variables in.
If the original module name contains dashes, they will be replaced with _ in
the package name.
=cut
sub foreign_require
{
my ($mod, $file, $pkg) = @_;
$pkg ||= $mod || "global";
$pkg =~ s/[^A-Za-z0-9]/_/g;
my @files;
if ($file) {
push(@files, $file);
}
else {
# Auto-detect files
my %minfo = &get_module_info($mod);
if ($minfo{'library'}) {
@files = split(/\s+/, $minfo{'library'});
}
else {
@files = ( ($minfo{'cloneof'} || $mod)."-lib.pl" );
}
}
@files = grep { !$main::done_foreign_require{$pkg,$_} } @files;
return 1 if (!@files);
foreach my $f (@files) {
$main::done_foreign_require{$pkg,$f}++;
}
my @OLDINC = @INC;
my $mdir = &module_root_directory($mod);
$mdir =~ /^(.*)$/; # untaint, part 1
$mdir = $1; # untaint, part 2
$mdir && -d $mdir || &error("Module $mod does not exist");
@INC = &unique($mdir, @INC);
if (!&get_module_name() && $mod) {
chdir($mdir);
}
my $old_fmn = $ENV{'FOREIGN_MODULE_NAME'};
my $old_frd = $ENV{'FOREIGN_ROOT_DIRECTORY'};
my $code = "package $pkg; ".
"\$ENV{'FOREIGN_MODULE_NAME'} = '$mod'; ".
"\$ENV{'FOREIGN_ROOT_DIRECTORY'} = '$root_directory'; ";
foreach my $f (@files) {
$code .= "do '$mdir/$f' || die \$@; ";
}
eval $code;
if (defined($old_fmn)) {
$ENV{'FOREIGN_MODULE_NAME'} = $old_fmn;
}
else {
delete($ENV{'FOREIGN_MODULE_NAME'});
}
if (defined($old_frd)) {
$ENV{'FOREIGN_ROOT_DIRECTORY'} = $old_frd;
}
else {
delete($ENV{'FOREIGN_ROOT_DIRECTORY'});
}
@INC = @OLDINC;
if ($@) { &error("
Require $mod/$files[0] failed : $@
"); }
return 1;
}
=head2 foreign_call(module, function, [arg]*)
Call a function in another module. The module parameter is the target module
directory name, function is the perl sub to call, and the remaining parameters
are the arguments. However, unless you need to call a function whose name
is dynamic, it is better to use Perl's cross-module function call syntax
like module::function(args).
=cut
sub foreign_call
{
my $pkg = $_[0] || "global";
$pkg =~ s/[^A-Za-z0-9]/_/g;
my @args = @_[2 .. @_-1];
$main::foreign_args = \@args;
my @rv = eval <&1`;
if ($out =~ /\-+\r?\n(\S+)/) {
$main::get_system_hostname[$m] = $1;
}
else {
$main::get_system_hostname[$m] = "windows";
}
}
}
return $main::get_system_hostname[$m];
}
=head2 get_webmin_version
Returns the version of Webmin currently being run, such as 1.450.
=cut
sub get_webmin_version
{
my ($ui_format_dev) = @_;
if (!$get_webmin_version) {
open(VERSION, "<$root_directory/version") || return 0;
($get_webmin_version = ) =~ tr/\r|\n//d;
close(VERSION);
}
# Format dev version nicely
if ($ui_format_dev && length($get_webmin_version) == 13) {
return substr($get_webmin_version, 0, 5) . "." . substr($get_webmin_version, 5, 5 - 1) . "." . substr($get_webmin_version, 5 * 2 - 1);
}
else {
return $get_webmin_version;
}
}
=head2 get_module_acl([user], [module], [no-rbac], [no-default])
Returns a hash containing access control options for the given user and module.
By default the current username and module name are used. If the no-rbac flag
is given, the permissions will not be updated based on the user's RBAC role
(as seen on Solaris). If the no-default flag is given, default permissions for
the module will not be included.
=cut
sub get_module_acl
{
my $u = defined($_[0]) ? $_[0] : $base_remote_user;
my $m = defined($_[1]) ? $_[1] : &get_module_name();
my $norbac = $_[2];
my $nodef = $_[3];
$m ||= "";
my $mdir = &module_root_directory($m);
my %rv;
if (!$nodef) {
# Read default ACL first, to be overridden by per-user settings
&read_file_cached("$mdir/defaultacl", \%rv);
# If this isn't a master admin user, apply the negative permissions
# so that he doesn't un-expectedly gain access to new features
my %gacccess;
&read_file_cached("$config_directory/$u.acl", \%gaccess);
if ($gaccess{'negative'}) {
&read_file_cached("$mdir/negativeacl", \%rv);
}
}
my %usersacl;
if (!$norbac && &supports_rbac($m) && &use_rbac_module_acl($u, $m)) {
# RBAC overrides exist for this user in this module
my $rbac = &get_rbac_module_acl(
defined($_[0]) ? $_[0] : $remote_user, $m);
foreach my $r (keys %$rbac) {
$rv{$r} = $rbac->{$r};
}
}
elsif ($u ne '') {
# Use normal Webmin ACL, if a user is set
my $userdb = &get_userdb_string();
my $foundindb = 0;
if ($userdb && ($u ne $base_remote_user || $remote_user_proto)) {
# Look for this user in the user/group DB, if one is defined
# and if the user might be in the DB
my ($dbh, $proto, $prefix, $args) = &connect_userdb($userdb);
if (!ref($dbh)) {
print STDERR "Failed to connect to user database : ".
$dbh."\n";
}
elsif ($proto eq "mysql" || $proto eq "postgresql") {
# Find the user in the SQL DB
my $cmd = $dbh->prepare(
"select id from webmin_user where name = ?");
$cmd && $cmd->execute($u) ||
&error(&text('euserdbacl', $dbh->errstr));
my ($id) = $cmd->fetchrow();
$foundindb = 1 if (defined($id));
$cmd->finish();
# Fetch ACLs with SQL
if ($foundindb) {
my $cmd = $dbh->prepare(
"select attr,value from webmin_user_acl ".
"where id = ? and module = ?");
$cmd && $cmd->execute($id, $m) ||
&error(&text('euserdbacl', $dbh->errstr));
while(my ($a, $v) = $cmd->fetchrow()) {
$rv{$a} = $v;
}
$cmd->finish();
}
}
elsif ($proto eq "ldap") {
# Find user in LDAP
my $rv = $dbh->search(
base => $prefix,
filter => '(&(cn='.$u.')(objectClass='.
$args->{'userclass'}.'))',
scope => 'sub');
if (!$rv || $rv->code) {
&error(&text('euserdbacl',
$rv ? $rv->error : "Unknown error"));
}
my ($user) = $rv->all_entries;
# Find ACL sub-object for the module
my $ldapm = $m || "global";
if ($user) {
my $rv = $dbh->search(
base => $user->dn(),
filter => '(cn='.$ldapm.')',
scope => 'one');
if (!$rv || $rv->code) {
&error(&text('euserdbacl',
$rv ? $rv->error : "Unknown error"));
}
my ($acl) = $rv->all_entries;
if ($acl) {
foreach my $av ($acl->get_value(
'webminAclEntry')) {
my ($a, $v) = split(/=/, $av,2);
$rv{$a} = $v;
}
}
}
}
if (ref($dbh)) {
&disconnect_userdb($userdb, $dbh);
}
}
if (!$foundindb) {
# Read from local files
&read_file_cached("$config_directory/$m/$u.acl", \%rv);
if ($remote_user ne $base_remote_user && !defined($_[0])) {
&read_file_cached(
"$config_directory/$m/$remote_user.acl",\%rv);
}
}
}
# If the ACL says the user should get only safe settings for this module,
# read and apply them
if ($rv{'_safe'}) {
&read_file_cached("$mdir/safeacl", \%rv);
$rv{'noconfig'} = 1;
}
if ($tconfig{'preload_functions'}) {
&load_theme_library();
}
if (defined(&theme_get_module_acl)) {
%rv = &theme_get_module_acl($u, $m, \%rv);
}
# In case module's config expected to be user-based
# only, we must not consider `noconfig` option at all.
if (&get_module_preferences_acl($m, 'allowed') eq "*") {
$rv{'noconfig'} = 0;
}
return %rv;
}
=head2 get_group_module_acl(group, [module], [no-default])
Returns the ACL for a Webmin group, in an optional module (which defaults to
the current module).
=cut
sub get_group_module_acl
{
my $g = $_[0];
my $m = defined($_[1]) ? $_[1] : &get_module_name();
my $mdir = &module_root_directory($m);
my %rv;
if (!$_[2]) {
&read_file_cached("$mdir/defaultacl", \%rv);
}
my $userdb = &get_userdb_string();
my $foundindb = 0;
if ($userdb) {
# Look for this group in the user/group DB
my ($dbh, $proto, $prefix, $args) = &connect_userdb($userdb);
ref($dbh) || &error(&text('egroupdbacl', $dbh));
if ($proto eq "mysql" || $proto eq "postgresql") {
# Find the group in the SQL DB
my $cmd = $dbh->prepare(
"select id from webmin_group where name = ?");
if (!$cmd || !$cmd->execute($g)) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl', $dbh->errstr));
}
my ($id) = $cmd->fetchrow();
$foundindb = 1 if (defined($id));
$cmd->finish();
# Fetch ACLs with SQL
if ($foundindb) {
my $cmd = $dbh->prepare(
"select attr,value from webmin_group_acl ".
"where id = ? and module = ?");
if (!$cmd || !$cmd->execute($id, $m)) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl', $dbh->errstr));
}
while(my ($a, $v) = $cmd->fetchrow()) {
$rv{$a} = $v;
}
$cmd->finish();
}
}
elsif ($proto eq "ldap") {
# Find group in LDAP
my $rv = $dbh->search(
base => $prefix,
filter => '(&(cn='.$g.')(objectClass='.
$args->{'groupclass'}.'))',
scope => 'sub');
if (!$rv || $rv->code) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl',
$rv ? $rv->error : "Unknown error"));
}
my ($group) = $rv->all_entries;
# Find ACL sub-object for the module
my $ldapm = $m || "global";
if ($group) {
my $rv = $dbh->search(
base => $group->dn(),
filter => '(cn='.$ldapm.')',
scope => 'one');
if (!$rv || $rv->code) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl',
$rv ? $rv->error : "Unknown error"));
}
my ($acl) = $rv->all_entries;
if ($acl) {
foreach my $av ($acl->get_value(
'webminAclEntry')) {
my ($a, $v) = split(/=/, $av, 2);
$rv{$a} = $v;
}
}
}
}
&disconnect_userdb($userdb, $dbh);
}
if (!$foundindb) {
# Read from local files
&read_file_cached("$config_directory/$m/$g.gacl", \%rv);
}
if (defined(&theme_get_module_acl)) {
%rv = &theme_get_module_acl($g, $m, \%rv);
}
return %rv;
}
=head2 save_module_acl(&acl, [user], [module], [never-update-group])
Updates the acl hash for some user and module. The parameters are :
=item acl - Hash reference for the new access control options, or undef to clear
=item user - User to update, defaulting to the current user.
=item module - Module to update, defaulting to the caller.
=item never-update-group - Never update the user's group's ACL
=cut
sub save_module_acl
{
my $u = defined($_[1]) ? $_[1] : $base_remote_user;
my $m = defined($_[2]) ? $_[2] : &get_module_name();
$u eq "webmin" && &error("Invalid username webmin for ACL");
if (!$_[3] && &foreign_check("acl")) {
# Check if this user is a member of a group, and if he gets the
# module from a group. If so, update its ACL as well
&foreign_require("acl", "acl-lib.pl");
my $group;
foreach my $g (&acl::list_groups()) {
if (&indexof($u, @{$g->{'members'}}) >= 0 &&
&indexof($m, @{$g->{'modules'}}) >= 0) {
$group = $g;
last;
}
}
if ($group) {
&save_group_module_acl($_[0], $group->{'name'}, $m);
}
}
my $userdb = &get_userdb_string();
my $foundindb = 0;
if ($userdb && ($u ne $base_remote_user || $remote_user_proto)) {
# Look for this user in the user/group DB
my ($dbh, $proto, $prefix, $args) = &connect_userdb($userdb);
ref($dbh) || &error(&text('euserdbacl', $dbh));
if ($proto eq "mysql" || $proto eq "postgresql") {
# Find the user in the SQL DB
my $cmd = $dbh->prepare(
"select id from webmin_user where name = ?");
if (!$cmd || !$cmd->execute($u)) {
&disconnect_userdb($userdb, $dbh);
&error(&text('euserdbacl2', $dbh->errstr));
}
my ($id) = $cmd->fetchrow();
$foundindb = 1 if (defined($id));
$cmd->finish();
# Replace ACLs for user
if ($foundindb) {
my $cmd = $dbh->prepare("delete from webmin_user_acl ".
"where id = ? and module = ?");
if (!$cmd || !$cmd->execute($id, $m)) {
&disconnect_userdb($userdb, $dbh);
&error(&text('euserdbacl', $dbh->errstr));
}
$cmd->finish();
if ($_[0]) {
my $cmd = $dbh->prepare(
"insert into webmin_user_acl ".
"(id,module,attr,value) values (?,?,?,?)");
$cmd || &error(&text('euserdbacl2',
$dbh->errstr));
foreach my $a (keys %{$_[0]}) {
if (!$cmd->execute($id,$m,$a,
$_[0]->{$a})) {
&disconnect_userdb(
$userdb, $dbh);
&error(&text('euserdbacl2',
$dbh->errstr));
}
$cmd->finish();
}
}
}
}
elsif ($proto eq "ldap") {
# Find the user in LDAP
my $rv = $dbh->search(
base => $prefix,
filter => '(&(cn='.$u.')(objectClass='.
$args->{'userclass'}.'))',
scope => 'sub');
if (!$rv || $rv->code) {
&disconnect_userdb($userdb, $dbh);
&error(&text('euserdbacl',
$rv ? $rv->error : "Unknown error"));
}
my ($user) = $rv->all_entries;
if ($user) {
# Find the ACL sub-object for the module
$foundindb = 1;
my $ldapm = $m || "global";
my $rv = $dbh->search(
base => $user->dn(),
filter => '(cn='.$ldapm.')',
scope => 'one');
if (!$rv || $rv->code) {
&disconnect_userdb($userdb, $dbh);
&error(&text('euserdbacl',
$rv ? $rv->error : "Unknown error"));
}
my ($acl) = $rv->all_entries;
my @al;
foreach my $a (keys %{$_[0]}) {
push(@al, $a."=".$_[0]->{$a});
}
if ($acl) {
# Update attributes
$rv = $dbh->modify($acl->dn(),
replace => { "webminAclEntry", \@al });
}
else {
# Add a sub-object
my @attrs = ( "cn", $ldapm,
"objectClass", "webminAcl",
"webminAclEntry", \@al );
$rv = $dbh->add("cn=".$ldapm.",".$user->dn(),
attr => \@attrs);
}
if (!$rv || $rv->code) {
&disconnect_userdb($userdb, $dbh);
&error(&text('euserdbacl2',
$rv ? $rv->error : "Unknown error"));
}
}
}
&disconnect_userdb($userdb, $dbh);
}
if (!$foundindb) {
# Save ACL to local file
if (!-d "$config_directory/$m") {
mkdir("$config_directory/$m", 0755);
}
if ($_[0]) {
&write_file("$config_directory/$m/$u.acl", $_[0]);
}
else {
&unlink_file("$config_directory/$m/$u.acl");
}
}
}
=head2 save_group_module_acl(&acl, group, [module], [never-update-group])
Updates the acl hash for some group and module. The parameters are :
=item acl - Hash reference for the new access control options.
=item group - Group name to update.
=item module - Module to update, defaulting to the caller.
=item never-update-group - Never update the parent group's ACL
=cut
sub save_group_module_acl
{
my $g = $_[1];
my $m = defined($_[2]) ? $_[2] : &get_module_name();
if (!$_[3] && &foreign_check("acl")) {
# Check if this group is a member of a group, and if it gets the
# module from a group. If so, update the parent ACL as well
&foreign_require("acl", "acl-lib.pl");
my $group;
foreach my $pg (&acl::list_groups()) {
if (&indexof('@'.$g, @{$pg->{'members'}}) >= 0 &&
&indexof($m, @{$pg->{'modules'}}) >= 0) {
$group = $g;
last;
}
}
if ($group) {
&save_group_module_acl($_[0], $group->{'name'}, $m);
}
}
my $userdb = &get_userdb_string();
my $foundindb = 0;
if ($userdb) {
# Look for this group in the user/group DB
my ($dbh, $proto, $prefix, $args) = &connect_userdb($userdb);
ref($dbh) || &error(&text('egroupdbacl', $dbh));
if ($proto eq "mysql" || $proto eq "postgresql") {
# Find the group in the SQL DB
my $cmd = $dbh->prepare(
"select id from webmin_group where name = ?");
if (!$cmd || !$cmd->execute($g)) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl2', $dbh->errstr));
}
my ($id) = $cmd->fetchrow();
$foundindb = 1 if (defined($id));
$cmd->finish();
# Replace ACLs for group
if ($foundindb) {
my $cmd = $dbh->prepare("delete from webmin_group_acl ".
"where id = ? and module = ?");
if (!$cmd || !$cmd->execute($id, $m)) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl', $dbh->errstr));
}
$cmd->finish();
if ($_[0]) {
my $cmd = $dbh->prepare(
"insert into webmin_group_acl ".
"(id,module,attr,value) values (?,?,?,?)");
if (!$cmd) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl2',
$dbh->errstr));
}
foreach my $a (keys %{$_[0]}) {
if (!$cmd->execute($id,$m,$a,
$_[0]->{$a})) {
&disconnect_userdb(
$userdb, $dbh);
&error(&text('egroupdbacl2',
$dbh->errstr));
}
$cmd->finish();
}
}
}
}
elsif ($proto eq "ldap") {
# Find the group in LDAP
my $rv = $dbh->search(
base => $prefix,
filter => '(&(cn='.$g.')(objectClass='.
$args->{'groupclass'}.'))',
scope => 'sub');
if (!$rv || $rv->code) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl',
$rv ? $rv->error : "Unknown error"));
}
my ($group) = $rv->all_entries;
my $ldapm = $m || "global";
if ($group) {
# Find the ACL sub-object for the module
$foundindb = 1;
my $rv = $dbh->search(
base => $group->dn(),
filter => '(cn='.$ldapm.')',
scope => 'one');
if (!$rv || $rv->code) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl',
$rv ? $rv->error : "Unknown error"));
}
my ($acl) = $rv->all_entries;
my @al;
foreach my $a (keys %{$_[0]}) {
push(@al, $a."=".$_[0]->{$a});
}
if ($acl) {
# Update attributes
$rv = $dbh->modify($acl->dn(),
replace => { "webminAclEntry", \@al });
}
else {
# Add a sub-object
my @attrs = ( "cn", $ldapm,
"objectClass", "webminAcl",
"webminAclEntry", \@al );
$rv = $dbh->add("cn=".$ldapm.",".$group->dn(),
attr => \@attrs);
}
if (!$rv || $rv->code) {
&disconnect_userdb($userdb, $dbh);
&error(&text('egroupdbacl2',
$rv ? $rv->error : "Unknown error"));
}
}
}
&disconnect_userdb($userdb, $dbh);
}
if (!$foundindb) {
# Save ACL to local file
if (!-d "$config_directory/$m") {
mkdir("$config_directory/$m", 0755);
}
if ($_[0]) {
&write_file("$config_directory/$m/$g.gacl", $_[0]);
}
else {
&unlink_file("$config_directory/$m/$g.gacl");
}
}
}
=head2 init_config
This function must be called by all Webmin CGI scripts, either directly or
indirectly via a per-module lib.pl file. It performs a number of initialization
and housekeeping tasks, such as working out the module name, checking that the
current user has access to the module, and populating global variables. Some
of the variables set include :
=item $config_directory - Base Webmin config directory, typically /etc/webmin
=item $var_directory - Base logs directory, typically /var/webmin
=item %config - Per-module configuration.
=item %gconfig - Global configuration.
=item $scriptname - Base name of the current perl script.
=item $module_name - The name of the current module.
=item $module_config_directory - The config directory for this module.
=item $module_config_file - The config file for this module.
=item $module_var_directory - The data directory for this module.
=item $module_root_directory - This module's code directory.
=item $webmin_logfile - The detailed logfile for webmin.
=item $remote_user - The actual username used to login to webmin.
=item $base_remote_user - The username whose permissions are in effect.
=item $current_theme - The theme currently in use.
=item $root_directory - The first root directory of this webmin install.
=item @root_directories - All root directories for this webmin install.
=cut
sub init_config
{
# Record first process ID that called this, so we know when it exited to clean
# up temp files
$main::initial_process_id ||= $$;
# Configuration and spool directories
if (!defined($ENV{'WEBMIN_CONFIG'})) {
die "WEBMIN_CONFIG not set";
}
$config_directory = $ENV{'WEBMIN_CONFIG'};
if (!defined($ENV{'WEBMIN_VAR'})) {
open(VARPATH, "<$config_directory/var-path");
chop($var_directory = );
close(VARPATH);
}
else {
$var_directory = $ENV{'WEBMIN_VAR'};
}
$main::http_cache_directory = $ENV{'WEBMIN_VAR'}."/cache";
$main::default_debug_log_file = $ENV{'WEBMIN_VAR'}."/webmin.debug";
if ($ENV{'SESSION_ID'}) {
# Hide this variable from called programs, but keep it for internal use
$main::session_id = $ENV{'SESSION_ID'};
delete($ENV{'SESSION_ID'});
}
if ($ENV{'REMOTE_PASS'}) {
# Hide the password too
$main::remote_pass = $ENV{'REMOTE_PASS'};
delete($ENV{'REMOTE_PASS'});
}
if ($> == 0 && $< != 0 && !$ENV{'FOREIGN_MODULE_NAME'}) {
# Looks like we are running setuid, but the real UID hasn't been set.
# Do so now, so that executed programs don't get confused
$( = $);
$< = $>;
}
# Read the webmin global config file. This contains the OS type and version,
# OS specific configuration and global options such as proxy servers
$config_file = "$config_directory/config";
%gconfig = ( );
&read_file_cached($config_file, \%gconfig);
$gconfig{'webprefix'} = '' if (!exists($gconfig{'webprefix'}));
$null_file = $gconfig{'os_type'} eq 'windows' ? "NUL" : "/dev/null";
$path_separator = $gconfig{'os_type'} eq 'windows' ? ';' : ':';
# Work out of this is a web, command line or cron job
if (!$main::webmin_script_type) {
if ($ENV{'SCRIPT_NAME'}) {
# Run via a CGI
$main::webmin_script_type = 'web';
}
else {
# Cron jobs have no TTY
if ($gconfig{'os_type'} eq 'windows' ||
open(DEVTTY, ">/dev/tty")) {
$main::webmin_script_type = 'cmd';
close(DEVTTY);
}
else {
$main::webmin_script_type = 'cron';
}
}
}
# If this is a cron job, suppress STDERR warnings
if ($main::webmin_script_type eq 'cron') {
$SIG{__WARN__} = sub { };
}
# If debugging is enabled, open the debug log
if (($ENV{'WEBMIN_DEBUG'} || $gconfig{'debug_enabled'}) &&
!$main::opened_debug_log++) {
my $dlog = $gconfig{'debug_file'} || $main::default_debug_log_file;
my $dsize = $gconfig{'debug_size'} || $main::default_debug_log_size;
my @st = stat($dlog);
if ($dsize && $st[7] > $dsize) {
rename($dlog, $dlog.".0");
}
open(main::DEBUGLOG, ">>$dlog");
$main::opened_debug_log = 1;
if ($gconfig{'debug_what_start'}) {
my $script_name = $0 =~ /([^\/]+)$/ ? $1 : '-';
$main::debug_log_start_time = time();
&webmin_debug_log("START", "script=$script_name");
}
}
# Set PATH and LD_LIBRARY_PATH
if ($gconfig{'path'}) {
if ($gconfig{'syspath'}) {
# Webmin only
$ENV{'PATH'} = $gconfig{'path'};
}
else {
# Include OS too
$ENV{'PATH'} = $gconfig{'path'}.$path_separator.$ENV{'PATH'};
}
$ENV{'PATH'} = join($path_separator,
&unique(split($path_separator, $ENV{'PATH'})));
}
$ENV{$gconfig{'ld_env'}} = $gconfig{'ld_path'} if ($gconfig{'ld_env'});
# Set http_proxy and ftp_proxy environment variables, based on Webmin settings
if ($gconfig{'http_proxy'}) {
$ENV{'http_proxy'} = $gconfig{'http_proxy'};
}
if ($gconfig{'ftp_proxy'}) {
$ENV{'ftp_proxy'} = $gconfig{'ftp_proxy'};
}
if ($gconfig{'noproxy'}) {
$ENV{'no_proxy'} = $gconfig{'noproxy'};
}
# Find all root directories
my %miniserv;
if (&get_miniserv_config(\%miniserv)) {
@root_directories = ( $miniserv{'root'} );
for($i=0; defined($miniserv{"extraroot_$i"}); $i++) {
push(@root_directories, $miniserv{"extraroot_$i"});
}
}
# Work out which module we are in, and read the per-module config file
$0 =~ s/\\/\//g; # Force consistent path on Windows
if (defined($ENV{'FOREIGN_MODULE_NAME'}) && $ENV{'FOREIGN_ROOT_DIRECTORY'}) {
# In a foreign call - use the module name given
$root_directory = $ENV{'FOREIGN_ROOT_DIRECTORY'};
$module_name = $ENV{'FOREIGN_MODULE_NAME'};
@root_directories = ( $root_directory ) if (!@root_directories);
}
elsif ($ENV{'SCRIPT_NAME'}) {
my $sn = $ENV{'SCRIPT_NAME'};
$sn =~ s/^$gconfig{'webprefix'}\//\//
if (!$gconfig{'webprefixnoredir'});
if ($sn =~ /^\/([^\/]+)\//) {
# Get module name from CGI path
$module_name = $1;
}
if ($ENV{'SERVER_ROOT'}) {
$root_directory = $ENV{'SERVER_ROOT'};
}
elsif ($ENV{'SCRIPT_FILENAME'}) {
$root_directory = $ENV{'SCRIPT_FILENAME'};
$root_directory =~ s/$sn$//;
}
@root_directories = ( $root_directory ) if (!@root_directories);
}
else {
# Get root directory from miniserv.conf, and deduce module name from $0
$root_directory = $root_directories[0];
my $rok = 0;
foreach my $r (@root_directories) {
if ($0 =~ /^$r\/([^\/]+)\/[^\/]+$/i) {
# Under a module directory
$module_name = $1;
$rok = 1;
last;
}
elsif ($0 =~ /^$root_directory\/[^\/]+$/i) {
# At the top level
$rok = 1;
last;
}
}
&error("Script was not run with full path (failed to find $0 under $root_directory)") if (!$rok);
}
# Set the umask based on config
if ($gconfig{'umask'} ne '' && !$main::umask_already++) {
umask(oct($gconfig{'umask'}));
}
# If this is a cron job or other background task, set the nice level
if (!$main::nice_already && $main::webmin_script_type eq 'cron') {
# Set nice level
if ($gconfig{'nice'}) {
eval 'POSIX::nice($gconfig{\'nice\'});';
}
# Set IO scheduling class and priority
if ($gconfig{'sclass'} ne '' || $gconfig{'sprio'} ne '') {
my $cmd = "ionice";
$cmd .= " -c ".quotemeta($gconfig{'sclass'})
if ($gconfig{'sclass'} ne '');
$cmd .= " -n ".quotemeta($gconfig{'sprio'})
if ($gconfig{'sprio'} ne '');
$cmd .= " -p $$";
&execute_command("$cmd >/dev/null 2>&1");
}
}
$main::nice_already++;
# Get the username
my $u = $ENV{'BASE_REMOTE_USER'} || $ENV{'REMOTE_USER'};
$base_remote_user = $u;
$remote_user = $ENV{'REMOTE_USER'};
# Work out if user is definitely in the DB, and if so get his attrs
$remote_user_proto = $ENV{"REMOTE_USER_PROTO"};
%remote_user_attrs = ( );
if ($remote_user_proto) {
my $userdb = &get_userdb_string();
my ($dbh, $proto, $prefix, $args) =
$userdb ? &connect_userdb($userdb) : ( );
if (ref($dbh)) {
if ($proto eq "mysql" || $proto eq "postgresql") {
# Read attrs from SQL
my $cmd = $dbh->prepare("select webmin_user_attr.attr,webmin_user_attr.value from webmin_user_attr,webmin_user where webmin_user_attr.id = webmin_user.id and webmin_user.name = ?");
if ($cmd && $cmd->execute($base_remote_user)) {
while(my ($attr, $value) = $cmd->fetchrow()) {
$remote_user_attrs{$attr} = $value;
}
$cmd->finish();
}
}
elsif ($proto eq "ldap") {
# Read attrs from LDAP
my $rv = $dbh->search(
base => $prefix,
filter => '(&(cn='.$base_remote_user.')'.
'(objectClass='.
$args->{'userclass'}.'))',
scope => 'sub');
my ($u) = $rv && !$rv->code ? $rv->all_entries : ( );
if ($u) {
foreach $la ($u->get_value('webminAttr')) {
my ($attr, $value) = split(/=/, $la, 2);
$remote_user_attrs{$attr} = $value;
}
}
}
&disconnect_userdb($userdb, $dbh);
}
}
if ($module_name) {
# Find and load the configuration file for this module
my (@ruinfo, $rgroup);
$module_config_directory = "$config_directory/$module_name";
if (&get_product_name() eq "usermin" &&
-r "$module_config_directory/config.$remote_user") {
# Based on username
$module_config_file = "$module_config_directory/config.$remote_user";
}
elsif (&get_product_name() eq "usermin" &&
(@ruinfo = getpwnam($remote_user)) &&
($rgroup = getgrgid($ruinfo[3])) &&
-r "$module_config_directory/config.\@$rgroup") {
# Based on group name
$module_config_file = "$module_config_directory/config.\@$rgroup";
}
else {
# Global config
$module_config_file = "$module_config_directory/config";
}
%config = ( );
&read_file_cached($module_config_file, \%config);
&load_module_preferences($module_name, \%config);
# Create a module-specific var directory
my $var_base = "$var_directory/modules";
if (!-d $var_base) {
&make_dir($var_base, 0700);
}
$module_var_directory = "$var_base/$module_name";
if (!-d $module_var_directory) {
&make_dir($module_var_directory, 0700);
}
# Fix up windows-specific substitutions in values
foreach my $k (keys %config) {
if ($config{$k} =~ /\$\{systemroot\}/) {
my $root = &get_windows_root();
$config{$k} =~ s/\$\{systemroot\}/$root/g;
}
}
}
# Record the initial module
$main::initial_module_name ||= $module_name;
# Set some useful variables
my $current_themes;
$current_themes = defined($ENV{'THEME_DIRS'}) ? $ENV{'THEME_DIRS'} :
$ENV{'MOBILE_DEVICE'} && defined($gconfig{'mobile_theme'}) ?
$gconfig{'mobile_theme'} :
defined($remote_user_attrs{'theme'}) ?
$remote_user_attrs{'theme'} :
defined($gconfig{'theme_'.$remote_user}) ?
$gconfig{'theme_'.$remote_user} :
defined($gconfig{'theme_'.$base_remote_user}) ?
$gconfig{'theme_'.$base_remote_user} :
$gconfig{'theme'};
@current_themes = split(/\s+/, $current_themes);
$current_theme = $current_themes[0];
@theme_root_directories = map { "$root_directory/$_" } @current_themes;
$theme_root_directory = $theme_root_directories[0];
@theme_configs = ( );
foreach my $troot (@theme_root_directories) {
my %onetconfig;
&read_file_cached("$troot/config", \%onetconfig);
&read_file_cached("$troot/config", \%tconfig);
push(@theme_configs, \%onetconfig);
}
$tb = defined($tconfig{'cs_header'}) ? "bgcolor=\"#$tconfig{'cs_header'}\"" :
defined($gconfig{'cs_header'}) ? "bgcolor=\"#$gconfig{'cs_header'}\"" :
"bgcolor=\"#9999ff\"";
$cb = defined($tconfig{'cs_table'}) ? "bgcolor=\"#$tconfig{'cs_table'}\"" :
defined($gconfig{'cs_table'}) ? "bgcolor=\"#$gconfig{'cs_table'}\"" :
"bgcolor=\"#cccccc\"";
$tb .= ' '.$tconfig{'tb'} if ($tconfig{'tb'});
$cb .= ' '.$tconfig{'cb'} if ($tconfig{'cb'});
if ($tconfig{'preload_functions'}) {
# Force load of theme functions right now, if requested
&load_theme_library();
}
if ($tconfig{'oofunctions'} && !$main::loaded_theme_oo_library++) {
# Load the theme's Webmin:: package classes
do "$theme_root_directory/$tconfig{'oofunctions'}";
}
$0 =~ /([^\/]+)$/;
$scriptname = $1;
$webmin_logfile = $gconfig{'webmin_log'} ? $gconfig{'webmin_log'}
: "$var_directory/webmin.log";
# Load language strings into %text
my @langs = &list_languages();
my $accepted_lang;
if ($gconfig{'acceptlang'}) {
foreach my $a (split(/,/, $ENV{'HTTP_ACCEPT_LANGUAGE'})) {
$a =~ s/;.*//; # Remove ;q=0.5 or similar
my ($al) = grep { $_->{'lang'} eq $a } @langs;
if ($al) {
$accepted_lang = $al->{'lang'};
last;
}
}
}
$current_lang = safe_language($force_lang ? $force_lang :
$accepted_lang ? $accepted_lang :
$remote_user_attrs{'lang'} ? $remote_user_attrs{'lang'} :
$gconfig{"lang_$remote_user"} ? $gconfig{"lang_$remote_user"} :
$gconfig{"lang_$base_remote_user"} ? $gconfig{"lang_$base_remote_user"} :
$gconfig{"lang"} ? $gconfig{"lang"} : $default_lang);
foreach my $l (@langs) {
$current_lang_info = $l if ($l->{'lang'} eq $current_lang);
}
@lang_order_list = &unique($default_lang,
split(/:/, $current_lang_info->{'fallback'}),
$current_lang);
%text = &load_language($module_name);
%text || &error("Failed to determine Webmin root from SERVER_ROOT, SCRIPT_FILENAME or the full command line");
# Get the %module_info for this module
if ($module_name) {
my ($mi) = grep { $_->{'dir'} eq $module_name }
&get_all_module_infos(2);
%module_info = %$mi;
$module_root_directory = &module_root_directory($module_name);
}
if ($module_name && !$main::no_acl_check &&
!defined($ENV{'FOREIGN_MODULE_NAME'}) &&
$main::webmin_script_type eq 'web') {
# Check if the HTTP user can access this module
if (!&foreign_available($module_name)) {
if (!&foreign_check($module_name)) {
&error(&text('emodulecheck',
"$module_info{'desc'}"));
}
else {
&error(&text('emodule', "$u",
"$module_info{'desc'}"));
}
}
$main::no_acl_check++;
}
# Check the Referer: header for nasty redirects
my @referers = split(/\s+/, $gconfig{'referers'});
my $referer_site;
my $r = $ENV{'HTTP_REFERER'};
my $referer_port = $r =~ /^https:/ ? 443 : 80;
if ($r =~ /^(http|https|ftp):\/\/([^:\/]+:[^@\/]+@)?\[([^\]]+)\](:(\d+))?/ ||
$r =~ /^(http|https|ftp):\/\/([^:\/]+:[^@\/]+@)?([^\/:@]+)(:(\d+))?/) {
$referer_site = $3;
$referer_port = $5 if ($5);
}
my $http_host = $ENV{'HTTP_HOST'};
my $http_port = $ENV{'SERVER_PORT'} || 80;
if ($http_host =~ s/:(\d+)$//) {
$http_port = $1;
}
$http_host =~ s/^\[(\S+)\]$/$1/;
my $unsafe_index = $unsafe_index_cgi ||
&get_module_variable('$unsafe_index_cgi');
my $trustvar = $trust_unknown_referers ||
&get_module_variable('$trust_unknown_referers');
my $trust = 0;
if (!$0) {
# Script name not known
$trust = 1;
}
elsif ($trustvar == 1) {
# Module doesn't want referer checking at all
$trust = 1;
}
elsif ($ENV{'DISABLE_REFERERS_CHECK'}) {
# Check disabled by environment, perhaps due to cross-module call
$trust = 1;
}
elsif (($ENV{'SCRIPT_NAME'} =~ /^\/(index.cgi)?$/ ||
$ENV{'SCRIPT_NAME'} =~ /^\/([a-z0-9\_\-]+)\/(index.cgi)?$/i) &&
!$unsafe_index) {
# Script is a module's index.cgi, which is normally safe
$trust = 1;
}
elsif ($0 =~ /(session_login|pam_login)\.cgi$/) {
# Webmin login page, which doesn't get a referer
$trust = 1;
}
elsif ($gconfig{'referer'}) {
# Referer checking disabled completely
$trust = 1;
}
elsif (!$ENV{'MINISERV_CONFIG'}) {
# Not a CGI script
$trust = 1;
}
elsif ($main::no_referers_check) {
# Caller requested disabling of checks completely
$trust = 1;
}
elsif ($ENV{'HTTP_USER_AGENT'} =~ /^Webmin/i) {
# Remote call from Webmin itself
$trust = 1;
}
elsif (!$referer_site) {
# No referer set in URL
if (!$gconfig{'referers_none'}) {
# Known referers are allowed
$trust = 1;
}
elsif ($trustvar == 2) {
# Module wants to trust unknown referers
$trust = 1;
}
else {
$trust = 0;
}
}
elsif (&indexof($referer_site, @referers) >= 0) {
# Site is on the trusted list
$trust = 1;
}
elsif ($referer_site eq $http_host &&
(!$referer_port || !$http_port || $referer_port == $http_port)) {
# Link came from this website
$trust = 1;
}
else {
# Unknown link source
$trust = 0;
}
# Check for trigger URL to simply redirect to root: required for Authentic Theme 19.00+
if ($ENV{'HTTP_X_REQUESTED_WITH'} ne "XMLHttpRequest" &&
$ENV{'REQUEST_URI'} !~ /xhr/ &&
$ENV{'REQUEST_URI'} !~ /pjax/ &&
$ENV{'REQUEST_URI'} !~ /link.cgi\/\d+/ &&
$ENV{'REQUEST_URI'} =~ /xnavigation=1/) {
# Store requested URI if safe
if ($main::session_id && $remote_user) {
my %var;
my $key = 'goto';
my $xnav = "xnavigation=1";
my $url = "$gconfig{'webprefix'}$ENV{'REQUEST_URI'}";
my $salt = substr(encode_base64($main::session_id), 0, 16);
$url =~ s/[?|&]$xnav//g;
$salt =~ tr/A-Za-z0-9//cd;
if (!$trust) {
my @parent_dir = split('/', $url);
$url = $gconfig{'webprefix'} ? $parent_dir[2] : $parent_dir[1];
if ($url =~ /.cgi/) {
$url = "/";
}
else {
$url = "/" . $url . "/";
}
}
# Append random string to stored file name, to process multiple, simultaneous requests
my $url_salt = int(rand() * 10000000);
$var{$key} = $url;
# Write follow URL only once
if (!$main::redirect_built) {
write_file(tempname('.theme_' . $salt . '_' . $url_salt . '_' . get_product_name() . '_' . $key . '_' . $remote_user), \%var);
}
$main::redirect_built++
}
&redirect("/");
}
if (!$trust) {
# Looks like a link from elsewhere .. show an error
$current_theme = undef;
&header($text{'referer_title'}, "", undef, 0, 1, 1);
$prot = lc($ENV{'HTTPS'}) eq 'on' ? "https" : "http";
my $url = "".&html_escape("$prot://$ENV{'HTTP_HOST'}$ENV{'REQUEST_URI'}")."";
if ($referer_site) {
# From a known host
print &text('referer_warn',
"".&html_escape($r)."", $url);
print "
\n";
}
else {
# No referer info given
print &text('referer_warn_unknown', $url),"
\n";
print &text('referer_fix3u'),"
\n";
print &text('referer_fix2u'),"
\n";
}
print "
\n";
&footer();
exit;
}
$main::no_referers_check++;
$main::completed_referers_check++;
# Call theme post-init
if (defined(&theme_post_init_config)) {
&theme_post_init_config(@_);
}
# Record that we have done the calling library in this package
my ($callpkg, $lib) = caller();
$lib =~ s/^.*\///;
$main::done_foreign_require{$callpkg,$lib} = 1;
# If a licence checking is enabled, do it now
if ($gconfig{'licence_module'} && !$main::done_licence_module_check &&
&foreign_check($gconfig{'licence_module'}) &&
-r "$root_directory/$gconfig{'licence_module'}/licence_check.pl") {
my $oldpwd = &get_current_dir();
$main::done_licence_module_check++;
$main::licence_module = $gconfig{'licence_module'};
&foreign_require($main::licence_module, "licence_check.pl");
($main::licence_status, $main::licence_message) =
&foreign_call($main::licence_module, "check_licence");
chdir($oldpwd);
}
# Export global variables to caller
if ($main::export_to_caller) {
foreach my $v ('$config_file', '%gconfig', '$null_file',
'$path_separator', '@root_directories',
'$root_directory', '$module_name',
'$base_remote_user', '$remote_user',
'$remote_user_proto', '%remote_user_attrs',
'$module_config_directory', '$module_config_file',
'%config', '@current_themes', '$current_theme',
'@theme_root_directories', '$theme_root_directory',
'%tconfig','@theme_configs', '$tb', '$cb', '$scriptname',
'$webmin_logfile', '$current_lang',
'$current_lang_info', '@lang_order_list', '%text',
'%module_info', '$module_root_directory',
'$module_var_directory') {
my ($vt, $vn) = split('', $v, 2);
eval "${vt}${callpkg}::${vn} = ${vt}${vn}";
}
}
return 1;
}
=head2 load_language_auto()
Returns 1 or 0, if *.auto files should be used based on options (user
or lang_list.txt)
=cut
sub load_language_auto
{
my $auto = $gconfig{"langauto_$remote_user"};
if (!defined($auto)) {
my $glangauto = $gconfig{'langauto'};
if (defined($glangauto)) {
$auto = $glangauto;
}
else {
my ($clanginfo) = grep { $_->{'lang'} eq $current_lang }
&list_languages();
$auto = $clanginfo->{'auto'} if ($clanginfo);
}
}
return $auto;
}
=head2 load_language([module], [directory])
Returns a hashtable mapping text codes to strings in the appropriate language,
based on the $current_lang global variable, which is in turn set based on
the Webmin user's selection. The optional module parameter tells the function
which module to load strings for, and defaults to the calling module. The
optional directory parameter can be used to load strings from a directory
other than lang.
In regular module development you will never need to call this function
directly, as init_config calls it for you, and places the module's strings
into the %text hash.
=cut
sub load_language
{
my %text;
my $root = $root_directory;
my $ol = $gconfig{'overlang'};
my $auto = load_language_auto();
my ($dir) = ($_[1] || "lang");
# Read global lang files
foreach my $o (@lang_order_list) {
my $ok = &read_file_cached_with_stat("$root/$dir/$o", \%text);
my $ok_auto;
$ok_auto = &read_file_cached_with_stat("$root/$dir/$o.auto", \%text)
if ($auto && -r "$root/$dir/$o.auto");
return () if (!$ok && !$ok_auto && $o eq $default_lang);
}
if ($ol) {
foreach my $o (@lang_order_list) {
&read_file_cached("$root/$ol/$o", \%text);
&read_file_cached("$root/$ol/$o.auto", \%text)
if ($auto && -r "$root/$ol/$o.auto");
}
}
&read_file_cached("$config_directory/custom-lang", \%text);
foreach my $o (@lang_order_list) {
next if ($o eq "en");
&read_file_cached("$config_directory/custom-lang.$o", \%text);
}
my $norefs = $text{'__norefs'};
if ($_[0]) {
# Read module's lang files
delete($text{'__norefs'});
my $mdir = &module_root_directory($_[0]);
foreach my $o (@lang_order_list) {
&read_file_cached_with_stat("$mdir/$dir/$o", \%text);
&read_file_cached_with_stat("$mdir/$dir/$o.auto", \%text)
if($auto && -r "$mdir/$dir/$o.auto");
}
if ($ol) {
foreach my $o (@lang_order_list) {
&read_file_cached("$mdir/$ol/$o", \%text);
&read_file_cached("$mdir/$ol/$o.auto", \%text)
if ($auto && -r "$mdir/$ol/$o.auto");
}
}
&read_file_cached("$config_directory/$_[0]/custom-lang", \%text);
foreach my $o (@lang_order_list) {
next if ($o eq "en");
&read_file_cached("$config_directory/$_[0]/custom-lang.$o",
\%text);
}
$norefs = $text{'__norefs'} if ($norefs);
}
# Replace references to other strings
if (!$norefs) {
foreach $k (keys %text) {
$text{$k} =~ s/\$(\{([^\}]+)\}|([A-Za-z0-9\.\-\_]+))/text_subs($2 || $3,\%text)/ge;
}
}
if (defined(&theme_load_language)) {
&theme_load_language(\%text, $_[0]);
}
return %text;
}
=head2 text_subs(string)
Used internally by load_language to expand $code substitutions in language
files.
=cut
sub text_subs
{
if (substr($_[0], 0, 8) eq "include:") {
local $_;
my $rv;
open(INCLUDE, "<".substr($_[0], 8));
while() {
$rv .= $_;
}
close(INCLUDE);
return $rv;
}
else {
my $t = $_[1]->{$_[0]};
return defined($t) ? $t : '$'.$_[0];
}
}
=head2 text(message, [substitute]+)
Returns a translated message from %text, but with $1, $2, etc.. replaced with
the substitute parameters. This makes it easy to use strings with placeholders
that get replaced with programmatically generated text. For example :
print &text('index_hello', $remote_user),"