further cleanup, #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 ( $self, $custnum, $priority ) = @_;
176   my $Tickets = $self->_customer_tickets_search($custnum, 0, $priority);
177   return $Tickets->CountAll;
178 }
179
180 sub _ticket_info {
181   # Takes an RT::Ticket; returns a hashref of the ticket's fields, including 
182   # custom fields.  Also returns custom and selfservice priority values as 
183   # _custom_priority and _selfservice_priority.
184   my $t = shift;
185
186   my $custom_priority = 
187     FS::Conf->new->config('ticket_system-custom_priority_field') || '';
188   my $ss_priority = selfservice_priority();
189
190   my %ticket_info;
191   foreach my $name ( $t->ReadableAttributes ) {
192     # lowercase names, and skip attributes with non-scalar values
193     $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
194   }
195   $ticket_info{'owner'} = $t->OwnerObj->Name;
196   $ticket_info{'queue'} = $t->QueueObj->Name;
197   foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
198     my $name = 'CF.{'.$CF->Name.'}';
199     $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
200   }
201   # make this easy to find
202   if ( $custom_priority ) {
203     $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
204   }
205   if ( $ss_priority ) {
206     $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
207   }
208   return \%ticket_info;
209 }
210
211 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
212
213 Class method.  Creates a ticket.  If there is an error, returns the scalar
214 error, otherwise returns the newly created RT::Ticket object.
215
216 Accepts the following options:
217
218 =over 4
219
220 =item queue
221
222 Queue name or Id
223
224 =item subject
225
226 Ticket subject
227
228 =item requestor
229
230 Requestor email address or arrayref of addresses
231
232 =item cc
233
234 Cc: email address or arrayref of addresses
235
236 =item message
237
238 Ticket message
239
240 =item mime_type
241
242 MIME type to use for message.  Defaults to text/plain.  Specifying text/html
243 can be useful to use HTML markup in message.
244
245 =item custnum
246
247 Customer number (see L<FS::cust_main>) to associate with ticket.
248
249 =item svcnum
250
251 Service number (see L<FS::cust_svc>) to associate with ticket.  Will also
252 associate the customer who has this service (unless the service is unlinked).
253
254 =back
255
256 =cut
257
258 sub create_ticket {
259   my($self, $session, %param) = @_;
260
261   $session = $self->session($session);
262
263   my $Queue = RT::Queue->new($session->{'CurrentUser'});
264   $Queue->Load( $param{'queue'} );
265
266   my $req = ref($param{'requestor'})
267               ? $param{'requestor'}
268               : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
269
270   my $cc = ref($param{'cc'})
271              ? $param{'cc'}
272              : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
273
274   my $mimeobj = MIME::Entity->build(
275     'Data' => $param{'message'},
276     'Type' => ( $param{'mime_type'} || 'text/plain' ),
277   );
278
279   my %ticket = (
280     'Queue'     => $Queue->Id,
281     'Subject'   => $param{'subject'},
282     'Requestor' => $req,
283     'Cc'        => $cc,
284     'MIMEObj'   => $mimeobj,
285   );
286   warn Dumper(\%ticket) if $DEBUG > 1;
287
288   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
289   my( $id, $Transaction, $ErrStr );
290   {
291     local $SIG{__DIE__};
292     ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
293   }
294   return $ErrStr if $id == 0;
295
296   warn "ticket got id $id\n" if $DEBUG;
297
298   #XXX check errors adding custnum/svcnum links (put it in a transaction)...
299   # but we do already know they're good
300
301   if ( $param{'custnum'} ) {
302     my( $val, $msg ) = $Ticket->_AddLink(
303      'Type'   => 'MemberOf',
304      'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
305     );
306   }
307
308   if ( $param{'svcnum'} ) {
309     my( $val, $msg ) = $Ticket->_AddLink(
310      'Type'   => 'MemberOf',
311      'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
312     );
313   }
314
315   $Ticket;
316 }
317
318 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
319
320 Class method. Retrieves a ticket. If there is an error, returns the scalar
321 error. Otherwise, currently returns a slightly tricky data structure containing
322 the ticket's attributes, a list of the linked customers, each transaction's 
323 content, description, and create time.
324
325 Accepts the following options:
326
327 =over 4
328
329 =item ticket_id 
330
331 The ticket id
332
333 =back
334
335 =cut
336
337 sub get_ticket {
338   my($self, $session, %param) = @_;
339
340   $session = $self->session($session);
341
342   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
343   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
344   return 'Could not load ticket' unless $ticketid;
345
346   my @custs = ();
347   foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
348     my $cust = $link->Target;
349     push @custs, $1 if $cust =~ /\/(\d+)$/;
350   }
351
352   my @txns = ();
353   my $transactions = $Ticket->Transactions;
354   while ( my $transaction = $transactions->Next ) {
355     my $t = { created => $transaction->Created,
356         content => $transaction->Content,
357         description => $transaction->Description,
358         type => $transaction->Type,
359     };
360     push @txns, $t;
361   }
362
363   { txns => [ @txns ],
364     custs => [ @custs ],
365     fields => _ticket_info($Ticket),
366   };
367 }
368
369 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
370
371 Class method.  Retrieve the RT::Ticket object with the specified 
372 ticket_id.  If custnum is supplied, will also check that the object 
373 is a member of that customer.  If there is no ticket or the custnum 
374 check fails, returns nothing.  The meaning of that case is 
375 "to this customer, the ticket does not exist".
376
377 Options:
378
379 =over 4
380
381 =item ticket_id
382
383 =item custnum
384
385 =back
386
387 =cut
388
389 sub get_ticket_object {
390   my $self = shift;
391   my ($session, %opt) = @_;
392   $session = $self->session(shift);
393   my $Ticket = RT::Ticket->new($session->{CurrentUser});
394   $Ticket->Load($opt{'ticket_id'});
395   return if ( !$Ticket->id );
396   my $custnum = $opt{'custnum'};
397   if ( defined($custnum) && $custnum =~ /^\d+$/ ) {
398     # probably the most efficient way to check ticket ownership
399     my $Link = RT::Link->new($session->{CurrentUser});
400     $Link->LoadByCols( LocalBase => $opt{'ticket_id'},
401                        Type      => 'MemberOf',
402                        Target    => "freeside://freeside/cust_main/$custnum",
403                      );
404     return if ( !$Link->id );
405   }
406   return $Ticket;
407 }
408
409
410 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
411
412 Class method. Correspond on a ticket. If there is an error, returns the scalar
413 error. Otherwise, returns the transaction id, error message, and
414 RT::Transaction object.
415
416 Accepts the following options:
417
418 =over 4
419
420 =item ticket_id 
421
422 The ticket id
423
424 =item content
425
426 Correspondence content
427
428 =back
429
430 =cut
431
432 sub correspond_ticket {
433   my($self, $session, %param) = @_;
434
435   $session = $self->session($session);
436
437   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
438   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
439   return 'Could not load ticket' unless $ticketid;
440   return 'No content' unless $param{'content'};
441
442   $Ticket->Correspond( Content => $param{'content'} );
443 }
444
445 =item queues SESSION_HASHREF [, ACL ]
446
447 Retrieve a list of queues.  Pass the name of an RT access control right, 
448 such as 'CreateTicket', to return only queues on which the current user 
449 has that right.  Otherwise this will return all queues with the 'SeeQueue' 
450 right.
451
452 =cut
453
454 sub queues {
455   my( $self, $session, $acl ) = @_;
456   $session = $self->session($session);
457
458   my $showall = $acl ? 0 : 1;
459   my @result = ();
460   my $q = new RT::Queues($session->{'CurrentUser'});
461   $q->UnLimit;
462   while (my $queue = $q->Next) {
463     if ($showall || $queue->CurrentUserHasRight($acl)) {
464       push @result, {
465         Id          => $queue->Id,
466         Name        => $queue->Name,
467         Description => $queue->Description,
468       };
469     }
470   }
471   return map { $_->{Id} => $_->{Name} } @result;
472 }
473
474 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
475 # to get logged into RT from afar
476 sub _web_external_auth {
477   my( $self, $session ) = @_;
478
479   my $user = $FS::CurrentUser::CurrentUser->username;
480
481   eval 'use RT::CurrentUser;';
482   die $@ if $@;
483
484   $session ||= {};
485   $session->{'CurrentUser'} = RT::CurrentUser->new();
486
487   warn "$me _web_external_auth loading RT user for $user\n"
488     if $DEBUG > 1;
489
490   $session->{'CurrentUser'}->Load($user);
491
492   if ( ! $session->{'CurrentUser'}->Id() ) {
493
494       # Create users on-the-fly
495
496       warn "can't load RT user for $user; auto-creating\n"
497         if $DEBUG;
498
499       my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
500
501       my ( $val, $msg ) = $UserObj->Create(
502           %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
503           Name  => $user,
504           Gecos => $user,
505       );
506
507       if ($val) {
508
509           # now get user specific information, to better create our user.
510           my $new_user_info
511               = RT::Interface::Web::WebExternalAutoInfo($user);
512
513           # set the attributes that have been defined.
514           # FIXME: this is a horrible kludge. I'm sure there's something cleaner
515           foreach my $attribute (
516               'Name',                  'Comments',
517               'Signature',             'EmailAddress',
518               'PagerEmailAddress',     'FreeformContactInfo',
519               'Organization',          'Disabled',
520               'Privileged',            'RealName',
521               'NickName',              'Lang',
522               'EmailEncoding',         'WebEncoding',
523               'ExternalContactInfoId', 'ContactInfoSystem',
524               'ExternalAuthId',        'Gecos',
525               'HomePhone',             'WorkPhone',
526               'MobilePhone',           'PagerPhone',
527               'Address1',              'Address2',
528               'City',                  'State',
529               'Zip',                   'Country'
530               )
531           {
532               #uhh, wrong root
533               #$m->comp( '/Elements/Callback', %ARGS,
534               #    _CallbackName => 'NewUser' );
535
536               my $method = "Set$attribute";
537               $UserObj->$method( $new_user_info->{$attribute} )
538                   if ( defined $new_user_info->{$attribute} );
539           }
540           $session->{'CurrentUser'}->Load($user);
541       }
542       else {
543
544          # we failed to successfully create the user. abort abort abort.
545           delete $session->{'CurrentUser'};
546
547           die "can't auto-create RT user"; #an error message would be nice :/
548           #$m->abort() unless $RT::WebFallbackToInternalAuth;
549           #$m->comp( '/Elements/Login', %ARGS,
550           #    Error => loc( 'Cannot create user: [_1]', $msg ) );
551       }
552   }
553
554   unless ( $session->{'CurrentUser'}->Id() ) {
555       delete $session->{'CurrentUser'};
556
557       die "can't auto-create RT user";
558       #$user = $orig_user;
559       # 
560       #if ($RT::WebExternalOnly) {
561       #    $m->comp( '/Elements/Login', %ARGS,
562       #        Error => loc('You are not an authorized user') );
563       #    $m->abort();
564       #}
565   }
566
567   $session;
568
569 }
570
571 =item selfservice_priority
572
573 Returns the configured self-service priority field.
574
575 =cut
576
577 my $selfservice_priority;
578
579 sub selfservice_priority {
580   return $selfservice_priority ||= do {
581     my $conf = FS::Conf->new;
582     $conf->config('ticket_system-selfservice_priority_field') || '';
583   }
584 }
585
586 1;
587