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