2022-03-08 15:55:41 +01:00
< ? php namespace ProcessWire ;
/**
* ProcessWire PageFinder
*
* Matches selector strings to pages
*
2022-11-05 18:32:48 +01:00
* ProcessWire 3. x , Copyright 2021 by Ryan Cramer
2022-03-08 15:55:41 +01:00
* https :// processwire . com
*
* Hookable methods :
* =================
* @ method array | DatabaseQuerySelect find ( Selectors | string | array $selectors , $options = array ())
* @ method DatabaseQuerySelect getQuery ( $selectors , array $options )
* @ method string getQueryAllowedTemplatesWhere ( DatabaseQuerySelect $query , $where )
* @ method void getQueryJoinPath ( DatabaseQuerySelect $query , $selector )
* @ method bool | Field getQueryUnknownField ( $fieldName , array $data );
*
* @ property string $includeMode
* @ property bool $checkAccess
*
*/
class PageFinder extends Wire {
/**
* Options ( and their defaults ) that may be provided as the 2 nd argument to find ()
*
*/
protected $defaultOptions = array (
/**
* Specify that you only want to find 1 page and don ' t need info for pagination
*
*/
'findOne' => false ,
/**
* Specify that it ' s okay for hidden pages to be included in the results
*
*/
'findHidden' => false ,
/**
* Specify that it ' s okay for hidden AND unpublished pages to be included in the results
*
*/
'findUnpublished' => false ,
/**
* Specify that it ' s okay for hidden AND unpublished AND trashed pages to be included in the results
*
*/
'findTrash' => false ,
/**
* Specify that no page should be excluded - results can include unpublished , trash , system , no - access pages , etc .
*
*/
'findAll' => false ,
/**
* Always allow these page IDs to be included regardless of findHidden , findUnpublished , findTrash , findAll settings
*
*/
'alwaysAllowIDs' => array (),
/**
* This is an optimization used by the Pages :: find method , but we observe it here as we may be able
* to apply some additional optimizations in certain cases . For instance , if loadPages = false , then
* we can skip retrieval of IDs and omit sort fields .
*
*/
'loadPages' => true ,
/**
* When true , this function returns array of arrays containing page ID , parent ID , template ID and score .
* When false , returns only an array of page IDs . returnVerbose = true is required by most usage from Pages
* class . False is only for specific cases .
*
*/
'returnVerbose' => true ,
/**
* Return parent IDs rather than page IDs ? ( requires that returnVerbose is false )
*
*/
'returnParentIDs' => false ,
/**
* Return [ page_id => template_id ] IDs array ? ( cannot be combined with other 'return*' options )
* @ since 3.0 . 152
*
*/
'returnTemplateIDs' => false ,
/**
* Return all columns from pages table ( cannot be combined with other 'return*' options )
* @ since 3.0 . 153
*
*/
'returnAllCols' => false ,
/**
* Additional options when when 'returnAllCols' option is true
* @ since 3.0 . 172
*
*/
'returnAllColsOptions' => array (
'joinFields' => array (), // names of additional fields to join
'joinSortfield' => false , // include 'sortfield' in returned columns? (joined from pages_sortfields table)
'joinPath' => false , // include the 'path' in returned columns (joined from pages_paths table, requires PagePaths module)
'getNumChildren' => false , // include 'numChildren' in returned columns? (sub-select from pages table)
'unixTimestamps' => false , // return dates as unix timestamps?
),
/**
* When true , only the DatabaseQuery object is returned by find (), for internal use .
*
*/
'returnQuery' => false ,
/**
* Whether the total quantity of matches should be determined and accessible from getTotal ()
*
* null : determine automatically ( disabled when limit = 1 , enabled in all other cases )
* true : always calculate total
* false : never calculate total
*
*/
'getTotal' => null ,
/**
* Method to use when counting total records
*
* If 'count' , total will be calculated using a COUNT ( * ) .
* If ' calc , total will calculate using SQL_CALC_FOUND_ROWS .
* If blank or something else , method will be determined automatically .
*
*/
'getTotalType' => 'calc' ,
/**
* Only start loading pages after this ID
*
*/
'startAfterID' => 0 ,
/**
* Stop and load no more if a page having this ID is found
*
*/
'stopBeforeID' => 0 ,
/**
* For internal use with startAfterID or stopBeforeID ( when combined with a 'limit=n' selector )
*
*/
'softLimit' => 0 ,
/**
* Reverse whatever sort is specified
*
*/
'reverseSort' => false ,
/**
* Allow use of _custom = " another selector " in Selectors ?
*
*/
'allowCustom' => false ,
/**
* Use sortsAfter feature where PageFinder lets you perform the sorting manually after the find ()
*
* When in use , you can access the PageFinder :: getSortsAfter () method to retrieve an array of sort
* fields that should be sent to PageArray :: sort ()
*
* So far this option seems to add more overhead in most cases ( rather than save it ) so recommend not
* using it . Kept for further experimenting .
*
*/
'useSortsAfter' => false ,
/**
* Options passed to DatabaseQuery :: bindOptions () for primary query generated by this PageFinder
*
*/
'bindOptions' => array (),
);
/**
* @ var Fields
*
*/
protected $fields ;
/**
* @ var Pages
*
*/
protected $pages ;
/**
* @ var Sanitizer
*
*/
protected $sanitizer ;
/**
* @ var WireDatabasePDO
*
*/
protected $database ;
/**
* @ var Languages | null
*
*/
protected $languages ;
/**
* @ var Templates
*
*/
protected $templates ;
/**
* @ var Config
*
*/
protected $config ;
/**
* Whether to find the total number of matches
*
* @ var bool
*
*/
protected $getTotal = true ;
/**
* Method to use for getting total , may be : 'calc' , 'count' , or blank to auto - detect .
*
* @ var string
*
*/
protected $getTotalType = 'calc' ;
/**
* Total found
*
* @ var int
*
*/
protected $total = 0 ;
/**
* Limit setting for pagination
*
* @ var int
*
*/
protected $limit = 0 ;
/**
* Start setting for pagination
*
* @ var int
*
*/
protected $start = 0 ;
/**
* Parent ID value when query includes a single parent
*
* @ var int | null
*
*/
protected $parent_id = null ;
/**
* Templates ID value when query includes a single template
* @ var null
*
*/
protected $templates_id = null ;
/**
* Check access enabled ? Becomes false if check_access = 0 or include = all
*
* @ var bool
*
*/
protected $checkAccess = true ;
/**
* Include mode ( when specified ) : all , hidden , unpublished
*
* @ var string
*
*/
protected $includeMode = '' ;
/**
* Number of times the getQueryNumChildren () method has been called
*
* @ var int
*
*/
protected $getQueryNumChildren = 0 ;
/**
* Options that were used in the most recent find ()
*
* @ var array
*
*/
protected $lastOptions = array ();
/**
* Extra OR selectors used for OR - groups , array of arrays indexed by group name
*
* @ var array
*
*/
protected $extraOrSelectors = array (); // one from each field must match
/**
* Array of sortfields that should be applied to resulting PageArray after loaded
*
* Also see `useSortsAfter` option
*
* @ var array
*
*/
protected $sortsAfter = array ();
/**
* Reverse order of pages after load ?
*
* @ var bool
*
*/
protected $reverseAfter = false ;
/**
2022-11-05 18:32:48 +01:00
* Data that should be conditionally populated back to any resulting PageArray’ s data () method
2022-03-08 15:55:41 +01:00
*
* @ var array
*
*/
2022-11-05 18:32:48 +01:00
protected $pageArrayData = array (
/* may include :
'fields' => array ()
'extends' => array ()
'joinFields' => array ()
*/
);
2022-03-08 15:55:41 +01:00
/**
* The fully parsed / final selectors used in the last find () operation
*
* @ var Selectors | null
*
*/
protected $finalSelectors = null ; // Fully parsed final selectors
/**
* Number of Selector objects that have alternate operators
*
* @ var int
*
*/
protected $numAltOperators = 0 ;
/**
* Cached value from supportsLanguagePageNames () method
*
* @ var null | bool
*
*/
protected $supportsLanguagePageNames = null ;
/**
* Fields that can only be used by themselves ( not OR ' d with other fields )
*
* @ var array
*
*/
protected $singlesFields = array (
'has_parent' ,
'hasParent' ,
'num_children' ,
'numChildren' ,
'children.count' ,
'limit' ,
'start' ,
);
// protected $extraSubSelectors = array(); // subselectors that are added in after getQuery()
// protected $extraJoins = array();
// protected $nativeWheres = array(); // where statements for native fields, to be reused in subselects where appropriate.
2023-03-10 19:41:40 +01:00
public function __get ( $name ) {
if ( $name === 'includeMode' ) return $this -> includeMode ;
if ( $name === 'checkAccess' ) return $this -> checkAccess ;
return parent :: __get ( $name );
2022-03-08 15:55:41 +01:00
}
/**
* Initialize new find operation and prepare options
*
* @ param Selectors $selectors
* @ param array $options
* @ return array Returns updated options with all present
*
*/
protected function init ( Selectors $selectors , array $options ) {
$this -> fields = $this -> wire ( 'fields' );
$this -> pages = $this -> wire ( 'pages' );
$this -> sanitizer = $this -> wire ( 'sanitizer' );
$this -> database = $this -> wire ( 'database' );
$this -> languages = $this -> wire ( 'languages' );
$this -> templates = $this -> wire ( 'templates' );
$this -> config = $this -> wire ( 'config' );
$this -> parent_id = null ;
$this -> templates_id = null ;
$this -> checkAccess = true ;
$this -> getQueryNumChildren = 0 ;
$this -> pageArrayData = array ();
$options = array_merge ( $this -> defaultOptions , $options );
2022-11-05 18:32:48 +01:00
$options = $this -> initSelectors ( $selectors , $options );
2022-03-08 15:55:41 +01:00
// move getTotal option to a class property, after initStatusChecks
$this -> getTotal = $options [ 'getTotal' ];
$this -> getTotalType = $options [ 'getTotalType' ] == 'count' ? 'count' : 'calc' ;
unset ( $options [ 'getTotal' ]); // so we get a notice if we try to access it
$this -> lastOptions = $options ;
return $options ;
}
/**
* Initialize the selectors to add Page status checks
*
* @ param Selectors $selectors
* @ param array $options
* @ return array
*
*/
2022-11-05 18:32:48 +01:00
protected function initSelectors ( Selectors $selectors , array $options ) {
2022-03-08 15:55:41 +01:00
$limit = 0 ; // for getTotal auto detection
$start = 0 ;
$limitSelector = null ;
2022-11-05 18:32:48 +01:00
$startSelector = null ;
$addSelectors = array ();
2022-03-08 15:55:41 +01:00
$hasParents = array (); // requests for parent(s) in the selector
$hasSort = false ; // whether or not a sort is requested
2022-11-05 18:32:48 +01:00
// field names that do not accept array values
$noArrayFields = array (
'status' => 1 , // 1: array not allowed for field only
'include' => 2 , // 2: array not allowed for field or value
'check_access' => 2 ,
'checkAccess' => 2 ,
'limit' => 1 ,
'start' => 2 ,
'getTotal' => 2 ,
'get_total' => 2 ,
);
// include mode names to option names
$includeOptions = array (
'hidden' => 'findHidden' ,
'unpublished' => 'findUnpublished' ,
'trash' => 'findTrash' ,
'all' => 'findAll' ,
);
2022-03-08 15:55:41 +01:00
foreach ( $selectors as $key => $selector ) {
2022-11-05 18:32:48 +01:00
$fieldName = $selector -> field ;
$operator = $selector -> operator ;
$value = $selector -> value ;
$disallow = '' ;
if ( is_array ( $fieldName )) {
foreach ( $fieldName as $name ) {
if ( isset ( $noArrayFields [ $name ])) $disallow = " field: $name " ;
if ( $disallow ) break ;
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
$fieldName = $selector -> field (); // force string
} else if ( isset ( $noArrayFields [ $fieldName ]) && is_array ( $value )) {
if ( $noArrayFields [ $fieldName ] > 1 ) $disallow = 'value' ;
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
if ( $disallow ) {
$this -> syntaxError ( " OR-condition not supported for $disallow in ' $selector ' " );
}
if ( $fieldName === 'include' ) {
$value = strtolower ( $value );
if ( $operator !== '=' ) {
// disallowed operator for include
$this -> syntaxError ( " Unsupported operator ' $operator ' in ' $selector ' " );
} else if ( ! isset ( $includeOptions [ $value ])) {
// unrecognized include option
$useOnly = implode ( ', ' , array_keys ( $includeOptions ));
$this -> syntaxError ( " Unrecognized ' $value ' in ' $selector ' - use only: $useOnly " );
2022-03-08 15:55:41 +01:00
} else {
2022-11-05 18:32:48 +01:00
// i.e. hidden=findHidden, findUnpublished, findTrash, findAll
$option = $includeOptions [ $value ];
$options [ $option ] = true ;
$this -> includeMode = $value ;
$selectors -> remove ( $key );
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
} else if ( $fieldName === 'limit' ) {
2022-03-08 15:55:41 +01:00
// for getTotal auto detect
2022-11-05 18:32:48 +01:00
if ( is_array ( $value )) {
if ( count ( $value ) === 2 ) {
// limit and start, i.e. limit=20,10 means start at 20 and limit to 10
$limit = ( int ) $value [ 1 ];
if ( ! $startSelector ) {
// use start value only if it was not previously specified
$start = ( int ) $value [ 0 ];
$startSelector = new SelectorEqual ( 'start' , $start );
$addSelectors [ 'start' ] = $startSelector ;
}
} else {
$limit = ( int ) $value [ 0 ];
}
$selector -> value = $limit ;
} else {
$limit = ( int ) $value ;
}
2022-03-08 15:55:41 +01:00
$limitSelector = $selector ;
2022-11-05 18:32:48 +01:00
} else if ( $fieldName === 'start' ) {
2022-03-08 15:55:41 +01:00
// for getTotal auto detect
2022-11-05 18:32:48 +01:00
$start = ( int ) $value ;
$startSelector = $selector ;
unset ( $addSelectors [ 'start' ]); // just in case specified twice
2022-03-08 15:55:41 +01:00
2022-11-05 18:32:48 +01:00
} else if ( $fieldName === 'sort' ) {
2022-03-08 15:55:41 +01:00
// sorting is not needed if we are only retrieving totals
if ( $options [ 'loadPages' ] === false ) $selectors -> remove ( $selector );
$hasSort = true ;
2022-11-05 18:32:48 +01:00
} else if ( $fieldName === 'parent' || $fieldName === 'parent_id' ) {
$hasParents [] = $value ;
2022-03-08 15:55:41 +01:00
2022-11-05 18:32:48 +01:00
} else if ( $fieldName === 'getTotal' || $fieldName === 'get_total' ) {
2022-03-08 15:55:41 +01:00
// whether to retrieve the total, and optionally what type: calc or count
// this applies only if user hasn't themselves created a field called getTotal or get_total
2022-11-05 18:32:48 +01:00
if ( $this -> fields -> get ( $fieldName )) {
// user has created a field having name 'getTotal' or 'get_total'
// so we do not provide the getTotal option
} else {
if ( ctype_digit ( " $value " )) {
$options [ 'getTotal' ] = ( bool ) (( int ) $value );
} else if ( $value === 'calc' || $value === 'count' ) {
2022-03-08 15:55:41 +01:00
$options [ 'getTotal' ] = true ;
2022-11-05 18:32:48 +01:00
$options [ 'getTotalType' ] = $value ;
} else {
// warning: unknown getTotal type
$options [ 'getTotal' ] = $value ? true : false ;
2022-03-08 15:55:41 +01:00
}
$selectors -> remove ( $selector );
}
2022-11-05 18:32:48 +01:00
} else if ( $fieldName === 'children' || $fieldName === 'child' ) {
// i.e. children=/path/to/page|/another/path - convert to IDs
$values = is_array ( $value ) ? $value : array ( $value );
foreach ( $values as $k => $v ) {
if ( ctype_digit ( " $v " )) continue ;
if ( strpos ( $v , '/' ) !== 0 ) continue ;
$child = $this -> pages -> get ( $v );
$values [ $k ] = $child -> id ;
}
$selector -> value = count ( $values ) > 1 ? $values : reset ( $values );
2022-03-08 15:55:41 +01:00
}
} // foreach($selectors)
2022-11-05 18:32:48 +01:00
foreach ( $addSelectors as $selector ) {
$selectors -> add ( $selector );
2022-03-08 15:55:41 +01:00
}
2022-11-05 18:32:48 +01:00
// find max status, and update selector to bitwise when needed
$this -> initStatus ( $selectors , $options );
2022-03-08 15:55:41 +01:00
if ( $options [ 'findOne' ]) {
// findOne option is never paginated, always starts at 0
2022-11-05 18:32:48 +01:00
if ( $startSelector ) $selectors -> remove ( $startSelector );
2022-03-08 15:55:41 +01:00
$selectors -> add ( new SelectorEqual ( 'start' , 0 ));
if ( empty ( $options [ 'startAfterID' ]) && empty ( $options [ 'stopBeforeID' ])) {
2022-11-05 18:32:48 +01:00
if ( $limitSelector ) $selectors -> remove ( $limitSelector );
2022-03-08 15:55:41 +01:00
$selectors -> add ( new SelectorEqual ( 'limit' , 1 ));
}
// getTotal default is false when only finding 1 page
if ( is_null ( $options [ 'getTotal' ])) $options [ 'getTotal' ] = false ;
} else if ( ! $limit && ! $start ) {
// getTotal is not necessary since there is no limit specified (getTotal=same as count)
if ( is_null ( $options [ 'getTotal' ])) $options [ 'getTotal' ] = false ;
} else {
// get Total default is true when finding multiple pages
if ( is_null ( $options [ 'getTotal' ])) $options [ 'getTotal' ] = true ;
}
2022-11-05 18:32:48 +01:00
if ( count ( $hasParents ) === 1 && ! $hasSort ) {
2022-03-08 15:55:41 +01:00
// if single parent specified and no sort requested, default to the sort specified with the requested parent
try {
$parent = $this -> pages -> get ( reset ( $hasParents ));
} catch ( \Exception $e ) {
// don't try to add sort
$parent = null ;
}
if ( $parent && $parent -> id ) {
$sort = $parent -> template -> sortfield ;
if ( ! $sort ) $sort = $parent -> sortfield ;
if ( $sort ) $selectors -> add ( new SelectorEqual ( 'sort' , $sort ));
}
}
if ( ! $options [ 'findOne' ] && $limitSelector && ( $options [ 'startAfterID' ] || $options [ 'stopBeforeID' ])) {
2022-11-05 18:32:48 +01:00
$options [ 'softLimit' ] = $limit ;
2022-03-08 15:55:41 +01:00
$selectors -> remove ( $limitSelector );
}
return $options ;
}
2022-11-05 18:32:48 +01:00
/**
* Initialize status checks
*
* @ param Selectors $selectors
* @ param array $options
*
*/
protected function initStatus ( Selectors $selectors , array $options ) {
$maxStatus = null ;
$lessStatus = 0 ;
$statuses = array (); // i.e. [ 'hidden' => 1024, 'unpublished' => 2048, ], etc
$checkAccessSpecified = false ;
$findAll = $options [ 'findAll' ];
$findTrash = $options [ 'findTrash' ];
$findHidden = $options [ 'findHidden' ];
$findUnpublished = $options [ 'findUnpublished' ];
foreach ( $selectors as $key => $selector ) {
$fieldName = $selector -> field ();
if ( $fieldName === 'check_access' || $fieldName === 'checkAccess' ) {
if ( $fieldName === 'checkAccess' ) $selector -> field = 'check_access' ;
$this -> checkAccess = (( int ) $selector -> value ()) > 0 ? true : false ;
$checkAccessSpecified = true ;
$selectors -> remove ( $key );
continue ;
} else if ( $fieldName !== 'status' ) {
continue ;
}
$operator = $selector -> operator ;
$values = $selector -> values ();
$qty = count ( $values );
$not = false ;
// convert status name labels to status integers
foreach ( $values as $k => $v ) {
if ( ctype_digit ( " $v " )) {
$v = ( int ) $v ;
} else {
// allow use of some predefined labels for Page statuses
$v = strtolower ( $v );
if ( empty ( $statuses )) $statuses = Page :: getStatuses ();
$v = isset ( $statuses [ $v ]) ? $statuses [ $v ] : 1 ;
}
$values [ $k ] = $v ;
}
if (( $operator === '!=' && ! $selector -> not ) || ( $selector -> not && $operator === '=' )) {
// NOT MATCH condition: replace with bitwise AND NOT selector
2023-03-10 19:41:40 +01:00
/** @var Selector $s */
2022-11-05 18:32:48 +01:00
$s = $this -> wire ( new SelectorBitwiseAnd ( 'status' , $qty > 1 ? $values : reset ( $values )));
$s -> not = true ;
$not = true ;
$selectors [ $key ] = $s ;
} else if ( $operator === '=' || ( $operator === '!=' && $selector -> not )) {
// MATCH condition: replace with bitwise AND selector
$selectors [ $key ] = $this -> wire ( new SelectorBitwiseAnd ( 'status' , $qty > 1 ? $values : reset ( $values )));
} else {
// some other operator like: >, <, >=, <=, &
$not = $selector -> not ;
}
if ( $not ) {
// NOT condition does not apply to maxStatus
} else {
foreach ( $values as $v ) {
if ( $maxStatus === null || $v > $maxStatus ) $maxStatus = ( int ) $v ;
}
}
}
if ( $findAll ) {
// findAll option means that unpublished, hidden, trash, system may be included
if ( ! $checkAccessSpecified ) $this -> checkAccess = false ;
} else if ( $findHidden ) {
$lessStatus = Page :: statusUnpublished ;
} else if ( $findUnpublished ) {
$lessStatus = Page :: statusTrash ;
} else if ( $findTrash ) {
$lessStatus = Page :: statusDeleted ;
} else if ( $maxStatus !== null ) {
// status already present in the selector, without a findAll/findUnpublished/findHidden: use maxStatus value
if ( $maxStatus < Page :: statusHidden ) {
$lessStatus = Page :: statusHidden ;
} else if ( $maxStatus < Page :: statusUnpublished ) {
$lessStatus = Page :: statusUnpublished ;
} else if ( $maxStatus < Page :: statusTrash ) {
$lessStatus = Page :: statusTrash ;
}
} else {
// no status is present, so exclude everything hidden and above
$lessStatus = Page :: statusHidden ;
}
if ( $lessStatus ) {
$selectors -> add ( new SelectorLessThan ( 'status' , $lessStatus ));
}
}
2022-03-08 15:55:41 +01:00
/**
* Return all pages matching the given selector .
*
* @ param Selectors | string | array $selectors Selectors object , selector string or selector array
* @ param array $options
* - `findOne` ( bool ) : Specify that you only want to find 1 page and don ' t need info for pagination ( default = false ) .
* - `findHidden` ( bool ) : Specify that it ' s okay for hidden pages to be included in the results ( default = false ) .
* - `findUnpublished` ( bool ) : Specify that it ' s okay for hidden AND unpublished pages to be included in the
* results ( default = false ) .
* - `findTrash` ( bool ) : Specify that it ' s okay for hidden AND unpublished AND trashed pages to be included in the
* results ( default = false ) .
* - `findAll` ( bool ) : Specify that no page should be excluded - results can include unpublished , trash , system ,
* no - access pages , etc . ( default = false )
* - `getTotal` ( bool | null ) : Whether the total quantity of matches should be determined and accessible from
* getTotal () method call .
* - null : determine automatically ( default is disabled when limit = 1 , enabled in all other cases ) .
* - true : always calculate total .
* - false : never calculate total .
* - `getTotalType` ( string ) : Method to use to get total , specify 'count' or 'calc' ( default = 'calc' ) .
* - `returnQuery` ( bool ) : When true , only the DatabaseQuery object is returned by find (), for internal use . ( default = false )
* - `loadPages` ( bool ) : This is an optimization used by the Pages :: find () method , but we observe it here as we
* may be able to apply some additional optimizations in certain cases . For instance , if loadPages = false , then
* we can skip retrieval of IDs and omit sort fields . ( default = true )
* - `stopBeforeID` ( int ) : Stop loading pages once a page matching this ID is found . Page having this ID will be
* excluded as well ( default = 0 ) .
* - `startAfterID` ( int ) : Start loading pages once a page matching this ID is found . Page having this ID will be
* excluded as well ( default = 0 ) .
* - `reverseSort` ( bool ) : Reverse whatever sort is specified .
* - `returnVerbose` ( bool ) : When true , this function returns array of arrays containing page ID , parent ID ,
* template ID and score . When false , returns only an array of page IDs . True is required by most usage from
* Pages class . False is only for specific cases .
* - `returnParentIDs` ( bool ) : Return parent IDs only ? ( default = false , requires that 'returnVerbose' option is false ) .
* - `returnTemplateIDs` ( bool ) : Return [ pageID => templateID ] array ? [ 3.0 . 152 + only ] ( default = false , cannot be combined with other 'return*' options ) .
* - `returnAllCols` ( bool ) : Return [ pageID => [ all columns ]] array ? [ 3.0 . 153 + only ] ( default = false , cannot be combined with other 'return*' options ) .
* - `allowCustom` ( bool ) : Whether or not to allow _custom = 'selector string' type values ( default = false ) .
* - `useSortsAfter` ( bool ) : When true , PageFinder may ask caller to perform sort manually in some cases ( default = false ) .
* @ return array | DatabaseQuerySelect
* @ throws PageFinderException
*
*/
public function ___find ( $selectors , array $options = array ()) {
if ( is_string ( $selectors ) || is_array ( $selectors )) {
2022-11-05 18:32:48 +01:00
list ( $s , $selectors ) = array ( $selectors , $this -> wire ( new Selectors ()));
2023-03-10 19:41:40 +01:00
/** @var Selectors $selectors */
2022-11-05 18:32:48 +01:00
$selectors -> init ( $s );
2022-03-08 15:55:41 +01:00
} else if ( ! $selectors instanceof Selectors ) {
throw new PageFinderException ( " find() requires Selectors object, string or array " );
}
$options = $this -> init ( $selectors , $options );
$stopBeforeID = ( int ) $options [ 'stopBeforeID' ];
$startAfterID = ( int ) $options [ 'startAfterID' ];
$database = $this -> database ;
$matches = array ();
$query = $this -> getQuery ( $selectors , $options ); /** @var DatabaseQuerySelect $query */
if ( $options [ 'returnQuery' ]) return $query ;
2022-11-05 18:32:48 +01:00
if ( $options [ 'loadPages' ] || $this -> getTotalType === 'calc' ) {
2022-03-08 15:55:41 +01:00
try {
$stmt = $query -> prepare ();
$database -> execute ( $stmt );
$error = '' ;
} catch ( \Exception $e ) {
$this -> trackException ( $e , true );
$error = $e -> getMessage ();
$stmt = null ;
}
if ( $error ) {
$this -> log ( $error );
throw new PageFinderException ( $error );
}
if ( $options [ 'loadPages' ]) {
$softCnt = 0 ; // for startAfterID when combined with 'limit'
/** @noinspection PhpAssignmentInConditionInspection */
while ( $row = $stmt -> fetch ( \PDO :: FETCH_ASSOC )) {
if ( $startAfterID > 0 ) {
if ( $row [ 'id' ] != $startAfterID ) continue ;
$startAfterID = - 1 ; // -1 indicates that recording may start
continue ;
}
if ( $stopBeforeID && $row [ 'id' ] == $stopBeforeID ) {
if ( $options [ 'findOne' ]) {
2022-11-05 18:32:48 +01:00
$matches = count ( $matches ) ? array ( end ( $matches )) : array ();
2022-03-08 15:55:41 +01:00
} else if ( $options [ 'softLimit' ]) {
$matches = array_slice ( $matches , - 1 * $options [ 'softLimit' ]);
}
break ;
}
if ( $options [ 'returnVerbose' ]) {
// determine score for this row
$score = 0.0 ;
foreach ( $row as $k => $v ) if ( strpos ( $k , '_score_' ) === 0 ) {
$v = ( float ) $v ;
if ( $v === 111.1 || $v === 222.2 || $v === 333.3 ) continue ; // signal scores of non-match
$score += $v ;
unset ( $row [ $k ]);
}
$row [ 'score' ] = $score ;
$matches [] = $row ;
} else if ( $options [ 'returnAllCols' ]) {
$matches [( int ) $row [ 'id' ]] = $row ;
} else if ( $options [ 'returnTemplateIDs' ]) {
$matches [( int ) $row [ 'id' ]] = ( int ) $row [ 'templates_id' ];
} else {
$matches [] = ( int ) $row [ 'id' ];
}
if ( $startAfterID === - 1 ) {
// -1 indicates that recording may start
if ( $options [ 'findOne' ]) {
break ;
} else if ( $options [ 'softLimit' ] && ++ $softCnt >= $options [ 'softLimit' ]) {
break ;
}
}
}
}
$stmt -> closeCursor ();
}
if ( $this -> getTotal ) {
if ( $this -> getTotalType === 'count' ) {
$query -> set ( 'select' , array ( 'COUNT(*)' ));
$query -> set ( 'orderby' , array ());
$query -> set ( 'groupby' , array ());
$query -> set ( 'limit' , array ());
$stmt = $query -> execute ();
$errorInfo = $stmt -> errorInfo ();
if ( $stmt -> errorCode () > 0 ) throw new PageFinderException ( $errorInfo [ 2 ]);
list ( $this -> total ) = $stmt -> fetch ( \PDO :: FETCH_NUM );
$stmt -> closeCursor ();
} else {
$this -> total = ( int ) $database -> query ( " SELECT FOUND_ROWS() " ) -> fetchColumn ();
}
} else {
$this -> total = count ( $matches );
}
if ( ! $this -> total && $this -> numAltOperators ) {
// check if any selectors provided alternate operators to try
$matches = $this -> findAlt ( $selectors , $options , $matches );
}
$this -> lastOptions = $options ;
if ( $this -> reverseAfter ) $matches = array_reverse ( $matches );
return $matches ;
}
/**
* Perform an alternate / fallback find when first fails to match and alternate operators available
*
* @ param Selectors $selectors
* @ param array $options
* @ param array $matches
* @ return array
*
*/
protected function findAlt ( $selectors , $options , $matches ) {
// check if any selectors provided alternate operators to try
$numAlts = 0 ;
foreach ( $selectors as $key => $selector ) {
$altOperators = $selector -> altOperators ;
if ( ! count ( $altOperators )) continue ;
$altOperator = array_shift ( $altOperators );
$sel = Selectors :: getSelectorByOperator ( $altOperator );
if ( ! $sel ) continue ;
$selector -> copyTo ( $sel );
$selectors [ $key ] = $sel ;
$numAlts ++ ;
}
if ( ! $numAlts ) return $matches ;
$this -> numAltOperators = 0 ;
return $this -> ___find ( $selectors , $options );
}
/**
* Same as find () but returns just a simple array of page IDs without any other info
*
* @ param Selectors | string | array $selectors Selectors object , selector string or selector array
* @ param array $options
* @ return array of page IDs
*
*/
public function findIDs ( $selectors , $options = array ()) {
$options [ 'returnVerbose' ] = false ;
return $this -> find ( $selectors , $options );
}
/**
* Returns array of arrays with all columns in pages table indexed by page ID
*
* @ param Selectors | string | array $selectors Selectors object , selector string or selector array
* @ param array $options
* - `joinFields` ( array ) : Names of additional fields to join ( default = []) 3.0 . 172 +
* - `joinSortfield` ( bool ) : Include 'sortfield' in returned columns ? Joined from pages_sortfields table . ( default = false ) 3.0 . 172 +
* - `getNumChildren` ( bool ) : Include 'numChildren' in returned columns ? Calculated in query . ( default = false ) 3.0 . 172 +
* - `unixTimestamps` ( bool ) : Return created / modified / published dates as unix timestamps rather than ISO - 8601 ? ( default = false ) 3.0 . 172 +
* @ return array | DatabaseQuerySelect
* @ since 3.0 . 153
*
*/
public function findVerboseIDs ( $selectors , $options = array ()) {
$hasCustomOptions = count ( $options ) > 0 ;
$options [ 'returnVerbose' ] = false ;
$options [ 'returnAllCols' ] = true ;
$options [ 'returnAllColsOptions' ] = $this -> defaultOptions [ 'returnAllColsOptions' ];
if ( $hasCustomOptions ) {
// move some from $options into $options['returnAllColsOptions']
foreach ( $options [ 'returnAllColsOptions' ] as $name => $default ) {
if ( ! isset ( $options [ $name ])) continue ;
$options [ 'returnAllColsOptions' ][ $name ] = $options [ $name ];
unset ( $options [ $name ]);
}
}
return $this -> find ( $selectors , $options );
}
/**
* Same as findIDs () but returns the parent IDs of the pages that matched
*
* @ param Selectors | string | array $selectors Selectors object , selector string or selector array
* @ param array $options
* @ return array of page parent IDs
*
*/
public function findParentIDs ( $selectors , $options = array ()) {
$options [ 'returnVerbose' ] = false ;
$options [ 'returnParentIDs' ] = true ;
return $this -> find ( $selectors , $options );
}
/**
* Find template ID for each page — returns array of template IDs indexed by page ID
*
* @ param Selectors | string | array $selectors Selectors object , selector string or selector array
* @ param array $options
* @ return array
* @ since 3.0 . 152
*
*/
public function findTemplateIDs ( $selectors , $options = array ()) {
$options [ 'returnVerbose' ] = false ;
$options [ 'returnParentIDs' ] = false ;
$options [ 'returnTemplateIDs' ] = true ;
return $this -> find ( $selectors , $options );
}
/**
* Return a count of pages that match
*
* @ param Selectors | string | array $selectors Selectors object , selector string or selector array
* @ param array $options
* @ return int
* @ since 3.0 . 121
*
*/
public function count ( $selectors , $options = array ()) {
$defaults = array (
'getTotal' => true ,
'getTotalType' => 'count' ,
'loadPages' => false ,
'returnVerbose' => false
);
$options = array_merge ( $defaults , $options );
if ( ! empty ( $options [ 'startBeforeID' ]) || ! empty ( $options [ 'stopAfterID' ])) {
$options [ 'loadPages' ] = true ;
$options [ 'getTotalType' ] = 'calc' ;
$count = count ( $this -> find ( $selectors , $options ));
} else {
$this -> find ( $selectors , $options );
$count = $this -> total ;
}
return $count ;
}
/**
* Pre - process given Selectors object
*
* @ param Selectors $selectors
* @ param array $options
*
*/
protected function preProcessSelectors ( Selectors $selectors , $options = array ()) {
$sortAfterSelectors = array ();
$sortSelectors = array ();
$start = null ;
$limit = null ;
$eq = null ;
foreach ( $selectors as $selector ) {
$field = $selector -> field ();
if ( $field === '_custom' ) {
$selectors -> remove ( $selector );
if ( ! empty ( $options [ 'allowCustom' ])) {
$_selectors = $this -> wire ( new Selectors ( $selector -> value ()));
$this -> preProcessSelectors ( $_selectors , $options );
/** @var Selectors $_selectors */
foreach ( $_selectors as $s ) $selectors -> add ( $s );
} else {
// use of _custom has not been specifically allowed
}
} else if ( $field === 'sort' ) {
$sortSelectors [] = $selector ;
if ( ! empty ( $options [ 'useSortsAfter' ]) && $selector -> operator == '=' && strpos ( $selector -> value , '.' ) === false ) {
$sortAfterSelectors [] = $selector ;
}
} else if ( $field === 'limit' ) {
$limit = ( int ) $selector -> value ;
} else if ( $field === 'start' ) {
$start = ( int ) $selector -> value ;
2022-11-05 18:32:48 +01:00
} else if ( $field === 'eq' || $field === 'index' ) {
2022-03-08 15:55:41 +01:00
if ( $this -> fields -> get ( $field )) continue ;
$value = $selector -> value ;
if ( $value === 'first' ) {
$eq = 0 ;
} else if ( $value === 'last' ) {
$eq = - 1 ;
} else {
$eq = ( int ) $value ;
}
$selectors -> remove ( $selector );
} else if ( strpos ( $field , '.owner.' ) && ! $this -> fields -> get ( 'owner' )) {
$selector -> field = str_replace ( '.owner.' , '__owner.' , $selector -> field ());
} else if ( stripos ( $field , 'Fieldtype' ) === 0 ) {
$this -> preProcessFieldtypeSelector ( $selectors , $selector );
}
}
if ( ! is_null ( $eq )) {
if ( $eq === - 1 ) {
$limit = - 1 ;
$start = null ;
} else if ( $eq === 0 ) {
$start = 0 ;
$limit = 1 ;
} else {
$start = $eq ;
$limit = 1 ;
}
}
if ( ! $limit && ! $start && count ( $sortAfterSelectors )
&& $options [ 'returnVerbose' ] && ! empty ( $options [ 'useSortsAfter' ])
&& empty ( $options [ 'startAfterID' ]) && empty ( $options [ 'stopBeforeID' ])) {
// the `useSortsAfter` option is enabled and potentially applicable
$sortsAfter = array ();
foreach ( $sortAfterSelectors as $n => $selector ) {
if ( ! $n && $this -> pages -> loader () -> isNativeColumn ( $selector -> value )) {
// first iteration only, see if it's a native column and prevent sortsAfter if so
break ;
}
if ( strpos ( $selector -> value (), '.' ) !== false ) {
// we don't supports sortsAfter for subfields, so abandon entirely
$sortsAfter = array ();
break ;
}
if ( $selector -> operator != '=' ) {
// sort property being used for something else that we don't recognize
continue ;
}
$sortsAfter [] = $selector -> value ;
$selectors -> remove ( $selector );
}
$this -> sortsAfter = $sortsAfter ;
}
if ( $limit !== null && $limit < 0 ) {
// negative limit value means we pull results from end rather than start
if ( $start !== null && $start < 0 ) {
// we don't support a double negative, so double negative makes a positive
$start = abs ( $start );
$limit = abs ( $limit );
} else if ( $start > 0 ) {
$start = $start - abs ( $limit );
$limit = abs ( $limit );
} else {
$this -> reverseAfter = true ;
$limit = abs ( $limit );
}
}
if ( $start !== null && $start < 0 ) {
// negative start value means we start from a value from the end rather than the start
if ( $limit ) {
// determine how many pages total and subtract from that to get start
$o = $options ;
$o [ 'getTotal' ] = true ;
$o [ 'loadPages' ] = false ;
$o [ 'returnVerbose' ] = false ;
$sel = clone $selectors ;
foreach ( $sel as $s ) {
if ( $s -> field == 'limit' || $s -> field == 'start' ) $sel -> remove ( $s );
}
$sel -> add ( new SelectorEqual ( 'limit' , 1 ));
$finder = new PageFinder ();
$this -> wire ( $finder );
$finder -> find ( $sel );
$total = $finder -> getTotal ();
$start = abs ( $start );
$start = $total - $start ;
if ( $start < 0 ) $start = 0 ;
} else {
// same as negative limit
$this -> reverseAfter = true ;
$limit = abs ( $start );
$start = null ;
}
}
if ( $this -> reverseAfter ) {
// reverse the sorts
foreach ( $sortSelectors as $s ) {
if ( $s -> operator != '=' || ctype_digit ( $s -> value )) continue ;
if ( strpos ( $s -> value , '-' ) === 0 ) {
$s -> value = ltrim ( $s -> value , '-' );
} else {
$s -> value = '-' . $s -> value ;
}
}
}
$this -> limit = $limit ;
$this -> start = $start ;
}
/**
* Pre - process a selector having field name that begins with " Fieldtype "
*
* @ param Selectors $selectors
* @ param Selector $selector
*
*/
protected function preProcessFieldtypeSelector ( Selectors $selectors , Selector $selector ) {
$foundFields = null ;
$foundTypes = null ;
$replaceFields = array ();
$failFields = array ();
$languages = $this -> languages ;
$fieldtypes = $this -> wire () -> fieldtypes ;
$selectorCopy = null ;
foreach ( $selector -> fields () as $fieldName ) {
$subfield = '' ;
$findPerField = false ;
$findExtends = false ;
if ( strpos ( $fieldName , '.' )) {
$parts = explode ( '.' , $fieldName );
$fieldName = array_shift ( $parts );
foreach ( $parts as $k => $part ) {
if ( $part === 'fields' ) {
$findPerField = true ;
unset ( $parts [ $k ]);
} else if ( $part === 'extends' ) {
$findExtends = true ;
unset ( $parts [ $k ]);
}
}
if ( count ( $parts )) $subfield = implode ( '.' , $parts );
}
$fieldtype = $fieldtypes -> get ( $fieldName );
if ( ! $fieldtype ) continue ;
$fieldtypeLang = $languages ? $fieldtypes -> get ( " { $fieldName } Language " ) : null ;
foreach ( $this -> fields as $f ) {
2023-03-10 19:41:40 +01:00
/** @var Field $f */
2022-03-08 15:55:41 +01:00
if ( $findExtends ) {
// allow any Fieldtype that is an instance of given one, or extends it
if ( ! wireInstanceOf ( $f -> type , $fieldtype )
&& ( $fieldtypeLang === null || ! wireInstanceOf ( $f -> type , $fieldtypeLang ))) continue ;
/** potential replacement for the above 2 lines
if ( $f -> type -> className () === $fieldName ) {
// always allowed
} else if ( ! wireInstanceOf ( $f -> type , $fieldtype ) && ( $fieldtypeLang === null || ! wireInstanceOf ( $f -> type , $fieldtypeLang ))) {
// this field’ s type does not extend the one we are looking for
continue ;
} else {
// looks good, but now check operators
$selectorInfo = $f -> type -> getSelectorInfo ( $f );
// if operator used in selector is not an allowed one, then skip over this field
if ( ! in_array ( $selector -> operator (), $selectorInfo [ 'operators' ])) continue ;
}
*/
} else {
// only allow given Fieldtype
if ( $f -> type !== $fieldtype && ( $fieldtypeLang === null || $f -> type !== $fieldtypeLang )) continue ;
}
$fName = $subfield ? " $f->name . $subfield " : $f -> name ;
if ( $findPerField ) {
if ( $selectorCopy === null ) $selectorCopy = clone $selector ;
$selectorCopy -> field = $fName ;
$selectors -> replace ( $selector , $selectorCopy );
$count = $this -> pages -> count ( $selectors );
$selectors -> replace ( $selectorCopy , $selector );
if ( $count ) {
if ( $foundFields === null ) {
$foundFields = isset ( $this -> pageArrayData [ 'fields' ]) ? $this -> pageArrayData [ 'fields' ] : array ();
}
// include only fields that we know will match
$replaceFields [ $fName ] = $fName ;
if ( isset ( $foundFields [ $fName ])) {
$foundFields [ $fName ] += $count ;
} else {
$foundFields [ $fName ] = $count ;
}
} else {
$failFields [ $fName ] = $fName ;
}
} else {
// include all fields (faster)
$replaceFields [ $fName ] = $fName ;
}
if ( $findExtends ) {
if ( $foundTypes === null ) {
$foundTypes = isset ( $this -> pageArrayData [ 'extends' ]) ? $this -> pageArrayData [ 'extends' ] : array ();
}
$fType = $f -> type -> className ();
if ( isset ( $foundTypes [ $fType ])) {
$foundTypes [ $fType ][] = $fName ;
} else {
$foundTypes [ $fType ] = array ( $fName );
}
}
}
}
if ( count ( $replaceFields )) {
$selector -> fields = array_values ( $replaceFields );
} else if ( count ( $failFields )) {
// forced non-match and prevent field-not-found error after this method
$selector -> field = reset ( $failFields );
}
if ( is_array ( $foundFields )) {
arsort ( $foundFields );
$this -> pageArrayData [ 'fields' ] = $foundFields ;
}
if ( is_array ( $foundTypes )) {
$this -> pageArrayData [ 'extends' ] = $foundTypes ;
}
}
/**
* Pre - process the given selector to perform any necessary replacements
*
* This is primarily used to handle sub - selections , i . e . " bar=foo, id=[this=that, foo=bar] "
* and OR - groups , i . e . " (bar=foo), (foo=bar) "
*
* @ param Selector $selector
* @ param Selectors $selectors
* @ param array $options
* @ param int $level
* @ return bool | Selector Returns false if selector should be skipped over by getQuery (), returns Selector otherwise
* @ throws PageFinderSyntaxException
*
*/
protected function preProcessSelector ( Selector $selector , Selectors $selectors , array $options , $level = 0 ) {
$quote = $selector -> quote ;
$fieldsArray = $selector -> fields ;
$hasDoubleDot = false ;
$tags = null ;
foreach ( $fieldsArray as $key => $fn ) {
$dot = strpos ( $fn , '.' );
$parts = $dot ? explode ( '.' , $fn ) : array ( $fn );
// determine if it is a double-dot field (a.b.c)
if ( $dot && strrpos ( $fn , '.' ) !== $dot ) {
if ( strpos ( $fn , '__owner.' ) !== false ) continue ;
$hasDoubleDot = true ;
}
// determine if it is referencing any tags that should be coverted to field1|field2|field3
foreach ( $parts as $partKey => $part ) {
if ( $tags !== null && empty ( $tags )) continue ;
if ( $this -> fields -> get ( $part )) continue ; // maps to Field object
if ( $this -> fields -> isNative ( $part )) continue ; // maps to native property
if ( $tags === null ) $tags = $this -> fields -> getTags ( true ); // determine tags
if ( ! isset ( $tags [ $part ])) continue ; // not a tag
$tagFields = $tags [ $part ];
foreach ( $tagFields as $k => $fieldName ) {
$_parts = $parts ;
$_parts [ $partKey ] = $fieldName ;
$tagFields [ $k ] = implode ( '.' , $_parts );
}
if ( count ( $tagFields )) {
unset ( $fieldsArray [ $key ]);
$selector -> fields = array_merge ( $fieldsArray , $tagFields );
}
}
}
if ( $quote == '[' ) {
// selector contains another embedded selector that we need to convert to page IDs
// i.e. field=[id>0, name=something, this=that]
$this -> preProcessSubSelector ( $selector , $selectors );
} else if ( $quote == '(' ) {
// selector contains an OR group (quoted selector)
// at least one (quoted selector) must match for each field specified in front of it
$groupName = $selector -> group ? $selector -> group : $selector -> getField ( 'string' );
$groupName = $this -> sanitizer -> fieldName ( $groupName );
if ( ! $groupName ) $groupName = 'none' ;
if ( ! isset ( $this -> extraOrSelectors [ $groupName ])) $this -> extraOrSelectors [ $groupName ] = array ();
if ( $selector -> value instanceof Selectors ) {
$this -> extraOrSelectors [ $groupName ][] = $selector -> value ;
} else {
if ( $selector -> group ) {
// group is pre-identified, indicating Selector field=value is the OR-group condition
$s = clone $selector ;
$s -> quote = '' ;
$s -> group = null ;
$groupSelectors = new Selectors ();
$groupSelectors -> add ( $s );
} else {
// selector field is group name and selector value is another selector containing OR-group condition
$groupSelectors = new Selectors ( $selector -> value );
}
$this -> wire ( $groupSelectors );
$this -> extraOrSelectors [ $groupName ][] = $groupSelectors ;
}
return false ;
} else if ( $hasDoubleDot ) {
// has an "a.b.c" type string in the field, convert to a sub-selector
if ( count ( $fieldsArray ) > 1 ) {
$this -> syntaxError ( " Multi-dot 'a.b.c' type selectors may not be used with OR '|' fields " );
}
$fn = reset ( $fieldsArray );
$parts = explode ( '.' , $fn );
$fieldName = array_shift ( $parts );
$field = $this -> isPageField ( $fieldName );
if ( $field ) {
// we have a workable page field
/** @var Selectors $_selectors */
if ( $options [ 'findAll' ]) {
$s = " include=all " ;
} else if ( $options [ 'findHidden' ]) {
$s = " include=hidden " ;
} else if ( $options [ 'findUnpublished' ]) {
$s = " include=unpublished " ;
} else {
$s = '' ;
}
2023-03-10 19:41:40 +01:00
/** @var Selectors $_selectors */
2022-03-08 15:55:41 +01:00
$_selectors = $this -> wire ( new Selectors ( $s ));
$_selector = $_selectors -> create ( implode ( '.' , $parts ), $selector -> operator , $selector -> values );
$_selectors -> add ( $_selector );
$sel = new SelectorEqual ( " $fieldName " , $_selectors );
$sel -> quote = '[' ;
if ( ! $level ) $selectors -> replace ( $selector , $sel );
$selector = $sel ;
$sel = $this -> preProcessSelector ( $sel , $selectors , $options , $level + 1 );
if ( $sel ) $selector = $sel ;
} else {
// not a page field
}
}
return $selector ;
}
/*
* This turns out to be a lot slower than preProcessSubSelector (), but kept here for additional experiments
*
protected function preProcessSubquery ( Selector $selector ) {
$finder = $this -> wire ( new PageFinder ());
$selectors = $selector -> getValue ();
if ( ! $selectors instanceof Selectors ) return true ; // not a sub-selector
$subfield = '' ;
$fieldName = $selector -> field ;
if ( is_array ( $fieldName )) return true ; // we don't allow OR conditions for field here
if ( strpos ( $fieldName , '.' )) list ( $fieldName , $subfield ) = explode ( '.' , $fieldName );
$field = $this -> wire ( 'fields' ) -> get ( $fieldName );
if ( ! $field ) return true ; // does not resolve to a known field
$query = $finder -> find ( $selectors , array (
'returnQuery' => true ,
'returnVerbose' => false
));
$database = $this -> wire ( 'database' );
$table = $database -> escapeTable ( $field -> getTable ());
if ( $subfield == 'id' || ! $subfield ) {
$subfield = 'data' ;
} else {
$subfield = $database -> escapeCol ( $this -> wire ( 'sanitizer' ) -> fieldName ( $subfield ));
}
if ( ! $table || ! $subfield ) return true ;
static $n = 0 ;
$n ++ ;
$tableAlias = " _subquery_ { $n } _ $table " ;
$join = " $table AS $tableAlias ON $tableAlias .pages_id=pages.id AND $tableAlias . $subfield IN ( " . $query -> getQuery () . " ) " ;
echo $join . " <br /> " ;
$this -> extraJoins [] = $join ;
}
*/
/**
* Pre - process a Selector that has a [ quoted selector ] embedded within its value
*
* @ param Selector $selector
* @ param Selectors $parentSelectors
*
*/
protected function preProcessSubSelector ( Selector $selector , Selectors $parentSelectors ) {
// Selector contains another embedded selector that we need to convert to page IDs.
// Example: "field=[id>0, name=something, this=that]" converts to "field.id=123|456|789"
$selectors = $selector -> getValue ();
if ( ! $selectors instanceof Selectors ) return ;
$hasTemplate = false ;
$hasParent = false ;
$hasInclude = false ;
foreach ( $selectors as $s ) {
if ( is_array ( $s -> field )) continue ;
if ( $s -> field == 'template' ) $hasTemplate = true ;
if ( $s -> field == 'parent' || $s -> field == 'parent_id' || $s -> field == 'parent.id' ) $hasParent = true ;
if ( $s -> field == 'include' || $s -> field == 'status' ) $hasInclude = true ;
}
if ( ! $hasInclude ) {
// see if parent selector has an include mode, and copy it over to this one
foreach ( $parentSelectors as $s ) {
if ( $s -> field == 'include' || $s -> field == 'status' || $s -> field == 'check_access' ) {
$selectors -> add ( clone $s );
}
}
}
// special handling for page references, detect if parent or template is defined,
// and add it to the selector if available. This makes it faster.
if ( ! $hasTemplate || ! $hasParent ) {
$fields = is_array ( $selector -> field ) ? $selector -> field : array ( $selector -> field );
$templates = array ();
$parents = array ();
$findSelector = '' ;
foreach ( $fields as $fieldName ) {
if ( strpos ( $fieldName , '.' ) !== false ) {
/** @noinspection PhpUnusedLocalVariableInspection */
list ( $unused , $fieldName ) = explode ( '.' , $fieldName );
}
$field = $this -> fields -> get ( $fieldName );
if ( ! $field ) continue ;
if ( ! $hasTemplate && ( $field -> get ( 'template_id' ) || $field -> get ( 'template_ids' ))) {
$templateIds = FieldtypePage :: getTemplateIDs ( $field );
if ( count ( $templateIds )) {
$templates = array_merge ( $templates , $templateIds );
}
}
if ( ! $hasParent ) {
/** @var int|null $parentId */
$parentId = $field -> get ( 'parent_id' );
if ( $parentId ) {
if ( $this -> isRepeaterFieldtype ( $field -> type )) {
// repeater items not stored directly under parent_id, but as another parent under parent_id.
// so we use has_parent instead here
$selectors -> prepend ( new SelectorEqual ( 'has_parent' , $parentId ));
} else {
// direct parent: FieldtypePage or similar
$parents [] = ( int ) $parentId ;
}
}
}
if ( $field -> get ( 'findPagesSelector' ) && count ( $fields ) == 1 ) {
$findSelector = $field -> get ( 'findPagesSelector' );
}
}
if ( count ( $templates )) $selectors -> prepend ( new SelectorEqual ( 'template' , $templates ));
if ( count ( $parents )) $selectors -> prepend ( new SelectorEqual ( 'parent_id' , $parents ));
if ( $findSelector ) {
foreach ( new Selectors ( $findSelector ) as $s ) {
// add everything from findSelector, except for dynamic/runtime 'page.[something]' vars
if ( strpos ( $s -> getField ( 'string' ), 'page.' ) === 0 || strpos ( $s -> getValue ( 'string' ), 'page.' ) === 0 ) continue ;
$selectors -> append ( $s );
}
}
}
2022-11-05 18:32:48 +01:00
/** @var PageFinder $pageFinder */
2022-03-08 15:55:41 +01:00
$pageFinder = $this -> wire ( new PageFinder ());
$ids = $pageFinder -> findIDs ( $selectors );
$fieldNames = $selector -> fields ;
$fieldName = reset ( $fieldNames );
$natives = array ( 'parent' , 'parent.id' , 'parent_id' , 'children' , 'children.id' , 'child' , 'child.id' );
// populate selector value with array of page IDs
if ( count ( $ids ) == 0 ) {
// subselector resulted in 0 matches
// force non-match for this subselector by populating 'id' subfield to field name(s)
$fieldNames = array ();
foreach ( $selector -> fields as $key => $fieldName ) {
if ( strpos ( $fieldName , '.' ) !== false ) {
// reduce fieldName to just field name without subfield name
/** @noinspection PhpUnusedLocalVariableInspection */
list ( $fieldName , $subname ) = explode ( '.' , $fieldName ); // subname intentionally unused
}
$field = $this -> isPageField ( $fieldName );
if ( is_string ( $field ) && in_array ( $field , $natives )) {
// prevent matching something like parent_id=0, as that would match homepage
$fieldName = 'id' ;
} else if ( $field ) {
$fieldName .= '.id' ;
} else {
// non-Page value field
$selector -> forceMatch = false ;
}
$fieldNames [ $key ] = $fieldName ;
}
$selector -> fields = $fieldNames ;
$selector -> value = 0 ;
} else if ( in_array ( $fieldName , $natives )) {
// i.e. parent, parent_id, children, etc
$selector -> value = count ( $ids ) > 1 ? $ids : reset ( $ids );
} else {
$isPageField = $this -> isPageField ( $fieldName , true );
if ( $isPageField ) {
// FieldtypePage fields can use the "," separation syntax for speed optimization
$selector -> value = count ( $ids ) > 1 ? implode ( ',' , $ids ) : reset ( $ids );
} else {
// otherwise use array
$selector -> value = count ( $ids ) > 1 ? $ids : reset ( $ids );
}
}
$selector -> quote = '' ;
}
/**
* Given one or more selectors , create the SQL query for finding pages .
*
* @ TODO split this method up into more parts , it ' s too long
*
* @ param Selectors $selectors Array of selectors .
* @ param array $options
* @ return DatabaseQuerySelect
* @ throws PageFinderSyntaxException
*
*/
protected function ___getQuery ( $selectors , array $options ) {
$where = '' ;
$fieldCnt = array (); // counts number of instances for each field to ensure unique table aliases for ANDs on the same field
$lastSelector = null ;
$sortSelectors = array (); // selector containing 'sort=', which gets added last
$subqueries = array ();
$joins = array ();
$database = $this -> database ;
$autojoinTables = array ();
$this -> preProcessSelectors ( $selectors , $options );
$this -> numAltOperators = 0 ;
/** @var DatabaseQuerySelect $query */
$query = $this -> wire ( new DatabaseQuerySelect ());
if ( ! empty ( $options [ 'bindOptions' ])) {
foreach ( $options [ 'bindOptions' ] as $k => $v ) $query -> bindOption ( $k , $v );
}
if ( $options [ 'returnAllCols' ]) {
$opts = $this -> defaultOptions [ 'returnAllColsOptions' ];
if ( ! empty ( $options [ 'returnAllColsOptions' ])) $opts = array_merge ( $opts , $options [ 'returnAllColsOptions' ]);
$columns = array ( 'pages.*' );
if ( $opts [ 'unixTimestamps' ]) {
$columns [] = 'UNIX_TIMESTAMP(pages.created) AS created' ;
$columns [] = 'UNIX_TIMESTAMP(pages.modified) AS modified' ;
$columns [] = 'UNIX_TIMESTAMP(pages.published) AS published' ;
}
if ( $opts [ 'joinSortfield' ]) {
$columns [] = 'pages_sortfields.sortfield AS sortfield' ;
$query -> leftjoin ( 'pages_sortfields ON pages_sortfields.pages_id=pages.id' );
}
if ( $opts [ 'getNumChildren' ]) {
$query -> select ( '(SELECT COUNT(*) FROM pages AS children WHERE children.parent_id=pages.id) AS numChildren' );
}
if ( $opts [ 'joinPath' ]) {
if ( ! $this -> wire () -> modules -> isInstalled ( 'PagePaths' )) {
throw new PageFinderException ( 'Requested option for URL or path (joinPath) requires the PagePaths module be installed' );
}
$columns [] = 'pages_paths.path AS path' ;
$query -> leftjoin ( 'pages_paths ON pages_paths.pages_id=pages.id' );
}
if ( ! empty ( $opts [ 'joinFields' ])) {
2022-11-05 18:32:48 +01:00
$this -> pageArrayData [ 'joinFields' ] = array (); // identify whether each field supported autojoin
2022-03-08 15:55:41 +01:00
foreach ( $opts [ 'joinFields' ] as $joinField ) {
2022-11-05 18:32:48 +01:00
$joinField = $this -> fields -> get ( $joinField );
2023-03-10 19:41:40 +01:00
if ( ! $joinField instanceof Field ) continue ;
2022-03-08 15:55:41 +01:00
$joinTable = $database -> escapeTable ( $joinField -> getTable ());
if ( ! $joinTable || ! $joinField -> type ) continue ;
2022-11-05 18:32:48 +01:00
if ( $joinField -> type -> getLoadQueryAutojoin ( $joinField , $query )) {
$autojoinTables [ $joinTable ] = $joinTable ; // added at end if not already joined
$this -> pageArrayData [ 'joinFields' ][ $joinField -> name ] = true ;
} else {
// fieldtype does not support autojoin
$this -> pageArrayData [ 'joinFields' ][ $joinField -> name ] = false ;
}
2022-03-08 15:55:41 +01:00
}
}
} else if ( $options [ 'returnVerbose' ]) {
$columns = array ( 'pages.id' , 'pages.parent_id' , 'pages.templates_id' );
} else if ( $options [ 'returnParentIDs' ]) {
$columns = array ( 'pages.parent_id AS id' );
} else if ( $options [ 'returnTemplateIDs' ]) {
$columns = array ( 'pages.id' , 'pages.templates_id' );
} else {
$columns = array ( 'pages.id' );
}
$query -> select ( $columns );
$query -> from ( " pages " );
$query -> groupby ( $options [ 'returnParentIDs' ] ? 'pages.parent_id' : 'pages.id' );
$this -> getQueryStartLimit ( $query );
foreach ( $selectors as $selector ) {
/** @var Selector $selector */
if ( is_null ( $lastSelector )) $lastSelector = $selector ;
$selector = $this -> preProcessSelector ( $selector , $selectors , $options );
if ( ! $selector || $selector -> forceMatch === true ) continue ;
if ( $selector -> forceMatch === false ) {
$query -> where ( " 1>2 " ); // force non match
continue ;
}
2022-11-05 18:32:48 +01:00
$fields = $selector -> fields ();
2022-03-08 15:55:41 +01:00
$group = $selector -> group ; // i.e. @field
if ( count ( $fields ) > 1 ) $fields = $this -> arrangeFields ( $fields );
$field1 = reset ( $fields ); // first field including optional subfield
$this -> numAltOperators += count ( $selector -> altOperators );
// TODO Make native fields and path/url multi-field and multi-value aware
if ( $field1 === 'sort' && $selector -> operator === '=' ) {
$sortSelectors [] = $selector ;
continue ;
} else if ( $field1 === 'sort' || $field1 === 'page.sort' ) {
if ( ! in_array ( $selector -> operator , array ( '=' , '!=' , '<' , '>' , '>=' , '<=' ))) {
$this -> syntaxError ( " Property ' $field1 ' may not use operator: $selector->operator " );
}
$selector -> field = 'sort' ;
$selector -> value = ( int ) $selector -> value ();
$this -> getQueryNativeField ( $query , $selector , array ( 'sort' ), $options , $selectors );
continue ;
} else if ( $field1 === 'limit' || $field1 === 'start' ) {
continue ;
} else if ( $field1 === 'path' || $field1 === 'url' ) {
$this -> getQueryJoinPath ( $query , $selector );
continue ;
} else if ( $field1 === 'has_parent' || $field1 === 'hasParent' ) {
$this -> getQueryHasParent ( $query , $selector );
continue ;
} else if ( $field1 === 'num_children' || $field1 === 'numChildren' || $field1 === 'children.count' ) {
$this -> getQueryNumChildren ( $query , $selector );
continue ;
} else if ( $this -> hasNativeFieldName ( $fields )) {
$this -> getQueryNativeField ( $query , $selector , $fields , $options , $selectors );
continue ;
}
// where SQL specific to the foreach() of fields below, if needed.
// in this case only used by internally generated shortcuts like the blank value condition
$whereFields = '' ;
$whereFieldsType = 'AND' ;
foreach ( $fields as $fieldName ) {
// if a specific DB field from the table has been specified, then get it, otherwise assume 'data'
if ( strpos ( $fieldName , '.' )) {
// if fieldName is "a.b.c" $subfields (plural) retains "b.c" while $subfield is just "b"
list ( $fieldName , $subfields ) = explode ( '.' , $fieldName , 2 );
if ( strpos ( $subfields , '.' )) {
list ( $subfield ) = explode ( '.' , $subfields ); // just the first
} else {
$subfield = $subfields ;
}
} else {
$subfields = 'data' ;
$subfield = 'data' ;
}
$field = $this -> fields -> get ( $fieldName );
if ( ! $field ) {
// field does not exist, see if it can be processed in some other way
$field = $this -> getQueryUnknownField ( $fieldName , array (
'subfield' => $subfield ,
'subfields' => $subfields ,
'fields' => $fields ,
'query' => $query ,
'selector' => $selector ,
'selectors' => $selectors
));
if ( $field === true ) {
// true indicates the hook modified query to handle this (or ignore it), and should move to next field
continue ;
} else if ( $field instanceof Field ) {
// hook has mapped it to a field and processing of field should proceed
} else if ( $field ) {
// mapped it to an API var or something else where we need not continue processing $field or $fields
break ;
} else {
$this -> syntaxError ( " Field does not exist: $fieldName " );
}
}
// keep track of number of times this table name has appeared in the query
if ( isset ( $fieldCnt [ $field -> table ])) {
$fieldCnt [ $field -> table ] ++ ;
} else {
$fieldCnt [ $field -> table ] = 0 ;
}
// use actual table name if first instance, if second instance of table then add a number at the end
$tableAlias = $field -> table . ( $fieldCnt [ $field -> table ] ? $fieldCnt [ $field -> table ] : '' );
$tableAlias = $database -> escapeTable ( $tableAlias );
$join = '' ;
$numEmptyValues = 0 ;
$valueArray = $selector -> values ( true );
$fieldtype = $field -> type ;
$operator = $selector -> operator ;
if ( $operator === '<>' ) $operator = '!=' ;
foreach ( $valueArray as $value ) {
// shortcut for blank value condition: this ensures that NULL/non-existence is considered blank
// without this section the query would still work, but a blank value must actually be present in the field
$isEmptyValue = $fieldtype -> isEmptyValue ( $field , $value );
2024-04-04 14:37:20 +02:00
$useEmpty = $isEmptyValue || $operator [ 0 ] === '<' || (( int ) $value < 0 && $operator [ 0 ] === '>' )
|| ( $operator === '!=' && $isEmptyValue === false );
2023-03-10 19:41:40 +01:00
if ( $useEmpty && strpos ( $subfield , 'data' ) === 0 ) { // && !$fieldtype instanceof FieldtypeMulti) {
2022-03-08 15:55:41 +01:00
if ( $isEmptyValue ) $numEmptyValues ++ ;
if ( in_array ( $operator , array ( '=' , '!=' , '<' , '<=' , '>' , '>=' ))) {
// we only accommodate this optimization for single-value selectors...
if ( $this -> whereEmptyValuePossible ( $field , $subfield , $selector , $query , $value , $whereFields )) {
if ( count ( $valueArray ) > 1 && $operator == '=' ) $whereFieldsType = 'OR' ;
continue ;
}
}
}
/** @var DatabaseQuerySelect $q */
if ( isset ( $subqueries [ $tableAlias ])) {
$q = $subqueries [ $tableAlias ];
} else {
$q = $this -> wire ( new DatabaseQuerySelect ());
}
/** @var PageFinderDatabaseQuerySelect $q */
$q -> set ( 'field' , $field ); // original field if required by the fieldtype
$q -> set ( 'group' , $group ); // original group of the field, if required by the fieldtype
$q -> set ( 'selector' , $selector ); // original selector if required by the fieldtype
$q -> set ( 'selectors' , $selectors ); // original selectors (all) if required by the fieldtype
$q -> set ( 'parentQuery' , $query );
$q -> set ( 'pageFinder' , $this );
$q -> bindOption ( 'global' , true ); // ensures bound value key are globally unique
$q -> bindOption ( 'prefix' , 'pf' ); // pf=PageFinder
$q = $fieldtype -> getMatchQuery ( $q , $tableAlias , $subfield , $selector -> operator , $value );
$q -> copyTo ( $query , array ( 'select' , 'join' , 'leftjoin' , 'orderby' , 'groupby' ));
$q -> copyBindValuesTo ( $query );
if ( count ( $q -> where )) {
// $and = $selector->not ? "AND NOT" : "AND";
$and = " AND " ; /// moved NOT condition to entire generated $sql
$sql = '' ;
foreach ( $q -> where as $w ) $sql .= $sql ? " $and $w " : " $w " ;
$sql = " ( $sql ) " ;
if ( $selector -> operator == '!=' ) {
$join .= ( $join ? " \n \t \t AND $sql " : $sql );
} else if ( $selector -> not ) {
$sql = " ((NOT $sql ) OR ( $tableAlias .pages_id IS NULL)) " ;
$join .= ( $join ? " \n \t \t AND $sql " : $sql );
} else {
$join .= ( $join ? " \n \t \t OR $sql " : $sql );
}
}
}
if ( $join ) {
$joinType = 'join' ;
if ( count ( $fields ) > 1
|| ! empty ( $options [ 'startAfterID' ]) || ! empty ( $options [ 'stopBeforeID' ])
|| ( count ( $valueArray ) > 1 && $numEmptyValues > 0 )
|| ( $subfield == 'count' && ! $this -> isRepeaterFieldtype ( $field -> type ))
|| ( $selector -> not && $selector -> operator != '!=' )
|| $selector -> operator == '!=' ) {
// join should instead be a leftjoin
$joinType = " leftjoin " ;
if ( $where ) {
$whereType = $lastSelector -> str == $selector -> str ? " OR " : " ) AND ( " ;
$where .= " \n \t $whereType ( $join ) " ;
} else {
$where .= " ( $join ) " ;
}
if ( $selector -> not ) {
// removes condition from join, but ensures we still have a $join
$join = '1=1' ;
}
}
// we compile the joins after going through all the selectors, so that we can
// match up conditions to the same tables
if ( isset ( $joins [ $tableAlias ])) {
$joins [ $tableAlias ][ 'join' ] .= " AND ( $join ) " ;
} else {
$joins [ $tableAlias ] = array (
'joinType' => $joinType ,
'table' => $field -> table ,
'tableAlias' => $tableAlias ,
'join' => " ( $join ) " ,
);
}
}
$lastSelector = $selector ;
} // fields
if ( strlen ( $whereFields )) {
if ( strlen ( $where )) {
$where = " ( $where ) $whereFieldsType ( $whereFields ) " ;
} else {
$where .= " ( $whereFields ) " ;
}
}
} // selectors
if ( $where ) $query -> where ( " ( $where ) " );
$this -> getQueryAllowedTemplates ( $query , $options );
// complete the joins, matching up any conditions for the same table
foreach ( $joins as $j ) {
$joinType = $j [ 'joinType' ];
$query -> $joinType ( " $j[table] AS $j[tableAlias] ON $j[tableAlias] .pages_id=pages.id AND ( $j[join] ) " );
}
foreach ( $autojoinTables as $table ) {
if ( isset ( $fieldCnt [ $table ])) continue ; // already joined
$query -> leftjoin ( " $table ON $table .pages_id=pages.id " );
}
if ( count ( $sortSelectors )) {
foreach ( array_reverse ( $sortSelectors ) as $s ) {
$this -> getQuerySortSelector ( $query , $s );
}
}
if (( ! empty ( $options [ 'startAfterID' ]) || ! empty ( $options [ 'stopBeforeID' ])) && count ( $query -> where )) {
$wheres = array ( '(' . implode ( ' AND ' , $query -> where ) . ')' );
$query -> set ( 'where' , array ());
foreach ( array ( 'startAfterID' , 'stopBeforeID' ) as $key ) {
if ( empty ( $options [ $key ])) continue ;
$bindKey = $query -> bindValueGetKey ( $options [ $key ], \PDO :: PARAM_INT );
array_unshift ( $wheres , " pages.id= $bindKey " );
}
$query -> where ( implode ( " \n OR " , $wheres ));
}
$this -> postProcessQuery ( $query );
$this -> finalSelectors = $selectors ;
return $query ;
}
/**
* Post process a DatabaseQuerySelect for page finder
*
* @ param DatabaseQuerySelect $parentQuery
* @ throws WireException
*
*/
protected function postProcessQuery ( $parentQuery ) {
if ( count ( $this -> extraOrSelectors )) {
// there were embedded OR selectors where one of them must match
// i.e. id>0, field=(selector string), field=(selector string)
// in the above example at least one 'field' must match
// the 'field' portion is used only as a group name and isn't
// actually used as part of the resulting query or than to know
// what groups should be OR'd together
$sqls = array ();
2023-03-10 19:41:40 +01:00
foreach ( $this -> extraOrSelectors as /* $groupName => */ $selectorGroup ) {
2022-03-08 15:55:41 +01:00
$n = 0 ;
$sql = " \t pages.id IN ( \n " ;
foreach ( $selectorGroup as $selectors ) {
$pageFinder = $this -> wire ( new PageFinder ());
/** @var DatabaseQuerySelect $query */
$query = $pageFinder -> find ( $selectors , array (
'returnQuery' => true ,
'returnVerbose' => false ,
'findAll' => true ,
'bindOptions' => array (
'prefix' => 'pfor' ,
'global' => true ,
)
));
if ( $n > 0 ) $sql .= " \n \t OR pages.id IN ( \n " ;
$query -> set ( 'groupby' , array ());
$query -> set ( 'select' , array ( 'pages.id' ));
$query -> set ( 'orderby' , array ());
$sql .= tabIndent ( " \t \t " . $query -> getQuery () . " \n ) " , 2 );
$query -> copyBindValuesTo ( $parentQuery , array ( 'inSQL' => $sql ));
$n ++ ;
}
$sqls [] = $sql ;
}
if ( count ( $sqls )) {
$sql = implode ( " \n ) AND ( \n " , $sqls );
$parentQuery -> where ( " ( \n $sql\n ) " );
}
}
/* Possibly move existing subselectors to work like this rather than how they currently are
if ( count ( $this -> extraSubSelectors )) {
$sqls = array ();
foreach ( $this -> extraSubSelectors as $fieldName => $selectorGroup ) {
$fieldName = $this -> wire ( 'database' ) -> escapeCol ( $fieldName );
$n = 0 ;
$sql = " \t pages.id IN ( \n " ;
foreach ( $selectorGroup as $selectors ) {
$pageFinder = new PageFinder ();
$query = $pageFinder -> find ( $selectors , array ( 'returnQuery' => true , 'returnVerbose' => false ));
if ( $n > 0 ) $sql .= " \n \t AND pages.id IN ( \n " ;
$query -> set ( 'groupby' , array ());
$query -> set ( 'select' , array ( 'pages.id' ));
$query -> set ( 'orderby' , array ());
// foreach($this->nativeWheres as $where) $query->where($where);
$sql .= tabIndent ( " \t \t " . $query -> getQuery () . " \n ) " , 2 );
$n ++ ;
}
$sqls [] = $sql ;
}
if ( count ( $sqls )) {
$sql = implode ( " \n ) AND ( \n " , $sqls );
$parentQuery -> where ( " ( \n $sql\n ) " );
}
}
*/
}
/**
* Generate SQL and modify $query for situations where it should be possible to match empty values
*
* This can include equals / not - equals with blank or 0 , as well as greater / less - than searches that
* can potentially match blank or 0.
*
* @ param Field $field
* @ param string $col
* @ param Selector $selector
* @ param DatabaseQuerySelect $query
* @ param string $value The value presumed to be blank ( passed the empty () test )
* @ param string $where SQL where string that will be modified / appended
* @ return bool Whether or not the query was handled and modified
*
*/
protected function whereEmptyValuePossible ( Field $field , $col , $selector , $query , $value , & $where ) {
// look in table that has no pages_id relation back to pages, using the LEFT JOIN / IS NULL trick
// OR check for blank value as defined by the fieldtype
static $tableCnt = 0 ;
$ft = $field -> type ;
$operator = $selector -> operator ;
$database = $this -> database ;
$table = $database -> escapeTable ( $field -> table );
$tableAlias = $table . " __blank " . ( ++ $tableCnt );
$blankValue = $ft -> getBlankValue ( new NullPage (), $field );
$blankIsObject = is_object ( $blankValue );
$whereType = 'OR' ;
$sql = '' ;
$operators = array (
'=' => '!=' ,
'!=' => '=' ,
'<' => '>=' ,
'<=' => '>' ,
'>' => '<=' ,
'>=' => '<'
);
if ( $blankIsObject ) $blankValue = '' ;
if ( ! isset ( $operators [ $operator ])) return false ;
if ( $selector -> not ) $operator = $operators [ $operator ]; // reverse
if ( $col !== 'data' && ! ctype_alnum ( $col )) {
// check for unsupported column
if ( ! ctype_alnum ( str_replace ( '_' , '' , $col ))) return false ;
}
// ask Fieldtype if it would prefer to handle matching this empty value selector
if ( $ft -> isEmptyValue ( $field , $selector )) {
// fieldtype will handle matching the selector in its getMatchQuery
return false ;
} else if (( $operator === '=' || $operator === '!=' ) && $ft -> isEmptyValue ( $field , $value ) && $ft -> isEmptyValue ( $field , '0000-00-00' )) {
// matching empty in date, datetime, timestamp column with equals or not-equals condition
// non-presence of row is required in order to match empty/blank (in MySQL 8.x)
$is = $operator === '=' ? 'IS' : 'IS NOT' ;
$sql = " $tableAlias .pages_id $is NULL " ;
} else if ( $operator === '=' ) {
// equals
// non-presence of row is equal to value being blank
$bindKey = $query -> bindValueGetKey ( $blankValue );
if ( $ft -> isEmptyValue ( $field , $value )) {
2022-11-05 18:32:48 +01:00
// matching an empty value: null or literal empty value
2022-03-08 15:55:41 +01:00
$sql = " $tableAlias . $col IS NULL OR ( $tableAlias . $col = $bindKey " ;
2022-11-05 18:32:48 +01:00
if ( $value === '' && ! $ft -> isEmptyValue ( $field , '0' ) && $field -> get ( 'zeroNotEmpty' )) {
// MySQL blank string will also match zero (0) in some cases, so we prevent that here
// @todo remove the 'zeroNotEmpty' condition for test on dev as it limits to specific fieldtypes is likely unnecessary
$sql .= " AND $tableAlias . $col !='0' " ;
}
2022-03-08 15:55:41 +01:00
} else {
2022-11-05 18:32:48 +01:00
// matching a non-empty value
2022-03-08 15:55:41 +01:00
$sql = " ( $tableAlias . $col = $bindKey " ;
}
/*
if ( $value !== " 0 " && $blankValue !== " 0 " && ! $ft -> isEmptyValue ( $field , " 0 " )) {
// if zero is not considered an empty value, exclude it from matching
// if the search isn't specifically for a "0"
$sql .= " AND $tableAlias . $col !='0' " ;
}
*/
$sql .= " ) " ;
} else if ( $operator === '!=' || $operator === '<>' ) {
// not equals
2024-04-04 14:37:20 +02:00
$whereType = 'AND' ;
$zeroIsEmpty = $ft -> isEmptyValue ( $field , " 0 " );
$zeroIsNotEmpty = ! $zeroIsEmpty ;
$value = ( string ) $value ;
$blankValue = ( string ) $blankValue ;
if ( $value === '' ) {
// match present rows that do not contain a blank string (or 0, when applicable)
$sql = " $tableAlias . $col IS NOT NULL AND ( $tableAlias . $col !='' " ;
if ( $zeroIsEmpty ) {
$sql .= " AND $tableAlias . $col !='0' " ;
} else {
$sql .= " OR $tableAlias . $col ='0' " ;
}
$sql .= ')' ;
} else if ( $value === " 0 " && $zeroIsNotEmpty ) {
// may match non-rows (no value present) or row with value=0
2022-03-08 15:55:41 +01:00
$sql = " $tableAlias . $col IS NULL OR $tableAlias . $col !='0' " ;
2024-04-04 14:37:20 +02:00
} else if ( $value !== " 0 " && $zeroIsEmpty ) {
// match all rows except empty and those having specific non-empty value
$bindKey = $query -> bindValueGetKey ( $value );
$sql = " $tableAlias . $col IS NULL OR $tableAlias . $col != $bindKey " ;
2022-03-08 15:55:41 +01:00
} else if ( $blankIsObject ) {
2024-04-04 14:37:20 +02:00
// match all present rows
2022-03-08 15:55:41 +01:00
$sql = " $tableAlias . $col IS NOT NULL " ;
} else {
2024-04-04 14:37:20 +02:00
// match all present rows that are not blankValue and not given blank value...
$bindKeyBlank = $query -> bindValueGetKey ( $blankValue );
$bindKeyValue = $query -> bindValueGetKey ( $value );
$sql = " $tableAlias . $col IS NOT NULL AND $tableAlias . $col != $bindKeyValue AND ( $tableAlias . $col != $bindKeyBlank " ;
if ( $zeroIsNotEmpty && $blankValue !== " 0 " && $value !== " 0 " ) {
// ...allow for 0 to match also if 0 is not considered empty value
2022-03-08 15:55:41 +01:00
$sql .= " OR $tableAlias . $col ='0' " ;
}
$sql .= " ) " ;
}
2024-04-04 14:37:20 +02:00
if ( $ft instanceof FieldtypeMulti && ! $ft -> isEmptyValue ( $field , $value )) {
// when a multi-row field is in use, exclude match when any of the rows contain $value
$tableMulti = $table . " __multi $tableCnt " ;
$bindKey = $query -> bindValueGetKey ( $value );
$query -> leftjoin ( " $table AS $tableMulti ON $tableMulti .pages_id=pages.id AND $tableMulti . $col = $bindKey " );
$query -> where ( " $tableMulti . $col IS NULL " );
}
2022-03-08 15:55:41 +01:00
} else if ( $operator == '<' || $operator == '<=' ) {
// less than
if ( $value > 0 && $ft -> isEmptyValue ( $field , " 0 " )) {
// non-rows can be included as counting for 0
$bindKey = $query -> bindValueGetKey ( $value );
$sql = " $tableAlias . $col IS NULL OR $tableAlias . $col $operator $bindKey " ;
} else {
// we won't handle it here
return false ;
}
} else if ( $operator == '>' || $operator == '>=' ) {
if ( $value < 0 && $ft -> isEmptyValue ( $field , " 0 " )) {
// non-rows can be included as counting for 0
$bindKey = $query -> bindValueGetKey ( $value );
$sql = " $tableAlias . $col IS NULL OR $tableAlias . $col $operator $bindKey " ;
} else {
// we won't handle it here
return false ;
}
}
$query -> leftjoin ( " $table AS $tableAlias ON $tableAlias .pages_id=pages.id " );
$where .= strlen ( $where ) ? " $whereType ( $sql ) " : " ( $sql ) " ;
return true ;
}
/**
* Determine which templates the user is allowed to view
*
* @ param DatabaseQuerySelect $query
* @ param array $options
*
*/
protected function getQueryAllowedTemplates ( DatabaseQuerySelect $query , $options ) {
if ( $options ) {}
// if access checking is disabled then skip this
if ( ! $this -> checkAccess ) return ;
// no need to perform this checking if the user is superuser
$user = $this -> wire () -> user ;
if ( $user -> isSuperuser ()) return ;
static $where = null ;
static $where2 = null ;
static $leftjoin = null ;
static $cacheUserID = null ;
if ( $cacheUserID !== $user -> id ) {
// clear cached values
$where = null ;
$where2 = null ;
$leftjoin = null ;
$cacheUserID = $user -> id ;
}
$hasWhereHook = $this -> wire () -> hooks -> isHooked ( 'PageFinder::getQueryAllowedTemplatesWhere()' );
// if a template was specified in the search, then we won't attempt to verify access
// if($this->templates_id) return;
// if findOne optimization is set, we don't check template access
// if($options['findOne']) return;
// if we've already figured out this part from a previous query, then use it
if ( ! is_null ( $where )) {
if ( $hasWhereHook ) {
$where = $this -> getQueryAllowedTemplatesWhere ( $query , $where );
$where2 = $this -> getQueryAllowedTemplatesWhere ( $query , $where2 );
}
$query -> where ( $where );
$query -> where ( $where2 );
$query -> leftjoin ( $leftjoin );
return ;
}
// array of templates they ARE allowed to access
$yesTemplates = array ();
// array of templates they are NOT allowed to access
$noTemplates = array ();
$guestRoleID = $this -> config -> guestUserRolePageID ;
$cacheUserID = $user -> id ;
if ( $user -> isGuest ()) {
// guest
foreach ( $this -> templates as $template ) {
2023-03-10 19:41:40 +01:00
/** @var Template $template */
2022-03-08 15:55:41 +01:00
if ( $template -> guestSearchable || ! $template -> useRoles ) {
$yesTemplates [ $template -> id ] = $template ;
continue ;
}
foreach ( $template -> roles as $role ) {
if ( $role -> id != $guestRoleID ) continue ;
$yesTemplates [ $template -> id ] = $template ;
break ;
}
}
} else {
// other logged-in user
$userRoleIDs = array ();
foreach ( $user -> roles as $role ) {
$userRoleIDs [] = $role -> id ;
}
foreach ( $this -> templates as $template ) {
2023-03-10 19:41:40 +01:00
/** @var Template $template */
2022-03-08 15:55:41 +01:00
if ( $template -> guestSearchable || ! $template -> useRoles ) {
$yesTemplates [ $template -> id ] = $template ;
continue ;
}
foreach ( $template -> roles as $role ) {
if ( $role -> id != $guestRoleID && ! in_array ( $role -> id , $userRoleIDs )) continue ;
$yesTemplates [ $template -> id ] = $template ;
break ;
}
}
}
// determine which templates the user is not allowed to access
foreach ( $this -> templates as $template ) {
2023-03-10 19:41:40 +01:00
/** @var Template $template */
2022-03-08 15:55:41 +01:00
if ( ! isset ( $yesTemplates [ $template -> id ])) $noTemplates [ $template -> id ] = $template ;
}
$in = '' ;
$yesCnt = count ( $yesTemplates );
$noCnt = count ( $noTemplates );
if ( $noCnt ) {
// pages_access lists pages that are inheriting access from others.
// join in any pages that are using any of the noTemplates to get their access.
// we want pages_access.pages_id to be NULL, which indicates that none of the
// noTemplates was joined, and the page is accessible to the user.
$leftjoin = " pages_access ON (pages_access.pages_id=pages.id AND pages_access.templates_id IN( " ;
foreach ( $noTemplates as $template ) $leftjoin .= (( int ) $template -> id ) . " , " ;
$leftjoin = rtrim ( $leftjoin , " , " ) . " )) " ;
$query -> leftjoin ( $leftjoin );
$where2 = " pages_access.pages_id IS NULL " ;
if ( $hasWhereHook ) $where2 = $this -> getQueryAllowedTemplatesWhere ( $query , $where2 );
$query -> where ( $where2 );
}
if ( $noCnt > 0 && $noCnt < $yesCnt ) {
$templates = $noTemplates ;
$yes = false ;
} else {
$templates = $yesTemplates ;
$yes = true ;
}
foreach ( $templates as $template ) {
$in .= (( int ) $template -> id ) . " , " ;
}
$in = rtrim ( $in , " , " );
$where = " pages.templates_id " ;
if ( $in && $yes ) {
$where .= " IN( $in ) " ;
} else if ( $in ) {
$where .= " NOT IN( $in ) " ;
} else {
$where = " <0 " ; // no match possible
}
// allow for hooks to modify or add to the WHERE conditions
if ( $hasWhereHook ) $where = $this -> getQueryAllowedTemplatesWhere ( $query , $where );
$query -> where ( $where );
}
/**
* Method that allows external hooks to add to or modify the access control WHERE conditions
*
* Called only if it ' s hooked . To utilize it , modify the $where argument in a BEFORE hook
* or the $event -> return in an AFTER hook .
*
* @ param DatabaseQuerySelect $query
* @ param string $where SQL string for WHERE statement , not including the actual " WHERE "
* @ return string
*/
protected function ___getQueryAllowedTemplatesWhere ( DatabaseQuerySelect $query , $where ) {
return $where ;
}
protected function getQuerySortSelector ( DatabaseQuerySelect $query , Selector $selector ) {
// $field = is_array($selector->field) ? reset($selector->field) : $selector->field;
$values = is_array ( $selector -> value ) ? $selector -> value : array ( $selector -> value );
$fields = $this -> fields ;
$pages = $this -> pages ;
$database = $this -> database ;
$user = $this -> wire () -> user ;
2022-11-05 18:32:48 +01:00
$language = $this -> languages && $user && $user -> language ? $user -> language : null ;
2022-03-08 15:55:41 +01:00
2022-11-05 18:32:48 +01:00
// support `sort=a|b|c` in correct order (because orderby prepend used below)
if ( count ( $values ) > 1 ) $values = array_reverse ( $values );
2022-03-08 15:55:41 +01:00
foreach ( $values as $value ) {
$fc = substr ( $value , 0 , 1 );
$lc = substr ( $value , - 1 );
$descending = $fc == '-' || $lc == '-' ;
$value = trim ( $value , " -+ " );
$subValue = '' ;
// $terValue = ''; // not currently used, here for future use
if ( $this -> lastOptions [ 'reverseSort' ]) $descending = ! $descending ;
if ( strpos ( $value , " . " )) {
list ( $value , $subValue ) = explode ( " . " , $value , 2 ); // i.e. some_field.title
if ( strpos ( $subValue , " . " )) {
list ( $subValue , $terValue ) = explode ( " . " , $subValue , 2 );
$terValue = $this -> sanitizer -> fieldName ( $terValue );
if ( strpos ( $terValue , " . " )) $this -> syntaxError ( " $value . $subValue . $terValue not supported " );
}
$subValue = $this -> sanitizer -> fieldName ( $subValue );
}
$value = $this -> sanitizer -> fieldName ( $value );
if ( $value == 'parent' && $subValue == 'path' ) $subValue = 'name' ; // path not supported, substitute name
if ( $value == 'random' ) {
$value = 'RAND()' ;
} else if ( $value == 'num_children' || $value == 'numChildren' || ( $value == 'children' && $subValue == 'count' )) {
// sort by quantity of children
$value = $this -> getQueryNumChildren ( $query , $this -> wire ( new SelectorGreaterThan ( 'num_children' , " -1 " )));
} else if ( $value == 'parent' && ( $subValue == 'num_children' || $subValue == 'numChildren' || $subValue == 'children' )) {
throw new WireException ( " Sort by parent.num_children is not currently supported " );
} else if ( $value == 'parent' && ( empty ( $subValue ) || $pages -> loader () -> isNativeColumn ( $subValue ))) {
// sort by parent native field only
if ( empty ( $subValue )) $subValue = 'name' ;
$subValue = $database -> escapeCol ( $subValue );
$tableAlias = " _sort_parent_ $subValue " ;
$query -> join ( " pages AS $tableAlias ON $tableAlias .id=pages.parent_id " );
$value = " $tableAlias . $subValue " ;
} else if ( $value == 'template' ) {
// sort by template
$tableAlias = $database -> escapeTable ( " _sort_templates " . ( $subValue ? " _ $subValue " : '' ));
$query -> join ( " templates AS $tableAlias ON $tableAlias .id=pages.templates_id " );
$value = " $tableAlias . " . ( $subValue ? $database -> escapeCol ( $subValue ) : " name " );
} else if ( $fields -> isNative ( $value ) && ! $subValue && $pages -> loader () -> isNativeColumn ( $value )) {
// sort by a native field (with no subfield)
if ( $value == 'name' && $language && ! $language -> isDefault () && $this -> supportsLanguagePageNames ()) {
// substitute language-specific name field when LanguageSupportPageNames is active and language is not default
$value = " if(pages.name $language !='', pages.name $language , pages.name) " ;
} else {
$value = " pages. " . $database -> escapeCol ( $value );
}
2024-04-04 14:37:20 +02:00
} else if (( $value === 'path' || $value === 'url' ) && $this -> wire () -> modules -> isInstalled ( 'PagePaths' )) {
static $pathN = 0 ;
$pathN ++ ;
$pathsTable = " _sort_pages_paths $pathN " ;
if ( $language && ! $language -> isDefault () && $this -> supportsLanguagePageNames ()) {
$query -> leftjoin ( " pages_paths AS $pathsTable ON $pathsTable .pages_id=pages.id AND $pathsTable .language_id=0 " );
$lid = ( int ) $language -> id ;
$asc = $descending ? 'DESC' : 'ASC' ;
$pathsLangTable = $pathsTable . " _ $lid " ;
$s = " pages_paths AS $pathsLangTable ON $pathsLangTable .pages_id=pages.id AND $pathsLangTable .language_id= $lid " ;
$query -> leftjoin ( $s );
$query -> orderby ( " if( $pathsLangTable .pages_id IS NULL, $pathsTable .path, $pathsLangTable .path) $asc " );
$value = false ;
} else {
$query -> leftjoin ( " pages_paths AS $pathsTable ON $pathsTable .pages_id=pages.id " );
$value = " $pathsTable .path " ;
}
2022-03-08 15:55:41 +01:00
} else {
// sort by custom field, or parent w/custom field
if ( $value == 'parent' ) {
$useParent = true ;
$value = $subValue ? $subValue : 'title' ; // needs a custom field, not "name"
$subValue = 'data' ;
$idColumn = 'parent_id' ;
} else {
$useParent = false ;
$idColumn = 'id' ;
}
$field = $fields -> get ( $value );
if ( ! $field ) {
// unknown field
continue ;
}
$fieldName = $database -> escapeCol ( $field -> name );
$subValue = $database -> escapeCol ( $subValue );
$tableAlias = $useParent ? " _sort_parent_ $fieldName " : " _sort_ $fieldName " ;
if ( $subValue ) $tableAlias .= " _ $subValue " ;
$table = $database -> escapeTable ( $field -> table );
if ( $field -> type instanceof FieldtypePage ) {
$blankValue = new PageArray ();
} else {
$blankValue = $field -> type -> getBlankValue ( $this -> pages -> newNullPage (), $field );
}
$query -> leftjoin ( " $table AS $tableAlias ON $tableAlias .pages_id=pages. $idColumn " );
$customValue = $field -> type -> getMatchQuerySort ( $field , $query , $tableAlias , $subValue , $descending );
if ( ! empty ( $customValue )) {
// Fieldtype handled it: boolean true (handled by Fieldtype) or string to add to orderby
if ( is_string ( $customValue )) $query -> orderby ( $customValue , true );
$value = false ;
} else if ( $subValue === 'count' ) {
if ( $this -> isRepeaterFieldtype ( $field -> type )) {
// repeaters have a native count column that can be used for sorting
$value = " $tableAlias .count " ;
} else {
// sort by quantity of items
$value = " COUNT( $tableAlias .data) " ;
}
2023-03-10 19:41:40 +01:00
} else if ( $blankValue instanceof PageArray || $blankValue instanceof Page ) {
2022-03-08 15:55:41 +01:00
// If it's a FieldtypePage, then data isn't worth sorting on because it just contains an ID to the page
// so we also join the page and sort on it's name instead of the field's "data" field.
if ( ! $subValue ) $subValue = 'name' ;
$tableAlias2 = " _sort_ " . ( $useParent ? 'parent' : 'page' ) . " _ $fieldName " . ( $subValue ? " _ $subValue " : '' );
if ( $this -> fields -> isNative ( $subValue ) && $pages -> loader () -> isNativeColumn ( $subValue )) {
$query -> leftjoin ( " pages AS $tableAlias2 ON $tableAlias .data= $tableAlias2 . $idColumn " );
$value = " $tableAlias2 . $subValue " ;
if ( $subValue == 'name' && $language && ! $language -> isDefault () && $this -> supportsLanguagePageNames ()) {
// append language ID to 'name' when performing sorts within another language and LanguageSupportPageNames in place
$value = " if( $value $language !='', $value $language , $value ) " ;
}
} else if ( $subValue == 'parent' ) {
$query -> leftjoin ( " pages AS $tableAlias2 ON $tableAlias .data= $tableAlias2 . $idColumn " );
$value = " $tableAlias2 .name " ;
} else {
$subValueField = $this -> fields -> get ( $subValue );
if ( $subValueField ) {
$subValueTable = $database -> escapeTable ( $subValueField -> getTable ());
$query -> leftjoin ( " $subValueTable AS $tableAlias2 ON $tableAlias .data= $tableAlias2 .pages_id " );
$value = " $tableAlias2 .data " ;
if ( $language && ! $language -> isDefault () && $subValueField -> type instanceof FieldtypeLanguageInterface ) {
// append language id to data, i.e. "data1234"
$value .= $language ;
}
} else {
// error: unknown field
}
}
} else if ( ! $subValue && $language && ! $language -> isDefault () && $field -> type instanceof FieldtypeLanguageInterface ) {
// multi-language field, sort by the language version
$value = " if( $tableAlias .data $language != '', $tableAlias .data $language , $tableAlias .data) " ;
} else {
// regular field, just sort by data column
2023-03-10 19:41:40 +01:00
$value = " $tableAlias . " . ( $subValue ? $subValue : " data " );
2022-03-08 15:55:41 +01:00
}
}
if ( is_string ( $value ) && strlen ( $value )) {
if ( $descending ) {
$query -> orderby ( " $value DESC " , true );
} else {
$query -> orderby ( " $value " , true );
}
}
}
}
protected function getQueryStartLimit ( DatabaseQuerySelect $query ) {
$start = $this -> start ;
$limit = $this -> limit ;
if ( $limit ) {
$limit = ( int ) $limit ;
$input = $this -> wire () -> input ;
$sql = '' ;
if ( is_null ( $start ) && $input ) {
// if not specified in the selector, assume the 'start' property from the default page's pageNum
$pageNum = $input -> pageNum - 1 ; // make it zero based for calculation
$start = $pageNum * $limit ;
}
if ( ! is_null ( $start )) {
$start = ( int ) $start ;
$this -> start = $start ;
$sql .= " $start , " ;
}
$sql .= " $limit " ;
if ( $this -> getTotal && $this -> getTotalType != 'count' ) $query -> select ( " SQL_CALC_FOUND_ROWS " );
if ( $sql ) $query -> limit ( $sql );
}
}
/**
* Special case when requested value is path or URL
*
* @ param DatabaseQuerySelect $query
* @ param Selector $selector
* @ throws PageFinderSyntaxException
*
*/
protected function ___getQueryJoinPath ( DatabaseQuerySelect $query , $selector ) {
$database = $this -> database ;
$modules = $this -> wire () -> modules ;
$sanitizer = $this -> sanitizer ;
// determine whether we will include use of multi-language page names
if ( $this -> supportsLanguagePageNames ()) {
$langNames = array ();
foreach ( $this -> languages as $language ) {
2023-03-10 19:41:40 +01:00
/** @var Language $language */
2022-03-08 15:55:41 +01:00
if ( ! $language -> isDefault ()) $langNames [ $language -> id ] = " name " . ( int ) $language -> id ;
}
if ( ! count ( $langNames )) $langNames = null ;
} else {
$langNames = null ;
}
2022-11-05 18:32:48 +01:00
if ( $modules -> isInstalled ( 'PagePaths' )) {
2022-03-08 15:55:41 +01:00
$pagePaths = $modules -> get ( 'PagePaths' );
/** @var PagePaths $pagePaths */
$pagePaths -> getMatchQuery ( $query , $selector );
return ;
}
if ( $selector -> operator !== '=' ) {
$this -> syntaxError ( " Operator ' $selector->operator ' is not supported for path or url unless: 1) non-multi-language; 2) you install the PagePaths module. " );
}
$selectorValue = $selector -> value ;
if ( $selectorValue === '/' ) {
$parts = array ();
$query -> where ( " pages.id=1 " );
} else {
if ( is_array ( $selectorValue )) {
// only the PagePaths module can perform OR value searches on path/url
if ( $langNames ) {
$this -> syntaxError ( " OR values not supported for multi-language 'path' or 'url' " );
} else {
$this -> syntaxError ( " OR value support of 'path' or 'url' requires core PagePaths module " );
}
}
if ( $langNames ) {
2022-11-05 18:32:48 +01:00
$module = $this -> languages -> pageNames ();
if ( $module ) $selectorValue = $module -> removeLanguageSegment ( $selectorValue );
2022-03-08 15:55:41 +01:00
}
$parts = explode ( '/' , rtrim ( $selectorValue , '/' ));
$part = $sanitizer -> pageName ( array_pop ( $parts ), Sanitizer :: toAscii );
$bindKey = $query -> bindValueGetKey ( $part );
$sql = " pages.name= $bindKey " ;
if ( $langNames ) {
foreach ( $langNames as $langName ) {
$bindKey = $query -> bindValueGetKey ( $part );
$langName = $database -> escapeCol ( $langName );
$sql .= " OR pages. $langName = $bindKey " ;
}
}
$query -> where ( " ( $sql ) " );
if ( ! count ( $parts )) $query -> where ( " pages.parent_id=1 " );
}
$alias = 'pages' ;
$lastAlias = 'pages' ;
/** @noinspection PhpAssignmentInConditionInspection */
while ( $n = count ( $parts )) {
$n = ( int ) $n ;
$part = $sanitizer -> pageName ( array_pop ( $parts ), Sanitizer :: toAscii );
if ( strlen ( $part )) {
$alias = " parent $n " ;
//$query->join("pages AS $alias ON ($lastAlias.parent_id=$alias.id AND $alias.name='$part')");
$bindKey = $query -> bindValueGetKey ( $part );
$sql = " pages AS $alias ON ( $lastAlias .parent_id= $alias .id AND ( $alias .name= $bindKey " ;
2023-03-10 19:41:40 +01:00
if ( $langNames ) foreach ( $langNames as /* $id => */ $name ) {
2022-03-08 15:55:41 +01:00
// $status = "status" . (int) $id;
// $sql .= " OR ($alias.$name='$part' AND $alias.$status>0) ";
$bindKey = $query -> bindValueGetKey ( $part );
$sql .= " OR $alias . $name = $bindKey " ;
}
$sql .= '))' ;
$query -> join ( $sql );
} else {
$query -> join ( " pages AS rootparent $n ON ( $alias .parent_id=rootparent $n .id AND rootparent $n .id=1) " );
}
$lastAlias = $alias ;
}
}
/**
* Special case when field is native to the pages table
*
* TODO not all operators will work here , so may want to add some translation or filtering
*
* @ param DatabaseQuerySelect $query
* @ param Selector $selector
* @ param array $fields
* @ param array $options
* @ param Selectors $selectors
* @ throws PageFinderSyntaxException
*
*/
protected function getQueryNativeField ( DatabaseQuerySelect $query , $selector , $fields , array $options , $selectors ) {
$values = $selector -> values ( true );
$SQL = '' ;
$database = $this -> database ;
$sanitizer = $this -> sanitizer ;
2022-11-05 18:32:48 +01:00
$datetime = $this -> wire () -> datetime ;
2022-03-08 15:55:41 +01:00
foreach ( $fields as $field ) {
// the following fields are defined in each iteration here because they may be modified in the loop
$table = " pages " ;
$operator = $selector -> operator ;
2024-04-04 14:37:20 +02:00
$not = $selector -> not ;
2022-03-08 15:55:41 +01:00
$compareType = $selectors :: getSelectorByOperator ( $operator , 'compareType' );
$isPartialOperator = ( $compareType & Selector :: compareTypeFind );
$subfield = '' ;
$IDs = array (); // populated in special cases where we can just match parent IDs
$sql = '' ;
if ( strpos ( $field , '.' )) {
list ( $field , $subfield ) = explode ( '.' , $field );
$subfield = $sanitizer -> fieldName ( $subfield );
}
$field = $sanitizer -> fieldName ( $field );
if ( $field == 'sort' && $subfield ) $subfield = '' ;
if ( $field == 'child' ) $field = 'children' ;
if ( $field != 'children' && ! $this -> fields -> isNative ( $field )) {
$subfield = $field ;
$field = '_pages' ;
}
$isParent = $field === 'parent' || $field === 'parent_id' ;
$isChildren = $field === 'children' ;
$isPages = $field === '_pages' ;
if ( $isParent || $isChildren || $isPages ) {
// parent, children, pages
if (( $isPages || $isParent ) && ! $isPartialOperator && ( ! $subfield || in_array ( $subfield , array ( 'id' , 'path' , 'url' )))) {
// match by location (id or path)
// convert parent fields like '/about/company/history' to the equivalent ID
foreach ( $values as $k => $v ) {
if ( ctype_digit ( " $v " )) continue ;
$v = $sanitizer -> pagePathName ( $v , Sanitizer :: toAscii );
if ( strpos ( $v , '/' ) === false ) $v = " / $v " ; // prevent a plain string with no slashes
// convert path to id
$parent = $this -> pages -> get ( $v );
$values [ $k ] = $parent instanceof NullPage ? null : $parent -> id ;
}
$this -> parent_id = null ;
if ( $isParent ) {
2022-11-05 18:32:48 +01:00
if ( $operator === '=' ) $IDs = $values ;
2022-03-08 15:55:41 +01:00
$field = 'parent_id' ;
2022-11-05 18:32:48 +01:00
if ( count ( $values ) == 1 && count ( $fields ) == 1 && $operator === '=' ) {
2022-03-08 15:55:41 +01:00
$this -> parent_id = reset ( $values );
}
}
} else {
// matching by a parent's native or custom field (subfield)
if ( ! $this -> fields -> isNative ( $subfield )) {
$finder = $this -> wire ( new PageFinder ());
$finderMethod = 'findIDs' ;
$includeSelector = 'include=all' ;
if ( $field === 'children' || $field === '_pages' ) {
if ( $subfield ) {
$s = '' ;
if ( $field === 'children' ) $finderMethod = 'findParentIDs' ;
// inherit include mode from main selector
$includeSelector = $this -> getIncludeSelector ( $selectors );
} else if ( $field === 'children' ) {
$s = 'children.id' ;
} else {
$s = 'id' ;
}
} else {
$s = 'children.count>0, ' ;
}
$IDs = $finder -> $finderMethod ( new Selectors ( ltrim (
" $includeSelector , " .
" $s $subfield $operator " . $sanitizer -> selectorValue ( $values ),
','
)));
if ( ! count ( $IDs )) $IDs [] = - 1 ; // forced non match
} else {
// native
static $n = 0 ;
if ( $field === 'children' ) {
$table = " _children_native " . ( ++ $n );
$query -> join ( " pages AS $table ON $table .parent_id=pages.id " );
} else if ( $field === '_pages' ) {
$table = 'pages' ;
} else {
$table = " _parent_native " . ( ++ $n );
$query -> join ( " pages AS $table ON pages.parent_id= $table .id " );
}
$field = $subfield ;
}
}
2024-04-04 14:37:20 +02:00
} else if ( $field === 'id' && count ( $values ) > 1 ) {
if ( $operator === '=' ) {
$IDs = $values ;
} else if ( $operator === '!=' && ! $not ) {
$not = true ;
$operator = '=' ;
$IDs = $values ;
}
2022-11-05 18:32:48 +01:00
2022-03-08 15:55:41 +01:00
} else {
// primary field is not 'parent', 'children' or 'pages'
}
if ( count ( $IDs )) {
// parentIDs or IDs found via another query, and we don't need to match anything other than the parent ID
2024-04-04 14:37:20 +02:00
$in = $not ? " NOT IN " : " IN " ;
2022-03-08 15:55:41 +01:00
$sql .= in_array ( $field , array ( 'parent' , 'parent_id' )) ? " $table .parent_id " : " $table .id " ;
2024-04-04 14:37:20 +02:00
$IDs = $sanitizer -> intArray ( $IDs , array ( 'strict' => true ));
$strIDs = count ( $IDs ) ? implode ( ',' , $IDs ) : '-1' ;
2022-11-05 18:32:48 +01:00
$sql .= " $in ( $strIDs ) " ;
if ( $subfield === 'sort' ) $query -> orderby ( " FIELD( $table .id, $strIDs ) " );
unset ( $strIDs );
2022-03-08 15:55:41 +01:00
} else foreach ( $values as $value ) {
if ( is_null ( $value )) {
// an invalid/unknown walue was specified, so make sure it fails
$sql .= " 1>2 " ;
continue ;
}
if ( in_array ( $field , array ( 'templates_id' , 'template' ))) {
// convert templates specified as a name to the numeric template ID
// allows selectors like 'template=my_template_name'
$field = 'templates_id' ;
2022-11-05 18:32:48 +01:00
if ( count ( $values ) == 1 && $operator === '=' ) $this -> templates_id = reset ( $values );
2022-03-08 15:55:41 +01:00
if ( ! ctype_digit ( " $value " )) $value = (( $template = $this -> templates -> get ( $value )) ? $template -> id : 0 );
} else if ( in_array ( $field , array ( 'created' , 'modified' , 'published' ))) {
// prepare value for created, modified or published date fields
2023-03-10 19:41:40 +01:00
if ( ! ctype_digit ( " $value " )) {
2022-11-05 18:32:48 +01:00
$value = $datetime -> strtotime ( $value );
2022-03-08 15:55:41 +01:00
}
if ( empty ( $value )) {
$value = null ;
if ( $operator === '>' || $operator === '=>' ) {
$value = $field === 'published' ? '1000-01-01 00:00:00' : '1970-01-01 00:00:01' ;
}
} else {
$value = date ( 'Y-m-d H:i:s' , $value );
}
} else if ( in_array ( $field , array ( 'id' , 'parent_id' , 'templates_id' , 'sort' ))) {
$value = ( int ) $value ;
}
$isName = $field === 'name' || strpos ( $field , 'name' ) === 0 ;
$isPath = $field === 'path' || $field === 'url' ;
$isNumChildren = $field === 'num_children' || $field === 'numChildren' ;
if ( $isName && $operator == '~=' ) {
// handle one or more space-separated full words match to 'name' field in any order
$s = '' ;
foreach ( explode ( ' ' , $value ) as $n => $word ) {
$word = $sanitizer -> pageName ( $word , Sanitizer :: toAscii );
if ( $database -> getRegexEngine () === 'ICU' ) {
// MySQL 8.0.4+ uses ICU regex engine where "\\b" is used for word boundary
$bindKey = $query -> bindValueGetKey ( " \\ b $word\\b " );
} else {
// this Henry Spencer regex engine syntax works only in MySQL 8.0.3 and prior
$bindKey = $query -> bindValueGetKey ( '[[:<:]]' . $word . '[[:>:]]' );
}
$s .= ( $s ? ' AND ' : '' ) . " $table . $field RLIKE $bindKey " ;
}
} else if ( $isName && $isPartialOperator ) {
// handle partial match to 'name' field
$value = $sanitizer -> pageName ( $value , Sanitizer :: toAscii );
if ( $operator == '^=' || $operator == '%^=' ) {
$value = " $value % " ;
} else if ( $operator == '$=' || $operator == '%$=' ) {
$value = " % $value " ;
} else {
$value = " % $value % " ;
}
$bindKey = $query -> bindValueGetKey ( $value );
$s = " $table . $field LIKE $bindKey " ;
} else if (( $isPath && $isPartialOperator ) || $isNumChildren ) {
// match some other property that we need to launch a separate find to determine the IDs
// used for partial match of path (used when original selector is parent.path%=...), parent.property, etc.
$tempSelector = trim ( $this -> getIncludeSelector ( $selectors ) . " , $field $operator " . $sanitizer -> selectorValue ( $value ), ',' );
$tempIDs = $this -> pages -> findIDs ( $tempSelector );
if ( count ( $tempIDs )) {
$s = " $table .id IN( " . implode ( ',' , $sanitizer -> intArray ( $tempIDs )) . ')' ;
} else {
$s = " $table .id=-1 " ; // force non-match
}
} else if ( ! $database -> isOperator ( $operator )) {
$s = '' ;
2023-03-10 19:41:40 +01:00
$this -> syntaxError ( " Operator ' $operator ' is not supported for ' $field '. " );
2022-03-08 15:55:41 +01:00
} else if ( $this -> isModifierField ( $field )) {
$s = '' ;
2023-03-10 19:41:40 +01:00
$this -> syntaxError ( " Modifier ' $field ' is not allowed here " );
2022-03-08 15:55:41 +01:00
} else if ( ! $this -> pagesColumnExists ( $field )) {
$s = '' ;
2023-03-10 19:41:40 +01:00
$this -> syntaxError ( " Field ' $field ' is not a known field, column or selector modifier " );
2022-03-08 15:55:41 +01:00
} else {
$not = false ;
if ( $isName ) $value = $sanitizer -> pageName ( $value , Sanitizer :: toAscii );
if ( $field === 'status' && ! ctype_digit ( " $value " )) {
// named status
$statuses = Page :: getStatuses ();
if ( ! isset ( $statuses [ $value ])) $this -> syntaxError ( " Unknown Page status: ' $value ' " );
$value = ( int ) $statuses [ $value ];
if ( $operator === '=' || $operator === '!=' ) $operator = '&' ; // bitwise
if ( $operator === '!=' ) $not = true ;
}
if ( $value === null ) {
$s = " $table . $field " . ( $not ? 'IS NOT NULL' : 'IS NULL' );
} else {
if ( ctype_digit ( " $value " ) && $field != 'name' ) $value = ( int ) $value ;
$bindKey = $query -> bindValueGetKey ( $value );
$s = " $table . $field " . $operator . $bindKey ;
if ( $not ) $s = " NOT ( $s ) " ;
}
if ( $field === 'status' && strpos ( $operator , '<' ) === 0 && $value >= Page :: statusHidden && count ( $options [ 'alwaysAllowIDs' ])) {
// support the 'alwaysAllowIDs' option for specific page IDs when requested but would
// not otherwise appear in the results due to hidden or unpublished status
$allowIDs = array ();
foreach ( $options [ 'alwaysAllowIDs' ] as $id ) $allowIDs [] = ( int ) $id ;
$s = " ( $s OR $table .id IN( " . implode ( ',' , $allowIDs ) . '))' ;
}
}
if ( $selector -> not ) $s = " NOT ( $s ) " ;
if ( $operator == '!=' || $selector -> not ) {
$sql .= $sql ? " AND $s " : " $s " ;
} else {
$sql .= $sql ? " OR $s " : " $s " ;
}
}
if ( $sql ) {
if ( $SQL ) {
$SQL .= " OR ( $sql ) " ;
} else {
$SQL .= " ( $sql ) " ;
}
}
}
if ( count ( $fields ) > 1 ) {
$SQL = " ( $SQL ) " ;
}
$query -> where ( $SQL );
//$this->nativeWheres[] = $SQL;
}
/**
* Get the include | status | check_access portions from given Selectors and return selector string for them
*
* If given $selectors lacks an include or check_access selector , then it will pull from the
* equivalent PageFinder setting if present in the original initiating selector .
*
* @ param Selectors | string $selectors
* @ return string
*
*/
protected function getIncludeSelector ( $selectors ) {
if ( ! $selectors instanceof Selectors ) $selectors = new Selectors ( $selectors );
$a = array ();
$include = $selectors -> getSelectorByField ( 'include' );
if ( empty ( $include ) && $this -> includeMode ) $include = " include= $this->includeMode " ;
if ( $include ) $a [] = $include ;
$status = $selectors -> getSelectorByField ( 'status' );
if ( ! empty ( $status )) $a [] = $status ;
$checkAccess = $selectors -> getSelectorByField ( 'check_access' );
if ( empty ( $checkAccess ) && $this -> checkAccess === false && $this -> includeMode !== 'all' ) $checkAccess = " check_access=0 " ;
if ( $checkAccess ) $a [] = $checkAccess ;
return implode ( ', ' , $a );
}
/**
* Make the query specific to all pages below a certain parent ( children , grandchildren , great grandchildren , etc . )
*
* @ param DatabaseQuerySelect $query
* @ param Selector $selector
*
*/
protected function getQueryHasParent ( DatabaseQuerySelect $query , $selector ) {
static $cnt = 0 ;
$wheres = array ();
$parent_ids = $selector -> value ;
if ( ! is_array ( $parent_ids )) $parent_ids = array ( $parent_ids );
foreach ( $parent_ids as $parent_id ) {
if ( ! ctype_digit ( " $parent_id " )) {
// parent_id is a path, convert a path to a parent
$parent = $this -> pages -> newNullPage ();
$path = $this -> sanitizer -> path ( $parent_id );
if ( $path ) $parent = $this -> pages -> get ( '/' . trim ( $path , '/' ) . '/' );
$parent_id = $parent -> id ;
if ( ! $parent_id ) {
$query -> where ( " 1>2 " ); // force the query to fail
return ;
}
}
$parent_id = ( int ) $parent_id ;
$cnt ++ ;
if ( $parent_id == 1 ) {
// homepage
if ( $selector -> operator == '!=' ) {
// homepage is only page that can match not having a has_parent of 1
$query -> where ( " pages.id=1 " );
} else {
// no different from not having a has_parent, so we ignore it
}
return ;
}
// the subquery performs faster than the old method (further below) on sites with tens of thousands of pages
if ( $selector -> operator == '!=' ) {
$in = 'NOT IN' ;
$op = '!=' ;
$andor = 'AND' ;
} else {
$in = 'IN' ;
$op = '=' ;
$andor = 'OR' ;
}
$wheres [] = " ( " .
" pages.parent_id $op $parent_id " .
" $andor pages.parent_id $in ( " .
" SELECT pages_id FROM pages_parents WHERE parents_id= $parent_id OR pages_id= $parent_id " .
" ) " .
" ) " ;
}
$andor = $selector -> operator == '!=' ? ' AND ' : ' OR ' ;
$query -> where ( '(' . implode ( $andor , $wheres ) . ')' );
/*
// OLD method kept for reference
$joinType = 'join' ;
$table = " pages_has_parent $cnt " ;
if ( $selector -> operator == '!=' ) {
$joinType = 'leftjoin' ;
$query -> where ( " $table .pages_id IS NULL " );
}
$query -> $joinType (
" pages_parents AS $table ON ( " .
" ( $table .pages_id=pages.id OR $table .pages_id=pages.parent_id) " .
" AND ( $table .parents_id= $parent_id OR $table .pages_id= $parent_id ) " .
" ) "
);
*/
}
/**
* Match a number of children count
*
* @ param DatabaseQuerySelect $query
* @ param Selector $selector
* @ return string
* @ throws WireException
*
*/
protected function getQueryNumChildren ( DatabaseQuerySelect $query , $selector ) {
if ( ! in_array ( $selector -> operator , array ( '=' , '<' , '>' , '<=' , '>=' , '!=' ))) {
$this -> syntaxError ( " Operator ' $selector->operator ' not allowed for 'num_children' selector. " );
}
$value = ( int ) $selector -> value ;
$this -> getQueryNumChildren ++ ;
$n = ( int ) $this -> getQueryNumChildren ;
$a = " pages_num_children $n " ;
$b = " num_children $n " ;
if ( ( in_array ( $selector -> operator , array ( '<' , '<=' , '!=' )) && $value ) ||
( in_array ( $selector -> operator , array ( '>' , '>=' , '!=' )) && $value < 0 ) ||
(( $selector -> operator == '=' || $selector -> operator == '>=' ) && ! $value )) {
// allow for zero values
$query -> select ( " COUNT( $a .id) AS $b " );
$query -> leftjoin ( " pages AS $a ON ( $a .parent_id=pages.id) " );
$query -> groupby ( " HAVING COUNT( $a .id) { $selector -> operator } $value " );
/* FOR REFERENCE
$query -> select ( " count(pages_num_children $n .id) AS num_children $n " );
$query -> leftjoin ( " pages AS pages_num_children $n ON (pages_num_children $n .parent_id=pages.id) " );
$query -> groupby ( " HAVING count(pages_num_children $n .id) { $selector -> operator } $value " );
*/
return $b ;
} else {
// non zero values
$query -> select ( " $a . $b AS $b " );
$query -> leftjoin (
" ( " .
" SELECT p $n .parent_id, COUNT(p $n .id) AS $b " .
" FROM pages AS p $n " .
" GROUP BY p $n .parent_id " .
" HAVING $b { $selector -> operator } $value " .
" ) $a ON $a .parent_id=pages.id " );
$where = " $a . $b { $selector -> operator } $value " ;
$query -> where ( $where );
/* FOR REFERENCE
$query -> select ( " pages_num_children $n .num_children $n AS num_children $n " );
$query -> leftjoin (
" ( " .
" SELECT p $n .parent_id, count(p $n .id) AS num_children $n " .
" FROM pages AS p $n " .
" GROUP BY p $n .parent_id " .
" HAVING num_children $n { $selector -> operator } $value " .
" ) pages_num_children $n ON pages_num_children $n .parent_id=pages.id " );
$query -> where ( " pages_num_children $n .num_children $n { $selector -> operator } $value " );
*/
return " $a . $b " ;
}
}
/**
* Arrange the order of field names where necessary
*
* @ param array $fields
* @ return array
*
*/
protected function arrangeFields ( array $fields ) {
$custom = array ();
$native = array ();
$singles = array ();
foreach ( $fields as $name ) {
if ( $this -> fields -> isNative ( $name )) {
$native [] = $name ;
} else {
$custom [] = $name ;
}
if ( in_array ( $name , $this -> singlesFields )) {
$singles [] = $name ;
}
}
if ( count ( $singles ) && count ( $fields ) > 1 ) {
// field in use that may no be combined with others
if ( $this -> config -> debug || $this -> config -> installed > 1549299319 ) {
// debug mode or anything installed after February 4th, 2019
$f = reset ( $singles );
$fs = implode ( '|' , $fields );
$this -> syntaxError ( " Field ' $f ' cannot OR with other fields in ' $fs ' " );
}
}
return array_merge ( $native , $custom );
}
/**
* Returns the total number of results returned from the last find () operation
*
* If the last find () included limit , then this returns the total without the limit
*
* @ return int
*
*/
public function getTotal () {
return $this -> total ;
}
/**
* Returns the limit placed upon the last find () operation , or 0 if no limit was specified
*
* @ return int
*
*/
public function getLimit () {
return $this -> limit === null ? 0 : $this -> limit ;
}
/**
* Returns the start placed upon the last find () operation
*
* @ return int
*
*/
public function getStart () {
return $this -> start === null ? 0 : $this -> start ;
}
/**
* Returns the parent ID , if it was part of the selector
*
* @ return int
*
*/
public function getParentID () {
return $this -> parent_id ;
}
/**
* Returns the templates ID , if it was part of the selector
*
2023-03-10 19:41:40 +01:00
* @ return int | null
2022-03-08 15:55:41 +01:00
*
*/
public function getTemplatesID () {
return $this -> templates_id ;
}
/**
* Return array of the options provided to PageFinder , as well as those determined at runtime
*
* @ return array
*
*/
public function getOptions () {
return $this -> lastOptions ;
}
/**
* Returns array of sortfields that should be applied to resulting PageArray after loaded
*
* See the `useSortsAfter` option which must be enabled to use this .
*
* #pw-internal
*
* @ return array
*
*/
public function getSortsAfter () {
return $this -> sortsAfter ;
}
/**
* Does the given field or fieldName resolve to a field that uses Page or PageArray values ?
*
* @ param string | Field $fieldName Field name or object
* @ param bool $literal Specify true to only allow types that literally use FieldtypePage :: getMatchQuery ()
* @ return Field | bool | string Returns Field object or boolean true ( children | parent ) if valid Page field , or boolean false if not
*
*/
protected function isPageField ( $fieldName , $literal = false ) {
$is = false ;
if ( $fieldName === 'parent' || $fieldName === 'children' ) {
return $fieldName ; // early exit
2023-03-10 19:41:40 +01:00
} else if ( $fieldName instanceof Field ) {
2022-03-08 15:55:41 +01:00
$field = $fieldName ;
} else if ( is_string ( $fieldName ) && strpos ( $fieldName , '.' )) {
// check if this is a multi-part field name
list ( $fieldName , $subfieldName ) = explode ( '.' , $fieldName , 2 );
if ( $subfieldName === 'id' ) {
// id property is fine and can be ignored
} else {
// some other property, see if it resolves to a literal Page field
$f = $this -> isPageField ( $subfieldName , true );
if ( $f ) {
// subfield resolves to literal Page field, so we can pass this one through
} else {
// some other property, that doesn't resolve to a Page field, we can early-exit now
return false ;
}
}
$field = $this -> fields -> get ( $fieldName );
} else {
$field = $this -> fields -> get ( $fieldName );
}
if ( $field ) {
if ( $field -> type instanceof FieldtypePage ) {
$is = true ;
} else if ( strpos ( $field -> type -> className (), 'FieldtypePageTable' ) !== false ) {
$is = true ;
} else if ( $this -> isRepeaterFieldtype ( $field -> type )) {
$is = $literal ? false : true ;
} else {
$test = $field -> type -> getBlankValue ( new NullPage (), $field );
2023-03-10 19:41:40 +01:00
if ( $test instanceof Page || $test instanceof PageArray ) {
2022-03-08 15:55:41 +01:00
$is = $literal ? false : true ;
}
}
}
if ( $is && $field ) $is = $field ;
return $is ;
}
/**
* Is the given Fieldtype for a repeater ?
*
* @ param Fieldtype $fieldtype
* @ return bool
*
*/
protected function isRepeaterFieldtype ( Fieldtype $fieldtype ) {
return wireInstanceOf ( $fieldtype , 'FieldtypeRepeater' );
}
/**
* Is given field name a modifier that does not directly refer to a field or column name ?
*
* @ param string $name
* @ return string Returns normalized modifier name if a modifier or boolean false if not
*
*/
protected function isModifierField ( $name ) {
$alternates = array (
'checkAccess' => 'check_access' ,
'getTotal' => 'get_total' ,
'hasParent' => 'has_parent' ,
);
$modifiers = array (
'include' ,
'_custom' ,
'limit' ,
'start' ,
'check_access' ,
'get_total' ,
'count' ,
'has_parent' ,
);
if ( isset ( $alternates [ $name ])) return $alternates [ $name ];
$key = array_search ( $name , $modifiers );
if ( $key === false ) return false ;
return $modifiers [ $key ];
}
/**
* Does the given column name exist in the 'pages' table ?
*
* @ param string $name
* @ return bool
*
*/
protected function pagesColumnExists ( $name ) {
if ( isset ( self :: $pagesColumns [ 'all' ][ $name ])) {
return self :: $pagesColumns [ 'all' ][ $name ];
}
$instanceID = $this -> wire () -> getProcessWireInstanceID ();
if ( ! isset ( self :: $pagesColumns [ $instanceID ])) {
self :: $pagesColumns [ $instanceID ] = array ();
if ( $this -> supportsLanguagePageNames ()) {
foreach ( $this -> languages as $language ) {
2023-03-10 19:41:40 +01:00
/** @var Language $language */
2022-03-08 15:55:41 +01:00
if ( $language -> isDefault ()) continue ;
self :: $pagesColumns [ $instanceID ][ " name $language->id " ] = true ;
self :: $pagesColumns [ $instanceID ][ " status $language->id " ] = true ;
}
}
}
if ( isset ( self :: $pagesColumns [ $instanceID ][ $name ])) {
return self :: $pagesColumns [ $instanceID ][ $name ];
}
self :: $pagesColumns [ $instanceID ][ $name ] = $this -> database -> columnExists ( 'pages' , $name );
return self :: $pagesColumns [ $instanceID ][ $name ];
}
/**
* Data and cache used by the pagesColumnExists method
*
* @ var array
*
*/
static private $pagesColumns = array (
// 'instance ID' => [ ... ]
'all' => array ( // available in all instances
'id' => true ,
'parent_id' => true ,
'templates_id' => true ,
'name' => true ,
'status' => true ,
'modified' => true ,
'modified_users_id' => true ,
'created' => true ,
'created_users_id' => true ,
'published' => true ,
'sort' => true ,
),
);
/**
* Are multi - language page names supported ?
*
* @ return bool
* @ since 3.0 . 165
*
*/
protected function supportsLanguagePageNames () {
if ( $this -> supportsLanguagePageNames === null ) {
2022-11-05 18:32:48 +01:00
$languages = $this -> languages ;
$this -> supportsLanguagePageNames = $languages && $languages -> hasPageNames ();
2022-03-08 15:55:41 +01:00
}
return $this -> supportsLanguagePageNames ;
}
/**
* Hook called when an unknown field is found in the selector
*
* By default , PW will throw a PageFinderSyntaxException but that behavior can be overridden by
* hooking this method and making it return true rather than false . It may also choose to
* map it to a Field by returning a Field object . If it returns integer 1 then it indicates the
* fieldName mapped to an API variable . If this method returns false , then it signals the getQuery ()
* method that it was unable to map it to anything and should be considered a fail .
*
* @ param string $fieldName
* @ param array $data Array of data containing the following in it :
* - `subfield` ( string ) : First subfield
* - `subfields` ( string ) : All subfields separated by period ( i . e . subfield . tertiaryfield )
* - `fields` ( array ) : Array of all other field names being processed in this selector .
* - `query` ( DatabaseQuerySelect ) : Database query select object
* - `selector` ( Selector ) : Selector that contains this field
* - `selectors` ( Selectors ) : All the selectors
* @ return bool | Field | int
* @ throws PageFinderSyntaxException
*
*/
protected function ___getQueryUnknownField ( $fieldName , array $data ) {
$_data = array (
'subfield ' => 'data' ,
'subfields' => 'data' ,
'fields' => array (),
'query' => null ,
'selector' => null ,
'selectors' => null ,
);
$data = array_merge ( $_data , $data );
2023-03-10 19:41:40 +01:00
$fields = $data [ 'fields' ]; /** @var array $fields */
$subfields = $data [ 'subfields' ]; /** @var string $subfields */
$selector = $data [ 'selector' ]; /** @var Selector $selector */
$query = $data [ 'query' ]; /** @var DatabaseQuerySelect $query */
$value = $this -> wire ( $fieldName ); /** @var Wire|null $value */
2022-03-08 15:55:41 +01:00
if ( $value ) {
// found an API var
if ( count ( $fields ) > 1 ) {
$this -> syntaxError ( " You may only match 1 API variable at a time " );
}
if ( is_object ( $value )) {
if ( $subfields == 'data' ) $subfields = 'id' ;
$selector -> field = $subfields ;
}
if ( ! $selector -> matches ( $value )) {
$query -> where ( " 1>2 " ); // force non match
}
return 1 ; // indicate no further fields need processing
}
// not an API var
if ( $this -> getQueryOwnerField ( $fieldName , $data )) return true ;
2023-03-10 19:41:40 +01:00
/** @var bool|int|Field $value Hooks can modify return value to be Field */
$value = false ;
return $value ;
2022-03-08 15:55:41 +01:00
}
/**
* Process an owner back reference selector for PageTable , Page and Repeater fields
*
* @ param string $fieldName Field name in " fieldName__owner " format
* @ param array $data Data as provided to getQueryUnknownField method
* @ return bool True if $fieldName was processed , false if not
* @ throws PageFinderSyntaxException
*
*/
protected function getQueryOwnerField ( $fieldName , array $data ) {
if ( substr ( $fieldName , - 7 ) !== '__owner' ) return false ;
2023-03-10 19:41:40 +01:00
$fields = $data [ 'fields' ]; /** @var array $fields */
$subfields = $data [ 'subfields' ]; /** @var string $subfields */
$selectors = $data [ 'selectors' ]; /** @var Selectors $selectors */
$selector = $data [ 'selector' ]; /** @var Selector $selector */
$query = $data [ 'query' ]; /** @var DatabaseQuerySelect $query */
2022-03-08 15:55:41 +01:00
if ( empty ( $subfields )) $this -> syntaxError ( " When using owner a subfield is required " );
list ( $ownerFieldName ,) = explode ( '__owner' , $fieldName );
$ownerField = $this -> fields -> get ( $ownerFieldName );
if ( ! $ownerField ) return false ;
$ownerTypes = array ( 'FieldtypeRepeater' , 'FieldtypePageTable' , 'FieldtypePage' );
if ( ! wireInstanceOf ( $ownerField -> type , $ownerTypes )) return false ;
if ( $selector -> get ( 'owner_processed' )) return true ;
static $ownerNum = 0 ;
$ownerNum ++ ;
// determine which templates are using $ownerFieldName
$templateIDs = array ();
foreach ( $this -> templates as $template ) {
2023-03-10 19:41:40 +01:00
/** @var Template $template */
2022-03-08 15:55:41 +01:00
if ( $template -> hasField ( $ownerFieldName )) {
$templateIDs [ $template -> id ] = $template -> id ;
}
}
if ( ! count ( $templateIDs )) $templateIDs [] = 0 ;
$templateIDs = implode ( '|' , $templateIDs );
// determine include=mode
$include = $selectors -> getSelectorByField ( 'include' );
$include = $include ? $include -> value : '' ;
if ( ! $include ) $include = $this -> includeMode ? $this -> includeMode : 'hidden' ;
$selectorString = " templates_id= $templateIDs , include= $include , get_total=0 " ;
if ( $include !== 'all' ) {
$checkAccess = $selectors -> getSelectorByField ( 'check_access' );
if ( $checkAccess && ctype_digit ( $checkAccess -> value )) {
$selectorString .= " , check_access= $checkAccess->value " ;
} else if ( $this -> checkAccess === false ) {
$selectorString .= " , check_access=0 " ;
}
}
/** @var Selectors $ownerSelectors Build selectors */
$ownerSelectors = $this -> wire ( new Selectors ( $selectorString ));
$ownerSelector = clone $selector ;
if ( count ( $fields ) > 1 ) {
// OR fields present
array_shift ( $fields );
$subfields = array ( $subfields );
foreach ( $fields as $name ) {
if ( strpos ( $name , " $fieldName . " ) === 0 ) {
list (, $name ) = explode ( '__owner.' , $name );
$subfields [] = $name ;
} else {
$this -> syntaxError (
" When owner is present, group of OR fields must all be ' $ownerFieldName .owner.subfield' format "
);
}
}
}
$ownerSelector -> field = $subfields ;
$ownerSelectors -> add ( $ownerSelector );
// use field.count>0 as an optimization?
$useCount = true ;
// find any other selectors referring to this same owner, bundle them in, and remove from source
foreach ( $selectors as $sel ) {
if ( strpos ( $sel -> field (), " $fieldName . " ) !== 0 ) continue ;
$sel -> set ( 'owner_processed' , true );
$op = $sel -> operator ();
if ( $useCount && ( $sel -> not || strpos ( $op , '!' ) !== false || strpos ( $op , '<' ) !== false )) {
$useCount = false ;
}
if ( $sel === $selector ) {
continue ; // skip main
}
$s = clone $sel ;
$s -> field = str_replace ( " $fieldName . " , '' , $sel -> field ());
$ownerSelectors -> add ( $s );
$selectors -> remove ( $sel );
}
if ( $useCount ) {
$sel = new SelectorGreaterThan ( " $ownerFieldName .count " , 0 );
$ownerSelectors -> add ( $sel );
}
/** @var PageFinder $finder */
$finder = $this -> wire ( new PageFinder ());
$ids = array ();
foreach ( $finder -> findIDs ( $ownerSelectors ) as $id ) {
$ids [] = ( int ) $id ;
}
if ( $this -> isRepeaterFieldtype ( $ownerField -> type )) {
// Repeater
$alias = " owner_parent $ownerNum " ;
$names = array ();
foreach ( $ids as $id ) {
$names [] = " 'for-page- $id ' " ;
}
$names = empty ( $names ) ? " 'force no match' " : implode ( " , " , $names );
$query -> join ( " pages AS $alias ON $alias .id=pages.parent_id AND $alias .name IN( $names ) " );
} else {
// Page or PageTable
$table = $ownerField -> getTable ();
$alias = " owner { $ownerNum } _ $table " ;
$ids = empty ( $ids ) ? " 0 " : implode ( ',' , $ids );
$query -> join ( " $table AS $alias ON $alias .data=pages.id AND $alias .pages_id IN( $ids ) " );
}
return true ;
}
/**
* Get data that should be populated back to any resulting PageArray’ s data () method
*
* @ param PageArray | null $pageArray Optionally populate given PageArray
* @ return array
*
*/
public function getPageArrayData ( PageArray $pageArray = null ) {
if ( $pageArray !== null && count ( $this -> pageArrayData )) {
$pageArray -> data ( $this -> pageArrayData );
}
return $this -> pageArrayData ;
}
/**
* Are any of the given field name ( s ) native to PW system ?
*
* This is primarily used to determine whether the getQueryNativeField () method should be called .
*
* @ param string | array | Selector $fieldNames Single field name , array of field names or pipe - separated string of field names
* @ return bool
*
*/
protected function hasNativeFieldName ( $fieldNames ) {
$fieldName = null ;
if ( is_object ( $fieldNames )) {
if ( $fieldNames instanceof Selector ) {
$fieldNames = $fieldNames -> fields ();
} else {
return false ;
}
}
if ( is_string ( $fieldNames )) {
if ( strpos ( $fieldNames , '|' )) {
$fieldNames = explode ( '|' , $fieldNames );
$fieldName = reset ( $fieldNames );
} else {
$fieldName = $fieldNames ;
$fieldNames = array ( $fieldName );
}
} else if ( is_array ( $fieldNames )) {
$fieldName = reset ( $fieldNames );
}
if ( $fieldName !== null ) {
if ( strpos ( $fieldName , '.' )) list ( $fieldName ,) = explode ( '.' , $fieldName , 2 );
if ( $this -> fields -> isNative ( $fieldName )) return true ;
}
if ( count ( $fieldNames )) {
$fieldsStr = ':' . implode ( ':' , $fieldNames ) . ':' ;
if ( strpos ( $fieldsStr , ':parent.' ) !== false ) return true ;
if ( strpos ( $fieldsStr , ':children.' ) !== false ) return true ;
if ( strpos ( $fieldsStr , ':child.' ) !== false ) return true ;
}
return false ;
}
/**
* Get the fully parsed / final selectors used in the last find () operation
*
* Should only be called after a find () or findIDs () operation , otherwise returns null .
*
* #pw-internal
*
* @ return Selectors | null
* @ since 3.0 . 146
*
*/
public function getSelectors () {
return $this -> finalSelectors ;
}
/**
* Throw a fatal syntax error
*
* @ param string $message
* @ throws PageFinderSyntaxException
*
*/
public function syntaxError ( $message ) {
throw new PageFinderSyntaxException ( $message );
}
}
/**
* Typehinting class for DatabaseQuerySelect object passed to Fieldtype :: getMatchQuery ()
*
* @ property Field $field Original field
* @ property string $group Original group of the field
* @ property Selector $selector Original Selector object
* @ property Selectors $selectors Original Selectors object
* @ property DatabaseQuerySelect $parentQuery Parent database query
* @ property PageFinder $pageFinder PageFinder instance that initiated the query
*/
abstract class PageFinderDatabaseQuerySelect extends DatabaseQuerySelect { }