# -- # 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