diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php index ec133a9..120a963 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php @@ -24,7 +24,9 @@ class EntityTypeBundleInfo implements EntityTypeBundleInfoInterface { /** * Static cache of bundle information. * - * @var array + * @var array[] + * Keys are entity types, and values are arrays of bundle information, keyed + * by bundle name. */ protected $bundleInfo; diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php index 40b0c1f..9ea1c7d 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php @@ -15,8 +15,9 @@ /** * Get the bundle info of all entity types. * - * @return array - * An array of all bundle information. + * @return array[] + * Keys are entity types, and values are arrays of bundle information, keyed + * by bundle name. */ public function getAllBundleInfo(); @@ -26,8 +27,8 @@ public function getAllBundleInfo(); * @param string $entity_type * The entity type. * - * @return array - * Returns the bundle information for the specified entity type. + * @return array[] + * Bundle information, keyed by bundle name. */ public function getBundleInfo($entity_type); diff --git a/core/modules/node/config/schema/node.schema.yml b/core/modules/node/config/schema/node.schema.yml index 84e4d12..b1922d8 100644 --- a/core/modules/node/config/schema/node.schema.yml +++ b/core/modules/node/config/schema/node.schema.yml @@ -45,6 +45,12 @@ search.plugin.node_search: sequence: type: integer label: 'Influence' + restricted_searchable_node_bundles: + type: sequence + label: 'Searchable node bundle names' + sequence: + type: string + label: 'Node bundle name' action.configuration.node_assign_owner_action: type: mapping diff --git a/core/modules/node/src/Plugin/Search/NodeSearch.php b/core/modules/node/src/Plugin/Search/NodeSearch.php index 0fa464e..bdb045f 100644 --- a/core/modules/node/src/Plugin/Search/NodeSearch.php +++ b/core/modules/node/src/Plugin/Search/NodeSearch.php @@ -7,6 +7,7 @@ namespace Drupal\node\Plugin\Search; +use Drupal\Component\Utility\Html; use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\Config; @@ -236,6 +237,8 @@ protected function findResults() { ->addTag('node_access') ->searchExpression($keys, $this->getPluginId()); + $filters = []; + // Handle advanced search filters in the f query string. // \Drupal::request()->query->get('f') is an array that looks like this in // the URL: ?f[]=type:page&f[]=term:27&f[]=term:13&f[]=langcode:en @@ -245,7 +248,6 @@ protected function findResults() { // the keywords string, and some of which are separate conditions. $parameters = $this->getParameters(); if (!empty($parameters['f']) && is_array($parameters['f'])) { - $filters = array(); // Match any query value that is an expected option and a value // separated by ':' like 'term:27'. $pattern = '/^(' . implode('|', array_keys($this->advanced)) . '):([^ ]*)/i'; @@ -255,21 +257,31 @@ protected function findResults() { $filters[$m[1]][$m[2]] = $m[2]; } } + } - // Now turn these into query conditions. This assumes that everything in - // $filters is a known type of advanced search. - foreach ($filters as $option => $matched) { - $info = $this->advanced[$option]; - // Insert additional conditions. By default, all use the OR operator. - $operator = empty($info['operator']) ? 'OR' : $info['operator']; - $where = new Condition($operator); - foreach ($matched as $value) { - $where->condition($info['column'], $value); - } - $query->condition($where); - if (!empty($info['join'])) { - $query->join($info['join']['table'], $info['join']['alias'], $info['join']['condition']); - } + // Restrict node bundle filtering. + if ($this->getRestrictedSearchableNodeBundles()) { + if (isset($filters['type'])) { + $filters['type'] = array_intersect($filters['type'], $this->getRestrictedSearchableNodeBundles()); + } + if (empty($filters['type'])) { + $filters['type'] = $this->getRestrictedSearchableNodeBundles(); + } + } + + // Turn the filters intp query conditions. This assumes that everything in + // $filters is a known type of advanced search. + foreach ($filters as $option => $matched) { + $info = $this->advanced[$option]; + // Insert additional conditions. By default, all use the OR operator. + $operator = empty($info['operator']) ? 'OR' : $info['operator']; + $where = new Condition($operator); + foreach ($matched as $value) { + $where->condition($info['column'], $value); + } + $query->condition($where); + if (!empty($info['join'])) { + $query->join($info['join']['table'], $info['join']['alias'], $info['join']['condition']); } } @@ -566,10 +578,11 @@ public function searchFormAlter(array &$form, FormStateInterface $form_state) { ); // Add node types. - $types = array_map(array('\Drupal\Component\Utility\Html', 'escape'), node_type_get_names()); + $types = $this->buildNodeTypeOptions($this->configuration['restricted_searchable_node_bundles']); $form['advanced']['types-fieldset'] = array( '#type' => 'fieldset', '#title' => t('Types'), + '#access' => count($types) > 1, ); $form['advanced']['types-fieldset']['type'] = array( '#type' => 'checkboxes', @@ -754,6 +767,9 @@ protected function getRankings() { public function defaultConfiguration() { $configuration = array( 'rankings' => array(), + // An array of IDs of searchable node bundles, or an empty array to allow + // content of any bundle to be searched. + 'restricted_searchable_node_bundles' => [], ); return $configuration; } @@ -762,6 +778,12 @@ public function defaultConfiguration() { * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['restricted_searchable_node_bundles'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Restrict searchable content types'), + '#options' => $this->buildNodeTypeOptions(), + '#default_value' => $this->configuration['restricted_searchable_node_bundles'], + ]; // Output form for defining rank factor weights. $form['content_ranking'] = array( '#type' => 'details', @@ -807,6 +829,52 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s unset($this->configuration['rankings'][$var]); } } + $this->configuration['restricted_searchable_node_bundles'] = array_filter($form_state->getValue('restricted_searchable_node_bundles')); + } + + /** + * Builds the node type options. + * + * @param string[] $restricted_node_bundles + * The machine names of the node types to restrict the options by, or an + * empty array for no restrictions. + * + * @return string[] + * Keys are node bundle names and values are bundle labels. + */ + protected function buildNodeTypeOptions(array $restricted_node_bundles = []) { + $bundle_info = $this->entityManager->getBundleInfo('node'); + if ($restricted_node_bundles) { + $bundle_info = array_intersect_key($bundle_info, array_flip($restricted_node_bundles)); + } + $options = array_map(function ($bundle_info) { + return $bundle_info['label']; + }, $bundle_info); + $options = array_map([Html::class, 'escape'], $options); + + return $options; + } + + /** + * Gets the restricted node bundles. + * + * @param string[] $bundles + * An array of IDs of searchable node bundles, or an empty array to allow + * content of any bundle to be searched. + */ + public function setRestrictedSearchableNodeBundles(array $bundles) { + $this->configuration['restricted_searchable_node_bundles'] = $bundles; + } + + /** + * Gets the restricted node bundles. + * + * @return string[] + * An array of IDs of searchable node bundles, or an empty array to allow + * content of any bundle to be searched. + */ + public function getRestrictedSearchableNodeBundles() { + return $this->configuration['restricted_searchable_node_bundles']; } } diff --git a/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php b/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php index 3650a93..bcaa12d 100644 --- a/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php +++ b/core/modules/search/src/Tests/SearchAdvancedSearchFormTest.php @@ -15,20 +15,73 @@ class SearchAdvancedSearchFormTest extends SearchTestBase { /** - * A node to use for testing. + * A searchable "page" node. * * @var \Drupal\node\NodeInterface */ - protected $node; + protected $searchablePageNode; + /** + * A searchable "article" node. + * + * @var \Drupal\node\NodeInterface + */ + protected $searchableArticleNode; + + /** + * A node that isn't searchable. + * + * @var \Drupal\node\NodeInterface + */ + protected $nonSearchableNode; + + /** + * The title of all nodes. + * + * @var string + */ + protected $nodeTitle = 'The dog ate an apple'; + + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); + /** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */ + $config_factory = $this->container->get('config.factory'); + $config_factory->getEditable('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save(); // Create and login user. $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes')); $this->drupalLogin($test_user); - // Create initial node. - $this->node = $this->drupalCreateNode(); + // Create nodes. They must have the same titles, so their visibility in the + // result set does not depend on the search query, but on the search + // filters. + $this->searchablePageNode = $this->drupalCreateNode([ + 'type' => 'page', + 'title' => $this->nodeTitle, + ]); + $this->searchableArticleNode = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => $this->nodeTitle, + ]); + $this->createContentType([ + 'type' => 'non-searchable', + ]); + $this->nonSearchableNode = $this->drupalCreateNode([ + 'type' => 'non-searchable', + 'title' => $this->nodeTitle, + ]); + + // Restrict the searchable node bundles. + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ + $entity_type_manager = $this->container->get('entity_type.manager'); + /** @var \Drupal\search\SearchPageInterface $search_page */ + $search_page = $entity_type_manager->getStorage('search_page')->load('node_search'); + /** @var \Drupal\node\Plugin\Search\NodeSearch $search_page_plugin */ + $search_page_plugin = $search_page->getPlugin(); + $search_page_plugin->setRestrictedSearchableNodeBundles(['article', 'page']); + $search_page->save(); // First update the index. This does the initial processing. $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); @@ -43,30 +96,51 @@ protected function setUp() { * Tests advanced search by node type. */ function testNodeType() { - // Verify some properties of the node that was created. - $this->assertTrue($this->node->getType() == 'page', 'Node type is Basic page.'); - $dummy_title = 'Lorem ipsum'; - $this->assertNotEqual($dummy_title, $this->node->label(), "Dummy title doesn't equal node title."); + $dummy_node_title = 'The puppy ran into the mud'; + + $searchable_page_node_url = sprintf('node/%d', $this->searchablePageNode->id()); + $searchable_article_node_url = sprintf('node/%d', $this->searchableArticleNode->id()); + $non_searchable_node_url = sprintf('node/%d', $this->nonSearchableNode->id()); // Search for the dummy title with a GET query. - $this->drupalGet('search/node', array('query' => array('keys' => $dummy_title))); - $this->assertNoText($this->node->label(), 'Basic page node is not found with dummy title.'); + $this->drupalGet('search/node', array('query' => array('keys' => $dummy_node_title))); + $this->assertNoText($this->searchablePageNode->label(), 'Basic page node is not found with dummy title.'); // Search for the title of the node with a GET query. - $this->drupalGet('search/node', array('query' => array('keys' => $this->node->label()))); - $this->assertText($this->node->label(), 'Basic page node is found with GET query.'); + $this->drupalGet('search/node', [ + 'query' => [ + 'keys' => $this->nodeTitle, + 'f' => [ + // Try and force the search to include non-searchable nodes, and + // confirm that this does not work. + 'type:non-searchable' + ], + 'advanced-form' => 1, + ], + ]); + $this->assertLinkByHref($searchable_page_node_url); + $this->assertLinkByHref($searchable_article_node_url); + $this->assertNoLinkByHref($non_searchable_node_url); // Search for the title of the node with a POST query. - $edit = array('or' => $this->node->label()); + $edit = [ + 'or' => $this->nodeTitle, + ]; $this->drupalPostForm('search/node', $edit, t('Advanced search')); - $this->assertText($this->node->label(), 'Basic page node is found with POST query.'); + $this->assertLinkByHref($searchable_page_node_url); + $this->assertLinkByHref($searchable_article_node_url); + $this->assertNoLinkByHref($non_searchable_node_url); // Search by node type. $this->drupalPostForm('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search')); - $this->assertText($this->node->label(), 'Basic page node is found with POST query and type:page.'); + $this->assertLinkByHref($searchable_page_node_url); + $this->assertNoLinkByHref($searchable_article_node_url); + $this->assertNoLinkByHref($non_searchable_node_url); $this->drupalPostForm('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search')); - $this->assertText('search yielded no results', 'Article node is not found with POST query and type:article.'); + $this->assertNoLinkByHref($searchable_page_node_url); + $this->assertLinkByHref($searchable_article_node_url); + $this->assertNoLinkByHref($non_searchable_node_url); } /**