/** * 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()); }