mirror of
https://github.com/webmin/webmin.git
synced 2026-02-28 16:32:28 +00:00
All $err type error messages in HTML are safely escaped now.
URL-encoding in links:
I have implemented urlize() in all places where user input ($in{'device'}, $in{'slice'}, $in{'part'}) was included in the URL (footer/redirect/other link), e.g. edit_slice.cgi?device=...&slice=....
Affected files include: create_part.cgi, create_slice.cgi, delete_part.cgi, delete_slice.cgi, change_slice_label.cgi, part_form.cgi, slice_form.cgi, edit_slice.cgi, edit_part.cgi, fsck.cgi, newfs.cgi, newfs_form.cgi, save_part.cgi, save_slice.cgi, save_slice_label.cgi, zfs_create.cgi, zvol_create.cgi.
498 lines
17 KiB
Perl
Executable File
498 lines
17 KiB
Perl
Executable File
#!/usr/local/bin/perl
|
|
# Show a form to create a filesystem on a partition
|
|
|
|
use strict;
|
|
use warnings;
|
|
no warnings 'redefine';
|
|
no warnings 'uninitialized';
|
|
require './bsdfdisk-lib.pl';
|
|
our ( %in, %text, $module_name );
|
|
&ReadParse();
|
|
|
|
# Get the disk and slice
|
|
# Validate input parameters to prevent command injection
|
|
$in{'device'} =~ /^[a-zA-Z0-9_\/.-]+$/ or &error("Invalid device name");
|
|
$in{'device'} !~ /\.\./ or &error("Invalid device name");
|
|
$in{'slice'} =~ /^\d+$/ or &error("Invalid slice number") if $in{'slice'};
|
|
$in{'part'} =~ /^[a-z]$/ or &error("Invalid partition letter") if $in{'part'};
|
|
my @disks = &list_disks_partitions();
|
|
my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks;
|
|
$disk || &error( $text{'disk_egone'} );
|
|
my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{ $disk->{'slices'} };
|
|
$slice || &error( $text{'slice_egone'} );
|
|
my $object;
|
|
my $url_device = &urlize( $in{'device'} );
|
|
my $url_slice = &urlize( $in{'slice'} );
|
|
my $url_part = &urlize( $in{'part'} );
|
|
|
|
if ( $in{'part'} ne '' ) {
|
|
$in{'part'} =~ /^[a-z]$/ or &error("Invalid partition letter");
|
|
my ($part) = grep { $_->{'letter'} eq $in{'part'} } @{ $slice->{'parts'} };
|
|
$part || &error( $text{'part_egone'} );
|
|
$object = $part;
|
|
}
|
|
else {
|
|
$object = $slice;
|
|
}
|
|
|
|
# If this device is part of a ZFS pool, offer dataset creation instead
|
|
my $zdev = get_zfs_device_info($object);
|
|
if ($zdev) {
|
|
&ui_print_header( $object->{'desc'}, $text{'newfs_title'}, "" );
|
|
|
|
# Parent pool summary (best effort)
|
|
my $parent = $zdev->{'pool'};
|
|
my $parent_q = quote_path($parent);
|
|
my $zlist = &backquote_command(
|
|
"zfs list -H -o name,used,avail,refer,mountpoint $parent_q 2>/dev/null"
|
|
);
|
|
if ($zlist) {
|
|
chomp($zlist);
|
|
my @cols = split( /\t/, $zlist );
|
|
if ( @cols >= 5 ) {
|
|
print "<b>$text{'newfs_zfs_parent'}</b>\n";
|
|
print ui_columns_start(
|
|
[
|
|
$text{'newfs_zfs_fs'}, $text{'newfs_zfs_used'},
|
|
$text{'newfs_zfs_avail'}, $text{'newfs_zfs_refer'},
|
|
$text{'newfs_zfs_mount'}
|
|
]
|
|
);
|
|
print ui_columns_row( [ map { html_escape($_) } @cols[ 0 .. 4 ] ] );
|
|
print ui_columns_end();
|
|
print "<br/>\n";
|
|
}
|
|
}
|
|
|
|
# Existing filesystems under this pool
|
|
my $zlist_fs = &backquote_command(
|
|
"zfs list -H -r -t filesystem -o name,used,avail,refer,mountpoint $parent_q 2>/dev/null"
|
|
);
|
|
if ($zlist_fs) {
|
|
my @lines = split( /\n/, $zlist_fs );
|
|
if ( @lines && $lines[0] =~ /^\Q$parent\E(\t|$)/ ) {
|
|
shift @lines;
|
|
}
|
|
if (@lines) {
|
|
print "<b>$text{'newfs_zfs_existing'}</b>\n";
|
|
print ui_columns_start(
|
|
[
|
|
$text{'newfs_zfs_fs'}, $text{'newfs_zfs_used'},
|
|
$text{'newfs_zfs_avail'}, $text{'newfs_zfs_refer'},
|
|
$text{'newfs_zfs_mount'}
|
|
]
|
|
);
|
|
foreach my $ln (@lines) {
|
|
my @cols = split( /\t/, $ln );
|
|
next unless @cols >= 5;
|
|
print ui_columns_row(
|
|
[ map { html_escape($_) } @cols[ 0 .. 4 ] ] );
|
|
}
|
|
print ui_columns_end();
|
|
print "<br/>\n";
|
|
}
|
|
}
|
|
|
|
# Existing volumes under this pool
|
|
my $zlist_vol = &backquote_command(
|
|
"zfs list -H -r -t volume -o name,used,avail,refer,volsize $parent_q 2>/dev/null"
|
|
);
|
|
if ($zlist_vol) {
|
|
my @lines = split( /\n/, $zlist_vol );
|
|
if ( @lines && $lines[0] =~ /^\Q$parent\E(\t|$)/ ) {
|
|
shift @lines;
|
|
}
|
|
if (@lines) {
|
|
print "<b>$text{'newfs_zvol_existing'}</b>\n";
|
|
print ui_columns_start(
|
|
[
|
|
$text{'newfs_zvol_vol'}, $text{'newfs_zfs_used'},
|
|
$text{'newfs_zfs_avail'}, $text{'newfs_zfs_refer'},
|
|
$text{'newfs_zvol_volsize'}
|
|
]
|
|
);
|
|
foreach my $ln (@lines) {
|
|
my @cols = split( /\t/, $ln );
|
|
next unless @cols >= 5;
|
|
print ui_columns_row(
|
|
[ map { html_escape($_) } @cols[ 0 .. 4 ] ] );
|
|
}
|
|
print ui_columns_end();
|
|
print "<br/>\n";
|
|
}
|
|
}
|
|
|
|
my %fs_descriptions = (
|
|
'recordsize' => {
|
|
'128K' => '128K (General/Default)',
|
|
'1M' => '1M (Media/Large files)',
|
|
'4M' => '4M',
|
|
'16K' => '16K (Database)',
|
|
'4K' => '4K (VM)'
|
|
},
|
|
'compression' => {
|
|
'lz4' => 'lz4 (Recommended)',
|
|
'off' => 'off (None)',
|
|
'gzip' => 'gzip (High compression)'
|
|
},
|
|
'atime' => {
|
|
'off' => 'off (Performance)',
|
|
'on' => 'on (Record access time)'
|
|
},
|
|
'sync' => {
|
|
'disabled' => 'disabled (Performance)',
|
|
'standard' => 'standard (Safety)',
|
|
'always' => 'always (Maximum Safety)'
|
|
},
|
|
'acltype' => {
|
|
'nfsv4' => 'nfsv4 (ZFS Default)',
|
|
'posixacl' => 'posixacl (Linux Default)'
|
|
},
|
|
'aclinherit' => {
|
|
'passthrough' => 'passthrough (SMB Recommended)',
|
|
'restricted' => 'restricted (ZFS Default)'
|
|
},
|
|
'aclmode' => {
|
|
'passthrough' => 'passthrough (SMB Recommended)',
|
|
'discard' => 'discard (ZFS Default)'
|
|
}
|
|
);
|
|
my @fs_order = (
|
|
'recordsize', 'compression', 'atime', 'sync',
|
|
'exec', 'canmount', 'acltype', 'aclinherit',
|
|
'aclmode', 'xattr'
|
|
);
|
|
my %fs_defaults = (
|
|
'recordsize' => '128K',
|
|
'compression' => 'lz4',
|
|
'atime' => 'off',
|
|
'sync' => 'default',
|
|
'acltype' => 'nfsv4',
|
|
'aclinherit' => 'passthrough',
|
|
'aclmode' => 'passthrough',
|
|
'canmount' => 'on',
|
|
'exec' => 'on',
|
|
'xattr' => 'sa',
|
|
);
|
|
my %fs_opts = (
|
|
'recordsize' =>
|
|
'512, 1K, 2K, 4K, 8K, 16K, 32K, 64K, 128K, 256K, 512K, 1M',
|
|
'compression' => 'on, off, lz4, gzip',
|
|
'atime' => 'on, off',
|
|
'sync' => 'standard, always, disabled',
|
|
'exec' => 'on, off',
|
|
'canmount' => 'on, off, noauto',
|
|
'acltype' => 'nfsv4, posixacl',
|
|
'aclinherit' =>
|
|
'discard, noallow, restricted, passthrough, passthrough-x',
|
|
'aclmode' => 'discard, groupmask, passthrough',
|
|
'xattr' => 'on, off, sa',
|
|
);
|
|
my %acl_tooltips = (
|
|
'acltype' => $text{'newfs_zfs_acltype_desc'},
|
|
'aclinherit' => $text{'newfs_zfs_aclinherit_desc'},
|
|
'aclmode' => $text{'newfs_zfs_aclmode_desc'},
|
|
);
|
|
|
|
my $base_url =
|
|
"newfs_form.cgi?device="
|
|
. $url_device
|
|
. "&slice=$url_slice";
|
|
$base_url .= "&part=$url_part" if ( $in{'part'} ne '' );
|
|
my @tabs = (
|
|
[ "zfs", $text{'newfs_zfs_tab_fs'}, $base_url . "&mode=zfs" ],
|
|
[ "zvol", $text{'newfs_zfs_tab_vol'}, $base_url . "&mode=zvol" ],
|
|
);
|
|
print &ui_tabs_start( \@tabs, "mode", $in{'mode'} || $tabs[0]->[0], 1 );
|
|
print &ui_tabs_start_tab( "mode", "zfs" );
|
|
|
|
print &ui_form_start( "zfs_create.cgi", "post", undef,
|
|
"onsubmit='return validateFsForm(this)'" );
|
|
print &ui_hidden( "device", $in{'device'} );
|
|
print &ui_hidden( "slice", $in{'slice'} );
|
|
print &ui_hidden( "part", $in{'part'} );
|
|
print &ui_hidden( "parent", $parent );
|
|
|
|
print &ui_table_start( $text{'newfs_zfs_header'}, 'width=100%', 2 );
|
|
print &ui_table_row( $text{'newfs_zfs_name'},
|
|
html_escape($parent) . "/" . &ui_textbox( "zfs", undef, 24 ) );
|
|
print &ui_table_row( $text{'newfs_zfs_mountpoint'},
|
|
&ui_filebox( 'mountpoint', '', 25, undef, undef, 1 ) . " ("
|
|
. $text{'newfs_zfs_mount_blank'}
|
|
. ")" );
|
|
print &ui_table_end();
|
|
|
|
print &ui_table_start( $text{'newfs_zfs_opts'}, "width=100%", undef );
|
|
foreach my $key (@fs_order) {
|
|
my @options;
|
|
push( @options, [ 'default', 'default' ] );
|
|
foreach my $opt ( split( ", ", $fs_opts{$key} ) ) {
|
|
my $label = $fs_descriptions{$key}{$opt} || $opt;
|
|
push( @options, [ $opt, $label ] );
|
|
}
|
|
my $default_val = $fs_defaults{$key} || 'default';
|
|
my $help =
|
|
$acl_tooltips{$key}
|
|
? "<br><small><i>$acl_tooltips{$key}</i></small>"
|
|
: "";
|
|
my $selected = defined( $in{$key} ) ? $in{$key} : $default_val;
|
|
print ui_table_row( $key . ': ',
|
|
ui_select( $key, $selected, \@options, 1, 0, 1 ) . $help );
|
|
}
|
|
my $add_inherit_default =
|
|
defined( $in{'add_inherit'} ) ? $in{'add_inherit'} : 1;
|
|
print ui_table_row(
|
|
$text{'newfs_zfs_aclflags'},
|
|
ui_checkbox(
|
|
'add_inherit', 1,
|
|
$text{'newfs_zfs_aclflags_desc'}, $add_inherit_default
|
|
)
|
|
);
|
|
print &ui_table_end();
|
|
print &ui_form_end( [ [ undef, $text{'create'} ] ] );
|
|
|
|
print &ui_tabs_end_tab( "mode", "zfs" );
|
|
|
|
# ZVOL tab
|
|
print &ui_tabs_start_tab( "mode", "zvol" );
|
|
my %zvol_defaults = (
|
|
'volblocksize' => '16K',
|
|
'compression' => 'lz4',
|
|
'sync' => 'default',
|
|
'logbias' => 'latency',
|
|
'primarycache' => 'all',
|
|
'secondarycache' => 'all',
|
|
);
|
|
my %zvol_opts = (
|
|
'volblocksize' => '512, 1K, 2K, 4K, 8K, 16K, 32K, 64K, 128K',
|
|
'compression' => 'on, off, lz4, gzip',
|
|
'sync' => 'standard, always, disabled',
|
|
'logbias' => 'latency, throughput',
|
|
'primarycache' => 'all, metadata, none',
|
|
'secondarycache' => 'all, metadata, none',
|
|
);
|
|
my %zvol_desc = (
|
|
'volblocksize' => {
|
|
'512' => '512B',
|
|
'1K' => '1K',
|
|
'2K' => '2K',
|
|
'4K' => '4K (Swap)',
|
|
'8K' => '8K (Databases)',
|
|
'16K' => '16K (VM/Default)',
|
|
'64K' => '64K (Backups)',
|
|
},
|
|
'logbias' => {
|
|
'latency' => 'latency (databases, NFS sync)',
|
|
'throughput' => 'throughput (VM, media/backups)',
|
|
},
|
|
'primarycache' => {
|
|
'all' => 'all (filesystems)',
|
|
'metadata' => 'metadata (VM, iSCSI)',
|
|
'none' => 'none (Swap)',
|
|
},
|
|
'secondarycache' => {
|
|
'all' => 'all (general filesystems)',
|
|
'metadata' => 'metadata (VM, databases)',
|
|
'none' => 'none (media)',
|
|
},
|
|
);
|
|
|
|
print &ui_form_start( "zvol_create.cgi", "post", undef,
|
|
"onsubmit='return validateZvolForm(this)'" );
|
|
print &ui_hidden( "device", $in{'device'} );
|
|
print &ui_hidden( "slice", $in{'slice'} );
|
|
print &ui_hidden( "part", $in{'part'} );
|
|
print &ui_hidden( "parent", $parent );
|
|
print &ui_table_start( $text{'newfs_zvol_header'}, 'width=100%', 2 );
|
|
print &ui_table_row( $text{'newfs_zvol_name'},
|
|
html_escape($parent) . "/" . &ui_textbox( "zvol", undef, 24 ) );
|
|
print &ui_table_row(
|
|
$text{'newfs_zvol_size'},
|
|
&ui_textbox(
|
|
'size', undef, 20, undef, undef, "oninput='updateRefres()'"
|
|
)
|
|
);
|
|
print &ui_table_end();
|
|
|
|
print &ui_table_start( $text{'newfs_zvol_opts'}, "width=100%", undef );
|
|
foreach my $key (
|
|
qw(volblocksize compression sync logbias primarycache secondarycache))
|
|
{
|
|
my @options;
|
|
push( @options, [ 'default', 'default' ] );
|
|
foreach my $opt ( split( ", ", $zvol_opts{$key} ) ) {
|
|
my $label = $zvol_desc{$key}{$opt} || $opt;
|
|
push( @options, [ $opt, $label ] );
|
|
}
|
|
my $default_val = $zvol_defaults{$key} || 'default';
|
|
my $selected = defined( $in{$key} ) ? $in{$key} : $default_val;
|
|
print ui_table_row( $key . ': ',
|
|
ui_select( $key, $selected, \@options, 1, 0, 1 ) );
|
|
}
|
|
my $sparse_default = defined( $in{'sparse'} ) ? $in{'sparse'} : 1;
|
|
print ui_table_row( $text{'newfs_zvol_sparse'},
|
|
ui_yesno_radio( 'sparse', $sparse_default )
|
|
. " <small>"
|
|
. $text{'newfs_zvol_sparse_desc'}
|
|
. "</small>" );
|
|
print ui_table_row( $text{'newfs_zvol_refreservation'},
|
|
ui_textbox( 'refreservation', 'none', 20 )
|
|
. "<span id='refres_label'></span>" );
|
|
print &ui_table_end();
|
|
print &ui_form_end( [ [ undef, $text{'create'} ] ] );
|
|
print &ui_tabs_end_tab( "mode", "zvol" );
|
|
|
|
print &ui_tabs_end(1);
|
|
|
|
print <<'EOF';
|
|
<script type="text/javascript">
|
|
function validateFsForm(form) {
|
|
var name = form.zfs.value;
|
|
var nameRegex = /^[a-zA-Z0-9_\-.:]+$/;
|
|
if (!name || !nameRegex.test(name)) {
|
|
alert("Invalid Name. Please use alphanumeric characters, -, _, ., or :");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
function validateZvolForm(form) {
|
|
var name = form.zvol.value;
|
|
var nameRegex = /^[a-zA-Z0-9_\-.:]+$/;
|
|
if (!name || !nameRegex.test(name)) {
|
|
alert("Invalid Name. Please use alphanumeric characters, -, _, ., or :");
|
|
return false;
|
|
}
|
|
if (name.indexOf("/") !== -1 || name.indexOf("@") !== -1 || name.indexOf("#") !== -1) {
|
|
alert("Invalid Name. Do not include '/', '@' or '#'.");
|
|
return false;
|
|
}
|
|
if (name.charAt(0) === "-" || name.charAt(0) === ".") {
|
|
alert("Invalid Name. It cannot start with '-' or '.'.");
|
|
return false;
|
|
}
|
|
if (name.indexOf("..") !== -1) {
|
|
alert("Invalid Name. It cannot contain '..'.");
|
|
return false;
|
|
}
|
|
var size = form.size.value;
|
|
var regex = /^\d+(\.\d+)?[KMGTP]?$/i;
|
|
if (!size || !regex.test(size) || parseFloat(size) <= 0) {
|
|
alert("Invalid size format. Please use format like 10G, 500M, etc.");
|
|
return false;
|
|
}
|
|
var ref = form.refreservation.value;
|
|
if (ref && ref.toLowerCase() !== "none") {
|
|
if (!regex.test(ref) || parseFloat(ref) <= 0) {
|
|
alert("Invalid refreservation format. Please use format like 10G, 500M, etc.");
|
|
return false;
|
|
}
|
|
// Compare sizes when possible
|
|
function toBytes(v) {
|
|
var m = v.match(/^(\d+(?:\.\d+)?)([KMGTP]?)$/i);
|
|
if (!m) return null;
|
|
var n = parseFloat(m[1]);
|
|
var u = (m[2] || "").toUpperCase();
|
|
var mult = 1;
|
|
if (u === "K") mult = 1024;
|
|
else if (u === "M") mult = 1024*1024;
|
|
else if (u === "G") mult = 1024*1024*1024;
|
|
else if (u === "T") mult = 1024*1024*1024*1024;
|
|
else if (u === "P") mult = 1024*1024*1024*1024*1024;
|
|
return n * mult;
|
|
}
|
|
var sizeB = toBytes(size);
|
|
var refB = toBytes(ref);
|
|
if (sizeB && refB && refB > sizeB) {
|
|
alert("Refreservation cannot be larger than the volume size.");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function updateRefres() {
|
|
var sparseList = document.getElementsByName("sparse");
|
|
var refres = document.getElementsByName("refreservation")[0];
|
|
var size = document.getElementsByName("size")[0];
|
|
var label = document.getElementById("refres_label");
|
|
if (!sparseList || sparseList.length === 0 || !refres || !size || !label) return;
|
|
var sparseOn = false;
|
|
for (var i = 0; i < sparseList.length; i++) {
|
|
if (sparseList[i].checked && sparseList[i].value === "1") {
|
|
sparseOn = true;
|
|
}
|
|
}
|
|
if (sparseOn) {
|
|
refres.disabled = false;
|
|
var sVal = size.value ? size.value : "Size";
|
|
label.innerHTML = " (" + sVal + " max, default: none)";
|
|
} else {
|
|
refres.disabled = true;
|
|
label.innerHTML = " (" + "Volume is thick provisioned)";
|
|
}
|
|
}
|
|
function bindSparseRadios() {
|
|
var sparseList = document.getElementsByName("sparse");
|
|
if (!sparseList) return;
|
|
for (var i = 0; i < sparseList.length; i++) {
|
|
sparseList[i].onclick = updateRefres;
|
|
}
|
|
}
|
|
window.onload = function() { updateRefres(); bindSparseRadios(); };
|
|
</script>
|
|
EOF
|
|
|
|
if ( $in{'part'} ne '' ) {
|
|
&ui_print_footer(
|
|
"edit_part.cgi?device=$url_device&slice=$url_slice&part=$url_part",
|
|
$text{'part_return'}
|
|
);
|
|
}
|
|
else {
|
|
&ui_print_footer(
|
|
"edit_slice.cgi?device=$url_device&slice=$url_slice",
|
|
$text{'slice_return'} );
|
|
}
|
|
exit;
|
|
}
|
|
|
|
# Default: UFS newfs form
|
|
&ui_print_header( $object->{'desc'}, $text{'newfs_title'}, "" );
|
|
|
|
my $confirm_msg = $text{'confirm_overwrite'}
|
|
|| 'You will destroy/overwrite existing data structures. Continue?';
|
|
my $confirm_js = $confirm_msg;
|
|
$confirm_js =~ s/\\/\\\\/g;
|
|
$confirm_js =~ s/'/\\'/g;
|
|
$confirm_js =~ s/\r?\n/\\n/g;
|
|
print &ui_form_start( "newfs.cgi", "post", undef,
|
|
"onsubmit=\"return confirm('$confirm_js')\"" );
|
|
print &ui_hidden( "device", $in{'device'} );
|
|
print &ui_hidden( "slice", $in{'slice'} );
|
|
print &ui_hidden( "part", $in{'part'} );
|
|
print &ui_table_start( $text{'newfs_header'}, undef, 2 );
|
|
|
|
print &ui_table_row( $text{'part_device'}, "<tt>$object->{'device'}</tt>" );
|
|
|
|
print &ui_table_row( $text{'newfs_free'},
|
|
&ui_opt_textbox( "free", undef, 4, $text{'newfs_deffree'} ) . "%" );
|
|
|
|
print &ui_table_row( $text{'newfs_trim'}, &ui_yesno_radio( "trim", 0 ) );
|
|
|
|
print &ui_table_row( $text{'newfs_label'},
|
|
&ui_opt_textbox( "label", undef, 20, $text{'newfs_none'} ) );
|
|
|
|
print &ui_table_end();
|
|
print &ui_form_end( [ [ undef, $text{'save'} ] ] );
|
|
|
|
if ( $in{'part'} ne '' ) {
|
|
&ui_print_footer(
|
|
"edit_part.cgi?device=$url_device&slice=$url_slice&part=$url_part",
|
|
$text{'part_return'}
|
|
);
|
|
}
|
|
else {
|
|
&ui_print_footer(
|
|
"edit_slice.cgi?device=$url_device&slice=$url_slice",
|
|
$text{'slice_return'} );
|
|
}
|