1114 lines
34 KiB
Perl
1114 lines
34 KiB
Perl
# --
|
|
# 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<https://otrs.org/>).
|
|
|
|
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<https://www.gnu.org/licenses/gpl-3.0.txt>.
|
|
|
|
=cut
|