2017-02-02 12:19:16 -05:00
< ? php
/**
* @ copyright Copyright ( c ) 2017 Robin Appelman < robin @ icewind . nl >
*
2020-04-29 05:57:22 -04:00
* @ author Christoph Wurst < christoph @ winzerhof - wurst . at >
2017-11-06 09:56:42 -05:00
* @ author Robin Appelman < robin @ icewind . nl >
2019-12-03 13:57:53 -05:00
* @ author Roeland Jago Douma < roeland @ famdouma . nl >
2019-12-19 07:16:31 -05:00
* @ author Tobias Kaminsky < tobias @ kaminsky . me >
2017-11-06 09:56:42 -05:00
*
2017-02-02 12:19:16 -05:00
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
2021-06-04 15:52:51 -04:00
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
2017-02-02 12:19:16 -05:00
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
2019-12-03 13:57:53 -05:00
* along with this program . If not , see < http :// www . gnu . org / licenses />.
2017-02-02 12:19:16 -05:00
*
*/
namespace OC\Files\Cache ;
2021-05-05 12:09:53 -04:00
use OC\Files\Search\SearchBinaryOperator ;
2021-05-04 13:06:02 -04:00
use OC\SystemConfig ;
2017-02-02 12:19:16 -05:00
use OCP\DB\QueryBuilder\IQueryBuilder ;
2021-05-04 13:06:02 -04:00
use OCP\Files\Cache\ICache ;
2017-02-02 12:19:16 -05:00
use OCP\Files\IMimeTypeLoader ;
use OCP\Files\Search\ISearchBinaryOperator ;
use OCP\Files\Search\ISearchComparison ;
use OCP\Files\Search\ISearchOperator ;
2017-03-15 09:46:23 -04:00
use OCP\Files\Search\ISearchOrder ;
2021-05-04 13:06:02 -04:00
use OCP\Files\Search\ISearchQuery ;
use OCP\IDBConnection ;
use OCP\ILogger ;
2017-02-02 12:19:16 -05:00
/**
* Tools for transforming search queries into database queries
*/
2017-02-02 12:20:08 -05:00
class QuerySearchHelper {
2020-04-10 10:48:31 -04:00
protected static $searchOperatorMap = [
2017-02-02 12:19:16 -05:00
ISearchComparison :: COMPARE_LIKE => 'iLike' ,
ISearchComparison :: COMPARE_EQUAL => 'eq' ,
ISearchComparison :: COMPARE_GREATER_THAN => 'gt' ,
ISearchComparison :: COMPARE_GREATER_THAN_EQUAL => 'gte' ,
ISearchComparison :: COMPARE_LESS_THAN => 'lt' ,
2021-05-05 12:09:53 -04:00
ISearchComparison :: COMPARE_LESS_THAN_EQUAL => 'lte' ,
2017-02-02 12:19:16 -05:00
];
2020-04-10 10:48:31 -04:00
protected static $searchOperatorNegativeMap = [
2017-02-02 12:19:16 -05:00
ISearchComparison :: COMPARE_LIKE => 'notLike' ,
ISearchComparison :: COMPARE_EQUAL => 'neq' ,
ISearchComparison :: COMPARE_GREATER_THAN => 'lte' ,
ISearchComparison :: COMPARE_GREATER_THAN_EQUAL => 'lt' ,
ISearchComparison :: COMPARE_LESS_THAN => 'gte' ,
2021-05-05 12:09:53 -04:00
ISearchComparison :: COMPARE_LESS_THAN_EQUAL => 'lt' ,
2017-02-02 12:19:16 -05:00
];
2020-04-10 10:54:27 -04:00
public const TAG_FAVORITE = '_$!<Favorite>!$_' ;
2017-03-08 09:17:39 -05:00
2017-02-02 12:19:16 -05:00
/** @var IMimeTypeLoader */
private $mimetypeLoader ;
2021-05-04 13:06:02 -04:00
/** @var IDBConnection */
private $connection ;
/** @var SystemConfig */
private $systemConfig ;
/** @var ILogger */
private $logger ;
2017-02-02 12:19:16 -05:00
2021-05-04 13:06:02 -04:00
public function __construct (
IMimeTypeLoader $mimetypeLoader ,
IDBConnection $connection ,
SystemConfig $systemConfig ,
ILogger $logger
) {
2017-02-02 12:19:16 -05:00
$this -> mimetypeLoader = $mimetypeLoader ;
2021-05-04 13:06:02 -04:00
$this -> connection = $connection ;
$this -> systemConfig = $systemConfig ;
$this -> logger = $logger ;
2017-02-02 12:19:16 -05:00
}
2017-03-08 09:17:39 -05:00
/**
* Whether or not the tag tables should be joined to complete the search
*
* @ param ISearchOperator $operator
* @ return boolean
*/
public function shouldJoinTags ( ISearchOperator $operator ) {
if ( $operator instanceof ISearchBinaryOperator ) {
return array_reduce ( $operator -> getArguments (), function ( $shouldJoin , ISearchOperator $operator ) {
return $shouldJoin || $this -> shouldJoinTags ( $operator );
}, false );
2020-04-10 04:35:09 -04:00
} elseif ( $operator instanceof ISearchComparison ) {
2017-03-08 09:17:39 -05:00
return $operator -> getField () === 'tagname' || $operator -> getField () === 'favorite' ;
}
return false ;
}
2018-01-16 07:22:28 -05:00
/**
* @ param IQueryBuilder $builder
* @ param ISearchOperator $operator
*/
public function searchOperatorArrayToDBExprArray ( IQueryBuilder $builder , array $operators ) {
2019-11-08 09:05:21 -05:00
return array_filter ( array_map ( function ( $operator ) use ( $builder ) {
2018-01-16 07:22:28 -05:00
return $this -> searchOperatorToDBExpr ( $builder , $operator );
2019-11-08 09:05:21 -05:00
}, $operators ));
2018-01-16 07:22:28 -05:00
}
2017-02-02 12:19:16 -05:00
public function searchOperatorToDBExpr ( IQueryBuilder $builder , ISearchOperator $operator ) {
$expr = $builder -> expr ();
if ( $operator instanceof ISearchBinaryOperator ) {
2019-11-08 09:05:21 -05:00
if ( count ( $operator -> getArguments ()) === 0 ) {
return null ;
}
2017-02-02 12:19:16 -05:00
switch ( $operator -> getType ()) {
case ISearchBinaryOperator :: OPERATOR_NOT :
$negativeOperator = $operator -> getArguments ()[ 0 ];
if ( $negativeOperator instanceof ISearchComparison ) {
return $this -> searchComparisonToDBExpr ( $builder , $negativeOperator , self :: $searchOperatorNegativeMap );
} else {
throw new \InvalidArgumentException ( 'Binary operators inside "not" is not supported' );
}
2021-05-05 12:09:53 -04:00
// no break
2017-02-02 12:19:16 -05:00
case ISearchBinaryOperator :: OPERATOR_AND :
2018-01-16 07:22:28 -05:00
return call_user_func_array ([ $expr , 'andX' ], $this -> searchOperatorArrayToDBExprArray ( $builder , $operator -> getArguments ()));
2017-02-02 12:19:16 -05:00
case ISearchBinaryOperator :: OPERATOR_OR :
2018-01-16 07:22:28 -05:00
return call_user_func_array ([ $expr , 'orX' ], $this -> searchOperatorArrayToDBExprArray ( $builder , $operator -> getArguments ()));
2017-02-02 12:19:16 -05:00
default :
throw new \InvalidArgumentException ( 'Invalid operator type: ' . $operator -> getType ());
}
2020-04-10 04:35:09 -04:00
} elseif ( $operator instanceof ISearchComparison ) {
2017-02-02 12:19:16 -05:00
return $this -> searchComparisonToDBExpr ( $builder , $operator , self :: $searchOperatorMap );
} else {
throw new \InvalidArgumentException ( 'Invalid operator type: ' . get_class ( $operator ));
}
}
private function searchComparisonToDBExpr ( IQueryBuilder $builder , ISearchComparison $comparison , array $operatorMap ) {
2017-02-02 12:20:08 -05:00
$this -> validateComparison ( $comparison );
2017-02-02 12:19:16 -05:00
2021-01-12 04:15:48 -05:00
[ $field , $value , $type ] = $this -> getOperatorFieldAndValue ( $comparison );
2017-02-02 12:20:08 -05:00
if ( isset ( $operatorMap [ $type ])) {
$queryOperator = $operatorMap [ $type ];
2017-02-02 12:19:16 -05:00
return $builder -> expr () -> $queryOperator ( $field , $this -> getParameterForValue ( $builder , $value ));
} else {
throw new \InvalidArgumentException ( 'Invalid operator type: ' . $comparison -> getType ());
}
}
private function getOperatorFieldAndValue ( ISearchComparison $operator ) {
$field = $operator -> getField ();
$value = $operator -> getValue ();
2017-02-02 12:20:08 -05:00
$type = $operator -> getType ();
2017-02-02 12:19:16 -05:00
if ( $field === 'mimetype' ) {
if ( $operator -> getType () === ISearchComparison :: COMPARE_EQUAL ) {
2019-10-28 16:56:05 -04:00
$value = ( int ) $this -> mimetypeLoader -> getId ( $value );
2020-04-10 04:35:09 -04:00
} elseif ( $operator -> getType () === ISearchComparison :: COMPARE_LIKE ) {
2017-02-02 12:19:16 -05:00
// transform "mimetype='foo/%'" to "mimepart='foo'"
if ( preg_match ( '|(.+)/%|' , $value , $matches )) {
$field = 'mimepart' ;
2019-10-28 16:56:05 -04:00
$value = ( int ) $this -> mimetypeLoader -> getId ( $matches [ 1 ]);
2017-02-02 12:20:08 -05:00
$type = ISearchComparison :: COMPARE_EQUAL ;
2020-04-10 04:35:09 -04:00
} elseif ( strpos ( $value , '%' ) !== false ) {
2017-02-02 12:20:08 -05:00
throw new \InvalidArgumentException ( 'Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported' );
2019-10-28 16:56:05 -04:00
} else {
$field = 'mimetype' ;
$value = ( int ) $this -> mimetypeLoader -> getId ( $value );
$type = ISearchComparison :: COMPARE_EQUAL ;
2017-02-02 12:19:16 -05:00
}
}
2020-04-10 04:35:09 -04:00
} elseif ( $field === 'favorite' ) {
2017-03-08 09:17:39 -05:00
$field = 'tag.category' ;
$value = self :: TAG_FAVORITE ;
2020-04-10 04:35:09 -04:00
} elseif ( $field === 'tagname' ) {
2017-03-08 09:17:39 -05:00
$field = 'tag.category' ;
2020-04-10 04:35:09 -04:00
} elseif ( $field === 'fileid' ) {
2019-12-10 03:47:30 -05:00
$field = 'file.fileid' ;
2021-01-19 09:30:27 -05:00
} elseif ( $field === 'path' && $type === ISearchComparison :: COMPARE_EQUAL ) {
$field = 'path_hash' ;
$value = md5 (( string ) $value );
2017-02-02 12:19:16 -05:00
}
2017-02-02 12:20:08 -05:00
return [ $field , $value , $type ];
2017-02-02 12:19:16 -05:00
}
2017-02-02 12:20:08 -05:00
private function validateComparison ( ISearchComparison $operator ) {
2017-02-02 12:19:16 -05:00
$types = [
'mimetype' => 'string' ,
2017-02-02 12:20:08 -05:00
'mtime' => 'integer' ,
2017-02-02 12:19:16 -05:00
'name' => 'string' ,
2021-01-14 13:03:39 -05:00
'path' => 'string' ,
2017-03-08 09:17:39 -05:00
'size' => 'integer' ,
'tagname' => 'string' ,
2017-04-05 09:12:30 -04:00
'favorite' => 'boolean' ,
2021-05-05 12:09:53 -04:00
'fileid' => 'integer' ,
'storage' => 'integer' ,
2017-02-02 12:19:16 -05:00
];
$comparisons = [
'mimetype' => [ 'eq' , 'like' ],
'mtime' => [ 'eq' , 'gt' , 'lt' , 'gte' , 'lte' ],
'name' => [ 'eq' , 'like' ],
2021-01-14 13:03:39 -05:00
'path' => [ 'eq' , 'like' ],
2017-03-08 09:17:39 -05:00
'size' => [ 'eq' , 'gt' , 'lt' , 'gte' , 'lte' ],
'tagname' => [ 'eq' , 'like' ],
'favorite' => [ 'eq' ],
2021-05-05 12:09:53 -04:00
'fileid' => [ 'eq' ],
'storage' => [ 'eq' ],
2017-02-02 12:19:16 -05:00
];
if ( ! isset ( $types [ $operator -> getField ()])) {
2017-02-02 12:20:08 -05:00
throw new \InvalidArgumentException ( 'Unsupported comparison field ' . $operator -> getField ());
2017-02-02 12:19:16 -05:00
}
$type = $types [ $operator -> getField ()];
2017-02-02 12:20:08 -05:00
if ( gettype ( $operator -> getValue ()) !== $type ) {
throw new \InvalidArgumentException ( 'Invalid type for field ' . $operator -> getField ());
}
if ( ! in_array ( $operator -> getType (), $comparisons [ $operator -> getField ()])) {
throw new \InvalidArgumentException ( 'Unsupported comparison for field ' . $operator -> getField () . ': ' . $operator -> getType ());
2017-02-02 12:19:16 -05:00
}
}
private function getParameterForValue ( IQueryBuilder $builder , $value ) {
if ( $value instanceof \DateTime ) {
$value = $value -> getTimestamp ();
}
if ( is_numeric ( $value )) {
$type = IQueryBuilder :: PARAM_INT ;
} else {
$type = IQueryBuilder :: PARAM_STR ;
}
return $builder -> createNamedParameter ( $value , $type );
}
2017-03-15 09:46:23 -04:00
/**
* @ param IQueryBuilder $query
* @ param ISearchOrder [] $orders
*/
public function addSearchOrdersToQuery ( IQueryBuilder $query , array $orders ) {
foreach ( $orders as $order ) {
$query -> addOrderBy ( $order -> getField (), $order -> getDirection ());
}
}
2021-05-04 13:06:02 -04:00
protected function getQueryBuilder () {
return new CacheQueryBuilder (
$this -> connection ,
$this -> systemConfig ,
$this -> logger
);
}
/**
* @ param ISearchQuery $searchQuery
* @ param ICache [] $caches
* @ return CacheEntry []
*/
public function searchInCaches ( ISearchQuery $searchQuery , array $caches ) : array {
// search in multiple caches at once by creating one query in the following format
// SELECT ... FROM oc_filecache WHERE
// <filter expressions from the search query>
// AND (
// <filter expression for storage1> OR
// <filter expression for storage2> OR
// ...
// );
//
// This gives us all the files matching the search query from all caches
//
// while the resulting rows don't have a way to tell what storage they came from (multiple storages/caches can share storage_id)
// we can just ask every cache if the row belongs to them and give them the cache to do any post processing on the result.
$builder = $this -> getQueryBuilder ();
$query = $builder -> selectFileCache ( 'file' );
if ( $this -> shouldJoinTags ( $searchQuery -> getSearchOperation ())) {
$user = $searchQuery -> getUser ();
if ( $user === null ) {
throw new \InvalidArgumentException ( " Searching by tag requires the user to be set in the query " );
}
$query
-> innerJoin ( 'file' , 'vcategory_to_object' , 'tagmap' , $builder -> expr () -> eq ( 'file.fileid' , 'tagmap.objid' ))
-> innerJoin ( 'tagmap' , 'vcategory' , 'tag' , $builder -> expr () -> andX (
$builder -> expr () -> eq ( 'tagmap.type' , 'tag.type' ),
$builder -> expr () -> eq ( 'tagmap.categoryid' , 'tag.id' )
))
-> andWhere ( $builder -> expr () -> eq ( 'tag.type' , $builder -> createNamedParameter ( 'files' )))
-> andWhere ( $builder -> expr () -> eq ( 'tag.uid' , $builder -> createNamedParameter ( $user -> getUID ())));
}
$searchExpr = $this -> searchOperatorToDBExpr ( $builder , $searchQuery -> getSearchOperation ());
if ( $searchExpr ) {
$query -> andWhere ( $searchExpr );
}
2021-05-05 12:09:53 -04:00
$storageFilters = array_map ( function ( ICache $cache ) {
return $cache -> getQueryFilterForStorage ();
}, $caches );
$query -> andWhere ( $this -> searchOperatorToDBExpr ( $builder , new SearchBinaryOperator ( ISearchBinaryOperator :: OPERATOR_OR , $storageFilters )));
2021-05-04 13:06:02 -04:00
if ( $searchQuery -> limitToHome () && ( $this instanceof HomeCache )) {
$query -> andWhere ( $builder -> expr () -> like ( 'path' , $query -> expr () -> literal ( 'files/%' )));
}
$this -> addSearchOrdersToQuery ( $query , $searchQuery -> getOrder ());
if ( $searchQuery -> getLimit ()) {
$query -> setMaxResults ( $searchQuery -> getLimit ());
}
if ( $searchQuery -> getOffset ()) {
$query -> setFirstResult ( $searchQuery -> getOffset ());
}
$result = $query -> execute ();
$files = $result -> fetchAll ();
$rawEntries = array_map ( function ( array $data ) {
return Cache :: cacheEntryFromData ( $data , $this -> mimetypeLoader );
}, $files );
$result -> closeCursor ();
// loop trough all caches for each result to see if the result matches that storage
$results = [];
foreach ( $rawEntries as $rawEntry ) {
foreach ( $caches as $cache ) {
$entry = $cache -> getCacheEntryFromSearchResult ( $rawEntry );
if ( $entry ) {
$results [] = $entry ;
}
}
}
return $results ;
}
2017-02-02 12:19:16 -05:00
}