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