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 ) = @_;
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 warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
163 $Tickets->FromSQL($rtql);
165 $Tickets->RowsPerPage($limit);
166 warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
171 sub href_customer_tickets {
172 my ($self, $custnum) = (shift, shift);
173 if ($custnum =~ /^(\d+)$/) {
174 return $self->href_search_tickets("Customer.number = $custnum", @_);
176 warn "bad custnum $custnum"; '';
179 sub href_service_tickets {
180 my ($self, $svcnum) = (shift, shift);
181 if ($svcnum =~ /^(\d+)$/ ) {
182 return $self->href_search_tickets("Service.number = $svcnum", @_);
184 warn "bad svcnum $svcnum"; '';
187 sub customer_tickets {
189 my $Tickets = $self->_tickets_search('Customer', @_);
191 my $conf = FS::Conf->new;
193 $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
196 { FIELD => 'Priority', ORDER => $priority_order },
197 { FIELD => 'Id', ORDER => 'DESC' },
200 $Tickets->OrderByCols(@order_by);
203 while ( my $t = $Tickets->Next ) {
204 push @tickets, _ticket_info($t);
210 sub num_customer_tickets {
211 my ( $self, $custnum, $priority ) = @_;
212 $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll;
215 sub service_tickets {
217 my $Tickets = $self->_tickets_search('Service', @_);
219 my $conf = FS::Conf->new;
221 $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
224 { FIELD => 'Priority', ORDER => $priority_order },
225 { FIELD => 'Id', ORDER => 'DESC' },
228 $Tickets->OrderByCols(@order_by);
231 while ( my $t = $Tickets->Next ) {
232 push @tickets, _ticket_info($t);
239 # Takes an RT::Ticket; returns a hashref of the ticket's fields, including
240 # custom fields. Also returns custom and selfservice priority values as
241 # _custom_priority and _selfservice_priority.
244 my $custom_priority =
245 FS::Conf->new->config('ticket_system-custom_priority_field') || '';
246 my $ss_priority = selfservice_priority();
249 foreach my $name ( $t->ReadableAttributes ) {
250 # lowercase names, and skip attributes with non-scalar values
251 $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
253 $ticket_info{'owner'} = $t->OwnerObj->Name;
254 $ticket_info{'queue'} = $t->QueueObj->Name;
255 foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
256 my $name = 'CF.{'.$CF->Name.'}';
257 $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
259 # make this easy to find
260 if ( $custom_priority ) {
261 $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
263 if ( $ss_priority ) {
264 $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
267 map { $_->Target =~ /cust_svc\/(\d+)/; $1 }
268 @{ $t->Services->ItemsArrayRef }
270 $ticket_info{'svcnums'} = $svcnums;
272 return \%ticket_info;
275 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
277 Class method. Creates a ticket. If there is an error, returns the scalar
278 error, otherwise returns the newly created RT::Ticket object.
280 Accepts the following options:
294 Requestor email address or arrayref of addresses
298 Cc: email address or arrayref of addresses
306 MIME type to use for message. Defaults to text/plain. Specifying text/html
307 can be useful to use HTML markup in message.
311 Customer number (see L<FS::cust_main>) to associate with ticket.
315 Service number (see L<FS::cust_svc>) to associate with ticket. Will also
316 associate the customer who has this service (unless the service is unlinked).
323 my($self, $session, %param) = @_;
325 $session = $self->session($session);
327 my $Queue = RT::Queue->new($session->{'CurrentUser'});
328 $Queue->Load( $param{'queue'} );
330 my $req = ref($param{'requestor'})
331 ? $param{'requestor'}
332 : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
334 my $cc = ref($param{'cc'})
336 : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
338 my $mimeobj = MIME::Entity->build(
339 'Data' => $param{'message'},
340 'Type' => ( $param{'mime_type'} || 'text/plain' ),
344 'Queue' => $Queue->Id,
345 'Subject' => $param{'subject'},
348 'MIMEObj' => $mimeobj,
350 warn Dumper(\%ticket) if $DEBUG > 1;
352 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
353 my( $id, $Transaction, $ErrStr );
356 ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
358 return $ErrStr if $id == 0;
360 warn "ticket got id $id\n" if $DEBUG;
362 #XXX check errors adding custnum/svcnum links (put it in a transaction)...
363 # but we do already know they're good
365 if ( $param{'custnum'} ) {
366 my( $val, $msg ) = $Ticket->_AddLink(
367 'Type' => 'MemberOf',
368 'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
372 if ( $param{'svcnum'} ) {
373 my( $val, $msg ) = $Ticket->_AddLink(
374 'Type' => 'MemberOf',
375 'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
382 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
384 Class method. Retrieves a ticket. If there is an error, returns the scalar
385 error. Otherwise, currently returns a slightly tricky data structure containing
386 the ticket's attributes, a list of the linked customers, each transaction's
387 content, description, and create time.
389 Accepts the following options:
402 my($self, $session, %param) = @_;
404 $session = $self->session($session);
406 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
407 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
408 return 'Could not load ticket' unless $ticketid;
411 foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
412 my $cust = $link->Target;
413 push @custs, $1 if $cust =~ /\/(\d+)$/;
417 my $transactions = $Ticket->Transactions;
418 while ( my $transaction = $transactions->Next ) {
419 my $t = { created => $transaction->Created,
420 content => $transaction->Content,
421 description => $transaction->Description,
422 type => $transaction->Type,
429 fields => _ticket_info($Ticket),
433 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
435 Class method. Retrieve the RT::Ticket object with the specified
436 ticket_id. If custnum is supplied, will also check that the object
437 is a member of that customer. If there is no ticket or the custnum
438 check fails, returns nothing. The meaning of that case is
439 "to this customer, the ticket does not exist".
453 sub get_ticket_object {
455 my ($session, %opt) = @_;
456 $session = $self->session(shift);
457 my $Ticket = RT::Ticket->new($session->{CurrentUser});
458 $Ticket->Load($opt{'ticket_id'});
459 return if ( !$Ticket->id );
460 my $custnum = $opt{'custnum'};
461 if ( defined($custnum) && $custnum =~ /^\d+$/ ) {
462 # probably the most efficient way to check ticket ownership
463 my $Link = RT::Link->new($session->{CurrentUser});
464 $Link->LoadByCols( LocalBase => $opt{'ticket_id'},
466 Target => "freeside://freeside/cust_main/$custnum",
468 return if ( !$Link->id );
474 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
476 Class method. Correspond on a ticket. If there is an error, returns the scalar
477 error. Otherwise, returns the transaction id, error message, and
478 RT::Transaction object.
480 Accepts the following options:
490 Correspondence content
496 sub correspond_ticket {
497 my($self, $session, %param) = @_;
499 $session = $self->session($session);
501 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
502 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
503 return 'Could not load ticket' unless $ticketid;
504 return 'No content' unless $param{'content'};
506 $Ticket->Correspond( Content => $param{'content'} );
509 =item queues SESSION_HASHREF [, ACL ]
511 Retrieve a list of queues. Pass the name of an RT access control right,
512 such as 'CreateTicket', to return only queues on which the current user
513 has that right. Otherwise this will return all queues with the 'SeeQueue'
519 my( $self, $session, $acl ) = @_;
520 $session = $self->session($session);
522 my $showall = $acl ? 0 : 1;
524 my $q = new RT::Queues($session->{'CurrentUser'});
526 while (my $queue = $q->Next) {
527 if ($showall || $queue->CurrentUserHasRight($acl)) {
530 Name => $queue->Name,
531 Description => $queue->Description,
535 return map { $_->{Id} => $_->{Name} } @result;
538 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
539 # to get logged into RT from afar
540 sub _web_external_auth {
541 my( $self, $session ) = @_;
543 my $user = $FS::CurrentUser::CurrentUser->username;
545 eval 'use RT::CurrentUser;';
549 $session->{'CurrentUser'} = RT::CurrentUser->new();
551 warn "$me _web_external_auth loading RT user for $user\n"
554 $session->{'CurrentUser'}->Load($user);
556 if ( ! $session->{'CurrentUser'}->Id() ) {
558 # Create users on-the-fly
560 warn "can't load RT user for $user; auto-creating\n"
563 my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
565 my ( $val, $msg ) = $UserObj->Create(
566 %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
573 # now get user specific information, to better create our user.
575 = RT::Interface::Web::WebExternalAutoInfo($user);
577 # set the attributes that have been defined.
578 # FIXME: this is a horrible kludge. I'm sure there's something cleaner
579 foreach my $attribute (
581 'Signature', 'EmailAddress',
582 'PagerEmailAddress', 'FreeformContactInfo',
583 'Organization', 'Disabled',
584 'Privileged', 'RealName',
586 'EmailEncoding', 'WebEncoding',
587 'ExternalContactInfoId', 'ContactInfoSystem',
588 'ExternalAuthId', 'Gecos',
589 'HomePhone', 'WorkPhone',
590 'MobilePhone', 'PagerPhone',
591 'Address1', 'Address2',
597 #$m->comp( '/Elements/Callback', %ARGS,
598 # _CallbackName => 'NewUser' );
600 my $method = "Set$attribute";
601 $UserObj->$method( $new_user_info->{$attribute} )
602 if ( defined $new_user_info->{$attribute} );
604 $session->{'CurrentUser'}->Load($user);
608 # we failed to successfully create the user. abort abort abort.
609 delete $session->{'CurrentUser'};
611 die "can't auto-create RT user: $msg"; #an error message would be nice :/
612 #$m->abort() unless $RT::WebFallbackToInternalAuth;
613 #$m->comp( '/Elements/Login', %ARGS,
614 # Error => loc( 'Cannot create user: [_1]', $msg ) );
618 unless ( $session->{'CurrentUser'}->Id() ) {
619 delete $session->{'CurrentUser'};
621 die "can't auto-create RT user";
624 #if ($RT::WebExternalOnly) {
625 # $m->comp( '/Elements/Login', %ARGS,
626 # Error => loc('You are not an authorized user') );
635 =item selfservice_priority
637 Returns the configured self-service priority field.
641 my $selfservice_priority;
643 sub selfservice_priority {
644 return $selfservice_priority ||= do {
645 my $conf = FS::Conf->new;
646 $conf->config('ticket_system-selfservice_priority_field') || '';