1 package FS::TicketSystem::RT_Internal;
4 use vars qw( @ISA $DEBUG $me );
6 use Date::Format qw( time2str );
10 use FS::CGI qw(popurl);
11 use FS::TicketSystem::RT_Libs;
13 @ISA = qw( FS::TicketSystem::RT_Libs );
16 $me = '[FS::TicketSystem::RT_Internal]';
18 sub sql_num_customer_tickets {
19 "( select count(*) from Tickets
20 join Links on ( Tickets.id = Links.LocalBase )
21 where ( Status = 'new' or Status = 'open' or Status = 'stalled' )
22 and Target = 'freeside://freeside/cust_main/' || custnum
28 if ( $RT::URI::freeside::URL ) {
29 $RT::URI::freeside::URL. '/rt/';
31 'http://you_need_to_set_RT_URI_freeside_URL_in_SiteConfig.pm/';
36 #ShowConfigTab ModifySelf
38 my( $self, $session, $right ) = @_;
40 return '' unless FS::Conf->new->config('ticket_system');
42 $session = $self->session($session);
44 #warn "$me access_right: CurrentUser ". $session->{'CurrentUser'}. ":\n".
45 # ( $DEBUG>1 ? Dumper($session->{'CurrentUser'}) : '' )
48 $session->{'CurrentUser'}->HasRight( Right => $right,
49 Object => $RT::System );
53 my( $self, $session ) = @_;
55 if ( $session && $session->{'CurrentUser'} ) { # does this even work?
56 warn "$me session: using existing session and CurrentUser: \n".
57 Dumper($session->{'CurrentUser'})
60 warn "$me session: loading session and CurrentUser\n" if $DEBUG > 1;
61 $session = $self->_web_external_auth($session);
73 # this part only needs to be done once
74 warn "$me init: loading RT libraries\n" if $DEBUG;
76 use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
79 #for web external auth...
80 use RT::Interface::Web;
84 warn "$me init: loading RT config\n" if $DEBUG;
87 eval 'RT::LoadConfig();';
94 # this needs to be done on each fork
95 warn "$me init: initializing RT\n" if $DEBUG;
99 eval 'RT::Init("NoSignalHandlers"=>1);';
103 warn "$me init: complete" if $DEBUG;
106 =item customer_tickets CUSTNUM [ PARAMS ]
108 Replacement for the one in RT_External so that we can access custom fields
109 properly. Accepts a hashref with the following parameters:
111 number - custnum/svcnum
121 resolved - only return tickets resolved after this timestamp
125 # create an RT::Tickets object for a specified custnum or svcnum
127 sub _tickets_search {
131 my( $number, $limit, $priority, $status, $queueid, $opt );
132 if ( ref($_[0]) eq 'HASH' ) {
134 $number = $$opt{'number'};
135 $limit = $$opt{'limit'};
136 $priority = $$opt{'priority'};
137 $status = $$opt{'status'};
138 $queueid = $$opt{'queueid'};
140 ( $number, $limit, $priority, $status, $queueid ) = @_;
144 $type =~ /^Customer|Service$/ or die "invalid type: $type";
145 $number =~ /^\d+$/ or die "invalid custnum/svcnum: $number";
146 $limit =~ /^\d+$/ or die "invalid limit: $limit";
148 my $session = $self->session();
149 my $CurrentUser = $session->{CurrentUser}
150 or die "unable to create an RT session";
152 my $Tickets = RT::Tickets->new($CurrentUser);
154 # "Customer.number" searches tickets linked via cust_svc also
155 my $rtql = "$type.number = $number";
157 if ( defined( $priority ) ) {
158 my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field');
159 if ( length( $priority ) ) {
160 $rtql .= " AND CF.{$custom_priority} = '$priority'";
163 $rtql .= " AND CF.{$custom_priority} IS NULL";
168 if ( defined($status) && $status ) {
169 if ( ref($status) ) {
170 if ( ref($status) eq 'HASH' ) {
171 @statuses = grep $status->{$_}, keys %$status;
172 } elsif ( ref($status) eq 'ARRAY' ) {
173 @statuses = @$status;
175 #what should be the failure mode here? die? return no tickets?
176 die 'unknown status ref '. ref($status);
179 @statuses = ( $status );
181 @statuses = grep /^\w+$/, @statuses; #injection prevention
183 @statuses = $self->statuses;
187 join(' OR ', map { "Status = '$_'" } @statuses).
190 $rtql .= " AND Queue = $queueid " if $queueid;
192 if ($$opt{'resolved'}) {
193 $rtql .= " AND Resolved >= " . dbh->quote(time2str('%Y-%m-%d %H:%M:%S',$$opt{'resolved'}));
196 warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
197 $Tickets->FromSQL($rtql);
199 $Tickets->RowsPerPage($limit);
200 warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
205 sub href_customer_tickets {
206 my ($self, $custnum) = (shift, shift);
207 if ($custnum =~ /^(\d+)$/) {
208 return $self->href_search_tickets("Customer.number = $custnum", @_);
210 warn "bad custnum $custnum"; '';
213 sub href_service_tickets {
214 my ($self, $svcnum) = (shift, shift);
215 if ($svcnum =~ /^(\d+)$/ ) {
216 return $self->href_search_tickets("Service.number = $svcnum", @_);
218 warn "bad svcnum $svcnum"; '';
221 sub customer_tickets {
223 my $Tickets = $self->_tickets_search('Customer', @_);
225 my $conf = FS::Conf->new;
227 $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
230 { FIELD => 'Priority', ORDER => $priority_order },
231 { FIELD => 'Id', ORDER => 'DESC' },
234 $Tickets->OrderByCols(@order_by);
237 while ( my $t = $Tickets->Next ) {
238 push @tickets, _ticket_info($t);
244 sub num_customer_tickets {
245 my ( $self, $custnum, $priority ) = @_;
246 $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll;
249 sub service_tickets {
251 my $Tickets = $self->_tickets_search('Service', @_);
253 my $conf = FS::Conf->new;
255 $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
258 { FIELD => 'Priority', ORDER => $priority_order },
259 { FIELD => 'Id', ORDER => 'DESC' },
262 $Tickets->OrderByCols(@order_by);
265 while ( my $t = $Tickets->Next ) {
266 push @tickets, _ticket_info($t);
273 # Takes an RT::Ticket; returns a hashref of the ticket's fields, including
274 # custom fields. Also returns custom and selfservice priority values as
275 # _custom_priority and _selfservice_priority, and the IsUnreplied property
279 my $custom_priority =
280 FS::Conf->new->config('ticket_system-custom_priority_field') || '';
281 my $ss_priority = selfservice_priority();
284 foreach my $name ( $t->ReadableAttributes ) {
285 # lowercase names, and skip attributes with non-scalar values
286 $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
288 $ticket_info{'owner'} = $t->OwnerObj->Name;
289 $ticket_info{'queue'} = $t->QueueObj->Name;
290 $ticket_info{'_cf_sort_order'} = {};
292 foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
293 $ticket_info{'_cf_sort_order'}{$CF->Name} = $cf_sort++;
294 my $name = 'CF.{'.$CF->Name.'}';
295 $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
297 # make this easy to find
298 if ( $custom_priority ) {
299 $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
301 if ( $ss_priority ) {
302 $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
304 $ticket_info{'is_unreplied'} = $t->IsUnreplied;
306 map { $_->Target =~ /cust_svc\/(\d+)/; $1 }
307 @{ $t->Services->ItemsArrayRef }
309 $ticket_info{'svcnums'} = $svcnums;
311 return \%ticket_info;
314 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
316 Class method. Creates a ticket. If there is an error, returns the scalar
317 error, otherwise returns the newly created RT::Ticket object.
319 Accepts the following options:
333 Requestor email address or arrayref of addresses
337 Cc: email address or arrayref of addresses
345 MIME type to use for message. Defaults to text/plain. Specifying text/html
346 can be useful to use HTML markup in message.
350 Customer number (see L<FS::cust_main>) to associate with ticket.
354 Service number (see L<FS::cust_svc>) to associate with ticket. Will also
355 associate the customer who has this service (unless the service is unlinked).
362 my($self, $session, %param) = @_;
364 $session = $self->session($session);
366 my $Queue = RT::Queue->new($session->{'CurrentUser'});
367 $Queue->Load( $param{'queue'} );
369 my $req = ref($param{'requestor'})
370 ? $param{'requestor'}
371 : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
373 my $cc = ref($param{'cc'})
375 : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
377 my $mimeobj = MIME::Entity->build(
378 'Data' => Encode::encode_utf8( $param{'message'} ),
379 'Type' => ( $param{'mime_type'} || 'text/plain' ),
383 'Queue' => $Queue->Id,
384 'Subject' => $param{'subject'},
387 'MIMEObj' => $mimeobj,
389 warn Dumper(\%ticket) if $DEBUG > 1;
391 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
392 my( $id, $Transaction, $ErrStr );
395 ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
397 return $ErrStr if $id == 0;
399 warn "ticket got id $id\n" if $DEBUG;
401 #XXX check errors adding custnum/svcnum links (put it in a transaction)...
402 # but we do already know they're good
404 if ( $param{'custnum'} ) {
405 my( $val, $msg ) = $Ticket->_AddLink(
406 'Type' => 'MemberOf',
407 'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
411 if ( $param{'svcnum'} ) {
412 my( $val, $msg ) = $Ticket->_AddLink(
413 'Type' => 'MemberOf',
414 'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
421 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
423 Class method. Retrieves a ticket. If there is an error, returns the scalar
424 error. Otherwise, currently returns a slightly tricky data structure containing
425 the ticket's attributes, a list of the linked customers, each transaction's
426 content, description, and create time.
428 Accepts the following options:
441 my($self, $session, %param) = @_;
443 $session = $self->session($session);
445 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
446 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
447 return 'Could not load ticket' unless $ticketid;
450 foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
451 my $cust = $link->Target;
452 push @custs, $1 if $cust =~ /\/(\d+)$/;
456 my $transactions = $Ticket->Transactions;
457 while ( my $transaction = $transactions->Next ) {
458 my $t = { created => $transaction->Created,
459 content => $transaction->Content,
460 description => $transaction->Description,
461 type => $transaction->Type,
468 fields => _ticket_info($Ticket),
472 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
474 Class method. Retrieve the RT::Ticket object with the specified
475 ticket_id. If custnum is supplied, will also check that the object
476 is a member of that customer. If there is no ticket or the custnum
477 check fails, returns nothing. The meaning of that case is
478 "to this customer, the ticket does not exist".
492 sub get_ticket_object {
494 my ($session, %opt) = @_;
495 $session = $self->session(shift);
496 # use a small search here so we can check ticket ownership
498 if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
503 if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
504 $query .= " AND Customer.number = $1"; # also checks ownership via services
506 my $Tickets = RT::Tickets->new($session->{CurrentUser});
507 $Tickets->FromSQL($query);
508 return $Tickets->First;
511 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
513 Class method. Correspond on a ticket. If there is an error, returns the scalar
514 error. Otherwise, returns the transaction id, error message, and
515 RT::Transaction object.
517 Accepts the following options:
527 Correspondence content
533 sub correspond_ticket {
534 my($self, $session, %param) = @_;
536 $session = $self->session($session);
538 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
539 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
540 return 'Could not load ticket' unless $ticketid;
541 return 'No content' unless $param{'content'};
543 $Ticket->Correspond( Content => $param{'content'} );
546 =item queues SESSION_HASHREF [, ACL ]
548 Retrieve a list of queues. Pass the name of an RT access control right,
549 such as 'CreateTicket', to return only queues on which the current user
550 has that right. Otherwise this will return all queues with the 'SeeQueue'
556 my( $self, $session, $acl ) = @_;
557 $session = $self->session($session);
559 my $showall = $acl ? 0 : 1;
561 my $q = new RT::Queues($session->{'CurrentUser'});
563 while (my $queue = $q->Next) {
564 if ($showall || $queue->CurrentUserHasRight($acl)) {
567 Name => $queue->Name,
568 Description => $queue->Description,
572 return map { $_->{Id} => $_->{Name} } @result;
575 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
576 # to get logged into RT from afar
577 sub _web_external_auth {
578 my( $self, $session ) = @_;
580 my $user = $FS::CurrentUser::CurrentUser->username;
582 eval 'use RT::CurrentUser;';
586 $session->{'CurrentUser'} = RT::CurrentUser->new();
588 warn "$me _web_external_auth loading RT user for $user\n"
591 $session->{'CurrentUser'}->Load($user);
593 if ( ! $session->{'CurrentUser'}->Id() ) {
595 # Create users on-the-fly
597 warn "can't load RT user for $user; auto-creating\n"
600 my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
602 my ( $val, $msg ) = $UserObj->Create(
603 %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
610 # now get user specific information, to better create our user.
612 = RT::Interface::Web::WebExternalAutoInfo($user);
614 # set the attributes that have been defined.
615 # FIXME: this is a horrible kludge. I'm sure there's something cleaner
616 foreach my $attribute (
618 'Signature', 'EmailAddress',
619 'PagerEmailAddress', 'FreeformContactInfo',
620 'Organization', 'Disabled',
621 'Privileged', 'RealName',
623 'EmailEncoding', 'WebEncoding',
624 'ExternalContactInfoId', 'ContactInfoSystem',
625 'ExternalAuthId', 'Gecos',
626 'HomePhone', 'WorkPhone',
627 'MobilePhone', 'PagerPhone',
628 'Address1', 'Address2',
634 #$m->comp( '/Elements/Callback', %ARGS,
635 # _CallbackName => 'NewUser' );
637 my $method = "Set$attribute";
638 $UserObj->$method( $new_user_info->{$attribute} )
639 if ( defined $new_user_info->{$attribute} );
641 $session->{'CurrentUser'}->Load($user);
645 # we failed to successfully create the user. abort abort abort.
646 delete $session->{'CurrentUser'};
648 die "can't auto-create RT user: $msg"; #an error message would be nice :/
649 #$m->abort() unless $RT::WebFallbackToInternalAuth;
650 #$m->comp( '/Elements/Login', %ARGS,
651 # Error => loc( 'Cannot create user: [_1]', $msg ) );
655 unless ( $session->{'CurrentUser'}->Id() ) {
656 delete $session->{'CurrentUser'};
658 die "can't auto-create RT user";
661 #if ($RT::WebExternalOnly) {
662 # $m->comp( '/Elements/Login', %ARGS,
663 # Error => loc('You are not an authorized user') );
672 =item selfservice_priority
674 Returns the configured self-service priority field.
678 my $selfservice_priority;
680 sub selfservice_priority {
681 return $selfservice_priority ||= do {
682 my $conf = FS::Conf->new;
683 $conf->config('ticket_system-selfservice_priority_field') || '';
689 Returns a hash of custom field names and descriptions.
691 Accepts the following options:
693 lookuptype - limit results to this lookuptype
695 valuetype - limit results to this valuetype
697 Fields must be visible to CurrentUser.
704 my $lookuptype = $opt{lookuptype};
705 my $valuetype = $opt{valuetype};
707 my $CurrentUser = RT::CurrentUser->new();
708 $CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
709 die "RT not configured" unless $CurrentUser->id;
710 my $CFs = RT::CustomFields->new($CurrentUser);
714 $CFs->Limit(FIELD => 'LookupType',
715 OPERATOR => 'ENDSWITH',
716 VALUE => $lookuptype)
719 $CFs->Limit(FIELD => 'Type',
724 while (my $CF = $CFs->Next) {
725 push @fields, $CF->Name, ($CF->Description || $CF->Name);