Source of file SearchEngine.php
Size: 22,772 Bytes - Last Modified: 2021-12-24T06:48:34+00:00
/var/www/docs.ssmods.com/process/src/src/Model/SearchEngine.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780 | <?php namespace Fromholdio\Sherlock\Model; use Fromholdio\Sherlock\SearchAction; use Fromholdio\CommonAncestor\CommonAncestor; use Fromholdio\Sherlock\Extensions\SearchPageExtension; use Sheadawson\DependentDropdown\Forms\DependentDropdownField; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Core\ClassInfo; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldAddNewButton; use SilverStripe\Forms\GridField\GridFieldConfig_RecordViewer; use SilverStripe\Forms\GridField\GridFieldDeleteAction; use SilverStripe\Forms\GridField\GridFieldEditButton; use SilverStripe\Forms\GridField\GridFieldViewButton; use SilverStripe\Forms\ReadonlyField; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; use SilverStripe\Security\Permission; use SilverStripe\Security\PermissionProvider; use SilverStripe\Versioned\Versioned; class SearchEngine extends DataObject implements PermissionProvider { private static $table_name = 'SearchEngine'; private static $singular_name = 'Search Engine'; private static $plural_name = 'Search Engines'; private static $engine_entry_class; private static $engine_config; private static $engine_log_enabled = true; private static $db = [ 'Title' => 'Varchar', 'Description' => 'Varchar', 'SortMode' => 'Varchar(30)', 'SortDirection' => 'Varchar(10)', 'DirectSortMode' => 'Varchar(30)', 'DirectSortDirection' => 'Varchar(10)' ]; private static $has_one = [ 'DefaultSearchPage' => SiteTree::class ]; private static $has_many = [ 'Logs' => SearchLog::class ]; private static $summary_fields = [ 'Title', 'Description', 'EntriesCount' => 'Entries' ]; private static $cascade_deletes = [ 'getEntries', 'Logs' ]; protected static $search_page_classes = []; public static function register_search_page_class($class) { self::$search_page_classes[$class] = $class; } public function Link($searchPhrase = null, int $sourcePageID = null) { $searchPage = $this->getTargetSearchPage(); if (!$searchPage) { return null; } return $searchPage->SearchLink($searchPhrase, $sourcePageID); } public function AbsoluteLink($searchPhrase = null, int $sourcePageID = null) { $searchPage = $this->getTargetSearchPage(); if (!$searchPage) { return null; } return $searchPage->SearchAbsoluteLink($searchPhrase, $sourcePageID); } public function EntriesCount() { $entries = $this->getEntries(); if (!$entries) { return 0; } return $entries->count(); } public function getCMSFields() { $fields = parent::getCMSFields(); $fields->removeByName([ 'DefaultSearchPageID', 'SortMode', 'SortDirection', 'DirectSortMode', 'DirectSortDirection', 'Logs' ]); if (!$this->isInDB()) { return $fields; } $availableSearchPages = $this->getAvailableSearchPages(); if ($availableSearchPages) { $searchPageField = DropdownField::create( 'DefaultSearchPageID', $this->fieldLabel('DefaultSearchPage'), $availableSearchPages->map()->toArray() ); $searchPageField->setHasEmptyDefault(true); $searchPageField->setEmptyString('- Please select (optional) -'); $searchPageField->setDescription( 'Set a default search page that searches of this engine should be redirected to.' ); } else { $searchPageField = ReadonlyField::create( 'DefaultSearchPageInfo', $this->fieldLabel('DefaultSearchPage'), 'A search page must be created and attached to this engine first.' ); } $directSortFields = $this->getSortFields( 'DirectSortMode', 'DirectSortDirection', 'direct' ); if ($directSortFields) { $fields->addFieldsToTab('Root.Config', $directSortFields); } $sortFields = $this->getSortFields( 'SortMode', 'SortDirection', 'fields' ); if ($sortFields) { $fields->addFieldsToTab('Root.Config', $sortFields); } $fields->addFieldToTab('Root.Config', $searchPageField); if (Permission::check('VIEW_SEARCH_LOGS')) { $logsField = GridField::create( 'Logs', 'Search Logs', $this->Logs(), $logsConfig = GridFieldConfig_RecordViewer::create(null, 15, true, false) ); $logsConfig->removeComponentsByType([ GridFieldEditButton::class, GridFieldAddNewButton::class, GridFieldDeleteAction::class, GridFieldViewButton::class ]); $fields->addFieldToTab('Root.Main', $logsField); } return $fields; } public function onBeforeWrite() { parent::onBeforeWrite(); if ($this->DefaultSearchPageID) { $defaultSearchPage = $this->DefaultSearchPage(); if (!$defaultSearchPage->has_extension(SearchPageExtension::class)) { $this->DefaultSearchPageID = 0; } } } protected function addStarsToKeywords($keywords) { if (!trim($keywords)) { return ""; } // Add * to each keyword $splitWords = preg_split("/ +/", trim($keywords)); $newWords = []; for ($i = 0; $i < count($splitWords); $i++) { $word = $splitWords[$i]; if ($word[0] == '"') { while (++$i < count($splitWords)) { $subword = $splitWords[$i]; $word .= ' ' . $subword; if (substr($subword, -1) == '"') { break; } } } else { $word .= '*'; } $newWords[] = $word; } return implode(" ", $newWords); } public function search(string $phrase = null, int $searchPageID = null) { return SearchAction::create( $phrase, $this, $searchPageID, $this->isLogEnabled() ); } public function getDirectSearchResult($phrase = null) { if (!$phrase) { return null; } if (!is_string($phrase)) { throw new \InvalidArgumentException( 'Invalid $phrase passed to ' . ClassInfo::class_name($this) . '::getDirectSearchResults(). ' . 'Supplied ' . gettype($phrase) . ' but expected variable type null or string.' ); } $phrase = strtolower(trim($phrase)); $entries = $this->getEntries(); $directFilter = $this->getDirectSearchFilter($phrase); if ($directFilter) { $directResults = $entries->filterAny($directFilter); if ($directResults->count() > 0) { $directSortSQL = $this->getSortSQL('direct'); if ($directSortSQL) { $directResults->sort($directSortSQL); } $directMatchFirst = $directResults->first(); return $directMatchFirst->getRecord(); } } return null; } public function getSearchResults($phrase = null) { if (!$phrase) { return null; } if (!is_string($phrase)) { throw new \InvalidArgumentException( 'Invalid $phrase passed to ' . ClassInfo::class_name($this) . '::getSearchResults(). ' . 'Supplied ' . gettype($phrase) . ' but expected variable type null or string.' ); } $phrase = strtolower(trim($phrase)); $entries = $this->getEntries(); $searchFilter = $this->getSearchFilter($phrase); if ($searchFilter) { $searchResults = $entries->filterAny($searchFilter); if ($searchResults->count() > 0) { $sortSQL = $this->getSortSQL('fields'); if ($sortSQL) { $searchResults = $searchResults->sort($sortSQL); } return $this->getRecords($searchResults); } } return null; } public function getEntry($record) { $class = $this->getEntryClass(); $entryIsVersioned = $class::singleton()->hasExtension(Versioned::class); $recordIsVersioned = $record->hasExtension(Versioned::class); if ( ($entryIsVersioned && !$recordIsVersioned) || (!$entryIsVersioned && $recordIsVersioned) ) { throw new \LogicException( 'Your SearchEngine ' . static::class . ' has an entry class of ' . $class . ' and a record class of ' . get_class($record) . '. The entry class ' . ' and record class must either both be extended by Versioned or both not.' ); } $entries = $class::get(); $filter = $this->getEntryFilter($record); if ($filter && is_array($filter)) { $entries = $entries->filter($filter); }; return $entries->first(); } public function findOrMakeEntry($record) { $entry = $this->getEntry($record); if ($entry && $entry->exists()) { return $entry; } $class = $this->getEntryClass(); $entry = $class::create(); if ($class::singleton()->hasExtension(Versioned::class)) { if (Versioned::get_stage() === Versioned::LIVE) { $draftEntries = Versioned::get_by_stage( static::class, Versioned::DRAFT ); $filter = $this->getEntryFilter($record); if ($filter && is_array($filter)) { $draftEntries = $draftEntries->filter($filter); } $draftEntry = $draftEntries->first(); if ($draftEntry && $draftEntry->exists()) { $entry->ID = $draftEntry->ID; } } } return $entry; } public function addEntry($record) { if ($record->hasExtension(Versioned::class)) { if ($record->isPublished()) { $publishedRecord = Versioned::get_by_stage( get_class($record), Versioned::LIVE )->byID($record->ID); $this->writeEntry($publishedRecord); $this->publishEntry($publishedRecord); } } $this->writeEntry($record); } public function writeEntry($record) { $valid = $this->isValidRecord($record); if ($valid) { $entry = $this->loadRecord($record); $entry->write(); } else { $this->deleteEntry($record); } } public function publishEntry($record) { $class = $this->getEntryClass(); if (!$class::singleton()->hasExtension(Versioned::class)) { return; } $valid = $this->isValidRecord($record); if ($valid) { $entry = $this->loadRecord($record); $entry->copyVersionToStage( Versioned::DRAFT, Versioned::LIVE ); } else { $this->unpublishEntry($record); } } public function unpublishEntry($record) { $class = $this->getEntryClass(); if (!$class::singleton()->hasExtension(Versioned::class)) { return; } $entry = $this->getEntry($record); if ($entry && $entry->exists()) { $entry->doUnpublish(); } else { $publishedEntries = Versioned::get_by_stage( $class, Versioned::LIVE ); $filter = $this->getEntryFilter($record); if ($filter && is_array($filter)) { $publishedEntries = $publishedEntries->filter($filter); }; if ($publishedEntries->count() > 0) { $publishedEntries->first()->doUnpublish(); } } } public function deleteEntry($record) { $entry = $this->getEntry($record); if ($entry && $entry->exists()) { $entry->delete(); } } public function isValidRecord($record) { return ($record->isInDB()); } public function loadRecord($record, $entry = null) { if (!$entry) { $entry = $this->findOrMakeEntry($record); } return $entry; } public function getEntries() { $class = $this->getEntryClass(); return $class::get()->filter('SearchEngineID', $this->ID); } public function getRecords($entries = null) { if (!$entries) { $entries = $this->getEntries(); } $records = []; foreach ($entries as $entry) { $records[] = $entry->getRecord(); } return ArrayList::create($records); } public function getTargetSearchPage() { if ($this->DefaultSearchPageID) { return $this->DefaultSearchPage(); } $pages = $this->getAvailableSearchPages(); if ($pages) { return $pages->first(); } return null; } protected function isLogEnabled() { return ($this->config()->get('engine_log_enabled')); } protected function getEntryFilter($record) { return null; } protected function getDirectSearchFilter($phrase) { $config = $this->getEngineConfig('direct'); return $this->buildFilter($config, $phrase); } protected function getSearchFilter($phrase) { $config = $this->getEngineConfig('fields'); return $this->buildFilter($config, $phrase); } protected function buildFilter($config, $phrase) { if (!$config) { return null; } if (!is_array($config)) { throw new \UnexpectedValueException( 'Engine configs must be an array on ' . static::class ); } $filter = []; foreach ($config as $field) { $filter[$field] = $phrase; } if (count($filter) < 1) { $filter = null; } return $filter; } protected function getEngineConfig($key = null) { $config = $this->config()->get('engine_config'); if (!$config) { throw new \UnexpectedValueException( '$engine_config must be set on ' . static::class ); } if (!is_array($config)) { throw new \UnexpectedValueException( '$engine_config must be an array on ' . static::class ); } if ( !isset($config['direct']) && !isset($config['fields']) ) { throw new \UnexpectedValueException( 'You must set at least one of $fields or $direct ' . 'values in $engine_config on ' . static::class ); } if ($key) { if (isset($config[$key])) { $config = $config[$key]; } else { $config = null; } } $this->extend('updateEngineConfig', $config, $key); return $config; } public function getSortSQL($key) { if ($key === 'direct') { $mode = $this->DirectSortMode; $direction = $this->DirectSortDirection; } else if ($key === 'fields') { $mode = $this->SortMode; $direction = $this->SortDirection; } else { return null; } if (!$mode) { $mode = $this->getDefaultSortMode($key); } if ($mode) { $config = $this->getSortConfig($key, $mode); if (isset($config['sql'])) { $sql = $config['sql']; if ($direction) { $sql .= ' ' . $direction; } return $sql; } } return null; } public function getDefaultSortMode($key) { return $this->getSortConfig($key, 'default'); } public function getSortConfig($key, $mode) { $sortConfig = $this->getEngineSortConfig($key); if (!$sortConfig) { return null; } if (isset($sortConfig[$mode])) { return $sortConfig[$mode]; } return null; } public function getSortFields($sortFieldName, $directionFieldName, $key) { $sortConfig = $this->getEngineSortConfig($key); if (!$sortConfig) { return null; } $sortSource = []; $directionsMap = []; foreach ($sortConfig as $mode => $settings) { if (strtolower($mode) === 'default') { continue; } $sortSource[$mode] = $settings['name']; if (isset($settings['direction'])) { $direction = strtolower($settings['direction']); if ($direction === 'asc') { $directionsMap[$mode] = [ 'ASC' => 'Ascending' ]; } else if ($direction === 'desc') { $directionsMap[$mode] = [ 'DESC' => 'Descending' ]; } } } $directionsSource = function($mode) use ($directionsMap) { if (isset($directionsMap[$mode])) { return $directionsMap[$mode]; } return [ 'ASC' => 'Ascending', 'DESC' => 'Descending' ]; }; $sortField = DropdownField::create( $sortFieldName, $this->fieldLabel($sortFieldName), $sortSource ); $sortField->setHasEmptyDefault(false); if (isset($sortConfig['default'])) { $defaultValue = $sortConfig['default']; $sortField->setValue($defaultValue); } $directionsField = DependentDropdownField::create( $directionFieldName, $this->fieldLabel($directionFieldName), $directionsSource )->setDepends($sortField); return [$sortField, $directionsField]; } protected function getEngineSortConfig($key = null) { $config = $this->config()->get('engine_sort_config'); if (!$config) { return null; } if (!is_array($config)) { throw new \UnexpectedValueException( '$engine_sort_config must be an array on ' . static::class ); } if ( !isset($config['direct']) && !isset($config['fields']) ) { throw new \UnexpectedValueException( 'You must set at least one of $fields or $direct ' . 'values in $engine_sort_config on ' . static::class ); } if ($key) { if (isset($config[$key])) { $config = $config[$key]; } else { $config = null; } } $this->extend('updateEngineSortConfig', $config, $key); return $config; } protected function getAvailableSearchPages(bool $includeSubclasses = true) { if (!$this->isInDB()) { return null; } $classes = self::$search_page_classes; if (count($classes) < 1) { return null; } $pageIDs = []; $filter = ['SearchEngineID' => $this->ID]; foreach ($classes as $class) { if (!$includeSubclasses) { $filter['ClassName'] = $class; } $pages = $class::get()->filter($filter); $pageIDs = array_merge($pageIDs, $pages->columnUnique('ID')); unset($filter['ClassName']); } if (count($pageIDs) < 1) { return null; } $commonClass = CommonAncestor::get_closest($classes); $searchPages = $commonClass::get()->filter('ID', $pageIDs); if ($searchPages->count() > 0) { return $searchPages; } return null; } protected function getEntryClass() { $class = $this->config()->get('engine_entry_class'); if (!$class) { throw new \UnexpectedValueException( '$engine_entry_class must be set on ' . static::class ); } if (!ClassInfo::exists($class)) { throw new \UnexpectedValueException( 'A non-existent class "' . $class . '"has been set as $engine_entry_class on ' . static::class ); } if (!ClassInfo::classImplements($class, SearchEngineEntry::class)) { throw new \UnexpectedValueException( 'The class "' . $class . '"has been set as $engine_entry_class on ' . static::class . ' but does not implement ' . SearchEngineEntry::class ); } return $class; } public function isConfigured() { try { $this->getEntryClass(); $this->getEngineConfig(); return true; } catch (\Exception $exception) { return false; } } public function canCreate($member = null, $context = []) { return false; } public function canView($member = null) { return Permission::checkMember($member, 'MANAGE_SEARCH'); } public function canEdit($member = null) { return Permission::checkMember($member, 'MANAGE_SEARCH'); } public function canDelete($member = null) { return false; } public function providePermissions() { return [ 'MANAGE_SEARCH' => array( 'name' => 'Manage search', 'category' => 'Search engines', ) ]; } } |