37e74f1c19eb3d1aea40d41e21162ce31e2d1941
[freeside.git] / rt / bin / rt-crontool.in
1 #!@PERL@
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
7 #                                          <sales@bestpractical.com>
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 use strict;
50 use warnings;
51 use Carp;
52
53 # fix lib paths, some may be relative
54 BEGIN { # BEGIN RT CMD BOILERPLATE
55     require File::Spec;
56     require Cwd;
57     my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
58     my $bin_path;
59
60     for my $lib (@libs) {
61         unless ( File::Spec->file_name_is_absolute($lib) ) {
62             $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
63             $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
64         }
65         unshift @INC, $lib;
66     }
67
68 }
69
70 use RT;
71
72 use Getopt::Long;
73
74 use RT::Interface::CLI qw(GetCurrentUser loc);
75
76 my ( $search, $condition, $actions, $search_arg, $condition_arg, $actions_arg,
77      $template, $template_id, $transaction, $transaction_type, $help, $log, $verbose );
78 GetOptions(
79     "search=s"           => \$search,
80     "search-arg=s"       => \$search_arg,
81     "condition=s"        => \$condition,
82     "condition-arg=s"    => \$condition_arg,
83     "action-arg=s@"      => \$actions_arg,
84     "action=s@"          => \$actions,
85     "template=s"         => \$template,
86     "template-id=s"      => \$template_id,
87     "transaction=s"      => \$transaction,
88     "transaction-type=s" => \$transaction_type,
89     "log=s"              => \$log,
90     "verbose|v"          => \$verbose,
91     "help"               => \$help,
92 );
93
94 # Load the config file
95 RT::LoadConfig();
96
97 # adjust logging to the screen according to options
98 RT->Config->Set( LogToSTDERR => $log ) if $log;
99
100 #Connect to the database and get RT::SystemUser and RT::Nobody loaded
101 RT::Init();
102
103 require RT::Tickets;
104 require RT::Template;
105
106 #Get the current user all loaded
107 my $CurrentUser = GetCurrentUser();
108
109 # show help even if there is no current user
110 help() if $help;
111
112 unless ( $CurrentUser->Id ) {
113     print loc("No RT user found. Please consult your RT administrator.") . "\n";
114     exit(1);
115 }
116
117 help() unless $search && $actions;
118
119 $transaction = lc( $transaction||'' );
120 if ( $transaction && $transaction !~ /^(first|all|last)$/i ) {
121     print STDERR loc("--transaction argument could be only 'first', 'last' or 'all'");
122     exit 1;
123 }
124
125 if ( $template && $template_id ) {
126     print STDERR loc("--template-id is deprecated argument and can not be used with --template");
127     exit 1;
128 }
129 elsif ( $template_id ) {
130 # don't warn
131     $template = $template_id;
132 }
133
134 # We _must_ have a search object
135 load_module($search);
136 for my $action (@$actions) {
137     load_module($action);
138 }
139 load_module($condition) if ($condition);
140
141 my $void_scrip = RT::Scrip->new( $CurrentUser );
142 my $void_scrip_action = RT::ScripAction->new( $CurrentUser );
143
144 #At the appointed time:
145
146 #find a bunch of tickets
147 my $tickets = RT::Tickets->new($CurrentUser);
148 $search  = $search->new(
149     TicketsObj  => $tickets,
150     Argument    => $search_arg,
151     CurrentUser => $CurrentUser
152 );
153 $search->Prepare();
154
155 #for each ticket we've found
156 while ( my $ticket = $tickets->Next() ) {
157     print $ticket->Id() . ":\n" if ($verbose);
158
159     my $template_obj = get_template( $ticket );
160
161     if ( $transaction ) {
162         my $txns = get_transactions($ticket);
163         my $found = 0;
164         while ( my $txn = $txns->Next ) {
165             print "\t".loc("Using transaction #[_1]...", $txn->id)."\n"
166                 if $verbose;
167             process($ticket, $txn, $template_obj);
168             $found = 1;
169         }
170         print "\t".loc("Couldn't find suitable transaction, skipping")."\n"
171             if $verbose && !$found;
172     } else {
173         print "\t".loc("Processing without transaction, some conditions and actions may fail. Consider using --transaction argument")."\n"
174             if $verbose;
175
176         process($ticket, undef, $template_obj);
177     }
178 }
179
180 sub process {
181     my $ticket = shift;
182     my $transaction = shift;
183     my $template_obj = shift;
184
185     # perform some more advanced check
186     if ($condition) {
187         my $condition_obj = $condition->new(
188             TransactionObj => $transaction,
189             TicketObj      => $ticket,
190             ScripObj       => $void_scrip,
191             TemplateObj    => $template_obj,
192             Argument       => $condition_arg,
193             CurrentUser    => $CurrentUser,
194         );
195
196         # if the condition doesn't apply, get out of here
197
198         return unless $condition_obj->IsApplicable;
199         print "\t".loc("Condition matches...")."\n" if $verbose;
200     }
201
202     my $i = 0;
203     for my $action (@$actions) {
204
205         # Given the current index of the actions array, see
206         # if there is still a corresponding element in the
207         # actions_arg array. If so, then use that argument,
208         # else use undef.
209         my $action_argument = ($i <= $#$actions_arg) ? $actions_arg->[$i] : undef;
210         $i++;
211
212         # prepare our action
213         my $action_obj = $action->new(
214             TicketObj      => $ticket,
215             TransactionObj => $transaction,
216             TemplateObj    => $template_obj,
217             Argument       => $action_argument,
218             ScripObj       => $void_scrip,
219             ScripActionObj => $void_scrip_action,
220             CurrentUser    => $CurrentUser,
221         );
222
223         # if our preparation failed, move onto the next action
224         next unless $action_obj->Prepare;
225         print "\t".loc("Action prepared...")."\n" if $verbose;
226
227         # commit our action.
228         next unless $action_obj->Commit;
229         print "\t".loc("Action committed.")."\n" if $verbose;
230     }
231 }
232
233 # =head2 get_transactions
234
235 # Takes ticket and returns L<RT::Transactions> object with transactions
236 # of the ticket according to command line arguments C<--transaction>
237 # and <--transaction-type>.
238
239 # =cut
240
241 sub get_transactions {
242     my $ticket = shift;
243     my $txns = $ticket->Transactions;
244     my $order = $transaction eq 'last'? 'DESC': 'ASC';
245     $txns->OrderByCols(
246         { FIELD => 'Created', ORDER => $order },
247         { FIELD => 'id', ORDER => $order },
248     );
249     if ( $transaction_type ) {
250         $transaction_type =~ s/^\s+//;
251         $transaction_type =~ s/\s+$//;
252         foreach my $type ( split /\s*,\s*/, $transaction_type ) {
253             $txns->Limit( FIELD => 'Type', VALUE => $type, ENTRYAGGREGATOR => 'OR' );
254         }
255     }
256     $txns->RowsPerPage(1) unless $transaction eq 'all';
257     return $txns;
258 }
259
260 # =head2 get_template
261
262 # Takes a ticket and returns a template according to command line options.
263
264 # =cut
265
266 sub get_template {
267     my $ticket = shift;
268     return undef unless $template;
269
270     unless ( $template =~ /\D/ ) {
271         # by id
272         my $template_obj = RT::Template->new( RT->SystemUser );
273         $template_obj->Load( $template );
274         die "Failed to load template '$template'"
275             unless $template_obj->id;
276         return $template_obj;
277     }
278
279     my $queue = $ticket->Queue;
280
281     my $res = RT::Template->new( RT->SystemUser );
282     $res->LoadQueueTemplate( Queue => $queue, Name => $template );
283     unless ( $res->id ) {
284         $res->LoadGlobalTemplate( $template );
285         die "Failed to load template '$template', either for queue #$queue or global"
286             unless $res->id;
287     }
288     return $res;
289 }
290
291
292 # =head2 load_module
293
294 # Loads a perl module, dying nicely if it can't find it.
295
296 # =cut
297
298 sub load_module {
299     my $modname = shift;
300     unless ($modname->require) {
301         my $error = $@;
302         die loc( "Failed to load module [_1]. ([_2])", $modname, $error );
303     }
304
305 }
306
307
308 sub help {
309
310     print loc( "[_1] is a tool to act on tickets from an external scheduling tool, such as cron.", $0 )
311       . "\n";
312     print loc("It takes several arguments:") . "\n\n";
313
314     print "        "
315       . loc( "[_1] - Specify the search module you want to use", "--search" )
316       . "\n";
317     print "        "
318       . loc( "[_1] - An argument to pass to [_2]", "--search-arg", "--search" )
319       . "\n";
320
321     print "        "
322       . loc( "[_1] - Specify the condition module you want to use", "--condition" )
323       . "\n";
324     print "        "
325       . loc( "[_1] - An argument to pass to [_2]", "--condition-arg", "--condition" )
326       . "\n";
327     print "        "
328       . loc( "[_1] - Specify the action module you want to use. This option may be repeated to apply multiple actions to found tickets.", "--action" )
329       . "\n";
330     print "        "
331       . loc( "[_1] - An argument to pass to [_2]. This option may be repeated to pass corresponding arguments to multiple calls of [_2].", "--action-arg", "--action" )
332       . "\n";
333     print "        "
334       . loc( "[_1] - Specify name or id of template(s) you want to use", "--template" )
335       . "\n";
336     print "        "
337       . loc( "[_1] - Specify if you want to use either 'first', 'last' or 'all' transactions", "--transaction" )
338       . "\n";
339     print "        "
340       . loc( "[_1] - Specify the comma separated list of transactions' types you want to use", "--transaction-type" )
341       . "\n";
342     print "        "
343       . loc( "[_1] - Adjust LogToSTDERR config option", "--log" ) . "\n";
344     print "        "
345       . loc( "[_1] - Output status updates to STDOUT", "--verbose" ) . "\n";
346     print "\n";
347     print "\n";
348     print loc("Security:")."\n";
349     print loc("This tool allows the user to run arbitrary perl modules from within RT.")." ". 
350         loc("If this tool were setgid, a hostile local user could use this tool to gain administrative access to RT.")." ".
351         loc("It is incredibly important that nonprivileged users not be allowed to run this tool."). " " . 
352         loc("It is suggested that you create a non-privileged unix user with the correct group membership and RT access to run this tool.")."\n";
353     print "\n";
354     print loc("Example:");
355     print "\n";
356     print " "
357       . loc( "The following command will find all active tickets in the queue 'general' and set their priority to 99 if they are overdue:"
358       )
359       . "\n\n";
360
361     print " bin/rt-crontool \\\n";
362     print "  --search RT::Search::ActiveTicketsInQueue  --search-arg general \\\n";
363     print "  --condition RT::Condition::Overdue \\\n";
364     print "  --action RT::Action::SetPriority --action-arg 99 \\\n";
365     print "  --verbose\n";
366
367     print "\n";
368     print loc("Escalate tickets"). "\n";
369     print " bin/rt-crontool \\\n";
370     print "  --search RT::Search::ActiveTicketsInQueue  --search-arg general \\\n";
371     print"  --action RT::Action::EscalatePriority\n";
372
373     exit(0);
374 }
375
376 __END__
377
378 =head1 NAME
379
380 rt-crontool - a tool to act on tickets from an external scheduling tool
381
382 =head1 SYNOPSIS
383
384     # find all active tickets in the queue 'general' and set their priority to 99 if they are overdue:
385     rt-crontool \
386       --search RT::Search::ActiveTicketsInQueue  --search-arg general \
387       --condition RT::Condition::Overdue \
388       --action RT::Action::SetPriority --action-arg 99 \
389       --verbose
390
391     # Escalate tickets
392       rt-crontool \
393         --search RT::Search::ActiveTicketsInQueue  --search-arg general \
394         --action RT::Action::EscalatePriority
395
396 =head1 DESCRIPTION
397
398 This script is a tool to act on tickets from an external scheduling tool, such
399 as cron.
400
401 Security:
402
403 This tool allows the user to run arbitrary perl modules from within RT. If
404 this tool were setgid, a hostile local user could use this tool to gain
405 administrative access to RT. It is incredibly important that nonprivileged
406 users not be allowed to run this tool. It is suggested that you create a
407 non-privileged unix user with the correct group membership and RT access to
408 run this tool.
409
410
411 =head1 OPTIONS
412
413 =over
414
415 =item search 
416
417 Specify the search module you want to use
418
419 =item search-arg 
420
421 An argument to pass to --search
422
423 =item condition
424
425 Specify the condition module you want to use
426
427 =item condition-arg
428
429 An argument to pass to --condition
430
431 =item action 
432
433 Specify the action module you want to use. This option may be repeated to apply multiple actions to found tickets.
434
435 =item action-arg
436
437 An argument to pass to --action. This option may be repeated to pass corresponding arguments to multiple calls of --action.
438
439 =item template
440
441 Specify name or id of template(s) you want to use
442
443 =item transaction
444
445 Specify if you want to use either 'first', 'last' or 'all' transactions
446
447
448 =item transaction-type
449
450 Specify the comma separated list of transactions' types you want to use
451
452 =item log
453
454 Adjust LogToSTDERR config option
455
456 =item verbose
457
458 Output status updates to STDOUT
459
460 =back
461