diff --git a/core/modules/help_topics/config/optional/block.block.seven_help_search.yml b/core/modules/help_topics/config/optional/block.block.seven_help_search.yml new file mode 100644 index 0000000000000000000000000000000000000000..d44236ce9d6e9914429a5a1dfa1228dbe24ff9d6 --- /dev/null +++ b/core/modules/help_topics/config/optional/block.block.seven_help_search.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + enforced: + config: + - search.page.help_search + module: + - search + - system + theme: + - seven +id: seven_help_search +theme: seven +region: help +weight: -4 +provider: null +plugin: search_form_block +settings: + id: search_form_block + label: 'Search help' + provider: search + label_display: visible + page_id: help_search +visibility: + request_path: + id: request_path + pages: /admin/help + negate: false + context_mapping: { } diff --git a/core/modules/help_topics/config/optional/search.page.help_search.yml b/core/modules/help_topics/config/optional/search.page.help_search.yml new file mode 100644 index 0000000000000000000000000000000000000000..5ae158e5ea5f24285b3332140f679d68ad22c1af --- /dev/null +++ b/core/modules/help_topics/config/optional/search.page.help_search.yml @@ -0,0 +1,11 @@ +langcode: en +status: true +dependencies: + module: + - help_topics +id: help_search +label: Help +path: help +weight: 0 +plugin: help_search +configuration: { } diff --git a/core/modules/help_topics/config/schema/help_topics.schema.yml b/core/modules/help_topics/config/schema/help_topics.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..77f1e80a05b5b830e91408a7e9b3f91dcc045fd9 --- /dev/null +++ b/core/modules/help_topics/config/schema/help_topics.schema.yml @@ -0,0 +1,3 @@ +search.plugin.help_search: + type: sequence + label: 'Help search' diff --git a/core/modules/help_topics/help_topics.install b/core/modules/help_topics/help_topics.install new file mode 100644 index 0000000000000000000000000000000000000000..ef3e343da25aa986cb046d9f94c4ac3f392cec1c --- /dev/null +++ b/core/modules/help_topics/help_topics.install @@ -0,0 +1,51 @@ +<?php + +/** + * @file + * Install and uninstall functions for help_topics module. + */ + +/** + * Implements hook_schema(). + */ +function help_topics_schema() { + $schema['help_search_items'] = [ + 'description' => 'Stores information about indexed help search items', + 'fields' => [ + 'sid' => [ + 'description' => 'Numeric index of this item in the search index', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'section_plugin_id' => [ + 'description' => 'The help section the item comes from', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + 'permission' => [ + 'description' => 'The permission needed to view this item', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + 'topic_id' => [ + 'description' => 'The topic ID of the item', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ], + ], + 'primary key' => ['sid'], + 'indexes' => [ + 'section_plugin_id' => ['section_plugin_id'], + 'topic_id' => ['topic_id'], + ], + ]; + + return $schema; +} diff --git a/core/modules/help_topics/help_topics.module b/core/modules/help_topics/help_topics.module index 4e63e952c359bc9b86b9f559e3973cae87fd6e43..9651d605712ceaa249f2a5a026e5e3641da152cf 100644 --- a/core/modules/help_topics/help_topics.module +++ b/core/modules/help_topics/help_topics.module @@ -15,10 +15,12 @@ function help_topics_help($route_name, RouteMatchInterface $route_match) { switch ($route_name) { case 'help.page.help_topics': $help_home = Url::fromRoute('help.main')->toString(); - $locale_help = (\Drupal::moduleHandler()->moduleExists('locale')) ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#'; + $module_handler = \Drupal::moduleHandler(); + $locale_help = ($module_handler->moduleExists('locale')) ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#'; + $search_help = ($module_handler->moduleExists('search')) ? Url::fromRoute('help.page', ['name' => 'search'])->toString() : '#'; $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Help Topics module adds module- and theme-provided help topics to the module overviews from the core Help module. For more information, see the <a href=":online">online documentation for the Help Topics module</a>.', [':online' => 'https://www.drupal.org/modules/help_topics']) . '</p>'; + $output .= '<p>' . t('The Help Topics module adds module- and theme-provided help topics to the module overviews from the core Help module. If the core Search module is enabled, these topics are also searchable. For more information, see the <a href=":online">online documentation for the Help Topics module</a>.', [':online' => 'https://www.drupal.org/modules/help_topics']) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Viewing help topics') . '</dt>'; @@ -27,6 +29,8 @@ function help_topics_help($route_name, RouteMatchInterface $route_match) { $output .= '<dd>' . t("Modules and themes can provide help topics as Twig-file-based plugins in a project sub-directory called <em>help_topics</em>; plugin meta-data is provided in YAML front matter within each Twig file. Plugin-based help topics provided by modules and themes will automatically be updated when a module or theme is updated. Use the plugins in <em>core/modules/help_topics/help_topics</em> as a guide when writing and formatting a help topic plugin for your theme or module.") . '</dd>'; $output .= '<dt>' . t('Translating help topics') . '</dt>'; $output .= '<dd>' . t('The title and body text of help topics provided by contributed modules and themes are translatable using the <a href=":locale_help">Interface Translation module</a>. Topics provided by custom modules and themes are also translatable if they have been viewed at least once in a non-English language, which triggers putting their translatable text into the translation database.', [':locale_help' => $locale_help]) . '</dd>'; + $output .= '<dt>' . t('Configuring help search') . '</dt>'; + $output .= '<dd>' . t('To search help, you will need to install the core Search module, configure a search page, and add a search block to the Help page or another administrative page. (A search page is provided automatically, and if you use the core Seven administrative theme, a help search block is shown on the main Help page.) Then users with search permissions, and permission to view help, will be able to search help. See the <a href=":search_help">Search module help page</a> for more information.', [':search_help' => $search_help]) . '</dd>'; $output .= '</dl>'; return ['#markup' => $output]; @@ -51,3 +55,30 @@ function help_topics_theme() { ], ]; } + +/** + * Implements hook_modules_uninstalled(). + */ +function help_topics_modules_uninstalled(array $modules) { + // Early return if search is not installed or if we're uninstalling this + // module. + if (!\Drupal::hasService('plugin.manager.search') || + in_array('help_topics', $modules)) { + return; + } + $search_plugin_manager = \Drupal::service('plugin.manager.search'); + if ($search_plugin_manager->hasDefinition('help_search')) { + // Ensure that topics for extensions that have been uninstalled are removed. + $help_search = $search_plugin_manager->createInstance('help_search'); + $help_search->updateTopicList(); + } +} + +/** + * Implements hook_themes_uninstalled(). + */ +function help_topics_themes_uninstalled(array $themes) { + // Use the same code as module uninstall to ensure that theme help is removed + // when a theme is uninstalled. + help_topics_modules_uninstalled([]); +} diff --git a/core/modules/help_topics/help_topics.services.yml b/core/modules/help_topics/help_topics.services.yml index 2262c6912efb8bf78a53ad9f08ef0ce70c5fb84c..47fe581b051efdcd6dd0117666c179c770c083c5 100644 --- a/core/modules/help_topics/help_topics.services.yml +++ b/core/modules/help_topics/help_topics.services.yml @@ -15,3 +15,11 @@ services: tags: - { name: twig.loader, priority: -200 } public: false + help_topics.plugin.manager.help_section: + class: Drupal\help_topics\HelpSectionManager + decorates: plugin.manager.help_section + parent: plugin.manager.help_section + calls: + - [setSearchManager, ['@?plugin.manager.search']] + tags: + - { name: plugin_manager_cache_clear } diff --git a/core/modules/help_topics/help_topics/help_topics.help_topic_search.html.twig b/core/modules/help_topics/help_topics/help_topics.help_topic_search.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..13ce68fa74f996236f0f7c66a6e61e29789035e6 --- /dev/null +++ b/core/modules/help_topics/help_topics/help_topics.help_topic_search.html.twig @@ -0,0 +1,20 @@ +--- +label: 'Configuring help search' +top_level: true +--- +<h2>{% trans %}Goal{% endtrans %}</h2> +<p>{% trans %}Set up your site so that users can search for help.{% endtrans %}</p> +<h2>{% trans %}Steps{% endtrans %}</h2> +<ol> + <li>{% trans %}From the <em>Extend</em> administrative page (<em>admin/modules</em>), verify that the Search, Help, Help Topics, and Block modules are installed (or install them if they are not already installed).{% endtrans %}</li> + <li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> > <em>Search and metadata</em> > <em>Search pages</em> (<em>admin/config/search/pages</em>).{% endtrans %}</li> + <li>{% trans %}Verify that a Help search page is listed in the <em>Search pages</em> section. If not, add a new page of type <em>Help</em>.{% endtrans %}</li> + <li>{% trans %}Check the indexing status of the Help search page. If it is not fully indexed, run Cron until indexing is complete.{% endtrans %}</li> + <li>{% trans %}In the future, you can click <em>Rebuild search index</em> on this page, or clear the site cache, in order to force help topic text to be reindexed for searching. This should be done whenever a module, theme, language, or string translation is updated.{% endtrans %}</li> + <li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> > <em>Block layout</em> (<em>admin/structure/block</em>).{% endtrans %}</li> + <li>{% trans %}Click the link for your administrative theme (such as the core Seven theme), near the top of the page.{% endtrans %}</li> + <li>{% trans %}See if there is already a help search block placed in the Help region of your administrative theme.{% endtrans %}</li> + <li>{% trans %}If there is not already a help search block placed, click <em>Place block</em>, and then click <em>Place block</em> in the row next to <em>Search form</em> in the dialog. Choose <em>Help</em> as the search page for the block, and in the <em>Visibility</em> > <em>Pages</em> field, enter <em>/admin/help</em> to make the search form only visible on the main <em>Help</em> page.{% endtrans %}</li> + <li>{% trans %}Fill in the rest of the form and save the block.{% endtrans %}</li> + <li>{% trans %}Visit the main <em>Help</em> page (<em>admin/help</em>) to verify that the search block is visible, and try a search.{% endtrans %}</li> +</ol> diff --git a/core/modules/help_topics/src/HelpSectionManager.php b/core/modules/help_topics/src/HelpSectionManager.php new file mode 100644 index 0000000000000000000000000000000000000000..44323ac7cca99d0cbeecc4ee7f61fdb417ef81b3 --- /dev/null +++ b/core/modules/help_topics/src/HelpSectionManager.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\help_topics; + +use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\help\HelpSectionManager as CoreHelpSectionManager; + +/** + * Decorates the Help Section plugin manager to provide help topic search. + */ +class HelpSectionManager extends CoreHelpSectionManager { + + /** + * The search manager. + * + * @var \Drupal\Component\Plugin\PluginManagerInterface + */ + protected $searchManager; + + /** + * Sets the search manager. + * + * @param \Drupal\Component\Plugin\PluginManagerInterface|null $search_manager + * The search manager if the Search module is installed. + */ + public function setSearchManager(PluginManagerInterface $search_manager = NULL) { + $this->searchManager = $search_manager; + } + + /** + * {@inheritdoc} + */ + public function clearCachedDefinitions() { + parent::clearCachedDefinitions(); + if ($this->searchManager && $this->searchManager->hasDefinition('help_search') && $this->moduleHandler->moduleExists('help_topics')) { + // Rebuild the index on cache clear so that new help topics are indexed + // and any changes due to help topics edits or translation changes are + // picked up. + $help_search = $this->searchManager->createInstance('help_search'); + $help_search->markForReindex(); + } + } + +} diff --git a/core/modules/help_topics/src/HelpTopicDiscovery.php b/core/modules/help_topics/src/HelpTopicDiscovery.php index 6ff7a10148c055ce0585fa2ae149ef95a4295f54..6f6240d43197eb97de0d456b63bfe0387f23f37b 100644 --- a/core/modules/help_topics/src/HelpTopicDiscovery.php +++ b/core/modules/help_topics/src/HelpTopicDiscovery.php @@ -108,7 +108,7 @@ public function findAll() { } $data = [ // The plugin ID is derived from the filename. The extension - // '.html.twig' is removed + // '.html.twig' is removed. 'id' => $plugin_id, 'provider' => $file_name_provider, 'class' => HelpTopicTwig::class, @@ -131,15 +131,18 @@ public function findAll() { } $data[$key] = $value; break; + case 'top_level': if (!is_bool($value)) { throw new DiscoveryException("$file contains invalid value for 'top_level' key, the value must be a Boolean"); } $data[$key] = $value; break; + case 'label': $data[$key] = new TranslatableMarkup($value); break; + default: throw new DiscoveryException("$file contains invalid key='$key'"); } diff --git a/core/modules/help_topics/src/Plugin/HelpSection/HelpTopicSection.php b/core/modules/help_topics/src/Plugin/HelpSection/HelpTopicSection.php index c68f5ff11f777413dc5aacd64dd5bbd420f5b778..c604d650ae466ff57483dfce924553966af4d45d 100644 --- a/core/modules/help_topics/src/Plugin/HelpSection/HelpTopicSection.php +++ b/core/modules/help_topics/src/Plugin/HelpSection/HelpTopicSection.php @@ -3,10 +3,17 @@ namespace Drupal\help_topics\Plugin\HelpSection; use Drupal\Core\Cache\CacheableMetadata; +use Drupal\help_topics\SearchableHelpInterface; use Drupal\help_topics\HelpTopicPluginInterface; use Drupal\help_topics\HelpTopicPluginManagerInterface; +use Drupal\Core\Language\LanguageDefault; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Render\RenderContext; +use Drupal\Core\StringTranslation\TranslationManager; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -25,7 +32,7 @@ * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. */ -class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface { +class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface, SearchableHelpInterface { /** * The plugin manager. @@ -43,9 +50,39 @@ class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactory /** * The merged top level help topic plugins cache metadata. + * + * @var \Drupal\Core\Cache\CacheableMetadata */ protected $cacheableMetadata; + /** + * The Renderer service to format the username and node. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * The default language object. + * + * @var \Drupal\Core\Language\LanguageDefault + */ + protected $defaultLanguage; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The string translation service. + * + * @var \Drupal\Core\StringTranslation\TranslationManager + */ + protected $stringTranslation; + /** * Constructs a HelpTopicSection object. * @@ -57,10 +94,23 @@ class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactory * The plugin implementation definition. * @param \Drupal\help_topics\HelpTopicPluginManagerInterface $plugin_manager * The help topic plugin manager service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. + * @param \Drupal\Core\Language\LanguageDefault $default_language + * The default language object. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\StringTranslation\TranslationManager $translation_manager + * The translation manager. We are using a method that doesn't exist on an + * interface, so require this class. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, HelpTopicPluginManagerInterface $plugin_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, HelpTopicPluginManagerInterface $plugin_manager, RendererInterface $renderer, LanguageDefault $default_language, LanguageManagerInterface $language_manager, TranslationManager $translation_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->pluginManager = $plugin_manager; + $this->renderer = $renderer; + $this->defaultLanguage = $default_language; + $this->languageManager = $language_manager; + $this->translationManager = $translation_manager; } /** @@ -71,7 +121,11 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('plugin.manager.help_topic') + $container->get('plugin.manager.help_topic'), + $container->get('renderer'), + $container->get('language.default'), + $container->get('language_manager'), + $container->get('string_translation') ); } @@ -110,7 +164,7 @@ public function listTopics() { * Gets the top level help topic plugins. * * @return \Drupal\help_topics\HelpTopicPluginInterface[] - * The top level help topic plugins + * The top level help topic plugins. */ protected function getPlugins() { if (!isset($this->topLevelPlugins)) { @@ -137,6 +191,79 @@ protected function getPlugins() { return $this->topLevelPlugins; } + /** + * {@inheritdoc} + */ + public function listSearchableTopics() { + $definitions = $this->pluginManager->getDefinitions(); + return array_column($definitions, 'id'); + } + + /** + * {@inheritdoc} + */ + public function renderTopicForSearch($topic_id, LanguageInterface $language) { + $plugin = $this->pluginManager->createInstance($topic_id); + if (!$plugin) { + return []; + } + + // We are rendering this topic for search indexing or search results, + // possibly in a different language than the current language. The topic + // title and body come from translatable things in the Twig template, so we + // need to set the default language to the desired language, render them, + // then restore the default language so we do not affect other cron + // processes. Also, just in case there is an exception, wrap the whole + // thing in a try/finally block, and reset the language in the finally part. + $old_language = $this->defaultLanguage->get(); + try { + if ($old_language->getId() !== $language->getId()) { + $this->defaultLanguage->set($language); + $this->translationManager->setDefaultLangcode($language->getId()); + $this->languageManager->reset(); + } + $topic = []; + + // Render the title in this language. + $title_build = [ + 'title' => [ + '#type' => '#markup', + '#markup' => $plugin->getLabel(), + ], + ]; + $topic['title'] = $this->renderer->renderPlain($title_build); + $cacheable_metadata = CacheableMetadata::createFromRenderArray($title_build); + + // Render the body in this language. For this, we need to set up a render + // context, because the Twig plugins that provide the body assumes one + // is present. + $context = new RenderContext(); + $build = [ + 'body' => $this->renderer->executeInRenderContext($context, [$plugin, 'getBody']), + ]; + $topic['text'] = $this->renderer->renderPlain($build); + $cacheable_metadata->addCacheableDependency(CacheableMetadata::createFromRenderArray($build)); + $cacheable_metadata->addCacheableDependency($plugin); + if (!$context->isEmpty()) { + $cacheable_metadata->addCacheableDependency($context->pop()); + } + + // Add the other information. + $topic['url'] = $plugin->toUrl(); + $topic['cacheable_metadata'] = $cacheable_metadata; + } + finally { + // Restore the original language. + if ($old_language->getId() !== $language->getId()) { + $this->defaultLanguage->set($old_language); + $this->translationManager->setDefaultLangcode($old_language->getId()); + $this->languageManager->reset(); + } + } + + return $topic; + } + /** * Gets the merged CacheableMetadata for all the top level help topic plugins. * diff --git a/core/modules/help_topics/src/Plugin/Search/HelpSearch.php b/core/modules/help_topics/src/Plugin/Search/HelpSearch.php new file mode 100644 index 0000000000000000000000000000000000000000..fc5daa2e8830a2902eb4cf96d699bfea5740dc18 --- /dev/null +++ b/core/modules/help_topics/src/Plugin/Search/HelpSearch.php @@ -0,0 +1,489 @@ +<?php + +namespace Drupal\help_topics\Plugin\Search; + +use Drupal\Core\Access\AccessibleInterface; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Config\Config; +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Database\Query\PagerSelectExtender; +use Drupal\Core\Database\StatementInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\State\StateInterface; +use Drupal\help\HelpSectionManager; +use Drupal\help_topics\SearchableHelpInterface; +use Drupal\search\Plugin\SearchIndexingInterface; +use Drupal\search\Plugin\SearchPluginBase; +use Drupal\search\SearchQuery; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Handles searching for help using the Search module index. + * + * Help items are indexed if their HelpSection plugin implements + * \Drupal\help\HelpSearchInterface. + * + * @see \Drupal\help\HelpSearchInterface + * @see \Drupal\help\HelpSectionPluginInterface + * + * @SearchPlugin( + * id = "help_search", + * title = @Translation("Help") + * ) + * + * @internal + * Help Topic is currently experimental and should only be leveraged by + * experimental modules and development releases of contributed modules. + * See https://www.drupal.org/core/experimental for more information. + */ +class HelpSearch extends SearchPluginBase implements AccessibleInterface, SearchIndexingInterface { + + /** + * The current database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * A config object for 'search.settings'. + * + * @var \Drupal\Core\Config\Config + */ + protected $searchSettings; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The Drupal account to use for checking for access to search. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $account; + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * The state object. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * The help section plugin manager. + * + * @var \Drupal\help\HelpSectionManager + */ + protected $helpSectionManager; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('database'), + $container->get('config.factory')->get('search.settings'), + $container->get('language_manager'), + $container->get('messenger'), + $container->get('current_user'), + $container->get('state'), + $container->get('plugin.manager.help_section') + ); + } + + /** + * Constructs a \Drupal\help_search\Plugin\Search\HelpSearch object. + * + * @param array $configuration + * Configuration for the plugin. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Database\Connection $database + * The current database connection. + * @param \Drupal\Core\Config\Config $search_settings + * A config object for 'search.settings'. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. + * @param \Drupal\Core\Session\AccountInterface $account + * The $account object to use for checking for access to view help. + * @param \Drupal\Core\State\StateInterface $state + * The state object. + * @param \Drupal\help\HelpSectionManager $help_section_manager + * The help section manager. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, Config $search_settings, LanguageManagerInterface $language_manager, MessengerInterface $messenger, AccountInterface $account, StateInterface $state, HelpSectionManager $help_section_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->database = $database; + $this->searchSettings = $search_settings; + $this->languageManager = $language_manager; + $this->messenger = $messenger; + $this->account = $account; + $this->state = $state; + $this->helpSectionManager = $help_section_manager; + } + + /** + * {@inheritdoc} + */ + public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'access administration pages'); + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function execute() { + if ($this->isSearchExecutable()) { + $results = $this->findResults(); + + if ($results) { + return $this->prepareResults($results); + } + } + + return []; + } + + /** + * Finds the search results. + * + * @return \Drupal\Core\Database\StatementInterface|null + * Results from search query execute() method, or NULL if the search + * failed. + */ + protected function findResults() { + // We need to check access for the current user to see the topics that + // could be returned by search. Each entry in the help_search_items + // database has an optional permission that comes from the HelpSection + // plugin, in addition to the generic 'access administration pages' + // permission. In order to enforce these permissions so only topics that + // the current user has permission to view are selected by the query, make + // a list of the permission strings and pre-check those permissions. + $this->addCacheContexts(['user.permissions']); + if (!$this->account->hasPermission('access administration pages')) { + return NULL; + } + $permissions = $this->database + ->select('help_search_items', 'hsi') + ->distinct() + ->fields('hsi', ['permission']) + ->condition('permission', '', '<>') + ->execute() + ->fetchCol(); + $denied_permissions = array_filter($permissions, function ($permission) { + return !$this->account->hasPermission($permission); + }); + + $query = $this->database + ->select('search_index', 'i') + // Restrict the search to the current interface language. + ->condition('i.langcode', $this->languageManager->getCurrentLanguage()->getId()) + ->extend(SearchQuery::class) + ->extend(PagerSelectExtender::class); + $query->innerJoin('help_search_items', 'hsi', 'i.sid = hsi.sid AND i.type = :type', [':type' => $this->getType()]); + if ($denied_permissions) { + $query->condition('hsi.permission', $denied_permissions, 'NOT IN'); + } + $query->searchExpression($this->getKeywords(), $this->getType()); + + $find = $query + ->fields('i', ['langcode']) + ->fields('hsi', ['section_plugin_id', 'topic_id']) + // Since SearchQuery makes these into GROUP BY queries, if we add + // a field, for PostgreSQL we also need to make it an aggregate or a + // GROUP BY. In this case, we want GROUP BY. + ->groupBy('i.langcode') + ->groupBy('hsi.section_plugin_id') + ->groupBy('hsi.topic_id') + ->limit(10) + ->execute(); + + // Check query status and set messages if needed. + $status = $query->getStatus(); + + if ($status & SearchQuery::EXPRESSIONS_IGNORED) { + $this->messenger->addWarning($this->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', ['@count' => $this->searchSettings->get('and_or_limit')])); + } + + if ($status & SearchQuery::LOWER_CASE_OR) { + $this->messenger->addWarning($this->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.')); + } + + if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) { + $this->messenger->addWarning($this->formatPlural($this->searchSettings->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.')); + } + + return $find; + } + + /** + * Prepares search results for display. + * + * @param \Drupal\Core\Database\StatementInterface $found + * Results found from a successful search query execute() method. + * + * @return array + * List of search result render arrays, with links, snippets, etc. + */ + protected function prepareResults(StatementInterface $found) { + $results = []; + $plugins = []; + $languages = []; + $keys = $this->getKeywords(); + foreach ($found as $item) { + $section_plugin_id = $item->section_plugin_id; + if (!isset($plugins[$section_plugin_id])) { + $plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id); + } + if ($plugins[$section_plugin_id]) { + $langcode = $item->langcode; + if (!isset($languages[$langcode])) { + $languages[$langcode] = $this->languageManager->getLanguage($item->langcode); + } + $topic = $plugins[$section_plugin_id]->renderTopicForSearch($item->topic_id, $languages[$langcode]); + if ($topic) { + if (isset($topic['cacheable_metadata'])) { + $this->addCacheableDependency($topic['cacheable_metadata']); + } + $results[] = [ + 'title' => $topic['title'], + 'link' => $topic['url']->toString(), + 'snippet' => search_excerpt($keys, $topic['title'] . ' ' . $topic['text'], $item->langcode), + 'langcode' => $item->langcode, + ]; + } + } + } + + return $results; + } + + /** + * {@inheritdoc} + */ + public function updateIndex() { + // Update the list of items to be indexed. + $this->updateTopicList(); + + // Find some items that need to be updated. Start with ones that have + // never been indexed. + $limit = (int) $this->searchSettings->get('index.cron_limit'); + + $query = $this->database->select('help_search_items', 'hsi'); + $query->fields('hsi', ['sid', 'section_plugin_id', 'topic_id']); + $query->leftJoin('search_dataset', 'sd', 'sd.sid = hsi.sid AND sd.type = :type', [':type' => $this->getType()]); + $query->where('sd.sid IS NULL'); + $query->groupBy('hsi.sid') + ->groupBy('hsi.section_plugin_id') + ->groupBy('hsi.topic_id') + ->range(0, $limit); + $items = $query->execute()->fetchAll(); + + // If there is still space in the indexing limit, index items that have + // been indexed before, but are currently marked as needing a re-index. + if (count($items) < $limit) { + $query = $this->database->select('help_search_items', 'hsi'); + $query->fields('hsi', ['sid', 'section_plugin_id', 'topic_id']); + $query->leftJoin('search_dataset', 'sd', 'sd.sid = hsi.sid AND sd.type = :type', [':type' => $this->getType()]); + $query->condition('sd.reindex', 0, '<>'); + $query->groupBy('hsi.sid') + ->groupBy('hsi.section_plugin_id') + ->groupBy('hsi.topic_id') + ->range(0, $limit - count($items)); + $items = $items + $query->execute()->fetchAll(); + } + + // Index the items we have chosen, in all available languages. + $language_list = $this->languageManager->getLanguages(LanguageInterface::STATE_CONFIGURABLE); + $section_plugins = []; + + foreach ($items as $item) { + $section_plugin_id = $item->section_plugin_id; + if (!isset($section_plugins[$section_plugin_id])) { + $section_plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id); + } + + if (!$section_plugins[$section_plugin_id]) { + $this->removeItemsFromIndex($item->sid); + continue; + } + + $section_plugin = $section_plugins[$section_plugin_id]; + search_index_clear($this->getType(), $item->sid); + foreach ($language_list as $langcode => $language) { + $topic = $section_plugin->renderTopicForSearch($item->topic_id, $language); + if ($topic) { + // Index the title plus body text. + $text = '<h1>' . $topic['title'] . '</h1>' . "\n" . $topic['text']; + search_index($this->getType(), $item->sid, $langcode, $text); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function indexClear() { + search_index_clear($this->getType()); + } + + /** + * Rebuilds the database table containing topics to be indexed. + */ + public function updateTopicList() { + // Start by fetching the existing list, so we can remove items not found + // at the end. + $old_list = $this->database->select('help_search_items', 'hsi') + ->fields('hsi', ['sid', 'topic_id', 'section_plugin_id', 'permission']) + ->execute(); + $old_list_ordered = []; + $sids_to_remove = []; + foreach ($old_list as $item) { + $old_list_ordered[$item->section_plugin_id][$item->topic_id] = $item; + $sids_to_remove[$item->sid] = $item->sid; + } + + $section_plugins = $this->helpSectionManager->getDefinitions(); + foreach ($section_plugins as $section_plugin_id => $section_plugin_definition) { + $plugin = $this->getSectionPlugin($section_plugin_id); + if (!$plugin) { + continue; + } + $permission = $section_plugin_definition['permission'] ?? ''; + foreach ($plugin->listSearchableTopics() as $topic_id) { + if (isset($old_list_ordered[$section_plugin_id][$topic_id])) { + $old_item = $old_list_ordered[$section_plugin_id][$topic_id]; + if ($old_item->permission == $permission) { + // Record has not changed. + unset($sids_to_remove[$old_item->sid]); + continue; + } + + // Permission has changed, update record. + $this->database->update('help_search_items') + ->condition('sid', $old_item->sid) + ->fields(['permission' => $permission]) + ->execute(); + unset($sids_to_remove[$old_item->sid]); + continue; + } + + // New record, create it. + $this->database->insert('help_search_items') + ->fields([ + 'section_plugin_id' => $section_plugin_id, + 'permission' => $permission, + 'topic_id' => $topic_id, + ]) + ->execute(); + } + } + + // Remove remaining items from the index. + $this->removeItemsFromIndex($sids_to_remove); + } + + /** + * {@inheritdoc} + */ + public function markForReindex() { + $this->updateTopicList(); + search_mark_for_reindex($this->getType()); + } + + /** + * {@inheritdoc} + */ + public function indexStatus() { + $this->updateTopicList(); + $total = $this->database->select('help_search_items', 'hsi') + ->countQuery() + ->execute() + ->fetchField(); + + $query = $this->database->select('help_search_items', 'hsi'); + $query->addExpression('COUNT(DISTINCT(hsi.sid))'); + $query->leftJoin('search_dataset', 'sd', 'hsi.sid = sd.sid AND sd.type = :type', [':type' => $this->getType()]); + $condition = new Condition('OR'); + $condition->condition('sd.reindex', 0, '<>') + ->isNull('sd.sid'); + $query->condition($condition); + $remaining = $query->execute()->fetchField(); + + return [ + 'remaining' => $remaining, + 'total' => $total, + ]; + } + + /** + * Removes an item or items from the search index. + * + * @param int|int[] $sids + * Search ID (sid) of item or items to remove. + */ + protected function removeItemsFromIndex($sids) { + $sids = (array) $sids; + + // Remove items from our table in batches of 100, to avoid problems + // with having too many placeholders in database queries. + foreach (array_chunk($sids, 100) as $this_list) { + $this->database->delete('help_search_items') + ->condition('sid', $this_list, 'IN') + ->execute(); + } + // Remove items from the search tables individually, as there is no bulk + // function to delete items from the search index. + foreach ($sids as $sid) { + search_index_clear($this->getType(), $sid); + } + } + + /** + * Instantiates a help section plugin and verifies it is searchable. + * + * @param string $section_plugin_id + * Type of plugin to instantiate. + * + * @return \Drupal\help_topics\SearchableHelpInterface|false + * Plugin object, or FALSE if it is not searchable. + */ + protected function getSectionPlugin($section_plugin_id) { + /** @var \Drupal\help\HelpSectionPluginInterface $section_plugin */ + $section_plugin = $this->helpSectionManager->createInstance($section_plugin_id); + // Intentionally return boolean to allow caching of results. + return $section_plugin instanceof SearchableHelpInterface ? $section_plugin : FALSE; + } + +} diff --git a/core/modules/help_topics/src/SearchableHelpInterface.php b/core/modules/help_topics/src/SearchableHelpInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..06fdb939c1e3a7269b37cf0549faf1d0e2870e91 --- /dev/null +++ b/core/modules/help_topics/src/SearchableHelpInterface.php @@ -0,0 +1,46 @@ +<?php + +namespace Drupal\help_topics; + +use Drupal\Core\Language\LanguageInterface; + +/** + * Provides an interface for a HelpSection plugin that also supports search. + * + * @see \Drupal\help\HelpSectionPluginInterface + * + * @internal + * Help Topic is currently experimental and should only be leveraged by + * experimental modules and development releases of contributed modules. + * See https://www.drupal.org/core/experimental for more information. + */ +interface SearchableHelpInterface { + + /** + * Returns the IDs of topics that should be indexed for searching. + * + * @return string[] + * An array of topic IDs that should be searchable. IDs need to be + * unique within this HelpSection plugin. + */ + public function listSearchableTopics(); + + /** + * Renders one topic for search indexing or search results. + * + * @param string $topic_id + * The ID of the topic to be indexed. + * @param \Drupal\Core\Language\LanguageInterface $language + * The language to render the topic in. + * + * @return array + * An array of information about the topic, with elements: + * - title: The title of the topic in this language. + * - text: The text of the topic in this language. + * - url: The URL of the topic as a \Drupal\Core\Url object. + * - cacheable_metadata: (optional) An object to add as a cache dependency + * if this topic is shown in search results. + */ + public function renderTopicForSearch($topic_id, LanguageInterface $language); + +} diff --git a/core/modules/help_topics/tests/modules/help_topics_test/help_topics/help_topics_test.test.html.twig b/core/modules/help_topics/tests/modules/help_topics_test/help_topics/help_topics_test.test.html.twig index 0a1453602f035fbc552fa189d6282aee0ee63d8d..1b324a144711f188e17e37d5626e5eb122df0fbc 100644 --- a/core/modules/help_topics/tests/modules/help_topics_test/help_topics/help_topics_test.test.html.twig +++ b/core/modules/help_topics/tests/modules/help_topics_test/help_topics/help_topics_test.test.html.twig @@ -7,4 +7,5 @@ related: --- {% set help_topic_url = render_var(url('help_topics.help_topic', {id: 'help_topics_test.additional'})) %} <p>{% trans %}This is a test. It should <a href="{{ help_topic_url }}">link to the additional topic</a>. Also there should be a related topic link below to the Help module topic page and the linked topic.{% endtrans %}</p> +<p>{% trans %}Nonworditem totranslate.{% endtrans %}</p> <p>{% trans %}Test translation.{% endtrans %}</p> diff --git a/core/modules/help_topics/tests/modules/help_topics_test/help_topics_test.permissions.yml b/core/modules/help_topics/tests/modules/help_topics_test/help_topics_test.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..4147e30512bdffd5b81bebcef10ed7d5a0d0fac9 --- /dev/null +++ b/core/modules/help_topics/tests/modules/help_topics_test/help_topics_test.permissions.yml @@ -0,0 +1,2 @@ +access test help: + title: 'Access the test help section' diff --git a/core/modules/help_topics/tests/modules/help_topics_test/src/Plugin/HelpSection/TestHelpSection.php b/core/modules/help_topics/tests/modules/help_topics_test/src/Plugin/HelpSection/TestHelpSection.php new file mode 100644 index 0000000000000000000000000000000000000000..f4d2a741e0ee351ce09f59212cdab416d0d177ef --- /dev/null +++ b/core/modules/help_topics/tests/modules/help_topics_test/src/Plugin/HelpSection/TestHelpSection.php @@ -0,0 +1,79 @@ +<?php + +namespace Drupal\help_topics_test\Plugin\HelpSection; + +use Drupal\help_topics\SearchableHelpInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Url; +use Drupal\Core\Link; +use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase; + +/** + * Provides a searchable help section for testing. + * + * @HelpSection( + * id = "help_topics_test", + * title = @Translation("Test section"), + * weight = 100, + * description = @Translation("For testing search"), + * permission = "access test help" + * ) + */ +class TestHelpSection extends HelpSectionPluginBase implements SearchableHelpInterface { + + /** + * {@inheritdoc} + */ + public function listTopics() { + return [ + Link::fromTextAndUrl('Foo', Url::fromUri('https://foo.com')), + Link::fromTextAndUrl('Bar', Url::fromUri('https://bar.com')), + ]; + } + + /** + * {@inheritdoc} + */ + public function listSearchableTopics() { + return ['foo', 'bar']; + } + + /** + * {@inheritdoc} + */ + public function renderTopicForSearch($topic_id, LanguageInterface $language) { + switch ($topic_id) { + case 'foo': + if ($language->getId() == 'en') { + return [ + 'title' => 'Foo in English title wcsrefsdf', + 'text' => 'Something about foo body notawordenglish sqruct', + 'url' => Url::fromUri('https://foo.com'), + ]; + } + return [ + 'title' => 'Foomm Foreign heading', + 'text' => 'Fake foreign foo text notawordgerman asdrsad', + 'url' => Url::fromUri('https://mm.foo.com'), + ]; + + case 'bar': + if ($language->getId() == 'en') { + return [ + 'title' => 'Bar in English', + 'text' => 'Something about bar anotherwordenglish asdrsad', + 'url' => Url::fromUri('https://bar.com'), + ]; + } + return [ + 'title' => \Drupal::state()->get('help_topics_test:translated_title', 'Barmm Foreign sdeeeee'), + 'text' => 'Fake foreign barmm anotherwordgerman sqruct', + 'url' => Url::fromUri('https://mm.bar.com'), + ]; + + default: + throw new \InvalidArgumentException('Unexpected ID encountered'); + } + } + +} diff --git a/core/modules/help_topics/tests/modules/help_topics_test/src/Plugin/HelpTopic/TestHelpTopicPlugin.php b/core/modules/help_topics/tests/modules/help_topics_test/src/Plugin/HelpTopic/TestHelpTopicPlugin.php index 22b1bdd062851c10b07f881206d0d133f0bf5edc..421c26321f20f50d6fc3f837be8d924457ec57fb 100644 --- a/core/modules/help_topics/tests/modules/help_topics_test/src/Plugin/HelpTopic/TestHelpTopicPlugin.php +++ b/core/modules/help_topics/tests/modules/help_topics_test/src/Plugin/HelpTopic/TestHelpTopicPlugin.php @@ -14,7 +14,10 @@ class TestHelpTopicPlugin extends HelpTopicPluginBase { * {@inheritdoc} */ public function getBody() { - return $this->pluginDefinition['body']; + return [ + '#type' => 'markup', + '#markup' => $this->pluginDefinition['body'], + ]; } /** diff --git a/core/modules/help_topics/tests/src/Functional/HelpTopicSearchTest.php b/core/modules/help_topics/tests/src/Functional/HelpTopicSearchTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a886167700bc1b5f627659b8fd399d2094e0ffa0 --- /dev/null +++ b/core/modules/help_topics/tests/src/Functional/HelpTopicSearchTest.php @@ -0,0 +1,261 @@ +<?php + +namespace Drupal\Tests\help_topics\Functional; + +use Drupal\Tests\Traits\Core\CronRunTrait; +use Drupal\help_topics\Plugin\Search\HelpSearch; + +/** + * Verifies help topic search. + * + * @group help_topics + */ +class HelpTopicSearchTest extends HelpTopicTranslatedTestBase { + + use CronRunTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'search', + 'locale', + 'language', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Log in. + $this->drupalLogin($this->createUser([ + 'access administration pages', + 'administer site configuration', + 'view the administration theme', + 'administer permissions', + 'administer languages', + 'administer search', + 'access test help', + 'search content', + ])); + + // Add English language and set to default. + $this->drupalPostForm('admin/config/regional/language/add', [ + 'predefined_langcode' => 'en', + ], 'Add language'); + $this->drupalPostForm('admin/config/regional/language', [ + 'site_default_language' => 'en', + ], 'Save configuration'); + // When default language is changed, the container is rebuilt in the child + // site, so a rebuild in the main site is required to use the new container + // here. + $this->rebuildContainer(); + + // Before running cron, verify that a search returns no results. + $this->drupalPostForm('search/help', ['keys' => 'notawordenglish'], 'Search'); + $this->assertSearchResultsCount(0); + + // Run cron until the topics are fully indexed, with a limit of 100 runs + // to avoid infinite loops. + $num_runs = 100; + $plugin = HelpSearch::create($this->container, [], 'help_search', []); + do { + $this->cronRun(); + $remaining = $plugin->indexStatus()['remaining']; + } while (--$num_runs && $remaining); + $this->assertNotEmpty($num_runs); + $this->assertEmpty($remaining); + + // Visit the Search settings page and verify it says 100% indexed. + $this->drupalGet('admin/config/search/pages'); + $this->assertSession()->pageTextContains('100% of the site has been indexed'); + } + + /** + * Tests help topic search. + */ + public function testHelpSearch() { + $german = \Drupal::languageManager()->getLanguage('de'); + $session = $this->assertSession(); + + // Verify that when we search in English for a word that is only in + // English text, we find the topic. Note that these "words" are provided + // by the topics that come from + // \Drupal\help_topics_test\Plugin\HelpSection\TestHelpSection. + $this->drupalPostForm('search/help', ['keys' => 'notawordenglish'], 'Search'); + $this->assertSearchResultsCount(1); + $session->linkExists('Foo in English title wcsrefsdf'); + + // Same for German. + $this->drupalPostForm('search/help', ['keys' => 'notawordgerman'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(1); + $session->linkExists('Foomm Foreign heading'); + + // Verify when we search in English for a word that only exists in German, + // we get no results. + $this->drupalPostForm('search/help', ['keys' => 'notawordgerman'], 'Search'); + $this->assertSearchResultsCount(0); + $session->pageTextContains('no results'); + + // Same for German. + $this->drupalPostForm('search/help', ['keys' => 'notawordenglish'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(0); + $session->pageTextContains('no results'); + + // Verify when we search in English for a word that exists in one topic + // in English and a different topic in German, we only get the one English + // topic. + $this->drupalPostForm('search/help', ['keys' => 'sqruct'], 'Search'); + $this->assertSearchResultsCount(1); + $session->linkExists('Foo in English title wcsrefsdf'); + + // Same for German. + $this->drupalPostForm('search/help', ['keys' => 'asdrsad'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(1); + $session->linkExists('Foomm Foreign heading'); + + // All of the above tests used the TestHelpSection plugin. Also verify + // that we can search for translated regular help topics, in both English + // and German. + $this->drupalPostForm('search/help', ['keys' => 'nonworditem'], 'Search'); + $this->assertSearchResultsCount(1); + $session->linkExists('ABC Help Test module'); + // Click the link and verify we ended up on the topic page. + $this->clickLink('ABC Help Test module'); + $session->pageTextContains('This is a test'); + + $this->drupalPostForm('search/help', ['keys' => 'nonwordgerman'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(1); + $session->linkExists('ABC-Hilfetestmodul'); + $this->clickLink('ABC-Hilfetestmodul'); + $session->pageTextContains('Ãœbersetzung testen.'); + + // Verify that we can search from the admin/help page. + $this->drupalGet('admin/help'); + $session->pageTextContains('Search help'); + $this->drupalPostForm(NULL, ['keys' => 'nonworditem'], 'Search'); + $this->assertSearchResultsCount(1); + $session->linkExists('ABC Help Test module'); + + // Same for German. + $this->drupalPostForm('admin/help', ['keys' => 'nonwordgerman'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(1); + $session->linkExists('ABC-Hilfetestmodul'); + + // Verify we can search for title text (other searches used text + // that was part of the body). + $this->drupalPostForm('search/help', ['keys' => 'wcsrefsdf'], 'Search'); + $this->assertSearchResultsCount(1); + $session->linkExists('Foo in English title wcsrefsdf'); + + $this->drupalPostForm('admin/help', ['keys' => 'sdeeeee'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(1); + $session->linkExists('Barmm Foreign sdeeeee'); + + // Just changing the title and running cron is not enough to reindex so + // 'sdeeeee' still hits a match. The content is updated because the help + // topic is rendered each time. + \Drupal::state()->set('help_topics_test:translated_title', 'Updated translated title'); + $this->cronRun(); + $this->drupalPostForm('admin/help', ['keys' => 'sdeeeee'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(1); + $session->linkExists('Updated translated title'); + // Searching for the updated test shouldn't produce a match. + $this->drupalPostForm('admin/help', ['keys' => 'translated title'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(0); + + // Clear the caches and re-run cron - this should re-index the help. + $this->rebuildAll(); + $this->cronRun(); + $this->drupalPostForm('admin/help', ['keys' => 'sdeeeee'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(0); + $this->drupalPostForm('admin/help', ['keys' => 'translated title'], 'Search', [ + 'language' => $german, + ]); + $this->assertSearchResultsCount(1); + $session->linkExists('Updated translated title'); + + // Verify the cache tags and contexts. + $session->responseHeaderContains('X-Drupal-Cache-Tags', 'config:search.page.help_search'); + $session->responseHeaderContains('X-Drupal-Cache-Tags', 'search_index:help_search'); + $session->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions'); + $session->responseHeaderContains('X-Drupal-Cache-Contexts', 'languages:language_interface'); + + // Log in as a user that does not have permission to see TestHelpSection + // items, and verify they can still search for help topics but not see these + // items. + $this->drupalLogin($this->createUser([ + 'access administration pages', + 'administer site configuration', + 'view the administration theme', + 'administer permissions', + 'administer languages', + 'administer search', + 'search content', + ])); + + $this->drupalGet('admin/help'); + $session->pageTextContains('Search help'); + + $this->drupalPostForm('search/help', ['keys' => 'nonworditem'], 'Search'); + $this->assertSearchResultsCount(1); + $session->linkExists('ABC Help Test module'); + + $this->drupalPostForm('search/help', ['keys' => 'notawordenglish'], 'Search'); + $this->assertSearchResultsCount(0); + $session->pageTextContains('no results'); + + // Uninstall the test module and verify its topics are immediately not + // searchable. + \Drupal::service('module_installer')->uninstall(['help_topics_test']); + $this->drupalPostForm('search/help', ['keys' => 'nonworditem'], 'Search'); + $this->assertSearchResultsCount(0); + } + + /** + * Tests uninstalling the help_topics module. + */ + public function testUninstall() { + // Ensure we can uninstall help_topics and use the help system without + // breaking. + $this->drupalLogin($this->rootUser); + $edit = []; + $edit['uninstall[help_topics]'] = TRUE; + $this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall')); + $this->drupalPostForm(NULL, NULL, t('Uninstall')); + $this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.'); + $this->drupalGet('admin/help'); + $this->assertResponse(200); + } + + /** + * Asserts that help search returned the expected number of results. + * + * @param int $count + * The expected number of search results. + */ + protected function assertSearchResultsCount($count) { + $this->assertSession()->elementsCount('css', '.help_search-results > li', $count); + } + +} diff --git a/core/modules/help_topics/tests/src/Functional/HelpTopicTest.php b/core/modules/help_topics/tests/src/Functional/HelpTopicTest.php index b5877f7d48b3efb3b891275d6c04e8c6fa8ff8da..0f2a6b4019356d8ee023d6d03c73c4e3d3dabc70 100644 --- a/core/modules/help_topics/tests/src/Functional/HelpTopicTest.php +++ b/core/modules/help_topics/tests/src/Functional/HelpTopicTest.php @@ -239,10 +239,10 @@ protected function getTopicList() { public function verifyBreadCrumb() { // Verify Help Topics administration breadcrumbs. $trail = [ - '' => 'Home', - 'admin' => 'Administration', - 'admin/help' => 'Help', - ]; + '' => 'Home', + 'admin' => 'Administration', + 'admin/help' => 'Help', + ]; $this->assertBreadcrumb('admin/help/topic/help_topics_test.test', $trail); // Ensure we are on the expected help topic page. $this->assertSession()->pageTextContains('Also there should be a related topic link below to the Help module topic page and the linked topic.'); @@ -253,7 +253,7 @@ public function verifyBreadCrumb() { 'admin' => 'Administration', 'admin/config' => 'Configuration', 'admin/config/system' => 'System', - ]; + ]; $this->assertBreadcrumb('admin/config/system/site-information', $trail); } diff --git a/core/modules/help_topics/tests/src/Functional/HelpTopicTranslatedTestBase.php b/core/modules/help_topics/tests/src/Functional/HelpTopicTranslatedTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..c2f1158aa543f5e7de4b331a01cb2fb6b1c40f7b --- /dev/null +++ b/core/modules/help_topics/tests/src/Functional/HelpTopicTranslatedTestBase.php @@ -0,0 +1,86 @@ +<?php + +namespace Drupal\Tests\help_topics\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * Provides a base class for functional help topic tests that use translation. + * + * Installs in German, with a small PO file, and sets up the task, help, and + * page title blocks. + */ +abstract class HelpTopicTranslatedTestBase extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'help_topics_test', + 'help', + 'help_topics', + 'block', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // These tests rely on some markup from the 'Seven' theme. + \Drupal::service('theme_installer')->install(['seven']); + \Drupal::configFactory()->getEditable('system.theme') + ->set('admin', 'seven') + ->save(TRUE); + + // Place various blocks. + $settings = [ + 'theme' => 'seven', + 'region' => 'help', + ]; + $this->placeBlock('help_block', $settings); + $this->placeBlock('local_tasks_block', $settings); + $this->placeBlock('local_actions_block', $settings); + $this->placeBlock('page_title_block', $settings); + + // Create user. + $this->drupalLogin($this->createUser([ + 'access administration pages', + 'view the administration theme', + 'administer permissions', + ])); + } + + /** + * {@inheritdoc} + */ + protected function installParameters() { + $parameters = parent::installParameters(); + // Install in German. This will ensure the language and locale modules are + // installed. + $parameters['parameters']['langcode'] = 'de'; + // Create a po file so we don't attempt to download one from + // localize.drupal.org and to have a test translation that will not change. + \Drupal::service('file_system')->mkdir($this->publicFilesDirectory . '/translations', NULL, TRUE); + $contents = <<<ENDPO +msgid "" +msgstr "" + +msgid "ABC Help Test module" +msgstr "ABC-Hilfetestmodul" + +msgid "Test translation." +msgstr "Ãœbersetzung testen." + +msgid "Nonworditem totranslate." +msgstr "Nonwordgerman sdfwedrsdf." + +ENDPO; + include_once $this->root . '/core/includes/install.core.inc'; + $version = _install_get_version_info(\Drupal::VERSION)['major'] . '.0.0'; + file_put_contents($this->publicFilesDirectory . "/translations/drupal-{$version}.de.po", $contents); + return $parameters; + } + +} diff --git a/core/modules/help_topics/tests/src/Functional/HelpTopicTranslationTest.php b/core/modules/help_topics/tests/src/Functional/HelpTopicTranslationTest.php index 67999548970dde0b2275948162ae861f63db3a1d..ead7df35715f19e564a243aafa702c9703e441c6 100644 --- a/core/modules/help_topics/tests/src/Functional/HelpTopicTranslationTest.php +++ b/core/modules/help_topics/tests/src/Functional/HelpTopicTranslationTest.php @@ -2,26 +2,12 @@ namespace Drupal\Tests\help_topics\Functional; -use Drupal\Tests\BrowserTestBase; - /** - * Verifies help topic display and user access to help based on permissions. + * Verifies help topic translations. * * @group help_topics */ -class HelpTopicTranslationTest extends BrowserTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = [ - 'help_topics_test', - 'help', - 'help_topics', - 'block', - ]; +class HelpTopicTranslationTest extends HelpTopicTranslatedTestBase { /** * {@inheritdoc} @@ -29,21 +15,7 @@ class HelpTopicTranslationTest extends BrowserTestBase { protected function setUp() { parent::setUp(); - // These tests rely on some markup from the 'Seven' theme. - \Drupal::service('theme_installer')->install(['seven']); - \Drupal::service('config.factory')->getEditable('system.theme')->set('admin', 'seven')->save(); - - // Place various blocks. - $settings = [ - 'theme' => 'seven', - 'region' => 'help', - ]; - $this->placeBlock('help_block', $settings); - $this->placeBlock('local_tasks_block', $settings); - $this->placeBlock('local_actions_block', $settings); - $this->placeBlock('page_title_block', $settings); - - // Create user. + // Create user and log in. $this->drupalLogin($this->createUser([ 'access administration pages', 'view the administration theme', @@ -70,32 +42,4 @@ public function testHelpTopicTranslations() { $session->responseHeaderContains('X-Drupal-Cache-Contexts', 'languages:language_interface'); } - /** - * {@inheritdoc} - */ - protected function installParameters() { - $parameters = parent::installParameters(); - // Install in German. This will ensure the language and locale modules are - // installed. - $parameters['parameters']['langcode'] = 'de'; - // Create a po file so we don't attempt to download one from - // localize.drupal.org and to have a test translation that will not change. - \Drupal::service('file_system')->mkdir($this->publicFilesDirectory . '/translations', NULL, TRUE); - $contents = <<<ENDPO -msgid "" -msgstr "" - -msgid "ABC Help Test module" -msgstr "ABC-Hilfetestmodul" - -msgid "Test translation." -msgstr "Ãœbersetzung testen." - -ENDPO; - include_once $this->root . '/core/includes/install.core.inc'; - $version = _install_get_version_info(\Drupal::VERSION)['major'] . '.0.0'; - file_put_contents($this->publicFilesDirectory . "/translations/drupal-{$version}.de.po", $contents); - return $parameters; - } - }