1 package FS::TicketSystem::RT_Internal;
4 use vars qw( @ISA $DEBUG $me );
6 use Date::Format qw( time2str );
9 use FS::CGI qw(popurl);
10 use FS::TicketSystem::RT_Libs;
12 @ISA = qw( FS::TicketSystem::RT_Libs );
15 $me = '[FS::TicketSystem::RT_Internal]';
17 sub sql_num_customer_tickets {
18 "( select count(*) from Tickets
19 join Links on ( Tickets.id = Links.LocalBase )
20 where ( Status = 'new' or Status = 'open' or Status = 'stalled' )
21 and Target = 'freeside://freeside/cust_main/' || custnum
27 if ( $RT::URI::freeside::URL ) {
28 $RT::URI::freeside::URL. '/rt/';
30 'http://you_need_to_set_RT_URI_freeside_URL_in_SiteConfig.pm/';
35 #ShowConfigTab ModifySelf
37 my( $self, $session, $right ) = @_;
39 return '' unless FS::Conf->new->config('ticket_system');
41 $session = $self->session($session);
43 #warn "$me access_right: CurrentUser ". $session->{'CurrentUser'}. ":\n".
44 # ( $DEBUG>1 ? Dumper($session->{'CurrentUser'}) : '' )
47 $session->{'CurrentUser'}->HasRight( Right => $right,
48 Object => $RT::System );
52 my( $self, $session ) = @_;
54 if ( $session && $session->{'CurrentUser'} ) { # does this even work?
55 warn "$me session: using existing session and CurrentUser: \n".
56 Dumper($session->{'CurrentUser'})
59 warn "$me session: loading session and CurrentUser\n" if $DEBUG > 1;
60 $session = $self->_web_external_auth($session);
72 # this part only needs to be done once
73 warn "$me init: loading RT libraries\n" if $DEBUG;
75 use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
78 #for web external auth...
79 use RT::Interface::Web;
83 warn "$me init: loading RT config\n" if $DEBUG;
86 eval 'RT::LoadConfig();';
93 # this needs to be done on each fork
94 warn "$me init: initializing RT\n" if $DEBUG;
98 eval 'RT::Init("NoSignalHandlers"=>1);';
102 warn "$me init: complete" if $DEBUG;
105 =item customer_tickets CUSTNUM [ PARAMS ]
107 Replacement for the one in RT_External so that we can access custom fields
108 properly. Accepts a hashref with the following parameters:
110 number - custnum/svcnum
120 resolved - only return tickets resolved after this timestamp
124 # create an RT::Tickets object for a specified custnum or svcnum
126 sub _tickets_search {
130 my( $number, $limit, $priority, $status, $queueid, $opt );
131 if ( ref($_[0]) eq 'HASH' ) {
133 $number = $$opt{'number'};
134 $limit = $$opt{'limit'};
135 $priority = $$opt{'priority'};
136 $status = $$opt{'status'};
137 $queueid = $$opt{'queueid'};
139 ( $number, $limit, $priority, $status, $queueid ) = @_;
143 $type =~ /^Customer|Service$/ or die "invalid type: $type";
144 $number =~ /^\d+$/ or die "invalid custnum/svcnum: $number";
145 $limit =~ /^\d+$/ or die "invalid limit: $limit";
147 my $session = $self->session();
148 my $CurrentUser = $session->{CurrentUser}
149 or die "unable to create an RT session";
151 my $Tickets = RT::Tickets->new($CurrentUser);
153 # "Customer.number" searches tickets linked via cust_svc also
154 my $rtql = "$type.number = $number";
156 if ( defined( $priority ) ) {
157 my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field');
158 if ( length( $priority ) ) {
159 $rtql .= " AND CF.{$custom_priority} = '$priority'";
162 $rtql .= " AND CF.{$custom_priority} IS NULL";
167 if ( defined($status) && $status ) {
168 if ( ref($status) ) {
169 if ( ref($status) eq 'HASH' ) {
170 @statuses = grep $status->{$_}, keys %$status;
171 } elsif ( ref($status) eq 'ARRAY' ) {
172 @statuses = @$status;
174 #what should be the failure mode here? die? return no tickets?
175 die 'unknown status ref '. ref($status);
178 @statuses = ( $status );
180 @statuses = grep /^\w+$/, @statuses; #injection prevention
182 @statuses = $self->statuses;
186 join(' OR ', map { "Status = '$_'" } @statuses).
189 $rtql .= " AND Queue = $queueid " if $queueid;
191 if ($$opt{'resolved'}) {
192 $rtql .= " AND Resolved >= " . dbh->quote(time2str('%Y-%m-%d %H:%M:%S',$$opt{'resolved'}));
195 warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
196 $Tickets->FromSQL($rtql);
198 $Tickets->RowsPerPage($limit);
199 warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
204 sub href_customer_tickets {
205 my ($self, $custnum) = (shift, shift);
206 if ($custnum =~ /^(\d+)$/) {
207 return $self->href_search_tickets("Customer.number = $custnum", @_);
209 warn "bad custnum $custnum"; '';
212 sub href_service_tickets {
213 my ($self, $svcnum) = (shift, shift);
214 if ($svcnum =~ /^(\d+)$/ ) {
215 return $self->href_search_tickets("Service.number = $svcnum", @_);
217 warn "bad svcnum $svcnum"; '';
220 sub customer_tickets {
222 my $Tickets = $self->_tickets_search('Customer', @_);
224 my $conf = FS::Conf->new;
226 $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
229 { FIELD => 'Priority', ORDER => $priority_order },
230 { FIELD => 'Id', ORDER => 'DESC' },
233 $Tickets->OrderByCols(@order_by);
236 while ( my $t = $Tickets->Next ) {
237 push @tickets, _ticket_info($t);
243 sub num_customer_tickets {
244 my ( $self, $custnum, $priority ) = @_;
245 $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll;
248 sub service_tickets {
250 my $Tickets = $self->_tickets_search('Service', @_);
252 my $conf = FS::Conf->new;
254 $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
257 { FIELD => 'Priority', ORDER => $priority_order },
258 { FIELD => 'Id', ORDER => 'DESC' },
261 $Tickets->OrderByCols(@order_by);
264 while ( my $t = $Tickets->Next ) {
265 push @tickets, _ticket_info($t);
272 # Takes an RT::Ticket; returns a hashref of the ticket's fields, including
273 # custom fields. Also returns custom and selfservice priority values as
274 # _custom_priority and _selfservice_priority, and the IsUnreplied property
278 my $custom_priority =
279 FS::Conf->new->config('ticket_system-custom_priority_field') || '';
280 my $ss_priority = selfservice_priority();
283 foreach my $name ( $t->ReadableAttributes ) {
284 # lowercase names, and skip attributes with non-scalar values
285 $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
287 $ticket_info{'owner'} = $t->OwnerObj->Name;
288 $ticket_info{'queue'} = $t->QueueObj->Name;
289 $ticket_info{'_cf_sort_order'} = {};
291 foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
292 $ticket_info{'_cf_sort_order'}{$CF->Name} = $cf_sort++;
293 my $name = 'CF.{'.$CF->Name.'}';
294 $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
296 # make this easy to find
297 if ( $custom_priority ) {
298 $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
300 if ( $ss_priority ) {
301 $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
303 $ticket_info{'is_unreplied'} = $t->IsUnreplied;
305 map { $_->Target =~ /cust_svc\/(\d+)/; $1 }
306 @{ $t->Services->ItemsArrayRef }
308 $ticket_info{'svcnums'} = $svcnums;
310 return \%ticket_info;
313 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
315 Class method. Creates a ticket. If there is an error, returns the scalar
316 error, otherwise returns the newly created RT::Ticket object.
318 Accepts the following options:
332 Requestor email address or arrayref of addresses
336 Cc: email address or arrayref of addresses
344 MIME type to use for message. Defaults to text/plain. Specifying text/html
345 can be useful to use HTML markup in message.
349 Customer number (see L<FS::cust_main>) to associate with ticket.
353 Service number (see L<FS::cust_svc>) to associate with ticket. Will also
354 associate the customer who has this service (unless the service is unlinked).
361 my($self, $session, %param) = @_;
363 $session = $self->session($session);
365 my $Queue = RT::Queue->new($session->{'CurrentUser'});
366 $Queue->Load( $param{'queue'} );
368 my $req = ref($param{'requestor'})
369 ? $param{'requestor'}
370 : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
372 my $cc = ref($param{'cc'})
374 : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
376 my $mimeobj = MIME::Entity->build(
377 'Data' => $param{'message'},
378 'Type' => ( $param{'mime_type'} || 'text/plain' ),
382 'Queue' => $Queue->Id,
383 'Subject' => $param{'subject'},
386 'MIMEObj' => $mimeobj,
388 warn Dumper(\%ticket) if $DEBUG > 1;
390 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
391 my( $id, $Transaction, $ErrStr );
394 ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
396 return $ErrStr if $id == 0;
398 warn "ticket got id $id\n" if $DEBUG;
400 #XXX check errors adding custnum/svcnum links (put it in a transaction)...
401 # but we do already know they're good
403 if ( $param{'custnum'} ) {
404 my( $val, $msg ) = $Ticket->_AddLink(
405 'Type' => 'MemberOf',
406 'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
410 if ( $param{'svcnum'} ) {
411 my( $val, $msg ) = $Ticket->_AddLink(
412 'Type' => 'MemberOf',
413 'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
420 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
422 Class method. Retrieves a ticket. If there is an error, returns the scalar
423 error. Otherwise, currently returns a slightly tricky data structure containing
424 the ticket's attributes, a list of the linked customers, each transaction's
425 content, description, and create time.
427 Accepts the following options:
440 my($self, $session, %param) = @_;
442 $session = $self->session($session);
444 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
445 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
446 return 'Could not load ticket' unless $ticketid;
449 foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
450 my $cust = $link->Target;
451 push @custs, $1 if $cust =~ /\/(\d+)$/;
455 my $transactions = $Ticket->Transactions;
456 while ( my $transaction = $transactions->Next ) {
457 my $t = { created => $transaction->Created,
458 content => $transaction->Content,
459 description => $transaction->Description,
460 type => $transaction->Type,
467 fields => _ticket_info($Ticket),
471 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
473 Class method. Retrieve the RT::Ticket object with the specified
474 ticket_id. If custnum is supplied, will also check that the object
475 is a member of that customer. If there is no ticket or the custnum
476 check fails, returns nothing. The meaning of that case is
477 "to this customer, the ticket does not exist".
491 sub get_ticket_object {
493 my ($session, %opt) = @_;
494 $session = $self->session(shift);
495 # use a small search here so we can check ticket ownership
497 if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
502 if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
503 $query .= " AND Customer.number = $1"; # also checks ownership via services
505 my $Tickets = RT::Tickets->new($session->{CurrentUser});
506 $Tickets->FromSQL($query);
507 return $Tickets->First;
510 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
512 Class method. Correspond on a ticket. If there is an error, returns the scalar
513 error. Otherwise, returns the transaction id, error message, and
514 RT::Transaction object.
516 Accepts the following options:
526 Correspondence content
532 sub correspond_ticket {
533 my($self, $session, %param) = @_;
535 $session = $self->session($session);
537 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
538 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
539 return 'Could not load ticket' unless $ticketid;
540 return 'No content' unless $param{'content'};
542 $Ticket->Correspond( Content => $param{'content'} );
545 =item queues SESSION_HASHREF [, ACL ]
547 Retrieve a list of queues. Pass the name of an RT access control right,
548 such as 'CreateTicket', to return only queues on which the current user
549 has that right. Otherwise this will return all queues with the 'SeeQueue'
555 my( $self, $session, $acl ) = @_;
556 $session = $self->session($session);
558 my $showall = $acl ? 0 : 1;
560 my $q = new RT::Queues($session->{'CurrentUser'});
562 while (my $queue = $q->Next) {
563 if ($showall || $queue->CurrentUserHasRight($acl)) {
566 Name => $queue->Name,
567 Description => $queue->Description,
571 return map { $_->{Id} => $_->{Name} } @result;
574 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
575 # to get logged into RT from afar
576 sub _web_external_auth {
577 my( $self, $session ) = @_;
579 my $user = $FS::CurrentUser::CurrentUser->username;
581 eval 'use RT::CurrentUser;';
585 $session->{'CurrentUser'} = RT::CurrentUser->new();
587 warn "$me _web_external_auth loading RT user for $user\n"
590 $session->{'CurrentUser'}->Load($user);
592 if ( ! $session->{'CurrentUser'}->Id() ) {
594 # Create users on-the-fly
596 warn "can't load RT user for $user; auto-creating\n"
599 my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
601 my ( $val, $msg ) = $UserObj->Create(
602 %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
609 # now get user specific information, to better create our user.
611 = RT::Interface::Web::WebExternalAutoInfo($user);
613 # set the attributes that have been defined.
614 # FIXME: this is a horrible kludge. I'm sure there's something cleaner
615 foreach my $attribute (
617 'Signature', 'EmailAddress',
618 'PagerEmailAddress', 'FreeformContactInfo',
619 'Organization', 'Disabled',
620 'Privileged', 'RealName',
622 'EmailEncoding', 'WebEncoding',
623 'ExternalContactInfoId', 'ContactInfoSystem',
624 'ExternalAuthId', 'Gecos',
625 'HomePhone', 'WorkPhone',
626 'MobilePhone', 'PagerPhone',
627 'Address1', 'Address2',
633 #$m->comp( '/Elements/Callback', %ARGS,
634 # _CallbackName => 'NewUser' );
636 my $method = "Set$attribute";
637 $UserObj->$method( $new_user_info->{$attribute} )
638 if ( defined $new_user_info->{$attribute} );
640 $session->{'CurrentUser'}->Load($user);
644 # we failed to successfully create the user. abort abort abort.
645 delete $session->{'CurrentUser'};
647 die "can't auto-create RT user: $msg"; #an error message would be nice :/
648 #$m->abort() unless $RT::WebFallbackToInternalAuth;
649 #$m->comp( '/Elements/Login', %ARGS,
650 # Error => loc( 'Cannot create user: [_1]', $msg ) );
654 unless ( $session->{'CurrentUser'}->Id() ) {
655 delete $session->{'CurrentUser'};
657 die "can't auto-create RT user";
660 #if ($RT::WebExternalOnly) {
661 # $m->comp( '/Elements/Login', %ARGS,
662 # Error => loc('You are not an authorized user') );
671 =item selfservice_priority
673 Returns the configured self-service priority field.
677 my $selfservice_priority;
679 sub selfservice_priority {
680 return $selfservice_priority ||= do {
681 my $conf = FS::Conf->new;
682 $conf->config('ticket_system-selfservice_priority_field') || '';
688 Returns a hash of custom field names and descriptions.
690 Accepts the following options:
692 lookuptype - limit results to this lookuptype
694 valuetype - limit results to this valuetype
696 Fields must be visible to CurrentUser.
703 my $lookuptype = $opt{lookuptype};
704 my $valuetype = $opt{valuetype};
706 my $CurrentUser = RT::CurrentUser->new();
707 $CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
708 die "RT not configured" unless $CurrentUser->id;
709 my $CFs = RT::CustomFields->new($CurrentUser);
713 $CFs->Limit(FIELD => 'LookupType',
714 OPERATOR => 'ENDSWITH',
715 VALUE => $lookuptype)
718 $CFs->Limit(FIELD => 'Type',
723 while (my $CF = $CFs->Next) {
724 push @fields, $CF->Name, ($CF->Description || $CF->Name);