1 # Girocco::CLIUtil.pm -- Command Line Interface Utility Functions
2 # Copyright (C) 2016 Kyle J. McKay. All rights reserved.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 ## This package MUST NOT be used by any CGI script as it cancels
22 ## the effect of CGI::Carp::fatalsToBrowser which could result in the
23 ## output of a CGI script becoming unparseable by the web server!
26 package Girocco
::CLIUtil
;
31 use base
qw(Exporter);
32 our ($VERSION, @EXPORT, @EXPORT_OK);
36 diename recreate_file strict_bool
37 is_yes is_no is_yesno valid_bool clean_bool
39 ynprompt ynprompt_or_die
40 prompt_noecho prompt_noecho_or_die
41 prompt_noecho_nl prompt_noecho_nl_or_die
42 yes_to_continue yes_to_continue_or_die
43 get_all_users get_user get_all_projects get_project
47 _parse_options _prompt_rl _prompt_rl_or_die
56 use Girocco::HashUtil;
58 BEGIN {noFatalsToBrowser}
62 require Term::ReadLine;
67 package Girocco::CLIUtil::NoEcho;
71 my $class = shift; # ignored
75 $self->{ios
} = POSIX
::Termios
->new;
76 $self->{ios
}->getattr($self->{fd
});
77 my $noecho = POSIX
::Termios
->new;
78 $noecho->getattr($fd);
79 $noecho->setlflag($noecho->getlflag & ~(&POSIX
::ECHO
));
80 $noecho->setattr($fd, &POSIX
::TCSANOW
);
86 $self->{ios
}->setattr($self->{fd
}, &POSIX
::TCSANOW
);
93 my $result = $diename;
94 $diename = join(" ", @_) if @_;
100 # Remove any leading options matching the given specs from the @ARGV array and
101 # store them as indicated. Parsing stops when an unknown option is encountered,
102 # "--" is encountered (in which case it's removed) or a non-option is encountered.
103 # Note that "-" by itself is considered a non-option argument.
105 # Option bundling for single-letter options is NOT supported.
107 # Optional first arg is CODE ref:
108 # sub {my ($err, $opt) = @_; ...}
109 # with $err of '?' meaning $opt is unknown
110 # with $err of ':' meaning $opt is missing its argument
111 # $opt is the full option as given on the command line (including leading - etc.)
112 # the default if omitted dies with an error
113 # If the sub returns, _parse_options exits immediately with 0
115 # The rest of the arguments form pairs:
117 # where ref must be either a SCALAR ref or a CODE ref, if it's neither
118 # then the "spec" => ref pair is silently ignored.
120 # "name" -- an incrementing flag (matches -name and --name)
121 # ":name" -- an option with a value (matches -name=val and --name=val)
122 # Using option "--name" matches spec "name" if given otherwise matches spec
123 # ":name" if given and there's at least one more argument (if not the ':' error
125 # Using option "--name=val" only matches spec ":name" (but "val" can be "").
126 # For flags, a SCALAR ref is incremented, a CODE ref is called with no arguments.
127 # For values (":name" specs) a SCALAR ref is assigned the value a CODE ref is
128 # called with the value as its single argument.
130 # _parse_options returns 1 as long as there were no errors
133 my $failsub = sub {die((($_[0]eq'?')?
"unrecognized":"missing argument for")." option \"$_[1]\"\n")};
134 $failsub = shift if @_ && ref($_[0]) eq "CODE";
137 if (defined($_[0]) && $_[0] =~ /^:?[^-:\s]/ &&
138 defined($_[1]) && (ref($_[1]) eq "SCALAR" || ref($_[1]) eq "CODE")) {
139 $opts{$_[0]} = $_[1];
144 while (@ARGV && $ARGV[0] =~ /^--?[^-:\s]/) {
145 my $opt = shift @ARGV;
148 if ($sopt =~ /^([^=]+)=(.*)$/) {
149 my ($name, $val) = ($1, $2);
150 if ($opts{":$name"}) {
151 ${$opts{":$name"}} = $val if ref($opts{":$name"}) eq "SCALAR";
152 &{$opts{":$name"}}($val) if ref($opts{":$name"}) eq "CODE";
154 &$failsub('?', $opt);
157 } elsif ($opts{$sopt}) {
158 ++${$opts{$sopt}} if ref($opts{$sopt}) eq "SCALAR";
159 &{$opts{$sopt}}() if ref($opts{$sopt}) eq "CODE";
160 } elsif ($opts{":$sopt"}) {
161 &$failsub(':', $opt),return(0) unless @ARGV;
162 my $val = shift @ARGV;
163 ${$opts{":$sopt"}} = $val if ref($opts{":$sopt"} eq "SCALAR");
164 &{$opts{":$sopt"}}($val) if ref($opts{":$sopt"} eq "CODE");
166 &$failsub('?', $opt);
170 if (@ARGV && $ARGV[0] eq "--") {
174 if (@ARGV && $ARGV[0] =~ /^-./) {
175 &$failsub('?', $ARGV[0]);
182 open F
, '>', $_[0] or die "failed to create $_[0]: $!\n";
189 defined ($b) or $b = "";
190 return lc($b) eq "yes" || (!$strict && lc($b) eq "y");
196 defined ($b) or $b = "";
197 return lc($b) eq "no" || (!$strict && lc($b) eq "n");
201 return is_yes
(@_) || is_no
(@_);
222 exists($boolvals{lc($_[0])});
227 return $boolvals{lc($b)} || 0;
231 my ($norl, $prompt, $default, $promptsfx) = @_;
232 ! -t STDIN
and $norl = 1;
233 defined($promptsfx) or $promptsfx = ': ';
234 defined($prompt) or $prompt = '';
236 $ds = " [" . $default . "]" if defined($default);
237 if ($have_rl && !$norl) {
238 my $rl = Term
::ReadLine
->new(basename
($0), \
*STDIN
, \
*STDOUT
);
240 $_ = $rl->readline($prompt . $ds . $promptsfx);
241 $rl->addhistory($_) if defined($_) && $_ =~ /\S/;
243 print $prompt, $ds, $promptsfx;
246 return undef unless defined($_);
248 return $_ eq '' && defined($default) ?
$default : $_;
252 return _prompt_rl
(undef, @_);
258 $args[2] = "? " unless defined$args[2];
260 $result = prompt
(@args);
261 return undef unless defined($result);
262 redo unless is_yesno
($result);
264 return clean_bool
($result);
267 sub _prompt_rl_or_die
{
268 my $result = _prompt_rl
(@_);
269 unless (defined($result)) {
271 defined($nm) or $nm = "";
272 $nm eq "" or $nm .= " ";
273 die "\n${nm}aborted\n";
279 return _prompt_rl_or_die
(undef, @_);
282 sub ynprompt_or_die
{
283 my $result = ynprompt
(@_);
284 unless (defined($result)) {
286 defined($nm) or $nm = "";
287 $nm eq "" or $nm .= " ";
288 die "\n${nm}aborted\n";
294 my $ne = Girocco
::CLIUtil
::NoEcho
->new;
298 sub prompt_noecho_or_die
{
299 my $ne = Girocco
::CLIUtil
::NoEcho
->new;
300 _prompt_rl_or_die
(1, @_);
303 sub prompt_noecho_nl
{
304 my $result = prompt_noecho
(@_);
309 sub prompt_noecho_nl_or_die
{
310 my $result = prompt_noecho_or_die
(@_);
315 sub yes_to_continue
{
316 return !!ynprompt
(($_[0]||"Continue (enter \"yes\" to continue)"), "no");
319 sub yes_to_continue_or_die
{
320 unless (ynprompt_or_die
(($_[0]||"Continue (enter \"yes\" to continue)"), "no")) {
322 defined($nm) or $nm = "";
323 $nm .= " " if $nm ne "";
324 die "${nm}aborted\n";
330 my $user_list_loaded;
332 my $full_user_list_loaded;
334 # If single argument is true, return ALL passwd entries not just "...@..." ones
335 sub _get_all_users_internal
{
336 my $full = shift || 0;
338 return @full_user_list if $full_user_list_loaded;
340 return @user_list if $user_list_loaded;
342 my $passwd_file = jailed_file
("/etc/passwd");
343 open my $fd, '<', $passwd_file or die "could not open \"$passwd_file\": $!\n";
347 @users = map {/^([^:\s#][^:\s]*):[^:]*:(-?\d+):(-?\d+)(:|$)/
348 ?
[++$line,split(':',$_,-1)] : ()} <$fd>;
350 @users = map {/^([^:_\s#][^:\s#]*):[^:]+:(\d{5,}):(\d+):([^:,][^:]*)/
351 ?
[++$line,$1,$2,$3,split(',',$4)] : ()} <$fd>;
355 $$_[5] = [split(',', $$_[5])] foreach @users;
356 @full_user_list = @users;
357 $full_user_list_loaded = 1;
359 @users = grep({$$_[4] =~ /\@/} @users);
361 $user_list_loaded = 1;
366 # Return array of arrayref where each arrayref has:
367 # [0] = ordering ordinal from $chroot/etc/passwd
369 # [2] = user id number
370 # [3] = user group number
372 # [5] = user UUID (text as 8x-4x-4x-4x-12x) or undef if none
373 # [6] = user creation date as YYYYMMDD_HHMMSS (UTC) or undef if none
374 sub get_all_users
{ return _get_all_users_internal
; }
376 # Return array of arrayref where each arrayref has:
377 # [0] = ordering ordinal from $chroot/etc/passwd
379 # [2] = user password field (usually "x")
380 # [3] = user id number
381 # [4] = user group number
382 # [5] = [info fields] from passwd line (usually email,uuid,creation)
383 # [6] = home dir field
385 # [...] possibly more, but [7] is usually max
386 sub get_full_users
{ return _get_all_users_internal
(1); }
388 # Result of Girocco::User->load or fatal die if that fails
389 # Returns undef if passed undef or ""
391 my $username = shift;
392 defined($username) && $username ne "" or return undef;
393 Girocco
::User
::does_exist
($username, 1) or die "No such user: \"$username\"\n";
396 $user = Girocco
::User
->load($username);
398 } && $user->{uid
} or die "Could not load user \"$username\"\n";
403 my $project_list_loaded;
405 # Return array of arrayref where each arrayref has:
406 # [0] = ordering ordinal from $chroot/etc/group
408 # [2] = group password hash
409 # [3] = group id number
410 # [4] = owner from gitproj.list
411 # [5] = list of comma-separated push user names (can be "") or ":" if mirror
412 sub get_all_projects
{
413 return @project_list if $project_list_loaded;
415 my $projlist_file = $Girocco::Config
::projlist_cache_dir
."/gitproj.list";
416 open $fd, '<', $projlist_file or die "could not open \"$projlist_file\": $!\n";
417 my $chomper = sub {chomp(my $x = shift); $x;};
418 my %owners = map {(split(/\s+/, &$chomper($_), 3))[0,2]} <$fd>;
420 my $group_file = jailed_file
("/etc/group");
421 open $fd, '<', $group_file or die "could not open \"$group_file\": $!\n";
425 return ':' if $list =~ /^:/;
429 my $defu = sub {defined($_[0])?
$_[0]:""};
430 my @projects = map {/^([^:_\s#][^:\s#]*):([^:]*):(\d{5,}):(.*)$/
431 ?
[++$line,$1,$2,$3,&$defu($owners{$1}),&$trimu($4)] : ()} <$fd>;
433 @project_list = @projects;
434 $project_list_loaded = 1;
438 # Result of Girocco::Project->load or fatal die if that fails
439 # Returns undef if passed undef or ""
441 my $projname = shift;
442 $projname =~ s/\.git$//i if defined($projname);
443 defined($projname) && $projname ne "" or return undef;
444 Girocco
::Project
::does_exist
($projname, 1) or die "No such project: \"$projname\"\n";
447 $project = Girocco
::Project
->load($projname);
449 } && $project->{loaded
} or die "Could not load project \"$projname\"\n";
453 # return true if $enc_passwd is a match for $plain_passwd
454 sub check_passwd_match
{
455 my ($enc_passwd, $plain_passwd) = @_;
456 defined($enc_passwd) or $enc_passwd = '';
457 defined($plain_passwd) or $plain_passwd = '';
458 # $enc_passwd may be crypt or crypt_sha1
459 if ($enc_passwd =~ m
(^\
$sha1\
$(\d
+)\
$([./0-9A-Za-z]{1,64})\$[./0-9A
-Za
-z
]{28}$)) {
460 # It's using sha1-crypt
461 return $enc_passwd eq crypt_sha1
($plain_passwd, $2, -(0+$1));
464 return $enc_passwd eq crypt($plain_passwd, $enc_passwd);