diff --git a/src/Plugin/views/query/SearchApiQuery.php b/src/Plugin/views/query/SearchApiQuery.php index 325e747e..66513f20 100644 --- a/src/Plugin/views/query/SearchApiQuery.php +++ b/src/Plugin/views/query/SearchApiQuery.php @@ -3,6 +3,7 @@ namespace Drupal\search_api\Plugin\views\query; use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Database\Query\ConditionInterface; use Drupal\Core\Entity\EntityInterface; @@ -120,6 +121,13 @@ class SearchApiQuery extends QueryPluginBase { */ protected $messenger; + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + /** * The cacheability metadata of the result. * @@ -150,6 +158,7 @@ class SearchApiQuery extends QueryPluginBase { $plugin->setModuleHandler($container->get('module_handler')); $plugin->setMessenger($container->get('messenger')); $plugin->setLogger($container->get('logger.channel.search_api')); + $plugin->setEntityTypeManager($container->get('entity_type.manager')); return $plugin; } @@ -262,6 +271,29 @@ class SearchApiQuery extends QueryPluginBase { return $this; } + /** + * Returns the entity type manager. + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager. + */ + public function getEntityTypeManager() { + return $this->entityTypeManager; + } + + /** + * Sets the entity type manager. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * + * @return $this + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + return $this; + } + /** * {@inheritdoc} */ @@ -711,31 +743,27 @@ class SearchApiQuery extends QueryPluginBase { $this->cacheableMetadata = $result_set->getCacheableMetadata($account); } - /** - * {@inheritdoc} - */ - public function getCacheMaxAge() { - // The parent implementation is of no use for us. - return $this->cacheableMetadata->getCacheMaxAge(); - } - /** * {@inheritdoc} */ public function getCacheContexts() { - // The parent implementation is of no use for us. - return $this->cacheableMetadata->getCacheContexts(); + $contexts = []; + + foreach ($this->getIndex()->getEntityTypes() as $entity_type_id) { + $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id); + $contexts = Cache::mergeContexts($entity_type_definition->getListCacheContexts(), $contexts); + } + + return $contexts; } /** * {@inheritdoc} */ public function getCacheTags() { - // The parent implementation is of no use for us. - return $this->cacheableMetadata->getCacheTags(); + return $this->getIndex()->getCacheTagsToInvalidate(); } - /** * Retrieves the conditions placed on this query. * diff --git a/tests/search_api_test_node_indexing/config/install/search_api.index.test_node_index.yml b/tests/search_api_test_node_indexing/config/install/search_api.index.test_node_index.yml new file mode 100644 index 00000000..4b3488e9 --- /dev/null +++ b/tests/search_api_test_node_indexing/config/install/search_api.index.test_node_index.yml @@ -0,0 +1,48 @@ +id: test_node_index +name: 'Test node index' +description: 'An index of node entities used for testing' +read_only: false +field_settings: + status: + label: Published + datasource_id: 'entity:node' + property_path: status + type: boolean + dependencies: + module: + - node + title: + label: Title + datasource_id: 'entity:node' + property_path: title + type: string + dependencies: + module: + - node +processor_settings: + add_url: { } + aggregated_field: { } + rendered_item: { } +options: + cron_limit: -1 + index_directly: true +datasource_settings: + 'entity:node': + bundles: + default: true + selected: { } + languages: + default: true + selected: { } +tracker_settings: + default: + indexing_order: fifo +server: database_search_server +status: true +langcode: en +dependencies: + config: + - search_api.server.database_search_server + module: + - node + - search_api diff --git a/tests/search_api_test_node_indexing/config/install/search_api.server.database_search_server.yml b/tests/search_api_test_node_indexing/config/install/search_api.server.database_search_server.yml new file mode 100644 index 00000000..0e79fda4 --- /dev/null +++ b/tests/search_api_test_node_indexing/config/install/search_api.server.database_search_server.yml @@ -0,0 +1,13 @@ +id: database_search_server +name: 'Database search server' +description: 'A server used for testing' +backend: search_api_db +backend_config: + database: 'default:default' + min_chars: 3 + matching: words +status: true +langcode: en +dependencies: + module: + - search_api_db diff --git a/tests/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml b/tests/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml new file mode 100644 index 00000000..6f381fab --- /dev/null +++ b/tests/search_api_test_node_indexing/config/install/views.view.search_api_test_node_view.yml @@ -0,0 +1,173 @@ +base_field: search_api_id +base_table: search_api_index_test_node_index +core: 8.x +description: '' +status: true +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: search_api_query + options: + bypass_access: false + skip_access: false + preserve_facet_query_args: false + exposed_form: + type: basic + options: + submit_button: Search + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 20, 40, 60' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + row: + type: fields + fields: + title: + id: title + table: search_api_index_test_node_index + field: title + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: false + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + field_rendering: true + fallback_handler: search_api + fallback_options: + link_to_item: false + use_highlighting: false + multi_type: separator + multi_separator: ', ' + plugin_id: search_api_field + filters: { } + sorts: { } + title: 'Search API test node view' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - user.is_super_user + - 'user.node_grants:view' + tags: + - 'config:search_api.index.test_node_index' + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: search-api-test-node-view + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + tags: + - 'config:search_api.index.test_node_index' +label: 'Search API test node view' +module: views +id: search_api_test_node_view +tag: '' +langcode: en +dependencies: + config: + - search_api.index.test_node_index + module: + - search_api diff --git a/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml b/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml new file mode 100644 index 00000000..10cdc56f --- /dev/null +++ b/tests/search_api_test_node_indexing/search_api_test_node_indexing.info.yml @@ -0,0 +1,8 @@ +type: module +name: 'Search API node indexing test' +description: 'Test module for testing indexing of nodes in Search API.' +package: 'Search API' +dependencies: + - search_api:search_api_db +core: 8.x +hidden: true diff --git a/tests/src/Kernel/Views/ViewsCacheInvalidationTest.php b/tests/src/Kernel/Views/ViewsCacheInvalidationTest.php new file mode 100644 index 00000000..8041c92d --- /dev/null +++ b/tests/src/Kernel/Views/ViewsCacheInvalidationTest.php @@ -0,0 +1,267 @@ +getParameter('renderer.config'); + $renderer_config['required_cache_contexts'] = []; + $container->setParameter('renderer.config', $renderer_config); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installSchema('node', ['node_access']); + $this->installSchema('search_api', ['search_api_item']); + $this->installSchema('system', ['sequences']); + + $this->installEntitySchema('node'); + $this->installEntitySchema('search_api_task'); + $this->installEntitySchema('user'); + + $this->installConfig([ + 'node', + 'search_api', + 'search_api_test_node_indexing', + 'search_api_test_views', + ]); + + $this->entityTypeManager = $this->container->get('entity_type.manager'); + $this->viewExecutableFactory = $this->container->get('views.executable'); + $this->renderer = $this->container->get('renderer'); + $this->renderCache = $this->container->get('render_cache'); + + + // Use the test search index from the search_api_test_db module. + $this->index = Index::load('test_node_index'); + + // Create some demo content and index it. + foreach (['Cheery' => TRUE, 'Carrot' => TRUE, 'Detritus' => FALSE] as $title => $status) { + $this->createNode($title, $status); + } + $this->index->indexItems(); + + // Create two test users, one with permission to view unpublished entities, + // and one without. + $this->users['no-access'] = $this->createUser(); + $this->users['has-access'] = $this->createUser(['bypass node access']); + } + + public function testDisplayCacheInvalidation() { + // We are testing two variants of the view, one for users that have + // permission to view test entities, and one for users that do not. + // Initially both variants should be uncached. + $this->assertNotCached('no-access'); + $this->assertNotCached('has-access'); + + // Check that the user with the 'bypass node access' permission can see all + // 3 items. + $this->assertViewsResult('has-access', ['Cheery', 'Carrot', 'Detritus']); + + // The result should now be cached for the privileged user. + $this->assertNotCached('no-access'); + $this->assertCached('has-access'); + + // Check that the user without the 'bypass node access' permission can only + // see the published items. + $this->assertViewsResult('no-access', ['Cheery', 'Carrot']); + + // Both results should now be cached. + $this->assertCached('no-access'); + $this->assertCached('has-access'); + + // Add another unpublished item. + $this->createNode('Angua', FALSE); + + // Our search index is not configured to automatically index items, so just + // creating a node should not invalidate the caches. + $this->assertCached('no-access'); + $this->assertCached('has-access'); + + // Index the item, this should invalidate the caches. + $this->index->indexItems(); + $this->assertNotCached('no-access'); + $this->assertNotCached('has-access'); + + // Check that the user without the 'bypass node access' permission can still + // only see the published items. + $this->assertViewsResult('no-access', ['Cheery', 'Carrot']); + $this->assertCached('no-access'); + $this->assertNotCached('has-access'); + + // Check that the user with the 'bypass node access' permission can see all + // 4 items. + $this->assertViewsResult('has-access', ['Angua', 'Cheery', 'Carrot', 'Detritus']); + $this->assertCached('no-access'); + $this->assertCached('has-access'); + + // Grant the permission to 'bypass node access' to the unprivileged user. + $privileged_role = reset($this->users['has-access']->getRoles()); + $this->users['no-access']->addRole($privileged_role); + $this->users['no-access']->save(); + + // The user should now be able to see all 4 items. + $this->assertViewsResult('no-access', ['Angua', 'Cheery', 'Carrot', 'Detritus']); + $this->assertCached('no-access'); + $this->assertCached('has-access'); + } + + /** + * Checks that the view is cached for the given user. + * + * @param string $user_key + * The key of the user for which to perform the check. + */ + protected function assertCached($user_key) { + $this->doAssertCached('assertNotEmpty', $user_key); + } + + /** + * Checks that the view is not cached for the given user. + * + * @param string $user_key + * The key of the user for which to perform the check. + */ + protected function assertNotCached($user_key) { + $this->doAssertCached('assertEmpty', $user_key); + } + + /** + * Checks the cache status of the view for the given user. + * + * @param string $assert_method + * The method to use for asserting that the view is cached or not cached. + * @param int $user_key + * The key of the user for which to perform the check. + */ + protected function doAssertCached($assert_method, $user_key) { + $this->setCurrentUser($this->users[$user_key]); + + // Retrieve the view to render, and apply the required cache contexts that + // are also applied when RendererInterface::renderRoot() is executed. This + // ensures that we pass the same cache information to the render cache as is + // done when actually rendering the HTML root. + $view = $this->getView(); + $render_array = $view->buildRenderable(); + $render_array['#cache']['contexts'] = Cache::mergeContexts($render_array['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']); + + // Retrieve the cached data and perform the assertion. + $cached_data = $this->renderCache->get($render_array); + $this->$assert_method($cached_data); + } + + protected function setCurrentUser($user_key) { + throw new \Exception(__METHOD__); + } + + protected function createNode($title, $status) { + $values = [ + 'title' => $title, + 'status' => $status, + 'type' => 'page', + ]; + $this->nodes[$title] = Node::create($values); + $this->nodes[$title]->save(); + } + +}