diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 611e042..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -github: ['Firesphere'] -custom: ['https://www.paypal.com/donate?hosted_button_id=C7VA5RQB6TWB6', 'https://ko-fi.com/B0B11GKLY'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea7..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index da0e903..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,20 +0,0 @@ -# Hi there and thank you for taking the time and effort to make a Pull Request - -No, seriously, we are delighted you made time and effort to help us, even in the smallest way! - - -# Please check the following - -- [ ] Have you opened an issue to discuss the feature and agree its general design? -- [ ] Do you have a use case and, ideally, an example program using the feature? -- [ ] Do you have tests covering 90%+ of the feature code (and, of course passing) -- [ ] Have you written complete and accurate doc comments? -- [ ] Have you updated the README or docs where needed? -- [ ] Code is readable -- [ ] Code is tested -- [ ] I've read the Code of Conduct (Or, at least, I do believe I agree with it) -- [ ] Frontend features don't look completely uncared for -- [ ] Code is PSR-2 compliant -- [ ] I'm fairly sure this is useful -- [ ] I'm not just making this PR for HacktoberFest -- [ ] You rock. Thanks a lot. diff --git a/Solr/9/templates/types.ss b/Solr/9/templates/types.ss index bdfbb5e..9ba904c 100644 --- a/Solr/9/templates/types.ss +++ b/Solr/9/templates/types.ss @@ -119,28 +119,8 @@ NOTE: autoGeneratePhraseQueries="true" tends to not work well for non whitespace delimited languages. --> - - - - - - - - - - + @@ -158,16 +138,23 @@ - + + + - + @@ -175,7 +162,7 @@ - + @@ -185,7 +172,7 @@ - + @@ -199,7 +186,7 @@ - + @@ -266,7 +253,7 @@ - + @@ -278,7 +265,7 @@ - + 1 Although not required, it's highly recommended diff --git a/readme.md b/readme.md index 006bb94..ebbdd23 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ +**BASED ON [firesphere/solr-search](https://codeberg.org/Firesphere/silverstripe-solr)** + [![Maintainability](https://api.codeclimate.com/v1/badges/55c8967ef25e37182e3d/maintainability)](https://codeclimate.com/github/Firesphere/silverstripe-solr-search/maintainability) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Firesphere/silverstripe-solr-search/badges/quality-score.png?b=primary)](https://scrutinizer-ci.com/g/Firesphere/silverstripe-solr-search/?branch=primary) [![Code Coverage](https://scrutinizer-ci.com/g/Firesphere/silverstripe-solr-search/badges/coverage.png?b=primary)](https://scrutinizer-ci.com/g/Firesphere/silverstripe-solr-search/?branch=primary) diff --git a/src/Admins/SearchAdmin.php b/src/Admins/SearchAdmin.php index ac95779..49a0e03 100644 --- a/src/Admins/SearchAdmin.php +++ b/src/Admins/SearchAdmin.php @@ -57,4 +57,12 @@ public function init() Requirements::css('firesphere/solr-search:client/dist/main.css'); } + + protected function getManagedModelTabs() + { + $tabs = parent::getManagedModelTabs(); + return $tabs->filterByCallback(function ($tab) { + return singleton($tab->ClassName)->canView(); + }); + } } diff --git a/src/Extensions/DataObjectExtension.php b/src/Extensions/DataObjectExtension.php index 8f2c541..dfb2c5e 100644 --- a/src/Extensions/DataObjectExtension.php +++ b/src/Extensions/DataObjectExtension.php @@ -14,7 +14,6 @@ use Firesphere\SolrSearch\Helpers\SolrLogger; use Firesphere\SolrSearch\Models\DirtyClass; use Firesphere\SolrSearch\Services\SolrCoreService; -use Firesphere\SolrSearch\Tests\DataObjectExtensionTest; use Psr\Log\LoggerInterface; use Psr\SimpleCache\InvalidArgumentException; use ReflectionException; @@ -94,7 +93,7 @@ protected function shouldPush() */ protected function pushToSolr(DataObject $owner) { - $service = new SolrCoreService(); + $service = Injector::inst()->get(SolrCoreService::class); if (!$service->isValidClass($owner->ClassName)) { return; } @@ -102,7 +101,7 @@ protected function pushToSolr(DataObject $owner) /** @var DataObject $owner */ $record = $this->getDirtyClass(SolrCoreService::UPDATE_TYPE); - $ids = json_decode($record->IDs, 1) ?: []; + $ids = json_decode($record->IDs ?? '[]', 1) ?: []; $mode = Versioned::get_reading_mode(); try { Versioned::set_reading_mode(Versioned::LIVE); @@ -129,19 +128,21 @@ protected function pushToSolr(DataObject $owner) * Find or create a new DirtyClass for recording dirty IDs * * @param string $type + * @param string $class optional class to use. If not set uses current owner class * @return DirtyClass * @throws ValidationException */ - protected function getDirtyClass(string $type) + protected function getDirtyClass(string $type, string $class = null) { + $params = [ + 'Class' => ($class ?? $this->owner->ClassName), + 'Type' => $type + ]; // Get the DirtyClass object for this item /** @var null|DirtyClass $record */ - $record = DirtyClass::get()->filter(['Class' => $this->owner->ClassName, 'Type' => $type])->first(); + $record = DirtyClass::get()->filter($params)->first(); if (!$record || !$record->exists()) { - $record = DirtyClass::create([ - 'Class' => $this->owner->ClassName, - 'Type' => $type, - ]); + $record = DirtyClass::create($params); $record->write(); } @@ -213,6 +214,53 @@ public function doReindex() $this->pushToSolr($this->owner); } + /** + * Attempt to remove the item from Solr + * + * @throws ValidationException + * @throws HTTPException + */ + private function removeItem(DataObject $item = null) + { + /** @var DataObject $owner */ + $owner = $item ?? $this->owner; + /** @var DirtyClass $record */ + $record = $this->getDirtyClass(SolrCoreService::DELETE_TYPE, $owner->ClassName); + $record->IDs = $record->IDs ?? '[]'; // If the record is new, or the IDs list is null, default + $ids = json_decode($record->IDs, 1); + + try { + Injector::inst()->get(SolrCoreService::class) + ->updateItems(ArrayList::create([$owner]), SolrCoreService::DELETE_TYPE); + // If successful, remove it from the array + // Added bonus, array_flip removes duplicates + $this->clearIDs($owner, $ids, $record); + // @codeCoverageIgnoreStart + } catch (Exception $error) { + $this->registerException($ids, $record, $error); + } + // @codeCoverageIgnoreEnd + } + + /** + * Clear old page type from Solr before publishing if required + * + * @throws ValidationException + * @throws HTTPException + * @throws ReflectionException + * @throws InvalidArgumentException + */ + public function onBeforePublish() + { + // Check for changed classname and delete old record before pushing new if required. + if ($this->owner instanceof SiteTree) { + $lastPublished = Versioned::get_by_stage(SiteTree::class, Versioned::LIVE)->byID($this->owner->ID); + if ($lastPublished && $this->owner->ClassName !== $lastPublished->ClassName) { + $this->removeItem($lastPublished); + } + } + } + /** * Push the item to Solr after publishing * @@ -231,31 +279,14 @@ public function onAfterPublish() } /** - * Attempt to remove the item from Solr + * Attempt to remove the item from Solr when deleted * * @throws ValidationException * @throws HTTPException */ public function onAfterDelete(): void { - /** @var DataObject $owner */ - $owner = $this->owner; - /** @var DirtyClass $record */ - $record = $this->getDirtyClass(SolrCoreService::DELETE_TYPE); - $record->IDs = $record->IDs ?? '[]'; // If the record is new, or the IDs list is null, default - $ids = json_decode($record->IDs, 1); - - try { - (new SolrCoreService()) - ->updateItems(ArrayList::create([$owner]), SolrCoreService::DELETE_TYPE); - // If successful, remove it from the array - // Added bonus, array_flip removes duplicates - $this->clearIDs($owner, $ids, $record); - // @codeCoverageIgnoreStart - } catch (Exception $error) { - $this->registerException($ids, $record, $error); - } - // @codeCoverageIgnoreEnd + $this->removeItem(); } /** diff --git a/src/Factories/DocumentFactory.php b/src/Factories/DocumentFactory.php index 7f34320..e72f281 100644 --- a/src/Factories/DocumentFactory.php +++ b/src/Factories/DocumentFactory.php @@ -179,7 +179,7 @@ protected function addField($doc, $object, $options): void $type = $typeMap[$options['type']] ?? $typeMap['*']; foreach ($valuesForField as $value) { - $this->extend('onBeforeAddDoc', $options, $value); + $this->extend('onBeforeAddDoc', $options, $value, $object); $this->addToDoc($doc, $options, $type, $value); } } diff --git a/src/Factories/SchemaFactory.php b/src/Factories/SchemaFactory.php index e3ba1e5..d94fd70 100644 --- a/src/Factories/SchemaFactory.php +++ b/src/Factories/SchemaFactory.php @@ -188,7 +188,7 @@ public function getCopyFieldDefinitions() $this->getFieldDefinition($copyField, $return, $field); } } - + $this->extend('onBeforeCopyFieldDefinitions', $return); return $return; } diff --git a/src/Indexes/BaseIndex.php b/src/Indexes/BaseIndex.php index d3d9249..1224330 100644 --- a/src/Indexes/BaseIndex.php +++ b/src/Indexes/BaseIndex.php @@ -106,9 +106,10 @@ abstract class BaseIndex public function __construct() { // Set up the client - $config = Config::inst()->get(SolrCoreService::class, 'config'); + $service = Injector::inst()->get(SolrCoreService::class); + $config = $service->getClient()->getOptions(); $config['endpoint'] = $this->getConfig($config['endpoint']); - $this->client = (new SolrCoreService())->getClient(); + $this->client = $service->getClient(); $this->client->setOptions($config); // Set up the schema service, only used in the generation of the schema @@ -287,7 +288,7 @@ protected function buildFactory(BaseQuery $query, Query $clientQuery) * Conditions are: * It is not already a retry with spellchecking * Spellchecking is enabled - * If spellchecking is enabled and nothing is found OR it should follow spellchecking none the less + * Spellcheck following is enabled and nothing is found * There is a spellcheck output * * @param BaseQuery $query @@ -299,7 +300,7 @@ protected function doRetry(BaseQuery $query, Result $result, SearchResult $searc { return !$this->retry && $query->hasSpellcheck() && - ($query->shouldFollowSpellcheck() || $result->getNumFound() === 0) && + ($query->shouldFollowSpellcheck() && $result->getNumFound() === 0) && $searchResult->getCollatedSpellcheck(); } diff --git a/src/Models/DirtyClass.php b/src/Models/DirtyClass.php index de98a9b..b6e78c3 100644 --- a/src/Models/DirtyClass.php +++ b/src/Models/DirtyClass.php @@ -13,6 +13,8 @@ use SilverStripe\Forms\ReadonlyField; use SilverStripe\ORM\DataObject; use SilverStripe\Security\Member; +use SilverStripe\Security\Permission; +use SilverStripe\Security\PermissionProvider; /** * Class \Firesphere\SolrSearch\Models\DirtyClass @@ -23,7 +25,7 @@ * @property string $Class * @property string $IDs */ -class DirtyClass extends DataObject +class DirtyClass extends DataObject implements PermissionProvider { /** * @var string Table name @@ -109,4 +111,37 @@ public function canCreate($member = null, $context = []) { return false; } + + /** + * Member has view access? + * + * @param null|Member $member + * @return bool|mixed + */ + public function canView($member = null) + { + return Permission::checkMember($member, 'VIEW_DIRTY_CLASSES'); + } + + /** + * Return a map of permission codes to add to the dropdown shown in the Security section of the CMS. + * array( + * 'VIEW_SITE' => 'View the site', + * ); + * + * @return array + */ + public function providePermissions() + { + return [ + 'VIEW_DIRTY_CLASSES' => [ + 'name' => _t(self::class . '.PERMISSION_VIEW_CLASSES_DESCRIPTION', 'View Solr dirty classes'), + 'category' => _t('Permissions.LOGS_CATEGORIES', 'Solr logs permissions'), + 'help' => _t( + self::class . '.PERMISSION_VIEW_CLASSES_HELP', + 'Permission required to view existing Solr dirty classes.' + ), + ], + ]; + } } diff --git a/src/Models/SearchSynonym.php b/src/Models/SearchSynonym.php index a2f926f..b109c6c 100644 --- a/src/Models/SearchSynonym.php +++ b/src/Models/SearchSynonym.php @@ -9,8 +9,11 @@ namespace Firesphere\SolrSearch\Models; +use Firesphere\SolrSearch\Admins\SearchAdmin; use SilverStripe\Forms\FieldList; use SilverStripe\ORM\DataObject; +use SilverStripe\Security\Permission; +use SilverStripe\Security\PermissionProvider; /** * Class \Firesphere\SolrSearch\Models\SearchSynonym @@ -20,7 +23,7 @@ * @property string $Keyword * @property string $Synonym */ -class SearchSynonym extends DataObject +class SearchSynonym extends DataObject implements PermissionProvider { /** * @var string Table name @@ -81,4 +84,70 @@ public function getCombinedSynonym() { return sprintf("\n%s,%s", $this->Keyword, $this->Synonym); } + + /** + * Member has view access? + * + * @param null|Member $member + * @return bool|mixed + */ + public function canView($member = null) + { + return SearchAdmin::singleton()->canView($member); + } + + /** + * Only deleteable by members with permission + * + * @param null|Member $member + * @return bool|mixed + */ + public function canDelete($member = null) + { + return Permission::checkMember($member, 'EDIT_SYNONYMS'); + } + + /** + * Only createable by members with permission + * + * @param null|Member $member + * @return boolean + */ + public function canCreate($member = null, $context = []) + { + return Permission::checkMember($member, 'EDIT_SYNONYMS'); + } + + /** + * Only editable by members with permission + * + * @param null|Member $member + * @return boolean + */ + public function canEdit($member = null) + { + return Permission::checkMember($member, 'EDIT_SYNONYMS'); + } + + /** + * Return a map of permission codes to add to the dropdown shown in the Security section of the CMS. + * array( + * 'VIEW_SITE' => 'View the site', + * ); + * + * @return array + */ + public function providePermissions() + { + return [ + 'EDIT_SYNONYMS' => [ + 'name' => _t(self::class . '.PERMISSION_EDIT_SYNONYMS_DESCRIPTION', 'Edit Solr synonyms'), + 'category' => _t('Permissions.LOGS_CATEGORIES', 'Solr logs permissions'), + 'help' => _t( + self::class . '.PERMISSION_EDIT_SYNONYMS_HELP', + 'Permission required to create, edit and delete existing Solr synonyms.' + ), + ], + ]; + } } diff --git a/src/Models/SolrLog.php b/src/Models/SolrLog.php index 6c413da..c32b537 100644 --- a/src/Models/SolrLog.php +++ b/src/Models/SolrLog.php @@ -13,6 +13,7 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\Security\Member; +use SilverStripe\Security\Permission; use SilverStripe\Security\PermissionProvider; /** @@ -132,22 +133,18 @@ public function canEdit($member = null) */ public function canView($member = null) { - return parent::canView($member); + return Permission::checkMember($member, 'VIEW_LOG'); } /** - * Only deleteable by admins or when in dev mode to clean up + * Only deleteable by members with permission or when in dev mode to clean up * * @param null|Member $member * @return bool|mixed */ public function canDelete($member = null) { - if ($member) { - return $member->inGroup('administrators') || Director::isDev(); - } - - return parent::canDelete($member) || Director::isDev(); + return Permission::checkMember($member, 'DELETE_LOG') || Director::isDev(); } /** @@ -162,7 +159,6 @@ public function getExtraClass() return $classMap[$this->Level] ?? 'alert alert-info'; } - /** * Return a map of permission codes to add to the dropdown shown in the Security section of the CMS. * array( diff --git a/src/Services/SolrCoreService.php b/src/Services/SolrCoreService.php index 5a58335..40ca194 100644 --- a/src/Services/SolrCoreService.php +++ b/src/Services/SolrCoreService.php @@ -98,6 +98,16 @@ class SolrCoreService "4.99999999.0", ]; + /** + * @var array|null Option for use in Solarium\QueryType\Update\Query\Query::addDocuments() function + */ + private static $add_docs_overwrite; + + /** + * @var array|null Option for use in Solarium\QueryType\Update\Query\Query::addDocuments() function + */ + private static $add_docs_commitWithin; + /** * SolrCoreService constructor. * @@ -282,7 +292,11 @@ public function updateIndex($index, $items, $update): void $factory = $this->getFactory($items); $docs = $factory->buildItems($fields, $index, $update); if (count($docs)) { - $update->addDocuments($docs); + $update->addDocuments( + $docs, + $this->config()->get('add_docs_overwrite'), + $this->config()->get('add_docs_commitWithin') + ); } } diff --git a/src/Tasks/SolrIndexTask.php b/src/Tasks/SolrIndexTask.php index a232ee8..45c7f0e 100644 --- a/src/Tasks/SolrIndexTask.php +++ b/src/Tasks/SolrIndexTask.php @@ -398,8 +398,11 @@ private function indexStateClass(string $group, string $class): void // Generate filtered list of local records $baseClass = DataObject::getSchema()->baseDataClass($class); /** @var DataList|DataObject[] $items */ - $items = DataObject::get($baseClass) - ->sort('ID ASC') + $items = DataObject::get($baseClass); + if (!empty($classes = $this->getIndex()->config()->get('exclude_classes'))) { + $items = $items->exclude(['ClassName' => $classes]); + } + $items = $items->sort('ID ASC') ->limit($this->getBatchLength(), ($group * $this->getBatchLength())); if ($items->count()) { $this->updateIndex($items); diff --git a/src/Traits/CoreTraits/CoreServiceTrait.php b/src/Traits/CoreTraits/CoreServiceTrait.php index 2854659..07a7012 100644 --- a/src/Traits/CoreTraits/CoreServiceTrait.php +++ b/src/Traits/CoreTraits/CoreServiceTrait.php @@ -104,6 +104,9 @@ public function getValidClasses(): array $classes = []; foreach ($indexes as $index) { $classes = $this->getClassesInHierarchy($index, $classes); + if (!empty($exclude = singleton($index)->config()->get('exclude_classes'))) { + $classes = array_diff($classes, $exclude); + } } $cache->set('ValidClasses', array_unique($classes));