diff --git a/README.md b/README.md index 1cd9cf3..1d03480 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,81 @@ Adds support for fulltext search engines like Sphinx and Solr to SilverStripe CMS. Compatible with PHP 7.2 -## Maintainer Contact +## Important notes when upgrading to fulltextsearch 3.7.0+ -* Hamish Friedlander +There are some significant changes from previous versions: + +Draft content will no longer be automatically added to the search index. This new behaviour was previously an +opt-in behaviour that was enabled by adding the following line to a search index: + +``` +$this->excludeVariantState([SearchVariantVersioned::class => Versioned::DRAFT]); +``` + +A new `canView()` check against an anonymous user (i.e. someone not logged in) and a `ShowInSearch` check is now +performed by default against all records (DataObjects) before being added to the search index, and also before being +shown in search results. This may mean that some records that were previously being indexed and shown in search results +will no longer appear due to these additional checks. + +These additional checks have been added with data security in mind, and it's assumed that records failing these +checks probably should not be indexed in the first place. + +# Enable indexing of draft content: + +You can index draft content with the following yml configuration: + +``` +SilverStripe\FullTextSearch\Search\Services\SearchableService: + variant_state_draft_excluded: false +``` + +However, when set to false, it will still only index draft content when a DataObject is in a published state, not a +draft-only or modified state. This is because it will still fail the new anonymous user `canView()` check in +`SearchableService::isSearchable()` and be automatically deleted from the index. + +If you wish to also index draft content when a DataObject is in a draft-only or a modified state, then you'll need +to also configure `SearchableService::indexing_canview_exclude_classes`. See below for instructions on how to do this. + +# Disabling the anonymous user canView() pre-index check + +You can apply configuration to remove the new pre-index `canView()` check from your DataObjects if it is not necessary, +or if it impedes expected functionality (e.g. for sites where users must authenticate to view any content). This will +also disable the check for descendants of the specified DataObjects. Ensure that your implementation of fulltextsearch +is correctly performing a `canView()` check at query time before disabling the pre-index check, as this may result in +leakage of private data. + +``` +SilverStripe\FullTextSearch\Search\Services\SearchableService: + indexing_canview_exclude_classes: + - Some\Org\MyDataObject + # This will disable the check for all pagetypes: + - SilverStripe\CMS\Model\SiteTree +``` + +You can also use the `updateIsSearchable` extension point on `SearchableService` to modify the result of the method +after the `ShowInSearch` and `canView()` checks have run. + +It is highly recommend you run a [solr_reindex](https://github.com/silverstripe/silverstripe-fulltextsearch/blob/3/docs/en/03_configuration.md#solr-reindex) +on your production site after upgrading from 3.6 or earlier to purge any old data that should no longer be in the search index. + +These additional check can have an impact on the reindex performance due to additional queries for permission checks. +If your site also indexes content in files, such as pdf's or docx's, using the [text-extraction](https://github.com/silverstripe/silverstripe-textextraction) +module which is fairly time-intensive, then the relative performance impact of the `canView()` checks won't be as noticeable. + +## Details on filtering before adding content to the solr index +- `SearchableService::isIndexable()` check in `SolrReindexBase`. Used when indexing all records during Solr reindex. +- `SearchableService::isIndexable()` check in `SearchUpdateProcessor`. Used when indexing single records during +`DataObject->write()`. + +## Details on filtering when extracting results from the solr index +- `SearchableService::isViewable()` check in `SolrIndex`. This will often be used in CWP implementations that use the +`CwpSearchEngine` class, as well as most custom implementations that call `MySearchIndex->search()` +- `SearchableService::isViewable()` check in `SearchForm`. This will be used in solr implementations where a +`/SearchForm` url is used to display search results. +- Some implementations will call `SearchableService::isViewable()` twice. If this happens then the first call will be +cached in memory so there is virtually no performance penalty calling it a second time. +- If your implementation is very custom and does not subclass nor make use of either `SolrIndex` or `SearchForm`, then +it's recommended you update your implementation to call `SearchableService::isViewable()`. ## Requirements diff --git a/changelog.md b/changelog.md deleted file mode 100644 index 26bd7f7..0000000 --- a/changelog.md +++ /dev/null @@ -1,90 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -This project adheres to [Semantic Versioning](http://semver.org/). - -## [2.2.0] - -* FIX Indexes with custom index names that don't match the classname were breaking -* BUG Fix versioned writes where subtables have no fields key -* BUGFIX: Fixed issue where the $id variable would be overridden in sub sequent iterations of the derived fields loop -* adding stemming support -* BUG fix issues with search variants applying to more than one class -* API adding stemming support -* FIX: Fix initial dev/build on PDO Database. - -## [2.1.1] - -* Converted to PSR-2 -* FIX: remove parameters from function calls -* Added standard code of conduct -* Added standard editor config -* Updated license -* Added standard gitattributes -* MINOR: Don't include Hamcrest globally so it doesn't conflict with PHPUnit - -## [2.1.0] - -* 3.2 Compatibility -* Add ss 3.2 and PHP 5.6 to CI -* Added standard Scrutinizer config -* Added standard Travis config - -## [2.0.0] - -* Fix highlight support when querying by fields (or boosting fields) -* Updating travis provisioner -* Added docs about controller and template usage -* API Enable boosted fields to be specified on the index -* BUG Prevent subsites breaking solrindexversionedtest -* Enable indexes to upload custom config -* API Additional support for custom copy_fields -* API QueuedJob support for Solr_Reindex - -## [1.1.0] - -* API Solr_Reindex uses configured SearchUpdater instead of always doing a direct write -* Fix class limit on delete query in SolrIndex -* Regression in SearchUpdater_ObjectHandler -* API Separate searchupdate / commit into separate queued-jobs -* API Only allow one scheduled commit job at a time - -## [1.0.6] - -* Make spelling suggestions more useful -* BUG Add missing addStoredFields method - -## [1.0.5] - -* BUG Fix Solr 4.0 compatibility issue -* BUG Fix test case not elegantly failing on missing phockito -* API SearchUpdateQueuedJobProcessor now uses batching -* Fix many_many fieldData bug -* Adding tests for SearchIndex::fieldData() -* Add a no-op query to prevent database timeouts during a long reindex - -## [1.0.4] - -* BUG Patch up the information leak of debug information. -* FIX: will work for postgreSQL - -## [1.0.3] - -Users upgrading from 1.0.2 or below will need to run the Solr_Reindex task to refresh -each SolrIndex. This is due to a change in record IDs, which are now generated from -the base class of each DataObject, rather than the instance class, as well as fixes -to integration with the subsites module. - -Developers working locally should be aware that by default, all indexes will be updated -in realtime when the environment is in dev mode, rather than attempting to queue these -updates with the queued jobs module (if installed). - -### Bugfixes - - * BUG Fix old indexing storing against the incorrect class key - * [Don't rely on MySQL ordering of index->getAdded()](https://github.com/silverstripe-labs/silverstripe-fulltextsearch/commit/4b51393e014fc4c0cc8e192c74eb4594acaca605) - -### API - - * [API Disable queued processing for development environments](https://github.com/silverstripe-labs/silverstripe-fulltextsearch/commit/71fc359b3711cf5b9429d86da0f1e0b20bd43dee) diff --git a/composer.json b/composer.json index 8c56bd7..2776038 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ ], "require": { "php": ">=7.1", - "silverstripe/framework": "^4.0", + "silverstripe/framework": "^4", "monolog/monolog": "~1.15", "ptcinc/solr-php-client": "^1.0", "symfony/process": "^3.2", diff --git a/docs/en/03_configuration.md b/docs/en/03_configuration.md index f95970d..d6122dc 100644 --- a/docs/en/03_configuration.md +++ b/docs/en/03_configuration.md @@ -112,12 +112,13 @@ Depending on the size of the index and how much content needs to be processed, i If the [Queued Jobs module](https://github.com/symbiote/silverstripe-queuedjobs/) is installed, updates are queued up instead of executed in the same request. Queued jobs are usually processed every minute. Large index updates will be batched into multiple queued jobs to ensure a job can run to completion within common constraints, such as memory and execution time limits. You can check the status of jobs in an administrative interface under `admin/queuedjobs/`. -### Excluding draft content +### Draft content -By default, the `SearchUpdater` class indexes all available "variant states", so in the case of the `Versioned` extension, both "draft" and "live". -For most cases, you'll want to exclude draft content from your search results. +By default, the `SearchUpdater` class attempts to index all available "variant states", except for draft content. +Draft content is excluded by default via calls to SearchableService::variantStateExcluded(). -You can either prevent the draft content from being indexed in the first place, by adding the following to your `SearchIndex::init()` method: +Excluding draft content was a new default added in 3.7.0. Prior to that, draft content was previously indexed by + default and could be excluded fron the index by adding the following to the `SearchIndex::init()` method: ```php use Page; @@ -136,7 +137,10 @@ class MyIndex extends SolrIndex } ``` -Alternatively, you can index draft content, but simply exclude it from searches. This can be handy to preview search results on unpublished content, in case a CMS author is logged in. Before constructing your `SearchQuery`, conditionally switch to the "live" stage. +If required, you can opt-out of the secure default and index draft content, but simply exclude it from searches. +Read the inline documentation within SearchableService.php for more details on how to do this. +This can be handy to preview search results on unpublished content, in case a CMS author is logged in. +Before constructing your `SearchQuery`, conditionally switch to the "live" stage. ### Adding DataObjects diff --git a/src/Search/Processors/SearchUpdateProcessor.php b/src/Search/Processors/SearchUpdateProcessor.php index de64dfc..0ecc5bd 100644 --- a/src/Search/Processors/SearchUpdateProcessor.php +++ b/src/Search/Processors/SearchUpdateProcessor.php @@ -2,10 +2,12 @@ namespace SilverStripe\FullTextSearch\Search\Processors; -use SilverStripe\FullTextSearch\Search\Services\IndexableService; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; +use SilverStripe\FullTextSearch\Search\Variants\SearchVariantVersioned; use SilverStripe\ORM\DataObject; use SilverStripe\FullTextSearch\Search\Variants\SearchVariant; use SilverStripe\FullTextSearch\Search\FullTextSearch; +use SilverStripe\Versioned\Versioned; abstract class SearchUpdateProcessor { @@ -72,7 +74,8 @@ abstract class SearchUpdateProcessor $dirtyIndexes = array(); $dirty = $this->getSource(); $indexes = FullTextSearch::get_indexes(); - $indexableService = IndexableService::singleton(); + $searchableService = SearchableService::singleton(); + foreach ($dirty as $base => $statefulids) { if (!$statefulids) { continue; @@ -86,11 +89,15 @@ abstract class SearchUpdateProcessor // Ensure that indexes for all new / updated objects are included $objs = DataObject::get($base)->byIDs(array_keys($ids)); + + /** @var DataObject $obj */ foreach ($objs as $obj) { foreach ($ids[$obj->ID] as $index) { - if (!$indexes[$index]->variantStateExcluded($state)) { - // Remove any existing records from index if ShowInSearch is changed to false - if (!$indexableService->isIndexable($obj)) { + if (!$searchableService->variantStateExcluded($state) && + !$indexes[$index]->variantStateExcluded($state) + ) { + // Remove any existing data from index if the object is no longer indexable + if (!$searchableService->isIndexable($obj)) { $indexes[$index]->delete($base, $obj->ID, $state); } else { $indexes[$index]->add($obj); @@ -104,7 +111,9 @@ abstract class SearchUpdateProcessor // Generate list of records that do not exist and should be removed foreach ($ids as $id => $fromindexes) { foreach ($fromindexes as $index) { - if (!$indexes[$index]->variantStateExcluded($state)) { + if (!$searchableService->variantStateExcluded($state) && + !$indexes[$index]->variantStateExcluded($state) + ) { $indexes[$index]->delete($base, $id, $state); $dirtyIndexes[$index] = $indexes[$index]; } diff --git a/src/Search/Services/IndexableService.php b/src/Search/Services/IndexableService.php deleted file mode 100644 index 8f56129..0000000 --- a/src/Search/Services/IndexableService.php +++ /dev/null @@ -1,58 +0,0 @@ -cache = []; - } - - public function isIndexable(DataObject $obj): bool - { - // check if is a valid DataObject that has been persisted to the database - if (is_null($obj) || !$obj->ID) { - return false; - } - - $key = $this->getCacheKey($obj); - if (isset($this->cache[$key])) { - return $this->cache[$key]; - } - - $value = true; - - // This will also call $obj->getShowInSearch() if it exists - if (isset($obj->ShowInSearch) && !$obj->ShowInSearch) { - $value = false; - } - - $this->extend('updateIsIndexable', $obj, $value); - $this->cache[$key] = $value; - return $value; - } - - protected function getCacheKey(DataObject $obj): string - { - $key = $obj->ClassName . '_' . $obj->ID; - $this->extend('updateCacheKey', $obj, $key); - return $key; - } -} diff --git a/src/Search/Services/SearchableService.php b/src/Search/Services/SearchableService.php new file mode 100644 index 0000000..a621a75 --- /dev/null +++ b/src/Search/Services/SearchableService.php @@ -0,0 +1,212 @@ +cache = []; + } + + /** + * Check to exclude a variant state + * + * @param array $state + * @return bool + */ + public function variantStateExcluded(array $state): bool + { + if (self::config()->get('variant_state_draft_excluded') && $this->isDraftVariantState($state)) { + return true; + } + return false; + } + + /** + * Check if a state array represents a draft variant + * + * @param array $state + * @return bool + */ + private function isDraftVariantState(array $state): bool + { + $class = SearchVariantVersioned::class; + return isset($state[$class]) && $state[$class] == Versioned::DRAFT; + } + + /** + * Used during search reindex + * + * This is considered the primary layer of protection + * + * @param DataObject $obj + * @return bool + */ + public function isIndexable(DataObject $obj): bool + { + return $this->isSearchable($obj, true); + } + + /** + * Used when retrieving search results + * + * This is considered the secondary layer of protection + * + * It's important to still have this layer in conjuction with the index layer as non-searchable results may be + * in the search index because: + * a) they were added to the index pre-fulltextsearch 3.7 and a reindex to purge old records was never run, OR + * b) the DataObject has a non-deterministic canView() check such as `return $date <= $dateOfIndex;` + * + * @param DataObject $obj + * @return bool + */ + public function isViewable(DataObject $obj): bool + { + return $this->isSearchable($obj, false); + } + + /** + * Checks and caches whether the given DataObject can be indexed. This is determined by two factors: + * - Whether the ShowInSearch property / getShowInSearch() method evaluates to true + * - Whether the canView method evaluates to true against an anonymous user (optional, can be disabled) + * + * @param DataObject $obj + * @param bool $indexing + * @return bool + */ + private function isSearchable(DataObject $obj, bool $indexing): bool + { + // check if is a valid DataObject that has been persisted to the database + if (is_null($obj) || !$obj->ID) { + return false; + } + + $key = $this->getCacheKey($obj, $indexing); + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + + $value = true; + + // ShowInSearch check + // This will also call $obj->getShowInSearch() if it exists + if (isset($obj->ShowInSearch) && !$obj->ShowInSearch) { + $value = false; + } + + // canView() checker + if ($value) { + $objClass = $obj->getClassName(); + if ($indexing) { + // Anonymous member canView() for indexing + if (!$this->classSkipsCanViewCheck($objClass)) { + $value = Member::actAs(null, function () use ($obj) { + return $obj->canView(); + }); + } + } else { + // Current member canView() check for retrieving search results + $value = $obj->canView(); + } + } + $this->extend('updateIsSearchable', $obj, $indexing, $value); + $this->cache[$key] = $value; + return $value; + } + + /** + * @param DataObject $obj + * @param bool $indexing + * @return string + */ + private function getCacheKey(DataObject $obj, bool $indexing): string + { + $type = $indexing ? 'indexing' : 'viewing'; + // getUniqueKey() requires silverstripe/framework 4.6 + $uniqueKey = ''; + if (method_exists($obj, 'getUniqueKey')) { + try { + $uniqueKey = $obj->getUniqueKey(); + } catch (\Exception $e) { + $uniqueKey = ''; + } + } + if (!$uniqueKey) { + $uniqueKey = sprintf('%s-%s', $obj->ClassName, $obj->ID); + } + $key = sprintf('%s-%s', $type, $uniqueKey); + $this->extend('updateCacheKey', $obj, $indexing, $key); + return $key; + } + + /** + * @param string $class + * @return bool + */ + private function classSkipsCanViewCheck(string $class): bool + { + $skipClasses = self::config()->get('indexing_canview_exclude_classes') ?? []; + if (empty($skipClasses)) { + return false; + } + if (in_array($class, $skipClasses)) { + return true; + } + foreach ($skipClasses as $skipClass) { + if (in_array($skipClass, class_parents($class))) { + return true; + } + } + return false; + } +} diff --git a/src/Solr/Forms/SearchForm.php b/src/Solr/Forms/SearchForm.php index 8cd54f4..643379f 100644 --- a/src/Solr/Forms/SearchForm.php +++ b/src/Solr/Forms/SearchForm.php @@ -9,7 +9,7 @@ use SilverStripe\Forms\FormAction; use SilverStripe\Forms\TextField; use SilverStripe\FullTextSearch\Search\FullTextSearch; use SilverStripe\FullTextSearch\Search\Queries\SearchQuery; -use SilverStripe\FullTextSearch\Search\Services\IndexableService; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Solr\SolrIndex; use SilverStripe\ORM\DataObject; use SilverStripe\View\ArrayData; @@ -83,13 +83,13 @@ class SearchForm extends Form $index = $indexClass::singleton(); $results = $index->search($query, -1, -1, $params); - $indexableService = IndexableService::singleton(); + $searchableService = SearchableService::singleton(); // filter by permission if ($results) { foreach ($results->Matches as $match) { /** @var DataObject $match */ - if (!$indexableService->isIndexable($match)) { + if (!$searchableService->isViewable($match)) { $results->Matches->remove($match); } } diff --git a/src/Solr/Reindex/Handlers/SolrReindexBase.php b/src/Solr/Reindex/Handlers/SolrReindexBase.php index b022734..91fb747 100644 --- a/src/Solr/Reindex/Handlers/SolrReindexBase.php +++ b/src/Solr/Reindex/Handlers/SolrReindexBase.php @@ -4,7 +4,8 @@ namespace SilverStripe\FullTextSearch\Solr\Reindex\Handlers; use Psr\Log\LoggerInterface; use SilverStripe\Core\Environment; -use SilverStripe\FullTextSearch\Search\Services\IndexableService; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; +use SilverStripe\FullTextSearch\Search\Variants\SearchVariantVersioned; use SilverStripe\FullTextSearch\Solr\Solr; use SilverStripe\FullTextSearch\Solr\SolrIndex; use SilverStripe\FullTextSearch\Search\Variants\SearchVariant; @@ -12,6 +13,7 @@ use SilverStripe\FullTextSearch\Search\Queries\SearchQuery; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DB; +use SilverStripe\Versioned\Versioned; /** * Base class for re-indexing of solr content @@ -41,6 +43,8 @@ abstract class SolrReindexBase implements SolrReindexHandler $taskName, $classes = null ) { + $searchableService = SearchableService::singleton(); + // Filter classes for this index $indexClasses = $this->getClassesForIndex($indexInstance, $classes); @@ -53,7 +57,9 @@ abstract class SolrReindexBase implements SolrReindexHandler $includeSubclasses = $options['include_children']; foreach (SearchVariant::reindex_states($class, $includeSubclasses) as $state) { - $this->processVariant($logger, $indexInstance, $state, $class, $includeSubclasses, $batchSize, $taskName); + if (!$searchableService->variantStateExcluded($state)) { + $this->processVariant($logger, $indexInstance, $state, $class, $includeSubclasses, $batchSize, $taskName); + } } } } @@ -238,13 +244,12 @@ abstract class SolrReindexBase implements SolrReindexHandler $items = $items->filter('ClassName', $class); } - $indexableService = IndexableService::singleton(); + $searchableService = SearchableService::singleton(); - // ShowInSearch filter - // we cannot use $items->remove($item), as that deletes the record from the database + // Filter out objects that must not be indexed $idsToRemove = []; foreach ($items as $item) { - if (!$indexableService->isIndexable($item)) { + if (!$searchableService->isIndexable($item)) { $idsToRemove[] = $item->ID; } } diff --git a/src/Solr/SolrIndex.php b/src/Solr/SolrIndex.php index 89d6987..3d46e0e 100644 --- a/src/Solr/SolrIndex.php +++ b/src/Solr/SolrIndex.php @@ -9,7 +9,7 @@ use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex; use SilverStripe\FullTextSearch\Search\Queries\SearchQuery; use SilverStripe\FullTextSearch\Search\Queries\SearchQuery_Range; use SilverStripe\FullTextSearch\Search\SearchIntrospection; -use SilverStripe\FullTextSearch\Search\Services\IndexableService; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Search\Variants\SearchVariant; use SilverStripe\FullTextSearch\Search\Variants\SearchVariant_Caller; use SilverStripe\FullTextSearch\Solr\Services\SolrService; @@ -778,15 +778,14 @@ abstract class SolrIndex extends SearchIndex \Apache_Solr_Service::METHOD_POST ); - $indexableService = IndexableService::singleton(); + $searchableService = SearchableService::singleton(); $results = new ArrayList(); if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) { foreach ($res->response->docs as $doc) { $result = DataObject::get_by_id($doc->ClassName, $doc->ID); if ($result) { - // Filter out any results previously added to the solr index where ShowInSearch == false - if (!$indexableService->isIndexable($result)) { + if (!$searchableService->isViewable($result)) { continue; } diff --git a/tests/BatchedProcessorTest.php b/tests/BatchedProcessorTest.php index 3aa92b0..b3483da 100644 --- a/tests/BatchedProcessorTest.php +++ b/tests/BatchedProcessorTest.php @@ -7,6 +7,7 @@ use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; use SilverStripe\FullTextSearch\Search\FullTextSearch; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Tests\BatchedProcessorTest\BatchedProcessor_QueuedJobService; use SilverStripe\FullTextSearch\Tests\BatchedProcessorTest\BatchedProcessorTest_Index; use SilverStripe\FullTextSearch\Tests\BatchedProcessorTest\BatchedProcessorTest_Object; @@ -120,6 +121,9 @@ class BatchedProcessorTest extends SapphireTest */ public function testBatching() { + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', [SiteTree::class]); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); + $index = singleton(BatchedProcessorTest_Index::class); $index->reset(); $processor = $this->generateDirtyIds(); diff --git a/tests/IndexableServiceTest.php b/tests/IndexableServiceTest.php deleted file mode 100644 index b7a39ee..0000000 --- a/tests/IndexableServiceTest.php +++ /dev/null @@ -1,55 +0,0 @@ -clearCache(); - } - - public function testIsIndexable() - { - $indexableService = IndexableService::singleton(); - - $page = SiteTree::create(); - $page->CanViewType = 'Anyone'; - $page->ShowInSearch = 1; - $page->write(); - $this->assertTrue($indexableService->isIndexable($page)); - - $page = SiteTree::create(); - $page->CanViewType = 'Anyone'; - $page->ShowInSearch = 0; - $page->write(); - $this->assertFalse($indexableService->isIndexable($page)); - } - - public function testClearCache() - { - $indexableService = IndexableService::singleton(); - - $page = SiteTree::create(); - $page->CanViewType = 'Anyone'; - $page->ShowInSearch = 0; - $page->write(); - $this->assertFalse($indexableService->isIndexable($page)); - - // test the results are cached (expect stale result) - $page->ShowInSearch = 1; - $page->write(); - $this->assertFalse($indexableService->isIndexable($page)); - - // after clearing cache, expect fresh result - $indexableService->clearCache(); - $this->assertTrue($indexableService->isIndexable($page)); - } -} diff --git a/tests/SearchUpdaterTest.php b/tests/SearchUpdaterTest.php index aea22e3..5ed97ee 100644 --- a/tests/SearchUpdaterTest.php +++ b/tests/SearchUpdaterTest.php @@ -8,6 +8,7 @@ use SilverStripe\Dev\SapphireTest; use SilverStripe\FullTextSearch\Search\FullTextSearch; use SilverStripe\FullTextSearch\Search\Processors\SearchUpdateProcessor; use SilverStripe\FullTextSearch\Search\Processors\SearchUpdateImmediateProcessor; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Search\Updaters\SearchUpdater; use SilverStripe\FullTextSearch\Tests\SearchUpdaterTest\SearchUpdaterTest_Container; use SilverStripe\FullTextSearch\Tests\SearchUpdaterTest\SearchUpdaterTest_HasOne; @@ -49,6 +50,9 @@ class SearchUpdaterTest extends SapphireTest public function testHasOneHook() { + $classesToSkip = [SearchUpdaterTest_Container::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + $hasOne = new SearchUpdaterTest_HasOne(); $hasOne->write(); @@ -127,6 +131,9 @@ class SearchUpdaterTest extends SapphireTest public function testHasManyHook() { + $classesToSkip = [SearchUpdaterTest_Container::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + $container1 = new SearchUpdaterTest_Container(); $container1->write(); diff --git a/tests/SearchVariantVersionedTest.php b/tests/SearchVariantVersionedTest.php index 5c34da7..3eb243e 100644 --- a/tests/SearchVariantVersionedTest.php +++ b/tests/SearchVariantVersionedTest.php @@ -7,6 +7,7 @@ use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Dev\SapphireTest; use SilverStripe\FullTextSearch\Search\FullTextSearch; use SilverStripe\FullTextSearch\Search\Indexes\SearchIndex_Recording; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Search\Variants\SearchVariantVersioned; use SilverStripe\FullTextSearch\Tests\SearchVariantVersionedTest\SearchVariantVersionedTest_Index; use SilverStripe\FullTextSearch\Tests\SearchVariantVersionedTest\SearchVariantVersionedTest_Item; @@ -45,6 +46,9 @@ class SearchVariantVersionedTest extends SapphireTest public function testPublishing() { // Check that write updates Stage + $classesToSkip = [SearchVariantVersionedTest_Item::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); $item = new SearchVariantVersionedTest_Item(array('TestText' => 'Foo')); $item->write(); diff --git a/tests/SearchableServiceTest.php b/tests/SearchableServiceTest.php new file mode 100644 index 0000000..d4ea572 --- /dev/null +++ b/tests/SearchableServiceTest.php @@ -0,0 +1,120 @@ +clearCache(); + } + + public function testIsIndexable() + { + Versioned::set_draft_site_secured(false); + Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); + + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', [SiteTree::class]); + + Member::actAs(null, function () { + $searchableService = SearchableService::singleton(); + + $page = SiteTree::create(); + $page->CanViewType = 'Anyone'; + $page->ShowInSearch = 1; + $page->write(); + $this->assertTrue($searchableService->isIndexable($page)); + + $page = SiteTree::create(); + $page->CanViewType = 'Anyone'; + $page->ShowInSearch = 0; + $page->write(); + $this->assertFalse($searchableService->isIndexable($page)); + }); + } + + public function testIsViewable() + { + Versioned::set_draft_site_secured(false); + Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); + + Member::actAs(null, function () { + $searchableService = SearchableService::singleton(); + + $page = SiteTree::create(); + $page->CanViewType = 'Anyone'; + $page->ShowInSearch = 1; + $page->write(); + $this->assertTrue($searchableService->isViewable($page)); + + $page = SiteTree::create(); + $page->CanViewType = 'LoggedInUsers'; + $page->ShowInSearch = 1; + $page->write(); + $this->assertFalse($searchableService->isViewable($page)); + }); + } + + public function testClearCache() + { + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', [SiteTree::class]); + + $searchableService = SearchableService::singleton(); + + $page = SiteTree::create(); + $page->CanViewType = 'Anyone'; + $page->ShowInSearch = 0; + $page->write(); + $this->assertFalse($searchableService->isIndexable($page)); + + // test the results are cached (expect stale result) + $page->ShowInSearch = 1; + $page->write(); + $this->assertFalse($searchableService->isIndexable($page)); + + // after clearing cache, expect fresh result + $searchableService->clearCache(); + $this->assertTrue($searchableService->isIndexable($page)); + } + + public function testSkipIndexingCanViewCheck() + { + $searchableService = SearchableService::singleton(); + $page = SiteTree::create(); + $page->CanViewType = 'LoggedInUsers'; + $page->ShowInSearch = 1; + $page->write(); + $this->assertFalse($searchableService->isIndexable($page)); + + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', [SiteTree::class]); + $searchableService->clearCache(); + $this->assertTrue($searchableService->isIndexable($page)); + } + + public function testVariantStateExcluded() + { + $searchableService = SearchableService::singleton(); + $variantStateDraft = [SearchVariantVersioned::class => Versioned::DRAFT]; + $variantStateLive = [SearchVariantVersioned::class => Versioned::LIVE]; + + // default variant_state_draft_excluded = true + $this->assertTrue($searchableService->variantStateExcluded($variantStateDraft)); + $this->assertFalse($searchableService->variantStateExcluded($variantStateLive)); + + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); + $this->assertFalse($searchableService->variantStateExcluded($variantStateDraft)); + $this->assertFalse($searchableService->variantStateExcluded($variantStateLive)); + } +} diff --git a/tests/SolrIndexSubsitesTest.php b/tests/SolrIndexSubsitesTest.php index d5c292a..25ee074 100644 --- a/tests/SolrIndexSubsitesTest.php +++ b/tests/SolrIndexSubsitesTest.php @@ -13,6 +13,7 @@ use SilverStripe\Dev\SapphireTest; use SilverStripe\FullTextSearch\Search\FullTextSearch; use SilverStripe\FullTextSearch\Search\Processors\SearchUpdateImmediateProcessor; use SilverStripe\FullTextSearch\Search\Processors\SearchUpdateProcessor; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Search\Updaters\SearchUpdater; use SilverStripe\FullTextSearch\Search\Variants\SearchVariantSubsites; use SilverStripe\FullTextSearch\Solr\Services\Solr4Service; @@ -107,6 +108,10 @@ class SolrIndexSubsitesTest extends SapphireTest public function testPublishing() { + $classesToSkip = [SiteTree::class, File::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); + // Setup mocks $serviceMock = $this->getServiceMock(); self::$index->setService($serviceMock); diff --git a/tests/SolrIndexTest.php b/tests/SolrIndexTest.php index e8f4efe..34e71a7 100644 --- a/tests/SolrIndexTest.php +++ b/tests/SolrIndexTest.php @@ -4,7 +4,6 @@ namespace SilverStripe\FullTextSearch\Tests; use Apache_Solr_Document; use Page; -use SebastianBergmann\Version; use SilverStripe\Assets\File; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Core\Config\Config; @@ -14,10 +13,9 @@ use SilverStripe\Core\Kernel; use SilverStripe\Dev\SapphireTest; use SilverStripe\FullTextSearch\Search\FullTextSearch; use SilverStripe\FullTextSearch\Search\Queries\SearchQuery; -use SilverStripe\FullTextSearch\Search\Services\IndexableService; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Search\Updaters\SearchUpdater; use SilverStripe\FullTextSearch\Search\Variants\SearchVariantSubsites; -use SilverStripe\FullTextSearch\Solr\SolrIndex; use SilverStripe\FullTextSearch\Solr\Services\Solr3Service; use SilverStripe\FullTextSearch\Solr\Services\Solr4Service; use SilverStripe\FullTextSearch\Tests\SearchUpdaterTest\SearchUpdaterTest_Container; @@ -34,7 +32,6 @@ use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_ShowInSearchIn use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyPage; use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyDataObjectOne; use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyDataObjectTwo; -use SilverStripe\ORM\DataObject; use SilverStripe\Subsites\Model\Subsite; use SilverStripe\Versioned\Versioned; @@ -409,8 +406,10 @@ class SolrIndexTest extends SapphireTest */ public function testShowInSearch() { - $defaultMode = Versioned::get_reading_mode(); + // allow anonymous users to assess draft-only content to pass canView() check (will auto-reset for next test) + Versioned::set_draft_site_secured(false); Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); $serviceMock = $this->getMockBuilder(Solr4Service::class) ->setMethods(['addDocument', 'deleteById']) @@ -508,10 +507,106 @@ class SolrIndexTest extends SapphireTest })] ); - IndexableService::singleton()->clearCache(); + SearchableService::singleton()->clearCache(); + SearchUpdater::flush_dirty_indexes(); + } + + /** + * Test that canView() check is used to exclude DataObjects from being added to the index + * + * Note: this code path that really being tested here is SearchUpdateProcessor->prepareIndexes() + * This code path is used for 'inlet' filtering on CMS->save() + * The results of this will show-up in SolrIndex->_addAs() + */ + public function testCanView() + { + // allow anonymous users to assess draft-only content to pass canView() check (will auto-reset for next test) + Versioned::set_draft_site_secured(false); + Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); + + $serviceMock = $this->getMockBuilder(Solr4Service::class) + ->setMethods(['addDocument', 'deleteById']) + ->getMock(); + + $index = new SolrIndexTest_ShowInSearchIndex(); + $index->setService($serviceMock); + FullTextSearch::force_index_list($index); + + // will get added + $pageA = new Page(); + $pageA->Title = 'Test Page Anyone'; + $pageA->CanViewType = 'Anyone'; + $pageA->write(); + + // will get filtered out + $page = new Page(); + $page->Title = 'Test Page LoggedInUsers'; + $page->CanViewType = 'LoggedInUsers'; + $page->write(); + + // will get added + $fileA = new File(); + $fileA->Title = 'Test File Anyone'; + $fileA->CanViewType = 'Anyone'; + $fileA->write(); + + // will get filtered out + $file = new File(); + $file->Title = 'Test File LoggedInUsers'; + $file->CanViewType = 'LoggedInUsers'; + $file->write(); + + // will get added + $objOneA = new SolrIndexTest_MyDataObjectOne(); + $objOneA->Title = 'Test MyDataObjectOne true'; + $objOneA->ShowInSearch = true; + $objOneA->CanViewValue = true; + $objOneA->write(); + + // will get filtered out + $objOne = new SolrIndexTest_MyDataObjectOne(); + $objOne->Title = 'Test MyDataObjectOne false'; + $objOne->ShowInSearch = true; + $objOne->CanViewValue = false; + $objOne->write(); + + $callback = function (Apache_Solr_Document $doc) use ($pageA, $fileA, $objOneA): bool { + $validKeys = [ + Page::class . $pageA->ID, + File::class . $fileA->ID, + SolrIndexTest_MyDataObjectOne::class . $objOneA->ID + ]; + return in_array($this->createSolrDocKey($doc), $validKeys); + }; + + $serviceMock + ->expects($this->exactly(3)) + ->method('addDocument') + ->withConsecutive( + [$this->callback($callback)], + [$this->callback($callback)], + [$this->callback($callback)] + ); + + // This is what actually triggers all the solr stuff SearchUpdater::flush_dirty_indexes(); - Versioned::set_reading_mode($defaultMode); + // delete a solr doc by setting ShowInSearch to false + $pageA->ShowInSearch = false; + $pageA->write(); + + $serviceMock + ->expects($this->exactly(1)) + ->method('deleteById') + ->withConsecutive( + [$this->callback(function (string $docID) use ($pageA): bool { + return strpos($docID, $pageA->ID . '-' . SiteTree::class) !== false; + })] + ); + + SearchableService::singleton()->clearCache(); + SearchUpdater::flush_dirty_indexes(); } protected function createSolrDocKey(Apache_Solr_Document $doc) diff --git a/tests/SolrIndexTest/SolrIndexTest_MyDataObjectOne.php b/tests/SolrIndexTest/SolrIndexTest_MyDataObjectOne.php index f484784..066e809 100644 --- a/tests/SolrIndexTest/SolrIndexTest_MyDataObjectOne.php +++ b/tests/SolrIndexTest/SolrIndexTest_MyDataObjectOne.php @@ -9,8 +9,14 @@ class SolrIndexTest_MyDataObjectOne extends DataObject implements TestOnly { private static $db = [ 'Title' => 'Varchar(255)', - 'ShowInSearch' => 'Boolean' + 'ShowInSearch' => 'Boolean', + 'CanViewValue' => 'Boolean(true)', ]; private static $table_name = 'SolrIndexTestMyDataObjectOne'; + + public function canView($member = null) + { + return $this->CanViewValue; + } } diff --git a/tests/SolrIndexVersionedTest.php b/tests/SolrIndexVersionedTest.php index f0fdcfb..d88ccaa 100644 --- a/tests/SolrIndexVersionedTest.php +++ b/tests/SolrIndexVersionedTest.php @@ -6,6 +6,7 @@ use Apache_Solr_Document; use SilverStripe\Dev\SapphireTest; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\ORM\DataObject; use SilverStripe\FullTextSearch\Search\FullTextSearch; use SilverStripe\FullTextSearch\Search\SearchIntrospection; @@ -112,6 +113,10 @@ class SolrIndexVersionedTest extends SapphireTest public function testPublishing() { + $classesToSkip = [SearchVariantVersionedTest_Item::class, SolrIndexVersionedTest_Object::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); + // Check that write updates Stage Versioned::set_stage(Versioned::DRAFT); @@ -166,6 +171,10 @@ class SolrIndexVersionedTest extends SapphireTest public function testDelete() { + $classesToSkip = [SearchVariantVersionedTest_Item::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); + // Delete the live record (not the stage) Versioned::set_stage(Versioned::DRAFT); diff --git a/tests/SolrReindexQueuedTest.php b/tests/SolrReindexQueuedTest.php index 038b869..7adb103 100644 --- a/tests/SolrReindexQueuedTest.php +++ b/tests/SolrReindexQueuedTest.php @@ -6,12 +6,14 @@ use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; use SilverStripe\FullTextSearch\Search\FullTextSearch; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Solr\Reindex\Handlers\SolrReindexHandler; use SilverStripe\FullTextSearch\Solr\Reindex\Handlers\SolrReindexQueuedHandler; use SilverStripe\FullTextSearch\Solr\Reindex\Jobs\SolrReindexGroupQueuedJob; use SilverStripe\FullTextSearch\Solr\Reindex\Jobs\SolrReindexQueuedJob; use SilverStripe\FullTextSearch\Solr\Services\Solr4Service; use SilverStripe\FullTextSearch\Solr\Services\SolrService; +use SilverStripe\FullTextSearch\Tests\SearchVariantVersionedTest\SearchVariantVersionedTest_Item; use SilverStripe\FullTextSearch\Tests\SolrReindexQueuedTest\SolrReindexQueuedTest_Service; use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Index; use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Item; @@ -135,6 +137,9 @@ class SolrReindexQueuedTest extends SapphireTest */ public function testReindexSegmentsGroups() { + $classesToSkip = [SolrReindexTest_Item::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + $this->createDummyData(18); // Deletes are performed in the main task prior to individual groups being processed @@ -194,6 +199,9 @@ class SolrReindexQueuedTest extends SapphireTest */ public function testRunGroup() { + $classesToSkip = [SolrReindexTest_Item::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + $this->createDummyData(18); // Just do what the SolrReindexQueuedJob would do to create each sub diff --git a/tests/SolrReindexTest.php b/tests/SolrReindexTest.php index 413b34d..28513b1 100644 --- a/tests/SolrReindexTest.php +++ b/tests/SolrReindexTest.php @@ -10,6 +10,7 @@ use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; use SilverStripe\FullTextSearch\Search\FullTextSearch; +use SilverStripe\FullTextSearch\Search\Services\SearchableService; use SilverStripe\FullTextSearch\Search\Updaters\SearchUpdater; use SilverStripe\FullTextSearch\Search\Variants\SearchVariant; use SilverStripe\FullTextSearch\Search\Variants\SearchVariantVersioned; @@ -18,6 +19,7 @@ use SilverStripe\FullTextSearch\Solr\Reindex\Handlers\SolrReindexImmediateHandle use SilverStripe\FullTextSearch\Solr\Services\Solr4Service; use SilverStripe\FullTextSearch\Solr\Services\SolrService; use SilverStripe\FullTextSearch\Solr\Tasks\Solr_Reindex; +use SilverStripe\FullTextSearch\Tests\SearchVariantVersionedTest\SearchVariantVersionedTest_Item; use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyDataObjectOne; use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyDataObjectTwo; use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyPage; @@ -235,6 +237,9 @@ class SolrReindexTest extends SapphireTest */ public function testRunGroup() { + $classesToSkip = [SolrReindexTest_Item::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + $this->service->method('deleteByQuery') ->with('+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=2 u=2}mod(ID, 6)" +(_testvariant:"1")'); @@ -266,6 +271,9 @@ class SolrReindexTest extends SapphireTest */ public function testRunAllGroups() { + $classesToSkip = [SolrReindexTest_Item::class]; + Config::modify()->set(SearchableService::class, 'indexing_canview_exclude_classes', $classesToSkip); + $this->service->method('deleteByQuery') ->withConsecutive( ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=0 u=0}mod(ID, 6)" +(_testvariant:"1")'], @@ -305,8 +313,10 @@ class SolrReindexTest extends SapphireTest */ public function testShowInSearch() { - $defaultMode = Versioned::get_reading_mode(); + // allow anonymous users to assess draft-only content to pass canView() check (will auto-reset for next test) + Versioned::set_draft_site_secured(false); Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); // will get added $pageA = new Page(); @@ -394,8 +404,89 @@ class SolrReindexTest extends SapphireTest $handler->runGroup($logger, $index, $state, SiteTree::class, 1, 0); $handler->runGroup($logger, $index, $state, File::class, 1, 0); $handler->runGroup($logger, $index, $state, SolrIndexTest_MyDataObjectOne::class, 1, 0); + } - Versioned::set_reading_mode($defaultMode); + /** + * Test that CanView filtering is working correctly + */ + public function testCanView() + { + // allow anonymous users to assess draft-only content to pass canView() check (will auto-reset for next test) + Versioned::set_draft_site_secured(false); + Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); + Config::modify()->set(SearchableService::class, 'variant_state_draft_excluded', false); + + // will get added + $pageA = new Page(); + $pageA->Title = 'Test Page Anyone'; + $pageA->CanViewType = 'Anyone'; + $pageA->write(); + + // will get filtered out + $page = new Page(); + $page->Title = 'Test Page LoggedInUsers'; + $page->CanViewType = 'LoggedInUsers'; + $page->write(); + + // will get added + $fileA = new File(); + $fileA->Title = 'Test File Anyone'; + $fileA->CanViewType = 'Anyone'; + $fileA->write(); + + // will get filtered out + $file = new File(); + $file->Title = 'Test File LoggedInUsers'; + $file->CanViewType = 'LoggedInUsers'; + $file->write(); + + // will get added + $objOneA = new SolrIndexTest_MyDataObjectOne(); + $objOneA->Title = 'Test MyDataObjectOne true'; + $objOneA->CanViewValue = true; + $objOneA->ShowInSearch = true; + $objOneA->write(); + + // will get filtered out + $objOne = new SolrIndexTest_MyDataObjectOne(); + $objOne->Title = 'Test MyDataObjectOne false'; + $objOne->CanViewValue = false; + $objOneA->ShowInSearch = true; + $objOne->write(); + + $serviceMock = $this->getMockBuilder(Solr4Service::class) + ->setMethods(['addDocument', 'deleteByQuery']) + ->getMock(); + + $index = new SolrIndexTest_ShowInSearchIndex(); + $index->setService($serviceMock); + FullTextSearch::force_index_list($index); + + $callback = function (Apache_Solr_Document $doc) use ($pageA, $fileA, $objOneA): bool { + $validKeys = [ + Page::class . $pageA->ID, + File::class . $fileA->ID, + SolrIndexTest_MyDataObjectOne::class . $objOneA->ID, + ]; + $solrDocKey = $this->createSolrDocKey($doc); + return in_array($this->createSolrDocKey($doc), $validKeys); + }; + + $serviceMock + ->expects($this->exactly(3)) + ->method('addDocument') + ->withConsecutive( + [$this->callback($callback)], + [$this->callback($callback)], + [$this->callback($callback)] + ); + + $logger = new SolrReindexTest_RecordingLogger(); + $state = [SearchVariantVersioned::class => Versioned::DRAFT]; + $handler = Injector::inst()->get(SolrReindexImmediateHandler::class); + $handler->runGroup($logger, $index, $state, SiteTree::class, 1, 0); + $handler->runGroup($logger, $index, $state, File::class, 1, 0); + $handler->runGroup($logger, $index, $state, SolrIndexTest_MyDataObjectOne::class, 1, 0); } protected function createSolrDocKey(Apache_Solr_Document $doc)