# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
package Kernel::System::FAQSearch;
use strict;
use warnings;
use Kernel::System::VariableCheck qw(:all);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::DB',
'Kernel::System::DynamicField',
'Kernel::System::DynamicField::Backend',
'Kernel::System::Log',
'Kernel::System::DateTime',
'Kernel::System::Valid',
);
=head1 NAME
Kernel::System::FAQSearch - FAQ search lib
=head1 DESCRIPTION
All FAQ search functions.
=head1 PUBLIC INTERFACE
=head2 FAQSearch()
search in FAQ articles
my @IDs = $FAQObject->FAQSearch(
Number => '*134*', # (optional)
Title => '*some title*', # (optional)
# is searching in Number, Title, Keyword and Field1-6
What => '*some text*', # (optional)
Keyword => '*webserver*', # (optional)
States => { # (optional)
1 => 'internal',
2 => 'external',
},
LanguageIDs => [ 4, 5, 6 ], # (optional)
CategoryIDs => [ 7, 8, 9 ], # (optional)
ValidIDs => [ 1, 2, 3 ], # (optional) (default 1)
# Approved
# Only available in internal interface (agent interface)
Approved => 1, # (optional) 1 or 0,
# Votes
# At least one operator must be specified. Operators will be connected with AND,
# values in an operator with OR.
# You can also pass more than one argument to an operator: [123, 654]
Votes => {
Equals => 123,
GreaterThan => 123,
GreaterThanEquals => 123,
SmallerThan => 123,
SmallerThanEquals => 123,
}
# Rate
# At least one operator must be specified. Operators will be connected with AND,
# values in an operator with OR.
# You can also pass more than one argument to an operator: [50, 75]
Rate => {
Equals => 75,
GreaterThan => 75,
GreaterThanEquals => 75,
SmallerThan => 75,
SmallerThanEquals => 75,
}
# create FAQ item properties (optional)
CreatedUserIDs => [1, 12, 455, 32]
# change FAQ item properties (optional)
LastChangedUserIDs => [1, 12, 455, 32]
# DynamicFields
# At least one operator must be specified. Operators will be connected with AND,
# values in an operator with OR.
# You can also pass more than one argument to an operator: ['value1', 'value2']
DynamicField_FieldNameX => {
Equals => 123,
Like => 'value*', # "equals" operator with wild-card support
GreaterThan => '2001-01-01 01:01:01',
GreaterThanEquals => '2001-01-01 01:01:01',
SmallerThan => '2002-02-02 02:02:02',
SmallerThanEquals => '2002-02-02 02:02:02',
}
# FAQ items created more than 60 minutes ago (item older than 60 minutes) (optional)
ItemCreateTimeOlderMinutes => 60,
# FAQ item created less than 120 minutes ago (item newer than 120 minutes) (optional)
ItemCreateTimeNewerMinutes => 120,
# FAQ items with create time after ... (item newer than this date) (optional)
ItemCreateTimeNewerDate => '2006-01-09 00:00:01',
# FAQ items with created time before ... (item older than this date) (optional)
ItemCreateTimeOlderDate => '2006-01-19 23:59:59',
# FAQ items changed more than 60 minutes ago (optional)
ItemChangeTimeOlderMinutes => 60,
# FAQ items changed less than 120 minutes ago (optional)
ItemChangeTimeNewerMinutes => 120,
# FAQ item with changed time after ... (item changed newer than this date) (optional)
ItemChangeTimeNewerDate => '2006-01-09 00:00:01',
# FAQ item with changed time before ... (item changed older than this date) (optional)
ItemChangeTimeOlderDate => '2006-01-19 23:59:59',
OrderBy => [ 'FAQID', 'Title' ], # (optional)
# default: [ 'FAQID' ],
# (FAQID, Number, Title, Language, Category, Valid, Created,
# Changed, State, Votes, Result)
# Additional information for OrderBy:
# The OrderByDirection can be specified for each OrderBy attribute.
# The pairing is made by the array indexes.
OrderByDirection => [ 'Down', 'Up' ], # (optional)
# default: [ 'UP' ]
# (Down | Up)
Limit => 150,
Interface => { # (default internal)
StateID => 3,
Name => 'public', # public | external | internal
},
UserID => 1,
);
Returns:
@IDs = (
32,
13,
12,
9,
6,
5,
4,
1,
);
=cut
sub FAQSearch {
my ( $Self, %Param ) = @_;
if ( !$Param{UserID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need UserID!",
);
return;
}
# set default interface
if ( !$Param{Interface} || !$Param{Interface}->{Name} ) {
$Param{Interface}->{Name} = 'internal';
}
# verify that all passed array parameters contain an array reference
ARGUMENT:
for my $Argument (qw(OrderBy OrderByDirection)) {
if ( !defined $Param{$Argument} ) {
$Param{$Argument} ||= [];
next ARGUMENT;
}
if ( ref $Param{$Argument} ne 'ARRAY' ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "$Argument must be an array reference!",
);
return;
}
}
# define order table
my %OrderByTable = (
# FAQ item attributes
FAQID => 'i.id',
Number => 'i.f_number',
Title => 'i.f_subject',
Language => 'i.f_language_id',
Category => 'i.category_id',
Valid => 'i.valid_id',
Created => 'i.created',
Changed => 'i.changed',
# State attributes
State => 's.name',
# Vote attributes
Votes => 'votes',
Result => 'vrate',
);
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# quote id array elements
ARGUMENT:
for my $Key (qw(LanguageIDs CategoryIDs ValidIDs CreatedUserIDs LastChangedUserIDs)) {
next ARGUMENT if !$Param{$Key};
if ( !IsArrayRefWithData( $Param{$Key} ) ) {
# log error
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "The given param '$Key' is invalid or an empty array reference!",
);
return;
}
# quote elements
for my $Element ( @{ $Param{$Key} } ) {
if ( !defined $DBObject->Quote( $Element, 'Integer' ) ) {
# log error
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "The given param '$Element' in '$Key' is invalid!",
);
return;
}
}
}
my $FAQDynamicFields = [];
my %ValidDynamicFieldParams;
my %FAQDynamicFieldName2Config;
# Only fetch DynamicField data if a field was requested for searching or sorting
my $ParamCheckString = ( join '', keys %Param ) || '';
if ( ref $Param{OrderBy} eq 'ARRAY' ) {
$ParamCheckString .= ( join '', @{ $Param{OrderBy} } );
}
elsif ( ref $Param{OrderBy} ne 'HASH' ) {
$ParamCheckString .= $Param{OrderBy} || '';
}
if ( $ParamCheckString =~ m/DynamicField_/smx ) {
# Check all configured FAQ dynamic fields
$FAQDynamicFields = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
ObjectType => 'FAQ',
);
for my $DynamicField ( @{$FAQDynamicFields} ) {
$ValidDynamicFieldParams{ "DynamicField_" . $DynamicField->{Name} } = 1;
$FAQDynamicFieldName2Config{ $DynamicField->{Name} } = $DynamicField;
}
}
# check if OrderBy contains only unique valid values
my %OrderBySeen;
for my $OrderBy ( @{ $Param{OrderBy} } ) {
if (
!$OrderBy
|| ( !$OrderByTable{$OrderBy} && !$ValidDynamicFieldParams{$OrderBy} )
|| $OrderBySeen{$OrderBy}
)
{
# found an error
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "OrderBy contains invalid value '$OrderBy' "
. 'or the value is used more than once!',
);
return;
}
# remember the value to check if it appears more than once
$OrderBySeen{$OrderBy} = 1;
}
# check if OrderByDirection array contains only 'Up' or 'Down'
DIRECTION:
for my $Direction ( @{ $Param{OrderByDirection} } ) {
# only 'Up' or 'Down' allowed
next DIRECTION if $Direction eq 'Up';
next DIRECTION if $Direction eq 'Down';
# found an error
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "OrderByDirection can only contain 'Up' or 'Down'!",
);
return;
}
# SQL
my $SQL = 'SELECT i.id, count( v.item_id ) as votes, avg( v.rate ) as vrate '
. 'FROM faq_item i '
. 'LEFT JOIN faq_voting v ON v.item_id = i.id '
. 'LEFT JOIN faq_state s ON s.id = i.state_id';
# extended SQL
my $Ext = '';
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
# full-text search
if ( $Param{What} && $Param{What} ne '*' ) {
# define the search fields for full-text search
my @SearchFields = ( 'i.f_number', 'i.f_subject', 'i.f_keywords' );
# used from the agent interface (internal)
if ( $Param{Interface}->{Name} eq 'internal' ) {
for my $Number ( 1 .. 6 ) {
# get the state of the field (internal, external, public)
my $FieldState = $ConfigObject->Get( 'FAQ::Item::Field' . $Number )->{Show};
# add all internal, external and public fields
if (
$FieldState eq 'internal'
|| $FieldState eq 'external'
|| $FieldState eq 'public'
)
{
push @SearchFields, 'i.f_field' . $Number;
}
}
}
# used from the customer interface (external)
elsif ( $Param{Interface}->{Name} eq 'external' ) {
for my $Number ( 1 .. 6 ) {
# get the state of the field (internal, external, public)
my $FieldState = $ConfigObject->Get( 'FAQ::Item::Field' . $Number )->{Show};
# add all external and public fields
if ( $FieldState eq 'external' || $FieldState eq 'public' ) {
push @SearchFields, 'i.f_field' . $Number;
}
}
}
# used from the public interface (public)
else {
for my $Number ( 1 .. 6 ) {
# get the state of the field (internal, external, public)
my $FieldState = $ConfigObject->Get( 'FAQ::Item::Field' . $Number )->{Show};
# add all public fields
if ( $FieldState eq 'public' ) {
push @SearchFields, 'i.f_field' . $Number;
}
}
}
# add the SQL for the full-text search
$Ext .= $DBObject->QueryCondition(
Key => \@SearchFields,
Value => $Param{What},
SearchPrefix => '*',
SearchSuffix => '*',
);
}
# search for the number
if ( $Param{Number} ) {
$Param{Number} =~ s/\*/%/g;
$Param{Number} =~ s/%%/%/g;
$Param{Number} = $DBObject->Quote( $Param{Number}, 'Like' );
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= " LOWER(i.f_number) LIKE LOWER('" . $Param{Number} . "') $Self->{LikeEscapeString}";
}
# search for the title
if ( $Param{Title} ) {
$Param{Title} = "\%$Param{Title}\%";
$Param{Title} =~ s/\*/%/g;
$Param{Title} =~ s/%%/%/g;
$Param{Title} = $DBObject->Quote( $Param{Title}, 'Like' );
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= " LOWER(i.f_subject) LIKE LOWER('" . $Param{Title} . "') $Self->{LikeEscapeString}";
}
# search for languages
if ( $Param{LanguageIDs} && ref $Param{LanguageIDs} eq 'ARRAY' && @{ $Param{LanguageIDs} } ) {
my $InString = $Self->_InConditionGet(
TableColumn => 'i.f_language_id',
IDRef => $Param{LanguageIDs},
);
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= $InString;
}
# search for categories
if ( $Param{CategoryIDs} && ref $Param{CategoryIDs} eq 'ARRAY' && @{ $Param{CategoryIDs} } ) {
my $InString = $Self->_InConditionGet(
TableColumn => 'i.category_id',
IDRef => $Param{CategoryIDs},
);
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= $InString;
}
# set default value for ValidIDs (only search for valid FAQs)
if ( !defined $Param{ValidIDs} ) {
# get the valid ids
my @ValidIDs = $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet();
$Param{ValidIDs} = \@ValidIDs;
}
# search for ValidIDs
if ( $Param{ValidIDs} && ref $Param{ValidIDs} eq 'ARRAY' && @{ $Param{ValidIDs} } ) {
my $InString = $Self->_InConditionGet(
TableColumn => 'i.valid_id',
IDRef => $Param{ValidIDs},
);
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= $InString;
}
# search for states
if ( $Param{States} && ref $Param{States} eq 'HASH' && %{ $Param{States} } ) {
my @States = map { $DBObject->Quote( $_, 'Integer' ) } keys %{ $Param{States} };
return if scalar @States != keys %{ $Param{States} };
my $InString = $Self->_InConditionGet(
TableColumn => 's.type_id',
IDRef => \@States,
);
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= $InString;
}
# search for keywords
if ( $Param{Keyword} ) {
$Param{Keyword} =~ s/,/&&/g;
$Param{Keyword} =~ s/;/&&/g;
$Param{Keyword} =~ s/ /&&/g;
if ($Ext) {
$Ext .= ' AND ';
}
# add the SQL for the keyword search
$Ext .= $DBObject->QueryCondition(
Key => 'i.f_keywords',
Value => $Param{Keyword},
SearchPrefix => '*',
SearchSuffix => '*',
);
}
# show only approved FAQ articles for public and customer interface
if ( $Param{Interface}->{Name} eq 'public' || $Param{Interface}->{Name} eq 'external' ) {
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= ' i.approved = 1';
}
# otherwise check if need to search for approved status
elsif ( defined $Param{Approved} ) {
my $ApprovedValue = $Param{Approved} ? 1 : 0;
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= " i.approved = $ApprovedValue";
}
# search for create users
if (
$Param{CreatedUserIDs}
&& ref $Param{CreatedUserIDs} eq 'ARRAY'
&& @{ $Param{CreatedUserIDs} }
)
{
my $InString = $Self->_InConditionGet(
TableColumn => 'i.created_by',
IDRef => $Param{CreatedUserIDs},
);
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= $InString;
}
# search for last change users
if (
$Param{LastChangedUserIDs}
&& ref $Param{LastChangedUserIDs} eq 'ARRAY'
&& @{ $Param{LastChangedUserIDs} }
)
{
my $InString = $Self->_InConditionGet(
TableColumn => 'i.changed_by',
IDRef => $Param{LastChangedUserIDs},
);
if ($Ext) {
$Ext .= ' AND ';
}
$Ext .= $InString;
}
# Search for create and change times.
# Remember current time to prevent searches for future timestamps.
my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');
my $CurrentSystemTime = $DateTimeObject->ToEpoch();
# get FAQ items created older than x minutes
if ( defined $Param{ItemCreateTimeOlderMinutes} ) {
$Param{ItemCreateTimeOlderMinutes} ||= 0;
my $DateTime = $Kernel::OM->Create('Kernel::System::DateTime');
$DateTime->Subtract( Seconds => $Param{ItemCreateTimeOlderMinutes} * 60 );
$Param{ItemCreateTimeOlderDate} = $DateTime->ToString();
}
# get FAQ items created newer than x minutes
if ( defined $Param{ItemCreateTimeNewerMinutes} ) {
$Param{ItemCreateTimeNewerMinutes} ||= 0;
my $DateTime = $Kernel::OM->Create('Kernel::System::DateTime');
$DateTime->Subtract( Seconds => $Param{ItemCreateTimeNewerMinutes} * 60 );
$Param{ItemCreateTimeNewerDate} = $DateTime->ToString();
}
# get FAQ items created older than xxxx-xx-xx xx:xx date
my $CompareCreateTimeOlderNewerDate;
if ( $Param{ItemCreateTimeOlderDate} ) {
# check time format
if (
$Param{ItemCreateTimeOlderDate}
!~ /\d\d\d\d-(\d\d|\d)-(\d\d|\d) (\d\d|\d):(\d\d|\d):(\d\d|\d)/
)
{
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Invalid time format '$Param{ItemCreateTimeOlderDate}'!",
);
return;
}
my $Time = $Kernel::OM->Create(
'Kernel::System::DateTime',
ObjectParams => {
String => $Param{ItemCreateTimeOlderDate},
}
)->ToEpoch();
if ( !$Time ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message =>
"Search not executed due to invalid time '"
. $Param{ItemCreateTimeOlderDate} . "'!",
);
return;
}
$CompareCreateTimeOlderNewerDate = $Time;
$Ext .= " AND i.created <= '"
. $DBObject->Quote( $Param{ItemCreateTimeOlderDate} ) . "'";
}
# get Items changed newer than xxxx-xx-xx xx:xx date
if ( $Param{ItemCreateTimeNewerDate} ) {
if (
$Param{ItemCreateTimeNewerDate}
!~ /\d\d\d\d-(\d\d|\d)-(\d\d|\d) (\d\d|\d):(\d\d|\d):(\d\d|\d)/
)
{
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Invalid time format '$Param{ItemCreateTimeNewerDate}'!",
);
return;
}
my $Time = $Kernel::OM->Create(
'Kernel::System::DateTime',
ObjectParams => {
String => $Param{ItemCreateTimeNewerDate},
}
)->ToEpoch();
if ( !$Time ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message =>
"Search not executed due to invalid time '"
. $Param{ItemCreateTimeNewerDate} . "'!",
);
return;
}
# don't execute queries if newer date is after current date
return if $Time > $CurrentSystemTime;
# don't execute queries if older/newer date restriction show now valid time frame
return if $CompareCreateTimeOlderNewerDate && $Time > $CompareCreateTimeOlderNewerDate;
$Ext .= " AND i.created >= '"
. $DBObject->Quote( $Param{ItemCreateTimeNewerDate} ) . "'";
}
# get FAQ items changed older than x minutes
if ( defined $Param{ItemChangeTimeOlderMinutes} ) {
$Param{ItemChangeTimeOlderMinutes} ||= 0;
my $DateTime = $Kernel::OM->Create('Kernel::System::DateTime');
$DateTime->Subtract( Seconds => $Param{ItemChangeTimeOlderMinutes} * 60 );
$Param{ItemChangeTimeOlderDate} = $DateTime->ToString();
}
# get FAQ items changed newer than x minutes
if ( defined $Param{ItemChangeTimeNewerMinutes} ) {
$Param{ItemChangeTimeNewerMinutes} ||= 0;
my $DateTime = $Kernel::OM->Create('Kernel::System::DateTime');
$DateTime->Subtract( Seconds => $Param{ItemChangeTimeNewerMinutes} * 60 );
$Param{ItemChangeTimeNewerDate} = $DateTime->ToString();
}
# get FAQ items changed older than xxxx-xx-xx xx:xx date
my $CompareChangeTimeOlderNewerDate;
if ( $Param{ItemChangeTimeOlderDate} ) {
# check time format
if (
$Param{ItemChangeTimeOlderDate}
!~ /\d\d\d\d-(\d\d|\d)-(\d\d|\d) (\d\d|\d):(\d\d|\d):(\d\d|\d)/
)
{
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Invalid time format '$Param{ItemChangeTimeOlderDate}'!",
);
return;
}
my $Time = $Kernel::OM->Create(
'Kernel::System::DateTime',
ObjectParams => {
String => $Param{ItemChangeTimeOlderDate},
}
)->ToEpoch();
if ( !$Time ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message =>
"Search not executed due to invalid time '"
. $Param{ItemChangeTimeOlderDate} . "'!",
);
return;
}
$CompareChangeTimeOlderNewerDate = $Time;
$Ext .= " AND i.changed <= '"
. $DBObject->Quote( $Param{ItemChangeTimeOlderDate} ) . "'";
}
# get Items changed newer than xxxx-xx-xx xx:xx date
if ( $Param{ItemChangeTimeNewerDate} ) {
if (
$Param{ItemChangeTimeNewerDate}
!~ /\d\d\d\d-(\d\d|\d)-(\d\d|\d) (\d\d|\d):(\d\d|\d):(\d\d|\d)/
)
{
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Invalid time format '$Param{ItemChangeTimeNewerDate}'!",
);
return;
}
my $Time = $Kernel::OM->Create(
'Kernel::System::DateTime',
ObjectParams => {
String => $Param{ItemChangeTimeNewerDate},
}
)->ToEpoch();
if ( !$Time ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message =>
"Search not executed due to invalid time '"
. $Param{ItemChangeTimeNewerDate} . "'!",
);
return;
}
# don't execute queries if newer date is after current date
return if $Time > $CurrentSystemTime;
# don't execute queries if older/newer date restriction show now valid time frame
return if $CompareChangeTimeOlderNewerDate && $Time > $CompareChangeTimeOlderNewerDate;
$Ext .= " AND i.changed >= '"
. $DBObject->Quote( $Param{ItemChangeTimeNewerDate} ) . "'";
}
# add WHERE statement
if ($Ext) {
$Ext = ' WHERE ' . $Ext;
}
# Remember already joined tables for sorting.
my %DynamicFieldJoinTables;
my $DynamicFieldJoinCounter = 1;
# get dynamic field back-end object
my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');
DYNAMIC_FIELD:
for my $DynamicField ( @{$FAQDynamicFields} ) {
my $SearchParam = $Param{ "DynamicField_" . $DynamicField->{Name} };
next DYNAMIC_FIELD if ( !$SearchParam );
next DYNAMIC_FIELD if ( ref $SearchParam ne 'HASH' );
my $NeedJoin;
for my $Operator ( sort keys %{$SearchParam} ) {
my @SearchParams = ( ref $SearchParam->{$Operator} eq 'ARRAY' )
? @{ $SearchParam->{$Operator} }
: ( $SearchParam->{$Operator} );
my $SQLExtSub = ' AND (';
my $Counter = 0;
TEXT:
for my $Text (@SearchParams) {
next TEXT if ( !defined $Text || $Text eq '' );
$Text =~ s/\*/%/gi;
# check search attribute, we do not need to search for *
next TEXT if $Text =~ /^\%{1,3}$/;
# validate data type
my $ValidateSuccess = $DynamicFieldBackendObject->ValueValidate(
DynamicFieldConfig => $DynamicField,
Value => $Text,
UserID => $Param{UserID},
);
if ( !$ValidateSuccess ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message =>
"Search not executed due to invalid value '"
. $Text
. "' on field '"
. $DynamicField->{Name}
. "'!",
);
return;
}
if ($Counter) {
$SQLExtSub .= ' OR ';
}
$SQLExtSub .= $DynamicFieldBackendObject->SearchSQLGet(
DynamicFieldConfig => $DynamicField,
TableAlias => "dfv$DynamicFieldJoinCounter",
Operator => $Operator,
SearchTerm => $Text,
);
$Counter++;
}
$SQLExtSub .= ')';
if ($Counter) {
$Ext .= $SQLExtSub;
$NeedJoin = 1;
}
}
if ($NeedJoin) {
# Join the table for this dynamic field
$SQL .= " INNER JOIN dynamic_field_value dfv$DynamicFieldJoinCounter
ON (i.id = dfv$DynamicFieldJoinCounter.object_id
AND dfv$DynamicFieldJoinCounter.field_id = " .
$DBObject->Quote( $DynamicField->{ID}, 'Integer' ) . ") ";
$DynamicFieldJoinTables{ $DynamicField->{Name} } = "dfv$DynamicFieldJoinCounter";
$DynamicFieldJoinCounter++;
}
}
# add GROUP BY
$Ext
.= ' GROUP BY i.id, i.f_subject, i.f_language_id, i.created, i.changed, s.name, v.item_id ';
# add HAVING clause ( Votes and Rate are aggregated columns, they can't be in the WHERE clause)
# defined voting parameters (for Votes and Rate)
my %VotingOperators = (
Equals => '=',
GreaterThan => '>',
GreaterThanEquals => '>=',
SmallerThan => '<',
SmallerThanEquals => '<=',
);
my $HavingPrint;
my $AddedCondition;
HAVING_PARAM:
for my $HavingParam (qw(Votes Rate)) {
my $SearchParam = $Param{$HavingParam};
next HAVING_PARAM if ( !$SearchParam );
next HAVING_PARAM if ( ref $SearchParam ne 'HASH' );
OPERATOR:
for my $Operator ( sort keys %{$SearchParam} ) {
next OPERATOR if !( $VotingOperators{$Operator} );
# print HAVING clause just once if and just if the operator is valid
if ( !$HavingPrint ) {
$Ext .= ' HAVING ';
$HavingPrint = 1;
}
my $SQLExtSub;
my @SearchParams = ( ref $SearchParam->{$Operator} eq 'ARRAY' )
? @{ $SearchParam->{$Operator} }
: ( $SearchParam->{$Operator} );
# do not use AND on the first condition
if ($AddedCondition) {
$SQLExtSub .= ' AND (';
}
else {
$SQLExtSub .= ' (';
}
my $Counter = 0;
TEXT:
for my $Text (@SearchParams) {
next TEXT if ( !defined $Text || $Text eq '' );
$Text =~ s/\*/%/gi;
# check search attribute, we do not need to search for *
next TEXT if $Text =~ /^\%{1,3}$/;
$SQLExtSub .= ' OR ' if ($Counter);
# define aggregation column
my $AggregateColumn = 'count( v.item_id )';
if ( $HavingParam eq 'Rate' ) {
$AggregateColumn = 'avg( v.rate )';
}
# set condition
$SQLExtSub .= " $AggregateColumn $VotingOperators{$Operator} ";
$SQLExtSub .= $DBObject->Quote( $Text, 'Number' ) . " ";
$Counter++;
}
# close condition
$SQLExtSub .= ') ';
# add condition to the final SQL statement
if ($Counter) {
$Ext .= $SQLExtSub;
$AddedCondition = 1;
}
}
}
# database query for sort/order by option
my $ExtOrderBy = ' ORDER BY';
for my $Count ( 0 .. $#{ $Param{OrderBy} } ) {
if ( $Count > 0 ) {
$ExtOrderBy .= ',';
}
# sort by dynamic field
if ( $ValidDynamicFieldParams{ $Param{OrderBy}->[$Count] } ) {
my ($DynamicFieldName) = $Param{OrderBy}->[$Count] =~ m/^DynamicField_(.*)$/smx;
my $DynamicField = $FAQDynamicFieldName2Config{$DynamicFieldName};
# If the table was already joined for searching, we reuse it.
if ( !$DynamicFieldJoinTables{$DynamicFieldName} ) {
# Join the table for this dynamic field; use a left outer join in this case.
# With an INNER JOIN we'd limit the result set to tickets which have an entry
# for the DF which is used for sorting.
$SQL
.= " LEFT OUTER JOIN dynamic_field_value dfv$DynamicFieldJoinCounter
ON (i.id = dfv$DynamicFieldJoinCounter.object_id
AND dfv$DynamicFieldJoinCounter.field_id = " .
$DBObject->Quote( $DynamicField->{ID}, 'Integer' ) . ") ";
$DynamicFieldJoinTables{ $DynamicField->{Name} } = "dfv$DynamicFieldJoinCounter";
$DynamicFieldJoinCounter++;
}
my $SQLOrderField = $DynamicFieldBackendObject->SearchSQLOrderFieldGet(
DynamicFieldConfig => $DynamicField,
TableAlias => $DynamicFieldJoinTables{$DynamicFieldName},
);
$Ext .= ", $SQLOrderField ";
$ExtOrderBy .= " $SQLOrderField ";
}
else {
# Regular sort.
$ExtOrderBy .= ' ' . $OrderByTable{ $Param{OrderBy}->[$Count] };
}
if ( $Param{OrderByDirection}->[$Count] eq 'Up' ) {
$ExtOrderBy .= ' ASC';
}
else {
$ExtOrderBy .= ' DESC';
}
}
# if there is a possibility that the ordering is not determined
# we add an descending ordering by id
if ( !grep { $_ eq 'FAQID' } ( @{ $Param{OrderBy} } ) ) {
if ( $#{ $Param{OrderBy} } >= 0 ) {
$ExtOrderBy .= ',';
}
# set default order by direction
my $OrderByDirection = 'ASC';
# try to get the order by direction of the last
# used 'Created' or 'Changed' OrderBy parameters
my $Count = 0;
for my $OrderBy ( @{ $Param{OrderBy} } ) {
if ( $OrderBy eq 'Created' || $OrderBy eq 'Changed' ) {
if ( $Param{OrderByDirection}->[$Count] eq 'Up' ) {
$OrderByDirection = 'ASC';
}
else {
$OrderByDirection = 'DESC';
}
}
$Count++;
}
$ExtOrderBy .= ' ' . $OrderByTable{FAQID} . ' ' . $OrderByDirection;
}
# add extended SQL
$SQL .= $Ext . $ExtOrderBy;
# ask database
return if !$DBObject->Prepare(
SQL => $SQL,
Limit => $Param{Limit} || 500,
);
# fetch the result
my @List;
while ( my @Row = $DBObject->FetchrowArray() ) {
push @List, $Row[0];
}
return @List;
}
=head1 PRIVATE FUNCTIONS
=head2 _InConditionGet()
internal function to create an
table.column IN (values)
condition string from an array.
my $SQLPart = $TicketObject->_InConditionGet(
TableColumn => 'table.column',
IDRef => $ArrayRef,
);
=cut
sub _InConditionGet {
my ( $Self, %Param ) = @_;
if ( !$Param{TableColumn} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need TableColumn!",
);
return;
}
if ( !IsArrayRefWithData( $Param{IDRef} ) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need IDRef!",
);
return;
}
# sort ids to cache the SQL query
my @SortedIDs = sort { $a <=> $b } @{ $Param{IDRef} };
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# Error out if some values were not integers.
@SortedIDs = map { $Kernel::OM->Get('Kernel::System::DB')->Quote( $_, 'Integer' ) } @SortedIDs;
return if scalar @SortedIDs != scalar @{ $Param{IDRef} };
# split IN statement with more than 900 elements in more statements combined with OR
# because Oracle doesn't support more than 1000 elements in one IN statement.
my @SQLStrings;
LOOP:
while ( scalar @SortedIDs ) {
my @SortedIDsPart = splice @SortedIDs, 0, 900;
my $IDString = join ', ', @SortedIDsPart;
push @SQLStrings, " $Param{TableColumn} IN ($IDString) ";
}
my $SQL = join ' OR ', @SQLStrings;
$SQL = ' ( ' . $SQL . ' ) ';
return $SQL;
}
1;
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L).
This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L.
=cut