File Coverage

script/rt
Criterion Covered Total %
statement 192 937 20.4
branch 68 594 11.4
condition 34 272 12.5
subroutine 21 58 36.2
pod n/a
total 315 1861 16.9


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2             # BEGIN BPS TAGGED BLOCK {{{
3             #
4             # COPYRIGHT:
5             #
6             # This software is Copyright (c) 1996-2025 Best Practical Solutions, LLC
7             #
8             #
9             # (Except where explicitly superseded by other copyright notices)
10             #
11             #
12             # LICENSE:
13             #
14             # This work is made available to you under the terms of Version 2 of
15             # the GNU General Public License. A copy of that license should have
16             # been provided with this software, but in any event can be snarfed
17             # from www.gnu.org.
18             #
19             # This work is distributed in the hope that it will be useful, but
20             # WITHOUT ANY WARRANTY; without even the implied warranty of
21             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22             # General Public License for more details.
23             #
24             # You should have received a copy of the GNU General Public License
25             # along with this program; if not, write to the Free Software
26             # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27             # 02110-1301 or visit their web page on the internet at
28             # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
29             #
30             #
31             # CONTRIBUTION SUBMISSION POLICY:
32             #
33             # (The following paragraph is not intended to limit the rights granted
34             # to you to modify and distribute this software under the terms of
35             # the GNU General Public License and is only of importance to you if
36             # you choose to contribute your changes and enhancements to the
37             # community by submitting them to Best Practical Solutions, LLC.)
38             #
39             # By intentionally submitting any modifications, corrections or
40             # derivatives to this work, or any other work intended for use with
41             # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42             # you are the copyright holder for those contributions and you grant
43             # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
44             # royalty-free, perpetual, license to use, copy, create derivative
45             # works based on those contributions, and sublicense and distribute
46             # those contributions and any derivatives thereof.
47             #
48             # END BPS TAGGED BLOCK }}}
49             # Designed and implemented for Best Practical Solutions, LLC by
50             # Abhijit Menon-Sen
51              
52 1     1   5909 use strict;
  1         2  
  1         48  
53 1     1   6 use warnings;
  1         2  
  1         237  
54              
55 1 50 33     237594 if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) {
56 0         0 require Pod::Usage;
57 0         0 print Pod::Usage::pod2usage( { verbose => 2 } );
58 0         0 exit;
59             }
60              
61             # This program is intentionally written to have as few non-core module
62             # dependencies as possible. It should stay that way.
63              
64 1     1   14 use Cwd;
  1         1  
  1         118  
65 1     1   524 use LWP;
  1         148098  
  1         62  
66 1     1   824 use Text::ParseWords;
  1         11370  
  1         161  
67 1     1   816 use HTTP::Request::Common;
  1         4061  
  1         168  
68 1     1   18 use HTTP::Headers;
  1         2  
  1         49  
69 1     1   1167 use Term::ReadLine;
  1         6310  
  1         60  
70 1     1   16 use Time::Local; # used in prettyshow
  1         2  
  1         112  
71 1     1   1239 use File::Temp;
  1         40860  
  1         5556  
72              
73             # We derive configuration information from hardwired defaults, dotfiles,
74             # and the RT* environment variables (in increasing order of precedence).
75             # Session information is stored in ~/.rt_sessions.
76              
77 1         4 my $VERSION = 0.02;
78             my $HOME = eval{(getpwuid($<))[7]}
79             || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
80 1   0     3 || ".";
81             my %config = (
82             (
83             debug => 0,
84             user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
85             passwd => undef,
86             server => 'http://localhost/',
87             query => "Status!='resolved' and Status!='rejected'",
88             orderby => 'id',
89             queue => undef,
90             # to protect against unlimited searches a better choice would be
91             # queue => 'Unknown_Queue',
92             auth => "rt",
93             ),
94 1   0     8 config_from_file($ENV{RTCONFIG} || ".rtrc"),
      50        
95             config_from_env()
96             );
97              
98 1 50       16 $config{auth} = "basic" if delete $config{externalauth};
99              
100 1         41 my $session = Session->new("$HOME/.rt_sessions");
101 1         12 my $REST = "$config{server}/REST/1.0";
102              
103 1         3 my $prompt = 'rt> ';
104              
105             sub whine;
106 0 0   0   0 sub DEBUG { warn @_ if $config{debug} >= shift }
107              
108             # These regexes are used by command handlers to parse arguments.
109             # (XXX: Ask Autrijus how i18n changes these definitions.)
110              
111 1         5 my $name = '[\w.-]+';
112 1         6 my $CF_name = '[^,]+?';
113 1         6 my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
114 1         8 my $label = '[^,\\/]+';
115 1         3 my $labels = "(?:$label,)*$label";
116 1         4 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
117              
118             # Our command line looks like this:
119             #
120             # rt [options] [arguments]
121             #
122             # We'll parse just enough of it to decide upon an action to perform, and
123             # leave the rest to per-action handlers to interpret appropriately.
124              
125 1         112 my %handlers = (
126             # handler => [ ...aliases... ],
127             version => ["version", "ver"],
128             shell => ["shell"],
129             logout => ["logout"],
130             help => ["help", "man"],
131             show => ["show", "cat"],
132             edit => ["create", "edit", "new", "ed"],
133             list => ["search", "list", "ls"],
134             comment => ["comment", "correspond"],
135             link => ["link", "ln"],
136             merge => ["merge"],
137             grant => ["grant", "revoke"],
138             take => ["take", "steal", "untake"],
139             quit => ["quit", "exit"],
140             setcommand => ["del", "delete", "give", "res", "resolve",
141             "subject"],
142             );
143              
144 1         4 my %actions;
145 1         11 foreach my $fn (keys %handlers) {
146 14         20 foreach my $alias (@{ $handlers{$fn} }) {
  14         29  
147 33         43 $actions{$alias} = \&{"$fn"};
  33         190  
148             }
149             }
150              
151             # Once we find and call an appropriate handler, we're done.
152              
153             sub handler {
154 1     1   3 my $action;
155              
156 1 50       5 push @ARGV, 'shell' if (!@ARGV); # default to shell mode
157 1 50       17 shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt'
158 1 50 33     11 if (@ARGV && exists $actions{$ARGV[0]}) {
159 1         6 $action = shift @ARGV;
160 1         10 return $actions{$action}->($action);
161             }
162             else {
163 0         0 print STDERR "rt: Unknown command '@ARGV'.\n";
164 0         0 print STDERR "rt: For help, run 'rt help'.\n";
165 0         0 return 1;
166             }
167             }
168              
169 1         9 exit handler();
170              
171             # Handler functions.
172             # ------------------
173             #
174             # The following subs are handlers for each entry in %actions.
175              
176             sub shell {
177 0     0   0 $|=1;
178 0         0 my $term = Term::ReadLine->new('RT CLI');
179 0         0 while ( defined ($_ = $term->readline($prompt)) ) {
180 0 0 0     0 next if /^#/ || /^\s*$/;
181              
182 0         0 @ARGV = shellwords($_);
183 0         0 handler();
184             }
185             }
186              
187             sub version {
188 0     0   0 print "rt $VERSION\n";
189 0         0 return 0;
190             }
191              
192             sub logout {
193 0 0   0   0 submit("$REST/logout") if defined $session->cookie;
194 0         0 return 0;
195             }
196              
197             sub quit {
198 0     0   0 logout();
199 0         0 exit;
200             }
201              
202 0         0 my %help;
203             sub help {
204 1     1   10 my ($action, $type, $rv) = @_;
205 1 50       5 $rv = defined $rv ? $rv : 0;
206 1         2 my $key;
207              
208             # What help topics do we know about?
209 1 50       4 if (!%help) {
210 1         33 local $/ = undef;
211 1         4 foreach my $item (@{ Form::parse() }) {
  1         119  
212 30         77 my $title = $item->[2]{Title};
213 30 100       74 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
214              
215 30         53 foreach $title (grep $_, @titles) {
216 48         189 $help{$title} = $item->[2]{Text};
217             }
218             }
219             }
220              
221             # What does the user want help with?
222 1 50 33     60 undef $action if ($action && $actions{$action} eq \&help);
223 1 50 33     11 unless ($action || $type) {
224             # If we don't know, we'll look for clues in @ARGV.
225 1         5 foreach (@ARGV) {
226 0 0       0 if (exists $help{$_}) { $key = $_; last; }
  0         0  
  0         0  
227             }
228 1 50       3 unless ($key) {
229             # Tolerate possibly plural words.
230 1         3 foreach (@ARGV) {
231 0 0 0     0 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
  0         0  
  0         0  
232             }
233             }
234             }
235              
236 1 50 33     5 if ($type && $action) {
237 0         0 $key = "$type.$action";
238             }
239 1   50     24 $key ||= $type || $action || "introduction";
      33        
240              
241             # Find a suitable topic to display.
242 1         5 while (!exists $help{$key}) {
243 0 0 0     0 if ($type && $action) {
244 0 0       0 if ($key eq "$type.$action") { $key = $action; }
  0 0       0  
245 0         0 elsif ($key eq $action) { $key = $type; }
246 0         0 else { $key = "introduction"; }
247             }
248             else {
249 0         0 $key = "introduction";
250             }
251             }
252              
253 1         51 print STDERR $help{$key}, "\n\n";
254 1         188 return $rv;
255             }
256              
257             # Displays a list of objects that match some specified condition.
258              
259             sub list {
260 0     0   0 my ($q, $type, %data);
261 0         0 my $orderby = $config{orderby};
262            
263 0 0       0 if ($config{orderby}) {
264 0         0 $data{orderby} = $config{orderby};
265             }
266 0         0 my $bad = 0;
267 0         0 my $rawprint = 0;
268 0         0 my $reverse_sort = 0;
269 0         0 my $queue = $config{queue};
270              
271 0         0 while (@ARGV) {
272 0         0 $_ = shift @ARGV;
273              
274 0 0 0     0 if (/^-t$/) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
275 0 0       0 $bad = 1, last unless defined($type = get_type_argument());
276             }
277             elsif (/^-S$/) {
278 0 0       0 $bad = 1, last unless get_var_argument(\%data);
279             }
280             elsif (/^-o$/) {
281 0         0 $data{'orderby'} = shift @ARGV;
282             }
283             elsif (/^-([isl])$/) {
284 0         0 $data{format} = $1;
285 0         0 $rawprint = 1;
286             }
287             elsif (/^-q$/) {
288 0         0 $queue = shift @ARGV;
289             }
290             elsif (/^-r$/) {
291 0         0 $reverse_sort = 1;
292             }
293             elsif (/^-f$/) {
294 0 0       0 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
295 0         0 whine "No valid field list in '-f $ARGV[0]'.";
296 0         0 $bad = 1; last;
  0         0  
297             }
298 0         0 $data{fields} = shift @ARGV;
299 0 0       0 $data{format} = 's' if ! $data{format};
300 0         0 $rawprint = 1;
301             }
302             elsif (!defined $q && !/^-/) {
303 0         0 $q = $_;
304             }
305             else {
306 0 0       0 my $datum = /^-/ ? "option" : "argument";
307 0         0 whine "Unrecognised $datum '$_'.";
308 0         0 $bad = 1; last;
  0         0  
309             }
310             }
311 0 0 0     0 if ( ! $rawprint and ! exists $data{format} ) {
312 0         0 $data{format} = 'l';
313 0         0 $data{fields} = 'subject,status,queue,created,told,owner,requestors';
314             }
315 0 0 0     0 if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
    0          
316 0         0 $data{orderby} =~ s/^-/+/;
317             } elsif ($reverse_sort) {
318 0         0 $data{orderby} =~ s/^\+?(.*)/-$1/;
319             }
320              
321 0   0     0 $type ||= "ticket";
322              
323 0 0       0 if (!defined $q ) {
324 0 0       0 if ( $type eq 'ticket' ) {
325 0         0 $q = $config{query};
326             }
327             else {
328 0         0 $q = '';
329             }
330             }
331              
332 0 0       0 if ( $type ne 'ticket' ) {
333 0         0 $rawprint = 1;
334             }
335              
336 0 0       0 unless (defined $q) {
337 0 0       0 my $item = $type ? "query string" : "object type";
338 0         0 whine "No $item specified.";
339 0         0 $bad = 1;
340             }
341              
342 0         0 $q =~ s/^#//; # get rid of leading hash
343 0 0       0 if ( $type eq 'ticket' ) {
344 0 0       0 if ( $q =~ /^\d+$/ ) {
345              
346             # only digits, must be an id, formulate a correct query
347 0 0       0 $q = "id=$q" if $q =~ /^\d+$/;
348             }
349             else {
350              
351             # a string only, take it as an owner or requestor (quoting done later)
352 0 0       0 $q = "(Owner=$q or Requestor like $q) and $config{query}"
353             if $q =~ /^[\w\-]+$/;
354              
355             # always add a query for a specific queue or (comma separated) queues
356 0 0       0 $queue =~ s/,/ or Queue=/g if $queue;
357 0 0 0     0 $q .= " and (Queue=$queue)"
      0        
      0        
358             if $queue
359             and $q
360             and $q !~ /Queue\s*=/i
361             and $q !~ /id\s*=/i;
362             }
363              
364             # correctly quote strings in a query
365 0         0 $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
366             }
367              
368             #return help("list", $type) if $bad;
369 0 0       0 return suggest_help("list", $type, $bad) if $bad;
370              
371 0 0       0 print "Query:$q\n" if ! $rawprint;
372 0         0 my $r = submit("$REST/search/$type", { query => $q, %data });
373 0 0       0 if ( $rawprint ) {
374 0         0 print $r->content;
375             } else {
376 0         0 my $forms = Form::parse($r->content);
377 0         0 prettylist ($forms);
378             }
379 0         0 return 0;
380             }
381              
382             # Displays selected information about a single object.
383              
384             sub show {
385 0     0   0 my ($type, @objects, %data);
386 0         0 my $slurped = 0;
387 0         0 my $bad = 0;
388 0         0 my $rawprint = 0;
389 0         0 my $histspec;
390              
391 0         0 while (@ARGV) {
392 0         0 $_ = shift @ARGV;
393 0 0       0 s/^#// if /^#\d+/; # get rid of leading hash
394 0 0 0     0 if (/^-t$/) {
    0 0        
    0 0        
    0          
    0          
    0          
    0          
    0          
395 0 0       0 $bad = 1, last unless defined($type = get_type_argument());
396             }
397             elsif (/^-S$/) {
398 0 0       0 $bad = 1, last unless get_var_argument(\%data);
399             }
400             elsif (/^-([isl])$/) {
401 0         0 $data{format} = $1;
402 0         0 $rawprint = 1;
403             }
404             elsif (/^-$/ && !$slurped) {
405 0         0 chomp(my @lines = );
406 0         0 foreach (@lines) {
407 0 0       0 unless (is_object_spec($_, $type)) {
408 0         0 whine "Invalid object on STDIN: '$_'.";
409 0         0 $bad = 1; last;
  0         0  
410             }
411 0         0 push @objects, $_;
412             }
413 0         0 $slurped = 1;
414             }
415             elsif (/^-f$/) {
416 0 0       0 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
417 0         0 whine "No valid field list in '-f $ARGV[0]'.";
418 0         0 $bad = 1; last;
  0         0  
419             }
420 0         0 $data{fields} = shift @ARGV;
421             # option f requires short raw listing format
422 0         0 $data{format} = 's';
423 0         0 $rawprint = 1;
424             }
425             elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
426 0         0 push @objects, $spc2;
427 0         0 $histspec = is_object_spec("ticket/$_/history", $type);
428             }
429             elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) {
430 0         0 push @objects, $spc3;
431 0 0       0 $rawprint = 1 if $_ =~ /\/content$/;
432             }
433             elsif (my $spec = is_object_spec($_, $type)) {
434 0         0 push @objects, $spec;
435 0 0 0     0 $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
      0        
436             }
437             else {
438 0 0       0 my $datum = /^-/ ? "option" : "argument";
439 0         0 whine "Unrecognised $datum '$_'.";
440 0         0 $bad = 1; last;
  0         0  
441             }
442             }
443 0 0       0 if ( ! $rawprint ) {
444 0 0       0 push @objects, $histspec if $histspec;
445 0 0       0 $data{format} = 'l' if ! exists $data{format};
446             }
447              
448 0 0       0 unless (@objects) {
449 0         0 whine "No objects specified.";
450 0         0 $bad = 1;
451             }
452             #return help("show", $type) if $bad;
453 0 0       0 return suggest_help("show", $type, $bad) if $bad;
454              
455 0         0 my $r = submit("$REST/show", { id => \@objects, %data });
456 0         0 my $c = $r->content;
457             # if this isn't a text reply, remove the trailing newline so we
458             # don't corrupt things like tarballs when people do
459             # show ticket/id/attachments/id/content > foo.tar.gz
460 0 0       0 if ($r->content_type !~ /^text\//) {
461 0         0 chomp($c);
462 0         0 $rawprint = 1;
463             }
464 0 0       0 if ( $rawprint ) {
465 0         0 print $c;
466             } else {
467             # I do not know how to get more than one form correctly returned
468 0         0 $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg;
469 0         0 my $forms = Form::parse($c);
470 0         0 prettyshow ($forms);
471             }
472 0         0 return 0;
473             }
474              
475             # To create a new object, we ask the server for a form with the defaults
476             # filled in, allow the user to edit it, and send the form back.
477             #
478             # To edit an object, we must ask the server for a form representing that
479             # object, make changes requested by the user (either on the command line
480             # or interactively via $EDITOR), and send the form back.
481              
482             sub edit {
483 0     0   0 my ($action) = @_;
484 0         0 my (%data, $type, @objects);
485 0         0 my ($cl, $text, $edit, $input, $output, $content_type);
486              
487 1     1   17 use vars qw(%set %add %del);
  1         2  
  1         159356  
488 0         0 %set = %add = %del = ();
489 0         0 my $slurped = 0;
490 0         0 my $bad = 0;
491            
492 0         0 while (@ARGV) {
493 0         0 $_ = shift @ARGV;
494 0 0       0 s/^#// if /^#\d+/; # get rid of leading hash
495              
496 0 0 0     0 if (/^-e$/) { $edit = 1 }
  0 0 0     0  
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
497 0         0 elsif (/^-i$/) { $input = 1 }
498 0         0 elsif (/^-o$/) { $output = 1 }
499 0         0 elsif (/^-ct$/) { $content_type = shift @ARGV }
500             elsif (/^-t$/) {
501 0 0       0 $bad = 1, last unless defined($type = get_type_argument());
502             }
503             elsif (/^-S$/) {
504 0 0       0 $bad = 1, last unless get_var_argument(\%data);
505             }
506             elsif (/^-$/ && !($slurped || $input)) {
507 0         0 chomp(my @lines = );
508 0         0 foreach (@lines) {
509 0 0       0 unless (is_object_spec($_, $type)) {
510 0         0 whine "Invalid object on STDIN: '$_'.";
511 0         0 $bad = 1; last;
  0         0  
512             }
513 0         0 push @objects, $_;
514             }
515 0         0 $slurped = 1;
516             }
517             elsif (/^set$/i) {
518 0         0 my $vars = 0;
519              
520 0   0     0 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
521 0         0 my ($key, $op, $val) = ($1, $2, $3);
522 0 0       0 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
    0          
523              
524 0         0 vpush($hash, lc $key, $val);
525 0         0 shift @ARGV;
526 0         0 $vars++;
527             }
528 0 0       0 unless ($vars) {
529 0         0 whine "No variables to set.";
530 0         0 $bad = 1; last;
  0         0  
531             }
532 0         0 $cl = $vars;
533             }
534             elsif (/^(?:add|del)$/i) {
535 0         0 my $vars = 0;
536 0 0       0 my $hash = ($_ eq "add") ? \%add : \%del;
537              
538 0   0     0 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
539 0         0 my ($key, $val) = ($1, $2);
540              
541 0         0 vpush($hash, lc $key, $val);
542 0         0 shift @ARGV;
543 0         0 $vars++;
544             }
545 0 0       0 unless ($vars) {
546 0         0 whine "No variables to set.";
547 0         0 $bad = 1; last;
  0         0  
548             }
549 0         0 $cl = $vars;
550             }
551             elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
552 0         0 push @objects, $spc2;
553             }
554             elsif (my $spec = is_object_spec($_, $type)) {
555 0         0 push @objects, $spec;
556             }
557             else {
558 0 0       0 my $datum = /^-/ ? "option" : "argument";
559 0         0 whine "Unrecognised $datum '$_'.";
560 0         0 $bad = 1; last;
  0         0  
561             }
562             }
563              
564 0 0       0 if ($action =~ /^ed(?:it)?$/) {
565 0 0       0 unless (@objects) {
566 0         0 whine "No objects specified.";
567 0         0 $bad = 1;
568             }
569             }
570             else {
571 0 0       0 if (@objects) {
572 0         0 whine "You shouldn't specify objects as arguments to $action.";
573 0         0 $bad = 1;
574             }
575 0 0       0 unless ($type) {
576 0         0 whine "What type of object do you want to create?";
577 0         0 $bad = 1;
578             }
579 0 0       0 @objects = ("$type/new") if defined($type);
580             }
581             #return help($action, $type) if $bad;
582 0 0       0 return suggest_help($action, $type, $bad) if $bad;
583              
584             # We need a form to make changes to. We usually ask the server for
585             # one, but we can avoid that if we are fed one on STDIN, or if the
586             # user doesn't want to edit the form by hand, and the command line
587             # specifies only simple variable assignments. We *should* get a
588             # form if we're creating a new ticket, so that the default values
589             # get filled in properly.
590              
591 0         0 my @new_objects = grep /\/new$/, @objects;
592              
593 0 0 0     0 if ($input) {
    0 0        
      0        
      0        
594 0         0 local $/ = undef;
595 0         0 $text = ;
596             }
597             elsif ($edit || %add || %del || !$cl || @new_objects) {
598 0         0 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
599 0         0 $text = $r->content;
600             }
601              
602             # If any changes were specified on the command line, apply them.
603 0 0       0 if ($cl) {
604 0 0       0 if ($text) {
605             # We're updating forms from the server.
606 0         0 my $forms = Form::parse($text);
607              
608 0         0 foreach my $form (@$forms) {
609 0         0 my ($c, $o, $k, $e) = @$form;
610 0         0 my ($key, $val);
611              
612 0 0 0     0 next if ($e || !@$o);
613              
614 0         0 local %add = %add;
615 0         0 local %del = %del;
616 0         0 local %set = %set;
617              
618             # Make changes to existing fields.
619 0         0 foreach $key (@$o) {
620 0 0       0 if (exists $add{lc $key}) {
621 0         0 $val = delete $add{lc $key};
622 0         0 vpush($k, $key, $val);
623 0 0       0 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
624             }
625 0 0       0 if (exists $del{lc $key}) {
626 0         0 $val = delete $del{lc $key};
627 0         0 my %val = map {$_=>1} @{ vsplit($val) };
  0         0  
  0         0  
628 0         0 $k->{$key} = vsplit($k->{$key});
629 0         0 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
  0         0  
  0         0  
  0         0  
630             }
631 0 0       0 if (exists $set{lc $key}) {
632 0         0 $k->{$key} = delete $set{lc $key};
633             }
634             }
635            
636             # Then update the others.
637 0         0 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
  0         0  
638 0         0 foreach $key (keys %add) {
639 0         0 vpush($k, $key, $add{$key});
640 0         0 $k->{$key} = vsplit($k->{$key});
641             }
642 0         0 push @$o, (keys %add, keys %set);
643             }
644              
645 0         0 $text = Form::compose($forms);
646             }
647             else {
648             # We're rolling our own set of forms.
649 0         0 my @forms;
650 0         0 foreach (@objects) {
651 0         0 my ($type, $ids, $args) =
652             m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
653              
654 0   0     0 $args ||= "";
655 0         0 foreach my $obj (expand_list($ids)) {
656 0         0 my %set = (%set, id => "$type/$obj$args");
657 0         0 push @forms, ["", [keys %set], \%set];
658             }
659             }
660 0         0 $text = Form::compose(\@forms);
661             }
662             }
663              
664 0 0       0 if ($output) {
665 0         0 print $text;
666 0         0 return 0;
667             }
668              
669 0         0 my @files;
670 0 0       0 @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'};
  0         0  
671              
672 0         0 my $synerr = 0;
673              
674             EDIT:
675             # We'll let the user edit the form before sending it to the server,
676             # unless we have enough information to submit it non-interactively.
677 0 0 0     0 if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) {
      0        
678 0 0 0     0 $text .= "Content-Type: $content_type\n"
679             if $content_type and $content_type ne "text/plain";
680             }
681              
682 0 0 0     0 if ($edit || (!$input && !$cl)) {
      0        
683             my ($newtext) = vi_form_while(
684             $text,
685             sub {
686 0     0   0 my ($text, $form) = @_;
687 0 0       0 return 1 unless exists $form->[2]{'Attachment'};
688              
689 0         0 foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
  0         0  
690 0 0       0 return (0, "File '$f' doesn't exist") unless -f $f;
691             }
692 0         0 @files = @{ vsplit($form->[2]{'Attachment'}) };
  0         0  
693 0         0 return 1;
694             },
695 0         0 );
696 0 0       0 return $newtext unless $newtext;
697             # We won't resubmit a bad form unless it was changed.
698 0 0 0     0 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
699             }
700              
701 0         0 delete @data{ grep /^attachment_\d+$/, keys %data };
702 0         0 my $i = 1;
703 0         0 foreach my $file (@files) {
704 0         0 $data{"attachment_$i"} = bless([ $file ], "Attachment");
705 0         0 $i++;
706             }
707              
708 0 0       0 if ($text) {
709 0         0 my $r = submit("$REST/edit", {content => $text, %data});
710 0 0       0 if ($r->code == 409) {
711             # If we submitted a bad form, we'll give the user a chance
712             # to correct it and resubmit.
713 0 0 0     0 if ($edit || (!$input && !$cl)) {
      0        
714 0         0 my $content = $r->content . "\n";
715 0         0 $content =~ s/^(?!#)/# /mg;
716 0         0 $text = $content . $text;
717 0         0 $synerr = 1;
718 0         0 goto EDIT;
719             }
720             else {
721 0         0 print $r->content;
722 0         0 return 0;
723             }
724             }
725 0         0 print $r->content;
726             }
727 0         0 return 0;
728             }
729              
730             # handler for special edit commands. A valid edit command is constructed and
731             # further work is delegated to the edit handler
732              
733             sub setcommand {
734 0     0   0 my ($action) = @_;
735 0         0 my ($id, $bad, $what);
736 0 0       0 if ( @ARGV ) {
737 0         0 $_ = shift @ARGV;
738 0 0       0 $id = $1 if (m|^(?:ticket/)?($idlist)$|);
739             }
740 0 0       0 if ( ! $id ) {
741 0         0 $bad = 1;
742 0         0 whine "No ticket number specified.";
743             }
744 0 0       0 if ( @ARGV ) {
745 0 0       0 if ($action eq 'subject') {
    0          
746 0         0 my $subject = '"'.join (" ", @ARGV).'"';
747 0         0 @ARGV = ();
748 0         0 $what = "subject=$subject";
749             } elsif ($action eq 'give') {
750 0         0 my $owner = shift @ARGV;
751 0         0 $what = "owner=$owner";
752             }
753             } else {
754 0 0 0     0 if ( $action eq 'delete' or $action eq 'del' ) {
    0 0        
    0          
    0          
755 0         0 $what = "status=deleted";
756             } elsif ($action eq 'resolve' or $action eq 'res' ) {
757 0         0 $what = "status=resolved";
758             } elsif ($action eq 'take' ) {
759 0         0 $what = "owner=$config{user}";
760             } elsif ($action eq 'untake') {
761 0         0 $what = "owner=Nobody";
762             }
763             }
764 0 0       0 if (@ARGV) {
765 0         0 $bad = 1;
766 0         0 whine "Extraneous arguments for action $action: @ARGV.";
767             }
768 0 0       0 if ( ! $what ) {
769 0         0 $bad = 1;
770 0         0 whine "unrecognized action $action.";
771             }
772 0 0       0 return help("edit", undef, $bad) if $bad;
773 0         0 @ARGV = ( $id, "set", $what );
774 0         0 print "Executing: rt edit @ARGV\n";
775 0         0 return edit("edit");
776             }
777              
778             # We roll "comment" and "correspond" into the same handler.
779              
780             sub comment {
781 0     0   0 my ($action) = @_;
782 0         0 my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
783 0         0 my $bad = 0;
784 0         0 my $status = '';
785              
786 0         0 while (@ARGV) {
787 0         0 $_ = shift @ARGV;
788              
789 0 0 0     0 if (/^-e$/) {
    0          
    0          
790 0         0 $edit = 1;
791             }
792             elsif (/^-(?:[abcmws]|ct)$/) {
793 0 0       0 unless (@ARGV) {
794 0         0 whine "No argument specified with $_.";
795 0         0 $bad = 1; last;
  0         0  
796             }
797              
798 0 0       0 if (/-a/) {
    0          
    0          
    0          
    0          
    0          
799 0 0 0     0 unless (-f $ARGV[0] && -r $ARGV[0]) {
800 0         0 whine "Cannot read attachment: '$ARGV[0]'.";
801 0         0 return 0;
802             }
803 0         0 push @files, shift @ARGV;
804             }
805             elsif (/-ct/) {
806 0         0 $content_type = shift @ARGV;
807             }
808             elsif (/-s/) {
809 0         0 $status = shift @ARGV;
810             }
811             elsif (/-([bc])/) {
812 0 0       0 my $a = $_ eq "-b" ? \@bcc : \@cc;
813 0         0 @$a = split /\s*,\s*/, shift @ARGV;
814             }
815             elsif (/-m/) {
816 0         0 $msg = shift @ARGV;
817 0 0       0 if ( $msg =~ /^-$/ ) {
818 0         0 undef $msg;
819 0         0 while () { $msg .= $_ }
  0         0  
820             }
821             }
822 0         0 elsif (/-w/) { $wtime = shift @ARGV }
823             }
824             elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
825 0         0 $id = $1;
826             }
827             else {
828 0 0       0 my $datum = /^-/ ? "option" : "argument";
829 0         0 whine "Unrecognised $datum '$_'.";
830 0         0 $bad = 1; last;
  0         0  
831             }
832             }
833              
834 0 0       0 unless ($id) {
835 0         0 whine "No object specified.";
836 0         0 $bad = 1;
837             }
838             #return help($action, "ticket") if $bad;
839 0 0       0 return suggest_help($action, "ticket") if $bad;
840              
841 0   0     0 my $form = [
      0        
      0        
842             "",
843             [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
844             {
845             Ticket => $id,
846             Action => $action,
847             Cc => [ @cc ],
848             Bcc => [ @bcc ],
849             Attachment => [ @files ],
850             TimeWorked => $wtime || '',
851             'Content-Type' => $content_type || 'text/plain',
852             Text => $msg || '',
853             Status => $status
854             }
855             ];
856 0 0       0 if ($status ne '') {
857 0         0 push(@{$form->[1]}, "Status");
  0         0  
858             }
859              
860 0         0 my $text = Form::compose([ $form ]);
861              
862 0 0 0     0 if ($edit || !$msg) {
863             my ($tmp) = vi_form_while(
864             $text,
865             sub {
866 0     0   0 my ($text, $form) = @_;
867 0         0 foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
  0         0  
868 0 0       0 return (0, "File '$f' doesn't exist") unless -f $f;
869             }
870 0         0 @files = @{ vsplit($form->[2]{'Attachment'}) };
  0         0  
871 0         0 return 1;
872             },
873 0         0 );
874 0 0       0 return $tmp unless $tmp;
875 0         0 $text = $tmp;
876             }
877              
878 0         0 my $i = 1;
879 0         0 foreach my $file (@files) {
880 0         0 $data{"attachment_$i"} = bless([ $file ], "Attachment");
881 0         0 $i++;
882             }
883 0         0 $data{content} = $text;
884              
885 0         0 my $r = submit("$REST/ticket/$id/comment", \%data);
886 0         0 print $r->content;
887 0         0 return 0;
888             }
889              
890             # Merge one ticket into another.
891              
892             sub merge {
893 0     0   0 my @id;
894 0         0 my $bad = 0;
895              
896 0         0 while (@ARGV) {
897 0         0 $_ = shift @ARGV;
898 0 0       0 s/^#// if /^#\d+/; # get rid of leading hash
899              
900 0 0       0 if (/^\d+$/) {
901 0         0 push @id, $_;
902             }
903             else {
904 0         0 whine "Unrecognised argument: '$_'.";
905 0         0 $bad = 1; last;
  0         0  
906             }
907             }
908              
909 0 0       0 unless (@id == 2) {
910 0 0       0 my $evil = @id > 2 ? "many" : "few";
911 0         0 whine "Too $evil arguments specified.";
912 0         0 $bad = 1;
913             }
914             #return help("merge", "ticket") if $bad;
915 0 0       0 return suggest_help("merge", "ticket", $bad) if $bad;
916              
917 0         0 my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
918 0         0 print $r->content;
919 0         0 return 0;
920             }
921              
922             # Link one ticket to another.
923              
924             sub link {
925 0     0   0 my ($bad, $del, %data) = (0, 0, ());
926 0         0 my $type;
927              
928 0         0 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
  0         0  
929             ReferredToBy HasMember MemberOf);
930              
931 0   0     0 while (@ARGV && $ARGV[0] =~ /^-/) {
932 0         0 $_ = shift @ARGV;
933              
934 0 0       0 if (/^-d$/) {
    0          
935 0         0 $del = 1;
936             }
937             elsif (/^-t$/) {
938 0 0       0 $bad = 1, last unless defined($type = get_type_argument());
939             }
940             else {
941 0         0 whine "Unrecognised option: '$_'.";
942 0         0 $bad = 1; last;
  0         0  
943             }
944             }
945            
946 0 0       0 $type = "ticket" unless $type; # default type to tickets
947            
948 0 0       0 if (@ARGV == 3) {
949 0         0 my ($from, $rel, $to) = @ARGV;
950 0 0 0     0 if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
951 0         0 whine "Invalid link '$rel' for type $type specified.";
952 0         0 $bad = 1;
953             }
954 0         0 %data = (id => $from, rel => $rel, to => $to, del => $del);
955             }
956             else {
957 0 0       0 my $bad = @ARGV < 3 ? "few" : "many";
958 0         0 whine "Too $bad arguments specified.";
959 0         0 $bad = 1;
960             }
961 0 0       0 return suggest_help("link", $type, $bad) if $bad;
962            
963 0         0 my $r = submit("$REST/$type/link", \%data);
964 0         0 print $r->content;
965 0         0 return 0;
966             }
967              
968             # Take/steal a ticket
969             sub take {
970 0     0   0 my ($cmd) = @_;
971 0         0 my ($bad, %data) = (0, ());
972              
973 0         0 my $id;
974              
975             # get the ticket id
976 0 0       0 if (@ARGV == 1) {
977 0         0 ($id) = @ARGV;
978 0 0       0 unless ($id =~ /^\d+$/) {
979 0         0 whine "Invalid ticket ID $id specified.";
980 0         0 $bad = 1;
981             }
982 0         0 my $form = [
983             "",
984             [ "Ticket", "Action" ],
985             {
986             Ticket => $id,
987             Action => $cmd,
988             Status => '',
989             }
990             ];
991              
992 0         0 my $text = Form::compose([ $form ]);
993 0         0 $data{content} = $text;
994             }
995             else {
996 0 0       0 $bad = @ARGV < 1 ? "few" : "many";
997 0         0 whine "Too $bad arguments specified.";
998 0         0 $bad = 1;
999             }
1000 0 0       0 return suggest_help("take", "ticket", $bad) if $bad;
1001              
1002 0         0 my $r = submit("$REST/ticket/$id/take", \%data);
1003 0         0 print $r->content;
1004 0         0 return 0;
1005             }
1006              
1007             # Grant/revoke a user's rights.
1008              
1009             sub grant {
1010 0     0   0 my ($cmd) = @_;
1011              
1012 0         0 whine "$cmd is unimplemented.";
1013 0         0 return 1;
1014             }
1015              
1016             # Client <-> Server communication.
1017             # --------------------------------
1018             #
1019             # This function composes and sends an HTTP request to the RT server, and
1020             # interprets the response. It takes a request URI, and optional request
1021             # data (a string, or a reference to a set of key-value pairs).
1022              
1023             sub submit {
1024 0     0   0 my ($uri, $content) = @_;
1025 0         0 my ($req, $data);
1026 0         0 my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
1027 0         0 my $h = HTTP::Headers->new;
1028              
1029             # Did the caller specify any data to send with the request?
1030 0         0 $data = [];
1031 0 0       0 if (defined $content) {
1032 0 0 0     0 unless (ref $content) {
1033             # If it's just a string, make sure LWP handles it properly.
1034             # (By pretending that it's a file!)
1035 0         0 $content = [ content => [undef, "", Content => $content] ];
1036             }
1037             elsif (ref $content eq 'HASH') {
1038             my @data;
1039             foreach my $k (keys %$content) {
1040             if (ref $content->{$k} eq 'ARRAY') {
1041             foreach my $v (@{ $content->{$k} }) {
1042             push @data, $k, $v;
1043             }
1044             }
1045             else { push @data, $k, $content->{$k} }
1046             }
1047             $content = \@data;
1048             }
1049 0         0 $data = $content;
1050             }
1051              
1052             # Should we send authentication information to start a new session?
1053 0 0       0 my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
1054 0         0 my($server) = $config{server} =~ m{^.*//([^/]+)};
1055              
1056 0 0       0 if ($config{auth} eq "gssapi") {
    0          
    0          
1057             die "GSSAPI support not available; failed to load perl module GSSAPI:\n$@\n"
1058 0 0       0 unless eval { require GSSAPI; 1 };
  0         0  
  0         0  
1059             die "GSSAPI support not available; failed to load perl module LWP::Authen::Negotiate:\n$@\n"
1060 0 0       0 unless eval { require LWP::Authen::Negotiate; 1 };
  0         0  
  0         0  
1061             } elsif ($config{auth} eq "basic") {
1062             print " Password will be sent to $server $how\n",
1063             " Press CTRL-C now if you do not want to continue\n"
1064 0 0       0 if ! $config{passwd};
1065 0   0     0 $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
1066             } elsif ( !defined $session->cookie ) {
1067             print " Password will be sent to $server $how\n",
1068             " Press CTRL-C now if you do not want to continue\n"
1069 0 0       0 if ! $config{passwd};
1070 0         0 push @$data, ( user => $config{user} );
1071 0   0     0 push @$data, ( pass => $config{passwd} || read_passwd() );
1072             }
1073              
1074             # Now, we construct the request.
1075 0 0       0 if (@$data) {
1076 0         0 $req = POST($uri, $data, Content_Type => 'form-data');
1077             }
1078             else {
1079 0         0 $req = GET($uri);
1080             }
1081 0         0 $session->add_cookie_header($req);
1082 0 0       0 $req->header(%$h) if %$h;
1083              
1084             # Then we send the request and parse the response.
1085 0         0 DEBUG(3, $req->as_string);
1086 0         0 my $res = $ua->request($req);
1087 0         0 DEBUG(3, $res->as_string);
1088              
1089 0 0       0 if ($res->is_success) {
1090             # The content of the response we get from the RT server consists
1091             # of an HTTP-like status line followed by optional header lines,
1092             # a blank line, and arbitrary text.
1093              
1094 0         0 my ($head, $text) = split /\n\n/, $res->content, 2;
1095 0         0 my ($status, @headers) = split /\n/, $head;
1096 0 0       0 $text =~ s/\n*$/\n/ if ($text);
1097              
1098             # "RT/3.0.1 401 Credentials required"
1099 0 0       0 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
1100 0         0 warn "rt: Malformed RT response from $server.\n";
1101 0 0       0 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
1102 0         0 exit -1;
1103             }
1104              
1105             # Our caller can pretend that the server returned a custom HTTP
1106             # response code and message. (Doing that directly is apparently
1107             # not sufficiently portable and uncomplicated.)
1108 0         0 $res->code($1);
1109 0         0 $res->message($2);
1110 0         0 $res->content($text);
1111 0 0 0     0 $session->update($res) if ($res->is_success || $res->code != 401);
1112              
1113 0 0       0 if (!$res->is_success) {
1114             # We can deal with authentication failures ourselves. Either
1115             # we sent invalid credentials, or our session has expired.
1116 0 0       0 if ($res->code == 401) {
    0          
1117 0         0 my %d = @$data;
1118 0 0       0 if (exists $d{user}) {
    0          
1119 0         0 warn "rt: Incorrect username or password.\n";
1120 0         0 exit -1;
1121             }
1122             elsif ($req->header("Cookie")) {
1123             # We'll retry the request with credentials, unless
1124             # we only wanted to logout in the first place.
1125 0         0 $session->delete;
1126 0 0       0 return submit(@_) unless $uri eq "$REST/logout";
1127             }
1128             }
1129             # Conflicts should be dealt with by the handler and user.
1130             # For anything else, we just die.
1131             elsif ($res->code != 409) {
1132 0         0 warn "rt: ", $res->content;
1133             #exit;
1134             }
1135             }
1136             }
1137             else {
1138 0         0 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
1139 0         0 exit -1;
1140             }
1141              
1142 0         0 return $res;
1143             }
1144              
1145             # Session management.
1146             # -------------------
1147             #
1148             # Maintains a list of active sessions in the ~/.rt_sessions file.
1149             {
1150 0         0 package Session;
1151 0         0 my ($s, $u);
1152              
1153             # Initialises the session cache.
1154             sub new {
1155 1     1   8 my ($class, $file) = @_;
1156 1   33     18 my $self = {
1157             file => $file || "$HOME/.rt_sessions",
1158             sids => { }
1159             };
1160            
1161             # The current session is identified by the currently configured
1162             # server and user.
1163 1         11 ($s, $u) = @config{"server", "user"};
1164              
1165 1         9 bless $self, $class;
1166 1         13 $self->load();
1167              
1168 1         4 return $self;
1169             }
1170              
1171             # Returns the current session cookie.
1172             sub cookie {
1173 0     0   0 my ($self) = @_;
1174 0         0 my $cookie = $self->{sids}{$s}{$u};
1175 0 0       0 return defined $cookie ? "RT_SID_$cookie" : undef;
1176             }
1177              
1178             # Deletes the current session cookie.
1179             sub delete {
1180 0     0   0 my ($self) = @_;
1181 0         0 delete $self->{sids}{$s}{$u};
1182             }
1183              
1184             # Adds a Cookie header to an outgoing HTTP request.
1185             sub add_cookie_header {
1186 0     0   0 my ($self, $request) = @_;
1187 0         0 my $cookie = $self->cookie();
1188              
1189 0 0       0 $request->header(Cookie => $cookie) if defined $cookie;
1190             }
1191              
1192             # Extracts the Set-Cookie header from an HTTP response, and updates
1193             # session information accordingly.
1194             sub update {
1195 0     0   0 my ($self, $response) = @_;
1196 0         0 my $cookie = $response->header("Set-Cookie");
1197              
1198 0 0 0     0 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
1199 0         0 $self->{sids}{$s}{$u} = $1;
1200             }
1201             }
1202              
1203             # Loads the session cache from the specified file.
1204             sub load {
1205 1     1   7 my ($self, $file) = @_;
1206 1   33     34 $file ||= $self->{file};
1207              
1208 1 50       204 open( my $handle, '<', $file ) or return 0;
1209              
1210 0         0 $self->{file} = $file;
1211 0         0 my $sids = $self->{sids} = {};
1212 0         0 while (<$handle>) {
1213 0         0 chomp;
1214 0 0 0     0 next if /^$/ || /^#/;
1215 0 0       0 next unless m#^(https?://[^ ]+) (.+) ([^;,\s]+=[0-9A-Fa-f]+)$#;
1216 0         0 my ($server, $user, $cookie) = ($1, $2, $3);
1217 0         0 $sids->{$server}{$user} = $cookie;
1218             }
1219 0         0 return 1;
1220             }
1221              
1222             # Writes the current session cache to the specified file.
1223             sub save {
1224 0     0   0 my ($self, $file) = shift;
1225 0   0     0 $file ||= $self->{file};
1226              
1227 0 0       0 open( my $handle, '>', "$file" ) or return 0;
1228              
1229 0         0 my $sids = $self->{sids};
1230 0         0 foreach my $server (keys %$sids) {
1231 0         0 foreach my $user (keys %{ $sids->{$server} }) {
  0         0  
1232 0         0 my $sid = $sids->{$server}{$user};
1233 0 0       0 if (defined $sid) {
1234 0         0 print $handle "$server $user $sid\n";
1235             }
1236             }
1237             }
1238 0         0 close($handle);
1239 0         0 chmod 0600, $file;
1240 0         0 return 1;
1241             }
1242              
1243             sub DESTROY {
1244 0     0   0 my $self = shift;
1245 0         0 $self->save;
1246             }
1247             }
1248              
1249             # Form handling.
1250             # --------------
1251             #
1252             # Forms are RFC822-style sets of (field, value) specifications with some
1253             # initial comments and interspersed blank lines allowed for convenience.
1254             # Sets of forms are separated by --\n (in a cheap parody of MIME).
1255             #
1256             # Each form is parsed into an array with four elements: commented text
1257             # at the start of the form, an array with the order of keys, a hash with
1258             # key/value pairs, and optional error text if the form syntax was wrong.
1259              
1260             # Returns a reference to an array of parsed forms.
1261 0         0 sub Form::parse {
1262 1     1   3 my $state = 0;
1263 1         4 my @forms = ();
1264 1 50       339 my @lines = split /\n/, $_[0] if $_[0];
1265 1         13 my ($c, $o, $k, $e) = ("", [], {}, "");
1266              
1267             LINE:
1268 1         13 while (@lines) {
1269 156         247 my $line = shift @lines;
1270              
1271 156 100       340 next LINE if $line eq '';
1272              
1273 119 100       247 if ($line eq '--') {
    100          
1274             # We reached the end of one form. We'll ignore it if it was
1275             # empty, and store it otherwise, errors and all.
1276 29 50 33     158 if ($e || $c || @$o) {
      33        
1277 29         138 push @forms, [ $c, $o, $k, $e ];
1278 29         54 $c = ""; $o = []; $k = {}; $e = "";
  29         49  
  29         82  
  29         44  
1279             }
1280 29         67 $state = 0;
1281             }
1282             elsif ($state != -1) {
1283 79 50 66     1215 if ($state == 0 && $line =~ /^#/) {
    100 66        
    50          
1284             # Read an optional block of comments (only) at the start
1285             # of the form.
1286 0         0 $state = 1;
1287 0         0 $c = $line;
1288 0   0     0 while (@lines && $lines[0] =~ /^#/) {
1289 0         0 $c .= "\n".shift @lines;
1290             }
1291 0         0 $c .= "\n";
1292             }
1293             elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1294             # Read a field: value specification.
1295 78         197 my $f = $1;
1296 78 100 66     375 my @v = (defined $2 && length $2 ? $2 : ());
1297              
1298             # Read continuation lines, if any.
1299 78   100     370 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
      66        
1300 692         4445 push @v, shift @lines;
1301             }
1302 78   66     332 pop @v while (@v && $v[-1] eq '');
1303              
1304             # Strip longest common leading indent from text.
1305 78         134 my $ws = "";
1306 78         245 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
  632         1429  
1307 483 100 100     1590 $ws = $ls if (!$ws || length($ls) < length($ws));
1308             }
1309 78         10411 s/^$ws// foreach @v;
1310              
1311 78 100       1680 push(@$o, $f) unless exists $k->{$f};
1312 78         535 vpush($k, $f, join("\n", @v));
1313              
1314 78         320 $state = 1;
1315             }
1316             elsif ($line !~ /^#/) {
1317             # We've found a syntax error, so we'll reconstruct the
1318             # form parsed thus far, and add an error marker. (>>)
1319 1         2 $state = -1;
1320 1         16 $e = Form::compose([[ "", $o, $k, "" ]]);
1321 1 50       9 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1322             }
1323             }
1324             else {
1325             # We saw a syntax error earlier, so we'll accumulate the
1326             # contents of this form until the end.
1327 11         25 $e .= "$line\n";
1328             }
1329             }
1330 1 0 33     7 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
      33        
1331              
1332 1         7 foreach my $l (keys %$k) {
1333 2 100       18 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1334             }
1335              
1336 1         12 return \@forms;
1337             }
1338              
1339             # Returns text representing a set of forms.
1340             sub Form::compose {
1341 1     1   47 my ($forms) = @_;
1342 1         3 my @text;
1343              
1344 1         4 foreach my $form (@$forms) {
1345 1         5 my ($c, $o, $k, $e) = @$form;
1346 1         4 my $text = "";
1347              
1348 1 50       5 if ($c) {
1349 0         0 $c =~ s/\n*$/\n/;
1350 0         0 $text = "$c\n";
1351             }
1352 1 50       9 if ($e) {
    50          
1353 0         0 $text .= $e;
1354             }
1355             elsif ($o) {
1356 1         3 my @lines;
1357              
1358 1         5 foreach my $key (@$o) {
1359 2         5 my ($line, $sp);
1360 2         10 my $v = $k->{$key};
1361 2 100       9 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1362              
1363 2         13 $sp = " "x(length("$key: "));
1364 2 50       8 $sp = " "x4 if length($sp) > 16;
1365              
1366 2         5 foreach $v (@values) {
1367 3 100 66     33 if ($v =~ /\n/) {
    50          
1368 1         17 $v =~ s/^/$sp/gm;
1369 1         23 $v =~ s/^$sp//;
1370              
1371 1 50 33     26 if ($line) {
    50          
1372 0         0 push @lines, "$line\n\n";
1373 0         0 $line = "";
1374             }
1375             elsif (@lines && $lines[-1] !~ /\n\n$/) {
1376 1         4 $lines[-1] .= "\n";
1377             }
1378 1         5 push @lines, "$key: $v\n\n";
1379             }
1380             elsif ($line &&
1381             length($line)+length($v)-rindex($line, "\n") >= 70)
1382             {
1383 0         0 $line .= ",\n$sp$v";
1384             }
1385             else {
1386 2 100       9 $line = $line ? "$line,$v" : "$key: $v";
1387             }
1388             }
1389              
1390 2 50       6 $line = "$key:" unless @values;
1391 2 100       7 if ($line) {
1392 1 50       13 if ($line =~ /\n/) {
1393 0 0 0     0 if (@lines && $lines[-1] !~ /\n\n$/) {
1394 0         0 $lines[-1] .= "\n";
1395             }
1396 0         0 $line .= "\n";
1397             }
1398 1         5 push @lines, "$line\n";
1399             }
1400             }
1401              
1402 1         9 $text .= join "", @lines;
1403             }
1404             else {
1405 0         0 chomp $text;
1406             }
1407 1         3 push @text, $text;
1408             }
1409              
1410 1         8 return join "\n--\n\n", @text;
1411             }
1412              
1413             # Configuration.
1414             # --------------
1415              
1416             # Returns configuration information from the environment.
1417             sub config_from_env {
1418 1     1   26 my %env;
1419              
1420 1         10 foreach my $k (qw(EXTERNALAUTH AUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
1421              
1422 8 50       36 if (exists $ENV{"RT$k"}) {
1423 0         0 $env{lc $k} = $ENV{"RT$k"};
1424             }
1425             }
1426              
1427 1         98 return %env;
1428             }
1429              
1430             # Finds a suitable configuration file and returns information from it.
1431             sub config_from_file {
1432 1     1   8 my ($rc) = @_;
1433              
1434 1 50       8 if ($rc =~ m#^/#) {
1435             # We'll use an absolute path if we were given one.
1436 0         0 return parse_config_file($rc);
1437             }
1438             else {
1439             # Otherwise we'll use the first file we can find in the current
1440             # directory, or in one of its (increasingly distant) ancestors.
1441              
1442 1         53910 my @dirs = split /\//, cwd;
1443 1         118 while (@dirs) {
1444 5         38 my $file = join('/', @dirs, $rc);
1445 5 50       444 if (-r $file) {
1446 0         0 return parse_config_file($file);
1447             }
1448              
1449             # Remove the last directory component each time.
1450 5         23 pop @dirs;
1451             }
1452              
1453             # Still nothing? We'll fall back to some likely defaults.
1454 1         18 for ("$HOME/$rc", "/usr/local/etc/rt.conf", "/etc/rt.conf") {
1455 3 50       249 return parse_config_file($_) if (-r $_);
1456             }
1457             }
1458              
1459 1         29 return ();
1460             }
1461              
1462             # Makes a hash of the specified configuration file.
1463             sub parse_config_file {
1464 0     0   0 my %cfg;
1465 0         0 my ($file) = @_;
1466 0         0 local $_; # $_ may be aliased to a constant, from line 1163
1467              
1468 0 0       0 open( my $handle, '<', $file ) or return;
1469              
1470 0         0 while (<$handle>) {
1471 0         0 chomp;
1472 0 0 0     0 next if (/^#/ || /^\s*$/);
1473              
1474 0 0       0 if (/^(externalauth|auth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
1475 0         0 $cfg{$1} = $2;
1476             }
1477             else {
1478 0         0 die "rt: $file:$.: unknown configuration directive.\n";
1479             }
1480             }
1481              
1482 0         0 return %cfg;
1483             }
1484              
1485             # Helper functions.
1486             # -----------------
1487              
1488             sub whine {
1489 0     0   0 my $sub = (caller(1))[3];
1490 0         0 $sub =~ s/^main:://;
1491 0         0 warn "rt: $sub: @_\n";
1492 0         0 return 0;
1493             }
1494              
1495             sub read_passwd {
1496 0     0   0 eval 'require Term::ReadKey';
1497 0 0       0 if ($@) {
1498 0         0 die "No password specified (and Term::ReadKey not installed).\n";
1499             }
1500              
1501 0         0 print "Password: ";
1502 0         0 Term::ReadKey::ReadMode('noecho');
1503 0         0 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1504 0         0 Term::ReadKey::ReadMode('restore');
1505 0         0 print "\n";
1506              
1507 0         0 return $passwd;
1508             }
1509              
1510             sub vi_form_while {
1511 0     0   0 my $text = shift;
1512 0         0 my $cb = shift;
1513              
1514 0         0 my $error = 0;
1515 0         0 my ($c, $o, $k, $e);
1516 0         0 do {
1517 0         0 my $ntext = vi($text);
1518 0 0 0     0 return undef if ($error && $ntext eq $text);
1519              
1520 0         0 $text = $ntext;
1521              
1522 0         0 my $form = Form::parse($text);
1523 0         0 $error = 0;
1524 0         0 ($c, $o, $k, $e) = @{ $form->[0] };
  0         0  
1525 0 0       0 if ( $e ) {
    0          
1526 0         0 $error = 1;
1527 0         0 $c = "# Syntax error.";
1528 0         0 goto NEXT;
1529             }
1530             elsif (!@$o) {
1531 0         0 return 0;
1532             }
1533              
1534 0         0 my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] );
1535 0 0       0 unless ( $status ) {
1536 0         0 $error = 1;
1537 0         0 $c = "# $msg";
1538             }
1539              
1540             NEXT:
1541 0         0 $text = Form::compose([[$c, $o, $k, $e]]);
1542             } while ($error);
1543              
1544 0         0 return $text;
1545             }
1546              
1547             sub vi {
1548 0     0   0 my ($text) = @_;
1549 0   0     0 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1550              
1551 0         0 local $/ = undef;
1552              
1553 0         0 my $handle = File::Temp->new;
1554 0         0 print $handle $text;
1555 0         0 close($handle);
1556              
1557 0 0       0 system($editor, $handle->filename) && die "Couldn't run $editor.\n";
1558              
1559 0 0       0 open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
1560 0         0 $text = <$handle>;
1561 0         0 close($handle);
1562              
1563 0         0 return $text;
1564             }
1565              
1566             # Add a value to a (possibly multi-valued) hash key.
1567             sub vpush {
1568 78     78   180 my ($hash, $key, $val) = @_;
1569 78 50       214 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1570              
1571 78 100       136 if (exists $hash->{$key}) {
1572 18 100       57 unless (ref $hash->{$key} eq 'ARRAY') {
1573 14 50       48 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1574 14         42 $hash->{$key} = \@v;
1575             }
1576 18         30 push @{ $hash->{$key} }, @val;
  18         61  
1577             }
1578             else {
1579 60         188 $hash->{$key} = $val;
1580             }
1581             }
1582              
1583             # WARNING: this code is duplicated in lib/RT/Interface/REST.pm
1584             # If you change one, change both functions at once
1585             # "Normalise" a hash key that's known to be multi-valued.
1586             sub vsplit {
1587 1     1   5 my ($val, $strip) = @_;
1588 1         2 my @words;
1589 1 50       6 my @values = map {split /\n/} (ref $val eq 'ARRAY' ? @$val : $val);
  2         29  
1590              
1591 1         7 foreach my $line (@values) {
1592 2         12 while ($line =~ /\S/) {
1593 2 50       16 $line =~ s/^
1594             \s* # Trim leading whitespace
1595             (?:
1596             (") # Quoted string
1597             ((?>[^\\"]*(?:\\.[^\\"]*)*))"
1598             |
1599             (') # Single-quoted string
1600             ((?>[^\\']*(?:\\.[^\\']*)*))'
1601             |
1602             q\{(.*?)\} # A perl-ish q{} string; this does
1603             # no paren balancing, however, and
1604             # only exists for back-compat
1605             |
1606             (.*?) # Anything else, until the next comma
1607             )
1608             \s* # Trim trailing whitespace
1609             (?:
1610             \Z # Finish at end-of-line
1611             |
1612             , # Or a comma
1613             )
1614             //xs or last; # There should be no way this match
1615             # fails, but add a failsafe to
1616             # prevent infinite-looping if it
1617             # somehow does.
1618 2 50 33     38 my ($quote, $quoted) = ($1 ? ($1, $2) : $3 ? ($3, $4) : ('', $5 || $6));
    50          
1619             # Only unquote the quote character, or the backslash -- and
1620             # only if we were originally quoted..
1621 2 50       28 if ($5) {
1622 0         0 $quoted =~ s/([\\'])/\\$1/g;
1623 0         0 $quote = "'";
1624             }
1625 2 50       17 if ($strip) {
1626 0 0       0 $quoted =~ s/\\([\\$quote])/$1/g if $quote;
1627 0         0 push @words, $quoted;
1628             } else {
1629 2         9 push @words, "$quote$quoted$quote";
1630             }
1631             }
1632             }
1633 1         6 return \@words;
1634             }
1635              
1636             # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1637             # change both functions at once
1638             sub expand_list {
1639 0     0     my ($list) = @_;
1640              
1641 0           my @elts;
1642 0           foreach (split /\s*,\s*/, $list) {
1643 0 0         push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1644             }
1645              
1646             return map $_->[0], # schwartzian transform
1647             sort {
1648 0 0 0       defined $a->[1] && defined $b->[1]?
  0 0 0        
    0          
    0          
1649             # both numbers
1650             $a->[1] <=> $b->[1]
1651             :!defined $a->[1] && !defined $b->[1]?
1652             # both letters
1653             $a->[2] cmp $b->[2]
1654             # mix, number must be first
1655             :defined $a->[1]? -1: 1
1656             }
1657             map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1658             @elts;
1659             }
1660              
1661             sub get_type_argument {
1662 0     0     my $type;
1663              
1664 0 0         if (@ARGV) {
1665 0           $type = shift @ARGV;
1666 0 0         unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1667             # We want whine to mention our caller, not us.
1668 0           @_ = ("Invalid type '$type' specified.");
1669 0           goto &whine;
1670             }
1671             }
1672             else {
1673 0           @_ = ("No type argument specified with -t.");
1674 0           goto &whine;
1675             }
1676              
1677 0           $type =~ s/s$//; # "Plural". Ugh.
1678 0           return $type;
1679             }
1680              
1681             sub get_var_argument {
1682 0     0     my ($data) = @_;
1683              
1684 0 0         if (@ARGV) {
1685 0           my $kv = shift @ARGV;
1686 0 0         if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1687 0           push @{ $data->{$k} }, $v;
  0            
1688             }
1689             else {
1690 0           @_ = ("Invalid variable specification: '$kv'.");
1691 0           goto &whine;
1692             }
1693             }
1694             else {
1695 0           @_ = ("No variable argument specified with -S.");
1696 0           goto &whine;
1697             }
1698             }
1699              
1700             sub is_object_spec {
1701 0     0     my ($spec, $type) = @_;
1702              
1703 0 0         $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1704 0 0         return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1705 0           return 0;
1706             }
1707              
1708             sub suggest_help {
1709 0     0     my ($action, $type, $rv) = @_;
1710              
1711 0 0         print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1712 0 0         print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1713 0           return $rv;
1714             }
1715              
1716             sub str2time {
1717             # simplified procedure for parsing date, avoid loading Date::Parse
1718 0     0     my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5,
1719             Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
1720 0           $_ = shift;
1721 0           my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
1722 0 0         if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
    0          
1723 0           ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
1724 0 0         $mon = $month{$monstr} if exists $month{$monstr};
1725             } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
1726 0           ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
1727             }
1728 0 0 0       if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
      0        
      0        
      0        
1729 0           return timelocal($sec,$min,$hr,$day,$mon,$yr);
1730             } else {
1731 0           print "Unknown date format in parsedate: $_\n";
1732 0           return undef;
1733             }
1734             }
1735              
1736             sub date_diff {
1737 0     0     my ($old, $new) = @_;
1738 0 0         $new = time() if ! $new;
1739 0 0         $old = str2time($old) if $old !~ /^\d+$/;
1740 0 0         $new = str2time($new) if $new !~ /^\d+$/;
1741 0 0 0       return "???" if ! $old or ! $new;
1742              
1743 0           my %seconds = (min => 60,
1744             hr => 60*60,
1745             day => 60*60*24,
1746             wk => 60*60*24*7,
1747             mth => 60*60*24*30,
1748             yr => 60*60*24*365);
1749              
1750 0           my $diff = $new - $old;
1751 0           my $what = 'sec';
1752 0           my $howmuch = $diff;
1753 0           for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
  0            
1754 0 0         last if $diff < $seconds{$_};
1755 0           $what = $_;
1756 0           $howmuch = int($diff/$seconds{$_});
1757             }
1758 0           return "$howmuch $what";
1759             }
1760              
1761             sub prettyshow {
1762 0     0     my $forms = shift;
1763 0           my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
  0            
1764 0           my $k = $form->[2];
1765             # dates are in local time zone
1766 0 0         if ( $k ) {
1767 0           print "Date: $k->{Created}\n";
1768 0           print "From: $k->{Requestors}\n";
1769 0 0         print "Cc: $k->{Cc}\n" if $k->{Cc};
1770 0 0         print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
1771 0           print "X-Queue: $k->{Queue}\n";
1772 0           print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
1773             }
1774             # dates in these attributes are in GMT and will be converted
1775 0           foreach my $form (@$forms) {
1776 0           my ($c, $o, $k, $e) = @$form;
1777 0 0 0       next if ! $k->{id} or exists $k->{Queue};
1778 0 0         if ( exists $k->{Created} ) {
1779 0           my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
1780 0           $m--;
1781 0           my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
1782 0 0         if ( exists $k->{Description} ) {
1783 0           print "===> $k->{Description} on $created\n";
1784             }
1785             }
1786             print "$k->{Content}\n" if exists $k->{Content} and
1787             $k->{Content} !~ /to have no content$/ and
1788 0 0 0       ($k->{Type}||'') ne 'EmailRecord';
      0        
      0        
1789             print "$k->{Attachments}\n" if exists $k->{Attachments} and
1790 0 0 0       $k->{Attachments};
1791             }
1792             }
1793              
1794             sub prettylist {
1795 0     0     my $forms = shift;
1796 0           my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n";
1797 0           $heading .= '-' x 80 . "\n";
1798 0           my (@open, @me);
1799 0           foreach my $form (@$forms) {
1800 0           my ($c, $o, $k, $e) = @$form;
1801 0 0         next if ! $k->{id};
1802 0 0         print $heading if $heading;
1803 0           $heading = '';
1804 0           my $id = $k->{id};
1805 0           $id =~ s!^ticket/!!;
1806 0 0         my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
1807 0           $owner = substr($owner, 0, 5);
1808 0           my $queue = substr($k->{Queue}, 0, 5);
1809 0           my $subject = substr($k->{Subject}, 0, 30);
1810 0           my $age = date_diff($k->{Created});
1811 0 0         my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
1812 0           my $status = substr($k->{Status}, 0, 6);
1813 0           my $requestor = substr($k->{Requestors}, 0, 9);
1814 0           my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
1815             $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
1816 0 0         if ( $k->{Owner} eq 'Nobody' ) {
    0          
1817 0           push @open, $line;
1818             } elsif ($k->{Owner} eq $config{user} ) {
1819 0           push @me, $line;
1820             } else {
1821 0           print $line;
1822             }
1823             }
1824 0 0         print "No matches found\n" if $heading;
1825 0 0         printf "========== my %2d open tickets ==========\n", scalar @me if @me;
1826 0 0         print @me if @me;
1827 0 0         printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
1828 0 0         print @open if @open;
1829             }
1830              
1831             __DATA__