mirror of
https://github.com/webmin/webmin.git
synced 2026-06-24 04:50:30 +01:00
This PR adds a standalone Systemd Services and Units module for managing systemd units across system and user scopes. The module keeps systemd-specific behavior separate from the legacy Bootup and Shutdown module and is implemented as standalone `strict`/`warnings` Perl code rather than depending on its existing init helpers. Those helpers intentionally smooth over multiple init systems, while this module keeps systemd-specific file handling, user-manager behavior, ACL checks, and control operations explicit, scoped, and easier to audit. It includes: - Tabbed views for services, timers, sockets, paths, targets, storage, resources, devices, and user units - Guided creation and editing for common unit types, with contextual fields, validation, and help - User-scoped unit management with linger support and safe handling of home-directory unit files - Runtime actions for start, stop, restart, enable, disable, status, logs, properties, dependencies, and system-unit mask/unmask - Drop-in override inventory plus create, edit, and delete flows - Manual unit-file editing with daemon reload reminders and actions - Configurable module behavior, visible tabs, display options, and post-create navigation - Comprehensive ACL controls for system/user scopes, actions, manual edits, drop-ins, linger, reload, backup, and user filters - Safe Webmin user support through a scoped safe ACL preset - Virtualmin integration for granting domain owners access to their own systemd user units - Tests for unit generation, safety checks, ACL behavior, user-unit handling, backup coverage, and Perl::Critic compatibility A companion Virtualmin PR adds template integration so domain owners can be granted scoped access to their own systemd user units when this module is installed.
523 lines
17 KiB
Perl
Executable File
523 lines
17 KiB
Perl
Executable File
#!/usr/local/bin/perl
|
|
# Display systemd system and user units grouped by unit type.
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
require './systemd-lib.pl'; ## no critic
|
|
|
|
our (%access, %config, %in, %text);
|
|
|
|
ReadParse();
|
|
|
|
# The index can be reached by GET, so all query input below is either reduced
|
|
# to known values or validated before it is used.
|
|
has_command("systemctl") || error($text{'systemd_esystemctl'});
|
|
systemd_can_enter_module(\%access) || systemd_acl_error('penter');
|
|
|
|
# Print the page shell before building the tab contents.
|
|
ui_print_header(version_title(),
|
|
$text{'index_title'}, "", "intro", 1, 1, undef,
|
|
action_links());
|
|
print index_style();
|
|
|
|
# Query parameters only choose the active tab and optional user context.
|
|
my $scope = $in{'scope'} eq 'user' && tab_visible('user') ? 'user' : '';
|
|
my $unituser = clean_unit_value($in{'unituser'});
|
|
$unituser ||= systemd_acl_default_user(\%access) || "";
|
|
my $unituser_details = $unituser ?
|
|
get_user_details($unituser) : undef;
|
|
if ($scope eq 'user') {
|
|
$unituser_details || error($text{'systemd_euser'});
|
|
systemd_can_view_user_scope(\%access, $unituser) ||
|
|
systemd_acl_error('pview_user');
|
|
}
|
|
$unituser = "" if (!$unituser_details);
|
|
|
|
# Load both system and user units so the visible tab set matches reality.
|
|
my @system_units = systemd_can_view_system(\%access) ? list_units() : ( );
|
|
my @user_units = tab_visible('user') &&
|
|
systemd_can_view_user_scope(\%access) ?
|
|
grep { systemd_acl_user_allowed(\%access, $_->{'user'}) }
|
|
list_all_user_units() : ( );
|
|
my @tabs = index_tabs(\@system_units, \@user_units, $unituser);
|
|
|
|
# Pick a valid active tab. Invalid mode values fall back to the first tab.
|
|
my %valid_tabs = map { $_->{'id'}, 1 } @tabs;
|
|
my $requested = defined($in{'mode'}) ? $in{'mode'} : "";
|
|
my $mode = $requested && $valid_tabs{$requested} ? $requested :
|
|
$scope eq 'user' && $valid_tabs{'user'} ? 'user' :
|
|
$tabs[0]->{'id'};
|
|
my $formno = 0;
|
|
|
|
# When several unit groups exist, render Webmin tabs around each table.
|
|
if (@tabs > 1) {
|
|
my @uitabs = map { [ $_->{'id'}, $_->{'title'} ] } @tabs;
|
|
print ui_tabs_start(\@uitabs, "mode", $mode, 1);
|
|
foreach my $tab (@tabs) {
|
|
print ui_tabs_start_tab("mode", $tab->{'id'});
|
|
$formno++ if (print_index_tab($tab, $formno));
|
|
print ui_tabs_end_tab("mode", $tab->{'id'});
|
|
}
|
|
print ui_tabs_end(1);
|
|
}
|
|
else {
|
|
# A single unit group does not need tab chrome.
|
|
print_index_tab($tabs[0], $formno);
|
|
}
|
|
|
|
print_index_tools();
|
|
ui_print_footer("/", $text{'index'});
|
|
|
|
# version_title()
|
|
# Returns the first line of systemctl --version, or a plain fallback.
|
|
sub version_title
|
|
{
|
|
my $systemctl = has_command("systemctl");
|
|
return $text{'mode_systemd'} if (!$systemctl);
|
|
|
|
# Only the first line is useful as a subtitle, for example "systemd 252".
|
|
my $out = backquote_command(quotemeta($systemctl)." --version 2>/dev/null");
|
|
return $text{'mode_systemd'} if ($? || !defined($out) || $out eq "");
|
|
my ($first) = split(/\r?\n/, $out, 2);
|
|
$first = clean_unit_value($first);
|
|
return $first || $text{'mode_systemd'};
|
|
}
|
|
|
|
# index_style()
|
|
# Returns CSS used by the systemd index fragment.
|
|
sub index_style
|
|
{
|
|
# The style is emitted in the body so SPA theme navigation applies it too.
|
|
return ui_tag('style',
|
|
".systemd_linger_toggle { text-decoration: none; }\n".
|
|
".systemd_linger_toggle .ui_text_color { border-bottom: 1px dotted currentColor; }\n",
|
|
{ 'type' => 'text/css' });
|
|
}
|
|
|
|
# print_index_tools()
|
|
# Prints advanced module actions below the unit tables.
|
|
sub print_index_tools
|
|
{
|
|
my @buttons;
|
|
push(@buttons, [ "edit_manual.cgi",
|
|
$text{'index_edit_files'},
|
|
$text{'index_edit_filesdesc'} ])
|
|
if (systemd_acl_bool(\%access, 'manual') ||
|
|
systemd_acl_bool(\%access, 'manual_user'));
|
|
push(@buttons, [ "dropins.cgi",
|
|
$text{'index_dropins'},
|
|
$text{'index_dropinsdesc'} ])
|
|
if ($config{'show_dropin_inventory'} &&
|
|
systemd_can_enter_module(\%access));
|
|
push(@buttons, [ "restart.cgi",
|
|
$text{'index_reload'},
|
|
$text{'index_reloaddesc'} ])
|
|
if (systemd_can_reload(\%access));
|
|
return if (!@buttons);
|
|
print ui_hr();
|
|
print ui_buttons_start();
|
|
foreach my $button (@buttons) {
|
|
print ui_buttons_row(@$button);
|
|
}
|
|
print ui_buttons_end();
|
|
}
|
|
|
|
# index_tab_groups()
|
|
# Returns the system-unit tab layout and the unit suffixes each tab owns.
|
|
sub index_tab_groups
|
|
{
|
|
return (
|
|
{ 'id' => 'service', 'types' => [ 'service' ],
|
|
'create' => 'service' },
|
|
{ 'id' => 'timer', 'types' => [ 'timer' ],
|
|
'create' => 'timer' },
|
|
{ 'id' => 'socket', 'types' => [ 'socket' ],
|
|
'create' => 'socket' },
|
|
{ 'id' => 'path', 'types' => [ 'path' ],
|
|
'create' => 'path' },
|
|
{ 'id' => 'target', 'types' => [ 'target' ],
|
|
'create' => 'target' },
|
|
{ 'id' => 'storage', 'types' => [ 'mount', 'automount', 'swap' ],
|
|
'create' => 'mount', 'show_type' => 1 },
|
|
{ 'id' => 'resources', 'types' => [ 'slice', 'scope' ],
|
|
'create' => 'slice', 'show_type' => 1, 'inspect_only' => 1 },
|
|
{ 'id' => 'device', 'types' => [ 'device' ],
|
|
'inspect_only' => 1, 'selectable' => 0,
|
|
'show_unit_state' => 0 },
|
|
);
|
|
}
|
|
|
|
# index_type_tab(type)
|
|
# Returns the tab id that owns a unit type.
|
|
sub index_type_tab
|
|
{
|
|
my ($type) = @_;
|
|
foreach my $tab (index_tab_groups()) {
|
|
return $tab->{'id'} if (indexof($type, @{$tab->{'types'}}) >= 0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
# index_tabs(system-units, user-units, [user])
|
|
# Builds tab metadata for system unit types plus one user-units tab.
|
|
sub index_tabs
|
|
{
|
|
my ($system_units, $user_units, $unituser) = @_;
|
|
my %by_tab;
|
|
|
|
# System units are grouped by suffix, with related low-level types combined.
|
|
foreach my $u (@$system_units) {
|
|
my ($display, $type) = index_name_type($u->{'name'});
|
|
$type = 'service' if (!$type);
|
|
next if (indexof($type, get_list_unit_types()) < 0);
|
|
next if (!unit_visible_on_index($u));
|
|
my $tabid = index_type_tab($type);
|
|
next if (!$tabid);
|
|
$u->{'_display'} = $config{'show_unit_suffixes'} ?
|
|
$u->{'name'} : $display;
|
|
$u->{'_type'} = $type;
|
|
push(@{$by_tab{$tabid}}, $u);
|
|
}
|
|
|
|
# Keep tab ordering stable even when some unit types are absent.
|
|
my @tabs;
|
|
foreach my $group (index_tab_groups()) {
|
|
next if (!tab_visible($group->{'id'}));
|
|
next if (!systemd_can_view_system(\%access));
|
|
my $units = $by_tab{$group->{'id'}} || [ ];
|
|
push(@tabs, { %$group,
|
|
'title' => index_tab_title($group->{'id'}),
|
|
'desc' => index_tab_desc($group->{'id'}),
|
|
'units' => $units });
|
|
}
|
|
|
|
# User units share one tab because the owner column distinguishes accounts.
|
|
my @visible_user_units;
|
|
foreach my $u (@$user_units) {
|
|
next if (!unit_visible_on_index($u));
|
|
my ($display, $type) = index_name_type($u->{'name'});
|
|
$u->{'_display'} = $config{'show_unit_suffixes'} ?
|
|
$u->{'name'} : $display;
|
|
$u->{'_type'} = $type || 'service';
|
|
push(@visible_user_units, $u);
|
|
}
|
|
$user_units = \@visible_user_units;
|
|
if (tab_visible('user') &&
|
|
systemd_can_view_user_scope(\%access, $unituser)) {
|
|
push(@tabs, { 'id' => 'user',
|
|
'user' => 1,
|
|
'unituser' => $unituser,
|
|
'title' => $text{'systemd_tab_user'},
|
|
'desc' => $text{'systemd_tabdesc_user'},
|
|
'units' => $user_units });
|
|
}
|
|
return @tabs;
|
|
}
|
|
|
|
# print_index_tab(tab, form-number)
|
|
# Outputs one tab description and its mass-action table.
|
|
sub print_index_tab
|
|
{
|
|
my ($tab, $formno) = @_;
|
|
my $user_tab = $tab->{'user'} ? 1 : 0;
|
|
my $can_status = systemd_can_inspect(\%access, $user_tab, $tab->{'unituser'});
|
|
my $can_logs = systemd_can_logs(\%access, $user_tab, $tab->{'unituser'});
|
|
my $can_start = systemd_can_runtime(\%access, 'start',
|
|
$user_tab, $tab->{'unituser'});
|
|
my $can_stop = systemd_can_runtime(\%access, 'stop',
|
|
$user_tab, $tab->{'unituser'});
|
|
my $can_restart = systemd_can_runtime(\%access, 'restart',
|
|
$user_tab, $tab->{'unituser'});
|
|
my $can_boot = systemd_can_boot(\%access, $user_tab, $tab->{'unituser'});
|
|
my $can_mask = $user_tab ? 0 :
|
|
systemd_can_mask(\%access, $user_tab, $tab->{'unituser'});
|
|
my $can_delete = $user_tab ?
|
|
systemd_can_delete(\%access, $user_tab, $tab->{'unituser'}) : 0;
|
|
my $selectable = exists($tab->{'selectable'}) ? $tab->{'selectable'} : 1;
|
|
$selectable &&= $can_status || $can_logs || $can_start || $can_stop ||
|
|
$can_restart || $can_boot || $can_mask || $can_delete ? 1 : 0;
|
|
my $show_unit_state = exists($tab->{'show_unit_state'}) ?
|
|
$tab->{'show_unit_state'} : 1;
|
|
my $show_type = !$config{'show_unit_suffixes'} &&
|
|
($user_tab || $tab->{'show_type'});
|
|
my %linger_cache;
|
|
|
|
# The create link inherits tab context so the create form opens with the right
|
|
# unit type or user-unit mode selected.
|
|
my $create_type = $user_tab ? 'service' : $tab->{'create'};
|
|
my $create_link = index_create_link($tab, $user_tab, $create_type);
|
|
my @links = $selectable ?
|
|
( select_all_link("d", $formno),
|
|
select_invert_link("d", $formno) ) :
|
|
( );
|
|
push(@links, $create_link) if ($create_link);
|
|
|
|
print ui_div($tab->{'desc'});
|
|
if (!@{$tab->{'units'}}) {
|
|
print ui_tag('p', index_empty_message($tab));
|
|
print ui_links_row([ $create_link ]) if ($create_link);
|
|
return 0;
|
|
}
|
|
|
|
# Start the mass-action form and keep scope in a hidden field for user units.
|
|
print ui_form_start("mass_units.cgi", "post");
|
|
print ui_links_row(\@links) if (@links);
|
|
print ui_hidden("scope", "users") if ($user_tab);
|
|
|
|
# Mixed-type tabs only need a type column when unit suffixes are hidden.
|
|
my @heads = $selectable ? ( "" ) : ( );
|
|
push(@heads, $text{'systemd_name'});
|
|
push(@heads, $text{'systemd_desc'}) if ($config{'desc'});
|
|
push(@heads, $text{'systemd_type'}) if ($show_type);
|
|
push(@heads, $text{'systemd_unit_state'}) if ($show_unit_state);
|
|
push(@heads, $text{'systemd_runtime_state'});
|
|
push(@heads, $text{'systemd_owner'}, $text{'systemd_linger_status'})
|
|
if ($user_tab);
|
|
print ui_columns_start(\@heads);
|
|
foreach my $u (@{$tab->{'units'}}) {
|
|
# Generated units without real files and masked units are shown read-only.
|
|
my $editable = $u->{'file'} && -f $u->{'file'} ? 1 : 0;
|
|
my $link = index_edit_url($u, $user_tab);
|
|
my $title = (!$editable ||
|
|
(defined($u->{'boot'}) && $u->{'boot'} == -1) ?
|
|
html_escape($u->{'_display'}) :
|
|
ui_link($link, html_escape($u->{'_display'})));
|
|
my $checkvalue = $user_tab ?
|
|
user_unit_selection_value($u->{'user'}, $u->{'name'}) :
|
|
$u->{'name'};
|
|
|
|
# Build the row from common columns first, then append user-only columns.
|
|
my @row = $selectable ?
|
|
( ui_checkbox("d", $checkvalue, undef) ) :
|
|
( );
|
|
push(@row, $title);
|
|
push(@row, html_escape($u->{'desc'})) if ($config{'desc'});
|
|
push(@row, html_escape(index_unit_type_title($u->{'_type'})))
|
|
if ($show_type);
|
|
push(@row, index_unit_state_column($u->{'unitstate'}))
|
|
if ($show_unit_state);
|
|
push(@row, index_runtime_state_column(
|
|
$u->{'runtime'}, $u->{'substate'}));
|
|
if ($user_tab) {
|
|
# Linger is per user, so cache it instead of calling loginctl per row.
|
|
if (!exists($linger_cache{$u->{'user'}})) {
|
|
$linger_cache{$u->{'user'}} =
|
|
user_linger_enabled($u->{'user'});
|
|
}
|
|
my $linger_html = systemd_can_linger(\%access, $u->{'user'}) ?
|
|
linger_toggle_link(
|
|
$u->{'user'}, $linger_cache{$u->{'user'}}) :
|
|
html_escape($linger_cache{$u->{'user'}} ?
|
|
$text{'yes'} : $text{'no'});
|
|
push(@row, ui_tag('tt', html_escape($u->{'user'})),
|
|
$linger_html);
|
|
}
|
|
print ui_columns_row(\@row);
|
|
}
|
|
|
|
# Repeat row-selection links below the table for long lists.
|
|
print ui_columns_end();
|
|
if ($selectable) {
|
|
print ui_links_row(\@links);
|
|
my @runtime_buttons;
|
|
push(@runtime_buttons, [ "start", $text{'index_start'} ])
|
|
if ($can_start);
|
|
push(@runtime_buttons, [ "stop", $text{'index_stop'} ])
|
|
if ($can_stop);
|
|
push(@runtime_buttons, [ "restart", $text{'index_restart'} ])
|
|
if ($can_restart);
|
|
my @boot_buttons = $can_boot ?
|
|
( [ "addboot", $text{'index_addboot'} ],
|
|
[ "delboot", $text{'index_delboot'} ] ) : ( );
|
|
my @mask_buttons = $can_mask ?
|
|
( [ "mask", $text{'index_mask'} ],
|
|
[ "unmask", $text{'index_unmask'} ] ) : ( );
|
|
my @inspect_buttons;
|
|
push(@inspect_buttons, [ "status", $text{'index_statusnow'} ])
|
|
if ($can_status);
|
|
push(@inspect_buttons, [ "logs", $text{'index_logsnow'} ])
|
|
if ($can_logs);
|
|
my @delete_buttons = $can_delete ?
|
|
( [ "delete", $text{'index_delete'} ] ) : ( );
|
|
my @action_groups = $tab->{'inspect_only'} ?
|
|
grep { @$_ } ( \@inspect_buttons ) :
|
|
grep { @$_ } ( \@runtime_buttons, \@boot_buttons,
|
|
\@mask_buttons, \@inspect_buttons );
|
|
print ui_form_grouped_buttons([ [ @action_groups ],
|
|
[ \@delete_buttons ] ])
|
|
if (@action_groups || @delete_buttons);
|
|
}
|
|
print ui_form_end();
|
|
return 1;
|
|
}
|
|
|
|
# index_name_type(unit-name)
|
|
# Splits a full unit name into display name and unit type.
|
|
sub index_name_type
|
|
{
|
|
my ($name) = @_;
|
|
my $units_piped = join('|', map { quotemeta } get_unit_types());
|
|
|
|
# Only strip suffixes that systemd understands as unit types.
|
|
my ($type) = $name =~ /\.([^.]+)$/;
|
|
if (defined($type) && $type =~ /^(?:$units_piped)$/) {
|
|
my $display = $name;
|
|
$display =~ s/\.$type$//;
|
|
return ($display, $type);
|
|
}
|
|
return ($name, "");
|
|
}
|
|
|
|
# index_tab_title(type)
|
|
# Returns the plural tab title for a systemd unit type.
|
|
sub index_tab_title
|
|
{
|
|
my ($type) = @_;
|
|
return $text{'systemd_tab_'.$type} ||
|
|
$text{'systemd_type_'.$type} ||
|
|
ucfirst($type);
|
|
}
|
|
|
|
# index_tab_desc(type)
|
|
# Returns the explanatory text shown under a systemd unit tab.
|
|
sub index_tab_desc
|
|
{
|
|
my ($type) = @_;
|
|
return $text{'systemd_tabdesc_'.$type} || "";
|
|
}
|
|
|
|
# index_create_label(tab-id, create-type)
|
|
# Returns the tab-specific label for the create-unit link.
|
|
sub index_create_label
|
|
{
|
|
my ($tabid, $type) = @_;
|
|
return $text{'index_sadd_'.$tabid} ||
|
|
$text{'index_sadd_'.$type} ||
|
|
$text{'index_sadd'};
|
|
}
|
|
|
|
# index_create_link(tab, user-tab, create-type)
|
|
# Returns the create link for a tab, if allowed.
|
|
sub index_create_link
|
|
{
|
|
my ($tab, $user_tab, $create_type) = @_;
|
|
return "" if (!$create_type);
|
|
return "" if (!systemd_can_create(\%access, $user_tab, $tab->{'unituser'}));
|
|
my $create_url = $user_tab && $tab->{'unituser'} ?
|
|
"edit_unit.cgi?new=1&scope=user&unittype=service&unituser=".
|
|
urlize($tab->{'unituser'}) :
|
|
$user_tab ? "edit_unit.cgi?new=1&scope=user&unittype=service" :
|
|
"edit_unit.cgi?new=1&unittype=".urlize($create_type);
|
|
return ui_link($create_url,
|
|
index_create_label($tab->{'id'}, $create_type));
|
|
}
|
|
|
|
# index_empty_message(tab)
|
|
# Returns the empty-state message for a unit tab.
|
|
sub index_empty_message
|
|
{
|
|
my ($tab) = @_;
|
|
if ($tab->{'user'}) {
|
|
return text('index_empty_user_owner',
|
|
ui_tag('tt', html_escape($tab->{'unituser'})))
|
|
if ($tab->{'unituser'});
|
|
return $text{'index_empty_user'};
|
|
}
|
|
return $text{'index_empty_'.$tab->{'id'}} || $text{'index_empty_units'};
|
|
}
|
|
|
|
# index_unit_type_title(type)
|
|
# Returns the display label for a single unit type.
|
|
sub index_unit_type_title
|
|
{
|
|
my ($type) = @_;
|
|
return $text{'systemd_type_'.$type} || $type;
|
|
}
|
|
|
|
# user_unit_selection_value(user, unit)
|
|
# Encodes a user-unit owner and name into one checkbox value for mass actions.
|
|
sub user_unit_selection_value
|
|
{
|
|
my ($user, $unit) = @_;
|
|
return urlize($user)."\t".urlize($unit);
|
|
}
|
|
|
|
# linger_toggle_link(user, enabled)
|
|
# Returns a link to toggle linger for a user-unit owner.
|
|
sub linger_toggle_link
|
|
{
|
|
my ($user, $enabled) = @_;
|
|
|
|
# The link flips the current state and lets set_linger.cgi validate again.
|
|
my $target = $enabled ? 0 : 1;
|
|
my $label = $enabled ? $text{'yes'} : $text{'no'};
|
|
my $type = $enabled ? 'success' : 'warn';
|
|
my $title = $enabled ? text('systemd_linger_disable', $user) :
|
|
text('systemd_linger_enable', $user);
|
|
my $url = "set_linger.cgi?user=".urlize($user)."&enabled=".$target;
|
|
return ui_tag('a', ui_text_color(html_escape($label), $type),
|
|
{ 'href' => $url,
|
|
'class' => 'systemd_linger_toggle',
|
|
'title' => $title });
|
|
}
|
|
|
|
# index_unit_state_column(state)
|
|
# Returns a formatted UnitFileState value for a unit row.
|
|
sub index_unit_state_column
|
|
{
|
|
my ($state) = @_;
|
|
return index_state_column($state, {
|
|
'enabled' => 'success',
|
|
'enabled-runtime' => 'success',
|
|
'disabled' => 'warn',
|
|
'masked' => 'danger',
|
|
'masked-runtime' => 'danger',
|
|
'bad' => 'danger',
|
|
});
|
|
}
|
|
|
|
# index_runtime_state_column(state, substate)
|
|
# Returns a formatted ActiveState value, with SubState when systemd reports it.
|
|
sub index_runtime_state_column
|
|
{
|
|
my ($state, $substate) = @_;
|
|
return index_state_column($state, {
|
|
'active' => 'success',
|
|
'inactive' => 'warn',
|
|
'activating' => 'warn',
|
|
'deactivating' => 'warn',
|
|
'failed' => 'danger',
|
|
}, $substate);
|
|
}
|
|
|
|
# index_state_column(state, colors, [substate])
|
|
# Returns a displayed systemd state value with light semantic coloring.
|
|
sub index_state_column
|
|
{
|
|
my ($state, $colors, $substate) = @_;
|
|
return ui_tag('i', $text{'index_unknown'})
|
|
if (!defined($state) || $state eq "");
|
|
my $label = ucfirst($state);
|
|
$label .= " (".$substate.")"
|
|
if (defined($substate) && $substate ne "" && $substate ne $state);
|
|
my $safe = html_escape($label);
|
|
my $color = $colors->{$state};
|
|
return $color ? ui_text_color($safe, $color) : $safe;
|
|
}
|
|
|
|
# index_edit_url(unit, user-tab)
|
|
# Returns the edit URL for a unit, preferring its safe override file.
|
|
sub index_edit_url
|
|
{
|
|
my ($unit, $user_tab) = @_;
|
|
my $url = $user_tab ?
|
|
"edit_unit.cgi?scope=user&unituser=".urlize($unit->{'user'}).
|
|
"&name=".urlize($unit->{'name'}) :
|
|
"edit_unit.cgi?name=".urlize($unit->{'name'});
|
|
if (dropin_exists($user_tab, $unit->{'user'}, $unit->{'name'})) {
|
|
$url .= "&dropin=1";
|
|
}
|
|
return $url;
|
|
}
|