Compare commits

..

1 Commits

Author SHA1 Message Date
Ilia Ross
9342671b0b Fix Netplan deletion of interfaces in shared files
This PR fixes interface deletion when multiple interfaces share a Netplan YAML file.

Each deletion looks up the interface’s current position, so earlier deletions cannot leave stale line numbers.

Unrelated interfaces and global settings are preserved. Empty device sections are removed.

Live regression tests and nine native Netplan generation scenarios passed on Ubuntu.
2026-10-29 15:17:23 +01:00
59 changed files with 295 additions and 350 deletions

View File

@@ -22,7 +22,6 @@ my $fh = "IN";
&open_tempfile($fh, ">$temp1"); &open_tempfile($fh, ">$temp1");
foreach my $k ("emailAddress", "organizationalUnitName", "organizationName", foreach my $k ("emailAddress", "organizationalUnitName", "organizationName",
"stateOrProvinceName", "countryName", "commonName") { "stateOrProvinceName", "countryName", "commonName") {
$in{$k} =~ /:|\n|\0/ && &error($text{'cert_echars'});
&print_tempfile($fh, "$k = $in{$k}\n"); &print_tempfile($fh, "$k = $in{$k}\n");
} }
$in{'key'} =~ s/\s//g; $in{'key'} =~ s/\s//g;
@@ -32,12 +31,12 @@ my $cmd = &get_ssleay();
my $ssleay = &backquote_logged("$cmd ca -spkac $temp1 -out $temp2 -config $module_config_directory/openssl.cnf -days 1095 2>&1"); my $ssleay = &backquote_logged("$cmd ca -spkac $temp1 -out $temp2 -config $module_config_directory/openssl.cnf -days 1095 2>&1");
&unlink_file($temp1); &unlink_file($temp1);
if ($?) { if ($?) {
&error("<pre>".&html_escape($ssleay)."</pre>"); &error("<pre>$ssleay</pre>");
} }
else { else {
# Display status and redirect to actual cert file # Display status and redirect to actual cert file
&ui_print_unbuffered_header(undef, $text{'cert_title'}, ""); &ui_print_unbuffered_header(undef, $text{'cert_title'}, "");
print &text('cert_done', &html_escape($in{'commonName'})),"<p>\n"; print &text('cert_done', $in{'commonName'}),"<p>\n";
print &text('cert_pickup', "cert_output.cgi?file=$temp2"),"<p>\n"; print &text('cert_pickup', "cert_output.cgi?file=$temp2"),"<p>\n";
&ui_print_footer("", $text{'index_return'}); &ui_print_footer("", $text{'index_return'});

View File

@@ -7,7 +7,7 @@ use warnings;
no warnings 'redefine'; no warnings 'redefine';
no warnings 'uninitialized'; no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access, $config_directory, $base_remote_user); our (%in, %text, %config, %access, $config_directory);
&ReadParse(); &ReadParse();
$access{'groups'} || &error($text{'gedit_ecannot'}); $access{'groups'} || &error($text{'gedit_ecannot'});
my $g; my $g;
@@ -95,12 +95,6 @@ print &ui_hidden_table_start($text{'edit_mods'}, "width=100%", 2, "mods");
# Show available modules, under categories # Show available modules, under categories
my @mlist = &list_module_infos(); my @mlist = &list_module_infos();
my %has = map { $_, 1 } @{$group{'modules'}}; my %has = map { $_, 1 } @{$group{'modules'}};
# Disable modules that this administrator cannot grant or remove.
my $me = $access{'mode'} == 1 ? &get_user($base_remote_user) : undef;
@mcan = $access{'mode'} == 1 ? @{$me->{'modules'} || []} :
$access{'mode'} == 2 ? split(/\s+/, $access{'mods'}) :
map { $_->{'dir'} } @mlist;
my %mcan = map { $_, 1 } @mcan;
my @links = ( &select_all_link("mod", 0, $text{'edit_selall'}), my @links = ( &select_all_link("mod", 0, $text{'edit_selall'}),
&select_invert_link("mod", 0, $text{'edit_invert'}) ); &select_invert_link("mod", 0, $text{'edit_invert'}) );
my @cats = &unique(map { $_->{'category'} || "" } @mlist); my @cats = &unique(map { $_->{'category'} || "" } @mlist);
@@ -125,8 +119,7 @@ foreach my $c (sort { $b cmp $a } @cats) {
else { else {
$label = $m->{'desc'}; $label = $m->{'desc'};
} }
push(@grid, &ui_checkbox("mod", $md, $label, $has{$md}, push(@grid, &ui_checkbox("mod", $md, $label,$has{$md}));
undef, !$mcan{$md}));
} }
$grids .= &ui_grid_table(\@grid, 2, 100, [ "width=50%", "width=50%" ]); $grids .= &ui_grid_table(\@grid, 2, 100, [ "width=50%", "width=50%" ]);
} }

View File

@@ -175,7 +175,6 @@ cert_ekey=A new SSL key was not submitted by your browser - maybe it does not su
cert_eca=Failed to setup certificate authority : $1 cert_eca=Failed to setup certificate authority : $1
cert_already=Warning - you are already using the certificate $1. cert_already=Warning - you are already using the certificate $1.
cert_etempdir=Invalid certificate file cert_etempdir=Invalid certificate file
cert_echars=Invalid characters in cert field
acl_title=Module Access Control acl_title=Module Access Control
acl_title2=For $1 in $2 acl_title2=For $1 in $2

View File

@@ -7,7 +7,7 @@ use warnings;
no warnings 'redefine'; no warnings 'redefine';
no warnings 'uninitialized'; no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access, $config_directory, $base_remote_user); our (%in, %text, %config, %access, $config_directory);
&ReadParse(); &ReadParse();
$access{'groups'} || &error($text{'gedit_ecannot'}); $access{'groups'} || &error($text{'gedit_ecannot'});
@@ -21,10 +21,6 @@ elsif ($in{'but_delete'}) {
exit; exit;
} }
# Find logged-in webmin user
my @ulist = &list_users();
my ($me) = grep { $_->{'name'} eq $base_remote_user } @ulist;
my (%group, $old); my (%group, $old);
if ($in{'old'}) { if ($in{'old'}) {
# Get the original group # Get the original group
@@ -46,21 +42,6 @@ if (!$in{'old'} || $in{'old'} ne $in{'name'}) {
} }
$in{'desc'} !~ /:/ || &error($text{'gsave_edesc'}); $in{'desc'} !~ /:/ || &error($text{'gsave_edesc'});
# Check grantable modules before changing parent group membership.
$access{'mode'} == 1 && !$me && &error($text{'edit_egone'});
my @mcan = $access{'mode'} == 1 ? @{$me->{'modules'} || []} :
$access{'mode'} == 2 ? split(/\s+/, $access{'mods'}) :
&list_modules();
my %mcan = map { $_, 1 } @mcan;
# Keep existing modules that this administrator cannot change.
my %keep = map { $_, 1 } grep { !$mcan{$_} }
($old ? @{$old->{'modules'}} : ());
my @mods = split(/\0/, $in{'mod'});
foreach my $m (@mods) {
$mcan{$m} || $keep{$m} || &error(&text('save_emod', &html_escape($m)));
}
@mods = &unique(@mods, keys %keep);
# Find the current parent group # Find the current parent group
my $oldgroup; my $oldgroup;
if ($in{'old'}) { if ($in{'old'}) {
@@ -99,6 +80,9 @@ if (defined($in{'group'})) {
} }
} }
# Work out group modules
my @mods = split(/\0/, $in{'mod'});
if ($oldgroup) { if ($oldgroup) {
# Remove modules from the old parent group # Remove modules from the old parent group
@mods = grep { &indexof($_, @{$oldgroup->{'modules'}}) < 0 } @mods; @mods = grep { &indexof($_, @{$oldgroup->{'modules'}}) < 0 } @mods;

View File

@@ -103,7 +103,12 @@ if ($in{'temp'}) {
# Find logged-in webmin user # Find logged-in webmin user
my @ulist = &list_users(); my @ulist = &list_users();
my ($me) = grep { $_->{'name'} eq $base_remote_user } @ulist; my $me;
foreach my $u (@ulist) {
if ($u->{'name'} eq $base_remote_user) {
$me = $u;
}
}
# Find the current group # Find the current group
my $oldgroup = $in{'old'} ? &get_users_group($in{'old'}) : undef; my $oldgroup = $in{'old'} ? &get_users_group($in{'old'}) : undef;
@@ -153,7 +158,7 @@ my %mcan = map { $_, 1 } @mcan;
my @mods = split(/\0/, $in{'mod'}); my @mods = split(/\0/, $in{'mod'});
foreach my $m (@mods) { foreach my $m (@mods) {
$mcan{$m} || &error(&text('save_emod', &html_escape($m))); $mcan{$m} || &error(&text('save_emod', $m));
} }
if ($in{'old'}) { if ($in{'old'}) {
# Add modules that this user already has, but were not # Add modules that this user already has, but were not

View File

@@ -1474,25 +1474,6 @@ subtest 'save_user.cgi create privilege' => sub {
delete_user('admin'); delete_user('admin');
}; };
subtest 'save_user.cgi rejected module escaping' => sub {
_reset_fixture();
_seed_user_acl('admin', { create => 1, mode => 2, mods => 'proc' });
create_user({ name => 'admin', pass => 'x', modules => ['acl', 'proc'] });
# Ordinary and hostile module names must remain text in the error page.
for my $module ('cron', '<script>alert(1)</script>') {
my $r = run_cgi('save_user.cgi', {
name => 'newbie', pass_def => 1, mod => $module,
logouttime_def => 1, minsize_def => 1 });
my $expected = text('save_emod', html_escape($module));
like($r->{out}, qr/\Q$expected\E/, 'rejected module is escaped');
unlike($r->{out}, qr/<script>alert\(1\)<\/script>/,
'module error cannot inject a script');
_clear_caches();
ok(!get_user('newbie'), 'rejected module does not create an account');
}
};
# Unix conversion must not bypass the privileged-name guard in save_user.cgi. # Unix conversion must not bypass the privileged-name guard in save_user.cgi.
# An ACL-capable administrator can still deliberately convert such a user. # An ACL-capable administrator can still deliberately convert such a user.
subtest 'convert.cgi privileged username gate' => sub { subtest 'convert.cgi privileged username gate' => sub {
@@ -1669,125 +1650,6 @@ subtest 'save_group.cgi name validation' => sub {
ok(!$g, 'group with colon-bearing description not created'); ok(!$g, 'group with colon-bearing description not created');
}; };
# Group module limits must reject requests before any membership is changed.
subtest 'save_group.cgi module limits' => sub {
_reset_fixture();
create_user({ name => 'admin', pass => 'x', modules => ['acl', 'proc'] });
create_group({ name => 'parent', members => [], modules => ['proc'] });
_seed_user_acl('admin', { mode => 2, mods => 'proc', groups => 1,
gassign => '*' });
my $r = run_cgi('save_group.cgi', {
name => 'child', desc => 'd', group => 'parent', mod => ['proc', 'cron'] });
like($r->{out}, qr/You cannot grant access to module 'cron'/,
'explicit module limit rejects an unauthorized grant');
_clear_caches();
ok(!get_group('child'), 'rejected group is not created');
is_deeply(get_group('parent')->{members}, [],
'rejected save does not change parent membership');
$r = run_cgi('save_group.cgi', {
name => 'child', desc => 'd', group => 'parent', mod => 'proc' });
is($r->{status}, 0, 'allowed group creation exits successfully');
_clear_caches();
is_deeply(get_group('child')->{modules}, ['proc'], 'allowed module is saved');
is_deeply(get_group('parent')->{members}, ['@child'],
'successful save updates parent membership');
$r = run_cgi('save_group.cgi', {
old => 'child', name => 'child', desc => 'changed', group => '', mod => 'cron' });
like($r->{out}, qr/You cannot grant access to module 'cron'/,
'editing an existing group cannot grant an unauthorized module');
_clear_caches();
is_deeply(get_group('parent')->{members}, ['@child'],
'rejected edit does not detach the existing group');
my $payload = '<script>alert(1)</script>';
$r = run_cgi('save_group.cgi', { name => 'badmod', mod => $payload });
like($r->{out}, qr/&lt;script&gt;/, 'rejected module name is escaped');
unlike($r->{out}, qr/\Q$payload\E/, 'module error cannot inject HTML');
# Mode 1 uses the effective user's modules, including mapped logins.
_seed_user_acl('admin', { mode => 1, groups => 1 });
$r = run_cgi('save_group.cgi', { name => 'ownmods', mod => 'proc' },
user => 'mapped-user', env => { BASE_REMOTE_USER => 'admin' });
_clear_caches();
is_deeply(get_group('ownmods')->{modules}, ['proc'],
'mapped login uses the base user module list');
$r = run_cgi('save_group.cgi', { name => 'denied', mod => 'cron' });
like($r->{out}, qr/You cannot grant access to module 'cron'/,
'mode 1 rejects modules the base user does not hold');
delete_user('admin');
_seed_user_acl('admin', { mode => 1, groups => 1 });
$r = run_cgi('save_group.cgi', { name => 'missing', mod => 'proc' });
like($r->{out}, qr/\Q$text{'edit_egone'}\E/, 'missing user returns a normal error');
unlike($r->{err}, qr/undefined value|compilation errors/,
'missing user does not cause a Perl exception');
_clear_caches();
ok(!get_group('missing'), 'missing user cannot create a group');
# Unrestricted mode does not require the mode-1 user record.
_seed_user_acl('admin', { mode => 0, groups => 1 });
$r = run_cgi('save_group.cgi', { name => 'unrestricted', mod => 'cron' });
_clear_caches();
is_deeply(get_group('unrestricted')->{modules}, ['cron'],
'unrestricted module grants remain supported');
};
# Editing unrelated fields must retain modules outside the admin's grant list.
subtest 'existing group module preservation' => sub {
for my $mode (1, 2, 0) {
_reset_fixture();
create_user({ name => 'admin', pass => 'x', modules => ['acl', 'proc'] });
create_user({ name => 'member', pass => 'x', modules => ['proc', 'cron'] });
create_group({ name => 'existing', members => ['member'],
modules => ['proc', 'cron'], desc => 'before' });
_seed_user_acl('admin', { mode => $mode, mods => 'proc', groups => 1 });
my $r = run_cgi('edit_group.cgi', { group => 'existing' });
my ($checkbox) = $r->{out} =~ /(<input\b[^>]*value="cron"[^>]*>)/;
ok($checkbox && $checkbox =~ /checked/, "mode $mode shows existing cron access");
is($checkbox && $checkbox =~ /disabled/ ? 1 : 0, $mode ? 1 : 0,
"mode $mode disables only restricted modules");
# Disabled controls are omitted by the browser.
$r = run_cgi('save_group.cgi', {
old => 'existing', name => 'existing', desc => 'after', mod => 'proc' });
_clear_caches();
my $group = get_group('existing');
is($group->{desc}, 'after', "mode $mode permits description edits");
my @expected = $mode ? qw(cron proc) : qw(proc);
is_deeply([sort @{$group->{modules}}], \@expected,
"mode $mode preserves restricted modules but allows authorized removal");
is_deeply([sort @{get_user('member')->{modules}}], \@expected,
"mode $mode preserves the correct member access");
next if !$mode;
# An old form can still submit the existing restricted selection.
$r = run_cgi('save_group.cgi', {
old => 'existing', name => 'existing', desc => 'old form',
mod => ['proc', 'cron'] });
_clear_caches();
is(get_group('existing')->{desc}, 'old form',
"mode $mode accepts an unchanged restricted selection");
$r = run_cgi('save_group.cgi', {
old => 'existing', name => 'existing', desc => 'forged',
mod => ['proc', 'cron', 'webmin'] });
like($r->{out}, qr/You cannot grant access to module 'webmin'/,
"mode $mode rejects a new unauthorized module");
_clear_caches();
is(get_group('existing')->{desc}, 'old form',
"mode $mode rejects before writing changes");
$r = run_cgi('save_group.cgi', {
name => 'cloned', clone => 'existing', mod => ['proc', 'cron'] });
like($r->{out}, qr/You cannot grant access to module 'cron'/,
"mode $mode cannot grant restricted modules by cloning");
}
};
subtest 'save_user.cgi self-lockout guard' => sub { subtest 'save_user.cgi self-lockout guard' => sub {
# Editing yourself out of the acl module is refused — otherwise an # Editing yourself out of the acl module is refused — otherwise an
# admin could lock themselves out of Webmin entirely. # admin could lock themselves out of Webmin entirely.

View File

@@ -2,7 +2,7 @@ name=Partition Manager
category=hardware category=hardware
os_support=*-linux os_support=*-linux
desc=Partitions on Local Disks desc=Partitions on Local Disks
depends=mount proc depends=mount proc raid
longdesc=Create and edit partitions on local SCSI, IDE and SATA disks on Linux. longdesc=Create and edit partitions on local SCSI, IDE and SATA disks on Linux.
novserver=1 novserver=1
noopenvz=1 noopenvz=1

View File

@@ -297,7 +297,7 @@ else {
sub print_errors sub print_errors
{ {
my (@errors) = @_; my (@errors) = @_;
&ui_print_header(undef, $module_info{'desc'}, ""); &ui_print_header(undef, $module_info{'name'}, "");
print "<tt>$text{'errors_occured'}</tt><br>"; print "<tt>$text{'errors_occured'}</tt><br>";
print "<ul class=\"err-body\">"; print "<ul class=\"err-body\">";
foreach $error (@errors) { foreach $error (@errors) {

View File

@@ -13,7 +13,7 @@ unless (opendir(DIR, $cwd)) {
} }
else { else {
&ui_print_header( &ui_print_header(
undef, $module_info{'desc'}, "", undef, 0, 0, 0, undef, $module_info{'name'}, "", undef, 0, 0, 0,
"<a href='config.cgi?path=" . &urlize($path) . "<a href='config.cgi?path=" . &urlize($path) .
"' data-config-pagination='$userconfig{'per_page'}'>" . "' data-config-pagination='$userconfig{'per_page'}'>" .
"$text{'module_config'}</a>"); "$text{'module_config'}</a>");

View File

@@ -1,4 +1,5 @@
webmin=1 webmin=1
longdesc=Fast and light File Manager written in Perl. longdesc=Fast and light File Manager written in Perl.
name=File Manager
usermin=1 usermin=1
desc=File Manager desc=File Manager

View File

@@ -0,0 +1 @@
name_ar=مدير الملفات

View File

@@ -1,2 +1,3 @@
longdesc_bg=Бърз и лек файлов мениджър, написан на Perl. longdesc_bg=Бърз и лек файлов мениджър, написан на Perl.
desc_bg=Файлов мениджър desc_bg=Файлов мениджър
name_bg=Файлов мениджър

View File

@@ -0,0 +1 @@
name_ca=Gestor d'arxius

View File

@@ -1,2 +1,3 @@
longdesc_cs=Rychlý a lehký Správce souborů psaný v Perlu. longdesc_cs=Rychlý a lehký Správce souborů psaný v Perlu.
desc_cs=Správce souborů desc_cs=Správce souborů
name_cs=Správce souborů

View File

@@ -1,2 +1,3 @@
desc_da=Filhåndtering desc_da=Filhåndtering
name_da=Filhåndtering
longdesc_da=Hurtig og let File Manager skrevet i Perl. longdesc_da=Hurtig og let File Manager skrevet i Perl.

View File

@@ -0,0 +1 @@
name_de=Dateimanager

View File

@@ -1,2 +1,3 @@
longdesc_el=Γρήγορος και ελαφρύς διαχειριστής αρχείων γραμμένος σε Perl. longdesc_el=Γρήγορος και ελαφρύς διαχειριστής αρχείων γραμμένος σε Perl.
desc_el=Διαχείριση αρχείων desc_el=Διαχείριση αρχείων
name_el=Διαχείριση αρχείων

View File

@@ -1,2 +1,3 @@
desc_es=Administrador de archivos desc_es=Administrador de archivos
name_es=Administrador de archivos
longdesc_es=Administrador de archivos rápido y ligero escrito en Perl. longdesc_es=Administrador de archivos rápido y ligero escrito en Perl.

View File

@@ -1,2 +1,3 @@
name_eu=Fitxategi kudeatzailea
desc_eu=Fitxategi kudeatzailea desc_eu=Fitxategi kudeatzailea
longdesc_eu=Fitxategi kudeatzaile azkarra eta arina Perlen idatzita. longdesc_eu=Fitxategi kudeatzaile azkarra eta arina Perlen idatzita.

View File

@@ -1,2 +1,3 @@
name_fa=مدیر فایل
longdesc_fa=مدیر سریع و سبک فایل در Perl نوشته شده است. longdesc_fa=مدیر سریع و سبک فایل در Perl نوشته شده است.
desc_fa=مدیر فایل desc_fa=مدیر فایل

View File

@@ -1,2 +1,3 @@
name_fi=Tiedostonhallinta
desc_fi=Tiedostonhallinta desc_fi=Tiedostonhallinta
longdesc_fi=Nopea ja kevyt File Manager kirjoitettu Perl. longdesc_fi=Nopea ja kevyt File Manager kirjoitettu Perl.

View File

@@ -1 +0,0 @@
desc_fr=Gestionnaire de Fichiers

View File

@@ -1,2 +1,3 @@
longdesc_fr=Gestionnaire de fichiers rapide et léger écrit en Perl. longdesc_fr=Gestionnaire de fichiers rapide et léger écrit en Perl.
desc_fr=Gestionnaire de fichiers desc_fr=Gestionnaire de fichiers
name_fr=Gestionnaire de fichiers

View File

@@ -1,2 +1,3 @@
desc_hr=Upravitelj datoteka desc_hr=Upravitelj datoteka
name_hr=Upravitelj datoteka
longdesc_hr=Brz i lagan Upravitelj datoteka napisan na Perlu. longdesc_hr=Brz i lagan Upravitelj datoteka napisan na Perlu.

View File

@@ -1,2 +1,3 @@
desc_hu=Fájl kezelő desc_hu=Fájl kezelő
name_hu=Fájl kezelő
longdesc_hu=Gyors és könnyű fájlkezelő, Perl nyelven írva. longdesc_hu=Gyors és könnyű fájlkezelő, Perl nyelven írva.

View File

@@ -1,2 +1,3 @@
desc_it=Gestore file desc_it=Gestore file
name_it=Gestore file
longdesc_it=Gestore file veloce e leggero scritto in Perl. longdesc_it=Gestore file veloce e leggero scritto in Perl.

View File

@@ -1,2 +1,3 @@
name_ja=ファイル管理
desc_ja=ファイル管理 desc_ja=ファイル管理
longdesc_ja=Perlで書かれた高速で軽量のファイルマネージャー。 longdesc_ja=Perlで書かれた高速で軽量のファイルマネージャー。

View File

@@ -1,2 +1,3 @@
desc_ko=파일 관리자 desc_ko=파일 관리자
name_ko=파일 관리자
longdesc_ko=Perl로 작성된 빠르고 가벼운 파일 관리자. longdesc_ko=Perl로 작성된 빠르고 가벼운 파일 관리자.

View File

@@ -1,2 +1,3 @@
desc_ms=Pengurus Fail desc_ms=Pengurus Fail
name_ms=Pengurus Fail
longdesc_ms=Pengurus Fail pantas dan ringan yang ditulis dalam Perl. longdesc_ms=Pengurus Fail pantas dan ringan yang ditulis dalam Perl.

View File

@@ -1,2 +1,3 @@
name_nl=Bestandsbeheer
desc_nl=Bestandsbeheer desc_nl=Bestandsbeheer
longdesc_nl=Snelle en lichte File Manager geschreven in Perl. longdesc_nl=Snelle en lichte File Manager geschreven in Perl.

View File

@@ -1 +1,2 @@
name_no=Filbehandler
longdesc_no=Rask og lett File Manager skrevet i Perl. longdesc_no=Rask og lett File Manager skrevet i Perl.

View File

@@ -1,2 +1,3 @@
desc_pl=Menedżer plików desc_pl=Menedżer plików
longdesc_pl=Szybki i lekki menedżer plików napisany w Perlu. longdesc_pl=Szybki i lekki menedżer plików napisany w Perlu.
name_pl=Menedżer plików

View File

@@ -1,2 +1,3 @@
desc_pt=Gerenciador de arquivos desc_pt=Gerenciador de arquivos
name_pt=Gerenciador de arquivos
longdesc_pt=Gerenciador de arquivos rápido e leve, escrito em Perl. longdesc_pt=Gerenciador de arquivos rápido e leve, escrito em Perl.

View File

@@ -1,2 +1,3 @@
desc_pt_BR=Gerenciador de arquivos desc_pt_BR=Gerenciador de arquivos
name_pt_BR=Gerenciador de arquivos
longdesc_pt_BR=Gerenciador de arquivos rápido e leve, escrito em Perl. longdesc_pt_BR=Gerenciador de arquivos rápido e leve, escrito em Perl.

View File

@@ -1,2 +1,3 @@
longdesc_ru=Быстрый и легкий файловый менеджер, написанный на Perl. longdesc_ru=Быстрый и легкий файловый менеджер, написанный на Perl.
name_ru=Файловый менеджер
desc_ru=Файловый менеджер desc_ru=Файловый менеджер

View File

@@ -1,2 +1,3 @@
desc_sk=Správca súborov desc_sk=Správca súborov
name_sk=Správca súborov
longdesc_sk=Rýchly a ľahký Správca súborov napísaný v jazyku Perl. longdesc_sk=Rýchly a ľahký Správca súborov napísaný v jazyku Perl.

View File

@@ -1,2 +1,3 @@
desc_sv=Filhanterare desc_sv=Filhanterare
name_sv=Filhanterare
longdesc_sv=Snabb och lätt File Manager skriven i Perl. longdesc_sv=Snabb och lätt File Manager skriven i Perl.

View File

@@ -0,0 +1 @@
name_tr=Dosya Yöneticisi

View File

@@ -1,4 +1,6 @@
name_uk=Файловий менеджер
desc_uk=Файловий менеджер desc_uk=Файловий менеджер
longdesc_uk=Швидкий і легкий менеджер файлів, написаний на Perl. longdesc_uk=Швидкий і легкий менеджер файлів, написаний на Perl.
name=Файловий менеджер
desc=Файловий менеджер desc=Файловий менеджер
longdesc=Швидкий і легкий менеджер файлів, написаний на Perl. longdesc=Швидкий і легкий менеджер файлів, написаний на Perl.

View File

@@ -1,2 +1,3 @@
name_uk=Файловий менеджер
desc_uk=Файловий менеджер desc_uk=Файловий менеджер
longdesc_uk=Швидкий і легкий менеджер файлів, написаний на Perl. longdesc_uk=Швидкий і легкий менеджер файлів, написаний на Perl.

View File

@@ -1,2 +1,3 @@
longdesc_zh=用Perl编写的快捷文件管理器。 longdesc_zh=用Perl编写的快捷文件管理器。
name_zh=文件管理器
desc_zh=文件管理器 desc_zh=文件管理器

View File

@@ -1,2 +1,3 @@
desc_zh_TW=文件管理器 desc_zh_TW=文件管理器
longdesc_zh_TW=用Perl編寫的快捷文件管理器。 longdesc_zh_TW=用Perl編寫的快捷文件管理器。
name_zh_TW=文件管理器

View File

@@ -9,7 +9,6 @@ main_none=Vous n'avez accès à aucun modules Webmin.
main_skill=Niveau de compétence main_skill=Niveau de compétence
main_readonly=(Mode lecture seule) main_readonly=(Mode lecture seule)
main_return=Retourner à $1 main_return=Retourner à $1
main_unused=Modules Inutilisés
link_essl=The Net::Le module SSLeay Perl requis pour créer des connections HTTPS n'est pas installé sur votre système. link_essl=The Net::Le module SSLeay Perl requis pour créer des connections HTTPS n'est pas installé sur votre système.
@@ -21,7 +20,6 @@ category_net=Réseau
category_syslet=Syslets category_syslet=Syslets
category_cluster=Cluster category_cluster=Cluster
category_info=Information category_info=Information
category_=Outils
longcategory_servers=Modules pour les configurations web, email, FTP et autres serveurs longcategory_servers=Modules pour les configurations web, email, FTP et autres serveurs
longcategory_hardware=Modules pour les imprimantes, disques et autres configurations de matériel longcategory_hardware=Modules pour les imprimantes, disques et autres configurations de matériel

View File

@@ -904,9 +904,6 @@ sub device_input
{ {
local (%used, $vg, $pv, $d, $p); local (%used, $vg, $pv, $d, $p);
# Dropdown labels need plain text sizes without theme HTML.
local $main::theme_forbid_nice_size = 1;
# Find partitions that are part of an LVM # Find partitions that are part of an LVM
foreach $vg (&list_volume_groups()) { foreach $vg (&list_volume_groups()) {
foreach $pv (&list_physical_volumes($vg->{'name'})) { foreach $pv (&list_physical_volumes($vg->{'name'})) {
@@ -934,16 +931,14 @@ foreach $d (&fdisk::list_disks_partitions()) {
} }
} }
# Show available RAID devices when the module is installed # Show available RAID devices
if ($has_raid) { local $conf = &raid::get_raidtab();
local $conf = &raid::get_raidtab(); foreach $c (@$conf) {
foreach $c (@$conf) {
next if ($used{$c->{'value'}}); next if ($used{$c->{'value'}});
local @ds = &device_status($c->{'value'}); local @ds = &device_status($c->{'value'});
next if (@ds); next if (@ds);
push(@opts, [ $c->{'value'}, &text('pv_raid', $c->{'value'} =~ /md(\d+)$/ ? "$1" : $c->{'value'}) ]); push(@opts, [ $c->{'value'}, &text('pv_raid', $c->{'value'} =~ /md(\d+)$/ ? "$1" : $c->{'value'}) ]);
} }
}
push(@opts, [ '', $text{'pv_other'} ]); push(@opts, [ '', $text{'pv_other'} ]);
return &ui_select("device", $opts[0]->[0], \@opts)." ". return &ui_select("device", $opts[0]->[0], \@opts)." ".

View File

@@ -354,13 +354,32 @@ if ($iface->{'virtual'} ne '') {
&save_interface($parent, $boot); &save_interface($parent, $boot);
} }
else { else {
# Delete all the lines # Find current lines because earlier deletions may have shifted them.
&lock_file($iface->{'file'}); &lock_file($iface->{'file'});
&unflush_file_lines($iface->{'file'});
my $yaml = &read_yaml_file($iface->{'file'});
my @network = grep { $_->{'name'} eq 'network' } @$yaml;
my @groups = grep { $_->{'name'} eq 'ethernets' ||
$_->{'name'} eq 'bridges' }
map { @{$_->{'members'}} } @network;
my $entry;
foreach my $g (@groups) {
my ($e) = grep { $_->{'name'} eq $iface->{'fullname'} }
@{$g->{'members'}};
next if (!$e);
# Remove the group as well when deleting its last interface.
$entry = @{$g->{'members'}} == 1 ? $g : $e;
last;
}
$entry || &error("No interface named $iface->{'fullname'} exists");
# Delete the interface without changing neighboring definitions.
my $lref = &read_file_lines($iface->{'file'}); my $lref = &read_file_lines($iface->{'file'});
splice(@$lref, $iface->{'line'}, splice(@$lref, $entry->{'line'},
$iface->{'eline'} - $iface->{'line'} + 1); $entry->{'eline'} - $entry->{'line'} + 1);
&flush_file_lines($iface->{'file'}); &flush_file_lines($iface->{'file'});
if (&is_yaml_empty($iface->{'file'})) { if (&is_yaml_empty($iface->{'file'})) {
# Discard the file only when no settings or interfaces remain.
&unlink_file($iface->{'file'}); &unlink_file($iface->{'file'});
} }
&unlock_file($iface->{'file'}); &unlock_file($iface->{'file'});
@@ -368,22 +387,22 @@ else {
} }
# is_yaml_empty(file) # is_yaml_empty(file)
# Return 1 if a YAML file contains only network and ethernets line, with no # Returns 1 if only the version and empty Ethernet or bridge groups remain
# other interfaces
sub is_yaml_empty sub is_yaml_empty
{ {
my ($file) = @_; my ($file) = @_;
my $yaml = &read_yaml_file($file); my $yaml = &read_yaml_file($file);
return 1 if (!$yaml); return 1 if (!$yaml);
my @rest = grep { $_->{'name'} ne 'network' } @$yaml;
return 0 if (@rest);
foreach my $n (@$yaml) { foreach my $n (@$yaml) {
my @rest = grep { $_->{'name'} ne 'ethernets' && # Preserve unknown directives and inline network settings.
$_->{'name'} ne 'bridges' } return 0 if ($n->{'name'} ne 'network' ||
@{$network->{'members'}}; ($n->{'value'} ne '' && $n->{'value'} ne '{}'));
return 0 if (@rest); # Only the version and empty device groups are safe to discard.
foreach my $ens (@{$network->{'members'}}) { foreach my $ens (@{$n->{'members'}}) {
return 0 if (@{$ens->{'members'}}); next if ($ens->{'name'} eq 'version');
return 0 if (($ens->{'name'} ne 'ethernets' &&
$ens->{'name'} ne 'bridges') || @{$ens->{'members'}} ||
($ens->{'value'} ne '' && $ens->{'value'} ne '{}'));
} }
} }
return 1; return 1;

View File

@@ -5,7 +5,7 @@
require './proc-lib.pl'; require './proc-lib.pl';
&ReadParse(); &ReadParse();
&switch_acl_uid(); &switch_acl_uid();
&error_setup(&text('kill_err', &html_escape($in{signal}), &html_escape($in{pid}))); &error_setup(&text('kill_err', $in{signal}, $in{pid}));
foreach $s ('KILL', 'TERM', 'HUP', 'STOP', 'CONT') { foreach $s ('KILL', 'TERM', 'HUP', 'STOP', 'CONT') {
$in{'signal'} = $s if ($in{$s}); $in{'signal'} = $s if ($in{$s});
} }

View File

@@ -14,10 +14,10 @@ foreach $s ('KILL', 'TERM', 'HUP', 'STOP', 'CONT') {
@pinfo = &list_processes(@pidlist); @pinfo = &list_processes(@pidlist);
for($i=0; $i<@pidlist; $i++) { for($i=0; $i<@pidlist; $i++) {
$in{"args$i"} = $pinfo[$i]->{'args'}; $in{"args$i"} = $pinfo[$i]->{'args'};
print "$text{'pid'} <tt>".&html_escape($pidlist[$i])."</tt> ... \n"; print "$text{'pid'} <tt>$pidlist[$i]</tt> ... \n";
if (&can_edit_process($pinfo[$i]->{'user'})) { if (&can_edit_process($pinfo[$i]->{'user'})) {
if (&kill_logged($in{signal}, $pidlist[$i])) { if (&kill_logged($in{signal}, $pidlist[$i])) {
print "SIG".&html_escape($in{'signal'})." $text{'kill_sent'}<br>\n"; print "SIG$in{signal} $text{'kill_sent'}<br>\n";
} }
else { else {
print "$!<br>\n"; print "$!<br>\n";
@@ -29,6 +29,5 @@ for($i=0; $i<@pidlist; $i++) {
} }
&webmin_log("kill", undef, undef, \%in); &webmin_log("kill", undef, undef, \%in);
print "<p>\n"; print "<p>\n";
&ui_print_footer("index_search.cgi?".&html_escape($in{'args'}), &ui_print_footer("index_search.cgi?$in{'args'}", $text{'search_return'});
$text{'search_return'});

View File

@@ -435,7 +435,6 @@ comment_header=Table comment details
comment_msg=Message comment_msg=Message
comment_err=Failed to save comment comment_err=Failed to save comment
comment_enone=No message entered comment_enone=No message entered
comment_ereturn=Invalid message entered
log_create_table=Added entry to the $1 table log_create_table=Added entry to the $1 table
log_modify_table=Modified entry in the $1 table log_modify_table=Modified entry in the $1 table

View File

@@ -59,7 +59,7 @@ if (@table) {
push(@cols, &ui_link( push(@cols, &ui_link(
"editcmt.cgi?table=".&urlize($in{'table'}). "editcmt.cgi?table=".&urlize($in{'table'}).
"&idx=".&urlize($i), "&idx=".&urlize($i),
"<i>".&html_escape(join(" ",@t[1..$#t]))."</i>")); "<i>".join(" ", @t[1..$#t])."</i>"));
@tds = ( "width=5", "colspan=".scalar(@colnames) ); @tds = ( "width=5", "colspan=".scalar(@colnames) );
} }
else { else {
@@ -71,11 +71,10 @@ if (@table) {
if ($j == 0) { if ($j == 0) {
$lnk = &ui_link( $lnk = &ui_link(
"edit.cgi?table=".&urlize($in{'table'}). "edit.cgi?table=".&urlize($in{'table'}).
"&idx=".&urlize($i), "&idx=".&urlize($i), $t[$j]);
&html_escape($t[$j]));
} }
else { else {
$lnk = &html_escape($t[$j]); $lnk = $t[$j];
} }
push(@cols, $lnk); push(@cols, $lnk);
} }

View File

@@ -15,7 +15,6 @@ if ($in{'delete'}) {
else { else {
# Validate inputs # Validate inputs
$in{'msg'} =~ /\S/ || &error($text{'comment_enone'}); $in{'msg'} =~ /\S/ || &error($text{'comment_enone'});
$in{'msg'} =~ /\r|\0/ && &error($text{'comment_ereturn'});
$line = (&version_atleast(4, 5, 11) ? "?" : "") . "COMMENT $in{'msg'}"; $line = (&version_atleast(4, 5, 11) ? "?" : "") . "COMMENT $in{'msg'}";
# Update or add # Update or add

View File

@@ -421,7 +421,6 @@ comment_header=Table comment details
comment_msg=Message comment_msg=Message
comment_err=Failed to save comment comment_err=Failed to save comment
comment_enone=No message entered comment_enone=No message entered
comment_ereturn=Invalid message entered
log_create_table=Added entry to the $1 table log_create_table=Added entry to the $1 table
log_modify_table=Modified entry in the $1 table log_modify_table=Modified entry in the $1 table

View File

@@ -57,10 +57,8 @@ if (@table) {
local @tds; local @tds;
if ($t[0] =~ /\??COMMENT/) { if ($t[0] =~ /\??COMMENT/) {
# Special case - a comment line # Special case - a comment line
push(@cols, "<a href='editcmt.cgi?table=". push(@cols, "<a href='editcmt.cgi?table=$in{'table'}&".
&urlize($in{'table'})."&idx=". "idx=$i'><i>".join(" ", @t[1..$#t]).
&urlize($i)."'><i>".
&html_escape(join(" ", @t[1..$#t])).
"</i></a>" ); "</i></a>" );
@tds = ( "width=5", "colspan=".scalar(@colnames) ); @tds = ( "width=5", "colspan=".scalar(@colnames) );
} }
@@ -71,13 +69,10 @@ if (@table) {
} }
for($j=0; $j<@colnames; $j++) { for($j=0; $j<@colnames; $j++) {
if ($j == 0) { if ($j == 0) {
$lnk = &ui_link( $lnk = &ui_link("edit.cgi?table=$in{'table'}&idx=$i",$t[$j]);
"edit.cgi?table=".&urlize($in{'table'}).
"&idx=".&urlize($i),
&html_escape($t[$j]));
} }
else { else {
$lnk = &html_escape($t[$j]); $lnk = $t[$j];
} }
push(@cols, $lnk); push(@cols, $lnk);
} }

View File

@@ -15,7 +15,6 @@ if ($in{'delete'}) {
else { else {
# Validate inputs # Validate inputs
$in{'msg'} =~ /\S/ || &error($text{'comment_enone'}); $in{'msg'} =~ /\S/ || &error($text{'comment_enone'});
$in{'msg'} =~ /\r|\0/ && &error($text{'comment_ereturn'});
$line = (&version_atleast(4, 5, 11) ? "?" : "") . "COMMENT $in{'msg'}"; $line = (&version_atleast(4, 5, 11) ? "?" : "") . "COMMENT $in{'msg'}";
# Update or add # Update or add

View File

@@ -523,8 +523,8 @@ close(PKG);
while(<PKG>) { while(<PKG>) {
s/\r|\n//g; s/\r|\n//g;
next if (/^(Loaded|updateinfo)/); next if (/^(Loaded|updateinfo)/);
if (/^\S+\s+(\S*[Ss]ec\S*)\s+(\S+?)\-([0-9]\S+)\.([^\.]+)$/) { if (/^\S+\s+\S+\s+(\S+?)\-([0-9]\S+)\.([^\.]+)$/) {
local ($name, $ver) = ($2, $3); local ($name, $ver) = ($1, $2);
if ($done->{$name}) { if ($done->{$name}) {
$done->{$name}->{'source'} ||= 'security'; $done->{$name}->{'source'} ||= 'security';
$done->{$name}->{'security'} = 1; $done->{$name}->{'security'} = 1;

View File

@@ -1,118 +0,0 @@
#!/usr/bin/perl
# Exercise CGI escaping without issuing certificates or signaling processes.
use strict;
use warnings;
no warnings qw(once redefine uninitialized);
use Test::More;
use FindBin;
my $root = "$FindBin::Bin/..";
require "$root/web-lib-funcs.pl";
require "$root/ui-lib.pl";
$INC{'./acl-lib.pl'} = $INC{'./proc-lib.pl'} = 1;
*main::ReadParse = *main::switch_acl_uid = *main::webmin_log = sub { };
*main::error_setup = *main::get_miniserv_config = sub { };
*main::error = sub { die $_[0]."\n" };
*main::get_module_variable = sub { return \%main::text };
*main::ui_print_unbuffered_header = sub { };
*main::load_theme_library = sub { };
*main::get_module_name = sub { 'proc' };
*main::get_module_info = sub { return () };
*main::get_webprefix = sub { '' };
for my $file ('lang/en', 'acl/lang/en', 'proc/lang/en') {
open(my $fh, '<', "$root/$file") or die $!;
while (<$fh>) {
$main::text{$1} = $2 if /^(\S+)=(.*)$/;
}
}
# render_cgi(file, inputs) captures the real CGI and UI helpers.
sub render_cgi
{
my ($file, $inputs) = @_;
local %main::in = %$inputs;
my $html = '';
open(my $out, '>', \$html) or die $!;
local *STDOUT = $out;
do "$root/$file";
return ($html, $@);
}
subtest 'certificate success and error output' => sub {
# Stub only certificate I/O and account writes; preserve submitted values.
my ($saved, $openssl_error);
local *main::transname = local *main::tempname = sub { '/fixture/cert' };
local *main::open_tempfile = local *main::print_tempfile =
local *main::close_tempfile = local *main::unlink_file = sub { };
local *main::get_ssleay = sub { 'openssl' };
local *main::backquote_logged = sub {
$? = $openssl_error ? 256 : 0;
return $openssl_error || '';
};
local *main::list_users = sub { return {name => 'fixture'} };
# Stop after the tested account write, before the CGI's delay and restart.
local *main::modify_user = sub { $saved = $_[1]->{cert}; die "fixture done\n" };
local $main::base_remote_user = 'fixture';
my $payload = '<script>alert(1)</script>';
for my $name ('Review User', q{O'Reilly & Partners}, $payload) {
my ($html, $error) = render_cgi('acl/cert_issue.cgi',
{ key => 'fixture-key', commonName => $name });
is($error, "fixture done\n", 'successful issuance reaches account update');
like($html, qr/\Q@{[main::html_escape($name)]}\E/,
'common name appears as text');
unlike($html, qr/\Q$payload\E/, 'common name cannot introduce a script');
like($saved, qr/\Q\/CN=$name\/Email=\E/, 'stored DN retains the original name');
}
$openssl_error = "Invalid subject $payload\n";
my ($html, $error) = render_cgi('acl/cert_issue.cgi',
{ key => 'fixture-key', commonName => 'Review User' });
like($error, qr/<pre>Invalid subject &lt;script&gt;/,
'OpenSSL diagnostics retain preformatted markup and escape input');
unlike($error, qr/\Q$payload\E/, 'OpenSSL errors cannot introduce a script');
};
subtest 'single-process permission error heading' => sub {
my ($heading, $pid);
local *main::error_setup = sub { $heading = $_[0] };
local *main::process_info = sub { $pid = $_[0]; return (user => 'fixture') };
local *main::can_edit_process = sub { 0 };
local *main::kill_logged = sub { die "unexpected signal\n" };
my $payload = '<script>alert(1)</script>';
for my $inputs ({signal => 'TERM', pid => '12345'},
{signal => $payload, pid => $payload}) {
my ($html, $error) = render_cgi('proc/kill_proc.cgi', $inputs);
is($error, "$main::text{'kill_ecannot'}\n", 'signaling is denied');
is($heading, main::text('kill_err',
main::html_escape($inputs->{signal}),
main::html_escape($inputs->{pid})),
'error heading renders submitted values as text');
is($pid, $inputs->{pid}, 'process lookup receives the original PID');
}
};
subtest 'process output and return link' => sub {
my @signals;
local *main::list_processes = sub { return map { +{user => 'fixture'} } @_ };
local *main::can_edit_process = sub { 1 };
local *main::kill_logged = sub { push @signals, [@_]; return 1 };
my $query = 'search=worker%20name&match=1';
my ($html, $error) = render_cgi('proc/kill_proc_list.cgi',
{pidlist => '12345 12346', signal => 'TERM', args => $query});
is($error, '', 'ordinary request renders successfully');
is_deeply(\@signals, [['TERM', '12345'], ['TERM', '12346']],
'signal and PID arguments are unchanged');
$html =~ /href="(index_search\.cgi\?[^"]+)"/;
is(main::html_unescape($1), "index_search.cgi?$query",
'HTML escaping preserves the return query');
my $payload = '<script>alert(1)</script>';
($html, $error) = render_cgi('proc/kill_proc_list.cgi',
{pidlist => $payload, signal => $payload, args => '">'.$payload});
is($error, '', 'hostile values render successfully');
unlike($html, qr/\Q$payload\E/, 'PID, signal and return link cannot inject scripts');
like($html, qr/SIG&lt;script&gt;/, 'signal output is escaped');
like($html, qr/href="index_search.cgi\?&quot;&gt;&lt;script&gt;/,
'return link cannot escape its quoted attribute');
};
done_testing();

123
t/net-netplan-vm.t Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/perl
# Run with WEBMIN_NETPLAN_VM_TEST=1 on a Linux debug VM with Netplan installed.
use strict;
use warnings;
no warnings 'once';
use Test::More;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../net";
plan skip_all => 'Set WEBMIN_NETPLAN_VM_TEST=1 on a Linux debug VM'
unless ($ENV{'WEBMIN_NETPLAN_VM_TEST'} || '') eq '1';
$^O eq 'linux' or die 'Linux VM required';
require "$FindBin::Bin/../web-lib-funcs.pl";
require "$FindBin::Bin/../net/netplan-lib.pl";
local %main::gconfig = ();
local $main::readonly_mode_cache = 0;
# Load real address helpers without initializing the VM's Webmin configuration.
my $net_lib = main::read_file_contents("$FindBin::Bin/../net/net-lib.pl");
foreach my $name (qw(prefix_to_mask mask_to_prefix log2)) {
my ($code) = $net_lib =~ /^(sub \Q$name\E\n\{.*?^\})/ms;
$code or die "Cannot find $name";
eval "no strict; $code";
die $@ if $@;
}
# Keep realistic addresses, DNS, routes, and comments in the surviving port.
my $header = "network:\n version: 2\n";
my $global = $header." renderer: networkd\n";
my $ethernet = <<'YAML';
ethernets:
eth0:
# Preserve the port's static settings.
addresses: [192.0.2.10/24, '2001:db8::10/64']
nameservers:
addresses: [192.0.2.53]
search: [example.test]
routes:
- to: default
via: 192.0.2.1
eth1:
dhcp4: true
YAML
my $bridge0 = " br0:\n interfaces: [eth0]\n";
my $bridge1 = " br1:\n interfaces: [eth1]\n";
my $standalone = " br0:\n interfaces: []\n dhcp4: true\n";
my $other_groups = <<'YAML';
vlans:
vlan10:
id: 10
link: eth0
bonds:
bond0:
interfaces: [eth1]
parameters:
mode: active-backup
YAML
my $inline = " ethernets: {eth0: {dhcp4: true}}\n";
my $bulk = $global." ethernets: {}\n bridges:\n".
" br0:\n dhcp4: false\n br1:\n dhcp4: true\n";
# Each case supplies valid input, deletion order, and exact expected contents.
foreach my $case (
['shared bridge', $header.$ethernet." bridges:\n".$bridge0,
['br0'], $header.$ethernet],
['remaining bridge', $header.$ethernet." bridges:\n".$bridge0.$bridge1,
['br0'], $header.$ethernet." bridges:\n".$bridge1],
['last Ethernet', $header." ethernets:\n eth0: {}\n",
['eth0'], undef],
['last bridge', $header." bridges:\n".$standalone,
['br0'], undef],
['VLAN and bond settings', $header.$ethernet.$other_groups.
" bridges:\n".$standalone, ['br0'], $header.$ethernet.$other_groups],
['inline Ethernet mapping', $header.$inline." bridges:\n".$bridge0,
['br0'], $header.$inline],
['bulk deletion preserves existing empty groups', $bulk, ['br1', 'br0'],
$global." ethernets: {}\n"],
['bulk deletion follows display order',
$global." ethernets:\n eth0: {}\n bridges:\n".$standalone,
['eth0', 'br0'], $global],
['virtual IPv4 address', $header." ethernets:\n eth0:\n".
" addresses: [192.0.2.10/24, 192.0.2.11/24]\n",
['eth0:0'], $header." ethernets:\n eth0:\n".
" addresses: ['192.0.2.10/24']\n"],
) {
subtest $case->[0] => sub {
# Generate only beneath this temporary root, without applying networking.
my $root = tempdir(CLEANUP => 1);
local $main::netplan_dir = "$root/etc/netplan";
make_path($main::netplan_dir);
my $file = "$main::netplan_dir/shared.yaml";
my $control = "$main::netplan_dir/00-renderer.yaml";
main::write_file_contents($file, $case->[1]);
main::write_file_contents($control, $global);
chmod(0600, $file, $control) == 2 or die "chmod: $!";
is(system('netplan', 'generate', '--root-dir', $root), 0,
'original configuration passes Netplan');
# Match the UI's reuse of one interface list throughout bulk deletion.
my @boot = main::boot_interfaces();
foreach my $name (@{$case->[2]}) {
my ($iface) = grep { $_->{'fullname'} eq $name } @boot;
$iface or die "Fixture interface $name not found";
main::delete_interface($iface);
}
if (defined($case->[3])) {
# Surviving settings and comments must retain their exact contents.
is(main::read_file_contents($file), $case->[3],
'remaining configuration is unchanged');
}
else {
# No interface or global setting remains in this file.
ok(!-e $file, 'empty file is removed');
}
is(main::read_file_contents($control), $global,
'another YAML file is untouched');
is(system('netplan', 'generate', '--root-dir', $root), 0,
'resulting configuration passes Netplan');
};
}
done_testing();

65
t/net-netplan.t Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/perl
# Exercise Netplan deletion using temporary files only, without host configuration.
use strict;
use warnings;
no warnings 'once';
use Test::More;
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../net";
require "$FindBin::Bin/../web-lib-funcs.pl";
require "$FindBin::Bin/../net/netplan-lib.pl";
local %main::gconfig = ();
local $main::readonly_mode_cache = 0;
my $root = tempdir(CLEANUP => 1);
my $file = "$root/shared.yaml";
local $main::netplan_dir = $root;
# Empty detection must retain real interface entries and global settings.
foreach my $case (
["network:\n version: 2\n ethernets:\n bridges:\n", 1, 'empty interface groups'],
["network:\n version: 2\n ethernets:\n eth0: {}\n", 0, 'empty Ethernet definition'],
["network:\n version: 2\n bridges:\n br1:\n dhcp4: true\n", 0, 'another bridge'],
["network:\n version: 2\n renderer: networkd\n", 0, 'global renderer'],
["network:\n version: 2\n vlans:\n vlan10:\n id: 10\n link: eth0\n", 0, 'VLAN settings']) {
main::write_file_contents($file, $case->[0]);
main::unflush_file_lines($file);
is(main::is_yaml_empty($file), $case->[1], "$case->[2] handled correctly");
}
# Deleting the last bridge must retain its port and unrelated YAML byte for byte.
my $remaining = "network:\n version: 2\n ethernets:\n eth0:\n dhcp4: true\n".
" eth1:\n dhcp4: false\n";
my $original = $remaining." bridges:\n br0:\n interfaces: [eth0]\n";
main::write_file_contents($file, $original);
main::unflush_file_lines($file);
main::delete_interface((grep { $_->{'fullname'} eq 'br0' } main::boot_interfaces())[0]);
ok(-f $file, 'shared YAML survives bridge deletion');
is(main::read_file_contents($file), $remaining, 'empty group removed, Ethernet settings untouched');
# Removing one bridge must preserve the group and the remaining bridge.
my $second = " br1:\n interfaces: [eth1]\n";
main::write_file_contents($file, $original.$second);
main::unflush_file_lines($file);
main::delete_interface((grep { $_->{'fullname'} eq 'br0' } main::boot_interfaces())[0]);
is(main::read_file_contents($file), $remaining." bridges:\n".$second,
'another bridge and its group remain unchanged');
# A bridge-only file is removed once its last interface is gone.
main::write_file_contents($file, "network:\n version: 2\n bridges:\n br0:\n dhcp4: true\n");
main::unflush_file_lines($file);
main::delete_interface((grep { $_->{'fullname'} eq 'br0' } main::boot_interfaces())[0]);
ok(!-e $file, 'empty bridge-only file is removed');
# Bulk deletion reuses interface details after earlier deletions shift lines.
my $global = "network:\n version: 2\n renderer: networkd\n";
main::write_file_contents($file, $global." ethernets: {}\n bridges:\n".
" br0:\n dhcp4: false\n br1:\n dhcp4: true\n");
main::unflush_file_lines($file);
my @boot = main::boot_interfaces();
foreach my $iface (@boot) {
main::delete_interface($iface);
}
is(main::read_file_contents($file), $global." ethernets: {}\n",
'bulk deletion handles line shifts and preserves unrelated settings');
done_testing();

View File

@@ -1270,8 +1270,8 @@ a.ui_list_link:hover { color: var(--ui-accent) !important; }
display: grid; display: grid;
gap: var(--ui-inset); gap: var(--ui-inset);
min-width: 0; min-width: 0;
/* Match vertical padding to the surrounding page gap */ /* Half the inset above and below, the full inset at the sides */
padding: var(--ui-gap) var(--ui-inset); padding: calc(var(--ui-inset) / 2) var(--ui-inset);
border: 1px solid var(--ui-border); border: 1px solid var(--ui-border);
background: var(--ui-surface); background: var(--ui-surface);
} }

View File

@@ -1 +1 @@
2.671 2.670