X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Fbin%2Frt.in;h=60eed6824502bcaecbdc04caadcc786c826f673f;hb=31f3763747b82764bb019cfab5b2a2945fc9a99d;hp=6ca302e19a5d7c6a7bc09df7d42130f2e675e5aa;hpb=624b2d44625f69d71175c3348cae635d580c890b;p=freeside.git diff --git a/rt/bin/rt.in b/rt/bin/rt.in index 6ca302e19..60eed6824 100644 --- a/rt/bin/rt.in +++ b/rt/bin/rt.in @@ -1,41 +1,41 @@ #!@PERL@ -w # BEGIN BPS TAGGED BLOCK {{{ -# +# # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC -# -# +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# +# # (Except where explicitly superseded by other copyright notices) -# -# +# +# # LICENSE: -# +# # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. -# +# # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# +# +# # CONTRIBUTION SUBMISSION POLICY: -# +# # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) -# +# # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that @@ -44,12 +44,19 @@ # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. -# +# # END BPS TAGGED BLOCK }}} # Designed and implemented for Best Practical Solutions, LLC by # Abhijit Menon-Sen use strict; +use warnings; + +if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) { + require Pod::Usage; + print Pod::Usage::pod2usage( { verbose => 2 } ); + exit; +} # This program is intentionally written to have as few non-core module # dependencies as possible. It should stay that way. @@ -61,6 +68,7 @@ use HTTP::Request::Common; use HTTP::Headers; use Term::ReadLine; use Time::Local; # used in prettyshow +use File::Temp; # strong (GSSAPI based) authentication is supported if the server does provide # it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed @@ -98,7 +106,7 @@ my %config = ( config_from_file($ENV{RTCONFIG} || ".rtrc"), config_from_env() ); -my $session = new Session("$HOME/.rt_sessions"); +my $session = Session->new("$HOME/.rt_sessions"); my $REST = "$config{server}/REST/1.0"; $no_strong_auth = 'switched off by externalauth=0' if defined $config{externalauth}; @@ -113,9 +121,9 @@ sub DEBUG { warn @_ if $config{debug} >= shift } # (XXX: Ask Autrijus how i18n changes these definitions.) my $name = '[\w.-]+'; -my $CF_name = '[\sa-z0-9_ :()/-]+'; +my $CF_name = '[^,]+?'; my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})'; -my $label = '[a-zA-Z0-9@_.+-]+'; +my $label = '[^,\\/]+'; my $labels = "(?:$label,)*$label"; my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+'; @@ -179,7 +187,7 @@ exit handler(); sub shell { $|=1; - my $term = new Term::ReadLine 'RT CLI'; + my $term = Term::ReadLine->new('RT CLI'); while ( defined ($_ = $term->readline($prompt)) ) { next if /^#/ || /^\s*$/; @@ -314,6 +322,7 @@ sub list { } if ( ! $rawprint and ! exists $data{format} ) { $data{format} = 'l'; + $data{fields} = 'subject,status,queue,created,told,owner,requestors'; } if ( $reverse_sort and $data{orderby} =~ /^-/ ) { $data{orderby} =~ s/^-/+/; @@ -414,7 +423,7 @@ sub show { } elsif (my $spec = is_object_spec($_, $type)) { push @objects, $spec; - $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/; + $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/; } else { my $datum = /^-/ ? "option" : "argument"; @@ -464,7 +473,7 @@ sub show { sub edit { my ($action) = @_; my (%data, $type, @objects); - my ($cl, $text, $edit, $input, $output); + my ($cl, $text, $edit, $input, $output, $content_type); use vars qw(%set %add %del); %set = %add = %del = (); @@ -478,6 +487,7 @@ sub edit { if (/^-e$/) { $edit = 1 } elsif (/^-i$/) { $input = 1 } elsif (/^-o$/) { $output = 1 } + elsif (/^-ct$/) { $content_type = shift @ARGV } elsif (/^-t$/) { $bad = 1, last unless defined($type = get_type_argument()); } @@ -647,24 +657,54 @@ sub edit { return 0; } + my @files; + @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'}; + my $synerr = 0; EDIT: # We'll let the user edit the form before sending it to the server, # unless we have enough information to submit it non-interactively. + if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) { + $text .= "Content-Type: $content_type\n" + if $content_type and $content_type ne "text/plain"; + } + if ($edit || (!$input && !$cl)) { - my $newtext = vi($text); + my ($newtext) = vi_form_while( + $text, + sub { + my ($text, $form) = @_; + return 1 unless exists $form->[2]{'Attachment'}; + + foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) { + return (0, "File '$f' doesn't exist") unless -f $f; + } + @files = @{ vsplit($form->[2]{'Attachment'}) }; + return 1; + }, + ); + return $newtext unless $newtext; # We won't resubmit a bad form unless it was changed. $text = ($synerr && $newtext eq $text) ? undef : $newtext; } + delete @data{ grep /^attachment_\d+$/, keys %data }; + my $i = 1; + foreach my $file (@files) { + $data{"attachment_$i"} = bless([ $file ], "Attachment"); + $i++; + } + if ($text) { my $r = submit("$REST/edit", {content => $text, %data}); if ($r->code == 409) { # If we submitted a bad form, we'll give the user a chance # to correct it and resubmit. if ($edit || (!$input && !$cl)) { - $text = $r->content; + my $content = $r->content . "\n"; + $content =~ s/^(?!#)/# /mg; + $text = $content . $text; $synerr = 1; goto EDIT; } @@ -730,7 +770,7 @@ sub setcommand { sub comment { my ($action) = @_; - my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit); + my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit); my $bad = 0; while (@ARGV) { @@ -739,7 +779,7 @@ sub comment { if (/^-e$/) { $edit = 1; } - elsif (/^-[abcmw]$/) { + elsif (/^-(?:[abcmw]|ct)$/) { unless (@ARGV) { whine "No argument specified with $_."; $bad = 1; last; @@ -752,6 +792,9 @@ sub comment { } push @files, shift @ARGV; } + elsif (/-ct/) { + $content_type = shift @ARGV; + } elsif (/-([bc])/) { my $a = $_ eq "-b" ? \@bcc : \@cc; @$a = split /\s*,\s*/, shift @ARGV; @@ -763,7 +806,6 @@ sub comment { while () { $msg .= $_ } } } - elsif (/-w/) { $wtime = shift @ARGV } } elsif (!$id && m|^(?:ticket/)?($idlist)$|) { @@ -785,7 +827,7 @@ sub comment { my $form = [ "", - [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ], + [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ], { Ticket => $id, Action => $action, @@ -793,6 +835,7 @@ sub comment { Bcc => [ @bcc ], Attachment => [ @files ], TimeWorked => $wtime || '', + 'Content-Type' => $content_type || 'text/plain', Text => $msg || '', Status => '' } @@ -801,30 +844,19 @@ sub comment { my $text = Form::compose([ $form ]); if ($edit || !$msg) { - my $error = 0; - my ($c, $o, $k, $e); - - do { - my $ntext = vi($text); - return if ($error && $ntext eq $text); - $text = $ntext; - $form = Form::parse($text); - $error = 0; - - ($c, $o, $k, $e) = @{ $form->[0] }; - if ($e) { - $error = 1; - $c = "# Syntax error."; - goto NEXT; - } - elsif (!@$o) { - return 0; - } - @files = @{ vsplit($k->{Attachment}) }; - - NEXT: - $text = Form::compose([[$c, $o, $k, $e]]); - } while ($error); + my ($tmp) = vi_form_while( + $text, + sub { + my ($text, $form) = @_; + foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) { + return (0, "File '$f' doesn't exist") unless -f $f; + } + @files = @{ vsplit($form->[2]{'Attachment'}) }; + return 1; + }, + ); + return $tmp unless $tmp; + $text = $tmp; } my $i = 1; @@ -899,11 +931,6 @@ sub link { if (@ARGV == 3) { my ($from, $rel, $to) = @ARGV; - if ($from !~ /^\d+$/ || $to !~ /^\d+$/) { - my $bad = $from =~ /^\d+$/ ? $to : $from; - whine "Invalid $type ID '$bad' specified."; - $bad = 1; - } if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) { whine "Invalid link '$rel' for type $type specified."; $bad = 1; @@ -966,12 +993,8 @@ sub take { sub grant { my ($cmd) = @_; - my $revoke = 0; - while (@ARGV) { - } - - $revoke = 1 if $cmd->{action} eq 'revoke'; - return 0; + whine "$cmd is unimplemented."; + return 1; } # Client <-> Server communication. @@ -984,7 +1007,7 @@ sub grant { sub submit { my ($uri, $content) = @_; my ($req, $data); - my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1); + my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1); my $h = HTTP::Headers->new; # Did the caller specify any data to send with the request? @@ -1012,7 +1035,7 @@ sub submit { # Should we send authentication information to start a new session? my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted'; - (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/; + my($server) = $config{server} =~ m{^.*//([^/]+)}; if ($config{externalauth}) { $h->authorization_basic($config{user}, $config{passwd} || read_passwd() ); print " Password will be sent to $server $how\n", @@ -1057,7 +1080,7 @@ sub submit { # "RT/3.0.1 401 Credentials required" if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) { - warn "rt: Malformed RT response from $config{server}.\n"; + warn "rt: Malformed RT response from $server.\n"; warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3; exit -1; } @@ -1164,44 +1187,40 @@ sub submit { sub load { my ($self, $file) = @_; $file ||= $self->{file}; - local *F; - - open(F, $file) && do { - $self->{file} = $file; - my $sids = $self->{sids} = {}; - while () { - chomp; - next if /^$/ || /^#/; - next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#; - my ($server, $user, $cookie) = split / /, $_; - $sids->{$server}{$user} = $cookie; - } - return 1; - }; - return 0; + + open( my $handle, '<', $file ) or return 0; + + $self->{file} = $file; + my $sids = $self->{sids} = {}; + while (<$handle>) { + chomp; + next if /^$/ || /^#/; + next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#; + my ($server, $user, $cookie) = split / /, $_; + $sids->{$server}{$user} = $cookie; + } + return 1; } # Writes the current session cache to the specified file. sub save { my ($self, $file) = shift; $file ||= $self->{file}; - local *F; - - open(F, ">$file") && do { - my $sids = $self->{sids}; - foreach my $server (keys %$sids) { - foreach my $user (keys %{ $sids->{$server} }) { - my $sid = $sids->{$server}{$user}; - if (defined $sid) { - print F "$server $user $sid\n"; - } + + open( my $handle, '>', "$file" ) or return 0; + + my $sids = $self->{sids}; + foreach my $server (keys %$sids) { + foreach my $user (keys %{ $sids->{$server} }) { + my $sid = $sids->{$server}{$user}; + if (defined $sid) { + print $handle "$server $user $sid\n"; } } - close(F); - chmod 0600, $file; - return 1; - }; - return 0; + } + close($handle); + chmod 0600, $file; + return 1; } sub DESTROY { @@ -1347,7 +1366,7 @@ sub Form::compose { $line .= ",\n$sp$v"; } else { - $line = $line ? "$line, $v" : "$key: $v"; + $line = $line ? "$line,$v" : "$key: $v"; } } @@ -1415,7 +1434,7 @@ sub config_from_file { } # Still nothing? We'll fall back to some likely defaults. - for ("$HOME/$rc", "/etc/rt.conf") { + for ("$HOME/$rc", "@LOCAL_ETC_PATH@/rt.conf", "/etc/rt.conf") { return parse_config_file($_) if (-r $_); } } @@ -1429,19 +1448,19 @@ sub parse_config_file { my ($file) = @_; local $_; # $_ may be aliased to a constant, from line 1163 - open(CFG, $file) && do { - while () { - chomp; - next if (/^#/ || /^\s*$/); + open( my $handle, '<', $file ) or return; - if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) { - $cfg{$1} = $2; - } - else { - die "rt: $file:$.: unknown configuration directive.\n"; - } + while (<$handle>) { + chomp; + next if (/^#/ || /^\s*$/); + + if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) { + $cfg{$1} = $2; + } + else { + die "rt: $file:$.: unknown configuration directive.\n"; } - }; + } return %cfg; } @@ -1471,18 +1490,58 @@ sub read_passwd { return $passwd; } +sub vi_form_while { + my $text = shift; + my $cb = shift; + + my $error = 0; + my ($c, $o, $k, $e); + do { + my $ntext = vi($text); + return undef if ($error && $ntext eq $text); + + $text = $ntext; + + my $form = Form::parse($text); + $error = 0; + ($c, $o, $k, $e) = @{ $form->[0] }; + if ( $e ) { + $error = 1; + $c = "# Syntax error."; + goto NEXT; + } + elsif (!@$o) { + return 0; + } + + my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] ); + unless ( $status ) { + $error = 1; + $c = "# $msg"; + } + + NEXT: + $text = Form::compose([[$c, $o, $k, $e]]); + } while ($error); + + return $text; +} + sub vi { my ($text) = @_; - my $file = "/tmp/rt.form.$$"; my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi"; - local *F; local $/ = undef; - open(F, ">$file") || die "$file: $!\n"; print F $text; close(F); - system($editor, $file) && die "Couldn't run $editor.\n"; - open(F, $file) || die "$file: $!\n"; $text = ; close(F); - unlink($file); + my $handle = File::Temp->new; + print $handle $text; + close($handle); + + system($editor, $handle->filename) && die "Couldn't run $editor.\n"; + + open( $handle, '<', $handle->filename ) or die "$handle: $!\n"; + $text = <$handle>; + close($handle); return $text; } @@ -1514,7 +1573,7 @@ sub vsplit { # XXX: This should become a real parser, à la Text::ParseWords. $line =~ s/^\s+//; $line =~ s/\s+$//; - my ( $a, $b ) = split /,/, $line, 2; + my ( $a, $b ) = split /\s*,\s*/, $line, 2; while ($a) { no warnings 'uninitialized'; @@ -1522,26 +1581,26 @@ sub vsplit { my $s = $a; while ( $a !~ /'$/ || ( $a !~ /(\\\\)+'$/ && $a =~ /(\\)+'$/ )) { - ( $a, $b ) = split /,/, $b, 2; + ( $a, $b ) = split /\s*,\s*/, $b, 2; $s .= ',' . $a; } push @words, $s; } - elsif ( $a =~ /^q{/ ) { + elsif ( $a =~ /^q\{/ ) { my $s = $a; - while ( $a !~ /}$/ ) { + while ( $a !~ /\}$/ ) { ( $a, $b ) = - split /,/, $b, 2; + split /\s*,\s*/, $b, 2; $s .= ',' . $a; } - $s =~ s/^q{/'/; - $s =~ s/}/'/; + $s =~ s/^q\{/'/; + $s =~ s/\}/'/; push @words, $s; } else { push @words, $a; } - ( $a, $b ) = split /,/, $b, 2; + ( $a, $b ) = split /\s*,\s*/, $b, 2; } @@ -1556,7 +1615,7 @@ sub expand_list { my ($list) = @_; my @elts; - foreach (split /,/, $list) { + foreach (split /\s*,\s*/, $list) { push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_; } @@ -1702,7 +1761,7 @@ sub prettyshow { } print "$k->{Content}\n" if exists $k->{Content} and $k->{Content} !~ /to have no content$/ and - $k->{Type} ne 'EmailRecord'; + ($k->{Type}||'') ne 'EmailRecord'; print "$k->{Attachments}\n" if exists $k->{Attachments} and $k->{Attachments}; } @@ -1826,7 +1885,8 @@ Text: The program looks for configuration directives in a file named .rtrc (or $RTCONFIG; see below) in the current directory, and then in more distant ancestors, until it reaches /. If no suitable configuration - files are found, it will also check for ~/.rtrc and /etc/rt.conf. + files are found, it will also check for ~/.rtrc, @LOCAL_ETC_PATH@/rt.conf + and /etc/rt.conf. Configuration directives: @@ -1905,8 +1965,6 @@ Text: ticket/1-3,5-7/history user/ams - user/ams/rights - user/ams,rai,1/rights For more information: @@ -2024,20 +2082,6 @@ Text: - edit - create - In addition, the following type-specific actions exist: - - - grant - - revoke - - Attributes: - - The following attributes can be used with "rt show" or "rt edit" - to retrieve or edit other information associated with users and - groups: - - rights Global rights granted to this user. - rights/ Queue rights for this user. - -- Title: queue @@ -2156,7 +2200,7 @@ Text: ("ls", "list", and "search" are synonyms.) Conditions are expressed in the SQL-like syntax used internally by - RT3. (For more information, see "rt help query".) The query string + RT. (For more information, see "rt help query".) The query string must be supplied as one argument. (Right now, the server doesn't support listing anything but tickets. @@ -2290,12 +2334,14 @@ Text: -S var=val Submits the specified variable with the request. -t type Specifies object type. + -ct content-type Specifies content type of message(tickets only). Examples: # Interactive (starts $EDITOR with a form). rt edit ticket/3 rt create -t ticket + rt create -t ticket -ct text/html # Non-interactive. rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow @@ -2327,6 +2373,7 @@ Text: Options: -m Specify comment text. + -ct Specify content-type of comment text. -a Attach a file to the comment. (May be used more than once to attach multiple files.) -c A comma-separated list of Cc addresses. @@ -2380,16 +2427,10 @@ Text: -- -Title: grant -Title: revoke -Text: - --- - Title: query Text: - RT3 uses an SQL-like syntax to specify object selection constraints. + RT uses an SQL-like syntax to specify object selection constraints. See the documentation for details. (XXX: I'm going to have to write it, aren't I?) @@ -2584,3 +2625,24 @@ Text: $ rt shell rt> quit $ + +__END__ + +=head1 NAME + +rt - command-line interface to RT 3.0 or newer + +=head1 SYNOPSIS + + rt help + +=head1 DESCRIPTION + +This script allows you to interact with an RT server over HTTP, and offers an +interface to RT's functionality that is better-suited to automation and +integration with other tools. + +In general, each invocation of this program should specify an action to +perform on one or more objects, and any other arguments required to complete +the desired action. +