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 foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
259 my $name = 'CF.{'.$CF->Name.'}';
260 $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
262 # make this easy to find
263 if ( $custom_priority ) {
264 $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
266 if ( $ss_priority ) {
267 $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
269 $ticket_info{'is_unreplied'} = $t->IsUnreplied;
271 map { $_->Target =~ /cust_svc\/(\d+)/; $1 }
272 @{ $t->Services->ItemsArrayRef }
274 $ticket_info{'svcnums'} = $svcnums;
276 return \%ticket_info;
279 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
281 Class method. Creates a ticket. If there is an error, returns the scalar
282 error, otherwise returns the newly created RT::Ticket object.
284 Accepts the following options:
298 Requestor email address or arrayref of addresses
302 Cc: email address or arrayref of addresses
310 MIME type to use for message. Defaults to text/plain. Specifying text/html
311 can be useful to use HTML markup in message.
315 Customer number (see L<FS::cust_main>) to associate with ticket.
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).
327 my($self, $session, %param) = @_;
329 $session = $self->session($session);
331 my $Queue = RT::Queue->new($session->{'CurrentUser'});
332 $Queue->Load( $param{'queue'} );
334 my $req = ref($param{'requestor'})
335 ? $param{'requestor'}
336 : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
338 my $cc = ref($param{'cc'})
340 : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
342 my $mimeobj = MIME::Entity->build(
343 'Data' => $param{'message'},
344 'Type' => ( $param{'mime_type'} || 'text/plain' ),
348 'Queue' => $Queue->Id,
349 'Subject' => $param{'subject'},
352 'MIMEObj' => $mimeobj,
354 warn Dumper(\%ticket) if $DEBUG > 1;
356 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
357 my( $id, $Transaction, $ErrStr );
360 ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
362 return $ErrStr if $id == 0;
364 warn "ticket got id $id\n" if $DEBUG;
366 #XXX check errors adding custnum/svcnum links (put it in a transaction)...
367 # but we do already know they're good
369 if ( $param{'custnum'} ) {
370 my( $val, $msg ) = $Ticket->_AddLink(
371 'Type' => 'MemberOf',
372 'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
376 if ( $param{'svcnum'} ) {
377 my( $val, $msg ) = $Ticket->_AddLink(
378 'Type' => 'MemberOf',
379 'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
386 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
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.
393 Accepts the following options:
406 my($self, $session, %param) = @_;
408 $session = $self->session($session);
410 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
411 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
412 return 'Could not load ticket' unless $ticketid;
415 foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
416 my $cust = $link->Target;
417 push @custs, $1 if $cust =~ /\/(\d+)$/;
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,
433 fields => _ticket_info($Ticket),
437 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
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".
457 sub get_ticket_object {
459 my ($session, %opt) = @_;
460 $session = $self->session(shift);
461 # use a small search here so we can check ticket ownership
463 if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
468 if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
469 $query .= " AND Customer.number = $1"; # also checks ownership via services
471 my $Tickets = RT::Tickets->new($session->{CurrentUser});
472 $Tickets->FromSQL($query);
473 return $Tickets->First;
476 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
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.
482 Accepts the following options:
492 Correspondence content
498 sub correspond_ticket {
499 my($self, $session, %param) = @_;
501 $session = $self->session($session);
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'};
508 $Ticket->Correspond( Content => $param{'content'} );
511 =item queues SESSION_HASHREF [, ACL ]
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'
521 my( $self, $session, $acl ) = @_;
522 $session = $self->session($session);
524 my $showall = $acl ? 0 : 1;
526 my $q = new RT::Queues($session->{'CurrentUser'});
528 while (my $queue = $q->Next) {
529 if ($showall || $queue->CurrentUserHasRight($acl)) {
532 Name => $queue->Name,
533 Description => $queue->Description,
537 return map { $_->{Id} => $_->{Name} } @result;
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 ) = @_;
545 my $user = $FS::CurrentUser::CurrentUser->username;
547 eval 'use RT::CurrentUser;';
551 $session->{'CurrentUser'} = RT::CurrentUser->new();
553 warn "$me _web_external_auth loading RT user for $user\n"
556 $session->{'CurrentUser'}->Load($user);
558 if ( ! $session->{'CurrentUser'}->Id() ) {
560 # Create users on-the-fly
562 warn "can't load RT user for $user; auto-creating\n"
565 my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
567 my ( $val, $msg ) = $UserObj->Create(
568 %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
575 # now get user specific information, to better create our user.
577 = RT::Interface::Web::WebRemoteUserAutocreateInfo($user);
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 (
583 'Signature', 'EmailAddress',
584 'PagerEmailAddress', 'FreeformContactInfo',
585 'Organization', 'Disabled',
586 'Privileged', 'RealName',
588 'EmailEncoding', 'WebEncoding',
589 'ExternalContactInfoId', 'ContactInfoSystem',
590 'ExternalAuthId', 'Gecos',
591 'HomePhone', 'WorkPhone',
592 'MobilePhone', 'PagerPhone',
593 'Address1', 'Address2',
599 #$m->comp( '/Elements/Callback', %ARGS,
600 # _CallbackName => 'NewUser' );
602 my $method = "Set$attribute";
603 $UserObj->$method( $new_user_info->{$attribute} )
604 if ( defined $new_user_info->{$attribute} );
606 $session->{'CurrentUser'}->Load($user);
610 # we failed to successfully create the user. abort abort abort.
611 delete $session->{'CurrentUser'};
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 ) );
620 unless ( $session->{'CurrentUser'}->Id() ) {
621 delete $session->{'CurrentUser'};
623 die "can't auto-create RT user";
626 #if ($RT::WebExternalOnly) {
627 # $m->comp( '/Elements/Login', %ARGS,
628 # Error => loc('You are not an authorized user') );
637 =item selfservice_priority
639 Returns the configured self-service priority field.
643 my $selfservice_priority;
645 sub selfservice_priority {
646 return $selfservice_priority ||= do {
647 my $conf = FS::Conf->new;
648 $conf->config('ticket_system-selfservice_priority_field') || '';