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