1 package FS::TicketSystem::RT_Internal;
4 use vars qw( @ISA $DEBUG $me );
8 use FS::CGI qw(popurl);
9 use FS::TicketSystem::RT_Libs;
11 @ISA = qw( FS::TicketSystem::RT_Libs );
14 $me = '[FS::TicketSystem::RT_Internal]';
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
26 if ( $RT::URI::freeside::URL ) {
27 $RT::URI::freeside::URL. '/rt/';
29 'http://you_need_to_set_RT_URI_freeside_URL_in_SiteConfig.pm/';
34 #ShowConfigTab ModifySelf
36 my( $self, $session, $right ) = @_;
38 return '' unless FS::Conf->new->config('ticket_system');
40 $session = $self->session($session);
42 #warn "$me access_right: CurrentUser ". $session->{'CurrentUser'}. ":\n".
43 # ( $DEBUG>1 ? Dumper($session->{'CurrentUser'}) : '' )
46 $session->{'CurrentUser'}->HasRight( Right => $right,
47 Object => $RT::System );
51 my( $self, $session ) = @_;
53 if ( $session && $session->{'CurrentUser'} ) { # does this even work?
54 warn "$me session: using existing session and CurrentUser: \n".
55 Dumper($session->{'CurrentUser'})
58 warn "$me session: loading session and CurrentUser\n" if $DEBUG > 1;
59 $session = $self->_web_external_auth($session);
71 # this part only needs to be done once
72 warn "$me init: loading RT libraries\n" if $DEBUG;
74 use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
77 #for web external auth...
78 use RT::Interface::Web;
82 warn "$me init: loading RT config\n" if $DEBUG;
85 eval 'RT::LoadConfig();';
92 # this needs to be done on each fork
93 warn "$me init: initializing RT\n" if $DEBUG;
97 eval 'RT::Init("NoSignalHandlers"=>1);';
101 warn "$me init: complete" if $DEBUG;
104 =item customer_tickets CUSTNUM [ LIMIT ] [ PRIORITYVALUE ]
106 Replacement for the one in RT_External so that we can access custom fields
111 # create an RT::Tickets object for a specified custnum or svcnum
113 sub _tickets_search {
114 my( $self, $type, $number, $limit, $priority, $status, $queueid ) = @_;
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";
120 my $session = $self->session();
121 my $CurrentUser = $session->{CurrentUser}
122 or die "unable to create an RT session";
124 my $Tickets = RT::Tickets->new($CurrentUser);
126 # "Customer.number" searches tickets linked via cust_svc also
127 my $rtql = "$type.number = $number";
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'";
135 $rtql .= " AND CF.{$custom_priority} IS NULL";
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;
147 #what should be the failure mode here? die? return no tickets?
148 die 'unknown status ref '. ref($status);
151 @statuses = ( $status );
153 @statuses = grep /^\w+$/, @statuses; #injection prevention
155 @statuses = $self->statuses;
159 join(' OR ', map { "Status = '$_'" } @statuses).
162 $rtql .= " AND Queue = $queueid " if $queueid;
164 warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
165 $Tickets->FromSQL($rtql);
167 $Tickets->RowsPerPage($limit);
168 warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
173 sub href_customer_tickets {
174 my ($self, $custnum) = (shift, shift);
175 if ($custnum =~ /^(\d+)$/) {
176 return $self->href_search_tickets("Customer.number = $custnum", @_);
178 warn "bad custnum $custnum"; '';
181 sub href_service_tickets {
182 my ($self, $svcnum) = (shift, shift);
183 if ($svcnum =~ /^(\d+)$/ ) {
184 return $self->href_search_tickets("Service.number = $svcnum", @_);
186 warn "bad svcnum $svcnum"; '';
189 sub customer_tickets {
191 my $Tickets = $self->_tickets_search('Customer', @_);
193 my $conf = FS::Conf->new;
195 $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
198 { FIELD => 'Priority', ORDER => $priority_order },
199 { FIELD => 'Id', ORDER => 'DESC' },
202 $Tickets->OrderByCols(@order_by);
205 while ( my $t = $Tickets->Next ) {
206 push @tickets, _ticket_info($t);
212 sub num_customer_tickets {
213 my ( $self, $custnum, $priority ) = @_;
214 $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll;
217 sub service_tickets {
219 my $Tickets = $self->_tickets_search('Service', @_);
221 my $conf = FS::Conf->new;
223 $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
226 { FIELD => 'Priority', ORDER => $priority_order },
227 { FIELD => 'Id', ORDER => 'DESC' },
230 $Tickets->OrderByCols(@order_by);
233 while ( my $t = $Tickets->Next ) {
234 push @tickets, _ticket_info($t);
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
247 my $custom_priority =
248 FS::Conf->new->config('ticket_system-custom_priority_field') || '';
249 my $ss_priority = selfservice_priority();
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);
256 $ticket_info{'owner'} = $t->OwnerObj->Name;
257 $ticket_info{'queue'} = $t->QueueObj->Name;
258 $ticket_info{'_cf_sort_order'} = {};
260 foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
261 $ticket_info{'_cf_sort_order'}{$CF->Name} = $cf_sort++;
262 my $name = 'CF.{'.$CF->Name.'}';
263 $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
265 # make this easy to find
266 if ( $custom_priority ) {
267 $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
269 if ( $ss_priority ) {
270 $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
272 $ticket_info{'is_unreplied'} = $t->IsUnreplied;
274 map { $_->Target =~ /cust_svc\/(\d+)/; $1 }
275 @{ $t->Services->ItemsArrayRef }
277 $ticket_info{'svcnums'} = $svcnums;
279 return \%ticket_info;
282 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
284 Class method. Creates a ticket. If there is an error, returns the scalar
285 error, otherwise returns the newly created RT::Ticket object.
287 Accepts the following options:
301 Requestor email address or arrayref of addresses
305 Cc: email address or arrayref of addresses
313 MIME type to use for message. Defaults to text/plain. Specifying text/html
314 can be useful to use HTML markup in message.
318 Customer number (see L<FS::cust_main>) to associate with ticket.
322 Service number (see L<FS::cust_svc>) to associate with ticket. Will also
323 associate the customer who has this service (unless the service is unlinked).
330 my($self, $session, %param) = @_;
332 $session = $self->session($session);
334 my $Queue = RT::Queue->new($session->{'CurrentUser'});
335 $Queue->Load( $param{'queue'} );
337 my $req = ref($param{'requestor'})
338 ? $param{'requestor'}
339 : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
341 my $cc = ref($param{'cc'})
343 : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
345 my $mimeobj = MIME::Entity->build(
346 'Data' => $param{'message'},
347 'Type' => ( $param{'mime_type'} || 'text/plain' ),
351 'Queue' => $Queue->Id,
352 'Subject' => $param{'subject'},
355 'MIMEObj' => $mimeobj,
357 warn Dumper(\%ticket) if $DEBUG > 1;
359 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
360 my( $id, $Transaction, $ErrStr );
363 ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
365 return $ErrStr if $id == 0;
367 warn "ticket got id $id\n" if $DEBUG;
369 #XXX check errors adding custnum/svcnum links (put it in a transaction)...
370 # but we do already know they're good
372 if ( $param{'custnum'} ) {
373 my( $val, $msg ) = $Ticket->_AddLink(
374 'Type' => 'MemberOf',
375 'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
379 if ( $param{'svcnum'} ) {
380 my( $val, $msg ) = $Ticket->_AddLink(
381 'Type' => 'MemberOf',
382 'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
389 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
391 Class method. Retrieves a ticket. If there is an error, returns the scalar
392 error. Otherwise, currently returns a slightly tricky data structure containing
393 the ticket's attributes, a list of the linked customers, each transaction's
394 content, description, and create time.
396 Accepts the following options:
409 my($self, $session, %param) = @_;
411 $session = $self->session($session);
413 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
414 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
415 return 'Could not load ticket' unless $ticketid;
418 foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
419 my $cust = $link->Target;
420 push @custs, $1 if $cust =~ /\/(\d+)$/;
424 my $transactions = $Ticket->Transactions;
425 while ( my $transaction = $transactions->Next ) {
426 my $t = { created => $transaction->Created,
427 content => $transaction->Content,
428 description => $transaction->Description,
429 type => $transaction->Type,
436 fields => _ticket_info($Ticket),
440 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
442 Class method. Retrieve the RT::Ticket object with the specified
443 ticket_id. If custnum is supplied, will also check that the object
444 is a member of that customer. If there is no ticket or the custnum
445 check fails, returns nothing. The meaning of that case is
446 "to this customer, the ticket does not exist".
460 sub get_ticket_object {
462 my ($session, %opt) = @_;
463 $session = $self->session(shift);
464 # use a small search here so we can check ticket ownership
466 if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
471 if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
472 $query .= " AND Customer.number = $1"; # also checks ownership via services
474 my $Tickets = RT::Tickets->new($session->{CurrentUser});
475 $Tickets->FromSQL($query);
476 return $Tickets->First;
479 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
481 Class method. Correspond on a ticket. If there is an error, returns the scalar
482 error. Otherwise, returns the transaction id, error message, and
483 RT::Transaction object.
485 Accepts the following options:
495 Correspondence content
501 sub correspond_ticket {
502 my($self, $session, %param) = @_;
504 $session = $self->session($session);
506 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
507 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
508 return 'Could not load ticket' unless $ticketid;
509 return 'No content' unless $param{'content'};
511 $Ticket->Correspond( Content => $param{'content'} );
514 =item queues SESSION_HASHREF [, ACL ]
516 Retrieve a list of queues. Pass the name of an RT access control right,
517 such as 'CreateTicket', to return only queues on which the current user
518 has that right. Otherwise this will return all queues with the 'SeeQueue'
524 my( $self, $session, $acl ) = @_;
525 $session = $self->session($session);
527 my $showall = $acl ? 0 : 1;
529 my $q = new RT::Queues($session->{'CurrentUser'});
531 while (my $queue = $q->Next) {
532 if ($showall || $queue->CurrentUserHasRight($acl)) {
535 Name => $queue->Name,
536 Description => $queue->Description,
540 return map { $_->{Id} => $_->{Name} } @result;
543 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
544 # to get logged into RT from afar
545 sub _web_external_auth {
546 my( $self, $session ) = @_;
548 my $user = $FS::CurrentUser::CurrentUser->username;
550 eval 'use RT::CurrentUser;';
554 $session->{'CurrentUser'} = RT::CurrentUser->new();
556 warn "$me _web_external_auth loading RT user for $user\n"
559 $session->{'CurrentUser'}->Load($user);
561 if ( ! $session->{'CurrentUser'}->Id() ) {
563 # Create users on-the-fly
565 warn "can't load RT user for $user; auto-creating\n"
568 my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
570 my ( $val, $msg ) = $UserObj->Create(
571 %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
578 # now get user specific information, to better create our user.
580 = RT::Interface::Web::WebRemoteUserAutocreateInfo($user);
582 # set the attributes that have been defined.
583 # FIXME: this is a horrible kludge. I'm sure there's something cleaner
584 foreach my $attribute (
586 'Signature', 'EmailAddress',
587 'PagerEmailAddress', 'FreeformContactInfo',
588 'Organization', 'Disabled',
589 'Privileged', 'RealName',
591 'EmailEncoding', 'WebEncoding',
592 'ExternalContactInfoId', 'ContactInfoSystem',
593 'ExternalAuthId', 'Gecos',
594 'HomePhone', 'WorkPhone',
595 'MobilePhone', 'PagerPhone',
596 'Address1', 'Address2',
602 #$m->comp( '/Elements/Callback', %ARGS,
603 # _CallbackName => 'NewUser' );
605 my $method = "Set$attribute";
606 $UserObj->$method( $new_user_info->{$attribute} )
607 if ( defined $new_user_info->{$attribute} );
609 $session->{'CurrentUser'}->Load($user);
613 # we failed to successfully create the user. abort abort abort.
614 delete $session->{'CurrentUser'};
616 die "can't auto-create RT user: $msg"; #an error message would be nice :/
617 #$m->abort() unless $RT::WebFallbackToInternalAuth;
618 #$m->comp( '/Elements/Login', %ARGS,
619 # Error => loc( 'Cannot create user: [_1]', $msg ) );
623 unless ( $session->{'CurrentUser'}->Id() ) {
624 delete $session->{'CurrentUser'};
626 die "can't auto-create RT user";
629 #if ($RT::WebExternalOnly) {
630 # $m->comp( '/Elements/Login', %ARGS,
631 # Error => loc('You are not an authorized user') );
640 =item selfservice_priority
642 Returns the configured self-service priority field.
646 my $selfservice_priority;
648 sub selfservice_priority {
649 return $selfservice_priority ||= do {
650 my $conf = FS::Conf->new;
651 $conf->config('ticket_system-selfservice_priority_field') || '';
657 Returns a hash of custom field names and descriptions.
659 Accepts the following options:
661 lookuptype - limit results to this lookuptype
663 valuetype - limit results to this valuetype
665 Fields must be visible to CurrentUser.
672 my $lookuptype = $opt{lookuptype};
673 my $valuetype = $opt{valuetype};
675 my $CurrentUser = RT::CurrentUser->new();
676 $CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
677 die "RT not configured" unless $CurrentUser->id;
678 my $CFs = RT::CustomFields->new($CurrentUser);
682 $CFs->Limit(FIELD => 'LookupType',
683 OPERATOR => 'ENDSWITH',
684 VALUE => $lookuptype)
687 $CFs->Limit(FIELD => 'Type',
692 while (my $CF = $CFs->Next) {
693 push @fields, $CF->Name, ($CF->Description || $CF->Name);