fix fallout from selfservice priority, #13199
[freeside.git] / FS / FS / TicketSystem / RT_Internal.pm
1 package FS::TicketSystem::RT_Internal;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me );
5 use Data::Dumper;
6 use MIME::Entity;
7 use FS::UID qw(dbh);
8 use FS::CGI qw(popurl);
9 use FS::TicketSystem::RT_Libs;
10
11 @ISA = qw( FS::TicketSystem::RT_Libs );
12
13 $DEBUG = 0;
14 $me = '[FS::TicketSystem::RT_Internal]';
15
16 sub sql_num_customer_tickets {
17   "( select count(*) from Tickets
18                      join Links on ( Tickets.id = Links.LocalBase )
19      where ( Status = 'new' or Status = 'open' or Status = 'stalled' )
20        and Target = 'freeside://freeside/cust_main/' || custnum
21    )";
22 }
23
24 sub baseurl {
25   #my $self = shift;
26   if ( $RT::URI::freeside::URL ) {
27     $RT::URI::freeside::URL. '/rt/';
28   } else {
29     'http://you_need_to_set_RT_URI_freeside_URL_in_SiteConfig.pm/';
30   }
31 }
32
33 #mapping/genericize??
34 #ShowConfigTab ModifySelf
35 sub access_right {
36   my( $self, $session, $right ) = @_;
37
38   return '' unless FS::Conf->new->config('ticket_system');
39
40   $session = $self->session($session);
41
42   #warn "$me access_right: CurrentUser ". $session->{'CurrentUser'}. ":\n".
43   #     ( $DEBUG>1 ? Dumper($session->{'CurrentUser'}) : '' )
44   #  if $DEBUG > 1;
45
46   $session->{'CurrentUser'}->HasRight( Right  => $right,
47                                        Object => $RT::System );
48 }
49
50 sub session {
51   my( $self, $session ) = @_;
52
53   if ( $session && $session->{'Current_User'} ) { # does this even work?
54     warn "$me session: using existing session and CurrentUser: \n".
55          Dumper($session->{'CurrentUser'})
56       if $DEBUG;
57  } else {
58     warn "$me session: loading session and CurrentUser\n" if $DEBUG > 1;
59     $session = $self->_web_external_auth($session);
60   }
61
62   $session;
63 }
64
65 my $firsttime = 1;
66
67 sub init {
68   my $self = shift;
69   if ( $firsttime ) {
70
71     # this part only needs to be done once
72     warn "$me init: loading RT libraries\n" if $DEBUG;
73     eval '
74       use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
75       use RT;
76
77       #for web external auth...
78       use RT::Interface::Web;
79     ';
80     die $@ if $@;
81
82     warn "$me init: loading RT config\n" if $DEBUG;
83     {
84       local $SIG{__DIE__};
85       eval 'RT::LoadConfig();';
86     }
87     die $@ if $@;
88
89     $firsttime = 0;
90   }
91
92   # this needs to be done on each fork
93   warn "$me init: initializing RT\n" if $DEBUG;
94   {
95     local $SIG{__DIE__};
96     eval 'RT::Init("NoSignalHandlers"=>1);';
97   }
98   die $@ if $@;
99
100   warn "$me init: complete" if $DEBUG;
101 }
102
103 =item customer_tickets CUSTNUM [ LIMIT ] [ PRIORITYVALUE ]
104
105 Replacement for the one in RT_External so that we can access custom fields 
106 properly.
107
108 =cut
109
110 sub _customer_tickets_search {
111   my ( $self, $custnum, $limit, $priority ) = @_;
112
113   $custnum =~ /^\d+$/ or die "invalid custnum: $custnum";
114   $limit =~ /^\d+$/ or die "invalid limit: $limit";
115
116   my $session = $self->session();
117   my $CurrentUser = $session->{CurrentUser} 
118     or die "unable to create an RT session";
119
120   my $Tickets = RT::Tickets->new($CurrentUser);
121
122   my $rtql = "MemberOf = 'freeside://freeside/cust_main/$custnum'";
123
124   if ( defined( $priority ) ) {
125     my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field');
126     if ( length( $priority ) ) {
127       $rtql .= " AND CF.{$custom_priority} = '$priority'";
128     }
129     else {
130       $rtql .= " AND CF.{$custom_priority} IS NULL";
131     }
132   }
133
134   $rtql .= ' AND ( ' .
135            join(' OR ', map { "Status = '$_'" } $self->statuses) .
136            ' )';
137
138   warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
139   $Tickets->FromSQL($rtql);
140
141   $Tickets->RowsPerPage($limit);
142   warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
143
144   return $Tickets;
145 }
146
147 sub customer_tickets {
148   my $Tickets = _customer_tickets_search(@_);
149
150   my $conf = FS::Conf->new;
151   my $priority_order =
152     $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
153   my $custom_priority = 
154     $conf->config('ticket_system-custom_priority_field') || '';
155
156   my @order_by;
157   my $ss_priority = selfservice_priority();
158   push @order_by, { FIELD => "CF.{$ss_priority}", ORDER => $priority_order }
159     if $ss_priority;
160   push @order_by,
161     { FIELD => 'Priority', ORDER => $priority_order },
162     { FIELD => 'Id',       ORDER => 'DESC' },
163   ;
164
165   $Tickets->OrderByCols(@order_by);
166
167   my @tickets;
168   while ( my $t = $Tickets->Next ) {
169     push @tickets, _ticket_info($t);
170   }
171   return \@tickets;
172 }
173
174 sub num_customer_tickets {
175   my $Tickets = _customer_tickets_search(@_);
176   return $Tickets->CountAll;
177 }
178
179 sub _ticket_info {
180   # Takes an RT::Ticket; returns a hashref of the ticket's fields, including 
181   # custom fields.  Also returns custom and selfservice priority values as 
182   # _custom_priority and _selfservice_priority.
183   my $t = shift;
184
185   my $custom_priority = 
186     FS::Conf->new->config('ticket_system-custom_priority_field') || '';
187   my $ss_priority = selfservice_priority();
188
189   my %ticket_info;
190   foreach my $name ( $t->ReadableAttributes ) {
191     # lowercase names, and skip attributes with non-scalar values
192     $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
193   }
194   $ticket_info{'owner'} = $t->OwnerObj->Name;
195   $ticket_info{'queue'} = $t->QueueObj->Name;
196   foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
197     my $name = 'CF.{'.$CF->Name.'}';
198     $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
199   }
200   # make this easy to find
201   if ( $custom_priority ) {
202     $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
203   }
204   if ( $ss_priority ) {
205     $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
206   }
207   return \%ticket_info;
208 }
209
210 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
211
212 Class method.  Creates a ticket.  If there is an error, returns the scalar
213 error, otherwise returns the newly created RT::Ticket object.
214
215 Accepts the following options:
216
217 =over 4
218
219 =item queue
220
221 Queue name or Id
222
223 =item subject
224
225 Ticket subject
226
227 =item requestor
228
229 Requestor email address or arrayref of addresses
230
231 =item cc
232
233 Cc: email address or arrayref of addresses
234
235 =item message
236
237 Ticket message
238
239 =item mime_type
240
241 MIME type to use for message.  Defaults to text/plain.  Specifying text/html
242 can be useful to use HTML markup in message.
243
244 =item custnum
245
246 Customer number (see L<FS::cust_main>) to associate with ticket.
247
248 =item svcnum
249
250 Service number (see L<FS::cust_svc>) to associate with ticket.  Will also
251 associate the customer who has this service (unless the service is unlinked).
252
253 =back
254
255 =cut
256
257 sub create_ticket {
258   my($self, $session, %param) = @_;
259
260   $session = $self->session($session);
261
262   my $Queue = RT::Queue->new($session->{'CurrentUser'});
263   $Queue->Load( $param{'queue'} );
264
265   my $req = ref($param{'requestor'})
266               ? $param{'requestor'}
267               : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
268
269   my $cc = ref($param{'cc'})
270              ? $param{'cc'}
271              : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
272
273   my $mimeobj = MIME::Entity->build(
274     'Data' => $param{'message'},
275     'Type' => ( $param{'mime_type'} || 'text/plain' ),
276   );
277
278   my %ticket = (
279     'Queue'     => $Queue->Id,
280     'Subject'   => $param{'subject'},
281     'Requestor' => $req,
282     'Cc'        => $cc,
283     'MIMEObj'   => $mimeobj,
284   );
285   warn Dumper(\%ticket) if $DEBUG > 1;
286
287   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
288   my( $id, $Transaction, $ErrStr );
289   {
290     local $SIG{__DIE__};
291     ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
292   }
293   return $ErrStr if $id == 0;
294
295   warn "ticket got id $id\n" if $DEBUG;
296
297   #XXX check errors adding custnum/svcnum links (put it in a transaction)...
298   # but we do already know they're good
299
300   if ( $param{'custnum'} ) {
301     my( $val, $msg ) = $Ticket->_AddLink(
302      'Type'   => 'MemberOf',
303      'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
304     );
305   }
306
307   if ( $param{'svcnum'} ) {
308     my( $val, $msg ) = $Ticket->_AddLink(
309      'Type'   => 'MemberOf',
310      'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
311     );
312   }
313
314   $Ticket;
315 }
316
317 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
318
319 Class method. Retrieves a ticket. If there is an error, returns the scalar
320 error. Otherwise, currently returns a slightly tricky data structure containing
321 the ticket's attributes, a list of the linked customers, each transaction's 
322 content, description, and create time.
323
324 Accepts the following options:
325
326 =over 4
327
328 =item ticket_id 
329
330 The ticket id
331
332 =back
333
334 =cut
335
336 sub get_ticket {
337   my($self, $session, %param) = @_;
338
339   $session = $self->session($session);
340
341   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
342   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
343   return 'Could not load ticket' unless $ticketid;
344
345   my @custs = ();
346   foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
347     my $cust = $link->Target;
348     push @custs, $1 if $cust =~ /\/(\d+)$/;
349   }
350
351   my @txns = ();
352   my $transactions = $Ticket->Transactions;
353   while ( my $transaction = $transactions->Next ) {
354     my $t = { created => $transaction->Created,
355         content => $transaction->Content,
356         description => $transaction->Description,
357         type => $transaction->Type,
358     };
359     push @txns, $t;
360   }
361
362   { txns => [ @txns ],
363     custs => [ @custs ],
364     fields => _ticket_info($Ticket),
365   };
366 }
367
368 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
369
370 Class method.  Retrieve the RT::Ticket object with the specified 
371 ticket_id.  If custnum is supplied, will also check that the object 
372 is a member of that customer.  If there is no ticket or the custnum 
373 check fails, returns nothing.  The meaning of that case is 
374 "to this customer, the ticket does not exist".
375
376 Options:
377
378 =over 4
379
380 =item ticket_id
381
382 =item custnum
383
384 =back
385
386 =cut
387
388 sub get_ticket_object {
389   my $self = shift;
390   my ($session, %opt) = @_;
391   $session = $self->session(shift);
392   my $Ticket = RT::Ticket->new($session->{CurrentUser});
393   $Ticket->Load($opt{'ticket_id'});
394   return if ( !$Ticket->id );
395   my $custnum = $opt{'custnum'};
396   if ( defined($custnum) && $custnum =~ /^\d+$/ ) {
397     # probably the most efficient way to check ticket ownership
398     my $Link = RT::Link->new($session->{CurrentUser});
399     $Link->LoadByCols( LocalBase => $opt{'ticket_id'},
400                        Type      => 'MemberOf',
401                        Target    => "freeside://freeside/cust_main/$custnum",
402                      );
403     return if ( !$Link->id );
404   }
405   return $Ticket;
406 }
407
408
409 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
410
411 Class method. Correspond on a ticket. If there is an error, returns the scalar
412 error. Otherwise, returns the transaction id, error message, and
413 RT::Transaction object.
414
415 Accepts the following options:
416
417 =over 4
418
419 =item ticket_id 
420
421 The ticket id
422
423 =item content
424
425 Correspondence content
426
427 =back
428
429 =cut
430
431 sub correspond_ticket {
432   my($self, $session, %param) = @_;
433
434   $session = $self->session($session);
435
436   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
437   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
438   return 'Could not load ticket' unless $ticketid;
439   return 'No content' unless $param{'content'};
440
441   $Ticket->Correspond( Content => $param{'content'} );
442 }
443
444 =item queues SESSION_HASHREF [, ACL ]
445
446 Retrieve a list of queues.  Pass the name of an RT access control right, 
447 such as 'CreateTicket', to return only queues on which the current user 
448 has that right.  Otherwise this will return all queues with the 'SeeQueue' 
449 right.
450
451 =cut
452
453 sub queues {
454   my( $self, $session, $acl ) = @_;
455   $session = $self->session($session);
456
457   my $showall = $acl ? 0 : 1;
458   my @result = ();
459   my $q = new RT::Queues($session->{'CurrentUser'});
460   $q->UnLimit;
461   while (my $queue = $q->Next) {
462     if ($showall || $queue->CurrentUserHasRight($acl)) {
463       push @result, {
464         Id          => $queue->Id,
465         Name        => $queue->Name,
466         Description => $queue->Description,
467       };
468     }
469   }
470   return map { $_->{Id} => $_->{Name} } @result;
471 }
472
473 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
474 # to get logged into RT from afar
475 sub _web_external_auth {
476   my( $self, $session ) = @_;
477
478   my $user = $FS::CurrentUser::CurrentUser->username;
479
480   eval 'use RT::CurrentUser;';
481   die $@ if $@;
482
483   $session ||= {};
484   $session->{'CurrentUser'} = RT::CurrentUser->new();
485
486   warn "$me _web_external_auth loading RT user for $user\n"
487     if $DEBUG > 1;
488
489   $session->{'CurrentUser'}->Load($user);
490
491   if ( ! $session->{'CurrentUser'}->Id() ) {
492
493       # Create users on-the-fly
494
495       warn "can't load RT user for $user; auto-creating\n"
496         if $DEBUG;
497
498       my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
499
500       my ( $val, $msg ) = $UserObj->Create(
501           %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
502           Name  => $user,
503           Gecos => $user,
504       );
505
506       if ($val) {
507
508           # now get user specific information, to better create our user.
509           my $new_user_info
510               = RT::Interface::Web::WebExternalAutoInfo($user);
511
512           # set the attributes that have been defined.
513           # FIXME: this is a horrible kludge. I'm sure there's something cleaner
514           foreach my $attribute (
515               'Name',                  'Comments',
516               'Signature',             'EmailAddress',
517               'PagerEmailAddress',     'FreeformContactInfo',
518               'Organization',          'Disabled',
519               'Privileged',            'RealName',
520               'NickName',              'Lang',
521               'EmailEncoding',         'WebEncoding',
522               'ExternalContactInfoId', 'ContactInfoSystem',
523               'ExternalAuthId',        'Gecos',
524               'HomePhone',             'WorkPhone',
525               'MobilePhone',           'PagerPhone',
526               'Address1',              'Address2',
527               'City',                  'State',
528               'Zip',                   'Country'
529               )
530           {
531               #uhh, wrong root
532               #$m->comp( '/Elements/Callback', %ARGS,
533               #    _CallbackName => 'NewUser' );
534
535               my $method = "Set$attribute";
536               $UserObj->$method( $new_user_info->{$attribute} )
537                   if ( defined $new_user_info->{$attribute} );
538           }
539           $session->{'CurrentUser'}->Load($user);
540       }
541       else {
542
543          # we failed to successfully create the user. abort abort abort.
544           delete $session->{'CurrentUser'};
545
546           die "can't auto-create RT user"; #an error message would be nice :/
547           #$m->abort() unless $RT::WebFallbackToInternalAuth;
548           #$m->comp( '/Elements/Login', %ARGS,
549           #    Error => loc( 'Cannot create user: [_1]', $msg ) );
550       }
551   }
552
553   unless ( $session->{'CurrentUser'}->Id() ) {
554       delete $session->{'CurrentUser'};
555
556       die "can't auto-create RT user";
557       #$user = $orig_user;
558       # 
559       #if ($RT::WebExternalOnly) {
560       #    $m->comp( '/Elements/Login', %ARGS,
561       #        Error => loc('You are not an authorized user') );
562       #    $m->abort();
563       #}
564   }
565
566   $session;
567
568 }
569
570 =item selfservice_priority
571
572 Returns the configured self-service priority field.
573
574 =cut
575
576 my $selfservice_priority;
577
578 sub selfservice_priority {
579   return $selfservice_priority ||= do {
580     my $conf = FS::Conf->new;
581     $conf->config('ticket_system-selfservice_priority_field') || '';
582   }
583 }
584
585 1;
586