2007-07-19 12:40:28 +02:00
< ? php
2008-02-25 03:10:37 +01:00
/**
2008-08-06 04:43:46 +02:00
* Standard basic search form which conducts a fulltext search on all { @ link SiteTree }
* objects .
2009-01-19 03:18:41 +01:00
*
* If multilingual content is enabled through the { @ link Translatable } extension ,
* only pages the currently set language on the holder for this searchform are found .
* The language is set through a hidden field in the form , which is prepoluated
ENHANCEMENT Adjusted SearchForm, Debug, ErrorPage, SiteTree to using locales instead of lang codes
API CHANGE Changed Translatable datamodel to use locales ("en_US") instead of lang values ("en).
API CHANGE Changed Translatable::$default_lang to $default_locale, Translatable::$reading_lang to $reading_locale
API CHANGE Using "locale" instead of "lang" in Translatable::choose_site_lang() to auto-detect language from cookies or GET parameters
API CHANGE Deprecated Translatable::is_default_lang(), set_default_lang(), get_default_lang(), current_lang(), set_reading_lang(), get_reading_lang(), get_by_lang(), get_one_by_lang()
API CHANGE Removed Translatable::get_original() - with the new "translation groups" concept there no longer is an original for a translation
BUGFIX Updated MigrateTranslatableTask to new Locale based datamodel
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@73468 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-03-20 09:47:06 +01:00
* with { @ link Translatable :: current_locale ()} when then form is constructed .
2008-08-06 04:43:46 +02:00
*
2008-09-26 06:35:29 +02:00
* @ see Use ModelController and SearchContext for a more generic search implementation based around DataObject
2008-02-25 03:10:37 +01:00
* @ package sapphire
* @ subpackage search
*/
2007-07-19 12:40:28 +02:00
class SearchForm extends Form {
2008-11-22 04:33:00 +01:00
/**
* @ var boolean $showInSearchTurnOn
* @ deprecated 2.3 SiteTree -> ShowInSearch should always be respected
*/
2007-07-19 12:40:28 +02:00
protected $showInSearchTurnOn ;
2008-11-22 04:33:00 +01:00
/**
2008-12-04 23:38:32 +01:00
* @ deprecated 2.3 Use { @ link $pageLength } .
*/
protected $numPerPage ;
/**
* @ var int $pageLength How many results are shown per page .
2008-11-22 04:33:00 +01:00
* Relies on pagination being implemented in the search results template .
*/
2008-12-04 23:38:32 +01:00
protected $pageLength = 10 ;
2008-11-22 04:33:00 +01:00
2009-02-02 00:49:53 +01:00
/**
* Classes to search
*/
protected $classesToSearch = array ( " SiteTree " , " File " );
2008-11-22 04:33:00 +01:00
/**
*
* @ param Controller $controller
* @ param string $name The name of the form ( used in URL addressing )
* @ param FieldSet $fields Optional , defaults to a single field named " Search " . Search logic needs to be customized
* if fields are added to the form .
* @ param FieldSet $actions Optional , defaults to a single field named " Go " .
* @ param boolean $showInSearchTurnOn DEPRECATED 2.3
*/
2007-07-19 12:40:28 +02:00
function __construct ( $controller , $name , $fields = null , $actions = null , $showInSearchTurnOn = true ) {
$this -> showInSearchTurnOn = $showInSearchTurnOn ;
2008-11-11 22:16:51 +01:00
2007-07-19 12:40:28 +02:00
if ( ! $fields ) {
2008-11-11 22:16:51 +01:00
$fields = new FieldSet (
new TextField ( 'Search' , _t ( 'SearchForm.SEARCH' , 'Search' )
));
2007-07-19 12:40:28 +02:00
}
2008-11-11 22:16:51 +01:00
2009-04-01 18:35:32 +02:00
if ( singleton ( 'SiteTree' ) -> hasExtension ( 'Translatable' )) {
$fields -> push ( new HiddenField ( 'locale' , 'locale' , Translatable :: current_locale ()));
2009-01-19 03:18:41 +01:00
}
2007-07-19 12:40:28 +02:00
if ( ! $actions ) {
$actions = new FieldSet (
2008-02-25 03:10:37 +01:00
new FormAction ( " getResults " , _t ( 'SearchForm.GO' , 'Go' ))
2007-07-19 12:40:28 +02:00
);
}
parent :: __construct ( $controller , $name , $fields , $actions );
2007-10-28 22:44:38 +01:00
2008-12-04 23:38:32 +01:00
$this -> setFormMethod ( 'get' );
2007-10-28 22:44:38 +01:00
$this -> disableSecurityToken ();
2007-07-19 12:40:28 +02:00
}
2008-12-04 23:38:32 +01:00
public function forTemplate () {
2008-11-11 22:16:51 +01:00
return $this -> renderWith ( array (
'SearchForm' ,
'Form'
));
2007-07-19 12:40:28 +02:00
}
2009-02-02 00:49:53 +01:00
/**
* Set the classes to search .
* Currently you can only choose from " SiteTree " and " File " , but a future version might improve this .
*/
function classesToSearch ( $classes ) {
$illegalClasses = array_diff ( $classes , array ( 'SiteTree' , 'File' ));
if ( $illegalClasses ) {
user_error ( " SearchForm::classesToSearch() passed illegal classes ' " . implode ( " ', ' " , $illegalClasses ) . " '. At this stage, only File and SiteTree are allowed " , E_USER_WARNING );
}
$legalClasses = array_intersect ( $classes , array ( 'SiteTree' , 'File' ));
$this -> classesToSearch = $legalClasses ;
}
2007-07-19 12:40:28 +02:00
/**
* Return dataObjectSet of the results using $_REQUEST to get info from form .
2008-11-22 04:33:00 +01:00
* Wraps around { @ link searchEngine ()} .
*
2008-12-04 23:38:32 +01:00
* @ param int $pageLength DEPRECATED 2.3 Use SearchForm -> pageLength
2008-11-22 04:33:00 +01:00
* @ param array $data Request data as an associative array . Should contain at least a key 'Search' with all searched keywords .
* @ return DataObjectSet
2007-07-19 12:40:28 +02:00
*/
2008-12-04 23:38:32 +01:00
public function getResults ( $pageLength = null , $data = null ){
2008-11-22 04:33:00 +01:00
// legacy usage: $data was defaulting to $_REQUEST, parameter not passed in doc.silverstripe.com tutorials
if ( ! isset ( $data )) $data = $_REQUEST ;
2009-01-19 03:18:41 +01:00
// set language (if present)
2009-04-01 18:35:32 +02:00
if ( singleton ( 'SiteTree' ) -> hasExtension ( 'Translatable' ) && isset ( $data [ 'locale' ])) {
Translatable :: set_reading_locale ( $data [ 'locale' ]);
2009-01-19 03:18:41 +01:00
}
2008-11-22 04:33:00 +01:00
$keywords = $data [ 'Search' ];
2007-07-19 12:40:28 +02:00
$andProcessor = create_function ( '$matches' , '
return " + " . $matches [ 2 ] . " + " . $matches [ 4 ] . " " ;
' );
$notProcessor = create_function ( '$matches' , '
return " - " . $matches [ 3 ];
' );
$keywords = preg_replace_callback ( '/()("[^()"]+")( and )("[^"()]+")()/i' , $andProcessor , $keywords );
$keywords = preg_replace_callback ( '/(^| )([^() ]+)( and )([^ ()]+)( |$)/i' , $andProcessor , $keywords );
$keywords = preg_replace_callback ( '/(^| )(not )("[^"()]+")/i' , $notProcessor , $keywords );
$keywords = preg_replace_callback ( '/(^| )(not )([^() ]+)( |$)/i' , $notProcessor , $keywords );
$keywords = $this -> addStarsToKeywords ( $keywords );
if ( strpos ( $keywords , '"' ) !== false || strpos ( $keywords , '+' ) !== false || strpos ( $keywords , '-' ) !== false || strpos ( $keywords , '*' ) !== false ) {
2008-12-04 23:38:32 +01:00
$results = $this -> searchEngine ( $keywords , $pageLength , " Relevance DESC " , " " , true );
2007-07-19 12:40:28 +02:00
} else {
2008-12-04 23:38:32 +01:00
$results = $this -> searchEngine ( $keywords , $pageLength );
2008-11-22 04:33:00 +01:00
}
// filter by permission
if ( $results ) foreach ( $results as $result ) {
if ( ! $result -> canView ()) $results -> remove ( $result );
}
return $results ;
2007-07-19 12:40:28 +02:00
}
2008-11-22 04:33:00 +01:00
protected function addStarsToKeywords ( $keywords ) {
2007-07-19 12:40:28 +02:00
if ( ! trim ( $keywords )) return " " ;
// Add * to each keyword
$splitWords = split ( " + " , trim ( $keywords ));
while ( list ( $i , $word ) = each ( $splitWords )) {
if ( $word [ 0 ] == '"' ) {
while ( list ( $i , $subword ) = each ( $splitWords )) {
$word .= ' ' . $subword ;
if ( substr ( $subword , - 1 ) == '"' ) break ;
}
} else {
$word .= '*' ;
}
$newWords [] = $word ;
}
return implode ( " " , $newWords );
}
2008-11-22 04:33:00 +01:00
2007-07-19 12:40:28 +02:00
/**
* The core search engine , used by this class and its subclasses to do fun stuff .
* Searches both SiteTree and File .
2008-11-22 04:33:00 +01:00
*
* @ param string $keywords Keywords as a string .
2007-07-19 12:40:28 +02:00
*/
2008-12-04 23:38:32 +01:00
public function searchEngine ( $keywords , $pageLength = null , $sortBy = " Relevance DESC " , $extraFilter = " " , $booleanSearch = false , $alternativeFileFilter = " " , $invertedMatch = false ) {
if ( ! $pageLength ) $pageLength = $this -> pageLength ;
2007-07-19 12:40:28 +02:00
$fileFilter = '' ;
2009-01-19 03:49:42 +01:00
$keywords = Convert :: raw2sql ( $keywords );
$htmlEntityKeywords = htmlentities ( $keywords );
2009-02-02 00:49:53 +01:00
$extraFilters = array ( 'SiteTree' => '' , 'File' => '' );
2007-07-19 12:40:28 +02:00
if ( $booleanSearch ) $boolean = " IN BOOLEAN MODE " ;
2009-01-19 03:49:42 +01:00
2007-07-19 12:40:28 +02:00
if ( $extraFilter ) {
2009-02-02 00:49:53 +01:00
$extraFilters [ 'SiteTree' ] = " AND $extraFilter " ;
if ( $alternativeFileFilter ) $extraFilters [ 'File' ] = " AND $alternativeFileFilter " ;
else $extraFilters [ 'File' ] = $extraFilters [ 'SiteTree' ];
2007-07-19 12:40:28 +02:00
}
2009-02-02 00:49:53 +01:00
if ( $this -> showInSearchTurnOn ) $extraFilters [ 'SiteTree' ] .= " AND showInSearch <> 0 " ;
2007-07-19 12:40:28 +02:00
2007-10-17 05:22:37 +02:00
$start = isset ( $_GET [ 'start' ]) ? ( int ) $_GET [ 'start' ] : 0 ;
2008-12-04 23:38:32 +01:00
$limit = $start . " , " . ( int ) $pageLength ;
2007-07-19 12:40:28 +02:00
$notMatch = $invertedMatch ? " NOT " : " " ;
if ( $keywords ) {
2009-02-04 02:36:39 +01:00
$match [ 'SiteTree' ] = "
MATCH ( Title , MenuTitle , Content , MetaTitle , MetaDescription , MetaKeywords ) AGAINST ( '$keywords' $boolean )
+ MATCH ( Content ) AGAINST ( '$htmlEntityKeywords' $boolean )
" ;
2009-02-02 00:49:53 +01:00
$match [ 'File' ] = " MATCH (Filename, Title, Content) AGAINST (' $keywords ' $boolean ) AND ClassName = 'File' " ;
2007-07-19 12:40:28 +02:00
// We make the relevance search by converting a boolean mode search into a normal one
$relevanceKeywords = str_replace ( array ( '*' , '+' , '-' ), '' , $keywords );
2009-01-19 03:49:42 +01:00
$htmlEntityRelevanceKeywords = str_replace ( array ( '*' , '+' , '-' ), '' , $htmlEntityKeywords );
2009-02-02 00:49:53 +01:00
$relevance [ 'SiteTree' ] = " MATCH (Title) AGAINST (' $relevanceKeywords ') + MATCH(Content) AGAINST (' $htmlEntityRelevanceKeywords ') + MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST (' $relevanceKeywords ') " ;
$relevance [ 'File' ] = " MATCH (Filename, Title, Content) AGAINST (' $relevanceKeywords ') " ;
2007-07-19 12:40:28 +02:00
} else {
2009-02-02 00:49:53 +01:00
$relevance [ 'SiteTree' ] = $relevance [ 'File' ] = 1 ;
$match [ 'SiteTree' ] = $match [ 'File' ] = " 1 = 1 " ;
2007-07-19 12:40:28 +02:00
}
2009-02-02 00:49:53 +01:00
// Generate initial queries and base table names
$baseClasses = array ( 'SiteTree' => '' , 'File' => '' );
foreach ( $this -> classesToSearch as $class ) {
$queries [ $class ] = singleton ( $class ) -> extendedSQL ( $notMatch . $match [ $class ] . $extraFilters [ $class ], " " );
$baseClasses [ $class ] = reset ( $queries [ $class ] -> from );
}
// Make column selection lists
$select = array (
'SiteTree' => array ( " ClassName " , " $baseClasses[SiteTree] .ID " , " ParentID " , " Title " , " URLSegment " , " Content " , " LastEdited " , " Created " , " _utf8'' AS Filename " , " _utf8'' AS Name " , " $relevance[SiteTree] AS Relevance " , " CanViewType " ),
'File' => array ( " ClassName " , " $baseClasses[File] .ID " , " _utf8'' AS ParentID " , " Title " , " _utf8'' AS URLSegment " , " Content " , " LastEdited " , " Created " , " Filename " , " Name " , " $relevance[File] AS Relevance " , " NULL AS CanViewType " ),
);
// Process queries
foreach ( $this -> classesToSearch as $class ) {
// There's no need to do all that joining
$queries [ $class ] -> from = array ( str_replace ( '`' , '' , $baseClasses [ $class ]) => $baseClasses [ $class ]);
$queries [ $class ] -> select = $select [ $class ];
$queries [ $class ] -> orderby = null ;
}
2007-07-19 12:40:28 +02:00
2009-02-02 00:49:53 +01:00
// Combine queries
$querySQLs = array ();
$totalCount = 0 ;
foreach ( $queries as $query ) {
$querySQLs [] = $query -> sql ();
$totalCount += $query -> unlimitedRowCount ();
}
$fullQuery = implode ( " UNION " , $querySQLs ) . " ORDER BY $sortBy LIMIT $limit " ;
2007-07-19 12:40:28 +02:00
2009-02-02 00:49:53 +01:00
// Get records
2007-07-19 12:40:28 +02:00
$records = DB :: query ( $fullQuery );
foreach ( $records as $record )
$objects [] = new $record [ 'ClassName' ]( $record );
2007-10-17 05:26:25 +02:00
if ( isset ( $objects )) $doSet = new DataObjectSet ( $objects );
else $doSet = new DataObjectSet ();
2007-07-19 12:40:28 +02:00
2008-12-04 23:38:32 +01:00
$doSet -> setPageLimits ( $start , $pageLength , $totalCount );
2007-07-19 12:40:28 +02:00
return $doSet ;
}
2008-11-11 22:16:51 +01:00
2008-11-22 04:33:00 +01:00
/**
* Get the search query for display in a " You searched for ... " sentence .
*
* @ param array $data
* @ return string
*/
public function getSearchQuery ( $data = null ) {
// legacy usage: $data was defaulting to $_REQUEST, parameter not passed in doc.silverstripe.com tutorials
if ( ! isset ( $data )) $data = $_REQUEST ;
return Convert :: raw2xml ( $data [ 'Search' ]);
2007-07-19 12:40:28 +02:00
}
2008-12-04 23:38:32 +01:00
/**
* Set the maximum number of records shown on each page .
*
* @ param int $length
*/
public function setPageLength ( $length ) {
$this -> pageLength = $length ;
}
/**
* @ return int
*/
public function getPageLength () {
// legacy handling for deprecated $numPerPage
return ( isset ( $this -> numPerPage )) ? $this -> numPerPage : $this -> pageLength ;
}
2007-07-19 12:40:28 +02:00
}
2009-02-02 00:49:53 +01:00
?>