/**
  * The core search engine, used by this class and its subclasses to do fun stuff.
  * Searches both SiteTree and File.
  *
  * @param array $classesToSearch
  * @param string $keywords Keywords as a string.
  * @param int $start
  * @param int $pageLength
  * @param string $sortBy
  * @param string $extraFilter
  * @param bool $booleanSearch
  * @param string $alternativeFileFilter
  * @param bool $invertedMatch
  * @return \SilverStripe\ORM\PaginatedList
  * @throws Exception
  */
 public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false)
 {
     $pageClass = 'SilverStripe\\CMS\\Model\\SiteTree';
     $fileClass = 'SilverStripe\\Assets\\File';
     $pageTable = DataObject::getSchema()->tableName($pageClass);
     $fileTable = DataObject::getSchema()->tableName($fileClass);
     if (!class_exists($pageClass)) {
         throw new Exception('MySQLDatabase->searchEngine() requires "SiteTree" class');
     }
     if (!class_exists($fileClass)) {
         throw new Exception('MySQLDatabase->searchEngine() requires "File" class');
     }
     $keywords = $this->escapeString($keywords);
     $htmlEntityKeywords = htmlentities($keywords, ENT_NOQUOTES, 'UTF-8');
     $extraFilters = array($pageClass => '', $fileClass => '');
     $boolean = '';
     if ($booleanSearch) {
         $boolean = "IN BOOLEAN MODE";
     }
     if ($extraFilter) {
         $extraFilters[$pageClass] = " AND {$extraFilter}";
         if ($alternativeFileFilter) {
             $extraFilters[$fileClass] = " AND {$alternativeFileFilter}";
         } else {
             $extraFilters[$fileClass] = $extraFilters[$pageClass];
         }
     }
     // Always ensure that only pages with ShowInSearch = 1 can be searched
     $extraFilters[$pageClass] .= " AND ShowInSearch <> 0";
     // File.ShowInSearch was added later, keep the database driver backwards compatible
     // by checking for its existence first
     $fields = $this->getSchemaManager()->fieldList($fileTable);
     if (array_key_exists('ShowInSearch', $fields)) {
         $extraFilters[$fileClass] .= " AND ShowInSearch <> 0";
     }
     $limit = $start . ", " . (int) $pageLength;
     $notMatch = $invertedMatch ? "NOT " : "";
     if ($keywords) {
         $match[$pageClass] = "\n\t\t\t\tMATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('{$keywords}' {$boolean})\n\t\t\t\t+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('{$htmlEntityKeywords}' {$boolean})\n\t\t\t";
         $fileClassSQL = Convert::raw2sql($fileClass);
         $match[$fileClass] = "MATCH (Name, Title) AGAINST ('{$keywords}' {$boolean}) AND ClassName = '{$fileClassSQL}'";
         // We make the relevance search by converting a boolean mode search into a normal one
         $relevanceKeywords = str_replace(array('*', '+', '-'), '', $keywords);
         $htmlEntityRelevanceKeywords = str_replace(array('*', '+', '-'), '', $htmlEntityKeywords);
         $relevance[$pageClass] = "MATCH (Title, MenuTitle, Content, MetaDescription) " . "AGAINST ('{$relevanceKeywords}') " . "+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('{$htmlEntityRelevanceKeywords}')";
         $relevance[$fileClass] = "MATCH (Name, Title) AGAINST ('{$relevanceKeywords}')";
     } else {
         $relevance[$pageClass] = $relevance[$fileClass] = 1;
         $match[$pageClass] = $match[$fileClass] = "1 = 1";
     }
     // Generate initial DataLists and base table names
     $lists = array();
     $baseClasses = array($pageClass => '', $fileClass => '');
     foreach ($classesToSearch as $class) {
         $lists[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class]);
         $baseClasses[$class] = '"' . $class . '"';
     }
     $charset = static::config()->get('charset');
     // Make column selection lists
     $select = array($pageClass => array("ClassName", "{$pageTable}.\"ID\"", "ParentID", "Title", "MenuTitle", "URLSegment", "Content", "LastEdited", "Created", "Name" => "_{$charset}''", "Relevance" => $relevance[$pageClass], "CanViewType"), $fileClass => array("ClassName", "{$fileTable}.\"ID\"", "ParentID", "Title", "MenuTitle" => "_{$charset}''", "URLSegment" => "_{$charset}''", "Content" => "_{$charset}''", "LastEdited", "Created", "Name", "Relevance" => $relevance[$fileClass], "CanViewType" => "NULL"));
     // Process and combine queries
     $querySQLs = array();
     $queryParameters = array();
     $totalCount = 0;
     foreach ($lists as $class => $list) {
         $table = DataObject::getSchema()->tableName($class);
         /** @var SQLSelect $query */
         $query = $list->dataQuery()->query();
         // There's no need to do all that joining
         $query->setFrom($table);
         $query->setSelect($select[$class]);
         $query->setOrderBy(array());
         $querySQLs[] = $query->sql($parameters);
         $queryParameters = array_merge($queryParameters, $parameters);
         $totalCount += $query->unlimitedRowCount();
     }
     $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY {$sortBy} LIMIT {$limit}";
     // Get records
     $records = $this->preparedQuery($fullQuery, $queryParameters);
     $objects = array();
     foreach ($records as $record) {
         $objects[] = new $record['ClassName']($record);
     }
     $list = new PaginatedList(new ArrayList($objects));
     $list->setPageStart($start);
     $list->setPageLength($pageLength);
     $list->setTotalItems($totalCount);
     // The list has already been limited by the query above
     $list->setLimitItems(false);
     return $list;
 }
 public function testPrevLink()
 {
     $list = new PaginatedList(new ArrayList());
     $list->setTotalItems(50);
     $this->assertNull($list->PrevLink());
     $list->setCurrentPage(2);
     $this->assertContains('start=0', $list->PrevLink());
     $list->setCurrentPage(3);
     $this->assertContains('start=10', $list->PrevLink());
     $list->setCurrentPage(5);
     $this->assertContains('start=30', $list->PrevLink());
     // Disable paging
     $list->setPageLength(0);
     $this->assertNull($list->PrevLink());
 }