1=head1 backup-config-lib.pl
2
3Functions for creating configuration file backups. Some example code :
4
5 foreign_require('backup-config', 'backup-config-lib.pl');
6 @backups = backup_config::list_backups();
7 ($apache_backup) = grep { $_->{'mods'} eq 'apache' } @backups;
8 $apache_backup->{'dest'} = '/tmp/apache.tar.gz';
9 &backup_config::save_backup($apache_backup);
10
11=cut
12
13BEGIN { push(@INC, ".."); };
14use strict;
15use warnings;
16use WebminCore;
17our (%text, $module_config_directory, %config);
18&init_config();
19&foreign_require("cron", "cron-lib.pl");
20
21our $cron_cmd = "$module_config_directory/backup.pl";
22our $backups_dir = "$module_config_directory/backups";
23our $manifests_dir = "/tmp/backup-config-manifests";
24
25=head2 list_backup_modules
26
27Returns details of all modules that allow backups, each of which is a hash
28ref in the same format as returned by get_module_info.
29
30=cut
31sub list_backup_modules
32{
33my ($m, @rv);
34foreach $m (&get_all_module_infos()) {
35	my $mdir = &module_root_directory($m->{'dir'});
36	if (&check_os_support($m) &&
37	    -r "$mdir/backup_config.pl") {
38		push(@rv, $m);
39		}
40	}
41return sort { $a->{'desc'} cmp $b->{'desc'} } @rv;
42}
43
44=head2 list_backups
45
46Returns a list of all configured backups, each of which is a hash ref with
47at least the following keys :
48
49=item mods - Space-separate list of modules to include.
50
51=item dest - Destination file, FTP or SSH server.
52
53=item configfile - Set to 1 if /etc/webmin/modulename files are included.
54
55=item nofiles - Set to 1 if server config files (like httpd.conf) are NOT included.
56
57=item others - A tab-separated list of other files to include.
58
59=item email -Email address to notify.
60
61=item emode - Set to 0 to send email only on failure, 1 to always send.
62
63=item sched - Set to 1 if regular scheduled backups are enabled.
64
65=item mins,hours,days,months,weekdays - Cron-style specification of backup time.
66
67=cut
68sub list_backups
69{
70my (@rv, $f);
71opendir(DIR, $backups_dir) || return ();
72foreach $f (sort { $a cmp $b } readdir(DIR)) {
73	next if ($f !~ /^(\S+)\.backup$/);
74	push(@rv, &get_backup($1));
75	}
76closedir(DIR);
77return @rv;
78}
79
80=head2 get_backup(id)
81
82Given a unique backup ID, returns a hash ref containing its details, in the
83same format as list_backups.
84
85=cut
86sub get_backup
87{
88my %backup;
89&read_file("$backups_dir/$_[0].backup", \%backup) || return undef;
90$backup{'id'} = $_[0];
91return \%backup;
92}
93
94=head2 save_backup(&backup)
95
96Given a hash ref containing backup details, saves them to disk. Must be in
97the same format as returned by list_backups, except for the ID which will be
98randomly assigned if missing.
99
100=cut
101sub save_backup
102{
103$_[0]->{'id'} ||= time().$$;
104mkdir($backups_dir, 0700);
105&lock_file("$backups_dir/$_[0]->{'id'}.backup");
106&write_file("$backups_dir/$_[0]->{'id'}.backup", $_[0]);
107&unlock_file("$backups_dir/$_[0]->{'id'}.backup");
108}
109
110=head2 delete_backup(&backup)
111
112Deletes the backup whose details are in the given hash ref.
113
114=cut
115sub delete_backup
116{
117&unlink_logged("$backups_dir/$_[0]->{'id'}.backup");
118}
119
120=head2 parse_backup_url(string)
121
122Converts a URL like ftp:// or a filename into its components. These are
123user, pass, host, page, port (optional)
124
125=cut
126sub parse_backup_url
127{
128if ($_[0] && $_[0] =~ /^ftp:\/\/([^:]*):([^\@]*)\@([^\/:]+)(:(\d+))?(\/.*)$/) {
129	return (1, $1, $2, $3, $6, $5);
130	}
131elsif ($_[0] &&
132       $_[0] =~ /^ssh:\/\/([^:]*):([^\@]*)\@([^\/:]+)(:(\d+))?(\/.*)$/) {
133	return (2, $1, $2, $3, $6, $5);
134	}
135elsif ($_[0] && $_[0] =~ /^upload:(.*)$/) {
136	return (3, undef, undef, undef, $1);
137	}
138elsif ($_[0] && $_[0] =~ /^download:$/) {
139	return (4, undef, undef, undef, undef);
140	}
141else {
142	return (0, undef, undef, undef, $_[0]);
143	}
144}
145
146=head2 show_backup_destination(name, value, [local-mode])
147
148Returns HTML for a field for selecting a local or FTP file.
149
150=cut
151sub show_backup_destination
152{
153my ($mode, $user, $pass, $server, $path, $port) = &parse_backup_url($_[1]);
154my $rv;
155$rv .= "<table id='show_backup_destination' cellpadding=1 cellspacing=0>";
156
157# Local file field
158$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 0, undef, $mode == 0)."</td>\n";
159$rv .= "<td>$text{'backup_mode0'}&nbsp;</td><td colspan='3'>".
160	&ui_textbox("$_[0]_file", $mode == 0 ? $path : "", 60).
161	" ".&file_chooser_button("$_[0]_file")."</td> </tr>\n";
162
163# FTP file fields
164$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 1, undef, $mode == 1)."</td>\n";
165$rv .= "<td>$text{'backup_mode1'}&nbsp;</td><td>".
166	&ui_textbox("$_[0]_server", $mode == 1 ? $server : undef, 20).
167	"</td>\n";
168$rv .= "<td>&nbsp;$text{'backup_path'}&nbsp;</td><td> ".
169	&ui_textbox("$_[0]_path", $mode == 1 ? $path : undef, 20).
170	"</td> </tr>\n";
171$rv .= "<tr> <td></td>\n";
172$rv .= "<td>$text{'backup_login'}&nbsp;</td><td> ".
173	&ui_textbox("$_[0]_user", $mode == 1 ? $user : undef, 20).
174	"</td>\n";
175$rv .= "<td>&nbsp;$text{'backup_pass'}&nbsp;</td><td> ".
176	&ui_password("$_[0]_pass", $mode == 1 ? $pass : undef, 20).
177	"</td> </tr>\n";
178$rv .= "<tr> <td></td>\n";
179$rv .= "<td colspan='4'>$text{'backup_port'} ".
180	&ui_opt_textbox("$_[0]_port", $mode == 1 ? $port : undef, 5,
181			$text{'default'})."</td> </tr>\n";
182
183# SCP file fields
184$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 2, undef, $mode == 2)."</td>\n";
185$rv .= "<td>$text{'backup_mode2'}&nbsp;</td><td>".
186	&ui_textbox("$_[0]_sserver", $mode == 2 ? $server : undef, 20).
187	"</td>\n";
188$rv .= "<td>&nbsp;$text{'backup_path'}&nbsp;</td><td> ".
189	&ui_textbox("$_[0]_spath", $mode == 2 ? $path : undef, 20).
190	"</td> </tr>\n";
191$rv .= "<tr> <td></td>\n";
192$rv .= "<td>$text{'backup_login'}&nbsp;</td><td> ".
193	&ui_textbox("$_[0]_suser", $mode == 2 ? $user : undef, 20).
194	"</td>\n";
195$rv .= "<td>&nbsp;$text{'backup_pass'}&nbsp;</td><td> ".
196	&ui_password("$_[0]_spass", $mode == 2 ? $pass : undef, 20).
197	"</td> </tr>\n";
198$rv .= "<tr> <td></td>\n";
199$rv .= "<td colspan='4'>$text{'backup_port'} ".
200	&ui_opt_textbox("$_[0]_sport", $mode == 2 ? $port : undef, 5,
201			$text{'default'})."</td> </tr>\n";
202
203if ($_[2] == 1) {
204	# Uploaded file field
205	$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 3, undef, $mode == 3).
206		"</td>\n";
207	$rv .= "<td colspan=4>$text{'backup_mode3'} ".
208		&ui_upload("$_[0]_upload", 40).
209		"</td> </tr>\n";
210	}
211elsif ($_[2] == 2) {
212	# Output to browser option
213	$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 4, undef, $mode == 4).
214		"</td>\n";
215	$rv .= "<td colspan=4>$text{'backup_mode4'}</td> </tr>\n";
216	}
217
218$rv .= "</table>\n";
219return $rv;
220}
221
222=head2 parse_backup_destination(name, &in)
223
224Returns a backup destination string, or calls error.
225
226=cut
227sub parse_backup_destination
228{
229my %in = %{$_[1]};
230my $mode = $in{"$_[0]_mode"} || 0;
231if ($mode == 0) {
232	# Local file
233	$in{"$_[0]_file"} && $in{"$_[0]_file"} =~ /^\/\S/ ||
234		&error($text{'backup_edest'});
235	return $in{"$_[0]_file"};
236	}
237elsif ($mode == 1) {
238	# FTP server
239	&to_ipaddress($in{"$_[0]_server"}) ||
240	  &to_ip6address($in{"$_[0]_server"}) ||
241	    &error($text{'backup_eserver1'});
242	$in{"$_[0]_path"} =~ /^\/\S/ || &error($text{'backup_epath'});
243	$in{"$_[0]_user"} =~ /^[^:]*$/ || &error($text{'backup_euser'});
244	$in{"$_[0]_pass"} =~ /^[^\@]*$/ || &error($text{'backup_epass'});
245	$in{"$_[0]_port_def"} || $in{"$_[0]_port"} =~ /^\d+$/ ||
246		&error($text{'backup_eport'});
247	return "ftp://".$in{"$_[0]_user"}.":".$in{"$_[0]_pass"}."\@".
248	       $in{"$_[0]_server"}.
249	       ($in{"$_[0]_port_def"} ? "" : ":".$in{"$_[0]_port"}).
250	       $in{"$_[0]_path"};
251	}
252elsif ($mode == 2) {
253	# SSH server
254	&to_ipaddress($in{"$_[0]_sserver"}) ||
255	  &to_ip6address($in{"$_[0]_sserver"}) ||
256	    &error($text{'backup_eserver2'});
257	$in{"$_[0]_spath"} =~ /^\/\S/ || &error($text{'backup_epath2'});
258	$in{"$_[0]_suser"} =~ /^[^:]*$/ || &error($text{'backup_euser'});
259	$in{"$_[0]_spass"} =~ /^[^\@]*$/ || &error($text{'backup_epass'});
260	$in{"$_[0]_sport_def"} || $in{"$_[0]_sport"} =~ /^\d+$/ ||
261		&error($text{'backup_esport'});
262	return "ssh://".$in{"$_[0]_suser"}.":".$in{"$_[0]_spass"}."\@".
263	       $in{"$_[0]_sserver"}.
264	       ($in{"$_[0]_sport_def"} ? "" : ":".$in{"$_[0]_sport"}).
265	       $in{"$_[0]_spath"};
266	}
267elsif ($mode == 3) {
268	# Uploaded file .. save as temp file?
269	$in{"$_[0]_upload"} || &error($text{'backup_eupload'});
270	return "upload:$_[0]_upload";
271	}
272elsif ($mode == 4) {
273	return "download:";
274	}
275}
276
277=head2 execute_backup(&modules, dest, &size, &files, include-webmin, exclude-files, &others)
278
279Backs up the configuration files for the modules to the selected destination.
280The backup is simply a tar file of config files. Returns undef on success,
281or an error message on failure.
282
283=cut
284sub execute_backup
285{
286# Work out modules we can use
287my @mods;
288foreach my $m (@{$_[0]}) {
289	my $mdir = &module_root_directory($m);
290	if ($m && &foreign_check($m) && -r "$mdir/backup_config.pl") {
291		push(@mods, $m);
292		}
293	}
294
295# Work out where to write to
296my ($mode, $user, $pass, $host, $path, $port) = &parse_backup_url($_[1]);
297my $file;
298if ($mode == 0) {
299	$file = &date_subs($path);
300	}
301else {
302	my $ext = &has_command("gzip") ? '.tar.gz' : '.tar';
303	$file = &transname_timestamped("backup", $ext);
304	}
305
306# Get module descriptions
307my $m;
308my %desc;
309foreach $m (@mods) {
310	my %minfo = &get_module_info($m);
311	$desc{$m} = $minfo{'desc'};
312	}
313
314my @files;
315my %manifestfiles;
316if (!$_[5]) {
317	# Build list of all files to save from modules
318	foreach my $m (@mods) {
319		&foreign_require($m, "backup_config.pl");
320		my @mfiles = &foreign_call($m, "backup_config_files");
321		foreach my $f (@mfiles) {
322			next if (!$f);
323			if (-d $f) {
324				# A directory .. recursively expand
325				foreach my $sf (&expand_directory($f)) {
326					next if (!$sf);
327					push(@files, $sf);
328					push(@{$manifestfiles{$m}}, $sf);
329					}
330				}
331			else {
332				# Just one file
333				push(@files, $f);
334				push(@{$manifestfiles{$m}}, $f);
335				}
336			}
337		}
338	}
339
340# Add module config files and custom langs
341if ($_[4]) {
342	foreach $m (@mods) {
343		my @cfiles = ( "$config_directory/$m/config" );
344		push(@cfiles, glob("$config_directory/$m/custom-lang*"));
345		push(@files, @cfiles);
346		push(@{$manifestfiles{$m}}, @cfiles);
347		}
348	}
349
350# Add other files
351foreach my $f (@{$_[6]}) {
352	next if (!$f);
353	if (-d $f) {
354		# A directory .. recursively expand
355		foreach my $sf (&expand_directory($f)) {
356			next if (!$sf);
357			push(@files, $sf);
358			push(@{$manifestfiles{"other"}}, $sf);
359			}
360		}
361	else {
362		# Just one file
363		push(@files, $f);
364		push(@{$manifestfiles{"other"}}, $f);
365		}
366	}
367
368# Save the manifest files
369&execute_command("rm -rf ".quotemeta($manifests_dir));
370mkdir($manifests_dir, 0755);
371my @manifests;
372foreach $m (@mods, "_others") {
373	next if (!defined($manifestfiles{$m}));
374	my $man = "$manifests_dir/$m";
375	my $fh;
376	&open_tempfile($fh, ">$man");
377	&print_tempfile($fh, map { "$_\n" } @{$manifestfiles{$m}});
378	&close_tempfile($fh);
379	push(@manifests, $man);
380	}
381
382# Make sure we have something to do
383@files = grep { $_ && -e $_ } @files;
384@files || (return $text{'backup_enone'});
385
386if (!$_[5]) {
387	# Call all module pre functions
388	my $m;
389	foreach $m (@mods) {
390		if (&foreign_defined($m, "pre_backup")) {
391			my $err = &foreign_call($m, "pre_backup", \@files);
392			if ($err) {
393				return &text('backup_epre', $desc{$m}, $err);
394				}
395			}
396		}
397	}
398
399# Make the tar (possibly .gz) file
400my $filestemp = &transname();
401my $fh;
402&open_tempfile($fh, ">$filestemp");
403foreach my $f (&unique(@files), @manifests) {
404	my $frel = $f;
405	$frel =~ s/^\///;
406	&print_tempfile($fh, $frel."\n");
407	}
408&close_tempfile($fh);
409my $qfile = quotemeta($file);
410my $out;
411if (&has_command("gzip")) {
412	&execute_command("cd / ; tar cfT - $filestemp | gzip -c >$qfile",
413			 undef, \$out, \$out);
414	}
415else {
416	&execute_command("cd / ; tar cfT $qfile $filestemp",
417			 undef, \$out, \$out);
418	}
419my $ex = $?;
420&unlink_file($filestemp);
421if ($ex) {
422	&unlink_file($file) if ($mode != 0);
423	return &text('backup_etar', "<pre>$out</pre>");
424	}
425my @st = stat($file);
426${$_[2]} = $st[7] if ($_[2]);
427@{$_[3]} = &unique(@files) if ($_[3]);
428&set_ownership_permissions(undef, undef, 0600, $file);
429
430if (!$_[5]) {
431	# Call all module post functions
432	foreach $m (@mods) {
433		if (&foreign_defined($m, "post_backup")) {
434			&foreign_call($m, "post_backup", \@files);
435			}
436		}
437	}
438
439if ($mode == 1) {
440	# FTP upload to destination
441	my $err;
442	&ftp_upload($host, &date_subs($path), $file, \$err, undef,
443		    $user, $pass, $port);
444	&unlink_file($file);
445	return $err if ($err);
446	}
447elsif ($mode == 2) {
448	# SCP to destination
449	my $err;
450	&scp_copy($file, "$user\@$host:".&date_subs($path), $pass, \$err,$port);
451	&unlink_file($file);
452	return $err if ($err);
453	}
454
455return undef;
456}
457
458=head2 execute_restore(&mods, source, &files, apply, [show-only],
459		       [&other-files])
460
461Restore configuration files from the specified source for the listed modules.
462Returns undef on success, or an error message.
463
464=cut
465sub execute_restore
466{
467my ($mods, $src, $files, $apply, $show, $others) = @_;
468
469# Fetch file if needed
470my ($mode, $user, $pass, $host, $path, $port) = &parse_backup_url($src);
471my $file;
472if ($mode == 0) {
473	$file = $path;
474	}
475else {
476	$file = &transname();
477	if ($mode == 2) {
478		# Download with SCP
479		my $err;
480		&scp_copy("$user\@$host:$path", $file, $pass, \$err, $port);
481		if ($err) {
482			&unlink_file($file);
483			return $err;
484			}
485		}
486	elsif ($mode == 1) {
487		# Download with FTP
488		my $err;
489		&ftp_download($host, $path, $file, \$err, undef,
490			      $user, $pass, $port);
491		if ($err) {
492			&unlink_file($file);
493			return $err;
494			}
495		}
496	}
497
498# Validate archive
499open(FILE, "<".$file);
500my $two;
501read(FILE, $two, 2);
502close(FILE);
503my $qfile = quotemeta($file);
504my $gzipped = ($two eq "\037\213");
505my $cmd;
506if ($gzipped) {
507	# Gzipped
508	&has_command("gunzip") || return $text{'backup_egunzip'};
509	$cmd = "gunzip -c $qfile | tar tf -";
510	}
511else {
512	$cmd = "tar tf $qfile";
513	}
514my $out;
515&execute_command($cmd, undef, \$out, \$out, 0, 1);
516if ($?) {
517	&unlink_file($file) if ($mode != 0);
518	return &text('backup_euntar', "<pre>$out</pre>");
519	}
520my @tarfiles = map { "/$_" } split(/\r?\n/, $out);
521my %tarfiles = map { $_, 1 } @tarfiles;
522
523# Extract manifests for each module
524my %hasmod = map { $_, 1 } @$mods;
525$hasmod{"_others"} = 1;
526&execute_command("rm -rf ".quotemeta($manifests_dir));
527my $rel_manifests_dir = $manifests_dir;
528$rel_manifests_dir =~ s/^\///;
529if ($gzipped) {
530	&execute_command("cd / ; gunzip -c $qfile | tar xf - $rel_manifests_dir", undef, \$out, \$out);
531	}
532else {
533	&execute_command("cd / ; tar xf $qfile $rel_manifests_dir", undef, \$out, \$out);
534	}
535opendir(DIR, $manifests_dir);
536my $m;
537my %mfiles;
538my @files;
539while($m = readdir(DIR)) {
540	next if ($m eq "." || $m eq ".." || !$hasmod{$m});
541	open(MAN, "<$manifests_dir/$m");
542	my @mfiles;
543	while(<MAN>) {
544		s/\r|\n//g;
545		if ($tarfiles{$_}) {
546			push(@mfiles, $_);
547			}
548		}
549	close(MAN);
550	$mfiles{$m} = \@mfiles;
551	push(@files, @mfiles);
552	}
553closedir(DIR);
554push(@files, @$others) if ($others);
555if (!@files) {
556	&unlink_file($file) if ($mode != 0);
557	return $text{'backup_enone2'};
558	}
559
560# Get descriptions for each module
561my %desc;
562foreach my $m (@$mods) {
563	my %minfo = &get_module_info($m);
564	$desc{$m} = $minfo{'desc'};
565	}
566
567# Call module pre functions
568foreach my $m (@$mods) {
569	my $mdir = &module_root_directory($m);
570	if ($m && &foreign_check($m) && !$show &&
571	    -r "$mdir/backup_config.pl") {
572		&foreign_require($m, "backup_config.pl");
573		if (&foreign_defined($m, "pre_restore")) {
574			my $err = &foreign_call($m, "pre_restore", \@files);
575			if ($err) {
576				&unlink_file($file) if ($mode != 0);
577				return &text('backup_epre2', $desc{$m}, $err);
578				}
579			}
580		}
581	}
582
583# Lock all files being extracted
584if (!$show) {
585	my $f;
586	foreach $f (@files) {
587		&lock_file($f);
588		}
589	}
590
591# Extract contents (only files specified by manifests)
592my $flag = $show ? "t" : "xv";
593my $qfiles = join(" ", map { s/^\///; quotemeta($_) } &unique(@files));
594if ($gzipped) {
595	&execute_command("cd / ; gunzip -c $qfile | tar ${flag}f - $qfiles",
596			 undef, \$out, \$out);
597	}
598else {
599	&execute_command("cd / ; tar ${flag}f $qfile $qfiles",
600			 undef, \$out, \$out);
601	}
602my $ex = $?;
603
604# Un-lock all files being extracted
605if (!$show) {
606	my $f;
607	foreach $f (@files) {
608		&unlock_file($f);
609		}
610	}
611
612# Check for tar error
613if ($ex) {
614	&unlink_file($file) if ($mode != 0);
615	return &text('backup_euntar', "<pre>$out</pre>");
616	}
617
618if ($apply && !$show) {
619	# Call all module apply functions
620	foreach $m (@$mods) {
621		if (&foreign_defined($m, "post_restore")) {
622			&foreign_call($m, "post_restore", \@files);
623			}
624		}
625	}
626
627@$files = split(/\n/, $out);
628return undef;
629}
630
631=head2 scp_copy(source, dest, password, &error, [port])
632
633Copies a file from some source to a destination. One or the other can be
634a server, like user@foo:/path/to/bar/
635
636=cut
637sub scp_copy
638{
639&foreign_require("proc", "proc-lib.pl");
640my $cmd = "scp -r ".($_[4] ? "-P $_[4] " : "").
641	  quotemeta($_[0])." ".quotemeta($_[1]);
642my ($fh, $fpid) = &proc::pty_process_exec($cmd);
643my $out;
644while(1) {
645	my $rv = &wait_for($fh, "password:", "yes\\/no", ".*\n");
646	$out .= $wait_for_input;
647	if ($rv == 0) {
648		syswrite($fh, "$_[2]\n");
649		}
650	elsif ($rv == 1) {
651		syswrite($fh, "yes\n");
652		}
653	elsif ($rv < 0) {
654		last;
655		}
656	}
657close($fh);
658my $got = waitpid($fpid, 0);
659if ($? || $out =~ /permission\s+denied/i) {
660	${$_[3]} = "scp failed : <pre>$out</pre>";
661	}
662}
663
664=head2 find_cron_job(&backup)
665
666MISSING DOCUMENTATION
667
668=cut
669sub find_cron_job
670{
671my @jobs = &cron::list_cron_jobs();
672my ($job) = grep { $_->{'user'} eq 'root' &&
673		   $_->{'command'} eq "$cron_cmd $_[0]->{'id'}" } @jobs;
674return $job;
675}
676
677=head2 nice_dest(destination, [subdates])
678
679Returns a backup filename in a human-readable format, with dates substituted.
680
681=cut
682sub nice_dest
683{
684my ($url, $subdates) = @_;
685my ($mode, $user, $pass, $server, $path, $port) = &parse_backup_url($url);
686if ($subdates) {
687	$path = &date_subs($path);
688	}
689if ($mode == 0) {
690	return "<tt>$path</tt>";
691	}
692elsif ($mode == 1) {
693	return &text($port ? 'nice_ftpp' : 'nice_ftp',
694		     "<tt>$server</tt>", "<tt>$path</tt>",
695		     $port ? "<tt>$port</tt>" : "");
696	}
697elsif ($mode == 2) {
698	return &text($port ? 'nice_sshp' : 'nice_ssh',
699		     "<tt>$server</tt>", "<tt>$path</tt>",
700		     $port ? "<tt>$port</tt>" : "");
701	}
702elsif ($mode == 3) {
703	return $text{'nice_upload'};
704	}
705elsif ($mode == 4) {
706	return $text{'nice_download'};
707	}
708}
709
710=head2 date_subs(string)
711
712Given a string with strftime-style format characters in it like %Y and %S,
713replaces them with the correct values for the current date and time.
714
715=cut
716sub date_subs
717{
718my ($path) = @_;
719my $rv;
720if ($config{'date_subs'}) {
721        eval "use POSIX";
722        eval "use posix" if ($@);
723        my @tm = localtime(time());
724        $rv = strftime($path, @tm);
725        }
726else {
727	$rv = $path;
728        }
729if ($config{'webmin_subs'}) {
730	$rv = &substitute_template($rv, { });
731	}
732return $rv;
733}
734
735=head2 show_backup_what(name, webmin?, nofiles?, others)
736
737Returns HTML for selecting what gets included in a backup.
738
739=cut
740sub show_backup_what
741{
742my ($name, $webmin, $nofiles, $others) = @_;
743$others ||= "";
744return &ui_checkbox($name."_webmin", 1, $text{'edit_webmin'}, $webmin)."\n".
745       &ui_checkbox($name."_nofiles", 1, $text{'edit_nofiles'}, !$nofiles)."\n".
746       &ui_checkbox($name."_other", 1, $text{'edit_other'}, $others)."<br>".
747       &ui_textarea($name."_files", join("\n", split(/\t+/, $others)), 3, 50);
748}
749
750=head2 parse_backup_what(name, &in)
751
752Returns the webmin and nofiles flags, and a tab-separated list of other
753files to include.
754
755=cut
756sub parse_backup_what
757{
758my ($name, $in) = @_;
759my $webmin = $in->{$name."_webmin"};
760my $nofiles = !$in->{$name."_nofiles"};
761$in->{$name."_files"} =~ s/\r//g;
762my $others = $in->{$name."_other"} ?
763	join("\t", split(/\n+/, $in->{$name."_files"})) : undef;
764$webmin || !$nofiles || $others || &error($text{'save_ewebmin'});
765return ($webmin, $nofiles, $others);
766}
767
768=head2 expand_directory(directory)
769
770Given a directory, return a list of full paths to all files within it.
771
772=cut
773sub expand_directory
774{
775my ($dir) = @_;
776my @rv;
777opendir(EXPAND, $dir);
778my @sf = readdir(EXPAND);
779closedir(EXPAND);
780foreach my $sf (@sf) {
781	next if ($sf eq "." || $sf eq "..");
782	my $path = "$dir/$sf";
783	if (-l $path || !-d $path) {
784		push(@rv, $path);
785		}
786	elsif (-d $path) {
787		push(@rv, &expand_directory($path));
788		}
789	}
790return @rv;
791}
792
7931;
794
795