rt 4.2.13 ticket#13852
[freeside.git] / rt / lib / RT / Scrips.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::Scrips - a collection of RT Scrip objects
52
53 =head1 SYNOPSIS
54
55   use RT::Scrips;
56
57 =head1 DESCRIPTION
58
59
60 =head1 METHODS
61
62
63
64 =cut
65
66
67 package RT::Scrips;
68
69 use strict;
70 use warnings;
71
72 use base 'RT::SearchBuilder';
73
74 use RT::Scrip;
75 use RT::ObjectScrips;
76
77 sub Table { 'Scrips'}
78
79 sub _Init {
80     my $self = shift;
81
82     $self->{'with_disabled_column'} = 1;
83
84     return ( $self->SUPER::_Init(@_) );
85 }
86
87 =head2 LimitToQueue
88
89 Takes a queue id (numerical) as its only argument. Makes sure that 
90 Scopes it pulls out apply to this queue (or another that you've selected with
91 another call to this method
92
93 =cut
94
95 sub LimitToQueue  {
96     my $self = shift;
97     my $queue = shift;
98     return unless defined $queue;
99
100     my $alias = RT::ObjectScrips->new( $self->CurrentUser )
101         ->JoinTargetToThis( $self );
102     $self->Limit(
103         ALIAS => $alias,
104         FIELD => 'ObjectId',
105         VALUE => int $queue,
106     );
107 }
108
109
110 =head2 LimitToGlobal
111
112 Makes sure that 
113 Scopes it pulls out apply to all queues (or another that you've selected with
114 another call to this method or LimitToQueue
115
116 =cut
117
118
119 sub LimitToGlobal  {
120     my $self = shift;
121     return $self->LimitToQueue(0);
122 }
123
124 sub LimitToAdded {
125     my $self = shift;
126     return RT::ObjectScrips->new( $self->CurrentUser )
127         ->LimitTargetToAdded( $self => @_ );
128 }
129
130 sub LimitToNotAdded {
131     my $self = shift;
132     return RT::ObjectScrips->new( $self->CurrentUser )
133         ->LimitTargetToNotAdded( $self => @_ );
134 }
135
136 sub LimitByStage  {
137     my $self = shift;
138     my %args = @_%2? (Stage => @_) : @_;
139     return unless defined $args{'Stage'};
140
141     my $alias = RT::ObjectScrips->new( $self->CurrentUser )
142         ->JoinTargetToThis( $self, %args );
143     $self->Limit(
144         ALIAS => $alias,
145         FIELD => 'Stage',
146         VALUE => $args{'Stage'},
147     );
148 }
149
150 =head2 LimitByTemplate
151
152 Takes a L<RT::Template> object and limits scrips to those that
153 use the template.
154
155 =cut
156
157 sub LimitByTemplate {
158     my $self = shift;
159     my $template = shift;
160
161     $self->Limit( FIELD => 'Template', VALUE => $template->Name );
162
163     if ( $template->Queue ) {
164         # if template is local then we are interested in global and
165         # queue specific scrips
166         $self->LimitToQueue( $template->Queue );
167         $self->LimitToGlobal;
168     }
169     else { # template is global
170
171         # if every queue has a custom version then there
172         # is no scrip that uses the template
173         {
174             my $queues = RT::Queues->new( RT->SystemUser );
175             my $alias = $queues->Join(
176                 TYPE   => 'LEFT',
177                 ALIAS1 => 'main',
178                 FIELD1 => 'id',
179                 TABLE2 => 'Templates',
180                 FIELD2 => 'Queue',
181             );
182             $queues->Limit(
183                 LEFTJOIN   => $alias,
184                 ALIAS      => $alias,
185                 FIELD      => 'Name',
186                 VALUE      => $template->Name,
187             );
188             $queues->Limit(
189                 ALIAS      => $alias,
190                 FIELD      => 'id',
191                 OPERATOR   => 'IS',
192                 VALUE      => 'NULL',
193             );
194             return $self->Limit( FIELD => 'id', VALUE => 0 )
195                 unless $queues->Count;
196         }
197
198         # otherwise it's either a global scrip or application to
199         # a queue with custom version of the template.
200         my $os_alias = RT::ObjectScrips->new( $self->CurrentUser )
201             ->JoinTargetToThis( $self );
202         my $tmpl_alias = $self->Join(
203             TYPE   => 'LEFT',
204             ALIAS1 => $os_alias,
205             FIELD1 => 'ObjectId',
206             TABLE2 => 'Templates',
207             FIELD2 => 'Queue',
208         );
209         $self->Limit(
210             LEFTJOIN => $tmpl_alias, ALIAS => $tmpl_alias, FIELD => 'Name', VALUE => $template->Name,
211         );
212         $self->Limit(
213             LEFTJOIN => $tmpl_alias, ALIAS => $tmpl_alias, FIELD => 'Queue', OPERATOR => '!=', VALUE => 0,
214         );
215
216         $self->_OpenParen('UsedBy');
217         $self->Limit( SUBCLAUSE => 'UsedBy', ALIAS => $os_alias, FIELD => 'ObjectId', VALUE => 0 );
218         $self->Limit(
219             SUBCLAUSE => 'UsedBy',
220             ALIAS => $tmpl_alias,
221             FIELD => 'id',
222             OPERATOR => 'IS',
223             VALUE => 'NULL',
224         );
225         $self->_CloseParen('UsedBy');
226     }
227 }
228
229 sub ApplySortOrder {
230     my $self = shift;
231     my $order = shift || 'ASC';
232     $self->OrderByCols( {
233         ALIAS => RT::ObjectScrips->new( $self->CurrentUser )
234             ->JoinTargetToThis( $self => @_ )
235         ,
236         FIELD => 'SortOrder',
237         ORDER => $order,
238     } );
239 }
240
241 =head2 AddRecord
242
243 Overrides the collection to ensure that only scrips the user can see are
244 returned.
245
246 =cut
247
248 sub AddRecord {
249     my $self = shift;
250     my ($record) = @_;
251
252     return unless $record->CurrentUserHasRight('ShowScrips');
253     return $self->SUPER::AddRecord( $record );
254 }
255
256 =head2 Apply
257
258 Run through the relevant scrips.  Scrips will run in order based on 
259 description.  (Most common use case is to prepend a number to the description,
260 forcing the scrips to run in ascending alphanumerical order.)
261
262 =cut
263
264 sub Apply {
265     my $self = shift;
266
267     my %args = ( TicketObj      => undef,
268                  Ticket         => undef,
269                  Transaction    => undef,
270                  TransactionObj => undef,
271                  Stage          => undef,
272                  Type           => undef,
273                  @_ );
274
275     $self->Prepare(%args);
276     $self->Commit();
277
278 }
279
280 =head2 Commit
281
282 Commit all of this object's prepared scrips
283
284 =cut
285
286 sub Commit {
287     my $self = shift;
288
289     foreach my $scrip (@{$self->Prepared}) {
290         $RT::Logger->debug(
291             "Committing scrip #". $scrip->id
292             ." on txn #". $self->{'TransactionObj'}->id
293             ." of ticket #". $self->{'TicketObj'}->id
294         );
295
296         $scrip->Commit( TicketObj      => $self->{'TicketObj'},
297                         TransactionObj => $self->{'TransactionObj'} );
298     }
299
300 }
301
302
303 =head2 Prepare
304
305 Only prepare the scrips, returning an array of the scrips we're interested in
306 in order of preparation, not execution
307
308 =cut
309
310 sub Prepare { 
311     my $self = shift;
312     my %args = ( TicketObj      => undef,
313                  Ticket         => undef,
314                  Transaction    => undef,
315                  TransactionObj => undef,
316                  Stage          => undef,
317                  Type           => undef,
318                  @_ );
319
320     #We're really going to need a non-acled ticket for the scrips to work
321     $self->_SetupSourceObjects( TicketObj      => $args{'TicketObj'},
322                                 Ticket         => $args{'Ticket'},
323                                 TransactionObj => $args{'TransactionObj'},
324                                 Transaction    => $args{'Transaction'} );
325
326
327     $self->_FindScrips( Stage => $args{'Stage'}, Type => $args{'Type'} );
328
329
330     #Iterate through each script and check it's applicability.
331     while ( my $scrip = $self->Next() ) {
332
333           unless ( $scrip->IsApplicable(
334                                      TicketObj      => $self->{'TicketObj'},
335                                      TransactionObj => $self->{'TransactionObj'}
336                    ) ) {
337                    $RT::Logger->debug("Skipping Scrip #".$scrip->Id." because it isn't applicable");
338                    next;
339                }
340
341         #If it's applicable, prepare and commit it
342           unless ( $scrip->Prepare( TicketObj      => $self->{'TicketObj'},
343                                     TransactionObj => $self->{'TransactionObj'}
344                    ) ) {
345                    $RT::Logger->debug("Skipping Scrip #".$scrip->Id." because it didn't Prepare");
346                    next;
347                }
348         push @{$self->{'prepared_scrips'}}, $scrip;
349
350     }
351
352     return (@{$self->Prepared});
353
354 };
355
356 =head2 Prepared
357
358 Returns an arrayref of the scrips this object has prepared
359
360
361 =cut
362
363 sub Prepared {
364     my $self = shift;
365     return ($self->{'prepared_scrips'} || []);
366 }
367
368 =head2  _SetupSourceObjects { TicketObj , Ticket, Transaction, TransactionObj }
369
370 Setup a ticket and transaction for this Scrip collection to work with as it runs through the 
371 relevant scrips.  (Also to figure out which scrips apply)
372
373 Returns: nothing
374
375 =cut
376
377
378 sub _SetupSourceObjects {
379
380     my $self = shift;
381     my %args = ( 
382             TicketObj => undef,
383             Ticket => undef,
384             Transaction => undef,
385             TransactionObj => undef,
386             @_ );
387
388
389     if ( $args{'TicketObj'} ) {
390         # This loads a clean copy of the Ticket object to ensure that we
391         # don't accidentally escalate the privileges of the passed in
392         # ticket (this function can be invoked from the UI).
393         # We copy the TransactionBatch transactions so that Scrips
394         # running against the new Ticket will have access to them. We
395         # use RanTransactionBatch to guard against running
396         # TransactionBatch Scrips more than once.
397         $self->{'TicketObj'} = RT::Ticket->new( $self->CurrentUser );
398         $self->{'TicketObj'}->Load( $args{'TicketObj'}->Id );
399         if ( $args{'TicketObj'}->TransactionBatch ) {
400             # try to ensure that we won't infinite loop if something dies, triggering DESTROY while 
401             # we have the _TransactionBatch objects;
402             $self->{'TicketObj'}->RanTransactionBatch(1);
403             $self->{'TicketObj'}->{'_TransactionBatch'} = $args{'TicketObj'}->{'_TransactionBatch'};
404         }
405     }
406     else {
407         $self->{'TicketObj'} = RT::Ticket->new( $self->CurrentUser );
408         $self->{'TicketObj'}->Load( $args{'Ticket'} )
409           || $RT::Logger->err("$self couldn't load ticket $args{'Ticket'}");
410     }
411
412     if ( ( $self->{'TransactionObj'} = $args{'TransactionObj'} ) ) {
413         $self->{'TransactionObj'}->CurrentUser( $self->CurrentUser );
414     }
415     else {
416         $self->{'TransactionObj'} = RT::Transaction->new( $self->CurrentUser );
417         $self->{'TransactionObj'}->Load( $args{'Transaction'} )
418           || $RT::Logger->err( "$self couldn't load transaction $args{'Transaction'}");
419     }
420
421
422
423
424 =head2 _FindScrips
425
426 Find only the appropriate scrips for whatever we're doing now.  Order
427 them by the SortOrder field from the ObjectScrips table.
428
429 =cut
430
431 sub _FindScrips {
432     my $self = shift;
433     my %args = (
434                  Stage => undef,
435                  Type => undef,
436                  @_ );
437
438
439     $self->LimitToQueue( $self->{'TicketObj'}->QueueObj->Id );
440     $self->LimitToGlobal;
441     $self->LimitByStage( $args{'Stage'} );
442
443     my $ConditionsAlias = $self->Join(
444         ALIAS1 => 'main',
445         FIELD1 => 'ScripCondition',
446         TABLE2 => 'ScripConditions',
447         FIELD2 => 'id',
448     );
449
450     #We only want things where the scrip applies to this sort of transaction
451     # TransactionBatch stage can define list of transaction
452     foreach( split /\s*,\s*/, ($args{'Type'} || '') ) {
453         $self->Limit(
454             ALIAS           => $ConditionsAlias,
455             FIELD           => 'ApplicableTransTypes',
456             OPERATOR        => 'LIKE',
457             VALUE           => $_,
458             ENTRYAGGREGATOR => 'OR',
459         )
460     }
461
462     # Or where the scrip applies to any transaction
463     $self->Limit(
464         ALIAS           => $ConditionsAlias,
465         FIELD           => 'ApplicableTransTypes',
466         OPERATOR        => 'LIKE',
467         VALUE           => "Any",
468         ENTRYAGGREGATOR => 'OR',
469     );
470
471     $self->ApplySortOrder;
472
473     # we call Count below, but later we always do search
474     # so just do search and get count from results
475     $self->_DoSearch if $self->{'must_redo_search'};
476
477     $RT::Logger->debug(
478         "Found ". $self->Count ." scrips for $args{'Stage'} stage"
479         ." with applicable type(s) $args{'Type'}"
480         ." for txn #".$self->{TransactionObj}->Id
481         ." on ticket #".$self->{TicketObj}->Id
482     );
483 }
484
485 RT::Base->_ImportOverlays();
486
487 1;