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.
246 my $custom_priority =
247 FS::Conf->new->config('ticket_system-custom_priority_field') || '';
248 my $ss_priority = selfservice_priority();
251 foreach my $name ( $t->ReadableAttributes ) {
252 # lowercase names, and skip attributes with non-scalar values
253 $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
255 $ticket_info{'owner'} = $t->OwnerObj->Name;
256 $ticket_info{'queue'} = $t->QueueObj->Name;
257 foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
258 my $name = 'CF.{'.$CF->Name.'}';
259 $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
261 # make this easy to find
262 if ( $custom_priority ) {
263 $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
265 if ( $ss_priority ) {
266 $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
269 map { $_->Target =~ /cust_svc\/(\d+)/; $1 }
270 @{ $t->Services->ItemsArrayRef }
272 $ticket_info{'svcnums'} = $svcnums;
274 return \%ticket_info;
277 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
279 Class method. Creates a ticket. If there is an error, returns the scalar
280 error, otherwise returns the newly created RT::Ticket object.
282 Accepts the following options:
296 Requestor email address or arrayref of addresses
300 Cc: email address or arrayref of addresses
308 MIME type to use for message. Defaults to text/plain. Specifying text/html
309 can be useful to use HTML markup in message.
313 Customer number (see L<FS::cust_main>) to associate with ticket.
317 Service number (see L<FS::cust_svc>) to associate with ticket. Will also
318 associate the customer who has this service (unless the service is unlinked).
325 my($self, $session, %param) = @_;
327 $session = $self->session($session);
329 my $Queue = RT::Queue->new($session->{'CurrentUser'});
330 $Queue->Load( $param{'queue'} );
332 my $req = ref($param{'requestor'})
333 ? $param{'requestor'}
334 : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
336 my $cc = ref($param{'cc'})
338 : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
340 my $mimeobj = MIME::Entity->build(
341 'Data' => $param{'message'},
342 'Type' => ( $param{'mime_type'} || 'text/plain' ),
346 'Queue' => $Queue->Id,
347 'Subject' => $param{'subject'},
350 'MIMEObj' => $mimeobj,
352 warn Dumper(\%ticket) if $DEBUG > 1;
354 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
355 my( $id, $Transaction, $ErrStr );
358 ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
360 return $ErrStr if $id == 0;
362 warn "ticket got id $id\n" if $DEBUG;
364 #XXX check errors adding custnum/svcnum links (put it in a transaction)...
365 # but we do already know they're good
367 if ( $param{'custnum'} ) {
368 my( $val, $msg ) = $Ticket->_AddLink(
369 'Type' => 'MemberOf',
370 'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
374 if ( $param{'svcnum'} ) {
375 my( $val, $msg ) = $Ticket->_AddLink(
376 'Type' => 'MemberOf',
377 'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
384 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
386 Class method. Retrieves a ticket. If there is an error, returns the scalar
387 error. Otherwise, currently returns a slightly tricky data structure containing
388 the ticket's attributes, a list of the linked customers, each transaction's
389 content, description, and create time.
391 Accepts the following options:
404 my($self, $session, %param) = @_;
406 $session = $self->session($session);
408 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
409 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
410 return 'Could not load ticket' unless $ticketid;
413 foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
414 my $cust = $link->Target;
415 push @custs, $1 if $cust =~ /\/(\d+)$/;
419 my $transactions = $Ticket->Transactions;
420 while ( my $transaction = $transactions->Next ) {
421 my $t = { created => $transaction->Created,
422 content => $transaction->Content,
423 description => $transaction->Description,
424 type => $transaction->Type,
431 fields => _ticket_info($Ticket),
435 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
437 Class method. Retrieve the RT::Ticket object with the specified
438 ticket_id. If custnum is supplied, will also check that the object
439 is a member of that customer. If there is no ticket or the custnum
440 check fails, returns nothing. The meaning of that case is
441 "to this customer, the ticket does not exist".
455 sub get_ticket_object {
457 my ($session, %opt) = @_;
458 $session = $self->session(shift);
459 # use a small search here so we can check ticket ownership
461 if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
466 if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
467 $query .= " AND Customer.number = $1"; # also checks ownership via services
469 my $Tickets = RT::Tickets->new($session->{CurrentUser});
470 $Tickets->FromSQL($query);
471 if ( $DEBUG ) { # temporary for RT#39536
472 warn "[get_ticket_object] " . $Tickets->BuildSelectQuery . "\n\n";
474 return $Tickets->First;
477 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
479 Class method. Correspond on a ticket. If there is an error, returns the scalar
480 error. Otherwise, returns the transaction id, error message, and
481 RT::Transaction object.
483 Accepts the following options:
493 Correspondence content
499 sub correspond_ticket {
500 my($self, $session, %param) = @_;
502 $session = $self->session($session);
504 my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
505 my $ticketid = $Ticket->Load( $param{'ticket_id'} );
506 return 'Could not load ticket' unless $ticketid;
507 return 'No content' unless $param{'content'};
509 $Ticket->Correspond( Content => $param{'content'} );
512 =item queues SESSION_HASHREF [, ACL ]
514 Retrieve a list of queues. Pass the name of an RT access control right,
515 such as 'CreateTicket', to return only queues on which the current user
516 has that right. Otherwise this will return all queues with the 'SeeQueue'
522 my( $self, $session, $acl ) = @_;
523 $session = $self->session($session);
525 my $showall = $acl ? 0 : 1;
527 my $q = new RT::Queues($session->{'CurrentUser'});
529 while (my $queue = $q->Next) {
530 if ($showall || $queue->CurrentUserHasRight($acl)) {
533 Name => $queue->Name,
534 Description => $queue->Description,
538 return map { $_->{Id} => $_->{Name} } @result;
541 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
542 # to get logged into RT from afar
543 sub _web_external_auth {
544 my( $self, $session ) = @_;
546 my $user = $FS::CurrentUser::CurrentUser->username;
548 eval 'use RT::CurrentUser;';
552 $session->{'CurrentUser'} = RT::CurrentUser->new();
554 warn "$me _web_external_auth loading RT user for $user\n"
557 $session->{'CurrentUser'}->Load($user);
559 if ( ! $session->{'CurrentUser'}->Id() ) {
561 # Create users on-the-fly
563 warn "can't load RT user for $user; auto-creating\n"
566 my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
568 my ( $val, $msg ) = $UserObj->Create(
569 %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
576 # now get user specific information, to better create our user.
578 = RT::Interface::Web::WebRemoteUserAutocreateInfo($user);
580 # set the attributes that have been defined.
581 # FIXME: this is a horrible kludge. I'm sure there's something cleaner
582 foreach my $attribute (
584 'Signature', 'EmailAddress',
585 'PagerEmailAddress', 'FreeformContactInfo',
586 'Organization', 'Disabled',
587 'Privileged', 'RealName',
589 'EmailEncoding', 'WebEncoding',
590 'ExternalContactInfoId', 'ContactInfoSystem',
591 'ExternalAuthId', 'Gecos',
592 'HomePhone', 'WorkPhone',
593 'MobilePhone', 'PagerPhone',
594 'Address1', 'Address2',
600 #$m->comp( '/Elements/Callback', %ARGS,
601 # _CallbackName => 'NewUser' );
603 my $method = "Set$attribute";
604 $UserObj->$method( $new_user_info->{$attribute} )
605 if ( defined $new_user_info->{$attribute} );
607 $session->{'CurrentUser'}->Load($user);
611 # we failed to successfully create the user. abort abort abort.
612 delete $session->{'CurrentUser'};
614 die "can't auto-create RT user: $msg"; #an error message would be nice :/
615 #$m->abort() unless $RT::WebFallbackToInternalAuth;
616 #$m->comp( '/Elements/Login', %ARGS,
617 # Error => loc( 'Cannot create user: [_1]', $msg ) );
621 unless ( $session->{'CurrentUser'}->Id() ) {
622 delete $session->{'CurrentUser'};
624 die "can't auto-create RT user";
627 #if ($RT::WebExternalOnly) {
628 # $m->comp( '/Elements/Login', %ARGS,
629 # Error => loc('You are not an authorized user') );
638 =item selfservice_priority
640 Returns the configured self-service priority field.
644 my $selfservice_priority;
646 sub selfservice_priority {
647 return $selfservice_priority ||= do {
648 my $conf = FS::Conf->new;
649 $conf->config('ticket_system-selfservice_priority_field') || '';