2017-02-02 20:19:16 +03:00
< ? php
/**
* @ copyright Copyright ( c ) 2017 Robin Appelman < robin @ icewind . nl >
*
2017-11-06 17:56:42 +03:00
* @ author Robin Appelman < robin @ icewind . nl >
2019-12-03 21:57:53 +03:00
* @ author Roeland Jago Douma < roeland @ famdouma . nl >
2019-12-19 15:16:31 +03:00
* @ author Tobias Kaminsky < tobias @ kaminsky . me >
2017-11-06 17:56:42 +03:00
*
2017-02-02 20:19:16 +03: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
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
2019-12-03 21:57:53 +03:00
* along with this program . If not , see < http :// www . gnu . org / licenses />.
2017-02-02 20:19:16 +03:00
*
*/
namespace OC\Files\Cache ;
use OCP\DB\QueryBuilder\IQueryBuilder ;
use OCP\Files\IMimeTypeLoader ;
use OCP\Files\Search\ISearchBinaryOperator ;
use OCP\Files\Search\ISearchComparison ;
use OCP\Files\Search\ISearchOperator ;
2017-03-15 16:46:23 +03:00
use OCP\Files\Search\ISearchOrder ;
2017-02-02 20:19:16 +03:00
/**
* Tools for transforming search queries into database queries
*/
2017-02-02 20:20:08 +03:00
class QuerySearchHelper {
2017-02-02 20:19:16 +03:00
static protected $searchOperatorMap = [
ISearchComparison :: COMPARE_LIKE => 'iLike' ,
ISearchComparison :: COMPARE_EQUAL => 'eq' ,
ISearchComparison :: COMPARE_GREATER_THAN => 'gt' ,
ISearchComparison :: COMPARE_GREATER_THAN_EQUAL => 'gte' ,
ISearchComparison :: COMPARE_LESS_THAN => 'lt' ,
ISearchComparison :: COMPARE_LESS_THAN_EQUAL => 'lte'
];
static protected $searchOperatorNegativeMap = [
ISearchComparison :: COMPARE_LIKE => 'notLike' ,
ISearchComparison :: COMPARE_EQUAL => 'neq' ,
ISearchComparison :: COMPARE_GREATER_THAN => 'lte' ,
ISearchComparison :: COMPARE_GREATER_THAN_EQUAL => 'lt' ,
ISearchComparison :: COMPARE_LESS_THAN => 'gte' ,
ISearchComparison :: COMPARE_LESS_THAN_EQUAL => 'lt'
];
2017-03-08 17:17:39 +03:00
const TAG_FAVORITE = '_$!<Favorite>!$_' ;
2017-02-02 20:19:16 +03:00
/** @var IMimeTypeLoader */
private $mimetypeLoader ;
/**
* QuerySearchUtil constructor .
*
* @ param IMimeTypeLoader $mimetypeLoader
*/
public function __construct ( IMimeTypeLoader $mimetypeLoader ) {
$this -> mimetypeLoader = $mimetypeLoader ;
}
2017-03-08 17:17:39 +03: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 );
} else if ( $operator instanceof ISearchComparison ) {
return $operator -> getField () === 'tagname' || $operator -> getField () === 'favorite' ;
}
return false ;
}
2018-01-16 15:22:28 +03:00
/**
* @ param IQueryBuilder $builder
* @ param ISearchOperator $operator
*/
public function searchOperatorArrayToDBExprArray ( IQueryBuilder $builder , array $operators ) {
2019-11-08 17:05:21 +03:00
return array_filter ( array_map ( function ( $operator ) use ( $builder ) {
2018-01-16 15:22:28 +03:00
return $this -> searchOperatorToDBExpr ( $builder , $operator );
2019-11-08 17:05:21 +03:00
}, $operators ));
2018-01-16 15:22:28 +03:00
}
2017-02-02 20:19:16 +03:00
public function searchOperatorToDBExpr ( IQueryBuilder $builder , ISearchOperator $operator ) {
$expr = $builder -> expr ();
if ( $operator instanceof ISearchBinaryOperator ) {
2019-11-08 17:05:21 +03:00
if ( count ( $operator -> getArguments ()) === 0 ) {
return null ;
}
2017-02-02 20:19:16 +03: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' );
}
case ISearchBinaryOperator :: OPERATOR_AND :
2018-01-16 15:22:28 +03:00
return call_user_func_array ([ $expr , 'andX' ], $this -> searchOperatorArrayToDBExprArray ( $builder , $operator -> getArguments ()));
2017-02-02 20:19:16 +03:00
case ISearchBinaryOperator :: OPERATOR_OR :
2018-01-16 15:22:28 +03:00
return call_user_func_array ([ $expr , 'orX' ], $this -> searchOperatorArrayToDBExprArray ( $builder , $operator -> getArguments ()));
2017-02-02 20:19:16 +03:00
default :
throw new \InvalidArgumentException ( 'Invalid operator type: ' . $operator -> getType ());
}
} else if ( $operator instanceof ISearchComparison ) {
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 20:20:08 +03:00
$this -> validateComparison ( $comparison );
2017-02-02 20:19:16 +03:00
2017-02-02 20:20:08 +03:00
list ( $field , $value , $type ) = $this -> getOperatorFieldAndValue ( $comparison );
if ( isset ( $operatorMap [ $type ])) {
$queryOperator = $operatorMap [ $type ];
2017-02-02 20:19:16 +03: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 20:20:08 +03:00
$type = $operator -> getType ();
2017-02-02 20:19:16 +03:00
if ( $field === 'mimetype' ) {
if ( $operator -> getType () === ISearchComparison :: COMPARE_EQUAL ) {
2019-10-28 23:56:05 +03:00
$value = ( int ) $this -> mimetypeLoader -> getId ( $value );
2017-02-02 20:19:16 +03:00
} else if ( $operator -> getType () === ISearchComparison :: COMPARE_LIKE ) {
// transform "mimetype='foo/%'" to "mimepart='foo'"
if ( preg_match ( '|(.+)/%|' , $value , $matches )) {
$field = 'mimepart' ;
2019-10-28 23:56:05 +03:00
$value = ( int ) $this -> mimetypeLoader -> getId ( $matches [ 1 ]);
2017-02-02 20:20:08 +03:00
$type = ISearchComparison :: COMPARE_EQUAL ;
2019-10-28 23:56:05 +03:00
} else if ( strpos ( $value , '%' ) !== false ) {
2017-02-02 20:20:08 +03:00
throw new \InvalidArgumentException ( 'Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported' );
2019-10-28 23:56:05 +03:00
} else {
$field = 'mimetype' ;
$value = ( int ) $this -> mimetypeLoader -> getId ( $value );
$type = ISearchComparison :: COMPARE_EQUAL ;
2017-02-02 20:19:16 +03:00
}
}
2017-03-08 17:17:39 +03:00
} else if ( $field === 'favorite' ) {
$field = 'tag.category' ;
$value = self :: TAG_FAVORITE ;
} else if ( $field === 'tagname' ) {
$field = 'tag.category' ;
2019-12-10 11:47:30 +03:00
} else if ( $field === 'fileid' ) {
$field = 'file.fileid' ;
2017-02-02 20:19:16 +03:00
}
2017-02-02 20:20:08 +03:00
return [ $field , $value , $type ];
2017-02-02 20:19:16 +03:00
}
2017-02-02 20:20:08 +03:00
private function validateComparison ( ISearchComparison $operator ) {
2017-02-02 20:19:16 +03:00
$types = [
'mimetype' => 'string' ,
2017-02-02 20:20:08 +03:00
'mtime' => 'integer' ,
2017-02-02 20:19:16 +03:00
'name' => 'string' ,
2017-03-08 17:17:39 +03:00
'size' => 'integer' ,
'tagname' => 'string' ,
2017-04-05 16:12:30 +03:00
'favorite' => 'boolean' ,
'fileid' => 'integer'
2017-02-02 20:19:16 +03:00
];
$comparisons = [
'mimetype' => [ 'eq' , 'like' ],
'mtime' => [ 'eq' , 'gt' , 'lt' , 'gte' , 'lte' ],
'name' => [ 'eq' , 'like' ],
2017-03-08 17:17:39 +03:00
'size' => [ 'eq' , 'gt' , 'lt' , 'gte' , 'lte' ],
'tagname' => [ 'eq' , 'like' ],
'favorite' => [ 'eq' ],
2017-04-05 16:12:30 +03:00
'fileid' => [ 'eq' ]
2017-02-02 20:19:16 +03:00
];
if ( ! isset ( $types [ $operator -> getField ()])) {
2017-02-02 20:20:08 +03:00
throw new \InvalidArgumentException ( 'Unsupported comparison field ' . $operator -> getField ());
2017-02-02 20:19:16 +03:00
}
$type = $types [ $operator -> getField ()];
2017-02-02 20:20:08 +03: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 20:19:16 +03: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 16:46:23 +03:00
/**
* @ param IQueryBuilder $query
* @ param ISearchOrder [] $orders
*/
public function addSearchOrdersToQuery ( IQueryBuilder $query , array $orders ) {
foreach ( $orders as $order ) {
$query -> addOrderBy ( $order -> getField (), $order -> getDirection ());
}
}
2017-02-02 20:19:16 +03:00
}