]*>|<\/a>//g if (!$allow_links);
$rv =~ s/|<\/pre>//g;
$rv =~ s/
|
]*>/$newline/g;
$rv =~ s/|<\/p>/$newline$newline/g;
$rv =~ s/
|
]*>/$newline$newline/g;
$rv = &entities_to_ascii($rv);
return $rv;
}
# Print functions for caturing output
sub first_capture_print
{
$print_output .= $indent_text.
join("", (map { &html_tags_to_text(&entities_to_ascii($_)) } @_))."\n";
}
sub second_capture_print
{
$print_output .= $indent_text.
join("", (map { &html_tags_to_text(&entities_to_ascii($_)) } @_))."\n\n";
}
sub null_print { }
sub set_all_null_print
{
$first_print = $second_print = $indent_print = $outdent_print = \&null_print;
}
sub set_all_text_print
{
$first_print = \&first_text_print;
$second_print = \&second_text_print;
$indent_print = \&indent_text_print;
$outdent_print = \&outdent_text_print;
}
sub set_all_html_print
{
$first_print = \&first_html_print;
$second_print = \&second_html_print;
$indent_print = \&indent_html_print;
$outdent_print = \&outdent_html_print;
}
sub set_all_capture_print
{
$first_print = \&first_capture_print;
$second_print = \&second_capture_print;
$indent_print = \&indent_text_print;
$outdent_print = \&outdent_text_print;
}
# These functions store and retrieve the current print commands
sub push_all_print
{
push(@print_function_stack, [ $first_print, $second_print,
$indent_print, $outdent_print ]);
&set_all_null_print();
}
sub pop_all_print
{
local $p = pop(@print_function_stack);
($first_print, $second_print, $indent_print, $outdent_print) = @$p;
}
# Start capturing output
sub start_print_capture
{
$print_capture = 1;
$print_output = undef;
}
# Stop capturing output, and return what we have
sub stop_print_capture
{
$print_capture = 1;
return $print_output;
}
sub print_and_capture
{
print @_;
if ($print_capture) {
$print_output .= join("", @_);
}
}
# will_send_domain_email(&domain)
# Returns 1 if email would be sent to this domain at signup time
sub will_send_domain_email
{
local $tmpl = &get_template($_[0]->{'template'});
return $tmpl->{'mail_on'} ne 'none';
}
# send_domain_email(&domain, [force-to], [password])
# Sends the signup email to a new domain owner. Returns a pair containing a
# number (0=failed, 1=success) and an optional message. Also outputs status
# messages.
sub send_domain_email
{
local ($d, $forceto, $pass) = @_;
local $tmpl = &get_template($d->{'template'});
local $mail = $tmpl->{'mail'};
local $subject = $tmpl->{'mail_subject'};
local $cc = $tmpl->{'mail_cc'};
local $bcc = $tmpl->{'mail_bcc'};
if ($tmpl->{'mail_on'} eq 'none') {
return (1, undef);
}
&$first_print($text{'setup_email'});
local %hash = &make_domain_substitions($d, 1);
$hash{'pass'} = $pass if ($pass);
local @erv = &send_template_email($mail, $forceto || $d->{'emailto'},
\%hash, $subject, $cc, $bcc, undef,
&get_global_from_address($d));
if ($erv[0]) {
&$second_print(&text('setup_emailok', $erv[1]));
}
else {
&$second_print(&text('setup_emailfailed', $erv[1]));
}
return @erv;
}
# make_domain_substitions(&domain, [nice-sizes])
# Returns a hash of substitions for email to a virtual server
sub make_domain_substitions
{
local ($d, $nice_sizes) = @_;
local %hash = %$d;
local $tmpl = &get_template($d->{'template'});
delete($hash{''});
$hash{'idndom'} = &show_domain_name($d->{'dom'}); # With unicode
# Convert disabled_time to timestamp
if ($d->{'disabled_time'}) {
$hash{'disabled_time'} = &make_date($d->{'disabled_time'});
}
# Add parent domain info
local $parent;
if ($d->{'parent'}) {
$parent = &get_domain($d->{'parent'});
foreach my $k (keys %$parent) {
$hash{'parent_domain_'.$k} = $parent->{$k};
}
delete($hash{'parent_domain_'});
}
# Add alias target domain info
if ($d->{'alias'}) {
local $alias = &get_domain($d->{'alias'});
foreach my $k (keys %$alias) {
$hash{'alias_domain_'.$k} = $alias->{$k};
}
delete($hash{'alias_domain_'});
}
# Add (first) reseller details
if ($d->{'reseller'} && defined(&get_reseller)) {
my @r = split(/\s+/, $d->{'reseller'});
local $resel = &get_reseller($r[0]);
local $acl = $resel->{'acl'};
$hash{'reseller_name'} = $resel->{'name'};
$hash{'reseller_theme'} = $resel->{'theme'};
$hash{'reseller_modules'} = join(" ", @{$resel->{'modules'}});
foreach my $a (keys %$acl) {
$hash{'reseller_'.$a} = $acl->{$a};
}
}
# Add plan details, if any
local $plan = &get_plan($d->{'plan'});
if ($plan) {
foreach my $k (keys %$plan) {
$hash{'plan_'.$k} = $plan->{$k};
}
}
# Add DNS serial number, for use in DNS templates
if ($config{'dns'}) {
&require_bind();
if ($bind8::config{'soa_style'} == 1) {
$hash{'dns_serial'} = &bind8::date_serial().
sprintf("%2.2d", $bind8::config{'soa_start'});
}
else {
# Use Unix time for date and running number serials
$hash{'dns_serial'} = time();
}
}
else {
# BIND not installed, so default to using unix time for serial
$hash{'dns_serial'} = time();
}
# Add DNS master nameserver
if ($config{'dns'}) {
$hash{'dns_master'} = &get_master_nameserver($tmpl);
}
# Add webmin and usermin ports
$hash{'virtualmin_url'} = &get_virtualmin_url($d);
local %miniserv;
&get_miniserv_config(\%miniserv);
$hash{'webmin_port'} = $miniserv{'port'};
$hash{'webmin_proto'} = $miniserv{'ssl'} ? 'https' : 'http';
if (&foreign_installed('usermin')) {
&foreign_require('usermin');
local %uminiserv;
&usermin::get_usermin_miniserv_config(\%uminiserv);
$hash{'usermin_port'} = $uminiserv{'port'};
$hash{'usermin_proto'} = $uminiserv{'ssl'} ? 'https' : 'http';
}
# Make quotas nicer, if needed
if ($nice_sizes) {
if ($hash{'quota'}) {
$hash{'quota'} = &nice_size($d->{'quota'}*"a_bsize("home"));
}
if ($hash{'uquota'}) {
$hash{'uquota'} = &nice_size($d->{'uquota'}*"a_bsize("home"));
}
if ($hash{'bw_limit'}) {
$hash{'bw_limit'} = &nice_size($d->{'bw_limit'});
}
if ($hash{'bw_usage'}) {
$hash{'bw_usage'} = &nice_size($d->{'bw_usage'});
}
if ($config{'bw_period'}) {
$hash{'bw_period'} = $config{'bw_period'};
$hash{'bw_past'} = '';
}
else {
$hash{'bw_past'} = $config{'bw_past'};
$hash{'bw_period'} = '';
}
}
# Set mysql_pass to blank if missing, so that it can be used if $IF
$hash{'mysql_pass'} ||= '';
$hash{'postgres_pass'} ||= '';
# Setup MySQL and PostgreSQL usernames if not set yet
if ($d->{'mysql'} && !$hash{'mysql_user'}) {
$hash{'mysql_user'} = &mysql_user($d);
if ($d->{'parent'}) {
$hash{'mysql_pass'} = $parent->{'mysql_pass'};
}
}
if ($d->{'postgres'} && !$hash{'postgres_user'}) {
$hash{'postgres_user'} = &postgres_user($d);
if ($d->{'parent'}) {
$hash{'postgres_pass'} = $parent->{'postgres_pass'};
}
}
# Add remote MySQL and PostgreSQL hosts
if ($d->{'mysql'}) {
$hash{'mysql_host'} = &get_database_host_mysql($d);
$hash{'mysql_port'} = &get_database_port_mysql($d);
}
if ($d->{'postgres'}) {
$hash{'postgres_host'} = &get_database_host_postgres($d);
$hash{'postgres_port'} = &get_database_port_postgres($d);
}
# Add random numbers length 1-10
&seed_random();
for(my $i=1; $i<=10; $i++) {
my $r;
do {
$r = int(rand()*(10**$i));
} while(length($r) != $i);
$hash{"RANDOM$i"} = $r;
}
# Add secondary mail servers
local %ids = map { $_, 1 } split(/\s+/, $d->{'mx_servers'});
if (%ids) {
local @servers = grep { $ids{$_->{'id'}} } &list_mx_servers();
$hash{'mx_slaves'} = join(" ", map { $_->{'host'} } @servers);
}
else {
$hash{'mx_slaves'} = '';
}
# Add secondary nameservers
if ($config{'dns'}) {
local %on = map { $_, 1 } split(/\s+/, $d->{'dns_slave'});
local @servers = grep { $on{$_->{'host'}} || $on{$_->{'nsname'}} }
&bind8::list_slave_servers();
$hash{'dns_server'} = &get_master_nameserver($tmpl);
$hash{'dns_slave'} = join(" ", map { $_->{'nsname'} || $_->{'host'} }
@servers);
}
else {
$hash{'dns_slave'} = '';
}
# Add sub-domain names
if (!$d->{'parent'}) {
my @sds = grep { !$_->{'alias'} } &get_domain_by("parent", $d->{'id'});
if (@sds) {
$hash{'sub_domains'} = join(" ", map { $_->{'dom'} } @sds);
}
else {
$hash{'sub_domains'} = '';
}
}
if (!$d->{'alias'}) {
my @ads = &get_domain_by("alias", $d->{'id'});
if (@ads) {
$hash{'alias_domains'} = join(" ", map { $_->{'dom'} } @ads);
}
else {
$hash{'alias_domains'} = '';
}
}
# If any website feature is enabled (like Nginx), set the web variable
if (&domain_has_website($d)) {
$hash{'web'} = 1;
}
# Add domain parts
my @parts = split(/\./, $d->{'dom'});
for(my $i=0; $i<@parts; $i++) {
$hash{'dompart'.$i} = $parts[$i];
}
return %hash;
}
# will_send_user_email([&domain], [new-flag])
# Returns 1 if a new mailbox email would be sent to a user in this domain.
# Will return 0 if no template is defined, or if sending mail to the mailbox
# has been deactivated, or if the domain doesn't even have email
sub will_send_user_email
{
local ($d, $isnew) = @_;
return 0 if ($d->{'domainowner'});
local $tmpl = &get_template($d ? $d->{'template'} : 0);
local $tmode = !$d || $isnew ? "newuser" : "updateuser";
if ($tmpl->{$tmode.'_on'} eq 'none' ||
$tmode eq "newuser" && !$tmpl->{$tmode.'_to_mailbox'}) {
return 0;
}
else {
return 1;
}
}
# send_user_email([&domain], &user, [mailbox-to|'none'], [update-mode])
# Sends email to a new mailbox user, and possibly the domain owner, reseller
# and master admin. Returns a pair containing a number (0=failed, 1=success)
# and an optional message
sub send_user_email
{
local ($d, $user, $userto, $mode) = @_;
local $tmpl = &get_template($d ? $d->{'template'} : 0);
local $tmode = $mode ? "updateuser" : "newuser";
local $subject = $tmpl->{$tmode.'_subject'};
if ($tmpl->{$tmode.'_on'} eq 'none') {
return (1, undef);
}
# Work out who we CC to
local @ccs;
push(@ccs, $tmpl->{$tmode.'_cc'}) if ($tmpl->{$tmode.'_cc'});
push(@ccs, $d->{'emailto'}) if ($d && $tmpl->{$tmode.'_to_owner'});
if ($tmpl->{$tmode.'_to_reseller'} && $d && $d->{'reseller'} &&
defined(&get_reseller)) {
foreach my $r (split(/\s+/, $d->{'reseller'})) {
local $resel = &get_reseller($r);
if ($resel && $resel->{'acl'}->{'email'}) {
push(@ccs, &extract_address_parts(
$resel->{'acl'}->{'email'}));
}
}
}
local $cc = join(",", @ccs);
local $bcc = $tmpl->{$tmode.'_bcc'};
local $mail = $tmpl->{$tmode};
return (1, undef) if ($mail eq 'none');
local %hash = &make_user_substitutions($user, $d);
local $email = $d ? $hash{'mailbox'}.'@'.$hash{'dom'}
: $hash{'user'}.'@'.&get_system_hostname();
# Work out who we send to
if ($userto) {
$email = $userto eq 'none' ? undef : $userto;
}
if ($d && !$tmpl->{$tmode.'_to_mailbox'}) {
# Don't email domain owner if disabled
$email = undef;
}
return (1, undef) if (!$email && !$cc && !$bcc);
return &send_template_email($mail, $email, \%hash,
$subject ||
&entities_to_ascii($mode ? $text{'mail_upsubject'}
: $text{'mail_usubject'}),
$cc, $bcc, $d);
}
# make_user_substitutions(&user, [&domain])
# Create a hash of email substitions for a user in some domain
sub make_user_substitutions
{
local ($user, $d) = @_;
local %hash;
if ($d) {
%hash = ( %$d, %$user );
$hash{'mailbox'} = &remove_userdom($user->{'user'}, $d);
}
else {
%hash = ( %$user );
$hash{'mailbox'} = $hash{'user'};
}
$hash{'plainpass'} ||= "";
$hash{'extra'} = join(" ", @{$user->{'extraemail'}});
# Check SSH and FTP shells
local ($shell) = grep { $_->{'shell'} eq $user->{'shell'} }
&list_available_shells();
if ($shell) {
$hash{'ftp'} = $shell->{'id'} eq 'nologin' ? 0 : 1;
$hash{'ssh'} = $shell->{'id'} eq 'ssh' ? 1 : 0;
}
else {
# Assume FTP but no SSH if unknown shell
$hash{'ftp'} = 1;
$hash{'ssh'} = 0;
}
# Make quotas use nice units
if ($hash{'quota'}) {
$hash{'quota'} = &nice_size($user->{'quota'}*"a_bsize("home"));
}
if ($hash{'uquota'}) {
$hash{'uquota'} = &nice_size($user->{'uquota'}*"a_bsize("home"));
}
if ($hash{'mquota'}) {
$hash{'mquota'} = &nice_size($user->{'mquota'}*"a_bsize("mail"));
}
if ($hash{'umquota'}) {
$hash{'umquota'} = &nice_size($user->{'umquota'}*"a_bsize("mail"));
}
return %hash;
}
# ensure_template(file)
sub ensure_template
{
local ($file) = @_;
local $mpath = "$module_root_directory/$file";
local $cpath = "$module_config_directory/$file";
if (!-s $cpath) {
©_source_dest($mpath, $cpath);
}
}
# get_miniserv_port_proto()
# Returns the port number, protocol (http or https) and hostname for Webmin
sub get_miniserv_port_proto
{
if ($ENV{'SERVER_PORT'}) {
# Running under miniserv
return ( $ENV{'SERVER_PORT'},
$ENV{'HTTPS'} eq 'ON' ? 'https' : 'http',
$ENV{'SERVER_NAME'} );
}
else {
# Get from miniserv config
local %miniserv;
&get_miniserv_config(\%miniserv);
return ( $miniserv{'port'},
$miniserv{'ssl'} ? 'https' : 'http',
&get_system_hostname() );
}
}
# get_usermin_miniserv_port_proto()
# Returns the port number, protocol (http or https) and hostname for Usermin
sub get_usermin_miniserv_port_proto
{
my ($port, $proto, $host);
if (&foreign_installed("usermin")) {
&foreign_require("usermin");
my %miniserv;
&usermin::get_usermin_miniserv_config(
\%miniserv);
$proto = $miniserv{'ssl'} ? 'https' : 'http';
$port = $miniserv{'port'};
}
# Fall back to standard defaults
$proto ||= "https";
$port ||= 20000;
$host ||= &get_system_hostname();
return ($port, $proto, $host);
}
# get_miniserv_base_url()
# Returns the base URL for this Virtualmin install, without the trailing /
sub get_miniserv_base_url
{
my ($port, $proto, $host) = &get_miniserv_port_proto();
my $portstr = $proto eq "http" && $port == 80 ? "" :
$proto eq "https" && $port == 443 ? "" : ":".$port;
return $proto."://".$host.$portstr;
}
# send_template_email(data, address, &substitions, subject, cc, bcc,
# [&domain], [from])
# Sends the given file to the specified address, with the substitions from
# a hash reference. The actual subs in the file must be like $XXX for entries
# in the hash like xxx - ie. $DOM is replaced by the domain name, and $HOME
# by the home directory
sub send_template_email
{
local ($template, $to, $subs, $subject, $cc, $bcc, $d, $from) = @_;
local %hash = %$subs;
# Add in Webmin info to the hash
($hash{'webmin_port'}, $hash{'webmin_proto'}) = &get_miniserv_port_proto();
$template = &substitute_virtualmin_template($template, \%hash);
# Work out the From: address - if a domain is given, use it's email address
# as long as that address is in a local domain with mail
if (!$from && $remote_user && !&master_admin() && $d) {
local $localdom = 0;
local ($emailtouser, $emailtodom) = split(/\@/, $d->{'emailto_addr'});
foreach my $ld (grep { $_->{'mail'} } &list_domains()) {
if (lc($ld->{'dom'}) eq lc($emailtodom)) {
$localdom = 1;
}
}
if ($emailtodom eq &get_system_hostname()) {
$localdom = 1;
}
if ($localdom) {
$from = $d->{'emailto'};
}
}
# Actually send using the mailboxes module
local $subject = &substitute_virtualmin_template($subject, \%hash);
local $cc = &substitute_virtualmin_template($cc, \%hash);
if (!$to) {
# This can happen when a mailbox is not notified about its
# own update or creation
$to = $cc;
$cc = undef;
}
&foreign_require("mailboxes");
# Set content type and encoding based on whether the email contains HTML
# and/or non-ascii characters
local $ctype = $template =~ /]*>|
]*>/i ? "text/html"
: "text/plain";
local $cs = &get_charset();
local $attach =
{ 'headers' => [ [ 'Content-Type', $ctype.'; charset='.$cs ],
[ 'Content-Transfer-Encoding', 'quoted-printable' ] ],
'data' => &mailboxes::quoted_encode($template) };
# Construct and send the email object
local $mail = { 'headers' => [ [ 'From', $from ||
$config{'from_addr'} ||
&mailboxes::get_from_address() ],
[ 'To', $to ],
$cc ? ( [ 'Cc', $cc ] ) : ( ),
$bcc ? ( [ 'Bcc', $bcc ] ) : ( ),
[ 'Subject', &entities_to_ascii($subject) ],
],
'attach' => [ $attach ] };
eval {
local $main::error_must_die = 1;
&mailboxes::send_mail($mail);
};
if ($@) {
return (0, $@);
}
else {
return (1, &text('mail_ok', $to));
}
}
# send_notify_email(from, &doms|&users, [&dom], subject, body,
# [attach, attach-filename, attach-type], [extra-admins],
# [send-many], [charset])
# Sends a single email to multiple recipients. These can be Virtualmin domains
# or users.
sub send_notify_email
{
local ($from, $recips, $d, $subject, $body, $attach, $attachfile, $attachtype,
$admins, $many, $charset) = @_;
&foreign_require("mailboxes");
local %done;
foreach my $r (@$recips) {
# Work out recipient type and addresses
local (@emails, %hash);
if ($r->{'id'}) {
# A domain
push(@emails, $r->{'emailto'});
%hash = &make_domain_substitions($r, 1);
if ($admins) {
# And extra admins
push(@emails, map { $_->{'email'} }
grep { $_->{'email'} }
&list_extra_admins($r));
}
}
else {
# A mailbox user
push(@emails, $r->{'email'} || $r->{'user'});
%hash = &make_user_substitutions($r, $d);
}
# Send to them
foreach my $email (@emails) {
next if (!$many && $done{$email}++);
local $ct = 'text/plain';
if ($charset) {
$ct .= "; charset=".$charset;
}
local $mail = { 'headers' =>
[ [ 'From' => $from ],
[ 'To' => $email ],
[ 'Subject' => &entities_to_ascii(
&substitute_virtualmin_template($subject, \%hash)) ] ],
'attach' =>
[ { 'headers' => [ [ 'Content-type', $ct ] ],
'data' => &entities_to_ascii(
&substitute_virtualmin_template($body, \%hash)) } ] };
if ($attach) {
local $filename = $attachfile;
$filename =~ s/^.*(\\|\/)//;
local $type = $attachtype." name=\"$filename\"";
local $disp = "inline; filename=\"$filename\"";
push(@{$mail->{'attach'}},
{ 'data' => $in{'attach'},
'headers' => [
[ 'Content-type', $type ],
[ 'Content-Disposition', $disp ],
[ 'Content-Transfer-Encoding', 'base64' ] ] });
}
&mailboxes::send_mail($mail);
}
}
}
# get_global_from_address([&domain])
# Returns the from address to use when sending email to some domain. This may
# be the reseller's email (if set), or the system-wide default
sub get_global_from_address
{
local ($d) = @_;
&foreign_require("mailboxes");
local $rv = $config{'from_addr'} || &mailboxes::get_from_address();
if ($d && $d->{'reseller'} && defined(&get_reseller) && $config{'from_reseller'}) {
# From first reseller
my @r = split(/\s+/, $d->{'reseller'});
local $resel = &get_reseller($r[0]);
if ($resel && $resel->{'acl'}->{'email'}) {
# Reseller has an email .... but is it valid for this system?
if ($resel->{'acl'}->{'from'}) {
# Custom from address set
$rv = $resel->{'acl'}->{'from'};
}
else {
my ($rs) = &extract_address_parts($resel->{'acl'}->{'email'});
my ($rsmbox, $rsdom) = split(/\@/, $rs);
my $rsd = &get_domain_by("dom", $rsdom);
if ($rsd || $rsdom eq &get_system_hostname() ||
$config{'from_reseller'} == 2) {
# Yes - safe to use
$rv = $rs;
}
}
}
}
return $rv;
}
# userdom_substitutions(&user, &dom)
# Returns a hash reference of substitutions for a user in a domain
sub userdom_substitutions
{
if ($_[1]) {
$_[0]->{'mailbox'} = &remove_userdom($_[0]->{'user'}, $_[1]);
$_[0]->{'dom'} = $_[1]->{'dom'};
$_[0]->{'dom_prefix'} = substr($_[1]->{'dom'}, 0, 1);
}
return $_[0];
}
# alias_type(string, [alias-name])
# Return the type and destination of some alias string. Type codes are:
# 1 - Email address
# 2 - Include file of addresses
# 3 - Write to file
# 4 - Pipe to program
# 5 - Virtualmin autoreply
# 6 - Webmin filter
# 7 - Mailbox of user
# 8 - Same address at other domain
# 9 - Bounce, possibly with message
# 10- Current user's mailbox
# 11- Throw away
# 13- Everyone in some domain
sub alias_type
{
local @rv;
if ($_[0] =~ /^\|\s*$module_config_directory\/autoreply.pl\s+(\S+)/) {
@rv = (5, $1);
}
elsif ($_[0] =~ /^\|\s*$module_config_directory\/filter.pl\s+(\S+)/) {
@rv = (6, $1);
}
elsif ($_[0] =~ /^\|\s*(.*)$/) {
@rv = (4, $1);
}
elsif ($_[0] eq "./Maildir/") {
return (10);
}
elsif ($_[0] eq "/dev/null") {
return (11);
}
elsif ($_[0] =~ /^(\/.*)$/ || $_[0] =~ /^\.\//) {
@rv = (3, $_[0]);
}
elsif ($_[0] =~ /^:include:\Q$everyone_alias_dir\E\/(\S+)$/) {
return (13, $1);
}
elsif ($_[0] =~ /^:include:(.*)$/) {
@rv = (2, $1);
}
elsif ($_[0] =~ /^\\(\S+)$/) {
if ($1 eq $_[1] || $1 eq "NEWUSER" || $1 eq &replace_atsign($_[1]) ||
$1 eq &escape_user($_[1])) {
return (10);
}
else {
@rv = (7, $1);
}
}
elsif ($_[0] =~ /^\%1\@(\S+)$/) {
@rv = (8, $1);
}
elsif ($_[0] =~ /^BOUNCE\s*(.*)$/) {
@rv = (9, $1);
}
else {
@rv = (1, $_[0]);
}
return wantarray ? @rv : $rv[0];
}
# set_alias_programs()
# Copy the wrapper scripts needed for autoresponders
sub set_alias_programs
{
&require_mail();
# Copy autoresponder
local $mailmod = &foreign_check("sendmail") ? "sendmail" :
$mail_system == 1 ? "sendmail" :
$mail_system == 0 ? "postfix" :
"qmailadmin";
©_source_dest("$root_directory/$mailmod/autoreply.pl",
$module_config_directory);
&system_logged("chmod 755 $module_config_directory/config");
if (-d $sendmail::config{'smrsh_dir'} &&
!-r "$sendmail::config{'smrsh_dir'}/autoreply.pl") {
&system_logged("ln -s $module_config_directory/autoreply.pl $sendmail::config{'smrsh_dir'}/autoreply.pl");
}
# Copy filter program
&system_logged("cp $root_directory/$mailmod/filter.pl $module_config_directory");
&system_logged("chmod 755 $module_config_directory/config");
if (-d $sendmail::config{'smrsh_dir'} &&
!-r "$sendmail::config{'smrsh_dir'}/filter.pl") {
&system_logged("ln -s $module_config_directory/filter.pl $sendmail::config{'smrsh_dir'}/filter.pl");
}
}
# set_domain_envs(&domain, action, [&new-domain], [&old-domain], [&other-envs])
# Sets up VIRTUALSERVER_ environment variables for a domain update or some kind,
# prior to calling making_changes or made_changes. action must be one of
# CREATE_DOMAIN, MODIFY_DOMAIN or DELETE_DOMAIN
sub set_domain_envs
{
local ($d, $action, $newd, $oldd, $others) = @_;
&reset_domain_envs();
$ENV{'VIRTUALSERVER_ACTION'} = $action;
foreach my $e (keys %$d) {
local $env = uc($e);
$env =~ s/\-/_/g;
$ENV{'VIRTUALSERVER_'.$env} = $d->{$e};
}
$ENV{'VIRTUALSERVER_IDNDOM'} = &show_domain_name($d->{'dom'});
if ($newd) {
# Set details of virtual server being changed to. This is only
# done in the pre-modify call
foreach my $e (keys %$newd) {
local $env = uc($e);
$env =~ s/\-/_/g;
$ENV{'VIRTUALSERVER_NEWSERVER_'.$env} = $newd->{$e};
}
$ENV{'VIRTUALSERVER_NEWSERVER_IDNDOM'} =
&show_domain_name($newd->{'dom'});
}
if ($oldd) {
# Set details of virtual server being changed from, in post-modify
foreach my $e (keys %$oldd) {
local $env = uc($e);
$env =~ s/\-/_/g;
$ENV{'VIRTUALSERVER_OLDSERVER_'.$env} = $oldd->{$e};
}
$ENV{'VIRTUALSERVER_OLDSERVER_IDNDOM'} =
&show_domain_name($oldd->{'dom'});
}
local $parent = $d->{'parent'} ? &get_domain($d->{'parent'}) : undef;
local $alias = $d->{'alias'} ? &get_domain($d->{'alias'}) : undef;
if (defined(&get_reseller)) {
# Set (first) reseller details, if we have one
local $rd = $d->{'reseller'} ? $d :
$parent && $parent->{'reseller'} ? $parent : undef;
local ($r) = $rd ? split(/\s+/, $rd->{'reseller'}) : undef;
local $resel = $r ? &get_reseller($r) : undef;
if ($resel) {
local $acl = $resel->{'acl'};
$ENV{'RESELLER_NAME'} = $resel->{'name'};
$ENV{'RESELLER_THEME'} = $resel->{'theme'};
$ENV{'RESELLER_MODULES'} = join(" ", @{$resel->{'modules'}});
foreach my $a (keys %$acl) {
local $env = uc($a);
$env =~ s/\-/_/g;
$ENV{'RESELLER_'.$env} = $acl->{$a};
}
}
}
if ($parent) {
# Set parent domain variables
foreach my $e (keys %$parent) {
local $env = uc($e);
$env =~ s/\-/_/g;
$ENV{'PARENT_VIRTUALSERVER_'.$env} = $parent->{$e};
}
$ENV{'PARENT_VIRTUALSERVER_IDNDOM'} =
&show_domain_name($parent->{'dom'});
}
if ($alias) {
# Set alias domain variables
foreach my $e (keys %$alias) {
local $env = uc($e);
$env =~ s/\-/_/g;
$ENV{'ALIAS_VIRTUALSERVER_'.$env} = $alias->{$e};
}
$ENV{'ALIAS_VIRTUALSERVER_IDNDOM'} =
&show_domain_name($alias->{'dom'});
}
# Set global variables
foreach my $v (&get_global_template_variables()) {
if ($v->{'enabled'}) {
$ENV{'GLOBAL_'.uc($v->{'name'})} = $v->{'value'};
}
}
# Set other variables
if ($others) {
foreach my $e (keys %$others) {
$ENV{uc($e)} = $others->{$e};
$ENV{"VIRTUALSERVER_".uc($e)} = $others->{$e};
}
}
}
# reset_domain_envs(&domain)
# Removes all environment variables set by set_domain_envs
sub reset_domain_envs
{
foreach my $e (keys %ENV) {
delete($ENV{$e}) if ($e =~ /^(VIRTUALSERVER_|RESELLER_|PARENT_VIRTUALSERVER_|GLOBAL_)/);
}
}
# making_changes()
# Called before a domain is created, modified or deleted to run the
# pre-change command
sub making_changes
{
my $cmd = $ENV{'VIRTUALMIN_PRE_COMMAND'} || $config{'pre_command'};
my $output = $ENV{'VIRTUALMIN_OUTPUT_COMMAND'} || $config{'output_command'};
if ($cmd =~ /\S/) {
&clean_changes_environment();
local $out = &backquote_logged(
"($cmd) 2>&1 &1 \n";
foreach $k (@_) {
print " | \${$k} | \n";
print "",$text{"sub_".$k}," |
\n";
}
print "\n";
print "$text{'sub_if'}\n";
}
# alias_form(&to, left, &domain, "user"|"alias", user|alias, [&tds])
# Prints HTML for selecting 0 or more alias destinations
sub alias_form
{
local ($to, $left, $d, $mode, $who, $tds) = @_;
&require_mail();
local @typenames = map { $text{"alias_type$_"} } (0 .. 13);
$typenames[0] = "<$typenames[0]>";
local @values = @$to;
local $i;
for(my $i=0; $i<=@values+2; $i++) {
local ($type, $val) = $values[$i] ? &alias_type($values[$i], $_[4])
: (0, "");
# Generate drop-down menu for alias type
local @opts;
local $j;
for($j=0; $j<@typenames; $j++) {
next if ($j == 8 && $_[3] eq "user"); # to domain not valid
# for users
next if ($j == 10 && $_[3] ne "user"); # user's mailbox not
# valid for aliases
next if ($j == 9 && $_[3] eq "user"); # bounce is not valid
# for users
next if ($j == 13 && $_[3] eq "user"); # everyone is not valid
# for users
if ($j == 0 || $can_alias_types{$j} || $type == $j) {
push(@opts, [ $j, $typenames[$j] ]);
}
}
local $f = &ui_select("type_$i", $type, \@opts);
if ($type == 7) {
$val = &unescape_user($val);
}
elsif ($type == 13) {
# Everyone in some domain
local $d = &get_domain($val);
if ($d) {
$val = $d->{'dom'};
}
}
$f .= &ui_textbox("val_$i", $val, 30)."\n";
if (&can_edit_afiles()) {
local $prog = $type == 2 ? "edit_afile.cgi" :
$type == 5 ? "edit_rfile.cgi" :
$type == 6 ? "edit_ffile.cgi" : undef;
if ($prog && $_[2]) {
local $di = $_[2] ? $_[2]->{'id'} : undef;
$f .= "$text{'alias_afile'}\n";
}
}
print &ui_table_row($left, $f, undef, $tds);
$left = " ";
}
}
# parse_alias(catchall, name, &old-values, "user"|"alias", &domain)
# Returns a list of values for an alias, taken from the form generated by
# &alias_form
sub parse_alias
{
local (@values, $i, $t, $anysame, $anybounce);
for($i=0; defined($t = $in{"type_$i"}); $i++) {
!$t || $can_alias_types{$t} ||
&error($text{'alias_etype'}." : ".$text{'alias_type'.$t});
local $v = $in{"val_$i"};
$v =~ s/^\s+//;
$v =~ s/\s+$//;
if ($t == 1 && $v !~ /^([^\|\:\"\' \t\/\\\%]\S*)$/) {
&error(&text('alias_etype1', $v));
}
elsif ($t == 1 && !&can_forward_alias($v)) {
&error(&text('alias_etype1f', $v));
}
elsif ($t == 3 && $v !~ /^\/(\S+)$/ && $v !~ /^\.\//) {
&error(&text('alias_etype3', $v));
}
elsif ($t == 4) {
$v =~ /^(\S+)/ || &error($text{'alias_etype4none'});
my $prog = $1;
(-x $prog) && &check_aliasfile($prog, 0) ||
$prog eq "if" || $prog eq "export" || &has_command($prog) ||
&error(&text('alias_etype4', $prog));
}
elsif ($t == 8 && $v !~ /^[a-z0-9\.\-\_]+$/) {
&error(&text('alias_etype8', $v));
}
elsif ($t == 8 && !$_[0]) {
&error(&text('alias_ecatchall', $v));
}
elsif ($t == 13 && !&get_domain_by("dom", $v)) {
&error(&text('alias_eeveryone', $v));
}
if ($t == 1 || $t == 3) { push(@values, $v); }
elsif ($t == 2) {
$v = "$d->{'home'}/$v" if ($v !~ /^\//);
push(@values, ":include:$v");
}
elsif ($t == 4) {
push(@values, "|$v");
}
elsif ($t == 5) {
# Setup autoreply script
$v = "$d->{'home'}/$v" if ($v !~ /^\//);
push(@values, "|$module_config_directory/autoreply.pl ".
"$v $name");
&set_alias_programs();
}
elsif ($t == 6) {
# Setup filter script
$v = "$d->{'home'}/$v" if ($v !~ /^\//);
push(@values, "|$module_config_directory/filter.pl ".
"$v $name");
&set_alias_programs();
}
elsif ($t == 7) {
push(@values, "\\".&escape_user($v));
}
elsif ($t == 8) {
push(@values, "\%1\@$v");
$anysame++;
}
elsif ($t == 9) {
push(@values, "BOUNCE".($v ? " $v" : ""));
$anybounce++;
}
elsif ($t == 10) {
# Alias to self .. may need to used at-escaped name
if ($mail_system == 0 && $_[1] =~ /\@/) {
push(@values, "\\".&escape_user(&replace_atsign_if_exists($_[1])));
}
else {
push(@values, "\\".&escape_user($_[1]));
}
}
elsif ($t == 11) {
push(@values, "/dev/null");
}
elsif ($t == 13) {
# Work out ID for everyone file
local $d = &get_domain_by("dom", $v);
&create_everyone_file($d);
push(@values, ":include:$everyone_alias_dir/$d->{'id'}");
}
}
if (@values > 1 && $anysame) {
&error(&text('alias_ecatchall2', $v));
}
if (@values > 1 && $anybounce) {
&error(&text('alias_ebounce'));
}
return @values;
}
# set_pass_change(&user)
# Set fields indicating that the password has just been changed
sub set_pass_change
{
&require_useradmin();
local $pft = &useradmin::passfiles_type();
if ($pft == 2 || $pft == 5 || $config{'ldap'}) {
$_[0]->{'change'} = int(time() / (60*60*24));
}
elsif ($pft == 4) {
$_[0]->{'change'} = time();
}
}
# set_pass_disable(&user, disable)
sub set_pass_disable
{
local ($user, $disable) = @_;
if ($disable && $user->{'pass'} !~ /^\!/) {
$user->{'pass'} = "!".$user->{'pass'};
}
elsif (!$disable && $user->{'pass'} =~ /^\!/) {
$user->{'pass'} = substr($user->{'pass'}, 1);
}
}
sub check_aliasfile
{
return 0 if (!-r $_[0] && !$_[1]);
return 1;
}
# list_all_users()
# Returns all local and LDAP users, including those from Qmail
sub list_all_users
{
&require_useradmin();
local @rv;
foreach my $u (&useradmin::list_users()) {
$u->{'module'} = 'useradmin';
push(@rv, $u);
}
if ($config{'ldap'}) {
foreach my $u (&ldap_useradmin::list_users()) {
$u->{'module'} = 'ldap-useradmin';
push(@rv, $u);
}
}
return @rv;
}
# list_all_groups()
# Returns all local and LDAP groups
sub list_all_groups
{
&require_useradmin();
local @rv;
foreach my $g (&useradmin::list_groups()) {
$g->{'module'} = 'useradmin';
push(@rv, $g);
}
if ($config{'ldap'}) {
foreach my $g (&ldap_useradmin::list_groups()) {
$g->{'module'} = 'ldap-useradmin';
push(@rv, $g);
}
}
return @rv;
}
# build_taken(&uid-taken, &username-taken, [&users])
# Fills in the the given hashes with used usernames and UIDs
sub build_taken
{
my ($uidmap, $usermap, $users) = @_;
&obtain_lock_unix();
&require_useradmin();
# Add Unix users
local @users = $users ? @$users : &list_all_users();
local $u;
foreach $u (@users) {
$uidmap->{$u->{'uid'}} ||= 'user' if ($uidmap);
$usermap->{$u->{'user'}} ||= 'user' if ($usermap);
}
# Add system users
setpwent();
while(my @uinfo = getpwent()) {
$uidmap->{$uinfo[2]} ||= 'user' if ($uidmap);
$usermap->{$uinfo[0]} ||= 'user' if ($usermap);
}
endpwent();
# Add domain users
local $d;
foreach $d (&list_domains()) {
$uidmap->{$d->{'uid'}} ||= 'dom' if ($uidmap);
$usermap->{$d->{'user'}} ||= 'dom' if ($usermap);
}
# Add UIDs used in the past
my %uids;
&read_file_cached($old_uids_file, \%uids);
foreach my $uid (keys %uids) {
$uidmap->{$uid} ||= 'old' if ($uidmap);
}
&release_lock_unix();
}
# build_group_taken(&gid-taken, &groupname-taken, [&groups])
# Fills in the the given hashes with used group names and GIDs
sub build_group_taken
{
my ($gidmap, $groupmap, $groups) = @_;
&obtain_lock_unix();
&require_useradmin();
# Add Unix groups
local @groups = $groups ? @$groups : &list_all_groups();
local $g;
foreach $g (@groups) {
$gidmap->{$g->{'gid'}} ||= 'group' if ($gidmap);
$groupmap->{$g->{'group'}} ||= 'group' if ($groupmap);
}
# Add system groups
setgrent();
while(my @ginfo = getgrent()) {
$gidmap->{$ginfo[2]} ||= 'group' if ($gidmap);
$groupmap->{$ginfo[0]} ||= 'group' if ($groupmap);
}
endgrent();
# Add domains
local $d;
foreach $d (&list_domains()) {
$gidmap->{$d->{'gid'}} ||= 'dom' if ($gidmap);
$groupmap->{$d->{'group'}} ||= 'dom' if ($groupmap);
}
# Add GIDs used in the past
my %gids;
&read_file_cached($old_gids_file, \%gids);
foreach my $gid (keys %gids) {
$gidmap->{$gid} ||= 'old' if ($gidmap);
}
&release_lock_unix();
}
# allocate_uid(&uid-taken)
# Given a hash of used UIDs, return one that is free
sub allocate_uid
{
local $uid;
if ($usermodule eq "ldap-useradmin") {
$uid = $luconfig{'base_uid'};
}
$uid ||= $uconfig{'base_uid'};
while($_[0]->{$uid}) {
$uid++;
}
return $uid;
}
# allocate_gid(&gid-taken)
# Given a hash of used GIDs, return one that is free
sub allocate_gid
{
local $gid;
if ($usermodule eq "ldap-useradmin") {
$gid = $luconfig{'base_gid'};
}
$gid ||= $uconfig{'base_gid'};
while($_[0]->{$gid}) {
$gid++;
}
return $gid;
}
# server_home_directory(&domain, [&parentdomain])
# Returns the home directory for a new virtual server user
sub server_home_directory
{
&require_useradmin();
if ($_[0]->{'parent'}) {
# Owned by some existing user, so under his home
local $dname = $_[0]->{'dom'};
$dname =~ s/^xn(-+)//;
return "$_[1]->{'home'}/domains/$dname";
}
elsif ($config{'home_format'}) {
# Use the template from the module config
local $home = "$home_base/$config{'home_format'}";
return &substitute_domain_template($home, $_[0]);
}
else {
# Just use the Users and Groups module settings
return &useradmin::auto_home_dir($home_base, $_[0]->{'user'},
$_[0]->{'ugroup'});
}
}
# set_quota(user, filesystem, quota, hard)
# Set hard or soft quotas for one user
sub set_quota
{
&require_useradmin();
if ($_[3]) {
"a::edit_user_quota($_[0], $_[1],
int($_[2]), int($_[2]), 0, 0);
}
else {
"a::edit_user_quota($_[0], $_[1],
int($_[2]), 0, 0, 0);
}
}
# set_server_quotas(&domain, [user-quota, group-quota])
# Set the user and possibly group quotas for a domain
sub set_server_quotas
{
my ($d, $uquota, $quota) = @_;
$uquota = $d->{'uquota'} if (!defined($uquota));
$quota = $d->{'quota'} if (!defined($quota));
local $tmpl = &get_template($d->{'template'});
if (&has_quota_commands()) {
# User and group quotas are set externally
&run_quota_command("set_user", $d->{'user'},
$tmpl->{'quotatype'} eq 'hard' ? ( int($uquota), int($uquota) )
: ( 0, int($uquota) ));
if (&has_group_quotas() && $d->{'group'}) {
&run_quota_command("set_group", $d->{'group'},
$tmpl->{'quotatype'} eq 'hard' ?
( int($quota), int($quota) ) : ( 0, $quota ));
}
}
else {
if (&has_home_quotas()) {
# Set Unix user quota for home
&set_quota($d->{'user'}, $config{'home_quotas'},
$uquota, $tmpl->{'quotatype'} eq 'hard');
}
if (&has_mail_quotas()) {
# Set Unix user quota for mail
&set_quota($d->{'user'}, $config{'mail_quotas'},
$uquota, $tmpl->{'quotatype'} eq 'hard');
}
if (&has_group_quotas() && $d->{'group'}) {
# Set group quotas for home and possibly mail
&require_useradmin();
local @qargs;
if ($tmpl->{'quotatype'} eq 'hard') {
@qargs = ( int($quota), int($quota), 0, 0 );
}
else {
@qargs = ( int($quota), 0, 0, 0 );
}
"a::edit_group_quota(
$d->{'group'}, $config{'home_quotas'}, @qargs);
if (&has_mail_quotas()) {
"a::edit_group_quota(
$d->{'group'}, $config{'mail_quotas'}, @qargs);
}
}
}
}
# disable_quotas(&domain)
# Temporarily disable quotas for some virtual server, so that file or DB
# operations don't fail
sub disable_quotas
{
local ($d) = @_;
return if (!&has_home_quotas());
if ($d->{'parent'}) {
local $pd = &get_domain($d->{'parent'});
&disable_quotas($pd);
}
elsif ($d->{'unix'} && $d->{'quota'}) {
local $nqd = { %$d };
$nqd->{'quota'} = 0;
$nqd->{'uquota'} = 0;
&set_server_quotas($nqd);
if (!@disable_quotas_users) {
@disable_quotas_users = &list_domain_users($d, 1, 1, 0, 1);
foreach my $u (@disable_quotas_users) {
next if ($u->{'noquota'});
&set_user_quotas($u->{'user'}, 0, 0, $d);
}
}
}
}
# enable_quotas(&domain)
# Must be called after disable_quotas to re-activate quotas for some domain
sub enable_quotas
{
local ($d) = @_;
return if (!&has_home_quotas());
if ($d->{'parent'}) {
local $pd = &get_domain($d->{'parent'});
&enable_quotas($pd);
}
elsif ($d->{'unix'} && $d->{'quota'}) {
&set_server_quotas($d);
if (@disable_quotas_users) {
foreach my $u (@disable_quotas_users) {
next if ($u->{'noquota'});
&set_user_quotas($u->{'user'}, $u->{'quota'},
$u->{'mquota'}, $d);
}
@disable_quotas_users = ( );
}
}
}
# users_table(&users, &dom, cgi, &buttons, &links, empty-msg)
# Output a table of mailbox users
sub users_table
{
local ($users, $d, $cgi, $buttons, $links, $empty) = @_;
local $can_quotas = &has_home_quotas() || &has_mail_quotas();
local @ashells = &list_available_shells($d);
# Given a list of services return
# user-friendly login access label
my $login_access_label = sub {
my @s = @_;
@s = map { $text{'users_login_access_'.$_} } @s;
@s = grep { $_ } @s;
my $n = scalar(@s);
if ($n == 0) {
return '';
}
elsif ($n == 1) {
return "$s[0] $text{'users_login_access__only'}";
}
else {
my $l = pop(@s);
return join(', ', @s) . " $text{'users_login_access__and'} $l";
}
};
# Work out table header
local @headers;
push(@headers, "") if ($cgi);
push(@headers, $text{'users_name'},
$text{'user_user2'},
$text{'users_real'} );
if ($can_quotas) {
push(@headers, $text{'users_quota'}, $text{'users_uquota'});
}
if ($config{'show_mailsize'} && $d->{'mail'}) {
push(@headers, $text{'users_size'});
}
if ($config{'show_lastlogin'} && $d->{'mail'}) {
push(@headers, $text{'users_ll'});
}
push(@headers, $text{'users_ushell'});
# Database column
if (($d->{'mysql'} || $d->{'postgres'}) && $config{'show_dbs'}) {
push(@headers, $text{'users_db'});
}
# Other plugins columns
local ($f, %plugcol);
if ($config{'show_plugins'}) {
foreach $f (&list_mail_plugins()) {
local $col = &plugin_call($f, "mailbox_header", $d);
if ($col) {
$plugcol{$f} = $col;
push(@headers, $col);
}
}
}
# Build table contents
local $u;
local $did = $d ? $d->{'id'} : 0;
local @table;
local $userdesc;
my @domsdbs = &domain_databases($d);
foreach $u (sort { $b->{'domainowner'} <=> $a->{'domainowner'} ||
$a->{'user'} cmp $b->{'user'} } @$users) {
local $pop3 = $d ? &remove_userdom($u->{'user'}, $d) : $u->{'user'};
local $pop3_dis =
&ui_text_color($pop3.&vui_inline_label('users_disabled_label', undef, 'disabled'), 'danger');
$pop3 = &html_escape($pop3);
local @cols;
local $filetype = $u->{'extra'} ? "&type=".&urlize($u->{'type'}) : "";
my $col_text =
($u->{'domainowner'} ?
"$pop3".&vui_inline_label('users_owner_label') :
$u->{'webowner'} && $u->{'pass'} =~ /^(\!|\*)/ ? $pop3_dis :
$u->{'webowner'} ? $pop3 :
$u->{'pass'} =~ /^(\!|\*)/ ? $pop3_dis : $pop3);
my $col_val = "$col_text";
if (!$virtualmin_pro && $u->{'extra'}) {
$col_val = $col_text;
}
push(@cols, "$col_val\n");
push(@cols, &html_escape($u->{'user'}));
push(@cols, &html_escape($u->{'real'}));
$userdesc++ if ($u->{'real'});
# Add columns for quotas
local $quota;
$quota += $u->{'quota'} if (&has_home_quotas());
$quota += $u->{'mquota'} if (&has_mail_quotas());
local $uquota;
$uquota += $u->{'uquota'} if (&has_home_quotas());
$uquota += $u->{'muquota'} if (&has_mail_quotas());
if (($u->{'webowner'} || $u->{'extra'}) && defined($quota)) {
# Website owners, virtual database and web users have no
# real quota
push(@cols, $u->{'type'} eq 'web' || $u->{'type'} eq 'db' ?
$text{'users_na'} : $text{'users_same'}, "");
}
elsif (defined($quota)) {
# Has Unix quotas
push(@cols, $quota ? "a_show($quota, "home")
: $text{'form_unlimit'});
my $color = $u->{'over_quota'} ? "#ff0000" :
$u->{'warn_quota'} ? "#df7d0e" :
$u->{'spam_quota'} ? "#aaaaaa" : undef;
if ($color) {
push(@cols, "".
"a_show($uquota, "home")."");
}
else {
push(@cols, "a_show($uquota, "home"));
}
}
if ($config{'show_mailsize'} && $d->{'mail'}) {
# Mailbox link, if this user has email enabled or is the owner
local ($szmsg, $sz, $szb);
if (!$u->{'nomailfile'} &&
($u->{'email'} || @{$u->{'extraemail'}})) {
($sz) = &mail_file_size($u);
$szb = $sz;
$sz = $sz ? &nice_size($sz) : $text{'users_empty'};
local $lnk = &read_mail_link($u, $d);
if ($lnk) {
$szmsg = &ui_link($lnk, $sz);
}
else {
$szmsg = $sz;
}
}
else {
$szmsg = $text{'users_noemail'};
$sz = 0;
$szb = 0;
}
push(@cols, { 'type' => 'string',
'td' => 'data-sort='.$szb,
'value' => $szmsg });
}
if ($config{'show_lastlogin'} && $d->{'mail'}) {
# Last mail login
my $ll = &get_last_login_time($u->{'user'});
my $llbest;
foreach $k (sort { $a cmp $b } keys %$ll) {
$llbest = $ll->{$k} if ($ll->{$k} > $llbest);
}
push(@cols, $llbest ? &human_time_delta($llbest)
: $text{'users_ll_never'});
}
# Show shell access level
if ($u->{'domainowner'}) {
$u->{'shell'} = &get_domain_shell($d, $u);
}
local ($shell) = grep { $_->{'shell'} eq &get_user_shell($u) } @ashells;
my $udbs = scalar(@{$u->{'dbs'}}) || $u->{'domainowner'};
push(@cols, ($u->{'extra'} && $u->{'type'} eq 'db') ? &$login_access_label('db') :
($u->{'extra'} && $u->{'type'} eq 'web') ? &$login_access_label('web') :
!$u->{'shell'} ? &$login_access_label($udbs ? 'db' : undef, 'mail') :
!$shell ? &text('users_shell', "$u->{'shell'}") :
$shell->{'id'} eq 'ftp' && !$u->{'email'} ?
&$login_access_label($udbs ? 'db' : undef, 'ftp') :
(&$login_access_label(
($u->{'extra'} && $u->{'type'} eq 'web') ? 'web' :
$udbs ? 'db' : undef,
$shell->{'id'} eq 'nologin' ? ($u->{'email'} ? 'mail' : undef) :
$shell->{'id'} eq 'ftp' ? ($u->{'email'} ? 'mail' : undef, 'ftp') :
$shell->{'id'} eq 'scp' ? ($u->{'email'} ? 'mail' : undef, 'scp') :
$shell->{'id'} eq 'ssh' ? ($u->{'email'} ? 'mail' : undef, 'ftp', 'ssh') : undef
) || ($shell->{'id'} eq 'nologin' ?
(!$u->{'email'} ? $text{'users_login_access_none'} :
$shell->{'desc'}) : $shell->{'desc'})));
# Show number of DBs
if (($d->{'mysql'} || $d->{'postgres'}) && $config{'show_dbs'}) {
my $userdbscnt = scalar(@{$u->{'dbs'}});
push(@cols, $u->{'domainowner'} ? $text{'users_all'} :
$userdbscnt ? scalar(@domsdbs) == $userdbscnt
? $text{'users_all'} : $userdbscnt
: $text{'no'});
}
# Show columns from plugins
if ($config{'show_plugins'}) {
foreach $f (grep { $plugcol{$_} } &list_mail_plugins()) {
push(@cols, &plugin_call($f, "mailbox_column", $u, $d));
}
}
# Insert checkbox, if needed
if ($cgi) {
unshift(@cols, { 'type' => 'checkbox',
'name' => 'd',
'value' => $u->{'user'},
'disabled' => $u->{'domainowner'} });
}
push(@table, \@cols);
}
# Drop "Real name" column if no column has any data
if (!$userdesc) {
my $colnum = $cgi ? 3 : 2;
map { splice(@$_, $colnum, 1) } @table;
splice(@headers, $colnum, 1);
}
# Generate the table, perhaps with a form
if ($cgi) {
print &ui_form_columns_table($cgi, $buttons, 1, $links,
$d ? [ [ "dom", $d->{'id'} ] ] : undef,
\@headers,
100, \@table, undef, 0, undef, $empty);
}
else {
print &ui_columns_table(\@headers, 100, \@table, undef, 0, undef,
$empty);
}
}
# quota_bsize(filesystem|"home"|"mail", [for-filesys])
sub quota_bsize
{
if (&has_quota_commands()) {
# When using quota commands, the block size is always 1024
return 1024;
}
local $fs = $_[0] eq "home" ? $config{'home_quotas'} :
$_[0] eq "mail" ? $config{'mail_quotas'} : $_[0];
local $forfs = int($_[1]);
if ($gconfig{'os_type'} =~ /-linux$/) {
# On linux, the quota block size is ALWAYS 1024, so we can shortcut
# any actual filesystem tests
return $forfs ? 512 : 1024;
}
&require_useradmin();
if (defined("a::block_size)) {
local $bsize;
if (!exists($bsize_cache{$fs,$forfs})) {
$bsize_cache{$fs,$forfs} = "a::block_size($fs, $forfs);
}
return $bsize_cache{$fs,$forfs};
}
return undef;
}
# quota_show(number, filesystem|"home"|"mail", [zero-means-none])
# Returns text for the quota on some filesystem, in a human-readable format
sub quota_show
{
my ($n, $fs, $zeromode) = @_;
if (!$n) {
return $zeromode == 1 ? $text{'resel_none'} :
$zeromode == 0 ? $text{'resel_unlimit'} : 0;
}
else {
local $bsize = "a_bsize($fs);
if ($bsize) {
return &nice_size($n*$bsize);
}
return $n." ".$text{'form_b'};
}
}
# quota_input(name, number, filesystem|"home"|"mail", [disabled])
# Returns HTML for an input for entering a quota, doing block->kb conversion
sub quota_input
{
my ($name, $value, $fs, $dis) = @_;
my $bsize = "a_bsize($fs);
if ($bsize) {
return &ui_bytesbox($name, $value*$bsize, 8, $dis, undef, 1024*1024);
}
else {
# Just show blocks input
return &ui_textbox($name, $value, 10, $dis)." ".$text{'form_b'};
}
}
# opt_quota_input(name, value, filesystem|"home"|"mail"|"none",
# [third-option], [set-label])
# Returns HTML for a field for selecting a quota or unlimited
sub opt_quota_input
{
local ($name, $value, $fs, $third, $label) = @_;
local $dis1 = &js_disable_inputs([ $name, $name."_units" ], [ ]);
local $dis2 = &js_disable_inputs([ ], [ $name, $name."_units" ]);
local $mode = $value eq "" ? 1 : $value eq "0" ? 1 : $value eq "none" ? 2 : 0;
local $qi = $fs eq "none" ? &ui_textbox($name, $mode ? "" : $value, 10)
: "a_input($name, $mode ? "" : $value, $fs,$mode);
return &ui_radio($name."_def", $mode,
[ $third ? ([ 2, $third, "onClick='$dis1'" ]) : ( ),
[ 1, $text{'form_unlimit'}, "onClick='$dis1'" ],
[ 0, $label." ".$qi, "onClick='$dis2'" ] ]);
}
# quota_parse(name, filesystem|"home"|"mail")
# Converts an entered quota into blocks
sub quota_parse
{
local $bsize = "a_bsize($_[1]);
if (!$bsize) {
return $in{$_[0]};
}
else {
return int($in{$_[0]}*$in{$_[0]."_units"}/$bsize);
}
}
# feature_check_chained_javascript(feature)
# Return inline JavaScript code to chain disable/enable dependent features
sub feature_check_chained_javascript
{
my ($f) = @_;
my $chained = {
'mail' => ['spam', 'virus'],
'web' => ['ssl', 'status', 'webalizer', 'virtualmin-awstats'],
'virtualmin-nginx' => ['virtualmin-nginx-ssl', 'status', 'webalizer', 'virtualmin-awstats'],
};
my $cfeature = $chained->{$f};
if ($cfeature) {
my $deps = join(', ', map { "form['$_'] && (form['$_'].checked = false)" }
@{$cfeature});
return "oninput=\"if (form['$f'] && !form['$f'].checked) { $deps }\"";
}
for my $c (keys %$chained) {
next if (!$config{$c} && &indexof($c, @plugins) < 0);
return "oninput=\"if (form['$f'] && form['$f'].checked) ".
"{ form['$c'] && (form['$c'].checked = true) }\""
if (grep { $_ eq $f } @{$chained->{$c}});
}
return undef;
}
# quota_javascript(name, value, filesystem|"bw"|"none", unlimited-possible)
# Returns Javascript to set some quota field using Javascript
sub quota_javascript
{
local ($name, $value, $fs, $unlimited) = @_;
local $bsize = $fs eq "none" ? 0 : $fs eq "bw" ? 1 : "a_bsize($fs);
local $rv;
if ($bsize) {
# Set value and units
local $val = $value eq "" ? "" : $value*$bsize;
local $index;
if ($val >= 1024*1024*1024) {
$val = $val/(1024*1024*1024);
$index = 3;
}
elsif ($val >= 1024*1024) {
$val = $val/(1024*1024);
$index = 2;
}
elsif ($val >= 1024) {
$val = $val/(1024);
$index = 1;
}
else {
$index = 0;
}
$val = sprintf("%.2f", $val) if ($val);
$val =~ s/\.00$//;
$rv .= " !!domain_form_target[0].${name} && (domain_form_target[0].${name}.value = \"$val\");\n";
$rv .= " !!domain_form_target[0].${name}_units && (domain_form_target[0].${name}_units.selectedIndex = $index);\n";
}
else {
# Just set blocks value
$rv .= " !!domain_form_target[0].${name} && (domain_form_target[0].${name}.value = \"$value\");\n";
}
if ($unlimited) {
if ($value eq "") {
$rv .= " !!domain_form_target[0].${name}_def && (domain_form_target[0].${name}_def[0].checked = true);\n";
$rv .= " !!domain_form_target[0].${name} && (domain_form_target[0].${name}.disabled = true);\n";
$rv .= " if (domain_form_target[0].${name}_units) {\n";
$rv .= " domain_form_target[0].${name}_units.disabled = true;\n";
$rv .= " }\n";
}
else {
$rv .= " !!domain_form_target[0].${name}_def && (domain_form_target[0].${name}_def[1].checked = true);\n";
$rv .= " !!domain_form_target[0].${name} && (domain_form_target[0].${name}.disabled = false);\n";
$rv .= " if (domain_form_target[0].${name}_units) {\n";
$rv .= " domain_form_target[0].${name}_units.disabled = false;\n";
$rv .= " }\n";
}
}
return $rv;
}
# quota_field(name, value, used, files-used, filesystem, &user)
sub quota_field
{
my ($name, $value, $used, $fused, $fs, $u) = @_;
my $rv;
my $color = $u->{'over_quota'} ? "#ff0000" :
$u->{'warn_quota'} ? "#df7d0e" :
$u->{'spam_quota'} ? "#aaaaaa" : undef;
if (&can_mailbox_quota()) {
# Show inputs for editing quotas
local $quota = $_[1];
$quota = undef if ($quota eq "none");
$rv .= &opt_quota_input($_[0], $quota, $_[3]);
$rv .= "\n";
}
else {
# Just show current settings, or default
$rv .= ($defmquota[0] ? "a_show($defmquota[0], $_[3]) : $text{'form_unlimit'})."\n";
}
return $rv;
}
# backup_virtualmin(&domain, file)
# Adds a domain's configuration file to the backup
sub backup_virtualmin
{
my ($d, $file) = @_;
&$first_print($text{'backup_virtualmincp'});
# Record parent's domain name, which can be used when restoring
if ($d->{'parent'}) {
local $parent = &get_domain($d->{'parent'});
$d->{'backup_parent_dom'} = $parent->{'dom'};
if ($d->{'alias'}) {
local $alias = &get_domain($d->{'alias'});
$d->{'backup_alias_dom'} = $alias->{'dom'};
}
if ($d->{'subdom'}) {
local $subdom = &get_domain($d->{'subdom'});
$d->{'backup_subdom_dom'} = $subdom->{'dom'};
}
}
# Record sub-directory for mail folders, used during mail restores
local %mconfig = &foreign_config("mailboxes");
if ($mconfig{'mail_usermin'}) {
$d->{'backup_mail_folders'} = $mconfig{'mail_usermin'};
}
else {
delete($d->{'backup_mail_folders'});
}
# Record encrypted Unix password
delete($d->{'backup_encpass'});
if ($d->{'unix'} && !$d->{'parent'} && !$d->{'disabled'}) {
local @users = &list_all_users();
local ($user) = grep { $_->{'user'} eq $d->{'user'} } @users;
if ($user) {
$d->{'backup_encpass'} = $user->{'pass'};
}
}
# Record if default for the IP
if (&domain_has_website($d)) {
$d->{'backup_web_default'} = &is_default_website($d);
}
else {
delete($d->{'backup_web_default'});
}
# Record webserver type
$d->{'backup_web_type'} = &domain_has_website($d);
$d->{'backup_ssl_type'} = &domain_has_ssl($d);
&lock_domain($d);
&save_domain($d);
&unlock_domain($d);
# Save the domain's data file
©_source_dest($d->{'file'}, $file);
if (-r "$initial_users_dir/$d->{'id'}") {
# Initial user settings
©_source_dest("$initial_users_dir/$d->{'id'}", $file."_initial");
}
if (-d "$extra_admins_dir/$d->{'id'}") {
# Extra admin details
&execute_command(
"cd ".quotemeta("$extra_admins_dir/$d->{'id'}").
" && ".&make_tar_command("cf", $file."_admins", "."));
}
if (-d "$extra_users_dir/$d->{'id'}") {
# Extra users details
&execute_command(
"cd ".quotemeta("$extra_users_dir/$d->{'id'}").
" && ".&make_tar_command("cf", $file."_users", "."));
}
if ($config{'bw_active'}) {
# Bandwidth logs
if (-r "$bandwidth_dir/$d->{'id'}") {
©_source_dest("$bandwidth_dir/$d->{'id'}", $file."_bw");
}
else {
# Create an empty file to indicate that we have no data
&open_tempfile(EMPTY, ">".$file."_bw");
&close_tempfile(EMPTY);
}
}
# Script logs
if (-d "$script_log_directory/$d->{'id'}") {
&execute_command(
"cd ".quotemeta("$script_log_directory/$d->{'id'}").
" && ".&make_tar_command("cf", $file."_scripts", "."));
}
else {
# Create an empty file to indicate that we have no scripts
&open_tempfile(EMPTY, ">".$file."_scripts");
&close_tempfile(EMPTY);
}
# Include template, in case the restore target doesn't have it
local ($tmpl) = grep { $_->{'id'} == $d->{'template'} } &list_templates();
if (!$tmpl->{'standard'}) {
©_source_dest($tmpl->{'file'}, $file."_template");
}
# Include plan too
local $plan = &get_plan($d->{'plan'});
if ($plan) {
©_source_dest($plan->{'file'}, $file."_plan");
}
# Save deleted aliases file
©_source_dest("$saved_aliases_dir/$d->{'id'}",
$file."_saved_aliases");
# Save SSL cert and key, in case Apache isn't enabled
foreach my $ssltype ('cert', 'key', 'ca') {
my $sslfile = &get_website_ssl_file($d, $ssltype);
if ($sslfile && -r $sslfile) {
©_write_as_domain_user($d, $sslfile,
$file."_ssl_".$ssltype);
}
}
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_backup_config(file, &vbs)
# Save the current module config to the specified file
sub virtualmin_backup_config
{
local ($file, $vbs) = @_;
&$first_print($text{'backup_vconfig_doing'});
©_source_dest($module_config_file, $file);
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_restore_config(file, &vbs)
# Replace the current config with the given file, *except* for the default
# template settings
sub virtualmin_restore_config
{
local ($file, $vbs) = @_;
&$first_print($text{'restore_vconfig_doing'});
local %oldconfig = %config;
local @tmpls = &list_templates();
©_source_dest($file, $module_config_file);
&read_file($module_config_file, \%config);
foreach my $t (@tmpls) {
if ($t->{'standard'}) {
&save_template($t);
}
}
# Put back site-specific settings, as those in the backup are unlikely to
# be correct.
$config{'iface'} = $oldconfig{'iface'};
$config{'defip'} = $oldconfig{'defip'};
$config{'sharedips'} = $oldconfig{'sharedips'};
$config{'sharedip6s'} = $oldconfig{'sharedip6s'};
$config{'home_quotas'} = $oldconfig{'home_quotas'};
$config{'mail_quotas'} = $oldconfig{'mail_quotas'};
$config{'group_quotas'} = $oldconfig{'group_quotas'};
$config{'old_defip'} = $oldconfig{'old_defip'};
$config{'old_defip6'} = $oldconfig{'old_defip6'};
$config{'last_check'} = $oldconfig{'last_check'};
# Remove plugins that aren't on the new system
&generate_plugins_list($config{'plugins'});
$config{'plugins'} = join(' ', @plugins);
&lock_file($module_config_file);
&save_module_config();
&unlock_file($module_config_file);
&$second_print($text{'setup_done'});
# Apply any new config settings
&run_post_config_actions(\%oldconfig);
return 1;
}
# virtualmin_backup_templates(file, &vbs)
# Write a tar file of all templates (including scripts) to the given file
sub virtualmin_backup_templates
{
local ($file, $vbs) = @_;
&$first_print($text{'backup_vtemplates_doing'});
local $temp = &transname();
mkdir($temp, 0700);
foreach my $tmpl (&list_templates()) {
my %tmplquoted = %$tmpl;
foreach my $k (keys %tmplquoted) {
$tmplquoted{$k} =~ s/\n/\\n/g;
}
&write_file("$temp/$tmpl->{'id'}", \%tmplquoted);
}
# Save template scripts
&execute_command("cp $template_scripts_dir/* $temp");
&execute_command("cd ".quotemeta($temp)." && ".
&make_tar_command("cf", $file, "."));
&unlink_file($temp);
# Save global variables file
if (-r $global_template_variables_file) {
©_source_dest($global_template_variables_file, $file."_global");
}
else {
# Create empty, as an indicator that it exists
&open_tempfile(GLOBAL, ">".$file."_global", 0, 1);
&close_tempfile(GLOBAL);
}
# Save skeleton directories for all templates
local %done;
foreach my $tmpl (&list_templates()) {
if ($tmpl->{'skel'} && $tmpl->{'skel'} ne 'none' &&
!$done{$tmpl->{'skel'}}++ &&
-d $tmpl->{'skel'}) {
local $skelfile = $file.'_skel_'.$tmpl->{'id'};
&execute_command(
"cd ".quotemeta($tmpl->{'skel'}).
" && ".&make_tar_command("cf", $skelfile, "."));
}
}
# Save plans
&make_dir($plans_dir, 0700);
&execute_command(
"cd ".quotemeta($plans_dir).
" && ".&make_tar_command("cf", $file."_plans", "."));
# Save ACME providers
if (defined(&list_acme_providers)) {
&make_dir($acme_providers_dir, 0700);
&execute_command(
"cd ".quotemeta($acme_providers_dir).
" && ".&make_tar_command("cf", $file."_acmes", "."));
}
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_restore_templates(file, &vbs)
# Extract all templates from a backup. Those that already exist are not deleted.
sub virtualmin_restore_templates
{
local ($file, $vbs) = @_;
&$first_print($text{'restore_vtemplates_doing'});
# Extract backup file
local $temp = &transname();
mkdir($temp, 0700);
&execute_command("cd ".quotemeta($temp)." && ".
&make_tar_command("xf", $file));
# Copy templates from backup across
opendir(DIR, $temp);
foreach my $t (readdir(DIR)) {
next if ($t eq "." || $t eq "..");
if ($t =~ /^(\d+)_(\d+)$/) {
# A script file
if (!-d $template_scripts_dir) {
&make_dir($template_scripts_dir, 0755);
}
©_source_dest("$temp/$t", "$template_scripts_dir/$t");
}
else {
# A template file
local %tmpl;
&read_file("$temp/$t", \%tmpl);
foreach my $k (keys %tmpl) {
$tmpl{$k} =~ s/\\n/\n/g;
}
&save_template(\%tmpl);
}
}
closedir(DIR);
&execute_command("rm -rf ".quotemeta($temp));
# Fix up bad Apache directives in templates
foreach my $tmpl (&list_templates()) {
&fix_options_template($tmpl);
}
# Restore global variables
if (-r $file."_global") {
©_source_dest($file."_global", $global_template_variables_file);
}
# Restore skeleton directories
local %done;
foreach my $tmpl (&list_templates()) {
if ($tmpl->{'skel'} && $tmpl->{'skel'} ne 'none' &&
!$done{$tmpl->{'skel'}}++) {
local $skelfile = $file.'_skel_'.$tmpl->{'id'};
if (-r $skelfile) {
# Delete and re-create skel directory
&unlink_file($tmpl->{'skel'});
&make_dir($tmpl->{'skel'}, 0755);
&execute_command(
"cd ".quotemeta($tmpl->{'skel'}).
" && ".&make_tar_command("xf",
quotemeta($skelfile)));
}
}
}
# Restore plans, if included
if (-r $file."_plans") {
&make_dir($plans_dir, 0700);
&execute_command(
"cd ".quotemeta($plans_dir)." && ".
&make_tar_command("xf", $file."_plans"));
}
# Restore ACME providers if included
if (-r $file."_acmes" && defined(&list_acme_providers)) {
&make_dir($acme_providers_dir, 0700);
&execute_command(
"cd ".quotemeta($acme_providers_dir)." && ".
&make_tar_command("xf", $file."_acmes"));
}
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_backup_scheds(file, &vbs)
# Create a tar file of all scheduled backups and keys
sub virtualmin_backup_scheds
{
local ($file, $vbs) = @_;
local $dokeys = defined(&list_backup_keys) && scalar(&list_backup_keys());
&$first_print($dokeys ? $text{'backup_vscheds_doing2'}
: $text{'backup_vscheds_doing'});
# Tar up scheduled backups dir
local $temp = &transname();
mkdir($temp, 0700);
foreach my $sched (&list_scheduled_backups()) {
&write_file("$temp/$sched->{'id'}", $sched);
}
&execute_command("cd ".quotemeta($temp)." && ".
&make_tar_command("cf", $file, "."));
&unlink_file($temp);
# Also tar up keys dir
if ($dokeys) {
&execute_command("cd ".quotemeta($backup_keys_dir)." && ".
&make_tar_command("cf", $file."_keys", "."));
}
# Also tar up S3 accounts dir
if (-d $s3_accounts_dir) {
&execute_command("cd ".quotemeta($s3_accounts_dir)." && ".
&make_tar_command("cf", $file."_s3accounts", "."));
}
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_restore_scheds(file, vbs)
# Re-create all scheduled backups
sub virtualmin_restore_scheds
{
local ($file, $vbs) = @_;
local $dokeys = -r $file."_keys" && defined(&list_backup_keys);
&$first_print($dokeys ? $text{'restore_vscheds_doing'}
: $text{'restore_vscheds_doing2'});
# Extract backup file
local $temp = &transname();
mkdir($temp, 0700);
&execute_command("cd ".quotemeta($temp)." && ".
&make_tar_command("xf", $file));
# Delete all current non-default schedules
foreach my $sched (&list_scheduled_backups()) {
if ($sched->{'id'} != 1) {
&delete_scheduled_backup($sched);
}
}
# Re-create the restored ones
opendir(BACKUPDIR, $temp);
foreach my $t (readdir(BACKUPDIR)) {
next if ($t eq "." || $t eq "..");
local %sched;
&read_file("$temp/$t", \%sched);
delete($sched{'file'});
&save_scheduled_backup(\%sched);
}
closedir(BACKUPDIR);
# Extract dir of backup keys, then re-save each to re-import to root
if ($dokeys) {
local %oldkeys = map { $_->{'id'}, 1 } &list_backup_keys();
&make_dir($backup_keys_dir, 0700) if (!-d $backup_keys_dir);
&execute_command("cd ".quotemeta($backup_keys_dir)." && ".
&make_tar_command("xf", $file."_keys"));
foreach my $key (&list_backup_keys()) {
if (!$oldkey{$key->{'id'}}) {
eval {
$main::error_must_die = 1;
&save_backup_key($key, 1);
};
}
}
}
# Extract dir of S3 accounts
if (-r $file."_s3accounts") {
&unlink_file($s3_accounts_dir);
&make_dir($s3_accounts_dir, 0700);
&execute_command("cd ".quotemeta($s3_accounts_dir)." && ".
&make_tar_command("xf", $file."_s3accounts"));
}
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_backup_resellers(file, &vbs)
# Create a tar file of reseller details. For each we need to store the Webmin
# user information, plus all ACL files
sub virtualmin_backup_resellers
{
local ($file, $vbs) = @_;
return 0 if (!defined(&list_resellers));
&$first_print($text{'backup_vresellers_doing'});
local $temp = &transname();
mkdir($temp, 0700);
foreach my $resel (&list_resellers()) {
&open_tempfile(RESEL, ">$temp/$resel->{'name'}.webmin");
&print_tempfile(RESEL, &serialise_variable($resel));
&close_tempfile(RESEL);
local $acldir = "$temp/$resel->{'name'}.acls";
mkdir($acldir, 0700);
foreach my $m (@{$resel->{'modules'}}) {
local %acl = &get_module_acl($resel->{'name'}, $m, 1, 1);
&write_file("$acldir/$m", \%acl);
}
}
&execute_command("cd ".quotemeta($temp)." && ".
&make_tar_command("cf", $file, "."));
&execute_command("rm -rf ".quotemeta($temp));
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_restore_resellers(file, &vbs)
# Delete all resellers and re-create them from the backup
sub virtualmin_restore_resellers
{
local ($file, $vbs) = @_;
return undef if (!defined(&list_resellers));
&$first_print($text{'restore_vresellers_doing'});
local $temp = &transname();
mkdir($temp, 0700);
&require_acl();
&execute_command("cd ".quotemeta($temp)." && ".
&make_tar_command("xf", $file));
foreach my $resel (&list_resellers()) {
&acl::delete_user($resel->{'name'});
&delete_reseller_unix_user($resel);
}
local %miniserv;
&get_miniserv_config(\%miniserv);
if (&check_pid_file($miniserv{'pidfile'})) {
&reload_miniserv();
}
opendir(DIR, $temp);
foreach my $f (readdir(DIR)) {
if ($f =~ /^(.*)\.webmin$/) {
local $acldir = "$temp/$1";
local $ser = &read_file_contents("$temp/$f");
local $resel = &unserialise_variable($ser);
&create_reseller($resel);
opendir(ACL, $acldir);
foreach my $a (readdir(ACL)) {
next if ($a eq "." || $a eq "..");
local %acl;
&read_file("$acldir/$a", \%acl);
&save_module_acl(\%acl, $resel->{'name'}, $a);
}
closedir(ACL);
}
}
&unlink_file($temp);
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_backup_email(file, &vbs)
# Creates a tar file of all email templates
sub virtualmin_backup_email
{
local ($file, $vbs) = @_;
&$first_print($text{'backup_vemail_doing'});
&execute_command(
"cd $module_config_directory && ".
&make_tar_command("cf", $file, @all_template_files));
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_restore_email(file, &vbs)
# Extract a tar file of all email templates
sub virtualmin_restore_email
{
local ($file, $vbs) = @_;
&$first_print($text{'restore_vemail_doing'});
&execute_command("cd $module_config_directory && ".
&make_tar_command("xf", $file));
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_backup_custom(file, &vbs)
# Copies the custom fields, links and shells files
sub virtualmin_backup_custom
{
local ($file, $vbs) = @_;
&$first_print($text{'backup_vcustom_doing'});
foreach my $fm ([ $custom_fields_file, $file ],
[ $custom_links_file, $file."_links" ],
[ $custom_link_categories_file, $file."_linkcats" ],
[ $custom_shells_file, $file."_shells" ]) {
if (-r $fm->[0]) {
©_source_dest($fm->[0], $fm->[1]);
}
else {
&create_empty_file($fm->[1]);
}
}
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_restore_custom(file, &vbs)
# Restores the custom fields, links and shells files
sub virtualmin_restore_custom
{
local ($file, $vbs) = @_;
&$first_print($text{'restore_vcustom_doing'});
foreach my $fm ([ $custom_fields_file, $file ],
[ $custom_links_file, $file."_links" ],
[ $custom_link_categories_file, $file."_linkcats" ]) {
if (-r $fm->[1]) {
©_source_dest($fm->[1], $fm->[0]);
}
}
if (-s $file."_shells") {
# A non-empty shells file means that the original system defined some
# custom shells.
©_source_dest($file."_shells", $custom_shells_file);
}
elsif (-r $file."_shells") {
# An empty shells file means that the original system was using the
# default shells, so so should we
&unlink_file($custom_shells_file);
}
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_backup_scripts(file, &vbs)
# Create a tar file of the scripts directory, and of the unavailable scripts
sub virtualmin_backup_scripts
{
local ($file, $vbs) = @_;
&$first_print($text{'backup_vscripts_doing'});
&make_dir("$module_config_directory/scripts", 0755);
&execute_command("cd $module_config_directory/scripts && ".
&make_tar_command("cf", $file, "."));
©_source_dest($scripts_unavail_file, $file."_unavail");
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_restore_scripts(file, &vbs)
# Extract a tar file of all third-party scripts
sub virtualmin_restore_scripts
{
local ($file, $vbs) = @_;
&$first_print($text{'restore_vscripts_doing'});
&make_dir("$module_config_directory/scripts", 0755);
&execute_command("cd $module_config_directory/scripts && ".
&make_tar_command("xf", $file));
if (-r $file."_unavail") {
©_source_dest($file."_unavail", $scripts_unavail_file);
}
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_backup_chroot(file, &vbs)
# Create a file of FTP directory restrictions
sub virtualmin_backup_chroot
{
local ($file, $vbs) = @_;
&$first_print($text{'backup_vchroot_doing'});
local @chroots = &list_ftp_chroots();
&open_tempfile(CHROOT, ">$file");
foreach my $c (@chroots) {
&print_tempfile(CHROOT,
join(" ", map { $_."=".&urlize($c->{$_}) }
grep { $_ ne 'dr' } keys %$c),"\n");
}
&close_tempfile(CHROOT);
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_restore_chroot(file, &vbs)
# Restore all chroot'd directories from a backup file
sub virtualmin_restore_chroot
{
local ($file, $vbs) = @_;
&$first_print($text{'restore_vchroot_doing'});
&obtain_lock_ftp();
local @chroots;
open(CHROOT, "<".$file);
while() {
s/\r|\n//g;
local %c = map { my ($n, $v) = split(/=/, $_, 2);
($n, &un_urlize($v)) }
split(/\s+/, $_);
push(@chroots, \%c);
}
close(CHROOT);
&save_ftp_chroots(\@chroots);
&release_lock_ftp();
&$second_print($text{'setup_done'});
return 1;
}
# virtualmin_backup_mailserver(file, &vbs)
# Save DKIM and Postgrey settings to a file
sub virtualmin_backup_mailserver
{
local ($file, $vbs) = @_;
&require_mail();
# Save DKIM settings
&$first_print($text{'backup_vmailserver_dkim'});
local %dkim;
if (!&check_dkim()) {
# DKIM can be used .. check if enabled
$dkim{'support'} = 1;
local $conf = &get_dkim_config();
$dkim{'enabled'} = $conf->{'enabled'};
$dkim{'selector'} = $conf->{'selector'};
$dkim{'extra'} = join(" ", @{$conf->{'extra'}});
$dkim{'keyfile'} = $conf->{'keyfile'};
$dkim{'sign'} = $conf->{'sign'};
$dkim{'verify'} = $conf->{'verify'};
if ($conf->{'keyfile'} && -r $conf->{'keyfile'}) {
©_source_dest($conf->{'keyfile'}, $file."_dkimkey");
}
&$second_print($text{'setup_done'});
}
else {
$dkim{'support'} = 0;
&$second_print($text{'backup_vmailserver_none'});
}
&write_file($file."_dkim", \%dkim);
# Save Postgrey settings
&$first_print($text{'backup_vmailserver_postgrey'});
local %grey;
if (!&check_postgrey()) {
# Postgrey can be used .. check if enabled, and with what opts
$grey{'support'} = 1;
$grey{'enabled'} = &is_postgrey_enabled();
local $cfile = &get_postgrey_data_file("clients");
if ($cfile) {
©_source_dest($cfile, $file."_greyclients");
}
local $rfile = &get_postgrey_data_file("recipients");
if ($rfile) {
©_source_dest($rfile, $file."_greyrecipients");
}
&$second_print($text{'setup_done'});
}
else {
$grey{'support'} = 0;
&$second_print($text{'backup_vmailserver_none'});
}
&write_file($file."_grey", \%grey);
# Save rate limiting settings
&$first_print($text{'backup_vmailserver_ratelimit'});
local %ratelimit;
if (!&check_ratelimit()) {
# Rate limiting can be used .. check if enabled, and save config
$ratelimit{'support'} = 1;
$ratelimit{'enabled'} = &is_ratelimit_enabled();
©_source_dest(&get_ratelimit_config_file(),
$file."_ratelimitconfig");
&$second_print($text{'setup_done'});
}
else {
$ratelimit{'support'} = 0;
&$second_print($text{'backup_vmailserver_none'});
}
&write_file($file."_ratelimit", \%ratelimit);
# Save mail server type
&$first_print($text{'backup_vmailserver_doing'});
&open_tempfile(MS, ">$file", 0, 1);
&print_tempfile(MS, $mail_system,"\n");
&close_tempfile(MS);
# Save general mail server settings
if ($mail_system == 0) {
# Save main.cf and master.cf
©_source_dest($postfix::config{'postfix_config_file'},
$file."_maincf");
©_source_dest($postfix::config{'postfix_master'},
$file."_mastercf");
foreach my $o ("smtpd_tls_cert_file", "smtpd_tls_key_file",
"smtpd_tls_CAfile") {
my $v = &postfix::get_current_value($o);
if ($v) {
©_source_dest($v, $file."_".$o);
}
}
&$second_print($text{'setup_done'});
}
elsif ($mail_system == 1) {
# Save sendmail.cf and sendmail.mc
©_source_dest($sendmail::config{'sendmail_cf'},
$file."_sendmailcf");
©_source_dest($sendmail::config{'sendmail_mc'},
$file."_sendmailmc");
&$second_print($text{'setup_done'});
}
elsif ($mail_system == 2) {
# Save Qmail dir
&execute_command("cd $qmailadmin::config{'qmail_dir'} && ".
&make_tar_command("cf", $file, "."));
&$second_print($text{'setup_done'});
}
else {
&$second_print($text{'backup_vmailserver_supp'});
}
return 1;
}
# virtualmin_restore_mailserver(file, &vbs)
# Apply DKIM and Postgrey settings from the backup
sub virtualmin_restore_mailserver
{
local ($file, $vbs) = @_;
&require_mail();
# Restore DKIM config
&$first_print($text{'restore_vmailserver_dkim'});
&obtain_lock_mail();
if (!&check_dkim()) {
# DKIM supported .. see what state was in the backup
local %dkim;
&read_file($file."_dkim", \%dkim);
if (!$dkim{'support'}) {
&$second_print($text{'restore_vmailserver_none'});
}
else {
local $conf = &get_dkim_config();
if ($dkim{'enabled'}) {
# Setup on this system, same as source
&$indent_print();
$conf->{'enabled'} = $dkim{'enabled'};
$conf->{'selector'} = $dkim{'selector'};
$conf->{'extra'} = [ split(/\s+/, $dkim{'extra'}) ];
$conf->{'sign'} = $dkim{'sign'};
$conf->{'verify'} = $dkim{'verify'};
local $copiedkey = 0;
if ($conf->{'keyfile'} && -r $file."_dkimkey") {
# Can copy key now
©_source_dest($file."_dkimkey",
$conf->{'keyfile'});
$copiedkey = 1;
}
&enable_dkim($conf);
$conf = &get_dkim_config();
if ($conf->{'keyfile'} && -r $file."_dkimkey" &&
!$copiedkey) {
# Copy key file and re-enable DKIM
©_source_dest($file."_dkimkey",
$conf->{'keyfile'});
&enable_dkim($conf);
}
&$outdent_print();
&$second_print($text{'setup_done'});
}
elsif ($conf->{'enabled'} && !$dkim{'enabled'}) {
# Disable on this system
&$indent_print();
&disable_dkim($conf);
&$outdent_print();
&$second_print($text{'setup_done'});
}
else {
# Nothing to do
&$second_print($text{'restore_vmailserver_already'});
}
}
}
else {
&$second_print($text{'backup_vmailserver_none'});
}
# Restore Postgrey config
&$first_print($text{'restore_vmailserver_grey'});
local %grey;
&read_file($file."_grey", \%grey);
if (&check_postgrey() ne '' && &can_install_postgrey() && $grey{'enabled'}) {
# Postgrey is not installed on this system yet, but it was enabled
# on the old machine - try to install it now
&install_postgrey_package();
}
if (&check_postgrey() eq '') {
if (!$grey{'support'}) {
&$second_print($text{'restore_vmailserver_none'});
}
else {
local $enabled = &is_postgrey_enabled();
if ($grey{'enabled'}) {
# Enable on this system, and copy client files
&$indent_print();
&enable_postgrey();
local $cfile = &get_postgrey_data_file("clients");
if ($cfile && -r $file."_greyclients") {
©_source_dest($file."_greyclients",
$cfile);
}
local $rfile = &get_postgrey_data_file("recipients");
if ($rfile && -r $file."_greyrecipients") {
©_source_dest($file."_greyrecipients",
$rfile);
}
&apply_postgrey_data();
&$outdent_print();
&$second_print($text{'setup_done'});
}
elsif ($enabled && !$grey{'enabled'}) {
# Disable on this system
&$indent_print();
&disable_postgrey();
&$outdent_print();
&$second_print($text{'setup_done'});
}
else {
# Nothing to do
&$second_print($text{'restore_vmailserver_already'});
}
}
}
else {
&$second_print($text{'backup_vmailserver_none'});
}
# Restore rate limiting config
&$first_print($text{'restore_vmailserver_ratelimit'});
local %ratelimit;
&read_file($file."_ratelimit", \%ratelimit);
if (&check_ratelimit() ne '' && &can_install_ratelimit() &&
$ratelimit{'enabled'}) {
# Rate limiting is not installed on this system yet, but it was enabled
# on the old machine - try to install it now
&install_ratelimit_package();
}
if (&check_ratelimit() eq '') {
if (!$ratelimit{'support'}) {
&$second_print($text{'restore_vmailserver_none'});
}
else {
local $enabled = &is_ratelimit_enabled();
# XXX put back socket
if ($ratelimit{'enabled'}) {
# Enable on this system, and copy config file
&$indent_print();
&enable_ratelimit();
my $conf = &get_ratelimit_config();
my ($oldsocket) = grep { $_->{'name'} eq 'socket' }
@$conf;
©_source_dest($file."_ratelimitconfig",
&get_ratelimit_config_file());
my $conf = &get_ratelimit_config();
my ($socket) = grep { $_->{'name'} eq 'socket' }
@$conf;
&save_ratelimit_directive($conf, $socket, $oldsocket);
&$outdent_print();
&$second_print($text{'setup_done'});
}
elsif ($enabled && !$ratelimit{'enabled'}) {
# Disable on this system
&$indent_print();
&disable_ratelimit();
&$outdent_print();
&$second_print($text{'setup_done'});
}
else {
# Nothing to do
&$second_print($text{'restore_vmailserver_already'});
}
}
}
else {
&$second_print($text{'restore_vmailserver_missing'});
}
&release_lock_mail();
# Get mail server type from the backup
local $bms = &read_file_contents($file);
$bms =~ s/\n//g;
# Restore mail server type, if matching. This is done last because DKIM or
# greylisting might be detected as disabled if done earlier.
&$first_print($text{'restore_vmailserver_doing'});
&obtain_lock_mail();
if ($bms eq $mail_system) {
if ($mail_system == 0) {
# Restore main.cf and master.cf
&lock_file($postfix::config{'postfix_config_file'});
&lock_file($postfix::config{'postfix_master'});
©_source_dest($file."_maincf",
$postfix::config{'postfix_config_file'});
©_source_dest($file."_mastercf",
$postfix::config{'postfix_master'});
&unlock_file($postfix::config{'postfix_master'});
&unlock_file($postfix::config{'postfix_config_file'});
undef(@postfix::master_config_cache);
foreach my $o ("smtpd_tls_cert_file", "smtpd_tls_key_file",
"smtpd_tls_CAfile") {
my $v = &postfix::get_current_value($o);
if ($v) {
©_source_dest($file."_".$o, $v);
}
}
&$second_print($text{'setup_done'});
}
elsif ($mail_system == 1) {
# Restore sendmail.cf and .mc
&lock_file($sendmail::config{'sendmail_cf'});
&lock_file($sendmail::config{'sendmail_mc'});
©_source_dest($file."_sendmailcf",
$sendmail::config{'sendmail_cf'});
©_source_dest($file."_sendmailmc",
$sendmail::config{'sendmail_mc'});
&unlock_file($sendmail::config{'sendmail_mc'});
&unlock_file($sendmail::config{'sendmail_cf'});
undef(@sendmail::sendmailcf_cache);
&$second_print($text{'setup_done'});
}
elsif ($mail_system == 2) {
# Un-tar qmail dir
&execute_command("cd $qmailadmin::config{'qmail_dir'} && ".
&make_tar_command("xf", $file));
&$second_print($text{'setup_done'});
}
else {
&$second_print($text{'backup_vmailserver_supp'});
}
}
else {
&$second_print(&text('restore_vmailserver_wrong',
$text{'mail_system_'.$bms},
$text{'mail_system_'.$mail_system}));
}
&release_lock_mail();
return 1;
}
# restore_virtualmin(&domain, file, &opts, &allopts)
# Restore the settings for a domain, such as quota, password and so on. Only
# selected settings are copied from the backup, such as limits.
sub restore_virtualmin
{
my ($d, $file, $opts, $allopts) = @_;
if (!$allopts->{'fix'}) {
# Merge current and backup configs
&$first_print($text{'restore_virtualmincp'});
local %oldd;
&read_file($file, \%oldd);
$d->{'quota'} = $oldd{'quota'};
$d->{'uquota'} = $oldd{'uquota'};
$d->{'bw_limit'} = $oldd{'bw_limit'};
$d->{'pass'} = $oldd{'pass'};
$d->{'email'} = $oldd{'email'};
foreach my $l (@limit_types) {
$d->{$l} = $oldd{$l};
}
$d->{'nodbname'} = $oldd{'nodbname'};
$d->{'norename'} = $oldd{'norename'};
$d->{'forceunder'} = $oldd{'forceunder'};
$d->{'safeunder'} = $oldd{'safeunder'};
$d->{'ipfollow'} = $oldd{'ipfollow'};
foreach my $f (@opt_features, &list_feature_plugins(), "virt") {
$d->{'limit_'.$f} = $oldd{'limit_'.$f};
}
$d->{'owner'} = $oldd{'owner'};
$d->{'proxy_pass_mode'} = $oldd{'proxy_pass_mode'};
$d->{'proxy_pass'} = $oldd{'proxy_pass'};
foreach my $f (&list_custom_fields()) {
$d->{$f->{'name'}} = $oldd{$f->{'name'}};
}
# Disable any features that are not on this system, as they can't
# be restored from the backup anyway.
foreach my $f (@features) {
next if ($f eq 'dir' || $f eq 'unix'); # Always on
if ($d->{$f} && !$config{$f}) {
$d->{$f} = 0;
}
}
&lock_domain($d);
&save_domain($d);
&unlock_domain($d);
if (-r $file."_initial") {
# Also restore user defaults file
©_source_dest($file."_initial",
"$initial_users_dir/$d->{'id'}");
}
if (-r $file."_admins") {
# Also restore extra admins
if ($d->{'id'}) {
&execute_command(
"rm -rf ".quotemeta("$extra_admins_dir/$d->{'id'}"));
}
if (!-d $extra_admins_dir) {
&make_dir($extra_admins_dir, 755);
}
&make_dir("$extra_admins_dir/$d->{'id'}", 0755);
&execute_command(
"cd ".quotemeta("$extra_admins_dir/$d->{'id'}")." && ".
&make_tar_command("xf", $file."_admins", "."));
}
if (-r $file."_users") {
# Also restore extra users
if ($d->{'id'}) {
&execute_command(
"rm -rf ".quotemeta("$extra_users_dir/$d->{'id'}"));
}
if (!-d $extra_users_dir) {
&make_dir($extra_users_dir, 0700);
}
&make_dir("$extra_users_dir/$d->{'id'}", 0700);
&execute_command(
"cd ".quotemeta("$extra_users_dir/$d->{'id'}")." && ".
&make_tar_command("xf", $file."_users", "."));
}
if ($config{'bw_active'} && -r $file."_bw" &&
!-r "$bandwidth_dir/$d->{'id'}") {
# Also restore bandwidth files for the domain, but only
# if missing.
&make_dir($bandwidth_dir, 0700);
©_source_dest($file."_bw", "$bandwidth_dir/$d->{'id'}");
}
if (-r $file."_scripts") {
# Also restore script logs
&execute_command("rm -rf ".
quotemeta("$script_log_directory/$d->{'id'}"));
if (-s $file."_scripts") {
if (!-d $script_log_directory) {
&make_dir($script_log_directory, 0755);
}
&make_dir("$script_log_directory/$d->{'id'}", 0755);
&execute_command(
"cd ".quotemeta("$script_log_directory/$d->{'id'}").
" && ".
&make_tar_command("xf",
$file."_scripts", "."));
}
# Fix up home dir on scripts
if ($_[5]->{'home'} && $_[5]->{'home'} ne $d->{'home'}) {
local $olddir = $_[5]->{'home'};
local $newdir = $d->{'home'};
foreach my $sinfo (&list_domain_scripts($d)) {
$sinfo->{'opts'}->{'dir'} =~
s/^\Q$olddir\E\//$newdir\//;
&save_domain_script($d, $sinfo);
}
}
}
if (-r $file."_saved_aliases") {
# Restore saved aliases
&make_dir($saved_aliases_dir, 0700);
©_source_dest($file."_saved_aliases",
"$saved_aliases_dir/$d->{'id'}");
}
# Restore SSL cert and key unless the domain has an SSL website, in
# which case we can assume that feature has covered it
if (!&domain_has_ssl($d)) {
my $changed = 0;
&create_ssl_certificate_directories($d);
foreach my $ssltype ('cert', 'key', 'ca') {
my $sslfile = &get_website_ssl_file($d, $ssltype) ||
&default_certificate_file($d, $ssltype);
my $bfile = $file."_ssl_".$ssltype;
if (-r $bfile) {
&lock_file($sslfile);
&write_ssl_file_contents($d, $sslfile, $bfile);
&unlock_file($sslfile);
$changed++;
}
}
if ($changed) {
&refresh_ssl_cert_expiry($d);
&sync_combined_ssl_cert($d);
}
}
&$second_print($text{'setup_done'});
}
return 1;
}
# scp_copy(source, dest, password, &error, port, [as-user])
# Copies a file from some source to a destination. One or the other can be
# a server, like user@foo:/path/to/bar/
sub scp_copy
{
local ($src, $dest, $pass, $err, $port, $asuser) = @_;
if ($src =~ /\s|\(|\)/) {
my ($host, $path) = split(/:/, $src, 2);
if ($path) {
$src = $host.":".quotemeta($path);
}
}
if ($dest =~ /\s|\(|\)/) {
my ($host, $path) = split(/:/, $dest, 2);
if ($path) {
$dest = $host.":".quotemeta($path);
}
}
local $cmd = "scp -r ".($port ? "-P $port " : "").
$config{'ssh_args'}." ".
$config{'scp_args'}." ".
quotemeta($src)." ".quotemeta($dest);
&run_ssh_command($cmd, $pass, $err, $asuser);
}
# run_ssh_command(command, pass, &error, [as-user])
# Attempt to run some command that uses ssh or scp, feeding in a password.
# Returns the output, and sets the error variable ref if failed.
sub run_ssh_command
{
my ($cmd, $pass, $err, $asuser) = @_;
if ($pass =~ /^\// && -r $pass) {
# Using a key file
my ($bin, $args) = split(/\s+/, $cmd, 2);
$cmd = $bin." -i ".quotemeta($pass)." ".$args;
$pass = undef;
}
&foreign_require("proc");
if ($asuser) {
$cmd = &command_as_user($asuser, 0, $cmd);
}
my ($fh, $fpid) = &proc::pty_process_exec($cmd);
my $out;
my $nopass = 0;
while(1) {
my $rv = &wait_for($fh, "password:", "yes\\/no", ".*\n");
$out .= $wait_for_input;
if ($rv == 0) {
if (!defined($pass) || $pass eq "") {
$nopass = 1;
last;
}
syswrite($fh, "$pass\n");
}
elsif ($rv == 1) {
syswrite($fh, "yes\n");
}
elsif ($rv < 0) {
last;
}
}
close($fh);
my $got = waitpid($fpid, 0);
if ($nopass) {
$$err = "Password required but not supplied";
}
elsif ($? || $out =~ /permission\s+denied/i || $out =~ /connection\s+refused/i) {
$$err = $out;
}
return $out;
}
# free_ip_address(&template|&acl)
# Returns an IP address within the allocation range which is not currently used.
# Checks this system's configured interfaces, and does pings.
sub free_ip_address
{
local ($tmpl) = @_;
local %taken = &interface_ip_addresses();
%taken = (%taken, &domain_ip_addresses(), &cloudmin_ip_addresses());
foreach my $ip (&get_default_ip(), &list_shared_ips()) {
$taken{$ip}++;
}
local @ranges = split(/\s+/, $tmpl->{'ranges'});
foreach my $rn (@ranges) {
my ($r, $n) = split(/\//, $rn);
$r =~ /^(\d+\.\d+\.\d+)\.(\d+)\-(\d+)$/ || next;
local ($base, $s, $e) = ($1, $2, $3);
for(my $j=$s; $j<=$e; $j++) {
local $try = "$base.$j";
if (!$taken{$try} && !&ping_ip_address($try)) {
return wantarray ? ( $try, $n ) : $try;
}
}
}
return wantarray ? ( ) : undef;
}
# free_ip6_address(&template|&acl)
# Returns an IPv6 address within the allocation range which is not currently
# used. Checks this system's configured interfaces, and does pings.
sub free_ip6_address
{
local ($tmpl) = @_;
local %taken = &interface_ip_addresses();
%taken = (%taken, &domain_ip_addresses());
local @ranges = split(/\s+/, $tmpl->{'ranges6'});
foreach my $rn (@ranges) {
my ($r, $n) = split(/\//, lc($rn));
$r =~ /^([0-9a-f:]+):([0-9a-f]+)\-([0-9a-f]+)$/ || next;
local ($base, $s, $e) = ($1, $2, $3);
for(my $j=hex($s); $j<=hex($e); $j++) {
local $try = sprintf "%s:%x", $base, $j;
if (!$taken{$try} && !&ping_ip_address($try)) {
return wantarray ? ( $try, $n ) : $try;
}
}
}
return wantarray ? ( ) : undef;
}
# interface_ip_addresses()
# Returns a hash of IP addresses that are in use by network interfaces, both
# active and boot-time
sub interface_ip_addresses
{
local %taken;
foreach my $ip (&active_ip_addresses(), &bootup_ip_addresses()) {
$taken{$ip} = 1;
}
return %taken;
}
# domain_ip_addresses()
# Returns a hash of IPs that are assigned to any domains as virtual IPs
sub domain_ip_addresses
{
local %taken;
foreach my $d (&list_domains()) {
$taken{$d->{'ip'}} = 1 if ($d->{'virt'});
$taken{$d->{'ip6'}} = 1 if ($d->{'virt6'});
}
return %taken;
}
# cloudmin_ip_addresses()
# Returns a hash of IPs that are assigned to any Cloudmin servers (if installed)
sub cloudmin_ip_addresses
{
local %taken;
if (&foreign_installed("server-manager")) {
&foreign_require("server-manager");
foreach my $s (&server_manager::list_managed_servers()) {
foreach my $ip (&server_manager::get_server_ip($s)) {
$taken{$ip} = 1;
}
foreach my $ip (&server_manager::get_server_ip6($s)) {
$taken{$ip} = 1;
}
}
}
return %taken;
}
# active_ip_addresses()
# Returns a list of IP addresses (v4 and v6) that are active on the system
# right now.
sub active_ip_addresses
{
&foreign_require("net");
local @rv;
push(@rv, map { $_->{'address'} } &net::active_interfaces());
if (&supports_ip6()) {
push(@rv, map { $_->{'address'} } &active_ip6_interfaces());
}
if (&has_command("ip")) {
# On Linux, the 'ip' command sometimes includes IPs that are not
# shown by ifconfig -a
local $out = &backquote_command("ip addr /dev/null");
foreach my $l (split(/\r?\n/, $out)) {
if ($l =~ /inet\s+([0-9\.]+)/) {
push(@rv, $1);
}
if ($l =~ /inet6\s+([a-f0-9:]+)/) {
push(@rv, $1);
}
}
}
return grep { $_ ne '' } &unique(@rv);
}
# bootup_ip_addresses()
# Returns a list of IP addresses (v4 and v6) that are activated at boot time
sub bootup_ip_addresses
{
&foreign_require("net");
local @rv;
foreach my $i (&net::boot_interfaces()) {
if ($i->{'range'} && $i->{'start'} && $i->{'end'}) {
local $start = &net::ip_to_integer($i->{'start'});
local $end = &net::ip_to_integer($i->{'end'});
for(my $j=$start; $j<=$end; $j++) {
push(@rv, &net::integer_to_ip($j));
}
}
elsif ($i->{'address'}) {
push(@rv, $i->{'address'});
}
}
if (&supports_ip6()) {
push(@rv, map { $_->{'address'} } &boot_ip6_interfaces());
}
return grep { $_ ne '' } &unique(@rv);
}
# ping_ip_address(hostname|ip|ipv6)
# Returns 1 if some host responds to a ping in 1 second
sub ping_ip_address
{
local ($host) = @_;
local $pinger = &check_ip6address($host) ? "ping6" : "ping";
local $pingcmd = $gconfig{'os_type'} =~ /-linux$/ ? "$pinger -c 1 -t 1"
: $pinger;
local ($out, $timed_out) = &backquote_with_timeout(
$pingcmd." ".$host." 2>&1", 1, 1);
return !$timed_out && !$?;
}
# parse_ip_ranges(ranges)
# Returns a list of all IP allocation ranges, each of which is a 3-element
# array of starting IP, ending IP and optional netmask
sub parse_ip_ranges
{
local @rv;
local @ranges = split(/\s+/, $_[0]);
foreach my $rn (@ranges) {
my ($r, $n) = split(/\//, $rn);
if ($r =~ /^(\d+\.\d+\.\d+)\.(\d+)\-(\d+)$/) {
# IPv4 range
push(@rv, [ "$1.$2", "$1.$3", $n ]);
}
elsif ($r =~ /^([0-9a-f:]+):([0-9a-f]+)-([0-9a-f]+)$/i) {
# IPv6 range
push(@rv, [ "$1:$2", "$1:$3", $n ]);
}
}
return @rv;
}
# join_ip_ranges(&ranges)
# Converts a list of ranges into a string
sub join_ip_ranges
{
local @ranges;
foreach my $r (@{$_[0]}) {
if (&check_ipaddress($r->[0])) {
# IPv4 range
local @start = split(/\./, $r->[0]);
local @end = split(/\./, $r->[1]);
push(@ranges, join(".", @start)."-".$end[3].
($r->[2] ? "/".$r->[2] : ""));
}
elsif (&check_ip6address($r->[0])) {
# IPv6 range
local @end = split(/:/, $r->[1]);
push(@ranges, $r->[0]."-".$end[$#end].
($r->[2] ? "/".$r->[2] : ""));
}
}
return join(" ", @ranges);
}
# ip_within_ranges(ip, ranges-string)
# Returns 1 if some IP falls within a space-separated list of ranges
sub ip_within_ranges
{
my ($ip, $ranges) = @_;
&foreign_require("net");
my $n = &net::ip_to_integer($ip);
foreach my $r (&parse_ip_ranges($ranges)) {
if (&check_ipaddress($r->[0])) {
# IPv4 range
if ($n >= &net::ip_to_integer($r->[0]) &&
$n <= &net::ip_to_integer($r->[1])) {
return 1;
}
}
elsif (&check_ip6address($r->[0])) {
# IPv6 range
my ($p, $n) = split(/\//, lc($r));
$p =~ /^([0-9a-f:]+):([0-9a-f]+)\-([0-9a-f]+)$/ || next;
local ($base, $s, $e) = ($1, $2, $3);
$ip =~ /^([0-9a-f:]+):([0-9a-f]+)$/ || next;
if (lc($1) eq lc($base) &&
hex($2) >= hex($s) && hex($2) <= hex($e)) {
return 1;
}
}
}
return 0;
}
# setup_for_subdomain(&parent-domain, subdomain-user, &sub-domain)
# Ensures that this virtual server can host sub-servers
sub setup_for_subdomain
{
local ($d, $subuser, $subd) = @_;
if (!-d "$d->{'home'}/domains") {
&make_dir_as_domain_user($d, "$d->{'home'}/domains", 0755);
}
}
# count_domains([type])
# Returns the number of additional domains the current user is allowed to
# create (-1 for infinite), the reason for the limit (2=this reseller,
# 1=reseller, 0=user), the number of domains allowed in total, and a flag
# indicating if this limit should be hidden from the user.
# May exclude alias domains if they don't count towards the max.
sub count_domains
{
local ($type) = @_;
$type ||= "doms";
local ($left, $reason, $max, $hide) = &count_feature($type);
if ($left != 0 && $type ne "aliasdoms") {
# If no limit has been hit, check the licence
local ($lstatus, $lexpiry, $lerr, $ldoms) = &check_licence_expired();
if ($ldoms) {
local $donedef = 0;
local @doms = grep { !$_->{'alias'} &&
(!$_->{'defaultdomain'} || $donedef++) }
&list_domains();
if (@doms > $ldoms) {
return (0, 3, $ldoms, 0);
}
else {
# Haven't reached .. check if the licence limit is
# less than the current limit
local $dleft = $ldoms - @doms;
if ($left == -1 || $dleft < $left) {
# Will hit licensed domains limit
return ($dleft, 3,
$max < $ldoms && $max > 0 ? $max : $ldoms, 0);
}
else {
# Will hit user or reseller limit
return ($left, $reason, $max, $hide);
}
}
}
}
return ($left, $reason, $max, $hide);
}
# count_mailboxes(&parent)
# Returns the number of mailboxes in this domain and all subdomains, and the
# max allowed for the current user
sub count_mailboxes
{
local $count = 0;
local $doms = 0;
local $parent = $_[0]->{'parent'} ? &get_domain($_[0]->{'parent'}) : $_[0];
local $d;
foreach $d ($parent, &get_domain_by("parent", $parent->{'id'})) {
local @users = &list_domain_users($d, 0, 1, 1, 1);
$count += @users;
$doms++;
}
return ( $count, $parent->{'mailboxlimit'} ? $parent->{'mailboxlimit'} : 0,
$doms );
}
# count_feature(feature, [user])
# Returns the number of extra instances of the given feature that the current
# user is allowed to create, the reason for the limit (2=this reseller,
# 1=reseller, 0=user), the total allowed, and a flag indicating if this
# limit should be hidden from the user.
# Feature can be "doms", "aliasdoms", "realdoms", "topdoms", "mailboxes",
# "aliases", "quota", "uquota", "dbs", "bw" or a feature
sub count_feature
{
local ($f) = @_;
local $user = $_[1] || $base_remote_user;
local %access = &get_module_acl($user);
# Master admin has no limit
return (-1, 0) if (&master_admin());
local $userleft = -1;
local $usermax;
if (!$access{'reseller'}) {
# Count the number that this user has
local @doms = &get_domain_by("user", $user);
local ($parent) = grep { !$_->{'parent'} } @doms;
local $limit = $f eq "doms" ? $parent->{'domslimit'} :
$f eq "aliasdoms" ? $parent->{'aliasdomslimit'} :
$f eq "realdoms" ? $parent->{'realdomslimit'} :
$f eq "mailboxes" ? $parent->{'mailboxlimit'} :
$f eq "aliases" ? $parent->{'aliaslimit'} :
$f eq "dbs" ? $parent->{'dbslimit'} : undef;
$limit = undef if ($limit eq "*");
if ($limit ne "") {
# A server-owner-level limit is in force .. check it
local $got = &count_domain_feature($f, @doms);
if ($got >= $limit) {
return (0, 0, $limit);
}
$userleft = $limit - $got;
$usermax = $limit;
}
if (($f eq "aliasdoms" || $f eq "realdoms") &&
$parent->{'domslimit'} && $parent->{'domslimit'} ne '*') {
# See if the owner is over the limit for all domains types too
local $got = &count_domain_feature("doms", @doms);
if ($got >= $parent->{'domslimit'}) {
return (0, 0, $parent->{'domslimit'});
}
else {
$userleft = $parent->{'domslimit'} - $got;
$usermax = $parent->{'domslimit'};
}
}
($reseller) = split(/\s+/, $parent->{'reseller'});
}
else {
$reseller = $user;
}
if ($reseller && defined(&get_reseller_domains)) {
# Either this user is owned by a reseller, or he is a reseller.
local @rdoms = &get_reseller_domains($reseller);
local %racl = &get_reseller_acl($reseller);
local $reason = $access{'reseller'} ? 2 : 1;
local $hide = $base_remote_user ne $reseller && $racl{'hide'};
local $limit = $racl{"max_".$f};
if ($limit ne "") {
# Reseller has a limit ..
local $got = &count_domain_feature($f, @rdoms);
if ($got > $limit || $got < 0) {
# Reseller has reached his limit
return (0, $reason, $limit, $hide);
}
else {
# Check if reseller limit is less than the user limit
local $reselleft = $limit - $got;
if ($userleft == -1 || $reselleft < $userleft) {
# Yes .. reseller limit applies
return ($reselleft, $reason, $limit, $hide);
}
}
}
if (($f eq "aliasdoms" || $f eq "realdoms" || $f eq "topdoms") &&
$racl{'max_doms'}) {
# See if the reseller is over the limit for all domains types
local $got = &count_domain_feature("doms", @rdoms);
if ($got >= $racl{'max_doms'}) {
return (0, $reason, $racl{'max_doms'}, $hide);
}
}
if ($f eq "topdoms" && $racl{'max_realdoms'}) {
# See if the reseller is over the limit for all real domains
local $got = &count_domain_feature("realdoms", @rdoms);
if ($got >= $racl{'max_realdoms'}) {
return (0, $reason, $racl{'max_realdoms'}, $hide);
}
}
}
return ($userleft, 0, $usermax);
}
# count_domain_feature(feature, &domain, ...)
# Returns the total for some feature in the given domains. May return -1 if
# any are set to unlimited (ie. quotas)
sub count_domain_feature
{
local ($f, @doms) = @_;
local $rv = 0;
local $d;
foreach $d (@doms) {
if ($f eq "dbs") {
local @dbs = &domain_databases($d);
$rv += scalar(@dbs);
}
elsif ($f eq "mailboxes") {
local @users = &list_domain_users($d, 0, 1, 1, 1);
$rv += scalar(@users);
}
elsif ($f eq "aliases") {
local @aliases = &list_domain_aliases($d, 1);
$rv += scalar(@aliases);
}
elsif ($f eq "quota" || $f eq "uquota") {
if (!$d->{'parent'}) {
return -1 if ($d->{$f} eq "" || $d->{$f} eq "0");
$rv += $d->{$f};
}
}
elsif ($f eq "bw") {
if (!$d->{'parent'}) {
return -1 if ($d->{'bw_limit'} eq "" ||
$d->{'bw_limit'} eq "0");
$rv += $d->{'bw_limit'};
}
}
elsif ($f eq "doms") {
$rv++ if (!$d->{'alias'} || !$config{'limitnoalias'});
}
elsif ($f eq "aliasdoms") {
$rv++ if ($d->{'alias'});
}
elsif ($f eq "realdoms") {
$rv++ if (!$d->{'alias'});
}
elsif ($f eq "topdoms") {
$rv++ if (!$d->{'parent'});
}
else {
$rv++ if ($d->{$f});
}
}
return $rv;
}
# database_name(&domain)
# Returns a suitable database name for a domain
sub database_name
{
local ($d) = @_;
local $tmpl = &get_template($d->{'template'});
local %hash = %$d;
if (!$hash{'uid'}) {
# Fake UID allocation now, in case the template uses it
local %taken;
&build_taken(\%taken);
$hash{'uid'} = &allocate_uid(\%taken);
}
if (!$hash{'gid'}) {
# Fake GID allocation
local %gtaken;
&build_group_taken(\%gtaken);
$hash{'gid'} = &allocate_gid(\%gtaken);
}
local $db = &substitute_domain_template($tmpl->{'mysql'}, \%hash);
$db = lc($db);
$db ||= $d->{'prefix'};
$db = &fix_database_name($db, $d->{'mysql'} && $d->{'postgres'} ? undef :
$d->{'mysql'} ? 'mysql' : 'postgres');
return $db;
}
# fix_database_name(dbname, [dbtype])
# If a database name starts with a number, convert it to a word to support
# PostgreSQL, which doesn't like numeric names. Also converts . and - to _,
# and handles reserved DB names.
sub fix_database_name
{
local ($db, $dbtype) = @_;
if (!$dbtype) {
# Guess DB type
my @dbtypes = grep { $config{$_} } @database_features;
if (scalar(@dbtypes) == 1) {
$dbtype = $dbtypes[0];
}
}
$db = lc($db);
$db =~ s/[\.\-]/_/g; # mysql doesn't like . or _
if ($db eq "test" || $db eq "mysql" || $db =~ /^template/) {
# These names are reserved by MySQL and PostgreSQL
$db = "db".$db;
}
if (!$dbtype || $dbtype eq "mysql") {
# MySQL DB names cannot be longer than 64 chars
if (length($db) > 64) {
$db = substr($db, 0, 64);
}
}
return $db;
}
# remove_numeric_prefix(name)
# If a name starts with a number, convert it to a work
sub remove_numeric_prefix
{
my ($db) = @_;
return $db if ($config{'allow_numbers'} && $db !~ /^\d+$/);
$db =~ s/^0/zero/g;
$db =~ s/^1/one/g;
$db =~ s/^2/two/g;
$db =~ s/^3/three/g;
$db =~ s/^4/four/g;
$db =~ s/^5/five/g;
$db =~ s/^6/six/g;
$db =~ s/^7/seven/g;
$db =~ s/^8/eight/g;
$db =~ s/^9/nine/g;
return $db;
}
# validate_database_name(&domain, type, name)
# Returns an error message if a name is invalid, undef if OK
sub validate_database_name
{
local ($d, $dbtype, $dbname) = @_;
local $vfunc = "validate_database_name_".$dbtype;
if (defined(&$vfunc)) {
return &$vfunc($d, $dbname);
}
else {
# Default rules
$dbname =~ /^[a-z0-9\_]+$/i && $dbname =~ /^[a-z]/i ||
return $text{'database_ename'};
return undef;
}
}
# generate_random_available_user(pattern)
# Generates free user available to be used as
# Unix user, group, and virtual server user
sub generate_random_available_user
{
my ($pattern) = @_;
$pattern ||= 'u-[0-9]{4}';
state $user;
if (!defined($user)) {
for (;;) {
my $rand_name = &substitute_pattern($pattern,
{'filter' => '[^A-Za-z0-9\\-_]',
'substitute-datetime' => 1});
my $uname_clash = defined(getpwnam($rand_name)) ||
defined(getgrnam($rand_name)) ||
&get_domain_by("prefix", $rand_name);
if (!$uname_clash) {
$user = $rand_name;
last;
}
}
}
return $user;
}
# unixuser_name(domainname)
# Returns a Unix username for some domain, or undef if none can be found
sub unixuser_name
{
my ($dname) = @_;
my ($dname_, $config_longname, $try1, $user) = ($dname, $config{'longname'});
if ($config_longname && $config_longname !~ /^[0-9]$/) {
# Extract random username based on the user pattern
$user = &generate_random_available_user($config_longname);
}
elsif ($config_longname == 2) {
$user = &compute_prefix($dname, undef, undef, 1);
}
else {
# Use one based on actual domain name
$dname =~ s/^xn(-+)//;
$dname = &remove_numeric_prefix($dname);
$dname =~ /^([^\.]+)/;
($try1, $user) = ($1, $1);
if (defined(getpwnam($try1)) || $config_longname) {
$user = &remove_numeric_prefix($dname_);
$try2 = $user;
if (defined(getpwnam($try2))) {
return (undef, $try1, $try2);
}
}
}
if ($config{'username_length'} && length($user) > $config{'username_length'}) {
# Admin asked for a short username
my $short = substr($user, 0, $config{'username_length'});
if (!defined(getpwnam($short))) {
$user = $short;
}
}
return ($user);
}
# unixgroup_name(domainname, username)
# Returns a Unix group name for some domain, or undef if none can be found
sub unixgroup_name
{
my ($dname, $user) = @_;
my ($dname_, $config_longname, $try1, $group) = ($dname, $config{'longname'});
if ($user && $config{'groupsame'}) {
# Same as username where possible
if (!defined(getgrnam($user))) {
return ($user);
}
return (undef, $user, $user);
}
# Extract random group based on the user pattern
if ($config_longname && $config_longname !~ /^[0-9]$/) {
$group = &generate_random_available_user($config_longname);
}
elsif ($config_longname == 2) {
$group = &compute_prefix($dname, undef, undef, 1);
}
# Use one based on actual domain name
else {
$dname =~ s/^xn(-+)//;
$dname = &remove_numeric_prefix($dname);
$dname =~ /^([^\.]+)/;
($try1, $group) = ($1, $1);
if (defined(getgrnam($try1)) || $config{'longname'}) {
$group = &remove_numeric_prefix($dname_);
$try2 = $group;
if (defined(getpwnam($try))) {
return (undef, $try1, $try2);
}
}
}
return ($group);
}
# virtual_server_clashes(&dom, [&features-to-check], [field-to-check],
# [replication-mode])
# Returns a clash error message if any were found for some new domain
sub virtual_server_clashes
{
local ($dom, $check, $field, $repl) = @_;
my $f;
foreach $f (@features) {
next if ($dom->{'parent'} && $f eq "webmin");
next if ($dom->{'parent'} && $f eq "unix");
if ($dom->{$f} && (!$check || $check->{$f})) {
local $cfunc = "check_${f}_clash";
local $err = defined(&$cfunc) ? &$cfunc($dom, $field, $repl)
: undef;
if ($err) {
if ($err eq '1') {
# Use a built-in error
$err = &text('setup_e'.$f,
$dom->{'dom'}, $dom->{'db'},
$dom->{'user'}, $dom->{'group'});
}
return $err;
}
}
}
foreach $f (&list_feature_plugins()) {
if ($dom->{$f} && (!$check || $check->{$f})) {
local $cerr = &plugin_call($f, "feature_clash", $dom, $field);
return $cerr if ($cerr);
}
}
return undef;
}
# virtual_server_depends(&dom, [feature], [&old-dom])
# Returns an error message if any of the features in the domain depend on
# missing features
sub virtual_server_depends
{
local ($d, $feat, $oldd) = @_;
local $f;
# Check features that are enabled
foreach $f (grep { $d->{$_} } @features) {
next if ($feat && $f ne $feat);
local $dfunc = "check_depends_$f";
if (defined(&$dfunc)) {
# Call dependecy function
local $derr = &$dfunc($d, $oldd);
return $derr if ($derr);
}
# Check fixed dependency list
local $fd;
foreach $fd (@{$feature_depends{$f}}) {
return &text('setup_edep'.$f) if (!$d->{$fd});
}
}
# Check plugins that are enabled
foreach $f (grep { $d->{$_} } &list_feature_plugins()) {
next if ($feat && $f ne $feat);
local $derr = &plugin_call($f, "feature_depends", $d, $oldd);
return $derr if ($derr);
}
# Check features that are NOT enabled, to ensure that any needed features are
# not missing. ie. mysql missing from parent but on children
foreach $f (grep { !$d->{$_} } @features) {
next if ($feat && $f ne $feat);
local $dfunc = "check_anti_depends_$f";
if (defined(&$dfunc)) {
# Call dependecy function
local $derr = &$dfunc($d);
return $derr if ($derr);
}
}
return undef;
}
# virtual_server_limits(&domain, [&old-domain])
# Checks if the addition of a feature would exceed any limit for the user
sub virtual_server_limits
{
local ($d, $oldd) = @_;
local ($left, $reason, $max);
local $tmpl = &get_template($d->{'template'});
# Check database limit
local $newdbs = 0;
$newdbs++ if ($d->{'mysql'} && (!$oldd || !$oldd->{'mysql'}) &&
$tmpl->{'mysql_mkdb'} && !$d->{'no_mysql_db'});
$newdbs++ if ($d->{'postgres'} && (!$oldd || !$oldd->{'postgres'}) &&
$tmpl->{'mysql_mkdb'});
if ($newdbs) {
($left, $reason, $max) = &count_feature("dbs");
if ($left == 0 || $newdbs == 2 && $left == 1) {
return &text('databases_noadd'.$reason, $max);
}
}
# Check quota limits
($left, $reason, $max) = &count_feature("quota");
if (!$d->{'parent'} && $d->{'quota'} eq "" && $left != -1) {
# Unlimited quota chosen, but not allowed!
return &text('setup_noquotainf'.$reason, "a_show($max, "home"));
}
local $newquota = $d->{'quota'} - ($oldd ? $oldd->{'quota'} : 0);
if ($left != -1 && $left-$newquota < 0) {
return &text('setup_noquotaadd'.$reason,
"a_show($left+($oldd ? $oldd->{'quota'} : 0),
"home", 2));
}
# Check bandwidth limits
($left, $reason, $max) = &count_feature("bw");
if (!$d->{'parent'} && $d->{'bw_limit'} eq "" && $left != -1) {
# Unlimited bandwidth chosen, but not allowed!
return &text('setup_nobwinf'.$reason, &nice_size($max));
}
local $newquota = $d->{'bw_limit'} - ($oldd ? $oldd->{'bw_limit'} : 0);
if ($left != -1 && $left-$newquota < 0) {
return &text('setup_nobwadd'.$reason,
&nice_size($left+($oldd ? $oldd->{'bw_limit'} : 0)));
}
# Check domains limit
if (!$oldd) {
($left, $reason, $max) = &count_domains(
$d->{'alias'} ? 'aliasdoms' :
$d->{'parent'} ? 'realdoms' : 'topdoms');
if ($left == 0) {
return &text('index_noadd'.$reason, $max);
}
}
return undef;
}
# virtual_server_warnings(&domain, [&old-domain], [replication-mode])
# Returns a list of warning messages related to the creation or modification
# of some virtual server.
sub virtual_server_warnings
{
local ($d, $oldd, $repl) = @_;
local @rv;
# Check core features
foreach my $f (grep { $d->{$_} } @features) {
local $wfunc = "check_warnings_$f";
if (defined(&$wfunc)) {
local $err = &$wfunc($d, $oldd, $repl);
push(@rv, $err) if ($err);
}
}
# Check plugins that are enabled
foreach $f (grep { $d->{$_} } &list_feature_plugins()) {
local $err = &plugin_call($f, "feature_warnings", $d, $oldd);
push(@rv, $err) if ($err);
}
return @rv;
}
# show_virtual_server_warnings(&domain, [&old-domain], &in)
# Checks if there are any warnings for the creation or modification of some
# domain, and if so shows a confirmation form - unless $in{'confirm_warnings'}
# is set. Returns 1 if the warning form was shown, 0 if not.
sub show_virtual_server_warnings
{
local ($d, $oldd, $in) = @_;
return 0 if ($in->{'confirm_warnings'});
local @warns = &virtual_server_warnings($d, $oldd);
return 0 if (!@warns);
my @hids;
foreach my $i (keys %$in) {
foreach my $v (split(/\0/, $in->{$i})) {
push(@hids, [ $i, $v ]);
}
}
print &ui_confirmation_form(
$script_name,
($oldd ? $text{'setup_warnings2'} : $text{'setup_warnings1'})."\n".
join("
\n", @warns)."
\n".
$text{'setup_warnrusure'},
\@hids,
[ [ 'confirm_warnings', $oldd ? $text{'setup_warnok2'}
: $text{'setup_warnok1'} ] ],
);
return 1;
}
# domain_name_clash(name)
# Returns 1 if some domain name is already in use
sub domain_name_clash
{
my ($domain) = @_;
foreach my $d (&list_domains()) {
return $d if (lc($d->{'dom'}) eq lc($domain));
}
return 0;
}
# create_virtual_server(&domain, [&parent-domain], [parent-user], [no-scripts],
# [no-post-actions], [password], [content])
# Given a complete domain object, setup all it's features
sub create_virtual_server
{
local ($dom, $parentdom, $parentuser, $noscripts, $nopost,
$pass, $content) = @_;
# Sanity checks
$dom->{'ip'} || $dom->{'ip6'} || return $text{'setup_edefip'};
# Run the before command
&set_domain_envs($dom, "CREATE_DOMAIN");
local $merr = &making_changes();
&reset_domain_envs($dom);
return &text('setup_emaking', "$merr") if (defined($merr));
# Get ready for hosting a subdomain
if ($dom->{'parent'}) {
&setup_for_subdomain($parentdom, $parentuser, $dom);
}
# Work out if this server is being created on the primary default IP address
if ($dom->{'ip'} eq &get_default_ip() &&
!$dom->{'virt'}) {
$dom->{'defip'} = 1;
}
# Work out the auto-alias domain name
local $tmpl = &get_template($dom->{'template'});
local $aliasname;
if ($tmpl->{'domalias'} ne 'none' && $tmpl->{'domalias'} && !$dom->{'alias'}) {
local $aliasprefix = $dom->{'dom'};
if ($tmpl->{'domalias_type'} eq '1') {
# Shorten to first part of domain
$aliasprefix =~ s/\..*$//;
}
elsif ($tmpl->{'domalias_type'} ne '0') {
# Use template
$aliasprefix = &substitute_domain_template(
$tmpl->{'domalias_type'}, $dom);
}
my $count;
while(1) {
$aliasname = $aliasprefix.$count.".".$tmpl->{'domalias'};
last if (!&get_domain_by("dom", $aliasname));
$count++;
}
$dom->{'autoalias'} = $aliasname;
}
# Work out auto disable schedule
if ($tmpl->{'domauto_disable'} &&
$tmpl->{'domauto_disable'} =~ /^(\d+)$/) {
$dom->{'disabled_auto'} = time() + $tmpl->{'domauto_disable'}*86400;
}
# Check if only hashed passwords are stored, and if so generate a random
# MySQL password now. This has to be done before any features are setup
# so that mysql_pass is available to all features.
if ($dom->{'hashpass'} && !$dom->{'parent'} && !$dom->{'mysql_pass'}) {
# Hashed passwords in use
$dom->{'mysql_pass'} = &random_password();
delete($dom->{'mysql_enc_pass'});
}
elsif ($tmpl->{'mysql_nopass'} == 2 && !$dom->{'parent'} &&
!$dom->{'mysql_pass'}) {
# Using random password by default
$dom->{'mysql_pass'} = &random_password();
delete($dom->{'mysql_enc_pass'});
}
# Same for PostgreSQL
if ($dom->{'hashpass'} && !$dom->{'parent'} && !$dom->{'postgres_pass'}) {
# Hashed passwords in use
$dom->{'postgres_pass'} = &random_password();
delete($dom->{'postgres_enc_pass'});
}
elsif ($tmpl->{'postgres_nopass'} == 2 && !$dom->{'parent'} &&
!$dom->{'postgres_pass'}) {
# Using random password by default
$dom->{'postgres_pass'} = &random_password();
delete($dom->{'postgres_enc_pass'});
}
# MySQL module comes from parent always
if ($parentdom) {
$dom->{'mysql_module'} = $parentdom->{'mysql_module'};
$dom->{'postgres_module'} = $parentdom->{'postgres_module'};
}
# Was this originally the default domain?
if (!defined($dom->{'defaultdomain'})) {
$dom->{'defaultdomain'} = 1, $dom->{'defaulthostdomain'} = 1
if ($dom->{'dom'} eq $config{'defaultdomain_name'});
}
# Save domain details before enabling features
&$first_print($text{'setup_save'});
&save_domain($dom, 1);
&$second_print($text{'setup_done'});
# Set up all the selected features (except Webmin login)
my $f;
local @dof = grep { $_ ne "webmin" } @features;
local $p = &domain_has_website($dom);
local $s = &domain_has_ssl($dom);
$dom->{'creating'} = 1; # Tell features that they are being called for creation
foreach $f (@dof) {
my $err;
if ($f eq 'web' && $p && $p ne 'web') {
# Web feature is provided by a plugin .. call it now
$err = &call_feature_setup($p, $dom);
}
elsif ($f eq 'ssl' && $s && $s ne 'ssl') {
# SSL feature is provided by a plugin .. call it now
$err = &call_feature_setup($s, $dom);
}
elsif ($dom->{$f}) {
$err = &call_feature_setup($f, $dom);
}
return $err if ($err);
}
# Set up all the selected plugins
foreach $f (&list_feature_plugins()) {
if ($dom->{$f} && $f ne $p && $f ne $s) {
my $err = &call_feature_setup($f, $dom);
return $err if ($err);
}
}
# Setup Webmin login last, once all plugins are done
if ($dom->{'webmin'}) {
local $sfunc = "setup_webmin";
local ($ok, $fok) = &try_function($f, $sfunc, $dom);
if (!$ok || !$fok) {
$dom->{$f} = 0;
}
}
# Add virtual IP address, if needed
if ($dom->{'virt'}) {
local ($ok, $fok) = &try_function("virt", "setup_virt", $dom);
if (!$ok || !$fok) {
$dom->{'virt'} = 0;
}
}
if ($dom->{'virt6'}) {
local ($ok, $fok) = &try_function("virt6", "setup_virt6", $dom);
if (!$ok || !$fok) {
$dom->{'virt6'} = 0;
}
}
delete($dom->{'creating'});
# Save domain details again at the end
&$first_print($text{'setup_save'});
&save_domain($dom);
&$second_print($text{'setup_done'});
# Now we have an ID, lock the domain
&lock_domain($dom);
# If mail client autoconfig is enabled globally, set it up for
# this domain
if ($config{'mail_autoconfig'} && $dom->{'mail'} &&
&domain_has_website($dom) && !$dom->{'alias'}) {
&enable_email_autoconfig($dom);
}
if (!$nopost) {
&run_post_actions();
}
if (!$dom->{'nocreationmail'}) {
# Notify the owner via email
&send_domain_email($dom, undef, $pass);
}
# Update the parent domain Webmin user
&refresh_webmin_user($dom);
if ($remote_user) {
# Add to this user's list of domains if needed
local %access = &get_module_acl();
if (!&can_edit_domain($dom)) {
$access{'domains'} = join(" ", split(/\s+/, $access{'domains'}),
$dom->{'id'});
&save_module_acl(\%access);
}
}
# Update any secondary groups that might contain the domain owner
if (!$dom->{'parent'}) {
&update_secondary_groups($dom);
}
# Create an automatic alias domain, if specified in template
if ($aliasname && $aliasname ne $dom->{'dom'}) {
&$first_print(&text('setup_domalias', $aliasname));
if ($aliasname !~ /^[a-z0-9\.\_\-]+$/i) {
&$second_print($text{'setup_domaliasbad'});
}
else {
&$indent_print();
local $parentdom = $dom->{'parent'} ?
&get_domain($dom->{'parent'}) : $dom;
local %alias = ( 'id', &domain_id(),
'dom', $aliasname,
'user', $dom->{'user'},
'group', $dom->{'group'},
'prefix', $dom->{'prefix'},
'ugroup', $dom->{'ugroup'},
'pass', $dom->{'pass'},
'alias', $dom->{'id'},
'uid', $dom->{'uid'},
'gid', $dom->{'gid'},
'ugid', $dom->{'ugid'},
'owner', "Automatic alias of $dom->{'dom'}",
'email', $dom->{'email'},
'nocreationmail', 1,
'name', 1,
'ip', $dom->{'ip'},
'dns_ip', $dom->{'dns_ip'},
'dns_ip6', $dom->{'dns_ip6'},
'virt', 0,
'source', $dom->{'source'},
'parent', $parentdom->{'id'},
'template', $dom->{'template'},
'plan', $dom->{'plan'},
'reseller', $dom->{'reseller'},
);
# Alias gets all features of domain, except for directory
# if it isn't needed
foreach my $f (@alias_features) {
next if ($f eq 'dir' && $config{$f} == 3 &&
$tmpl->{'aliascopy'});
$alias{$f} = $dom->{$f};
}
my $web = &domain_has_website($dom);
if ($web) {
# Website may use Nginx
$alias{$web} = 1;
}
$alias{'home'} = &server_home_directory(\%alias, $parentdom);
&generate_domain_password_hashes(\%dom, 1);
&set_provision_features(\%alias);
&complete_domain(\%alias);
&create_virtual_server(\%alias, $parentdom,
$parentdom->{'user'});
&$outdent_print();
&$second_print($text{'setup_done'});
}
}
# Set any PHP variables defined in the template
&setup_web_for_php($dom, undef, undef);
# Install any scripts specified in the template
local @scripts = &get_template_scripts($tmpl);
if (@scripts && !$dom->{'alias'} && !$noscripts &&
&domain_has_website($dom) && $dom->{'dir'} &&
!$dom->{'nocreationscripts'}) {
&$first_print($text{'setup_scripts'});
&$indent_print();
foreach my $sinfo (@scripts) {
# Work out install options
local ($name, $ver) = split(/\s+/, $sinfo->{'name'});
local $script = &get_script($name);
if (!$script) {
&$first_print(&text('setup_scriptgone', $name));
next;
}
# Work out actual version
local @allvers = @{$script->{'install_versions'}};
if ($ver eq "latest") {
$ver = $allvers[0];
}
&$first_print(&text('setup_scriptinstall',
$script->{'name'}, $ver));
local $opts = { 'path' => &substitute_scriptname_template(
$sinfo->{'path'}, $d) };
if ($sinfo->{'sopts'}) {
$opts->{$_->[0]} = $_->[1]
for map { [split(/=/, $_)] }
split(/,/, $sinfo->{'sopts'});
}
local $perr = &validate_script_path($opts, $script, $dom);
if ($perr) {
&$second_print(&text('setup_scriptpath', $perr));
next;
}
# Install needed packages
&setup_script_packages($script, $d, $ver);
# Check PHP version
local ($phpver, $phperr);
if (&indexof("php", @{$script->{'uses'}}) >= 0) {
($phpver, $phperr) = &setup_php_version(
$dom, $script, $ver, $opts->{'path'});
if (!$phpver) {
&$second_print(
&text('setup_scriptphpverset', $phperr));
next;
}
$opts->{'phpver'} = $phpver;
}
# Check Python version
local ($pyver, $pyerr);
if (&indexof("python", @{$script->{'uses'}}) >= 0) {
($pyver, $pyerr) = &setup_python_version(
$dom, $script, $ver, $opts->{'path'});
if ($pyerr) {
&$second_print($pyerr);
next;
}
$opts->{'pyver'} = $pyver;
}
# Check dependencies
local $derr = &check_script_depends($script, $dom, $ver,
$sinfo, $phpver);
if ($derr) {
&$second_print(&text('setup_scriptdeps', $derr));
next;
}
# Install needed PHP modules
if (!&setup_script_requirements($d, $script, $ver, $phpver, $opts)) {
&$second_print($text{'setup_scriptreqs'});
next;
}
# Find the database, if requested
if ($sinfo->{'db'}) {
local $dbname = &substitute_domain_template(
$sinfo->{'db'}, $dom);
if (!$dom->{$sinfo->{'dbtype'}}) {
# DB type isn't enabled for this domain
&$second_print(&text('setup_scriptnodb',
$text{'databases_'.$sinfo->{'dbtype'}}));
next;
}
$opts->{'db'} = $sinfo->{'dbtype'}."_".$dbname;
local @dbs = &domain_databases($dom);
local ($db) = grep {
$_->{'type'} eq $sinfo->{'dbtype'} &&
$_->{'name'} eq $dbname } @dbs;
if (!$db) {
# DB doesn't exist yet .. create it
$cfunc = "check_".$sinfo->{'dbtype'}.
"_database_clash";
if (&$cfunc($dom, $dbname)) {
&$second_print(
&text('setup_scriptclash', $dbname));
next;
}
$crfunc = "create_".$sinfo->{'dbtype'}.
"_database";
&$indent_print();
&$crfunc($dom, $dbname);
&$outdent_print();
}
}
# Check options
if (defined(&{$script->{'check_func'}})) {
my $oerr = &{$script->{'check_func'}}($dom, $ver,$opts);
if ($oerr) {
&$second_print(&text('setup_scriptopts',$oerr));
next;
}
}
# Fetch needed files
local %gotfiles;
local $ferr = &fetch_script_files($script, $dom, $ver, $opts,
undef, \%gotfiles, 1);
if ($derr) {
&$second_print(&text('setup_scriptfetch', $ferr));
next;
}
# Disable PHP timeouts
local $t = &disable_script_php_timeout($dom);
# Call the install function
local $dompass = $dom->{'pass'} || &random_password(8);
local ($ok, $msg, $desc, $url, $suser, $spass) =
&{$script->{'install_func'}}(
$dom, $ver, $opts, \%gotfiles, undef,
$dom->{'user'}, $dompass);
if ($ok) {
&$second_print(&text($ok < 0 ? 'setup_scriptpartial' :
'setup_scriptdone', $msg));
# Record script install in domain
&add_domain_script($dom, $name, $ver, $opts,
$desc, $url, $suser, $spass,
$ok < 0 ? $msg : undef);
}
else {
&$second_print(&text('setup_scriptfailed', $msg));
}
# Re-enable script PHP timeout
&enable_script_php_timeout($dom, $t);
}
&$outdent_print();
&$second_print($text{'setup_done'});
&save_domain($dom);
}
# Create any redirects or aliases specified in the template
my @redirs = map { [ split(/\s+/, $_, 4) ] }
split(/\t+/, $tmpl->{'web_redirects'});
if (@redirs && !$dom->{'alias'} && &domain_has_website($dom) &&
!$noscripts && !$dom->{'nocreationscripts'}) {
&$first_print($text{'setup_redirects'});
&$indent_print();
foreach my $r (@redirs) {
my %protos = map { $_, 1 } split(/,/, $r->[2]);
next if ($protos{'https'} && !$protos{'http'} &&
!&domain_has_ssl($dom));
my $path = &substitute_domain_template($r->[0], $dom);
my $dest = &substitute_domain_template($r->[1], $dom);
&$first_print(&text('setup_redirecting', $path, $dest));
if ($dest !~ /^(http|https):/) {
$dest = &public_html_dir($dom).$dest;
}
my $redirect = { 'path' => $path,
'dest' => $dest,
'alias' => $dest =~ /^(http|https):/ ? 0 : 1,
'regexp' => 0,
'http' => $protos{'http'},
'https' => $protos{'https'},
};
if ($r->[3]) {
$redirect->{'host'} =
&substitute_domain_template($r->[3], $dom);
}
$redirect = &add_wellknown_redirect($redirect);
my $err = &create_redirect($dom, $redirect);
if ($err) {
&$second_print(&text('setup_redirecterr', $err));
}
else {
&$second_print($text{'setup_redirected'});
}
}
&$outdent_print();
&$second_print($text{'setup_done'});
}
# If this was an alias domain, notify all features in the original domain. This
# is useful for things like awstats, which need to add the alias domain to those
# supported for the main site.
if ($dom->{'alias'}) {
local $aliasdom = &get_domain($dom->{'alias'});
foreach my $f (@features) {
local $safunc = "setup_alias_$f";
if ($aliasdom->{$f} && defined(&$safunc)) {
&try_function($f, $safunc, $aliasdom, $dom);
}
}
foreach $f (&list_feature_plugins()) {
if ($aliasdom->{$f} &&
&plugin_defined($f, "feature_setup_alias")) {
local $main::error_must_die = 1;
eval { &plugin_call($f, "feature_setup_alias",
$aliasdom, $dom) };
if ($@) {
&$second_print(&text('setup_aliasfailure',
&plugin_call($f, "feature_name"),"$@"));
}
}
}
}
# Refresh Unix group membership for reseller
if ($dom->{'reseller'} && defined(&update_reseller_unix_groups)) {
local $rinfo = &get_reseller($dom->{'reseller'});
if ($rinfo) {
&update_reseller_unix_groups($rinfo, 1);
}
}
# Setup MTA-STS if defined in the template
if (&can_mta_sts($dom) && $tmpl->{'mail_mta_sts'}) {
&$first_print($text{'mail_mta_sts_on'});
my $err = &enable_mta_sts($dom);
&$second_print($err ? &text('mail_mta_sts_err', $err)
: $text{'setup_done'});
}
# If an SSL cert wasn't generated because SSL wasn't enabled, do one now
my $always_ssl = defined($dom->{'always_ssl'}) ? $dom->{'always_ssl'}
: $tmpl->{'ssl_always_ssl'};
my $generated = 0;
if (!&domain_has_ssl($dom) && $always_ssl && !$dom->{'alias'}) {
$generated = &generate_default_certificate($dom) ? 1 : 0;
}
# Attempt to request a let's encrypt cert. This has to be done after the
# initial setup and when Apache has been restarted, so that it can serve the
# new website.
if (!defined($dom->{'auto_letsencrypt'})) {
$dom->{'auto_letsencrypt'} = $tmpl->{'ssl_auto_letsencrypt'};
if ($dom->{'dns'}) {
$dom->{'letsencrypt_dwild'} = $tmpl->{'ssl_letsencrypt_wild'};
}
}
if (!defined($dom->{'letsencrypt_subset'})) {
$dom->{'letsencrypt_subset'} = $tmpl->{'ssl_allow_subset'};
}
if ($dom->{'auto_letsencrypt'} && &domain_has_website($dom) &&
!$dom->{'disabled'} && !$dom->{'alias'} && !$dom->{'ssl_same'} &&
&under_public_dns_suffix($dom->{'dom'})) {
my $info = &cert_info($dom);
if ($info->{'self'}) {
if ($nopost) {
# Need to run any pending Apache restart
&run_post_actions(\&restart_apache);
}
if (&create_initial_letsencrypt_cert(
$dom, $dom->{'auto_letsencrypt'} == 2 ? 0 : 1,
$config{'err_letsencrypt'})) {
# Let's encrypt cert request worked
$generated = 2;
}
}
}
# Update service certs and DANE DNS records if a new Let's Encrypt cert
# was generated
if ($generated == 2) {
&enable_domain_service_ssl_certs($dom);
&sync_domain_tlsa_records($dom);
}
# For a new alias domain, if the target has a Let's Encrypt cert for all
# possible hostnames, re-request it to include the alias
if ($dom->{'alias'} && &domain_has_website($dom)) {
local $target = &get_domain($dom->{'alias'});
local $tinfo;
if ($target &&
&domain_has_website($target) &&
&domain_has_ssl_cert($target) &&
($tinfo = &cert_info($target)) &&
&is_acme_cert($tinfo) &&
!&check_domain_certificate($dom->{'dom'}, $tinfo)) {
&$first_print(&text('setup_letsaliases',
&show_domain_name($target),
&show_domain_name($dom)));
my $old_dname = $target->{'letsencrypt_dname'};
if ($target->{'letsencrypt_dname'}) {
# Add the alias domain's SSL hostnames to the list
$target->{'letsencrypt_dname'} =
join(" ", split(/\s+/, $target->{'letsencrypt_dname'}),
&get_hostnames_for_ssl($d));
}
my ($ok, $err, $dnames) = &renew_letsencrypt_cert($target);
if ($ok) {
&$second_print($text{'setup_done'});
}
else {
$target->{'letsencrypt_dname'} = $old_dname;
&$second_print(&text('setup_eletsaliases', $err));
}
}
}
&save_domain($dom);
# Put the user in a jail if possible
if (&is_domain_jailed($dom) && !&check_jailkit_support()) {
&$first_print($text{'setup_jail'});
my $err = &enable_domain_jailkit($dom);
if ($err) {
&$second_print(&text('setup_ejail', $err));
}
else {
&$second_print(&text('setup_jailed',
&domain_jailkit_dir($dom)));
}
&save_domain($dom);
}
if (!$dom->{'alias'} && &domain_has_website($dom) && defined($content)) {
# Just create virtualmin default index.html
&$first_print($text{'setup_contenting'});
eval {
local $main::error_must_die = 1;
&create_index_content($dom, $content, 0);
};
if ($@) {
&$second_print(&text('setup_econtenting', "$@"));
}
else {
&$second_print($text{'setup_done'});
}
}
# Run the after creation command
if (!$nopost) {
&run_post_actions();
}
&unlock_domain($dom);
&set_domain_envs($dom, "CREATE_DOMAIN");
local $merr = &made_changes();
&$second_print(&text('setup_emade', "$merr")) if (defined($merr));
&reset_domain_envs($dom);
return wantarray ? ($dom) : undef;
}
# create_initial_letsencrypt_cert(&domain, [validate-first], [show-errors])
# Create the initial default let's encrypt cert for a domain which has just
# had SSL enabled. May print stuff.
sub create_initial_letsencrypt_cert
{
local ($d, $valid, $showerrors) = @_;
&foreign_require("webmin");
my @dnames;
if ($d->{'letsencrypt_dname'}) {
@dnames = split(/\s+/, $d->{'letsencrypt_dname'});
}
else {
@dnames = &get_hostnames_for_ssl($d);
}
push(@dnames, "*.".$d->{'dom'}) if ($d->{'letsencrypt_dwild'});
&$first_print($text{'letsencrypt_doing3'});
if ($valid) {
my $vcheck = ['web'];
foreach my $dn (@dnames) {
$vcheck = ['dns'] if ($dn =~ /\*/);
}
my @errs = &validate_letsencrypt_config($d, $vcheck);
if (@errs) {
# Always store last Certbot error
my @estr;
foreach my $e (@errs) {
push(@estr, $e->{'desc'}." : ".
&html_strip($e->{'error'}));
}
my $err = &html_escape(join(", ", @estr));
$d->{'letsencrypt_last_failure'} = time();
$d->{'letsencrypt_last_err'} = $err;
$d->{'letsencrypt_last_err'} =~ s/\r?\n/\t/g;
if ($showerrors) {
&$second_print(&text('letsencrypt_evalid', $err));
}
else {
&$second_print($text{'letsencrypt_doing3failed'});
}
return 0;
}
if (defined(&check_domain_connectivity)) {
@errs = &check_domain_connectivity(
$d, { 'mail' => 1, 'ssl' => 1 });
if (@errs) {
# Always store last Certbot error
my $e = &html_escape(join(", ",
map { $_->{'desc'} } @errs));
$d->{'letsencrypt_last_failure'} = time();
$d->{'letsencrypt_last_err'} = $e;
$d->{'letsencrypt_last_err'} =~ s/\r?\n/\t/g;
if ($showerrors) {
&$second_print(&text('letsencrypt_econnect', $e));
}
else {
&$second_print($text{'letsencrypt_doing3failed'});
}
return 0;
}
}
}
my $phd = &public_html_dir($d);
my $before = &before_letsencrypt_website($d);
my @beforecerts = &get_all_domain_service_ssl_certs($d);
my $acme;
if ($tmpl->{'web_acme'} && defined(&list_acme_providers)) {
($acme) = grep { $_->{'id'} eq $tmpl->{'web_acme'} }
&list_acme_providers();
}
$d->{'letsencrypt_id'} = $acme->{'id'} if ($acme);
my @leargs = ($d, \@dnames, undef, undef, undef, undef, $acme,
$d->{'letsencrypt_subset'});
my ($ok, $cert, $key, $chain) =
&request_domain_letsencrypt_cert(@leargs);
if ($ok) {
$d->{'letsencrypt_nodnscheck'} = 1;
}
else {
# Try again with just externally resolvable hostnames
my @badnames;
my $fok = &filter_external_dns(\@dnames, \@badnames);
if ($fok == 0 && @dnames) {
$d->{'letsencrypt_nodnscheck'} = 0;
($ok, $cert, $key, $chain) =
&request_domain_letsencrypt_cert(@leargs);
}
}
&after_letsencrypt_website($d, $before);
if (!$ok) {
# Always store last Certbot error
$d->{'letsencrypt_last_failure'} = time();
$d->{'letsencrypt_last_err'} = $cert;
$d->{'letsencrypt_last_err'} =~ s/\r?\n/\t/g;
if ($showerrors) {
&$second_print(&text('letsencrypt_failed', $cert));
}
else {
&$second_print($text{'letsencrypt_doing3failed'});
}
return 0;
}
else {
&obtain_lock_ssl($d);
&install_letsencrypt_cert($d, $cert, $key, $chain);
$d->{'letsencrypt_dname'} = '';
$d->{'letsencrypt_dwild'} = 0;
$d->{'letsencrypt_last'} = time();
$d->{'letsencrypt_renew'} = 1 if ($d->{'letsencrypt_renew'} eq '');
$d->{'letsencrypt_last_id'} = $d->{'letsencrypt_id'};
# Inject initial SSL expiry to avoid wrong "until expiry"
&refresh_ssl_cert_expiry($d);
&lock_domain($d);
&save_domain($d);
&unlock_domain($d);
# Update other services using the cert
&update_all_domain_service_ssl_certs($d, \@beforecerts);
&break_invalid_ssl_linkages($d);
&sync_domain_tlsa_records($d);
&release_lock_ssl($d);
&$second_print(&text('letsencrypt_doing3ok',
join(", ", map { "$_" } @dnames)));
return 1;
}
}
# call_feature_setup(feature, &domain, [args])
# Calls the setup function for some feature or plugin. May print stuff.
# Returns an error message on a vital feature failure, sets flag to 0 for
# a non-vital failure.
sub call_feature_setup
{
local ($f, $dom, @args) = @_;
local %vital = map { $_, 1 } @vital_features;
if (&indexof($f, @features) >= 0) {
# Core feature
local $sfunc = "setup_$f";
if ($vital{$f}) {
# Failure of this feature should halt the entire setup
if (!&$sfunc($dom, @args)) {
return &text('setup_evital',
$text{'feature_'.$f});
}
}
else {
# Failure can be ignored
local ($ok, $fok) = &try_function($f, $sfunc, $dom);
if (!$ok || !$fok) {
$dom->{$f} = 0;
}
}
}
else {
# Plugin feature
local $main::error_must_die = 1;
eval { &plugin_call($f, "feature_setup", $dom, @args) };
if ($@) {
local $err = $@;
&$second_print(&text('setup_failure',
&plugin_call($f, "feature_name"), $err));
$dom->{$f} = 0;
}
}
return undef;
}
# delete_virtual_server(&domain, only-disconnect, no-post, preserve-remote)
# Deletes a Virtualmin domain and all sub-domains and aliases. Returns undef
# on succes, or an error message on failure.
sub delete_virtual_server
{
local ($d, $only, $nopost, $preserve) = @_;
# Get domain details
local @subs = &get_domain_by("parent", $d->{'id'});
local @aliasdoms = &get_domain_by("alias", $d->{'id'});
local @aliasdoms = grep { $_->{'parent'} ne $d->{'id'} } @aliasdoms;
local @alldoms = (@aliasdoms, @subs, $d);
# Check for DNS dependencies
foreach my $dd (@alldoms) {
foreach my $du (&get_domain_by("dns_subof", $dd->{'id'})) {
if (&indexof($du, @alldoms) < 0) {
# Domain is being used as a DNS parent, and user isn't
# being deleted
return &text('delete_ednssubof', &show_domain_name($dd),
&show_domain_name($du));
}
}
}
# Lock before deleting
foreach my $dd (@alldoms) {
&lock_domain($dd);
}
# Delete any jail
if (!&check_jailkit_support() && !$d->{'parent'}) {
&disable_domain_jailkit($d, 1);
}
# Go ahead and delete this domain and all sub-domains ..
&obtain_lock_mail();
&obtain_lock_unix();
foreach my $dd (@alldoms) {
if ($dd ne $d) {
# Show domain name
&$first_print(&text('delete_dom', &show_domain_name($dd)));
&$indent_print();
}
# What is shared for this domain?
my %remote;
if ($preserve) {
%remote = map { $_, 1 } &list_remote_domain_features($dd);
}
# Run the before command
&set_domain_envs($dd, "DELETE_DOMAIN");
local $merr = &making_changes();
&reset_domain_envs($dd);
return &text('delete_emaking', "$merr")
if (defined($merr));
if (!$only) {
local @users = $dd->{'alias'} && !$dd->{'aliasmail'} ||
!$dd->{'group'} ? ( )
: &list_domain_users($dd, 1, 0, 0, 0, 1);
local @aliases = &list_domain_aliases($dd);
# Stop any processes belonging to installed scripts, such
# as Ruby on Rails mongrels
local $done_stopscripts;
if (!$dd->{'alias'}) {
foreach my $sinfo (&list_domain_scripts($dd)) {
local $script = &get_script($sinfo->{'name'});
local $sfunc = $script->{'stop_func'};
if (defined(&$sfunc)) {
&$first_print(
$text{'delete_stopscripts'})
if (!$done_stopscripts++);
&$sfunc($dd, $sinfo);
}
}
}
if ($done_stopscripts) {
&$second_print($text{'setup_done'});
}
if (@users) {
# Delete mail users and their mail files
&$first_print($text{'delete_users'});
foreach my $u (@users) {
if (!$u->{'nomailfile'} && !$remote{'dir'}) {
&delete_mail_file($u);
}
$u->{'dbs'} = [ grep { !$remote{$_->{'type'}} }
@{$u->{'dbs'}} ];
&delete_user($u, $dd);
if (!$u->{'nocreatehome'} && !$remote{'dir'}) {
&delete_user_home($u, $d);
}
}
&$second_print($text{'setup_done'});
}
# Delete all virtusers
if (!$dd->{'aliascopy'} && !$remote{'mail'}) {
&$first_print($text{'delete_aliases'});
foreach my $v (&list_virtusers()) {
if ($v->{'from'} =~ /\@(\S+)$/ &&
$1 eq $dd->{'dom'}) {
&delete_virtuser($v);
}
}
&sync_alias_virtuals($dd);
&$second_print($text{'setup_done'});
}
# Take down IP
if ($dd->{'virt'}) {
&try_function("virt", "delete_virt", $dd);
}
if ($dd->{'virt6'}) {
&try_function("virt6", "delete_virt6", $dd);
}
}
if (!$dd->{'parent'}) {
# Delete any extra admins
foreach my $admin (&list_extra_admins($dd)) {
&delete_extra_admin($admin);
}
}
# If this is an alias domain, notify the target that it is being
# deleted. This allows things like extra awstats symlinks to be removed
$dd->{'deleting'} = 1; # so that features know about delete
if (!$only && $dd->{'alias'}) {
local $aliasdom = &get_domain($dd->{'alias'});
foreach my $f (@features) {
local $dafunc = "delete_alias_$f";
if ($aliasdom->{$f} && defined(&$dafunc)) {
&try_function($f, $dafunc, $aliasdom, $dd);
}
}
foreach $f (&list_feature_plugins()) {
if ($aliasdom->{$f} &&
&plugin_defined($f, "feature_delete_alias")) {
local $main::error_must_die = 1;
eval { &plugin_call($f, "feature_delete_alias",
$aliasdom, $dd) };
if ($@) {
&$second_print(
&text('delete_aliasfailure',
&plugin_call($f, "feature_name"),
"$@"));
}
}
}
}
# Remove SSL cert from Dovecot, Postfix, etc
&disable_domain_service_ssl_certs($dd);
# If this domain had it's own lets encrypt cert, delete any leftover
# files for it under /etc/letsencrypt
&foreign_require("webmin");
if ($dd->{'letsencrypt_last'} && !$dd->{'ssl_same'} &&
defined(&webmin::cleanup_letsencrypt_files)) {
&webmin::cleanup_letsencrypt_files($dd->{'dom'});
}
# Delete all features (or just 'webmin' if un-importing). Any
# failures are ignored!
my $f;
local $p = &domain_has_website($dd);
local @of;
if ($only) {
@of = ( "webmin" );
}
else {
@of = reverse(&list_ordered_features($dd, 1));
}
foreach $f (@of) {
if (($config{$f} || &indexof($f, @plugins) >= 0) &&
$dd->{$f} || $f eq 'unix') {
# Delete core feature
local @args = ( $preserve );
if ($f eq "mail") {
# Don't delete mail aliases, because we have
# already done so above
push(@args, 1);
}
&call_feature_delete($f, $dd, @args);
}
if (&indexof($f, @plugins) >= 0) {
&plugin_call($f, "feature_always_delete", $dd);
}
}
# Delete any FPM or FCGIwrap servers, just in case they
# were disassociated
&delete_php_fpm_pool($d);
if ($d->{'fcgiwrap_port'}) {
&delete_fcgiwrap_server($d);
}
# Delete SSL key files outside the home dir
if (!$dd->{'ssl_same'}) {
foreach my $k ('ssl_cert', 'ssl_key', 'ssl_chain',
'ssl_combined', 'ssl_everything') {
if ($dd->{$k} &&
!&is_under_directory($dd->{'home'}, $dd->{$k}) &&
-f $dd->{$k}) {
&unlink_logged($dd->{$k});
}
}
foreach my $dir (&ssl_certificate_directories($dd, 1)) {
if (!&is_under_directory($dd->{'home'}, $dir) &&
-d $dir &&
$dir =~ /\/[^\/]*(\Q$dd->{'dom'}\E|\Q$dd->{'id'}\E)[^\/]*$/ &&
&is_empty_directory($dir)) {
&unlink_logged($dir);
}
}
}
# Delete domain file
&$first_print(&text('delete_domain', &show_domain_name($dd)));
&delete_domain($dd);
&$second_print($text{'setup_done'});
# Update the parent domain Webmin user, so that his ACL
# is refreshed (and any resellers)
&refresh_webmin_user($dd);
# Call post script
&set_domain_envs($dd, "DELETE_DOMAIN");
local $merr = &made_changes();
&reset_domain_envs($dd);
&$second_print(&text('setup_emade', "$merr"))
if (defined($merr));
if ($dd ne $d) {
&$outdent_print();
&$second_print($text{'setup_done'});
}
}
&release_lock_mail();
&release_lock_unix();
# If deleted domain was default host name
if ($d->{'dom'} eq $config{'defaultdomain_name'}) {
&lock_file($module_config_file);
delete($config{'defaultdomain_name'});
$config{'default_domain_ssl'} = 0;
&save_module_config();
&unlock_file($module_config_file);
}
# Run the after deletion command
if (!$nopost) {
&run_post_actions();
}
# Unlock now we're done
foreach my $dd (reverse(@alldoms)) {
&unlock_domain($dd);
}
return undef;
}
# call_feature_delete(feature, &domain, arg, arg, ...)
# Calls the core or plugin-specific function to delete a feature. May print
# stuff.
sub call_feature_delete
{
local ($f, $dom, @args) = @_;
if (&indexof($f, @features) >= 0) {
# Call core delete function
local $dfunc = "delete_$f";
local ($ok, $fok) = &try_function($f, $dfunc, $dom, @args);
if (!$ok || !$fok) {
$dom->{$f} = 1;
}
}
else {
# Call plugin delete function
local $main::error_must_die = 1;
eval { &plugin_call($f, "feature_delete", $dom, @args) };
if ($@) {
local $err = $@;
&$second_print(&text('delete_failure',
&plugin_call($f, "feature_name"), $err));
}
}
}
# disable_scheduled_virtual_servers()
# Disables all scheduled virtual servers that are due to be disabled
sub disable_scheduled_virtual_servers
{
my @disdoms = grep {
!$_->{'protected'} && # if domain is not protected
!$_->{'disabled'} && # if not already disabled
&is_timestamp($_->{'disabled_auto'}) && # if actually a timestamp
$_->{'disabled_auto'} <= time() # if timestamp is in the past
} &list_domains();
foreach my $d (@disdoms) {
&push_all_print();
&set_all_null_print();
eval {
local $main::error_must_die = 1;
my $disabled_auto = &make_date($d->{'disabled_auto'});
&disable_virtual_server($d, 'schedule',
&text('disable_autodisabledone', $disabled_auto));
};
&pop_all_print();
&error_stderr("Disabling domain on schedule failed : $@") if ($@);
}
}
# disable_virtual_server(&domain, [reason-code], [reason-why], [&only-features])
# Disables all features of one virtual server. Returns undef on success, or
# an error message on failure.
sub disable_virtual_server
{
my ($d, $reason, $why, $only) = @_;
# Work out what can be disabled
my @disable = &get_disable_features($d);
if ($only) {
@disable = grep { &indexof($_, @$only) >= 0 } @disable;
@disable || return "None of the features to disable exist on this ".
"virtual server";
}
# Disable it
my %disable = map { $_, 1 } @disable;
$d->{'disabled_reason'} = $reason;
$d->{'disabled_why'} = $why;
$d->{'disabled_time'} = time();
delete($d->{'disabled_auto'});
# Run the before command
&set_domain_envs($d, "DISABLE_DOMAIN");
my $merr = &making_changes();
&reset_domain_envs($d);
return &text('disable_emaking', "".&html_escape($merr)."")
if (defined($merr));
# Disable all configured features
my @disabled;
foreach my $f (@features) {
if ($d->{$f} && $disable{$f}) {
my $dfunc = "disable_$f";
local ($ok, $fok) = &try_function($f, $dfunc, $d);
if ($ok && $fok) {
push(@disabled, $f);
}
}
}
foreach my $f (&list_feature_plugins()) {
if ($d->{$f} && $disable{$f}) {
&plugin_call($f, "feature_disable", $d);
push(@disabled, $f);
}
}
# Disable extra admins
&update_extra_webmin($d, 1);
# Save new domain details
&$first_print($text{'save_domain'});
&lock_domain($d);
$d->{'disabled'} = join(",", @disabled);
&save_domain($d);
&unlock_domain($d);
&$second_print($text{'setup_done'});
# Run the after command
&set_domain_envs($d, "DISABLE_DOMAIN");
my $merr = &made_changes();
&$second_print(&text('setup_emade', "$merr"))
if (defined($merr));
&reset_domain_envs($d);
return undef;
}
# enable_virtual_server(&domain)
# Enables all disabled features of one virtual server. Returns undef on
# success, or an error message on failure.
sub enable_virtual_server
{
my ($d) = @_;
# Work out what can be enabled
my @enable = &get_enable_features($d);
# Go ahead and do it
my %enable = map { $_, 1 } @enable;
delete($d->{'disabled_reason'});
delete($d->{'disabled_why'});
delete($d->{'disabled_auto'});
# Run the before command
&set_domain_envs($d, "ENABLE_DOMAIN");
my $merr = &making_changes();
&reset_domain_envs($d);
return &text('enable_emaking', "$merr") if (defined($merr));
# Enable all disabled features
foreach my $f (@features) {
if ($d->{$f} && $enable{$f}) {
my $efunc = "enable_$f";
&try_function($f, $efunc, $d);
}
}
foreach my $f (&list_feature_plugins()) {
if ($d->{$f} && $enable{$f}) {
&plugin_call($f, "feature_enable", $d);
}
}
# Enable extra admins
&update_extra_webmin($d, 0);
# Save new domain details
&$first_print($text{'save_domain'});
&lock_domain($d);
delete($d->{'disabled'});
&save_domain($d);
&unlock_domain($d);
&$second_print($text{'setup_done'});
# Run the after command
&set_domain_envs($d, "ENABLE_DOMAIN");
my $merr = &made_changes();
&$second_print(&text('setup_emade', "".&html_escape($merr).""))
if (defined($merr));
&reset_domain_envs($d);
return undef;
}
# register_post_action(&function, args)
sub register_post_action
{
push(@main::post_actions, [ @_ ]);
}
# run_post_actions([&only-action])
# Run all registered post-modification actions
sub run_post_actions
{
local @only = @_;
local %only = map { $_, 1 } @only;
local $a;
# Check if we are restarting Apache or Webmin, and if so don't reload it
my ($apache, $webmin);
foreach $a (@main::post_actions) {
if ($a->[0] eq \&restart_apache) {
$a->[1] ||= 0;
$apache = 1 if ($a->[1] == 1);
}
if ($a->[0] eq \&restart_webmin_fully) {
$webmin = 1;
}
}
if ($apache) {
@main::post_actions = grep { $_->[0] ne \&restart_apache ||
$_->[1] != 0 } @main::post_actions;
}
if ($webmin) {
@main::post_actions = grep { $_->[0] ne \&restart_webmin }
@main::post_actions;
}
# Run unique actions
local %done;
local @newpost;
foreach my $a (@main::post_actions) {
# Don't run multiple times
my $key;
if ($a->[0] eq \&restart_bind && $a->[1]) {
# Restarts for the same DNS server are equal
my $p = $a->[1]->{'provision_dns'} || $a->[1]->{'dns_cloud'};
$key = $a->[0].",".$p;
}
elsif ($a->[0] eq \&update_secondary_mx_virtusers) {
# Restarts in the same domain are considered equal
$key = $a->[0].($a->[1] ? ",".$a->[1]->{'dom'} : "");
}
elsif ($a->[0] eq \&restart_php_fpm_server) {
# Restarts of the same FPM version are equal
$key = $a->[0].($a->[1] ? ",".$a->[1]->{'init'} : "");
}
else {
$key = join(",", @$a);
}
next if ($done{$key}++);
# Skip if the caller requested specific actions
local ($afunc, @aargs) = @$a;
if (@only && !$only{$afunc}) {
push(@newpost, $a);
next;
}
# Call the restart function
local $main::error_must_die = 1;
eval { &$afunc(@aargs) };
if ($@) {
&$second_print(&text('setup_postfailure', "$@"));
}
}
@main::post_actions = @newpost;
}
# run_post_actions_silently()
# Just calls run_post_actions while supressing output
sub run_post_actions_silently
{
&push_all_print();
&set_all_null_print();
&run_post_actions();
&pop_all_print();
}
# find_bandwidth_job()
# Returns the cron job used for bandwidth monitoring
sub find_bandwidth_job
{
local $job = &find_cron_script($bw_cron_cmd);
return $job;
}
# get_bandwidth(&domain)
# Returns the bandwidth usage object for some domain
sub get_bandwidth
{
my ($d) = @_;
if (!defined($get_bandwidth_cache{$d->{'id'}})) {
local %bwinfo;
&read_file("$bandwidth_dir/$d->{'id'}", \%bwinfo);
foreach my $k (keys %bwinfo) {
if ($k =~ /^\d+$/) {
# Convert old web entries
$bwinfo{"web_$k"} = $bwinfo{$k};
delete($bwinfo{$k});
}
}
$get_bandwidth_cache{$d->{'id'}} = \%bwinfo;
}
return $get_bandwidth_cache{$d->{'id'}};
}
# save_bandwidth(&domain, &info)
# Update the bandwidth usage object for some domain
sub save_bandwidth
{
my ($d, $info) = @_;
&make_dir($bandwidth_dir, 0700);
&write_file("$bandwidth_dir/$d->{'id'}", $info);
$get_bandwidth_cache{$d->{'id'}} ||= $info;
}
# bandwidth_input(name, value, [no-unlimited], [dont-change])
# Returns HTML for a bandwidth input field, with an 'unlimited' option
sub bandwidth_input
{
local ($name, $value, $nounlimited, $dontchange) = @_;
local $rv;
local $dis1 = &js_disable_inputs([ $name, $name."_units" ], [ ]);
local $dis2 = &js_disable_inputs([ ], [ $name, $name."_units" ]);
local $dis;
if (!$nounlimited) {
if ($dontchange) {
# Show don't change option
$rv .= &ui_radio($name."_def", 2,
[ [ 2, $text{'massdomains_leave'}, "onClick='$dis1'" ],
[ 1, $text{'edit_bwnone'}, "onClick='$dis1'" ],
[ 0, " ", "onClick='$dis2'" ] ]);
$dis = 1;
}
else {
# Show unlimited option
$rv .= &ui_radio($name."_def", $value ? 0 : 1,
[ [ 1, $text{'edit_bwnone'}, "onClick='$dis1'" ],
[ 0, " ", "onClick='$dis2'" ] ]);
$dis = 1 if (!$value);
}
}
local ($val, $u);
my $unit = 1024;
if ($value eq "") {
# Default to GB, since bytes are rarely useful
$u = $text{"nice_size_GiB"};
}
elsif ($value && $value%($unit*$unit*$unit*$unit) == 0) {
$val = $value/($unit*$unit*$unit*$unit);
$u = $text{"nice_size_TiB"};
}
elsif ($value && $value%($unit*$unit*$unit) == 0) {
$val = $value/($unit*$unit*$unit);
$u = $text{"nice_size_GiB"};
}
elsif ($value && $value%($unit*$unit) == 0) {
$val = $value/($unit*$unit);
$u = $text{"nice_size_MiB"};
}
elsif ($value && $value%($unit) == 0) {
$val = $value/($unit);
$u = $text{"nice_size_kiB"};
}
else {
$val = $value;
$u = $text{"nice_size_b"};
}
local $sel = &ui_select($name."_units", $u,
[ [$text{"nice_size_b"}], [$text{"nice_size_kiB"}], [$text{"nice_size_MiB"}], [$text{"nice_size_GiB"}], [$text{"nice_size_TiB"}] ], 1, 0, 0, $dis);
$rv .= &text('edit_bwpast_'.$config{'bw_past'},
&ui_textbox($name, $val, 10, $dis)." ".$sel,
$config{'bw_period'});
return $rv;
}
# parse_bandwidth(name, error, [no-unlimited])
sub parse_bandwidth
{
if ($in{"$_[0]_def"} && !$_[2]) {
return undef;
}
else {
my $unit = 1024;
$in{$_[0]} =~ /^\d+$/ && $in{$_[0]} > 0 || &error($_[1]);
local $m = $in{"$_[0]_units"} eq $text{"nice_size_TiB"} ? $unit*$unit*$unit*$unit :
$in{"$_[0]_units"} eq $text{"nice_size_GiB"} ? $unit*$unit*$unit :
$in{"$_[0]_units"} eq $text{"nice_size_MiB"} ? $unit*$unit :
$in{"$_[0]_units"} eq $text{"nice_size_kiB"} ? $unit : 1;
return $in{$_[0]} * $m;
}
}
# email_template_input(template-file, subject, other-cc, other-bcc,
# [mailbox-cc, owner-cc, reseller-cc], [header],[filemode])
# Returns HTML for fields for editing an email template
sub email_template_input
{
local ($file, $subject, $cc, $bcc, $mailbox, $owner, $reseller, $header,
$filemode) = @_;
local $rv;
$rv .= &ui_table_start($header, undef, 2);
if ($filemode eq "none" || $filemode eq "default") {
# Show input for selecting if enabled
$rv .= &ui_table_row($text{'newdom_sending'},
&ui_yesno_radio("sending", $filemode eq "default" ? 1 : 0));
}
$rv .= &ui_table_row($text{'newdom_subject'},
&ui_textbox("subject", $subject, 60));
if (defined($mailbox)) {
# Show inputs for selecting destination
$rv .= &ui_table_row($text{'newdom_to'},
&ui_checkbox("mailbox", 1, $text{'newdom_mailbox'}, $mailbox)." ".
&ui_checkbox("owner", 1, $text{'newdom_owner'}, $owner)." ".
($virtualmin_pro ?
&ui_checkbox("reseller", 1, $text{'newdom_reseller'},
$reseller) : ""));
}
$rv .= &ui_table_row($text{'newdom_cc'},
&ui_textbox("cc", $cc, 60));
$rv .= &ui_table_row($text{'newdom_bcc'},
&ui_textbox("bcc", $bcc, 60));
if ($file) {
$rv .= &ui_table_row(undef,
&ui_textarea("template", &read_file_contents($file), 20, 70),
2);
}
$rv .= &ui_table_end();
return $rv;
}
# parse_email_template(file, subject-config, cc-config, bcc-config,
# [mailbox-config, owner-config, reseller-config],
# [filemode-config])
sub parse_email_template
{
local ($file, $subject_config, $cc_config, $bcc_config,
$mailbox_config, $owner_config, $reseller_config, $filemode_config) = @_;
$in{'template'} =~ s/\r//g;
&open_lock_tempfile(FILE, ">$file", 1) ||
&error(&text('efilewrite', $file, $!));
&print_tempfile(FILE, $in{'template'});
&close_tempfile(FILE);
&lock_file($module_config_file);
$config{$subject_config} = $in{'subject'};
$config{$cc_config} = $in{'cc'};
$config{$bcc_config} = $in{'bcc'};
if ($mailbox_config) {
$config{$mailbox_config} = $in{'mailbox'};
$config{$owner_config} = $in{'owner'};
if ($virtualmin_pro) {
$config{$reseller_config} = $in{'reseller'};
}
}
if ($filemode_config && defined($in{'sending'})) {
$config{$filemode_config} = $in{'sending'} ? "default" : "none";
}
$config{'last_check'} = time()+1; # no need for check.cgi to be run
&save_module_config();
&unlock_file($module_config_file);
}
# escape_user(username)
# Returns a Unix username with characters unsuitable for use in a mail
# destination (like @) escaped
sub escape_user
{
local $escuser = $_[0];
$escuser =~ s/\@/\\\@/g;
return $escuser;
}
# unescape_user(username)
# The reverse of escape_user
sub unescape_user
{
local $escuser = $_[0];
$escuser =~ s/\\\@/\@/g;
return $escuser;
}
# escape_alias(username)
# Converts a username into a suitable alias name
sub escape_alias
{
my ($escuser) = @_;
my $origuser = $escuser;
$escuser =~ s/\@/-/g;
if (!getpwnam($escuser)) {
$escuser = &escape_user($origuser);
}
return $escuser;
}
# replace_atsign(username)
# Replace an @ in a username with -
sub replace_atsign
{
my ($user) = @_;
$user =~ s/\@/-/g;
return $user;
}
# replace_atsign_if_exists(username)
# Replace an @ in a username with -
# if a user exists in system
sub replace_atsign_if_exists
{
my ($user) = @_;
my $origuser = $user;
$user =~ s/\@/-/g;
$user = $origuser if (!getpwnam($user));
return $user;
}
# escape_replace_atsign_if_exists(username)
# Replace an @ in a username with - if a
# user exists in system and if not return
# escaped @ user
sub escape_replace_atsign_if_exists
{
my ($user) = @_;
my $origuser = $user;
$user =~ s/\@/-/g;
$user = &escape_user($origuser)
if (!getpwnam($user));
return $user;
}
# add_atsign(username)
# Given a username like foo-bar.com, return foo@bar.com
sub add_atsign
{
local ($rv) = @_;
$rv =~ s/\-/\@/;
return $rv;
}
# dotqmail_file(&user)
sub dotqmail_file
{
return "$_[0]->{'home'}/.qmail";
}
# get_dotqmail(file)
sub get_dotqmail
{
$_[0] =~ /\.qmail(-(\S+))?$/;
local $alias = { 'file' => $_[0],
'name' => $2 };
local $_;
open(AFILE, "<".$_[0]) || return undef;
while() {
s/\r|\n//g;
s/#.*$//g;
if (/\S/) {
push(@{$alias->{'values'}}, $_);
}
}
close(AFILE);
return $alias;
}
# save_dotqmail(&alias, file, username|aliasname)
sub save_dotqmail
{
if (@{$_[0]->{'values'}}) {
&open_lock_tempfile(AFILE, ">$_[1]");
local $v;
foreach $v (@{$_[0]->{'values'}}) {
if ($v eq "\\$_[2]" || $v eq "\\NEWUSER") {
# Delivery to this user means to his maildir
&print_tempfile(AFILE, "./Maildir/\n");
}
else {
&print_tempfile(AFILE, $v,"\n");
}
}
&close_tempfile(AFILE);
}
else {
&unlink_file($_[1]);
}
}
# list_templates()
# Returns a list of all virtual server templates, including two defaults for
# top-level and sub-servers
sub list_templates
{
if (scalar(@list_templates_cache)) {
# Use cached copy
return @list_templates_cache;
}
local @rv;
&ensure_template("domain-template");
&ensure_template("subdomain-template");
&ensure_template("framefwd-template");
&ensure_template("user-template");
push(@rv, { 'id' => 0,
'name' => $text{'newtmpl_name0'},
'standard' => 1,
'default' => 1,
'web' => $config{'apache_config'},
'web_ssl' => $config{'apache_ssl_config'},
'web_user' => $config{'web_user'},
'web_fcgiwrap' => $config{'fcgiwrap'},
'web_cgimode' => $config{'cgimode'},
'web_html_dir' => $config{'html_dir'},
'web_html_perms' => $config{'html_perms'} || 750,
'web_stats_dir' => $config{'stats_dir'},
'web_stats_hdir' => $config{'stats_hdir'},
'web_stats_pass' => $config{'stats_pass'},
'web_stats_noedit' => $config{'stats_noedit'},
'web_port' => $default_web_port,
'web_sslport' => $default_web_sslport,
'web_urlport' => $config{'web_urlport'},
'web_urlsslport' => $config{'web_urlsslport'},
'web_sslprotos' => $config{'web_sslprotos'},
'web_alias' => $config{'alias_mode'},
'web_acme' => $config{'web_acme'},
'web_webmin_ssl' => $config{'webmin_ssl'},
'web_usermin_ssl' => $config{'usermin_ssl'},
'web_webmail' => $config{'web_webmail'},
'web_webmaildom' => $config{'web_webmaildom'},
'web_admin' => $config{'web_admin'},
'web_admindom' => $config{'web_admindom'},
'php_vars' => $config{'php_vars'} || "none",
'php_fpm' => $config{'php_fpm'} || "none",
'php_sock' => $config{'php_sock'} || 0,
'php_fpmtype' => $config{'php_fpmtype'} || "dynamic",
'php_log' => $config{'php_log'} || 0,
'php_log_path' => $config{'php_log_path'},
'web_php_suexec' => int($config{'php_suexec'}),
'web_ruby_suexec' => $config{'ruby_suexec'} eq '' ? -1 :
int($config{'ruby_suexec'}),
'web_phpver' => $config{'phpver'},
'web_php_noedit' => int($config{'php_noedit'}),
'web_phpchildren' => $config{'phpchildren'},
'web_ssi' => $config{'web_ssi'} eq '' ? 2 : $config{'web_ssi'},
'web_ssi_suffix' => $config{'web_ssi_suffix'},
'web_dovecot_ssl' => $config{'dovecot_ssl'},
'web_postfix_ssl' => $config{'postfix_ssl'},
'web_mysql_ssl' => $config{'mysql_ssl'},
'web_proftpd_ssl' => $config{'proftpd_ssl'},
'web_http2' => $config{'web_http2'},
'web_redirects' => $config{'web_redirects'},
'web_sslredirect' => $config{'auto_redirect'},
'ssl_key_size' => $config{'key_size'},
'ssl_cert_type' => $config{'cert_type'},
'ssl_auto_letsencrypt' => $config{'auto_letsencrypt'},
'ssl_letsencrypt_wild' => $config{'letsencrypt_wild'},
'ssl_renew_letsencrypt' => $config{'renew_letsencrypt'},
'ssl_always_ssl' => $config{'always_ssl'},
'ssl_tlsa_records' => $config{'tlsa_records'},
'ssl_combined_cert' => $config{'combined_cert'},
'ssl_allow_subset' => $config{'allow_subset'},
'ssl_connectivity' => $config{'ssl_connectivity'} // 1,
'webalizer' => $config{'def_webalizer'} || "none",
'content_web' => $config{'content_web'} // 2,
'content_web_html' => $config{'content_web_html'},
'disabled_web' => $config{'disabled_web'} || "none",
'disabled_url' => $config{'disabled_url'} || "none",
'dns' => $config{'bind_config'} || "none",
'dns_replace' => $config{'bind_replace'},
'dns_view' => $config{'dns_view'},
'dns_spf' => $config{'bind_spf'} || "none",
'dns_spfhosts' => $config{'bind_spfhosts'},
'dns_spfonly' => $config{'bind_spfonly'},
'dns_spfincludes' => $config{'bind_spfincludes'},
'dns_spfall' => $config{'bind_spfall'},
'dns_dmarc' => $config{'bind_dmarc'} || "none",
'dns_dmarcp' => $config{'bind_dmarcp'} || "none",
'dns_dmarcpct' => $config{'bind_dmarcpct'} || 100,
'dns_dmarcruf' => $config{'bind_dmarcruf'},
'dns_dmarcrua' => $config{'bind_dmarcrua'},
'dns_dmarcextra' => $config{'bind_dmarcextra'},
'dns_sub' => $config{'bind_sub'} || "none",
'dns_alias' => $config{'bind_alias'} || "none",
'dns_cloud' => $config{'bind_cloud'},
'dns_cloud_import' => $config{'bind_cloud_import'},
'dns_cloud_proxy' => $config{'bind_cloud_proxy'},
'dns_slaves' => $config{'bind_slaves'},
'dns_master' => $config{'bind_master'} || "none",
'dns_mx' => $config{'bind_mx'} || "none",
'dns_ns' => $config{'dns_ns'},
'dns_prins' => $config{'dns_prins'},
'dns_records' => $config{'dns_records'},
'dns_ttl' => $config{'dns_ttl'},
'dns_indom' => $config{'bind_indom'},
'dnssec' => $config{'dnssec'} || "none",
'dnssec_alg' => $config{'dnssec_alg'},
'dnssec_single' => $config{'dnssec_single'},
'namedconf' => $config{'namedconf'} || "none",
'namedconf_no_allow_transfer' =>
$config{'namedconf_no_allow_transfer'},
'namedconf_no_also_notify' =>
$config{'namedconf_no_also_notify'},
'ftp' => $config{'proftpd_config'},
'ftp_dir' => $config{'ftp_dir'},
'logrotate' => $config{'logrotate_config'} || "none",
'logrotate_files' => $config{'logrotate_files'} || "none",
'logrotate_shared' => $config{'logrotate_shared'} || "no",
'status' => $config{'statusemail'} || "none",
'statusonly' => int($config{'statusonly'}),
'statustimeout' => $config{'statustimeout'},
'statustmpl' => $config{'statustmpl'},
'statussslcert' => $config{'statussslcert'},
'mail_on' => $config{'domain_template'} eq "none" ? "none" : "yes",
'mail' => $config{'domain_template'} eq "none" ||
$config{'domain_template'} eq "" ||
$config{'domain_template'} eq "default" ?
&cat_file("domain-template") :
&cat_file($config{'domain_template'}),
'mail_subject' => $config{'newdom_subject'} ||
&entities_to_ascii($text{'mail_dsubject'}),
'mail_cc' => $config{'newdom_cc'},
'mail_bcc' => $config{'newdom_bcc'},
'mail_cloud' => $config{'mail_cloud'},
'mail_mta_sts' => $config{'mail_mta_sts'},
'newuser_on' => $config{'user_template'} eq "none" ? "none" : "yes",
'newuser' => $config{'user_template'} eq "none" ||
$config{'user_template'} eq "" ||
$config{'user_template'} eq "default" ?
&cat_file("user-template") :
&cat_file($config{'user_template'}),
'newuser_subject' => $config{'newuser_subject'} ||
&entities_to_ascii($text{'mail_usubject'}),
'newuser_cc' => $config{'newuser_cc'},
'newuser_bcc' => $config{'newuser_bcc'},
'newuser_to_mailbox' => $config{'newuser_to_mailbox'} || 0,
'newuser_to_owner' => $config{'newuser_to_owner'} || 0,
'newuser_to_reseller' => $config{'newuser_to_reseller'} || 0,
'updateuser_on' => $config{'update_template'} eq "none" ?
"none" : "yes",
'updateuser' => $config{'update_template'} eq "none" ||
$config{'update_template'} eq "" ||
$config{'update_template'} eq "default" ?
&cat_file("update-template") :
&cat_file($config{'update_template'}),
'updateuser_subject' => $config{'newupdate_subject'} ||
&entities_to_ascii($text{'mail_upsubject'}),
'updateuser_cc' => $config{'newupdate_cc'},
'updateuser_bcc' => $config{'newupdate_bcc'},
'updateuser_to_mailbox' => $config{'newupdate_to_mailbox'} || 0,
'updateuser_to_owner' => $config{'newupdate_to_owner'} || 0,
'updateuser_to_reseller' => $config{'newupdate_to_reseller'} || 0,
'aliascopy' => $config{'aliascopy'} || 0,
'bccto' => $config{'bccto'} || 'none',
'spamclear' => $config{'spamclear'} || 'none',
'trashclear' => $config{'trashclear'} || 'none',
'spamtrap' => $config{'spamtrap'} || 'none',
'defmquota' => $config{'defmquota'} || "none",
'user_aliases' => $config{'newuser_aliases'} || "none",
'dom_aliases' => $config{'newdom_aliases'} || "none",
'dom_aliases_bounce' => int($config{'newdom_alias_bounce'}),
'mysql' => $config{'mysql_db'} || '${PREFIX}',
'mysql_wild' => $config{'mysql_wild'},
'mysql_suffix' => $config{'mysql_suffix'} || "none",
'mysql_hosts' => $config{'mysql_hosts'} || "none",
'mysql_mkdb' => $config{'mysql_mkdb'},
'mysql_nopass' => $config{'mysql_nopass'},
'mysql_nouser' => $config{'mysql_nouser'},
'mysql_chgrp' => $config{'mysql_chgrp'},
'mysql_charset' => $config{'mysql_charset'},
'mysql_collate' => $config{'mysql_collate'},
'mysql_conns' => $config{'mysql_conns'} || "none",
'mysql_uconns' => $config{'mysql_uconns'} || "none",
'postgres_encoding' => $config{'postgres_encoding'} || "none",
'skel' => $config{'virtual_skel'} || "none",
'skel_subs' => int($config{'virtual_skel_subs'}),
'skel_nosubs' => $config{'virtual_skel_nosubs'},
'exclude' => $config{'default_exclude'} || "none",
'frame' => &cat_file("framefwd-template", 1),
'gacl' => 1,
'gacl_umode' => $config{'gacl_umode'},
'gacl_uusers' => $config{'gacl_uusers'},
'gacl_ugroups' => $config{'gacl_ugroups'},
'gacl_groups' => $config{'gacl_groups'},
'gacl_root' => $config{'gacl_root'},
'webmin_group' => $config{'webmin_group'},
'extra_prefix' => $config{'extra_prefix'} || "none",
'ugroup' => $config{'defugroup'} || "none",
'sgroup' => $config{'domains_group'} || "none",
'quota' => $config{'defquota'} || "none",
'uquota' => $config{'defuquota'} || "none",
'ushell' => $config{'defushell'} || "none",
'ujail' => $config{'defujail'},
'mailboxlimit' => $config{'defmailboxlimit'} eq "" ? "none" :
$config{'defmailboxlimit'},
'aliaslimit' => $config{'defaliaslimit'} eq "" ? "none" :
$config{'defaliaslimit'},
'dbslimit' => $config{'defdbslimit'} eq "" ? "none" :
$config{'defdbslimit'},
'domslimit' => $config{'defdomslimit'} eq "" ? 0 :
$config{'defdomslimit'} eq "*" ? "none" :
$config{'defdomslimit'},
'aliasdomslimit' => $config{'defaliasdomslimit'} eq "" ||
$config{'defaliasdomslimit'} eq "*" ? "none" :
$config{'defaliasdomslimit'},
'realdomslimit' => $config{'defrealdomslimit'} eq "" ||
$config{'defrealdomslimit'} eq "*" ? "none" :
$config{'defrealdomslimit'},
'bwlimit' => $config{'defbwlimit'} eq "" ? "none" :
$config{'defbwlimit'},
'mongrelslimit' => $config{'defmongrelslimit'} eq "" ? "none" :
$config{'defmongrelslimit'},
'capabilities' => $config{'defcapabilities'} || "none",
'featurelimits' => $config{'featurelimits'} || "none",
'nodbname' => $config{'defnodbname'},
'norename' => $config{'defnorename'},
'forceunder' => $config{'defforceunder'},
'safeunder' => $config{'defsafeunder'},
'ipfollow' => $config{'defipfollow'},
'resources' => $config{'defresources'} || "none",
'ranges' => $config{'ip_ranges'} || "none",
'ranges6' => $config{'ip_ranges6'} || "none",
'mailgroup' => $config{'mailgroup'} || "none",
'ftpgroup' => $config{'ftpgroup'} || "none",
'dbgroup' => $config{'dbgroup'} || "none",
'othergroups' => $config{'othergroups'} || "none",
'quotatype' => $config{'hard_quotas'} ? "hard" : "soft",
'hashpass' => $config{'hashpass'} || 0,
'hashtypes' => $config{'hashtypes'},
'append_style' => $config{'append_style'},
'domalias' => $config{'domalias'} || "none",
'domalias_type' => $config{'domalias_type'} || 0,
'domauto_disable' => $config{'domauto_disable'},
'for_parent' => 1,
'for_sub' => 0,
'for_alias' => 1,
'for_users' => !$config{'deftmpl_nousers'},
'resellers' => !defined($config{'tmpl_resellers'}) ? "*" :
$config{'tmpl_resellers'},
'owners' => !defined($config{'tmpl_owners'}) ? "*" :
$config{'tmpl_owners'},
'autoconfig' => $config{'tmpl_autoconfig'} || "none",
'outlook_autoconfig' => $config{'tmpl_outlook_autoconfig'} || "none",
'cert_key_tmpl' => $config{'key_tmpl'},
'cert_cert_tmpl' => $config{'cert_tmpl'},
'cert_ca_tmpl' => $config{'ca_tmpl'},
'cert_combined_tmpl' => $config{'combined_tmpl'},
'cert_everything_tmpl' => $config{'everything_tmpl'},
} );
foreach my $w (&list_php_wrapper_templates()) {
$rv[0]->{$w} = $config{$w} || 'none';
}
foreach my $phpver (@all_possible_php_versions) {
$rv[0]->{'web_php_ini_'.$phpver} =
defined($config{'php_ini_'.$phpver}) ?
$config{'php_ini_'.$phpver} : $config{'php_ini'},
}
if (!defined(getpwnam($rv[0]->{'web_user'})) &&
$rv[0]->{'web_user'} ne 'none' &&
$rv[0]->{'web_user'} ne '') {
# Apache user is invalid, due to bad Virtualmin install script. Fix it
$rv[0]->{'web_user'} = &get_apache_user();
}
my @avail;
foreach my $m (&list_domain_owner_modules()) {
push(@avail, $m->[0]."=".$config{'avail_'.$m->[0]});
}
$rv[0]->{'avail'} = join(' ', @avail);
push(@rv, { 'id' => 1,
'name' => $text{'newtmpl_name1'},
'standard' => 1,
'mail_on' => $config{'subdomain_template'} eq "none" ? "none" :
$config{'subdomain_template'} eq "" ? "" : "yes",
'mail' => $config{'subdomain_template'} eq "none" ||
$config{'subdomain_template'} eq "" ||
$config{'subdomain_template'} eq "default" ?
&cat_file("subdomain-template") :
&cat_file($config{'subdomain_template'}),
'mail_subject' => $config{'newsubdom_subject'} ||
&entities_to_ascii($text{'mail_dsubject'}),
'mail_cc' => $config{'newsubdom_cc'},
'mail_bcc' => $config{'newsubdom_bcc'},
'skel' => $config{'sub_skel'} || "none",
'for_parent' => 0,
'for_sub' => 1,
'for_alias' => 0,
'for_users' => !$config{'subtmpl_nousers'},
} );
local $f;
opendir(DIR, $templates_dir);
while(defined($f = readdir(DIR))) {
if ($f ne "." && $f ne "..") {
local %tmpl;
&read_file("$templates_dir/$f", \%tmpl);
$tmpl{'file'} = "$templates_dir/$f";
$tmpl{'mail'} =~ s/\t/\n/g;
$tmpl{'newuser'} =~ s/\t/\n/g;
$tmpl{'updateuser'} =~ s/\t/\n/g;
if ($tmpl{'id'} == 1 || $tmpl{'id'} == 0) {
foreach $k (keys %tmpl) {
$rv[$tmpl{'id'}]->{$k} = $tmpl{$k}
if (!defined($rv[$tmpl{'id'}]->{$k}));
}
}
else {
push(@rv, \%tmpl);
}
foreach my $phpver (@all_possible_php_versions) {
if (!defined($tmpl{'web_php_ini_'.$phpver})) {
$tmpl{'web_php_ini_'.$phpver} =
$tmpl{'web_php_ini'};
}
}
}
}
foreach my $tmpl (@rv) {
$tmpl->{'resellers'} = '*' if (!defined($tmpl->{'resellers'}));
$tmpl->{'owners'} = '*' if (!defined($tmpl->{'owners'}));
if (!defined($tmpl->{'web_cgimode'})) {
# Switch from the old CGI mode field to the new one
my @cgimodes = &has_cgi_support();
$tmpl->{'web_cgimode'} = $tmpl->{'web_fcgiwrap'} ? 'fcgiwrap' :
@cgimodes ? $cgimodes[0] : 'none';
delete($tmpl->{'web_fcgiwrap'});
}
}
closedir(DIR);
my @default_templates = grep { $_->{'default'} } @rv;
my @non_default_templates = grep { !$_->{'default'} } @rv;
my @sorted_templates = sort { $a->{'name'} cmp $b->{'name'} } @non_default_templates;
@list_templates_cache = (@default_templates, @sorted_templates);
return @list_templates_cache;
}
# list_available_templates([&parentdom], [&aliasdom])
# Returns a list of templates for creating a new server, with the given parent
# and alias target domains
sub list_available_templates
{
local ($parentdom, $aliasdom) = @_;
local @rv;
foreach my $t (&list_templates()) {
next if ($t->{'deleted'});
next if (($parentdom && !$aliasdom) && !$t->{'for_sub'});
next if (!$parentdom && !$t->{'for_parent'});
next if (!&master_admin() && !&reseller_admin() && !$t->{'for_users'});
next if ($aliasdom && !$t->{'for_alias'});
next if (!&can_use_template($t));
push(@rv, $t);
}
return @rv;
}
# save_template(&template)
# Create or update a template. If saving the standard template, updates the
# appropriate config options instead of the template file.
sub save_template
{
local ($tmpl) = @_;
local $save_config = 0;
if (!defined($tmpl->{'id'})) {
$tmpl->{'id'} = &domain_id();
}
if ($tmpl->{'id'} == 0) {
# Update appropriate config entries
$config{'deftmpl_nousers'} = !$tmpl->{'for_users'};
if ($tmpl->{'resellers'} eq '*') {
delete($config{'tmpl_resellers'});
}
else {
$config{'tmpl_resellers'} = $tmpl->{'resellers'};
}
if ($tmpl->{'owners'} eq '*') {
delete($config{'tmpl_owners'});
}
else {
$config{'tmpl_owners'} = $tmpl->{'owners'};
}
$config{'apache_config'} = $tmpl->{'web'};
$config{'apache_ssl_config'} = $tmpl->{'web_ssl'};
$config{'web_user'} = $tmpl->{'web_user'};
delete($config{'fcgiwrap'});
$config{'cgimode'} = $tmpl->{'web_cgimode'};
$config{'html_dir'} = $tmpl->{'web_html_dir'};
$config{'html_perms'} = $tmpl->{'web_html_perms'};
$config{'stats_dir'} = $tmpl->{'web_stats_dir'};
$config{'stats_hdir'} = $tmpl->{'web_stats_hdir'};
$config{'stats_pass'} = $tmpl->{'web_stats_pass'};
$config{'stats_noedit'} = $tmpl->{'web_stats_noedit'};
$config{'web_port'} = $tmpl->{'web_port'};
$config{'web_sslport'} = $tmpl->{'web_sslport'};
$config{'web_urlport'} = $tmpl->{'web_urlport'};
$config{'web_urlsslport'} = $tmpl->{'web_urlsslport'};
$config{'web_sslprotos'} = $tmpl->{'web_sslprotos'};
$config{'web_acme'} = $tmpl->{'web_acme'};
$config{'webmin_ssl'} = $tmpl->{'web_webmin_ssl'};
$config{'usermin_ssl'} = $tmpl->{'web_usermin_ssl'};
$config{'web_webmail'} = $tmpl->{'web_webmail'};
$config{'web_webmaildom'} = $tmpl->{'web_webmaildom'};
$config{'web_admin'} = $tmpl->{'web_admin'};
$config{'web_admindom'} = $tmpl->{'web_admindom'};
$config{'web_http2'} = $tmpl->{'web_http2'};
$config{'web_redirects'} = $tmpl->{'web_redirects'};
$config{'auto_redirect'} = $tmpl->{'web_sslredirect'};
$config{'key_size'} = $tmpl->{'ssl_key_size'};
$config{'cert_type'} = $tmpl->{'ssl_cert_type'};
$config{'auto_letsencrypt'} = $tmpl->{'ssl_auto_letsencrypt'};
$config{'letsencrypt_wild'} = $tmpl->{'ssl_letsencrypt_wild'};
$config{'renew_letsencrypt'}= $tmpl->{'ssl_renew_letsencrypt'};
$config{'always_ssl'} = $tmpl->{'ssl_always_ssl'};
$config{'tlsa_records'} = $tmpl->{'ssl_tlsa_records'};
$config{'combined_cert'} = $tmpl->{'ssl_combined_cert'};
$config{'allow_subset'} = $tmpl->{'ssl_allow_subset'};
$config{'ssl_connectivity'} = $tmpl->{'ssl_connectivity'};
$config{'php_vars'} = $tmpl->{'php_vars'} eq "none" ? "" :
$tmpl->{'php_vars'};
$config{'php_fpm'} = $tmpl->{'php_fpm'} eq "none" ? "" :
$tmpl->{'php_fpm'};
$config{'php_sock'} = $tmpl->{'php_sock'};
$config{'php_fpmtype'} = $tmpl->{'php_fpmtype'};
$config{'php_log'} = $tmpl->{'php_log'};
$config{'php_log_path'} = $tmpl->{'php_log_path'};
$config{'php_suexec'} = $tmpl->{'web_php_suexec'};
$config{'ruby_suexec'} = $tmpl->{'web_ruby_suexec'};
$config{'phpver'} = $tmpl->{'web_phpver'};
$config{'phpchildren'} = $tmpl->{'web_phpchildren'};
$config{'web_ssi'} = $tmpl->{'web_ssi'};
$config{'web_ssi_suffix'} = $tmpl->{'web_ssi_suffix'};
$config{'dovecot_ssl'} = $tmpl->{'web_dovecot_ssl'};
$config{'postfix_ssl'} = $tmpl->{'web_postfix_ssl'};
$config{'mysql_ssl'} = $tmpl->{'web_mysql_ssl'};
$config{'proftpd_ssl'} = $tmpl->{'web_proftpd_ssl'};
foreach my $phpver (@all_possible_php_versions) {
$config{'php_ini_'.$phpver} = $tmpl->{'web_php_ini_'.$phpver};
}
delete($config{'php_ini'});
$config{'php_noedit'} = $tmpl->{'web_php_noedit'};
$config{'def_webalizer'} = $tmpl->{'webalizer'} eq "none" ? "" :
$tmpl->{'webalizer'};
$config{'content_web'} = $tmpl->{'content_web'};
delete($config{'content_web_html'});
$config{'content_web_html'} = $tmpl->{'content_web_html'}
if ($config{'content_web'} eq "0");
$config{'disabled_web'} = $tmpl->{'disabled_web'} eq "none" ? "" :
$tmpl->{'disabled_web'};
$config{'disabled_url'} = $tmpl->{'disabled_url'} eq "none" ? "" :
$tmpl->{'disabled_url'};
$config{'alias_mode'} = $tmpl->{'web_alias'};
$config{'bind_config'} = $tmpl->{'dns'} eq "none" ? ""
: $tmpl->{'dns'};
$config{'bind_replace'} = $tmpl->{'dns_replace'};
$config{'bind_spf'} = $tmpl->{'dns_spf'} eq 'none' ? undef
: $tmpl->{'dns_spf'};
$config{'bind_spfhosts'} = $tmpl->{'dns_spfhosts'};
$config{'bind_spfonly'} = $tmpl->{'dns_spfonly'};
$config{'bind_spfincludes'} = $tmpl->{'dns_spfincludes'};
$config{'bind_spfall'} = $tmpl->{'dns_spfall'};
$config{'bind_dmarc'} = $tmpl->{'dns_dmarc'} eq 'none' ?
undef : $tmpl->{'dns_dmarc'};
$config{'bind_dmarcp'} = $tmpl->{'dns_dmarcp'};
$config{'bind_dmarcpct'} = $tmpl->{'dns_dmarcpct'};
$config{'bind_dmarcruf'} = $tmpl->{'dns_dmarcruf'};
$config{'bind_dmarcrua'} = $tmpl->{'dns_dmarcrua'};
$config{'bind_dmarcextra'} = $tmpl->{'dns_dmarcextra'};
$config{'bind_sub'} = $tmpl->{'dns_sub'} eq 'none' ?
undef : $tmpl->{'dns_sub'};
$config{'bind_alias'} = $tmpl->{'dns_alias'} eq 'none' ?
undef : $tmpl->{'dns_alias'};
$config{'bind_cloud'} = $tmpl->{'dns_cloud'};
$config{'bind_slaves'} = $tmpl->{'dns_slaves'};
$config{'bind_cloud_import'} = $tmpl->{'dns_cloud_import'};
$config{'bind_cloud_proxy'} = $tmpl->{'dns_cloud_proxy'};
$config{'bind_master'} = $tmpl->{'dns_master'} eq 'none' ? undef
: $tmpl->{'dns_master'};
$config{'bind_mx'} = $tmpl->{'dns_mx'} eq 'none' ? undef
: $tmpl->{'dns_mx'};
$config{'bind_indom'} = $tmpl->{'dns_indom'};
$config{'dns_view'} = $tmpl->{'dns_view'};
$config{'dns_ns'} = $tmpl->{'dns_ns'};
$config{'dns_prins'} = $tmpl->{'dns_prins'};
$config{'dns_records'} = $tmpl->{'dns_records'};
$config{'dns_ttl'} = $tmpl->{'dns_ttl'};
$config{'namedconf'} = $tmpl->{'namedconf'} eq 'none' ? undef :
$tmpl->{'namedconf'};
$config{'namedconf_no_also_notify'} =
$tmpl->{'namedconf_no_also_notify'};
$config{'namedconf_no_allow_transfer'} =
$tmpl->{'namedconf_no_allow_transfer'};
$config{'dnssec'} = $tmpl->{'dnssec'} eq 'none' ? undef
: $tmpl->{'dnssec'};
$config{'dnssec_alg'} = $tmpl->{'dnssec_alg'};
$config{'dnssec_single'} = $tmpl->{'dnssec_single'};
delete($config{'mx_server'});
$config{'proftpd_config'} = $tmpl->{'ftp'};
$config{'ftp_dir'} = $tmpl->{'ftp_dir'};
$config{'logrotate_config'} = $tmpl->{'logrotate'} eq "none" ?
"" : $tmpl->{'logrotate'};
$config{'logrotate_files'} = $tmpl->{'logrotate_files'} eq "none" ?
"" : $tmpl->{'logrotate_files'};
$config{'logrotate_shared'} = $tmpl->{'logrotate_shared'};
$config{'statusemail'} = $tmpl->{'status'} eq 'none' ?
'' : $tmpl->{'status'};
$config{'statusonly'} = $tmpl->{'statusonly'};
$config{'statustimeout'} = $tmpl->{'statustimeout'};
$config{'statustmpl'} = $tmpl->{'statustmpl'};
$config{'statussslcert'} = $tmpl->{'statussslcert'};
if ($tmpl->{'mail_on'} eq 'none') {
# Don't send
$config{'domain_template'} = 'none';
}
elsif ($config{'domain_template'} eq 'none') {
# Sending, but need to set a valid mail file
$config{'domain_template'} = 'default';
}
# Write message to default template file, or custom if set
&uncat_file($config{'domain_template'} eq "none" ||
$config{'domain_template'} eq "" ||
$config{'domain_template'} eq "default" ?
"domain-template" :
$config{'domain_template'}, $tmpl->{'mail'});
$config{'newdom_subject'} = $tmpl->{'mail_subject'};
$config{'newdom_cc'} = $tmpl->{'mail_cc'};
$config{'newdom_bcc'} = $tmpl->{'mail_bcc'};
if ($tmpl->{'newuser_on'} eq 'none') {
$config{'user_template'} = 'none';
}
elsif ($config{'user_template'} eq 'none') {
$config{'user_template'} = 'default';
}
&uncat_file($config{'user_template'} eq "none" ||
$config{'user_template'} eq "" ||
$config{'user_template'} eq "default" ?
"user-template" :
$config{'user_template'}, $tmpl->{'newuser'});
$config{'newuser_subject'} = $tmpl->{'newuser_subject'};
$config{'newuser_cc'} = $tmpl->{'newuser_cc'};
$config{'newuser_bcc'} = $tmpl->{'newuser_bcc'};
$config{'newuser_to_mailbox'} = $tmpl->{'newuser_to_mailbox'};
$config{'newuser_to_owner'} = $tmpl->{'newuser_to_owner'};
$config{'newuser_to_reseller'} = $tmpl->{'newuser_to_reseller'};
if ($tmpl->{'updateuser_on'} eq 'none') {
$config{'update_template'} = 'none';
}
elsif ($config{'user_template'} eq 'none') {
$config{'update_template'} = 'default';
}
&uncat_file($config{'update_template'} eq "none" ||
$config{'update_template'} eq "" ||
$config{'update_template'} eq "default" ?
"update-template" :
$config{'update_template'}, $tmpl->{'updateuser'});
$config{'newupdate_subject'} = $tmpl->{'updateuser_subject'};
$config{'newupdate_cc'} = $tmpl->{'updateuser_cc'};
$config{'newupdate_bcc'} = $tmpl->{'updateuser_bcc'};
$config{'newupdate_to_mailbox'} = $tmpl->{'updateuser_to_mailbox'};
$config{'newupdate_to_owner'} = $tmpl->{'updateuser_to_owner'};
$config{'newupdate_to_reseller'} = $tmpl->{'updateuser_to_reseller'};
$config{'mail_cloud'} = $tmpl->{'mail_cloud'};
$config{'mail_mta_sts'} = $tmpl->{'mail_mta_sts'};
$config{'aliascopy'} = $tmpl->{'aliascopy'};
$config{'bccto'} = $tmpl->{'bccto'};
$config{'spamclear'} = $tmpl->{'spamclear'};
$config{'trashclear'} = $tmpl->{'trashclear'};
$config{'spamtrap'} = $tmpl->{'spamtrap'};
$config{'defmquota'} = $tmpl->{'defmquota'} eq "none" ?
"" : $tmpl->{'defmquota'};
$config{'newuser_aliases'} = $tmpl->{'user_aliases'} eq "none" ?
"" : $tmpl->{'user_aliases'};
$config{'newdom_aliases'} = $tmpl->{'dom_aliases'} eq "none" ?
"" : $tmpl->{'dom_aliases'};
$config{'newdom_alias_bounce'} = $tmpl->{'dom_aliases_bounce'};
$config{'mysql_db'} = $tmpl->{'mysql'};
$config{'mysql_wild'} = $tmpl->{'mysql_wild'};
$config{'mysql_hosts'} = $tmpl->{'mysql_hosts'} eq "none" ?
"" : $tmpl->{'mysql_hosts'};
$config{'mysql_suffix'} = $tmpl->{'mysql_suffix'} eq "none" ?
"" : $tmpl->{'mysql_suffix'};
$config{'mysql_mkdb'} = $tmpl->{'mysql_mkdb'};
$config{'mysql_nopass'} = $tmpl->{'mysql_nopass'};
$config{'mysql_nouser'} = $tmpl->{'mysql_nouser'};
$config{'mysql_chgrp'} = $tmpl->{'mysql_chgrp'};
$config{'mysql_charset'} = $tmpl->{'mysql_charset'};
$config{'mysql_collate'} = $tmpl->{'mysql_collate'};
$config{'mysql_conns'} = $tmpl->{'mysql_conns'};
$config{'mysql_uconns'} = $tmpl->{'mysql_uconns'};
$config{'postgres_encoding'} = $tmpl->{'postgres_encoding'};
$config{'virtual_skel'} = $tmpl->{'skel'} eq "none" ? "" :
$tmpl->{'skel'};
$config{'default_exclude'} = $tmpl->{'exclude'} eq "none" ? "" :
$tmpl->{'exclude'};
$config{'virtual_skel_subs'} = $tmpl->{'skel_subs'};
$config{'virtual_skel_nosubs'} = $tmpl->{'skel_nosubs'};
$config{'gacl_umode'} = $tmpl->{'gacl_umode'};
$config{'gacl_ugroups'} = $tmpl->{'gacl_ugroups'};
$config{'gacl_users'} = $tmpl->{'gacl_users'};
$config{'gacl_groups'} = $tmpl->{'gacl_groups'};
$config{'gacl_root'} = $tmpl->{'gacl_root'};
$config{'webmin_group'} = $tmpl->{'webmin_group'};
$config{'extra_prefix'} = $tmpl->{'extra_prefix'} eq "none" ? "" :
$tmpl->{'extra_prefix'};
$config{'defugroup'} = $tmpl->{'ugroup'};
$config{'domains_group'} = $tmpl->{'sgroup'} eq "none" ? "" :
$tmpl->{'sgroup'};
$config{'defquota'} = $tmpl->{'quota'};
$config{'defuquota'} = $tmpl->{'uquota'};
$config{'defushell'} = $tmpl->{'ushell'};
$config{'defujail'} = $tmpl->{'ujail'};
$config{'defmailboxlimit'} = $tmpl->{'mailboxlimit'} eq 'none' ? undef :
$tmpl->{'mailboxlimit'};
$config{'defaliaslimit'} = $tmpl->{'aliaslimit'} eq 'none' ? undef :
$tmpl->{'aliaslimit'};
$config{'defdbslimit'} = $tmpl->{'dbslimit'} eq 'none' ? undef :
$tmpl->{'dbslimit'};
$config{'defdomslimit'} = $tmpl->{'domslimit'} eq 'none' ? "*" :
$tmpl->{'domslimit'} eq '0' ? "" :
$tmpl->{'domslimit'};
$config{'defaliasdomslimit'} = $tmpl->{'aliasdomslimit'} eq 'none' ?
"*" : $tmpl->{'aliasdomslimit'};
$config{'defrealdomslimit'} = $tmpl->{'realdomslimit'} eq 'none' ?
"*" : $tmpl->{'realdomslimit'};
$config{'defbwlimit'} = $tmpl->{'bwlimit'} eq 'none' ? undef :
$tmpl->{'bwlimit'};
$config{'defmongrelslimit'} = $tmpl->{'mongrelslimit'} eq 'none' ?
undef : $tmpl->{'mongrelslimit'};
$config{'defcapabilities'} = $tmpl->{'capabilities'};
$config{'featurelimits'} = $tmpl->{'featurelimits'};
$config{'defnodbname'} = $tmpl->{'nodbname'};
$config{'defnorename'} = $tmpl->{'norename'};
$config{'defforceunder'} = $tmpl->{'forceunder'};
$config{'defsafeunder'} = $tmpl->{'safeunder'};
$config{'defipfollow'} = $tmpl->{'ipfollow'};
$config{'defresources'} = $tmpl->{'resources'};
&uncat_file("framefwd-template", $tmpl->{'frame'}, 1);
$config{'ip_ranges'} = $tmpl->{'ranges'} eq 'none' ? undef :
$tmpl->{'ranges'};
$config{'ip_ranges6'} = $tmpl->{'ranges6'} eq 'none' ? undef :
$tmpl->{'ranges6'};
$config{'mailgroup'} = $tmpl->{'mailgroup'} eq 'none' ? undef :
$tmpl->{'mailgroup'};
$config{'ftpgroup'} = $tmpl->{'ftpgroup'} eq 'none' ? undef :
$tmpl->{'ftpgroup'};
$config{'dbgroup'} = $tmpl->{'dbgroup'} eq 'none' ? undef :
$tmpl->{'dbgroup'};
$config{'othergroups'} = $tmpl->{'othergroups'} eq 'none' ? undef :
$tmpl->{'othergroups'};
$config{'hard_quotas'} = $tmpl->{'quotatype'} eq "hard" ? 1 : 0;
$config{'hashpass'} = $tmpl->{'hashpass'};
$config{'hashtypes'} = $tmpl->{'hashtypes'};
$config{'append_style'} = $tmpl->{'append_style'};
$config{'domalias'} = $tmpl->{'domalias'} eq 'none' ? undef :
$tmpl->{'domalias'};
$config{'domalias_type'} = $tmpl->{'domalias_type'};
$config{'domauto_disable'} = $tmpl->{'domauto_disable'};
$config{'tmpl_autoconfig'} = $tmpl->{'autoconfig'};
$config{'tmpl_outlook_autoconfig'} = $tmpl->{'outlook_autoconfig'};
$config{'key_tmpl'} = $tmpl->{'cert_key_tmpl'};
$config{'cert_tmpl'} = $tmpl->{'cert_cert_tmpl'};
$config{'ca_tmpl'} = $tmpl->{'cert_ca_tmpl'};
$config{'combined_tmpl'} = $tmpl->{'cert_combined_tmpl'};
$config{'everything_tmpl'} = $tmpl->{'cert_everything_tmpl'};
foreach my $w (&list_php_wrapper_templates()) {
$config{$w} = $tmpl->{$w};
}
my %avail = map { split(/=/, $_) } split(/\s+/, $tmpl->{'avail'});
foreach my $m (&list_domain_owner_modules()) {
$config{'avail_'.$m->[0]} = $avail{$m->[0]} || 0;
}
$save_config = 1;
}
elsif ($tmpl->{'id'} == 1) {
# For the default for sub-servers, update mail and skel in config only
$config{'subtmpl_nousers'} = !$tmpl->{'for_users'};
if ($tmpl->{'mail_on'} eq 'none') {
# Don't send
$config{'subdomain_template'} = 'none';
}
elsif ($tmpl->{'mail_on'} eq '') {
# Use default message (for top-level servers)
$config{'subdomain_template'} = '';
}
else {
# Sending, but need to set a valid mail file
if ($config{'subdomain_template'} eq 'none') {
$config{'subdomain_template'} = 'default';
}
}
&uncat_file($config{'subdomain_template'} eq "none" ||
$config{'subdomain_template'} eq "" ||
$config{'subdomain_template'} eq "default" ?
"subdomain-template" :
$config{'subdomain_template'}, $tmpl->{'mail'});
$config{'newsubdom_subject'} = $tmpl->{'mail_subject'};
$config{'newsubdom_cc'} = $tmpl->{'mail_cc'};
$config{'newsubdom_bcc'} = $tmpl->{'mail_bcc'};
$config{'sub_skel'} = $tmpl->{'skel'} eq "none" ? "" :
$tmpl->{'skel'};
$save_config = 1;
}
if ($tmpl->{'id'} != 0) {
# Just save the entire template to a file
&make_dir($templates_dir, 0700);
$tmpl->{'created'} ||= time();
$tmpl->{'mail'} =~ s/\n/\t/g;
$tmpl->{'newuser'} =~ s/\n/\t/g;
$tmpl->{'updateuser'} =~ s/\n/\t/g;
&lock_file("$templates_dir/$tmpl->{'id'}");
&write_file("$templates_dir/$tmpl->{'id'}", $tmpl);
&unlock_file("$templates_dir/$tmpl->{'id'}");
}
else {
# Only plugin-specific options go to a file
&make_dir($templates_dir, 0700);
&lock_file("$templates_dir/$tmpl->{'id'}");
&read_file("$templates_dir/$tmpl->{'id'}", \%ptmpl);
local %ptmpl;
foreach my $p (@plugins) {
foreach my $k (keys %$tmpl) {
if ($k =~ /^\Q$p\E/) {
$ptmpl{$k} = $tmpl->{$k};
}
}
}
&write_file("$templates_dir/$tmpl->{'id'}", \%ptmpl);
&unlock_file("$templates_dir/$tmpl->{'id'}");
}
if ($save_config) {
&lock_file($module_config_file);
$config{'last_check'} = time()+1;
&write_file($module_config_file, \%config);
&unlock_file($module_config_file);
}
undef(@list_templates_cache);
}
# get_template(id, [template-overrides])
# Returns a template, with any default settings filled in from real default
sub get_template
{
my ($tmplid) = @_;
local @tmpls = &list_templates();
local ($tmpl) = grep { $_->{'id'} == $tmplid } @tmpls;
return undef if (!$tmpl); # not found
if (!$tmpl->{'default'}) {
local $def = $tmpls[0];
local $p;
local %done;
foreach $p ("dns_spf", "dns_sub", "dns_master", "dns_mx", "dns_dmarc",
"dns_cloud", "dns_slaves", "dns_prins", "dns_alias",
"web_acme", "web_webmail", "web_admin", "web_http2",
"web_redirects", "web_sslredirect", "web_php",
"web", "dns", "ftp", "mail", "frame", "user_aliases",
"ugroup", "sgroup", "quota", "uquota", "ushell", "ujail",
"mailboxlimit", "domslimit",
"dbslimit", "aliaslimit", "bwlimit", "mongrelslimit","skel",
"mysql_hosts", "mysql_mkdb", "mysql_suffix", "mysql_chgrp",
"mysql_nopass", "mysql_wild", "mysql_charset", "mysql",
"mysql_nouser", "postgres_encoding", "webalizer",
"dom_aliases", "ranges", "ranges6",
"mailgroup", "ftpgroup", "dbgroup",
"othergroups", "defmquota", "quotatype", "append_style",
"domalias", "logrotate_files", "logrotate_shared",
"logrotate", "disabled_web", "disabled_url", "php_sock",
"php_fpmtype", "php_fpm", "php_log", "php", "newuser",
"updateuser", "content_web",
"status", "extra_prefix", "capabilities",
"webmin_group", "spamclear", "trashclear",
"spamtrap", "namedconf",
"nodbname", "norename", "forceunder", "safeunder",
"ipfollow", "exclude", "cert_key_tmpl", "cert_cert_tmpl",
"cert_ca_tmpl", "cert_combined_tmpl","cert_everything_tmpl",
"ssl_key_size", "ssl_cert_type", "ssl_auto_letsencrypt",
"ssl_letsencrypt_wild", "ssl_always_ssl",
"ssl_tlsa_records", "ssl_combined_cert",
"ssl_renew_letsencrypt", "ssl_allow_subset",
"ssl_connectivity",
"aliascopy", "bccto", "resources", "dnssec", "avail",
@plugins,
&list_php_wrapper_templates(),
"capabilities",
"featurelimits",
"hashpass", "hashtypes", "autoconfig", "outlook_autoconfig",
(map { $_."limit", $_."server", $_."master", $_."view",
$_."passwd" } @plugins)) {
my $psel = $p eq "web_php" ? "web_php_suexec" :
$p eq "mail" ? "mail_on" : $p;
if ($tmpl->{$psel} eq "") {
local $k;
foreach $k (keys %$def) {
next if ($p eq "dns" &&
$k =~ /^dns_(spf|cloud|slaves|prins|alias)/);
next if ($p eq "php" &&
$k =~ /^php_(fpm|fpmtype|sock)/);
next if ($p eq "web" &&
$k =~ /^web_(webmail|admin|http2|redirects|sslredirect|php|acme)/);
next if ($p eq "mysql" &&
$k =~ /^mysql_(hosts|mkdb|suffix|chgrp|nopass|wild|charset|nouser)/);
next if ($done{$k});
if ($k =~ /^\Q$p\E_/ ||
$k eq $psel || $k eq $p) {
# If the selector is named like 'web',
# the template inherits default values
# like 'web' or 'web_foo'
$tmpl->{$k} = $def->{$k};
$done{$k}++;
}
elsif ($p =~ /^(dns_spf)$/ &&
$k =~ /^\Q$p\E/) {
# If the selector is named like 'dns_spf'
# the template inherits default values
# like 'dns_spfall'
$tmpl->{$k} = $def->{$k};
$done{$k}++;
}
}
}
}
# The ruby setting needs to default to -1 if the web section is defined
# in this template, but we are using the GPL release
$tmpl->{'web_ruby_suexec'} = -1 if ($tmpl->{'web_ruby_suexec'} eq '');
}
return $tmpl;
}
# delete_template(&template)
# If this template is used by any domains, just mark it as deleted.
# Otherwise, really delete it.
sub delete_template
{
local %tmpl;
&lock_file("$templates_dir/$_[0]->{'id'}");
local @users = &get_domain_by("template", $_[0]->{'id'});
if (@users) {
&read_file("$templates_dir/$_[0]->{'id'}", \%tmpl);
$tmpl{'deleted'} = 1;
&write_file("$templates_dir/$_[0]->{'id'}", \%tmpl);
}
else {
&unlink_file("$templates_dir/$_[0]->{'id'}");
}
&unlock_file("$templates_dir/$_[0]->{'id'}");
}
# list_template_scripts(&template)
# Returns a list of scripts specified for this template. May return "none"
# if there are none.
sub list_template_scripts
{
local ($tmpl) = @_;
return "none" if ($tmpl->{'noscripts'});
local @rv;
opendir(DIR, $template_scripts_dir);
foreach my $f (readdir(DIR)) {
if ($f =~ /^(\d+)_(\d+)$/ && $1 == $tmpl->{'id'}) {
local %script;
&read_file("$template_scripts_dir/$f", \%script);
$script{'id'} = $2;
$script{'file'} = "$template_scripts_dir/$f";
push(@rv, \%script);
}
}
closedir(DIR);
return \@rv;
}
# save_template_scripts(&template, &scripts|"none")
# Updates the scripts for some template
sub save_template_scripts
{
local ($tmpl, $scripts) = @_;
# Delete old scripts
opendir(DIR, $template_scripts_dir);
foreach my $f (readdir(DIR)) {
if ($f =~ /^(\d+)_(\d+)$/ && $1 == $tmpl->{'id'}) {
unlink("$template_scripts_dir/$f");
}
}
closedir(DIR);
if ($scripts eq "none") {
$tmpl->{'noscripts'} = 1;
}
else {
# Save new scripts
mkdir($template_scripts_dir, 0700);
foreach my $script (@$scripts) {
&write_file("$template_scripts_dir/$tmpl->{'id'}_$script->{'id'}", $script);
}
$tmpl->{'noscripts'} = 0;
}
&save_template($tmpl);
}
# get_template_scripts(&template)
# Returns the actual scripts that should be installed when a domain is setup
# using this template, taking defaults into account
sub get_template_scripts
{
local ($tmpl) = @_;
local $scripts = &list_template_scripts($tmpl);
if ($scripts eq "none") {
return ( );
}
elsif (@$scripts || $tmpl->{'default'}) {
return @$scripts;
}
else {
# Fall back to default
local @tmpls = &list_templates();
local $def = $tmpls[0];
return &get_template_scripts($def);
}
}
# cat_file(file, [newlines-to-tabs])
# Returns the contents of some file
sub cat_file
{
local ($file, $tabs) = @_;
local $path = $file =~ /^\// ? $file : "$module_config_directory/$file";
local $rv = &read_file_contents($path);
if ($tabs) {
$rv =~ s/\r//g;
$rv =~ s/\n/\t/g;
}
return $rv;
}
# uncat_file(file, data, [tabs-to-newlines])
# Writes to some file
sub uncat_file
{
local ($file, $data, $tabs) = @_;
if ($tabs) {
$data =~ s/\t/\n/g;
}
local $path = $file =~ /^\// ? $file : "$module_config_directory/$file";
&open_lock_tempfile(FILE, ">$path");
&print_tempfile(FILE, $data);
&close_tempfile(FILE);
}
# uncat_transname(data)
# Creates a temp file and writes some data to it
sub uncat_transname
{
local ($data, $tabs) = @_;
local $temp = &transname();
&uncat_file($temp, $data, $tabs);
return $temp;
}
# plugin_call(module, function, [arg, ...])
# If some plugin function is defined, call it and return the result,
# otherwise return undef
sub plugin_call
{
local ($mod, $func, @args) = @_;
&load_plugin_libraries($mod);
if (&plugin_defined($mod, $func)) {
if ($main::module_name ne "virtual_server") {
# Set up virtual_server package
&foreign_require("virtual-server", "virtual-server-lib.pl");
$virtual_server::first_print = $first_print;
$virtual_server::second_print = $second_print;
$virtual_server::indent_print = $indent_print;
$virtual_server::outdent_print = $outdent_print;
}
return &foreign_call($mod, $func, @args);
}
else {
return wantarray ? ( ) : undef;
}
}
# try_plugin_call(module, function, [arg, ...])
# Like plugin_call, but catches and prints errors
sub try_plugin_call
{
local ($mod, $func, @args) = @_;
local $main::error_must_die = 1;
eval { &plugin_call($mod, $func, @args) };
if ($@) {
my $fn = &plugin_call($mod, "feature_name");
&$second_print(&text('setup_failure', $fn, "$@"));
return 0;
}
return 1;
}
# plugin_defined(module, function)
# Returns 1 if some function is defined in a plugin
sub plugin_defined
{
local ($mod, $func) = @_;
&load_plugin_libraries($mod);
$mod =~ s/[^A-Za-z0-9]/_/g;
local $func = "${mod}::$func";
return defined(&$func);
}
# database_feature([&domain])
# Returns 1 if any feature that uses a database is enabled (perhaps in a domain)
sub database_feature
{
local ($d) = @_;
foreach my $f ('mysql', 'postgres') {
return 1 if ($config{$f} && (!$d || $d->{$f}));
}
foreach my $f (&list_database_plugins()) {
return 1 if ($config{$f} && (!$d || $d->{$f}));
}
return 0;
}
# list_custom_fields()
# Returns a list of structures containing custom field details. Each has keys :
# name - Unique name for this field
# type - 0=textbox, 1=unix user, 2=unix UID, 3=unix group, 4=unix GID,
# 5=file chooser, 6=directory chooser, 7=yes/no, 8=password,
# 9=options file, 10=text area
# opts - Name of options file
# desc - Human-readable description, with optional ; separated tooltip
# show - 1=show in list of domains, 0=hide
# visible - 0=anyone can edit
# 1=root can edit, others can view
# 2=only root can see
sub list_custom_fields
{
local @rv;
local $_;
open(FIELDS, "<".$custom_fields_file);
while() {
s/\r|\n//g;
local @a = split(/:/, $_, 6);
push(@rv, { 'name' => $a[0],
'type' => $a[1],
'opts' => $a[2],
'desc' => $a[3],
'show' => $a[4],
'visible' => $a[5], });
}
close(FIELDS);
return @rv;
}
# save_custom_fields(&fields)
sub save_custom_fields
{
&open_lock_tempfile(FIELDS, ">$custom_fields_file");
foreach my $a (@{$_[0]}) {
&print_tempfile(FIELDS, $a->{'name'},":",$a->{'type'},":",
$a->{'opts'},":",$a->{'desc'},":",$a->{'show'},":",
$a->{'visible'},"\n");
}
&close_tempfile(FIELDS);
}
# list_custom_links()
# Returns a list of structures containing custom link details
sub list_custom_links
{
local @rv;
local $_;
open(LINKS, "<".$custom_links_file);
while() {
s/\r|\n//g;
local @a = split(/\t/, $_);
push(@rv, { 'desc' => $a[0],
'url' => $a[1],
'who' => { map { $_ => 1 } split(/:/, $a[2]) },
'open' => $a[3],
'cat' => $a[4],
'tmpl' => $a[5] eq '-' ? undef : $a[5],
'feature' => $a[6] eq '-' ? undef : $a[6],
});
}
close(LINKS);
return @rv;
}
# save_custom_links(&links)
# Write out the given list of custom links to a file
sub save_custom_links
{
&open_lock_tempfile(LINKS, ">$custom_links_file");
foreach my $a (@{$_[0]}) {
&print_tempfile(LINKS,
$a->{'desc'}."\t".$a->{'url'}."\t".
join(":", keys %{$a->{'who'}})."\t".
int($a->{'open'})."\t".$a->{'cat'}."\t".
($a->{'tmpl'} eq "" ? "-" : $a->{'tmpl'})."\t".
($a->{'feature'} eq "" ? "-" : $a->{'feature'})."\t".
"\n");
}
&close_tempfile(LINKS);
}
# list_custom_link_categories()
# Returns a list of all custom link category hash refs
sub list_custom_link_categories
{
local @rv;
open(LINKCATS, "<".$custom_link_categories_file);
while() {
s/\r|\n//g;
local @a = split(/\t/, $_);
push(@rv, { 'id' => $a[0], 'desc' => $a[1] });
}
close(LINKCATS);
return @rv;
}
# save_custom_link_categories(&cats)
# Write out the given list of link categories to a file
sub save_custom_link_categories
{
&open_lock_tempfile(LINKCATS, ">$custom_link_categories_file");
foreach my $a (@{$_[0]}) {
&print_tempfile(LINKCATS, $a->{'id'}."\t".$a->{'desc'}."\n");
}
&close_tempfile(LINKCATS);
}
# list_visible_custom_links(&domain)
# Returns a list of descriptions and URLs for custom links in the given domain,
# for the current user type. Category names are also include.
sub list_visible_custom_links
{
local ($d) = @_;
local @rv;
local $me = &master_admin() ? 'master' :
&reseller_admin() ? 'reseller' : 'domain';
local %cats = map { $_->{'id'}, $_->{'desc'} } &list_custom_link_categories();
foreach my $l (&list_custom_links()) {
if (!$l->{'who'}->{$me}) {
# Not for you
next;
}
if ($l->{'tmpl'} && $d->{'template'} ne $l->{'tmpl'}) {
# Not for this domain template
next;
}
if ($l->{'feature'} && !$d->{$l->{'feature'}}) {
# Not for this domain feature
next;
}
local $nl = {
'desc' => &substitute_domain_template($l->{'desc'}, $d),
'url' => &substitute_domain_template($l->{'url'}, $d),
'open' => $l->{'open'},
'catname' => $cats{$l->{'cat'}},
'cat' => $l->{'cat'},
};
if ($nl->{'desc'} && $nl->{'url'}) {
push(@rv, $nl);
}
}
return @rv;
}
# show_custom_fields([&domain], [&tds])
# Returns HTML for custom field inputs, for inclusion in a table
sub show_custom_fields
{
local ($d, $tds) = @_;
local $rv;
local $col = 0;
foreach my $f (&list_custom_fields()) {
local ($desc, $tip) = split(/;/, $f->{'desc'}, 2);
$desc = &html_escape($desc);
if ($tip) {
$desc = "$desc
";
}
if ($f->{'visible'} == 0 || &master_admin()) {
# Can edit
local $n = "field_".$f->{'name'};
local $v = $d ? $d->{"field_".$f->{'name'}} :
$f->{'type'} == 7 ? 1 :
$f->{'type'} == 11 ? 0 : undef;
local $fv;
if ($f->{'type'} == 0) {
local $sz = $f->{'opts'} || 30;
$fv = &ui_textbox($n, $v, $sz);
}
elsif ($f->{'type'} == 1 || $f->{'type'} == 2) {
$fv = &ui_user_textbox($n, $v);
}
elsif ($f->{'type'} == 3 || $f->{'type'} == 4) {
$fv = &ui_group_textbox($n, $v);
}
elsif ($f->{'type'} == 5 || $f->{'type'} == 6) {
$fv = &ui_textbox($n, $v, 30)." ".
&file_chooser_button($n, $f->{'type'}-5);
}
elsif ($f->{'type'} == 7 || $f->{'type'} == 11) {
$fv = &ui_radio($n, $v ? 1 : 0, [ [ 1, $text{'yes'} ],
[ 0, $text{'no'} ] ]);
}
elsif ($f->{'type'} == 8) {
local $sz = $f->{'opts'} || 30;
$fv = &ui_password($n, $v, $sz);
}
elsif ($f->{'type'} == 9) {
local @opts = &read_opts_file($f->{'opts'});
local ($found) = grep { $_->[0] eq $v } @opts;
push(@opts, [ $v, $v ]) if (!$found && $v ne '');
$fv = &ui_select($n, $v, \@opts);
}
elsif ($f->{'type'} == 10) {
local ($w, $h) = split(/\s+/, $f->{'opts'});
$h ||= 4;
$w ||= 30;
$v =~ s/\t/\n/g;
$fv = &ui_textarea($n, $v, $h, $w);
}
$rv .= &ui_table_row($desc, $fv, 1, $tds);
}
elsif ($f->{'visible'} == 1 && $d) {
# Can only see
local $fv = $d->{"field_".$f->{'name'}};
$rv .= &ui_table_row($desc, $fv, 1, $tds);
}
}
return $rv;
}
# parse_custom_fields(&domain, &in)
# Updates a domain with custom fields
sub parse_custom_fields
{
local %in = %{$_[1]};
foreach my $f (&list_custom_fields()) {
next if ($f->{'visible'} != 0 && !&master_admin());
local $n = "field_".$f->{'name'};
local $rv;
if ($f->{'type'} == 0 || $f->{'type'} == 5 ||
$f->{'type'} == 6 || $f->{'type'} == 8) {
$rv = $in{$n};
}
elsif ($f->{'type'} == 10) {
$rv = $in{$n};
$rv =~ s/\r//g;
$rv =~ s/\n/\t/g;
}
elsif ($f->{'type'} == 1 || $f->{'type'} == 2) {
local @u = getpwnam($in{$n});
$rv = $f->{'type'} == 1 ? $in{$n} : $u[2];
}
elsif ($f->{'type'} == 3 || $f->{'type'} == 4) {
local @g = getgrnam($in{$n});
$rv = $f->{'type'} == 3 ? $in{$n} : $g[2];
}
elsif ($f->{'type'} == 7 || $f->{'type'} == 11) {
$rv = $in{$n} ? $f->{'opts'} : "";
}
elsif ($f->{'type'} == 9) {
$rv = $in{$n};
}
$_[0]->{"field_".$f->{'name'}} = $rv;
}
}
# read_opts_file(file)
sub read_opts_file
{
local @rv;
local $file = $_[0];
if ($file !~ /^\//) {
local @uinfo = getpwnam($remote_user);
if (@uinfo) {
$file = "$uinfo[7]/$file";
}
}
local $_;
open(FILE, "<".$file);
while() {
s/\r|\n//g;
if (/^"([^"]*)"\s+"([^"]*)"$/) {
push(@rv, [ $1, $2 ]);
}
elsif (/^"([^"]*)"$/) {
push(@rv, [ $1, $1 ]);
}
elsif (/^(\S+)\s+(\S.*)/) {
push(@rv, [ $1, $2 ]);
}
else {
push(@rv, [ $_, $_ ]);
}
}
close(FILE);
return @rv;
}
# create_initial_user(&dom, [no-template], [for-web])
# Returns a structure for a new mailbox user
sub create_initial_user
{
my ($d, $notmpl, $forweb) = @_;
my $user = { 'unix' => 1 };
if ($d && !$notmpl) {
# Initial aliases and quota come from template
local $tmpl = &get_template($d->{'template'});
if ($tmpl->{'user_aliases'} ne 'none') {
$user->{'to'} = [ map { &substitute_domain_template($_, $d) }
split(/\t+/, $tmpl->{'user_aliases'}) ];
}
if (&has_home_quotas()) {
$user->{'quota'} = $tmpl->{'defmquota'};
}
if (&has_mail_quotas()) {
$user->{'mquota'} = $tmpl->{'defmquota'};
}
}
if (!$user->{'noprimary'}) {
$user->{'email'} = !$d ? "newuser\@".&get_system_hostname() :
$d->{'mail'} ? "newuser\@$d->{'dom'}" : undef;
}
$user->{'secs'} = [ ];
$user->{'shell'} = &default_available_shell('mailbox');
# Merge in configurable initial user settings
if ($d) {
local %init;
&read_file("$initial_users_dir/$d->{'id'}", \%init);
foreach my $a ("email", "quota", "mquota", "shell") {
$user->{$a} = $init{$a} if (defined($init{$a}) &&
$init{$a} eq "none");
}
foreach my $a ("secs", "to") {
if (defined($init{$a})) {
$user->{$a} = [ split(/\t+/, $init{$a}) ];
}
}
if (defined($init{'dbs'})) {
local ($db, @dbs);
foreach $db (split(/\t+/, $init{'dbs'})) {
local ($type, $name) = split(/_/, $db, 2);
push(@dbs, { 'type' => $type,
'name' => $name,
'desc' => $text{'databases_'.$type} });
}
$user->{'dbs'} = \@dbs;
}
}
if ($forweb) {
# This is a website management user
local (undef, $ftp_shell, undef, $def_shell) =
&get_common_available_shells();
$user->{'webowner'} = 1;
$user->{'fixedhome'} = 0;
$user->{'home'} = &public_html_dir($d);
$user->{'noquota'} = 1;
$user->{'noprimary'} = 1;
$user->{'noextra'} = 1;
$user->{'noalias'} = 1;
$user->{'nocreatehome'} = 1;
$user->{'nomailfile'} = 1;
$user->{'shell'} = $ftp_shell ? $ftp_shell->{'shell'}
: $def_shell->{'shell'};
delete($user->{'email'});
}
# For a unix user, apply default password expiry restrictions
if ($gconfig{'os_type'} =~ /-linux$/) {
&require_useradmin();
local %uconfig = &foreign_config("useradmin");
if ($usermodule eq "ldap-useradmin") {
local %lconfig = &foreign_config($usermodule);
foreach my $k (keys %lconfig) {
if ($lconfig{$k} ne "") {
$uconfig{$k} = $lconfig{$k};
}
}
}
$user->{'min'} = $uconfig{'default_min'};
$user->{'max'} = $uconfig{'default_max'};
$user->{'warn'} = $uconfig{'default_warn'};
}
return $user;
}
# save_initial_user(&user, &domain)
# Saves default settings for new users in a virtual server
sub save_initial_user
{
local ($user, $dom) = @_;
if (!-d $initial_users_dir) {
mkdir($initial_users_dir, 0700);
}
&lock_file("$initial_users_dir/$dom->{'id'}");
local %init;
foreach my $a ("email", "quota", "mquota", "shell") {
$init{$a} = $user->{$a} if (defined($user->{$a}));
}
foreach my $a ("secs", "to") {
if (defined($user->{$a})) {
$init{$a} = join("\t", @{$user->{$a}});
}
}
if (defined($user->{'dbs'})) {
$init{'dbs'} = join("\t", map { $_->{'type'}."_".$_->{'name'} }
@{$user->{'dbs'}});
}
&write_file("$initial_users_dir/$dom->{'id'}", \%init);
&unlock_file("$initial_users_dir/$dom->{'id'}");
}
# allowed_domain_name(&parent, newdomain)
# Returns an error message if some domain name is invalid, or undef if OK.
# Checks domain-owner subdomain and reseller subdomain limits.
sub allowed_domain_name
{
local ($parent, $newdom) = @_;
# Check if forced to be under one of the domains he already owns
if ($parent && $access{'forceunder'}) {
local $ok = 0;
foreach my $pdom ($parent, &get_domain_by("parent", $parent->{'id'})) {
local $pd = $pdom->{'dom'};
if ($newdom =~ /\.\Q$pd\E$/i) {
$ok = 1;
last;
}
}
$ok || return &text('setup_eforceunder2', $parent->{'dom'});
}
# Check if under someone else's domain
if ($parent && $access{'safeunder'}) {
foreach my $d (&list_domains()) {
local $od = $d->{'dom'};
if ($d->{'id'} ne $parent->{'id'} &&
$d->{'parent'} ne $parent->{'id'} &&
$newdom =~ /\.\Q$od\E$/i) {
return &text('setup_esafeunder', $od);
}
}
}
# Check allowed domain regexp
if ($access{'subdom'}) {
if ($newdom !~ /\.\Q$access{'subdom'}\E$/i) {
return &text('setup_eforceunder', $access{'subdom'});
}
}
# Check if on denied list
if (!&master_admin()) {
foreach my $re (split(/\s+/, $config{'denied_domains'})) {
if ($newdom =~ /^$re$/i) {
return $text{'setup_edenieddomain'};
}
}
}
return undef;
}
# domain_databases(&domain, [&types])
# Returns a list of structures for databases in a domain
sub domain_databases
{
local ($d, $types) = @_;
local @dbs;
if ($d->{'mysql'} && (!$types || &indexof("mysql", @$types) >= 0)) {
local %done;
local $mymod = &require_dom_mysql($d);
local $av = &foreign_available($mymod);
local $myhost = &get_database_host_mysql($d);
$myhost = undef if ($myhost eq 'localhost');
foreach my $db (split(/\s+/, $d->{'db_mysql'})) {
next if ($done{$db}++);
push(@dbs, { 'name' => $db,
'type' => 'mysql',
'users' => 1,
'link' => $av ? "../$mymod/edit_dbase.cgi?db=$db"
: undef,
'desc' => $text{'databases_mysql'},
'host' => $myhost, });
}
}
if ($d->{'postgres'} && (!$types || &indexof("postgres", @$types) >= 0)) {
local %done;
local $pgmod = &require_dom_postgres($d);
local $av = &foreign_available("postgresql");
&require_postgres();
local $pghost = &get_database_host_postgres($d);
$pghost = undef if ($pghost eq 'localhost');
foreach my $db (split(/\s+/, $d->{'db_postgres'})) {
next if ($done{$db}++);
push(@dbs, { 'name' => $db,
'type' => 'postgres',
'link' => $av ? "../$pgmod/".
"edit_dbase.cgi?db=$db"
: undef,
'desc' => $text{'databases_postgres'},
'host' => $pghost, });
}
}
# Only check plugins if some non-core DB types were requested
local @nctypes = $types ? grep { $_ ne "mysql" && $_ ne "postgres" } @$types
: ( );
if (!$types || @nctypes) {
foreach my $f (&list_database_plugins()) {
if (!$types || &indexof($f, @$types) >= 0) {
push(@dbs, &plugin_call($f, "database_list", $d));
}
}
}
return @dbs;
}
# all_databases([&domain])
# Returns a list of all known databases on the system, possibly limited to
# those relevant for some domain.
sub all_databases
{
local ($d) = @_;
local @rv;
if ($config{'mysql'} && (!$d || $d->{'mysql'})) {
&require_mysql();
push(@rv, map { { 'name' => $_,
'type' => 'mysql',
'desc' => $text{'databases_mysql'},
'special' => $_ =~ /^(mysql|sys|information_schema|performance_schema)$/ } }
&list_all_mysql_databases($d));
}
if ($config{'postgres'} && (!$d || $d->{'postgres'})) {
&require_postgres();
push(@rv, map { { 'name' => $_,
'type' => 'postgres',
'desc' => $text{'databases_postgres'},
'special' => ($_ =~ /^template/i) } }
&list_all_postgres_databases($d));
}
foreach my $f (&list_database_plugins()) {
if (!$d || $d->{$f}) {
push(@rv, &plugin_call($f, "databases_all", $d));
}
}
return @rv;
}
# all_database_types()
# Returns a list of all database types on the system
sub all_database_types
{
return ( $config{'mysql'} ? ("mysql") : ( ),
$config{'postgres'} ? ("postgres") : ( ),
&list_database_plugins() );
}
# resync_all_databases(&domain, &all-dbs)
# Updates a domain object to remove databases that no longer really exist, and
# perhaps to change the 'db' field to the first actual database
sub resync_all_databases
{
local ($d, $all) = @_;
return if (!@$all); # If no DBs were found on the system, do nothing
# to avoid mass dis-association
local %all = map { ("$_->{'type'} $_->{'name'}", $_) } @$all;
local $removed = 0;
&lock_domain($d);
foreach my $k (keys %$d) {
if ($k =~ /^db_(\S+)$/) {
local $t = $1;
local @names = split(/\s+/, $d->{$k});
local @newnames = grep { $all{"$t $_"} } @names;
if (@names != @newnames) {
$d->{$k} = join(" ", @newnames);
$removed = 1;
}
}
}
if ($removed) {
&save_domain($d);
}
# Fix 'db' field if it is currently set to a missing DB
local @domdbs = &domain_databases($d);
local ($defdb) = grep { $_->{'name'} eq $d->{'db'} } @domdbs;
if (!$defdb && @domdbs) {
$d->{'db'} = $domdbs[0]->{'name'};
&save_domain($d);
}
&unlock_domain($d);
}
# get_database_host(type, [&domain])
# Returns the remote host that we use for the given database type. If the
# DB is on the same server, returns localhost
sub get_database_host
{
local ($type, $d) = @_;
local $rv;
if (&indexof($type, @features) >= 0) {
# Built-in DB
local $hfunc = "get_database_host_".$type;
$rv = &$hfunc($d);
}
elsif (&indexof($type, &list_database_plugins()) >= 0) {
# From plugin
$rv = &plugin_call($type, "database_host", $d);
}
$rv ||= "localhost";
if ($rv eq "localhost" && $type eq "mysql") {
# Is this domain under a chroot? If so, use 127.0.0.1 to force use
# of a TCP connection rather than a socket
my $parent = $d->{'parent'} ? &get_domain($d->{'parent'}) : $d;
if (&get_domain_jailkit($parent)) {
$rv = "127.0.0.1";
}
}
return $rv;
}
# check_domain_hashpass(&domain)
# Shows a checkbox to enable hashed passwords
# if domain and template don't have it aligned
sub check_domain_hashpass
{
my ($d) = @_;
my $tmpl = &get_template($d->{'template'});
if (defined($d->{'hashpass'}) && defined($tmpl->{'hashpass'}) &&
$d->{'hashpass'} != $tmpl->{'hashpass'}) {
return 1;
}
return 0;
}
# update_domain_hashpass(&dom, mode)
# Updates domain hashpass option if needed, and
# delete all *_enc_pass if disabling hashing
sub update_domain_hashpass
{
my ($d, $mode) = @_;
if (&check_domain_hashpass($d)) {
$d->{'hashpass'} = $mode;
if (!$mode) {
my @encpass_opts = grep { /enc_pass$/ } keys %{$d};
foreach my $encpass_opt (@encpass_opts) {
delete($d->{$encpass_opt});
}
}
return 1;
}
return 0;
}
# post_update_domain_hashpass(&@doms, mode, pass)
# Apply domain services hashpass clean ups
sub post_update_domain_hashpass
{
my ($d, $mode, $pass) = @_;
if ($mode) {
# On switching to hashed mode some passwords
# services need to be reset to plain text
my $opt = 'mysql_enc_pass';
if ($d->{$opt}) {
my $fn = $opt;
$fn =~ s/enc_//;
$d->{$fn} = $pass;
delete($d->{$opt});
}
}
}
# get_password_synced_types(&domain)
# Returns a list of DB types that are enabled for this domain and have their
# passwords synced with the admin login
sub get_password_synced_types
{
local ($d) = @_;
my @rv;
foreach my $t (&all_database_types()) {
my $sfunc = $t."_password_synced";
if (defined(&$sfunc) && &$sfunc($d)) {
# Definitely is synced
push(@rv, $t);
}
elsif (!defined(&$sfunc)) {
# Assume yes for other DB types
push(@rv, $t);
}
}
return @rv;
}
# count_ftp_bandwidth(logfile, start, &bw-hash, &users, prefix, include-rotated)
# Scans an FTP server log file for downloads by some user, and returns the
# total bytes and time of last log entry.
sub count_ftp_bandwidth
{
local $max_ltime = $_[1];
local $f;
foreach $f ($_[5] ? &all_log_files($_[0], $max_ltime) : ( $_[0] )) {
local $_;
if ($f =~ /\.gz$/i) {
open(LOG, "gunzip -c ".quotemeta($f)." |");
}
elsif ($f =~ /\.Z$/i) {
open(LOG, "uncompress -c ".quotemeta($f)." |");
}
else {
open(LOG, "<".$f);
}
while() {
if (/^(\S+)\s+(\S+)\s+(\S+)\s+\[(\d+)\/(\S+)\/(\d+):(\d+):(\d+):(\d+)\s+(\S+)\]\s+"([^"]*)"\s+(\S+)\s+(\S+)/) {
# ProFTPD extended log format line
local $ltime = timelocal($9, $8, $7, $4, $apache_mmap{lc($5)}, $6);
$max_ltime = $ltime if ($ltime > $max_ltime);
next if ($_[3] && &indexof($3, @{$_[3]}) < 0); # user
next if (substr($11, 0, 4) ne "RETR" &&
substr($11, 0, 4) ne "STOR");
if ($ltime > $_[1]) {
local $day = int($ltime / (24*60*60));
$_[2]->{$_[4]."_".$day} += $13;
}
}
elsif (/^\S+\s+(\S+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\d+)\s+\d+\s+\S+\s+(\d+)\s+\S+\s+\S+\s+\S+\s+(\S+)\s+\S+\s+(\S+)/) {
# xferlog format line
local $ltime = timelocal($5, $4, $3, $2, $apache_mmap{lc($1)}, $6);
$max_ltime = $ltime if ($ltime > $max_ltime);
next if ($_[3] && &indexof($9, @{$_[3]}) < 0); # user
next if ($8 ne "o" && $8 ne "i");
if ($ltime > $_[1]) {
local $day = int($ltime / (24*60*60));
$_[2]->{$_[4]."_".$day} += $7;
}
}
}
close(LOG);
}
return $max_ltime;
}
# random_password([len])
# Returns a random password of the specified length, or the configured default
sub random_password
{
&seed_random();
&require_useradmin();
my $random_password;
my $len = $_[0] || $config{'passwd_length'} || 25;
eval "utf8::decode(\$config{'passwd_chars'})"
if ($config{'passwd_chars'});
my @passwd_chars = split(//, $config{'passwd_chars'});
if (!@passwd_chars) {
@passwd_chars = @useradmin::random_password_chars;
}
# Number of characters to operate on
my $passwd_chars = scalar(@passwd_chars);
# Check resulting password and try again
# if it doesn't meet the requirements
for (my $i = 0; $i < $passwd_chars*100; $i++) {
$random_password = '';
foreach (1 .. $len) {
$random_password .= $passwd_chars[rand($passwd_chars)];
}
return $random_password
if (&random_password_validate(
\@passwd_chars, $len, $random_password) != 0);
}
return $random_password;
}
sub random_password_validate
{
my ($chars, $len, $pass) = @_;
# Define character type checks in a hash
my %check = (
uppercase => qr/[A-Z]/,
lowercase => qr/[a-z]/,
digit => qr/[0-9]/,
special => qr/[\@\#\$\%\^\&\*\(\)\_\+\!\-\=\[\]\{\}\;\:\'\"\,\<\.\>\/\?\~\`\\]/,
unicode => qr/(?![A-Za-z])\p{L}/,
);
# Determine required character types
my %required;
foreach my $key (keys %check) {
# If any character in @$chars matches the regex for this
# key, add the key to %required with a value of 1
if (grep { $_ =~ $check{$key} } @$chars) {
$required{$key} = 1;
}
}
# Return -1 if the number of required types exceeds the password length
return -1 if scalar(keys %required) > $len;
# Check if password contains one of each required types
my $matches = grep { $pass =~ $check{$_} } keys %required;
return 0 unless $matches == keys %required;
return 1;
}
# random_salt([len])
# Returns a crypt-format salt of the given length (default 2 chars)
sub random_salt
{
local $len = $_[0] || 2;
&seed_random();
local $rv;
local @saltchars = ( 'a' .. 'z', 'A' .. 'Z', 0 .. 9, '.', '/' );
for(my $i=0; $i<$len; $i++) {
$rv .= $saltchars[int(rand()*scalar(@saltchars))];
}
return $rv;
}
# try_function(feature, function, arg, ...)
# Executes some function, and if it fails prints an error message. In a scalar
# context returns 0 if the function failed, 1 otherwise. In an array context,
# returns this flag plus the function's actual return value.
sub try_function
{
local ($f, $func, @args) = @_;
local $main::error_must_die = 1;
local $rv;
eval { $rv = &$func(@args) };
if ($@) {
&$second_print(&text('setup_failure',
$text{'feature_'.$f}, "$@"));
return wantarray ? ( 0 ) : 0;
}
return wantarray ? ( 1, $rv ) : 1;
}
# bandwidth_period_start([ago])
# Returns the day number on which the current (or some previous)
# bandwidth period started
sub bandwidth_period_start
{
local ($ago) = @_;
local $now = time();
local $day = int($now / (24*60*60));
local @tm = localtime(time());
local $rv;
if ($config{'bw_past'} eq 'week') {
# Start on last sunday
$rv = $day - $tm[6];
$rv -= $ago*7;
}
elsif ($config{'bw_past'} eq 'month') {
# Start at 1st of month
for(my $i=0; $i<$ago; $i++) {
$tm[4]--;
if ($tm[4] < 0) {
$tm[5]--;
$tm[4] = 11;
}
}
$rv = int(timelocal(59, 59, 23, 1, $tm[4], $tm[5]) / (24*60*60));
}
elsif ($config{'bw_past'} eq 'year') {
# Start at start of year
$tm[4] -= $ago;
$rv = int(timelocal(59, 59, 23, 1, 0, $tm[5]) / (24*60*60));
}
else {
# Start N days ago
$rv = $day - $config{'bw_period'};
$rv -= $ago*$config{'bw_period'};
}
return $rv;
}
# bandwidth_period_end([ago])
# Returns the day number on which some bandwidth period ends (inclusive)
sub bandwidth_period_end
{
local ($ago) = @_;
local $now = time();
local $day = int($now / (24*60*60));
if ($ago == 0) {
return $day;
}
local $sday = &bandwidth_period_start($ago);
if ($config{'bw_past'} eq 'week') {
# 6 days after start
return $sday + 6;
}
elsif ($config{'bw_past'} eq 'month') {
# End of the month
return &bandwidth_period_start($ago-1)-1;
}
elsif ($config{'bw_past'} eq 'year') {
# End of the year
return &bandwidth_period_start($ago-1)-1;
}
else {
return $sday + $config{'bw_period'} - 1;
}
}
# servers_input(name, &ids, &domains, [disabled], [use-multi-select])
# Returns HTML for a multi-server selection field
sub servers_input
{
my ($name, $ids, $doms, $dis, $ms) = @_;
my $sz = scalar(@$doms) > 10 ? 10 : scalar(@$doms) < 5 ? 5 : scalar(@$doms);
my $optdis = " style='font-style:italic; color:#a94442'".
" title='$text{enable_tooltip}'";
my $opts = [ map { [ $_->{'id'},
($_->{'parent'} ? " " : "").&show_domain_name($_),
$_->{'disabled'} ? $optdis : undef ] }
&sort_indent_domains($doms) ];
my $vals = [ ];
foreach my $id (@$ids) {
my $d = &get_domain($id);
push(@$vals, [ $id, $d ? &show_domain_name($d) : $id,
$d && $d->{'disabled'} ? $optdis : undef ]);
}
if ($ms) {
return &ui_multi_select($name, $vals, $opts, $sz, 1, $dis);
}
else {
return &ui_select($name, $vals, $opts, $sz, 1, 0, $dis);
}
}
# one_server_input(name, id, &domains, [disabled])
# Returns HTML for a single-server selection field
sub one_server_input
{
local ($name, $id, $doms, $dis) = @_;
return &ui_select($name, $id,
[ map { [ $_->{'id'}, &show_domain_name($_) ] }
sort { $a->{'dom'} cmp $b->{'dom'} } @$doms ],
1, 0, 0, $dis);
}
# can_monitor_bandwidth(&domain)
# Returns 1 if bandwidth monitoring is enabled for some server
sub can_monitor_bandwidth
{
if ($config{'bw_servers'} eq "") {
return 1; # always
}
elsif ($config{'bw_servers'} =~ /^\!(.*)$/) {
# List of servers not to check
local @ids = split(/\s+/, $1);
return &indexof($_[0]->{'id'}, @ids) == -1;
}
else {
# List of servers to check
local @ids = split(/\s+/, $config{'bw_servers'});
return &indexof($_[0]->{'id'}, @ids) != -1;
}
}
# Returns 1 if the current user can see mailbox and domain passwords
sub can_show_pass
{
return &master_admin() || &reseller_admin() || $config{'show_pass'};
}
# Returns 1 if the user can change his own password. Not shown for root,
# because this is done outside of Virtualmin.
sub can_passwd
{
return &reseller_admin() || $access{'edit_passwd'};
}
# Returns 1 if the user can enroll for 2fa
sub can_user_2fa
{
my %miniserv;
&get_miniserv_config(\%miniserv);
return $miniserv{'twofactor_provider'} && $access{'edit_passwd'};
}
# Return 1 if the master admin or reseller can enroll for 2fa
sub can_master_reseller_2fa
{
my %miniserv;
&get_miniserv_config(\%miniserv);
return $miniserv{'twofactor_provider'} &&
(&master_admin() || &reseller_admin());
}
# Returns 1 if the user can change a domain's external IP address
sub can_dnsip
{
return &master_admin() || &reseller_admin() || $access{'edit_dnsip'};
}
# Returns 1 if the current user can set the chained certificate path to
# anywhere.
sub can_chained_cert_path
{
return &master_admin();
}
# Returns 1 if the user can copy a domain's cert to Webmin
sub can_webmin_cert
{
return &master_admin();
}
# Returns 1 if the current user can edit allowed remote DB hosts
sub can_allowed_db_hosts
{
return &master_admin() || &reseller_admin() || $access{'edit_allowedhosts'};
}
# Returns 2 if the current user can manage all plans, 1 if his own only,
# 0 if cannot manage any
sub can_edit_plans
{
return &master_admin() ? 2 :
&reseller_admin() && !$access{'noplans'} ? 1 : 0;
}
# Returns 1 if the current user can edit log file locations
sub can_log_paths
{
return &master_admin();
}
# Returns 1 if DNS records can be manually edited
sub can_manual_dns
{
return &master_admin();
}
sub can_edit_resellers
{
return &can_edit_templates() || $access{'createresellers'};
}
# has_proxy_balancer(&domain)
# Returns 2 if some domain supports proxy balancing to multiple URLs, 1 for
# proxying to a single URL, 0 if neither.
sub has_proxy_balancer
{
local ($d) = @_;
if ($config{'web'} && !$d->{'alias'} && !$d->{'proxy_pass_mode'}) {
# From Apache
&require_apache();
if ($apache::httpd_modules{'mod_proxy'} &&
$apache::httpd_modules{'mod_proxy_balancer'}) {
return 2;
}
elsif ($apache::httpd_modules{'mod_proxy'}) {
return 1;
}
}
else {
# From plugin, maybe
local $p = &domain_has_website($d);
return &plugin_defined($p, "feature_supports_web_balancers") ?
&plugin_call($p, "feature_supports_web_balancers", $d) : 0;
}
}
# has_proxy_none([&domain])
# Returns 1 if the system supports disabling proxying for some URL
sub has_proxy_none
{
local ($d) = @_;
local $p = &domain_has_website($d);
if ($p eq 'web') {
&require_apache();
return $apache::httpd_modules{'mod_proxy'} >= 2.0;
}
else {
return 1; # Assume OK for plugins
}
}
# has_webmail_rewrite(&domain)
# Returns 1 if this system has mod_rewrite, needed for redirecting webmail.$DOM
# to port 20000
sub has_webmail_rewrite
{
local ($d) = @_;
local $p = &domain_has_website($d);
if ($p eq 'web') {
# Check Apache modules
&require_apache();
return $apache::httpd_modules{'mod_rewrite'};
}
else {
# Call plugin
return &plugin_defined($p, "feature_supports_webmail_redirect") &&
&plugin_call($p, "feature_supports_webmail_redirect", $d);
}
}
# has_sni_support([&domain])
# Returns 1 if the webserver supports SNI for SSL cert selection
sub has_sni_support
{
local ($d) = @_;
local $p = &domain_has_website($d);
if ($p eq 'web') {
# Check Apache modules
&require_apache();
local @dirs = &list_apache_directives();
local ($sni) = grep { lc($_->[0]) eq lc("SSLStrictSNIVHostCheck") }
@dirs;
return 1 if ($sni);
if ($apache::httpd_modules{'mod_ssl'} >= 2.3 ||
$apache::httpd_modules{'mod_ssl'} =~ /^2\.2(\d+)/ && $1 >= 12) {
# Assume SNI works for Apache 2.2.12 or later
return 1;
}
return 0;
}
else {
# Call plugin
return &plugin_defined($p, "feature_supports_sni") &&
&plugin_call($p, "feature_supports_sni", $d);
}
}
# has_cgi_support([&domain])
# Returns a list of supported cgi modes, which can be 'suexec' and 'fcgiwrap'
sub has_cgi_support
{
my ($d) = @_;
my $p = &domain_has_website($d);
if ($p eq 'web') {
# Check if Apache supports suexec or fcgiwrap
my @rv;
push(@rv, 'suexec') if (&supports_suexec($d));
push(@rv, 'fcgiwrap') if (&supports_fcgiwrap());
return @rv;
}
elsif ($p) {
# Call plugin function
my $supp = &plugin_defined($p, "feature_web_supports_cgi") &&
&plugin_call($p, "feature_web_supports_cgi", $d);
return ('fcgiwrap') if ($supp);
}
return ( );
}
# get_domain_cgi_mode(&domain)
# Returns either 'suexec', 'fcgiwrap' or undef depending on how CGI scripts
# are being run
sub get_domain_cgi_mode
{
my ($d) = @_;
my $p = &domain_has_website($d);
if ($p eq 'web') {
# Check Apache suexec or fcgiwrap modes
if (&get_domain_suexec($d)) {
return 'suexec';
}
elsif ($d->{'fcgiwrap_port'} && &get_domain_fcgiwrap($d)) {
return 'fcgiwrap';
}
return undef;
}
elsif ($p) {
# Check plugin supported mode
return &plugin_call($p, "feature_web_get_domain_cgi_mode", $d);
}
return undef;
}
# save_domain_cgi_mode(&domain, mode)
# Change the mode for executing CGI scripts to either fcgiwrap, suexec or undef
# to disable them entirely
sub save_domain_cgi_mode
{
my ($d, $mode) = @_;
my $p = &domain_has_website($d);
if ($p eq 'web') {
# Is PHP mode compatible?
my $phpmode = &get_domain_php_mode($d);
if (!$mode && ($phpmode eq 'cgi' || $phpmode eq 'fcgid')) {
return $text{'website_ephpcgi'};
}
if ($mode eq 'fcgiwrap' && $phpmode eq 'fcgid') {
return $text{'website_ephpfcgid'};
}
&obtain_lock_web($d);
if ($mode ne 'fcgiwrap') {
&disable_apache_fcgiwrap($d);
}
if ($mode ne 'suexec') {
&disable_apache_suexec($d);
}
my $err = undef;
if ($mode eq 'suexec') {
$err = &enable_apache_suexec($d);
}
elsif ($mode eq 'fcgiwrap') {
$err = &enable_apache_fcgiwrap($d);
}
&lock_domain($d);
if (!$err) {
$d->{'last_cgimode'} = $mode;
}
&save_domain($d);
&unlock_domain($d);
&release_lock_web($d);
return $err;
}
elsif ($p) {
my $err = &plugin_call($p, "feature_web_save_domain_cgi_mode",
$d, $mode);
if (!$err) {
&lock_domain($d);
$d->{'last_cgimode'} = $mode;
&save_domain($d);
&unlock_domain($d);
}
return $err;
}
else {
return "Virtual server does not have a website!";
}
}
# require_licence()
# Reads in the file containing the licence_scheduled function.
# Returns 1 if OK, 0 if not
sub require_licence
{
my ($force) = @_;
return 0 if (!$virtualmin_pro && !$force);
foreach my $ls ("$module_root_directory/virtualmin-licence.pl",
$config{'licence_script'}) {
if ($ls && -r $ls) {
do $ls;
if ($@) {
&error("Licence script failed : $@");
}
return 1;
}
}
return 0;
}
# setup_licence_cron()
# Checks for and sets up the licence checking cron job (if needed)
sub setup_licence_cron
{
if (&require_licence()) {
&read_file($licence_status, \%licence);
return if (time() - $licence{'last'} < 24*60*60); # checked recently, so no worries
# Hasn't been checked from cron for 3 days .. do it now
local $job = &find_cron_script($licence_cmd);
if (!$job) {
# Create
$job = { 'mins' => int(rand()*60),
'hours' => int(rand()*24),
'days' => '*',
'months' => '*',
'weekdays' => '*',
'user' => 'root',
'active' => 1,
'command' => $licence_cmd };
}
else {
# Enforce a proper schedule
if ($job->{'mins'} !~ /^\d+$/) {
$job->{'mins'} = int(rand()*60);
}
if ($job->{'hours'} !~ /^\d+$/) {
$job->{'hours'} = int(rand()*24);
}
$job->{'days'} = '*';
$job->{'months'} = '*';
$job->{'weekdays'} = '*';
$job->{'active'} = 1;
$job->{'user'} = 'root';
$job->{'command'} = $licence_cmd;
}
&setup_cron_script($job);
}
}
# licence_state()
# Returns license state as hash
sub licence_state
{
my @keys = qw(Y D T Q P H);
my %hash;
for my $i (0 .. $#keys) {
$hash{$keys[$i]} = $i == 0 ? 0 + ~0 : $i + 1;
}
return %hash;
}
# licence_status()
# Checks license status
sub licence_status
{
# Licence related checks
if ($virtualmin_pro && -r $licence_status) {
my %licence_status;
&read_file($licence_status, \%licence_status);
my ($bind, $time) = ($licence_status{'bind'}, $licence_status{'time'});
my $scale = 1;
$scale = 3 if ($licence_status{'status'} == 3);
if ($main::webmin_script_type ne 'cron' && !$time && $bind &&
int(($bind-time())/86400)+(21/$scale) <= 0) {
my $title = $text{'licence_expired'};
my $body = &text('licence_expired_desc',
&get_webprefix_safe()."/$module_name/pro/licence.cgi");
if ($main::webmin_script_type eq 'cmd') {
my $chars = 75;
my $astrx = "*" x $chars;
my $attrx = "*" x 2;
$body = &html_tags_to_text($body);
$title = "$attrx $title $attrx";
my $ptitle = ' ' x (int(($chars - length($title)) / 2)).
$title;
$title .= ' ' x ($chars - length($ptitle));
$body =~ s/(.{1,$chars})(?:\s+|$)/$1\n/g;
$body = "\n$astrx\n$ptitle\n$body$astrx\n";
print "$body\n";
exit(99);
}
elsif ($main::webmin_script_type eq 'web') {
&ui_print_header(undef, $text{'error'}, "");
print &ui_alert_box("$body", 'warn', undef, 1, $title);
&ui_print_footer("javascript:history.back()",
$text{'error_previous'});
exit(99);
}
}
return;
}
}
# check_licence_expired()
# Returns 0 if the licence is valid, 1 if not, or 2 if could not be checked,
# 3 if expired, the expiry date, error message, number of domain, number
# of servers, and auto-renewal flag
sub check_licence_expired
{
return 0 if (!&require_licence());
local %licence;
&read_file_cached($licence_status, \%licence);
if (time() - $licence{'last'} > 3*24*60*60) {
# Hasn't been checked from cron for 3 days .. do it now
&update_licence_from_site(\%licence);
&write_file($licence_status, \%licence);
}
return ($licence{'status'}, $licence{'expiry'},
$licence{'err'} || $licence{'warn'}, $licence{'doms'}, $licence{'servers'},
$licence{'autorenew'}, $licence{'time'}, $licence{'bind'},
$licence{'subscription'});
}
# update_licence_from_site(&licence)
# Attempts to validate the license, and updates the given hash ref with
# license details.
sub update_licence_from_site
{
my ($licence) = @_;
my $lastpost = $config{'lastpost'};
return if (defined($licence->{'last'}) && $licence->{'status'} == 0 &&
$lastpost && time() - $lastpost < 60*60*60);
my ($status, $expiry, $err, $doms, $servers, $max_servers, $autorenew,
$state, $subscription) = &check_licence_site();
my %state = &licence_state();
$licence->{'last'} = $licence->{'time'} = time();
delete($licence->{'warn'});
if ($status == 2) {
# Networking / CGI error. Don't treat this as a failure unless we have
# seen it for at least 2 days
$licence->{'lastdown'} ||= time();
my $diff = time() - $licence->{'lastdown'};
if ($diff < 2*24*60*60) {
# A short-term failure - don't change anything
$licence->{'warn'} = $err;
return;
}
}
else {
delete($licence->{'lastdown'});
}
$licence->{'status'} = $status;
$licence->{'bind'} = $licence->{'time'} if (!$licence->{'bind'} && $status >= 1);
delete($licence->{'bind'}) if (defined($status) && $status == 0);
$licence->{'expiry'} = $expiry;
$licence->{'autorenew'} = $autorenew;
delete($licence->{'time'}) unless ($state{$state} && $state{$state} >= $servers);
$licence->{'subscription'} = $subscription;
$licence->{'err'} = $err;
if (defined($doms)) {
# Only store the max domains if we got something valid back
$licence->{'doms'} = $doms;
}
if (defined($servers)) {
# Same for servers
$licence->{'used_servers'} = $servers;
$licence->{'servers'} = $max_servers;
}
}
# check_licence_site()
# Calls the function to actually validate the licence, which must return 0 if
# valid, 1 if not, or 2 if could not be checked, 3 if expired, the expiry
# date, any error message, the max number of domains, number of servers,
# maximum servers, and the auto-renewal flag
sub check_licence_site
{
return (0) if (!&require_licence());
local $id = &get_licence_hostid();
my %serial;
&read_env_file($virtualmin_license_file, \%serial);
local ($status, $expiry, $err, $doms, $max_servers, $servers, $autorenew,
$state, $subscription) =
&licence_scheduled($id, undef, undef, &get_vps_type());
if (defined($status) && $status == 0 && $doms) {
# A domains limit exists .. check if we have exceeded it
local @doms = grep { !$_->{'alias'} &&
!$_->{'defaultdomain'} } &list_domains();
if (@doms > $doms) {
$status = 1;
$err = &text('licence_maxdoms', $doms, scalar(@doms));
}
}
if (defined($status) && $status == 0 && $max_servers && !$err) {
# A servers limit exists .. check if we have exceeded it
if ($servers > $max_servers) {
$status = 1;
$err = &text('licence_maxservers2', $max_servers,
"$servers",
"$serial{'SerialNumber'}");
}
}
return ($status, $expiry, $err, $doms, $servers, $max_servers,
$autorenew, $state, $subscription);
}
# get_licence_hostid()
# Return a host ID for licence checking, from the hostid command or
# MAC address or hostname
sub get_licence_hostid
{
my $id;
my $product_uuid_file = "/sys/class/dmi/id/product_uuid";
my $product_serial_file = "/sys/class/dmi/id/product_serial";
my $product_file = -f $product_uuid_file ? $product_uuid_file :
(-f $product_serial_file ? $product_serial_file : undef);
if ($product_file) {
$id = &read_file_contents($product_file);
$id =~ /(?([0-9a-fA-F]\s*){8})/;
$id = $+{id};
$id =~ s/\s+//g;
return lc($id) if ($id);
}
if (&has_command("hostid")) {
$id = &backquote_command("hostid 2>/dev/null");
chop($id);
}
if (!$id || $id =~ /^0+$/ || $id eq '7f0100') {
&foreign_require("net");
local ($iface) = grep { $_->{'fullname'} eq $config{'iface'} }
&net::active_interfaces();
$id = $iface->{'ether'} if ($iface);
}
if (!$id) {
$id = &get_system_hostname();
}
return $id;
}
# get_vps_type()
# If running under some kind of VPS, return a type code for it. This can be
# one of 'xen', 'vserver', 'zones' or undef for none.
sub get_vps_type
{
return defined(&running_in_zone) && &running_in_zone() ? 'zones' :
defined(&running_in_vserver) && &running_in_vserver() ? 'vserver' :
defined(&running_in_xen) && &running_in_xen() ? 'xen' : undef;
}
# warning_messages()
# Returns HTML for an error messages such as the licence being expired, if it
# is and if the current user is the master admin. Also warns about IP changes,
# need to reboot, etc.
sub warning_messages
{
my $rv;
foreach my $warn (&list_warning_messages()) {
$rv .= &ui_alert_box($warn, "warn");
}
return $rv;
}
# list_warning_messages()
# Returns all warning messages for the current user as an array
sub list_warning_messages
{
return () if (!&master_admin());
my @rv;
my $wp = &get_webprefix_safe();
# Get licence expiry date
local ($status, $expiry, $err, undef, undef, $autorenew, $state, $bind) =
&check_licence_expired();
local $expirytime;
if ($expiry =~ /^(\d+)\-(\d+)\-(\d+)$/) {
# Make Unix time
eval {
$expirytime = timelocal(59, 59, 23, $3, $2-1, $1);
};
}
if ($status != 0 && !$state) {
my $license_expired = $status == 3 ? 'e' : undef;
my $scale = 1;
$scale = 3 if ($license_expired);
my $alert_text;
# Not valid .. show message
if ($bind) {
my $prd = 21/$scale;
$bind = int(($bind-time())/86400)+$prd;
$bind = 0 if ($bind < 0 || $bind > $prd);
}
$alert_text .= "".$text{'licence_err'}."
\n";
$alert_text .= $err;
$alert_text = "$alert_text. " if ($err && $err !~ /\.$/);
my $renew_label = "licence_renew4$license_expired";
if ($bind) {
my $multi = $bind > 1 ? 'm' : '';
if ($license_expired) {
$alert_text .= " ".
&text("licence_renew3$license_expired$multi", $bind);
}
else {
$alert_text .= " $text{'licence_maxwarn'}";
$alert_text .= " ".&text("licence_renew3$multi", $bind);
}
}
else {
$alert_text .= " ".$text{$renew_label} if (defined($bind));
$alert_text .= " $text{'licence_maxwarn'}" if (!defined($bind));
}
if ($alert_text !~ /$virtualmin_renewal_url/) {
$alert_text .= " ".&text('licence_renew2',
$virtualmin_renewal_url, $text{'license_shop_name'});
}
if (&can_recheck_licence()) {
$alert_text .= &ui_form_start("$wp/$module_name/pro/licence.cgi");
$alert_text .= &ui_submit($text{'licence_manager_goto'});
$alert_text .= &ui_form_end();
}
$alert_text =~ s/\s+/ /g;
push(@rv, $alert_text);
}
elsif ($expirytime && $expirytime - time() < 7*24*60*60 && !$autorenew) {
# One week to expiry .. tell the user
local $days = int(($expirytime - time()) / (24*60*60));
local $hours = int(($expirytime - time()) / (60*60));
if ($days) {
$alert_text .= "".&text('licence_soon', $days)."
\n";
}
else {
$alert_text .= "".&text('licence_soon2', $hours)."
\n";
}
$alert_text .= " ".&text('licence_renew', $virtualmin_renewal_url,
$text{'license_shop_name'});
if (&can_recheck_licence()) {
$alert_text .= &ui_form_start("$wp/$module_name/pro/licence.cgi");
$alert_text .= &ui_submit($text{'licence_manager_goto'});
$alert_text .= &ui_form_end();
}
$alert_text =~ s/\s+/ /g;
push(@rv, $alert_text);
}
# Check if default IP has changed
local $defip = &get_default_ip();
if ($config{'old_defip'} && $defip && $config{'old_defip'} ne $defip) {
my $alert_text;
$alert_text .= "".&text('licence_ipchanged',
"$config{'old_defip'}",
"$defip")."\n";
$alert_text .= &ui_form_start("$wp/$module_name/edit_newips.cgi");
$alert_text .= &ui_hidden("old", $config{'old_defip'});
$alert_text .= &ui_hidden("new", $defip);
$alert_text .= &ui_hidden("setold", 1);
$alert_text .= &ui_hidden("also", 1);
$alert_text .= &ui_submit($text{'licence_changeip'});
$alert_text .= &ui_form_end();
push(@rv, $alert_text);
}
# Check if in SSL mode, and SSL cert is < 2048 bits
local $small;
if ($ENV{'HTTPS'} eq 'ON') {
local %miniserv;
&get_miniserv_config(\%miniserv);
local $certfile = $miniserv{'certfile'} || $miniserv{'keyfile'};
if ($certfile) {
local $cert = &cert_file_info($certfile);
if ($cert->{'algo'} eq 'rsa' && $cert->{'size'} && $cert->{'size'} < 2048) {
# Too small!
$small = $cert;
}
}
}
if ($small) {
my $alert_text;
local $msg = $small->{'issuer_c'} eq $small->{'c'} &&
$small->{'issuer_o'} eq $small->{'o'} ?
'licence_smallself' : 'licence_smallcert';
$alert_text .= &text($msg,
$small->{'size'},
"$small->{'cn'}",
$small->{'issuer_o'},
$small->{'issuer_cn'},
)."
\n";
my $formlink = "$wp/webmin/edit_ssl.cgi";
my $domid = &get_domain_by('dom', $small->{'cn'});
if ($domid) {
$formlink = "$wp/$module_name/cert_form.cgi?dom=$domid";
}
$alert_text .= &ui_form_start($formlink);
$alert_text .= &ui_hidden("mode", $msg eq 'licence_smallself' ?
'create' : &is_acme_cert($small) ?
'lets' : 'csr');
$alert_text .= &ui_submit($msg eq 'licence_smallself' ?
$text{'licence_newcert'} :
$text{'licence_newcsr'});
$alert_text .= &ui_form_end();
push(@rv, $alert_text);
}
# Check if symlinks need to be fixed. Blank means not checked yet, 0 means
# fixed, 1 means don't fix.
if ($config{'allow_symlinks'} eq '') {
my $alert_text;
# Do any domains have unsafe settings?
local @fixdoms = &fix_symlink_security(undef, 1);
if (@fixdoms) {
$alert_text .= "".&text('licence_fixlinks', scalar(@fixdoms))."
".
$text{'licence_fixlinks2'}."