X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FTicketSystem%2FRT_Internal.pm;h=06761552876312fc5f74ff1686d72dfee4392265;hp=f47648e7f75c59f2a3d1b0793d218ccdfdce3448;hb=57d4a5ffe7b86d032339d6eefe1a22277f3ca113;hpb=624b2d44625f69d71175c3348cae635d580c890b diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm index f47648e7f..067615528 100644 --- a/FS/FS/TicketSystem/RT_Internal.pm +++ b/FS/FS/TicketSystem/RT_Internal.pm @@ -3,11 +3,12 @@ package FS::TicketSystem::RT_Internal; use strict; use vars qw( @ISA $DEBUG $me ); use Data::Dumper; +use Date::Format qw( time2str ); use MIME::Entity; +use Encode; use FS::UID qw(dbh); use FS::CGI qw(popurl); use FS::TicketSystem::RT_Libs; -use RT::CurrentUser; @ISA = qw( FS::TicketSystem::RT_Libs ); @@ -36,7 +37,6 @@ sub baseurl { sub access_right { my( $self, $session, $right ) = @_; - #return '' unless $conf->config('ticket_system'); return '' unless FS::Conf->new->config('ticket_system'); $session = $self->session($session); @@ -52,7 +52,7 @@ sub access_right { sub session { my( $self, $session ) = @_; - if ( $session && $session->{'Current_User'} ) { + if ( $session && $session->{'CurrentUser'} ) { # does this even work? warn "$me session: using existing session and CurrentUser: \n". Dumper($session->{'CurrentUser'}) if $DEBUG; @@ -64,43 +64,37 @@ sub session { $session; } +my $firsttime = 1; + sub init { my $self = shift; - - warn "$me init: loading RT libraries\n" if $DEBUG; - eval ' - use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" ); - use RT; - #it looks like the rest are taken care of these days in RT::InitClasses - #use RT::Ticket; - #use RT::Transactions; - #use RT::Users; - #use RT::CurrentUser; - #use RT::Templates; - #use RT::Queues; - #use RT::ScripActions; - #use RT::ScripConditions; - #use RT::Scrips; - #use RT::Groups; - #use RT::GroupMembers; - #use RT::CustomFields; - #use RT::CustomFieldValues; - #use RT::ObjectCustomFieldValues; - - #for web external auth... - use RT::Interface::Web; - '; - die $@ if $@; - - warn "$me init: loading RT config\n" if $DEBUG; - { - local $SIG{__DIE__}; - eval 'RT::LoadConfig();'; + if ( $firsttime ) { + + # this part only needs to be done once + warn "$me init: loading RT libraries\n" if $DEBUG; + eval ' + use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" ); + use RT; + + #for web external auth... + use RT::Interface::Web; + '; + die $@ if $@; + + warn "$me init: loading RT config\n" if $DEBUG; + { + local $SIG{__DIE__}; + eval 'RT::LoadConfig();'; + } + die $@ if $@; + + $firsttime = 0; } - die $@ if $@; + # this needs to be done on each fork warn "$me init: initializing RT\n" if $DEBUG; { + local $SIG{__WARN__}; local $SIG{__DIE__}; eval 'RT::Init("NoSignalHandlers"=>1);'; } @@ -109,6 +103,214 @@ sub init { warn "$me init: complete" if $DEBUG; } +=item customer_tickets CUSTNUM [ PARAMS ] + +Replacement for the one in RT_External so that we can access custom fields +properly. Accepts a hashref with the following parameters: + +number - custnum/svcnum + +limit + +priority + +status + +queueid + +resolved - only return tickets resolved after this timestamp + +=cut + +# create an RT::Tickets object for a specified custnum or svcnum + +sub _tickets_search { + my $self = shift; + my $type = shift; + + my( $number, $limit, $priority, $status, $queueid, $opt ); + if ( ref($_[0]) eq 'HASH' ) { + $opt = shift; + $number = $$opt{'number'}; + $limit = $$opt{'limit'}; + $priority = $$opt{'priority'}; + $status = $$opt{'status'}; + $queueid = $$opt{'queueid'}; + } else { + ( $number, $limit, $priority, $status, $queueid ) = @_; + $opt = {}; + } + + $type =~ /^Customer|Service$/ or die "invalid type: $type"; + $number =~ /^\d+$/ or die "invalid custnum/svcnum: $number"; + $limit =~ /^\d+$/ or die "invalid limit: $limit"; + + my $session = $self->session(); + my $CurrentUser = $session->{CurrentUser} + or die "unable to create an RT session"; + + my $Tickets = RT::Tickets->new($CurrentUser); + + # "Customer.number" searches tickets linked via cust_svc also + my $rtql = "$type.number = $number"; + + if ( defined( $priority ) ) { + my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field'); + if ( length( $priority ) ) { + $rtql .= " AND CF.{$custom_priority} = '$priority'"; + } + else { + $rtql .= " AND CF.{$custom_priority} IS NULL"; + } + } + + my @statuses; + if ( defined($status) && $status ) { + if ( ref($status) ) { + if ( ref($status) eq 'HASH' ) { + @statuses = grep $status->{$_}, keys %$status; + } elsif ( ref($status) eq 'ARRAY' ) { + @statuses = @$status; + } else { + #what should be the failure mode here? die? return no tickets? + die 'unknown status ref '. ref($status); + } + } else { + @statuses = ( $status ); + } + @statuses = grep /^\w+$/, @statuses; #injection prevention + } else { + @statuses = $self->statuses; + } + + $rtql .= ' AND ( '. + join(' OR ', map { "Status = '$_'" } @statuses). + ' ) '; + + $rtql .= " AND Queue = $queueid " if $queueid; + + if ($$opt{'resolved'}) { + $rtql .= " AND Resolved >= " . dbh->quote(time2str('%Y-%m-%d %H:%M:%S',$$opt{'resolved'})); + } + + warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG; + $Tickets->FromSQL($rtql); + + $Tickets->RowsPerPage($limit); + warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1; + + return $Tickets; +} + +sub href_customer_tickets { + my ($self, $custnum) = (shift, shift); + if ($custnum =~ /^(\d+)$/) { + return $self->href_search_tickets("Customer.number = $custnum", @_); + } + warn "bad custnum $custnum"; ''; +} + +sub href_service_tickets { + my ($self, $svcnum) = (shift, shift); + if ($svcnum =~ /^(\d+)$/ ) { + return $self->href_search_tickets("Service.number = $svcnum", @_); + } + warn "bad svcnum $svcnum"; ''; +} + +sub customer_tickets { + my $self = shift; + my $Tickets = $self->_tickets_search('Customer', @_); + + my $conf = FS::Conf->new; + my $priority_order = + $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC'; + + my @order_by = ( + { FIELD => 'Priority', ORDER => $priority_order }, + { FIELD => 'Id', ORDER => 'DESC' }, + ); + + $Tickets->OrderByCols(@order_by); + + my @tickets; + while ( my $t = $Tickets->Next ) { + push @tickets, _ticket_info($t); + } + + return \@tickets; +} + +sub num_customer_tickets { + my ( $self, $custnum, $priority ) = @_; + $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll; +} + +sub service_tickets { + my $self = shift; + my $Tickets = $self->_tickets_search('Service', @_); + + my $conf = FS::Conf->new; + my $priority_order = + $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC'; + + my @order_by = ( + { FIELD => 'Priority', ORDER => $priority_order }, + { FIELD => 'Id', ORDER => 'DESC' }, + ); + + $Tickets->OrderByCols(@order_by); + + my @tickets; + while ( my $t = $Tickets->Next ) { + push @tickets, _ticket_info($t); + } + + return \@tickets; +} + +sub _ticket_info { + # Takes an RT::Ticket; returns a hashref of the ticket's fields, including + # custom fields. Also returns custom and selfservice priority values as + # _custom_priority and _selfservice_priority, and the IsUnreplied property + # as is_unreplied. + my $t = shift; + + my $custom_priority = + FS::Conf->new->config('ticket_system-custom_priority_field') || ''; + my $ss_priority = selfservice_priority(); + + my %ticket_info; + foreach my $name ( $t->ReadableAttributes ) { + # lowercase names, and skip attributes with non-scalar values + $ticket_info{lc($name)} = $t->$name if !ref($t->$name); + } + $ticket_info{'owner'} = $t->OwnerObj->Name; + $ticket_info{'queue'} = $t->QueueObj->Name; + $ticket_info{'_cf_sort_order'} = {}; + my $cf_sort = 0; + foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) { + $ticket_info{'_cf_sort_order'}{$CF->Name} = $cf_sort++; + my $name = 'CF.{'.$CF->Name.'}'; + $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id); + } + # make this easy to find + if ( $custom_priority ) { + $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"}; + } + if ( $ss_priority ) { + $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"}; + } + $ticket_info{'is_unreplied'} = $t->IsUnreplied; + my $svcnums = [ + map { $_->Target =~ /cust_svc\/(\d+)/; $1 } + @{ $t->Services->ItemsArrayRef } + ]; + $ticket_info{'svcnums'} = $svcnums; + + return \%ticket_info; +} + =item create_ticket SESSION_HASHREF, OPTION => VALUE ... Class method. Creates a ticket. If there is an error, returns the scalar @@ -173,7 +375,7 @@ sub create_ticket { : ( $param{'cc'} ? [ $param{'cc'} ] : [] ); my $mimeobj = MIME::Entity->build( - 'Data' => $param{'message'}, + 'Data' => Encode::encode_utf8( $param{'message'} ), 'Type' => ( $param{'mime_type'} || 'text/plain' ), ); @@ -216,6 +418,160 @@ sub create_ticket { $Ticket; } +=item get_ticket SESSION_HASHREF, OPTION => VALUE ... + +Class method. Retrieves a ticket. If there is an error, returns the scalar +error. Otherwise, currently returns a slightly tricky data structure containing +the ticket's attributes, a list of the linked customers, each transaction's +content, description, and create time. + +Accepts the following options: + +=over 4 + +=item ticket_id + +The ticket id + +=back + +=cut + +sub get_ticket { + my($self, $session, %param) = @_; + + $session = $self->session($session); + + my $Ticket = RT::Ticket->new($session->{'CurrentUser'}); + my $ticketid = $Ticket->Load( $param{'ticket_id'} ); + return 'Could not load ticket' unless $ticketid; + + my @custs = (); + foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) { + my $cust = $link->Target; + push @custs, $1 if $cust =~ /\/(\d+)$/; + } + + my @txns = (); + my $transactions = $Ticket->Transactions; + while ( my $transaction = $transactions->Next ) { + my $t = { created => $transaction->Created, + content => $transaction->Content, + description => $transaction->Description, + type => $transaction->Type, + }; + push @txns, $t; + } + + { txns => [ @txns ], + custs => [ @custs ], + fields => _ticket_info($Ticket), + }; +} + +=item get_ticket_object SESSION_HASHREF, OPTION => VALUE... + +Class method. Retrieve the RT::Ticket object with the specified +ticket_id. If custnum is supplied, will also check that the object +is a member of that customer. If there is no ticket or the custnum +check fails, returns nothing. The meaning of that case is +"to this customer, the ticket does not exist". + +Options: + +=over 4 + +=item ticket_id + +=item custnum + +=back + +=cut + +sub get_ticket_object { + my $self = shift; + my ($session, %opt) = @_; + $session = $self->session(shift); + # use a small search here so we can check ticket ownership + my $query; + if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) { + $query = "id = $1"; + } else { + return; + } + if ( $opt{'custnum'} =~ /^(\d+)$/ ) { + $query .= " AND Customer.number = $1"; # also checks ownership via services + } + my $Tickets = RT::Tickets->new($session->{CurrentUser}); + $Tickets->FromSQL($query); + return $Tickets->First; +} + +=item correspond_ticket SESSION_HASHREF, OPTION => VALUE ... + +Class method. Correspond on a ticket. If there is an error, returns the scalar +error. Otherwise, returns the transaction id, error message, and +RT::Transaction object. + +Accepts the following options: + +=over 4 + +=item ticket_id + +The ticket id + +=item content + +Correspondence content + +=back + +=cut + +sub correspond_ticket { + my($self, $session, %param) = @_; + + $session = $self->session($session); + + my $Ticket = RT::Ticket->new($session->{'CurrentUser'}); + my $ticketid = $Ticket->Load( $param{'ticket_id'} ); + return 'Could not load ticket' unless $ticketid; + return 'No content' unless $param{'content'}; + + $Ticket->Correspond( Content => $param{'content'} ); +} + +=item queues SESSION_HASHREF [, ACL ] + +Retrieve a list of queues. Pass the name of an RT access control right, +such as 'CreateTicket', to return only queues on which the current user +has that right. Otherwise this will return all queues with the 'SeeQueue' +right. + +=cut + +sub queues { + my( $self, $session, $acl ) = @_; + $session = $self->session($session); + + my $showall = $acl ? 0 : 1; + my @result = (); + my $q = new RT::Queues($session->{'CurrentUser'}); + $q->UnLimit; + while (my $queue = $q->Next) { + if ($showall || $queue->CurrentUserHasRight($acl)) { + push @result, { + Id => $queue->Id, + Name => $queue->Name, + Description => $queue->Description, + }; + } + } + return map { $_->{Id} => $_->{Name} } @result; +} + #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth # to get logged into RT from afar sub _web_external_auth { @@ -223,6 +579,9 @@ sub _web_external_auth { my $user = $FS::CurrentUser::CurrentUser->username; + eval 'use RT::CurrentUser;'; + die $@ if $@; + $session ||= {}; $session->{'CurrentUser'} = RT::CurrentUser->new(); @@ -286,7 +645,7 @@ sub _web_external_auth { # we failed to successfully create the user. abort abort abort. delete $session->{'CurrentUser'}; - die "can't auto-create RT user"; #an error message would be nice :/ + die "can't auto-create RT user: $msg"; #an error message would be nice :/ #$m->abort() unless $RT::WebFallbackToInternalAuth; #$m->comp( '/Elements/Login', %ARGS, # Error => loc( 'Cannot create user: [_1]', $msg ) ); @@ -310,5 +669,64 @@ sub _web_external_auth { } +=item selfservice_priority + +Returns the configured self-service priority field. + +=cut + +my $selfservice_priority; + +sub selfservice_priority { + return $selfservice_priority ||= do { + my $conf = FS::Conf->new; + $conf->config('ticket_system-selfservice_priority_field') || ''; + } +} + +=item custom_fields + +Returns a hash of custom field names and descriptions. + +Accepts the following options: + +lookuptype - limit results to this lookuptype + +valuetype - limit results to this valuetype + +Fields must be visible to CurrentUser. + +=cut + +sub custom_fields { + my $self = shift; + my %opt = @_; + my $lookuptype = $opt{lookuptype}; + my $valuetype = $opt{valuetype}; + + my $CurrentUser = RT::CurrentUser->new(); + $CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username); + die "RT not configured" unless $CurrentUser->id; + my $CFs = RT::CustomFields->new($CurrentUser); + + $CFs->UnLimit; + + $CFs->Limit(FIELD => 'LookupType', + OPERATOR => 'ENDSWITH', + VALUE => $lookuptype) + if $lookuptype; + + $CFs->Limit(FIELD => 'Type', + VALUE => $valuetype) + if $valuetype; + + my @fields; + while (my $CF = $CFs->Next) { + push @fields, $CF->Name, ($CF->Description || $CF->Name); + } + + return @fields; +} + 1;