Merge branch 'master' of git.freeside.biz:/home/git/freeside
[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->{'CurrentUser'} ) { # 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{__WARN__};
96     local $SIG{__DIE__};
97     eval 'RT::Init("NoSignalHandlers"=>1);';
98   }
99   die $@ if $@;
100
101   warn "$me init: complete" if $DEBUG;
102 }
103
104 =item customer_tickets CUSTNUM [ LIMIT ] [ PRIORITYVALUE ]
105
106 Replacement for the one in RT_External so that we can access custom fields 
107 properly.
108
109 =cut
110
111 # create an RT::Tickets object for a specified custnum or svcnum
112
113 sub _tickets_search {
114   my( $self, $type, $number, $limit, $priority, $status ) = @_;
115
116   $type =~ /^Customer|Service$/ or die "invalid type: $type";
117   $number =~ /^\d+$/ or die "invalid custnum/svcnum: $number";
118   $limit =~ /^\d+$/ or die "invalid limit: $limit";
119
120   my $session = $self->session();
121   my $CurrentUser = $session->{CurrentUser} 
122     or die "unable to create an RT session";
123
124   my $Tickets = RT::Tickets->new($CurrentUser);
125
126   # "Customer.number" searches tickets linked via cust_svc also
127   my $rtql = "$type.number = $number";
128
129   if ( defined( $priority ) ) {
130     my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field');
131     if ( length( $priority ) ) {
132       $rtql .= " AND CF.{$custom_priority} = '$priority'";
133     }
134     else {
135       $rtql .= " AND CF.{$custom_priority} IS NULL";
136     }
137   }
138
139   my @statuses;
140   if ( defined($status) && $status ) {
141     if ( ref($status) ) {
142       if ( ref($status) eq 'HASH' ) {
143         @statuses = grep $status->{$_}, keys %$status;
144       } elsif ( ref($status) eq 'ARRAY' ) {
145         @statuses = @$status;
146       } else {
147         #what should be the failure mode here?  die?  return no tickets?
148         die 'unknown status ref '. ref($status);
149       }
150     } else {
151       @statuses = ( $status );
152     }
153     @statuses = grep /^\w+$/, @statuses; #injection prevention
154   } else {
155     @statuses = $self->statuses;
156   }
157
158   $rtql .= ' AND ( '.
159                       join(' OR ', map { "Status = '$_'" } @statuses).
160                ' ) ';
161
162   warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
163   $Tickets->FromSQL($rtql);
164
165   $Tickets->RowsPerPage($limit);
166   warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
167
168   return $Tickets;
169 }
170
171 sub href_customer_tickets {
172   my ($self, $custnum) = (shift, shift);
173   if ($custnum =~ /^(\d+)$/) {
174     return $self->href_search_tickets("Customer.number = $custnum", @_);
175   }
176   warn "bad custnum $custnum"; '';
177 }
178
179 sub href_service_tickets {
180   my ($self, $svcnum) = (shift, shift);
181   if ($svcnum =~ /^(\d+)$/ ) {
182     return $self->href_search_tickets("Service.number = $svcnum", @_);
183   }
184   warn "bad svcnum $svcnum"; '';
185 }
186
187 sub customer_tickets {
188   my $self = shift;
189   my $Tickets = $self->_tickets_search('Customer', @_);
190
191   my $conf = FS::Conf->new;
192   my $priority_order =
193     $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
194
195   my @order_by = (
196     { FIELD => 'Priority', ORDER => $priority_order },
197     { FIELD => 'Id',       ORDER => 'DESC' },
198   );
199
200   $Tickets->OrderByCols(@order_by);
201
202   my @tickets;
203   while ( my $t = $Tickets->Next ) {
204     push @tickets, _ticket_info($t);
205   }
206
207   return \@tickets;
208 }
209
210 sub num_customer_tickets {
211   my ( $self, $custnum, $priority ) = @_;
212   $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll;
213 }
214
215 sub service_tickets  {
216   my $self = shift;
217   my $Tickets = $self->_tickets_search('Service', @_);
218
219   my $conf = FS::Conf->new;
220   my $priority_order =
221     $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
222
223   my @order_by = (
224     { FIELD => 'Priority', ORDER => $priority_order },
225     { FIELD => 'Id',       ORDER => 'DESC' },
226   );
227
228   $Tickets->OrderByCols(@order_by);
229
230   my @tickets;
231   while ( my $t = $Tickets->Next ) {
232     push @tickets, _ticket_info($t);
233   }
234
235   return \@tickets;
236 }
237
238 sub _ticket_info {
239   # Takes an RT::Ticket; returns a hashref of the ticket's fields, including 
240   # custom fields.  Also returns custom and selfservice priority values as 
241   # _custom_priority and _selfservice_priority.
242   my $t = shift;
243
244   my $custom_priority = 
245     FS::Conf->new->config('ticket_system-custom_priority_field') || '';
246   my $ss_priority = selfservice_priority();
247
248   my %ticket_info;
249   foreach my $name ( $t->ReadableAttributes ) {
250     # lowercase names, and skip attributes with non-scalar values
251     $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
252   }
253   $ticket_info{'owner'} = $t->OwnerObj->Name;
254   $ticket_info{'queue'} = $t->QueueObj->Name;
255   foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
256     my $name = 'CF.{'.$CF->Name.'}';
257     $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
258   }
259   # make this easy to find
260   if ( $custom_priority ) {
261     $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
262   }
263   if ( $ss_priority ) {
264     $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
265   }
266   my $svcnums = [ 
267     map { $_->Target =~ /cust_svc\/(\d+)/; $1 } 
268         @{ $t->Services->ItemsArrayRef }
269   ];
270   $ticket_info{'svcnums'} = $svcnums;
271
272   return \%ticket_info;
273 }
274
275 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
276
277 Class method.  Creates a ticket.  If there is an error, returns the scalar
278 error, otherwise returns the newly created RT::Ticket object.
279
280 Accepts the following options:
281
282 =over 4
283
284 =item queue
285
286 Queue name or Id
287
288 =item subject
289
290 Ticket subject
291
292 =item requestor
293
294 Requestor email address or arrayref of addresses
295
296 =item cc
297
298 Cc: email address or arrayref of addresses
299
300 =item message
301
302 Ticket message
303
304 =item mime_type
305
306 MIME type to use for message.  Defaults to text/plain.  Specifying text/html
307 can be useful to use HTML markup in message.
308
309 =item custnum
310
311 Customer number (see L<FS::cust_main>) to associate with ticket.
312
313 =item svcnum
314
315 Service number (see L<FS::cust_svc>) to associate with ticket.  Will also
316 associate the customer who has this service (unless the service is unlinked).
317
318 =back
319
320 =cut
321
322 sub create_ticket {
323   my($self, $session, %param) = @_;
324
325   $session = $self->session($session);
326
327   my $Queue = RT::Queue->new($session->{'CurrentUser'});
328   $Queue->Load( $param{'queue'} );
329
330   my $req = ref($param{'requestor'})
331               ? $param{'requestor'}
332               : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
333
334   my $cc = ref($param{'cc'})
335              ? $param{'cc'}
336              : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
337
338   my $mimeobj = MIME::Entity->build(
339     'Data' => $param{'message'},
340     'Type' => ( $param{'mime_type'} || 'text/plain' ),
341   );
342
343   my %ticket = (
344     'Queue'     => $Queue->Id,
345     'Subject'   => $param{'subject'},
346     'Requestor' => $req,
347     'Cc'        => $cc,
348     'MIMEObj'   => $mimeobj,
349   );
350   warn Dumper(\%ticket) if $DEBUG > 1;
351
352   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
353   my( $id, $Transaction, $ErrStr );
354   {
355     local $SIG{__DIE__};
356     ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
357   }
358   return $ErrStr if $id == 0;
359
360   warn "ticket got id $id\n" if $DEBUG;
361
362   #XXX check errors adding custnum/svcnum links (put it in a transaction)...
363   # but we do already know they're good
364
365   if ( $param{'custnum'} ) {
366     my( $val, $msg ) = $Ticket->_AddLink(
367      'Type'   => 'MemberOf',
368      'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
369     );
370   }
371
372   if ( $param{'svcnum'} ) {
373     my( $val, $msg ) = $Ticket->_AddLink(
374      'Type'   => 'MemberOf',
375      'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
376     );
377   }
378
379   $Ticket;
380 }
381
382 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
383
384 Class method. Retrieves a ticket. If there is an error, returns the scalar
385 error. Otherwise, currently returns a slightly tricky data structure containing
386 the ticket's attributes, a list of the linked customers, each transaction's 
387 content, description, and create time.
388
389 Accepts the following options:
390
391 =over 4
392
393 =item ticket_id 
394
395 The ticket id
396
397 =back
398
399 =cut
400
401 sub get_ticket {
402   my($self, $session, %param) = @_;
403
404   $session = $self->session($session);
405
406   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
407   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
408   return 'Could not load ticket' unless $ticketid;
409
410   my @custs = ();
411   foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
412     my $cust = $link->Target;
413     push @custs, $1 if $cust =~ /\/(\d+)$/;
414   }
415
416   my @txns = ();
417   my $transactions = $Ticket->Transactions;
418   while ( my $transaction = $transactions->Next ) {
419     my $t = { created => $transaction->Created,
420         content => $transaction->Content,
421         description => $transaction->Description,
422         type => $transaction->Type,
423     };
424     push @txns, $t;
425   }
426
427   { txns => [ @txns ],
428     custs => [ @custs ],
429     fields => _ticket_info($Ticket),
430   };
431 }
432
433 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
434
435 Class method.  Retrieve the RT::Ticket object with the specified 
436 ticket_id.  If custnum is supplied, will also check that the object 
437 is a member of that customer.  If there is no ticket or the custnum 
438 check fails, returns nothing.  The meaning of that case is 
439 "to this customer, the ticket does not exist".
440
441 Options:
442
443 =over 4
444
445 =item ticket_id
446
447 =item custnum
448
449 =back
450
451 =cut
452
453 sub get_ticket_object {
454   my $self = shift;
455   my ($session, %opt) = @_;
456   $session = $self->session(shift);
457   # use a small search here so we can check ticket ownership
458   my $query;
459   if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
460     $query = "id = $1";
461   } else {
462     return;
463   }
464   if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
465     $query .= " AND Customer.number = $1"; # also checks ownership via services
466   }
467   my $Tickets = RT::Tickets->new($session->{CurrentUser});
468   $Tickets->FromSQL($query);
469   return $Tickets->First;
470 }
471
472 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
473
474 Class method. Correspond on a ticket. If there is an error, returns the scalar
475 error. Otherwise, returns the transaction id, error message, and
476 RT::Transaction object.
477
478 Accepts the following options:
479
480 =over 4
481
482 =item ticket_id 
483
484 The ticket id
485
486 =item content
487
488 Correspondence content
489
490 =back
491
492 =cut
493
494 sub correspond_ticket {
495   my($self, $session, %param) = @_;
496
497   $session = $self->session($session);
498
499   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
500   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
501   return 'Could not load ticket' unless $ticketid;
502   return 'No content' unless $param{'content'};
503
504   $Ticket->Correspond( Content => $param{'content'} );
505 }
506
507 =item queues SESSION_HASHREF [, ACL ]
508
509 Retrieve a list of queues.  Pass the name of an RT access control right, 
510 such as 'CreateTicket', to return only queues on which the current user 
511 has that right.  Otherwise this will return all queues with the 'SeeQueue' 
512 right.
513
514 =cut
515
516 sub queues {
517   my( $self, $session, $acl ) = @_;
518   $session = $self->session($session);
519
520   my $showall = $acl ? 0 : 1;
521   my @result = ();
522   my $q = new RT::Queues($session->{'CurrentUser'});
523   $q->UnLimit;
524   while (my $queue = $q->Next) {
525     if ($showall || $queue->CurrentUserHasRight($acl)) {
526       push @result, {
527         Id          => $queue->Id,
528         Name        => $queue->Name,
529         Description => $queue->Description,
530       };
531     }
532   }
533   return map { $_->{Id} => $_->{Name} } @result;
534 }
535
536 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
537 # to get logged into RT from afar
538 sub _web_external_auth {
539   my( $self, $session ) = @_;
540
541   my $user = $FS::CurrentUser::CurrentUser->username;
542
543   eval 'use RT::CurrentUser;';
544   die $@ if $@;
545
546   $session ||= {};
547   $session->{'CurrentUser'} = RT::CurrentUser->new();
548
549   warn "$me _web_external_auth loading RT user for $user\n"
550     if $DEBUG > 1;
551
552   $session->{'CurrentUser'}->Load($user);
553
554   if ( ! $session->{'CurrentUser'}->Id() ) {
555
556       # Create users on-the-fly
557
558       warn "can't load RT user for $user; auto-creating\n"
559         if $DEBUG;
560
561       my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
562
563       my ( $val, $msg ) = $UserObj->Create(
564           %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
565           Name  => $user,
566           Gecos => $user,
567       );
568
569       if ($val) {
570
571           # now get user specific information, to better create our user.
572           my $new_user_info
573               = RT::Interface::Web::WebExternalAutoInfo($user);
574
575           # set the attributes that have been defined.
576           # FIXME: this is a horrible kludge. I'm sure there's something cleaner
577           foreach my $attribute (
578               'Name',                  'Comments',
579               'Signature',             'EmailAddress',
580               'PagerEmailAddress',     'FreeformContactInfo',
581               'Organization',          'Disabled',
582               'Privileged',            'RealName',
583               'NickName',              'Lang',
584               'EmailEncoding',         'WebEncoding',
585               'ExternalContactInfoId', 'ContactInfoSystem',
586               'ExternalAuthId',        'Gecos',
587               'HomePhone',             'WorkPhone',
588               'MobilePhone',           'PagerPhone',
589               'Address1',              'Address2',
590               'City',                  'State',
591               'Zip',                   'Country'
592               )
593           {
594               #uhh, wrong root
595               #$m->comp( '/Elements/Callback', %ARGS,
596               #    _CallbackName => 'NewUser' );
597
598               my $method = "Set$attribute";
599               $UserObj->$method( $new_user_info->{$attribute} )
600                   if ( defined $new_user_info->{$attribute} );
601           }
602           $session->{'CurrentUser'}->Load($user);
603       }
604       else {
605
606          # we failed to successfully create the user. abort abort abort.
607           delete $session->{'CurrentUser'};
608
609           die "can't auto-create RT user: $msg"; #an error message would be nice :/
610           #$m->abort() unless $RT::WebFallbackToInternalAuth;
611           #$m->comp( '/Elements/Login', %ARGS,
612           #    Error => loc( 'Cannot create user: [_1]', $msg ) );
613       }
614   }
615
616   unless ( $session->{'CurrentUser'}->Id() ) {
617       delete $session->{'CurrentUser'};
618
619       die "can't auto-create RT user";
620       #$user = $orig_user;
621       # 
622       #if ($RT::WebExternalOnly) {
623       #    $m->comp( '/Elements/Login', %ARGS,
624       #        Error => loc('You are not an authorized user') );
625       #    $m->abort();
626       #}
627   }
628
629   $session;
630
631 }
632
633 =item selfservice_priority
634
635 Returns the configured self-service priority field.
636
637 =cut
638
639 my $selfservice_priority;
640
641 sub selfservice_priority {
642   return $selfservice_priority ||= do {
643     my $conf = FS::Conf->new;
644     $conf->config('ticket_system-selfservice_priority_field') || '';
645   }
646 }
647
648 1;
649