first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

117
core/modules/help/help.api.php Executable file
View File

@@ -0,0 +1,117 @@
<?php
/**
* @file
* Hooks for the Help system.
*/
use Drupal\Core\Url;
/**
* @defgroup help_docs Help and documentation
* @{
* Documenting modules, themes, and install profiles
*
* @section sec_topics Help Topics
* Modules, themes, and install profiles can have a subdirectory help_topics
* that contains one or more Help Topics, to provide help to administrative
* users. These are shown on the main admin/help page. See
* @link https://www.drupal.org/docs/develop/documenting-your-project/help-topic-standards Help Topic Standards @endlink
* for more information.
*
* @section sec_hook hook_help
* Modules can implement hook_help() to provide a module overview (shown on the
* main admin/help page). This hook implementation can also provide help text
* that is shown in the Help block at the top of administrative pages. See the
* hook_help() documentation and
* @link https://www.drupal.org/docs/develop/documenting-your-project/help-text-standards Help text standards @endlink
* for more information.
* @}
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Provide online user help.
*
* By implementing hook_help(), a module can make documentation available to
* the user for the module as a whole, or for specific pages. Help for
* developers should usually be provided via function header comments in the
* code, or in special API example files.
*
* The page-specific help information provided by this hook appears in the
* Help block (provided by the core Help module), if the block is displayed on
* that page. The module overview help information is displayed by the Help
* module. It can be accessed from the page at /admin/help or from the Extend
* page. If a module implements hook_help() the help system expects module
* overview help to be provided.
*
* For detailed usage examples of:
* - Module overview help, see content_translation_help(). Module overview
* help should follow
* @link https://www.drupal.org/node/632280 the standard help template. @endlink
* - Page-specific help using only routes, see node_help().
* - Page-specific help using routes and $request, see block_help().
*
* @param string $route_name
* For page-specific help, use the route name as identified in the
* module's routing.yml file. For module overview help, the route name
* will be in the form of "help.page.$modulename".
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match. This can be used to generate different help
* output for different pages that share the same route.
*
* @return string|array
* A render array, localized string, or object that can be rendered into
* a string, containing the help text.
*/
function hook_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the block module.
case 'help.page.block':
return '<p>' . t('Blocks are boxes of content rendered into an area, or region, of a web page. The default theme Olivero, for example, implements the regions "Sidebar", "Highlighted", "Content", "Header", "Footer Top", "Footer Bottom", etc., and a block may appear in any one of these areas. The <a href=":blocks">blocks administration page</a> provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
// Help for another path in the block module.
case 'block.admin_display':
return '<p>' . t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page.') . '</p>';
}
}
/**
* Perform alterations on help page section plugin definitions.
*
* Sections for the page at /admin/help are provided by plugins. This hook
* allows modules to alter the plugin definitions.
*
* @param array $info
* Array of plugin information exposed by hook page section plugins, altered
* by reference.
*
* @see \Drupal\help\HelpSectionPluginInterface
* @see \Drupal\help\Annotation\HelpSection
* @see \Drupal\help\HelpSectionManager
*/
function hook_help_section_info_alter(array &$info) {
// Alter the header for the module overviews section.
$info['hook_help']['title'] = t('Overviews of modules');
// Move the module overviews section to the end.
$info['hook_help']['weight'] = 500;
}
/**
* Perform alterations on help topic definitions.
*
* @param array $info
* Array of help topic plugin definitions keyed by their plugin ID.
*/
function hook_help_topics_info_alter(array &$info) {
// Alter the help topic to be displayed on admin/help.
$info['example.help_topic']['top_level'] = TRUE;
}
/**
* @} End of "addtogroup hooks".
*/

10
core/modules/help/help.info.yml Executable file
View File

@@ -0,0 +1,10 @@
name: Help
type: module
description: 'Generates help pages and provides a Help block with page-level help.'
package: Core
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

98
core/modules/help/help.install Executable file
View File

@@ -0,0 +1,98 @@
<?php
/**
* @file
* Install and uninstall functions for help module.
*/
/**
* Implements hook_schema().
*/
function help_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;
}
/**
* Install search index table for help topics.
*/
function help_update_10200(&$sandbox = NULL) {
$connection = \Drupal::database();
if (!$connection->schema()->tableExists('help_search_items')) {
$table = [
'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'],
],
];
$connection->schema()->createTable('help_search_items', $table);
}
}

View File

@@ -0,0 +1,6 @@
help.main:
title: Help
description: 'Reference for usage, configuration, and modules.'
route_name: help.main
weight: 9
parent: system.admin

158
core/modules/help/help.module Executable file
View File

@@ -0,0 +1,158 @@
<?php
/**
* @file
* Manages displaying online help.
*/
use Drupal\Core\Url;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function help_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.main':
$output = '<h2>' . t('Getting Started') . '</h2>';
$output .= '<p>' . t('Follow these steps to set up and start using your website:') . '</p>';
$output .= '<ol>';
$output .= '<li>' . t('<strong>Configure your website</strong> Once logged in, visit the <a href=":admin">Administration page</a>, where you may <a href=":config">customize and configure</a> all aspects of your website.', [':admin' => Url::fromRoute('system.admin')->toString(), ':config' => Url::fromRoute('system.admin_config')->toString()]) . '</li>';
$output .= '<li>' . t('<strong>Enable additional functionality</strong> Next, visit the <a href=":modules">Extend page</a> and install modules that suit your specific needs. You can find additional modules at the <a href=":download_modules">Drupal.org modules page</a>.', [':modules' => Url::fromRoute('system.modules_list')->toString(), ':download_modules' => 'https://www.drupal.org/project/modules']) . '</li>';
$output .= '<li>' . t('<strong>Customize your website design</strong> To change the "look and feel" of your website, visit the <a href=":themes">Appearance page</a>. You may choose from one of the included themes or download additional themes from the <a href=":download_themes">Drupal.org themes page</a>.', [':themes' => Url::fromRoute('system.themes_page')->toString(), ':download_themes' => 'https://www.drupal.org/project/themes']) . '</li>';
// Display a link to the create content page if Node module is installed.
if (\Drupal::moduleHandler()->moduleExists('node')) {
$output .= '<li>' . t('<strong>Start posting content</strong> Finally, you may <a href=":content">add new content</a> to your website.', [':content' => Url::fromRoute('node.add_page')->toString()]) . '</li>';
}
$output .= '</ol>';
$output .= '<p>' . t('For more information, refer to the help listed on this page or to the <a href=":docs">online documentation</a> and <a href=":support">support</a> pages at <a href=":drupal">drupal.org</a>.', [':docs' => 'https://www.drupal.org/documentation', ':support' => 'https://www.drupal.org/support', ':drupal' => 'https://www.drupal.org']) . '</p>';
return ['#markup' => $output];
case 'help.page.help':
$help_home = Url::fromRoute('help.main')->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 = '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Help module generates <a href=":help-page">Help topics and reference pages</a> to guide you through the use and configuration of modules, and provides a Help block with page-level help. The reference pages are a starting point for <a href=":handbook">Drupal.org online documentation</a> pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the <a href=":help">online documentation for the Help module</a>.', [':help' => 'https://www.drupal.org/documentation/modules/help/', ':handbook' => 'https://www.drupal.org/documentation', ':help-page' => Url::fromRoute('help.main')->toString()]) . '</p>';
$output .= '<p>' . t('Help topics provided by modules and themes are also part of the Help module. If the core Search module is installed, these topics are searchable. For more information, see the <a href=":online">online documentation, Help Topic Standards</a>.', [':online' => 'https://www.drupal.org/docs/develop/managing-a-drupalorg-theme-module-or-distribution-project/documenting-your-project/help-topic-standards']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Providing a help reference') . '</dt>';
$output .= '<dd>' . t('The Help module displays explanations for using each module listed on the main <a href=":help">Help reference page</a>.', [':help' => Url::fromRoute('help.main')->toString()]) . '</dd>';
$output .= '<dt>' . t('Providing page-specific help') . '</dt>';
$output .= '<dd>' . t('Page-specific help text provided by modules is displayed in the Help block. This block can be placed and configured on the <a href=":blocks">Block layout page</a>.', [':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('block.admin_display')->toString() : '#']) . '</dd>';
$output .= '<dt>' . t('Viewing help topics') . '</dt>';
$output .= '<dd>' . t('The top-level help topics are listed on the main <a href=":help_page">Help page</a>. Links to other topics, including non-top-level help topics, can be found under the "Related" heading when viewing a topic page.', [':help_page' => $help_home]) . '</dd>';
$output .= '<dt>' . t('Providing help topics') . '</dt>';
$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/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 Claro 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];
case 'help.help_topic':
$help_home = Url::fromRoute('help.main')->toString();
return '<p>' . t('See the <a href=":help_page">Help page</a> for more topics.', [
':help_page' => $help_home,
]) . '</p>';
}
}
/**
* Implements hook_theme().
*/
function help_theme($existing, $type, $theme, $path) {
return [
'help_section' => [
'variables' => [
'title' => NULL,
'description' => NULL,
'links' => NULL,
'empty' => NULL,
],
],
'help_topic' => [
'variables' => [
'body' => [],
'related' => [],
],
],
];
}
/**
* Implements hook_preprocess_HOOK() for block templates.
*/
function help_preprocess_block(&$variables) {
if ($variables['plugin_id'] == 'help_block') {
$variables['attributes']['role'] = 'complementary';
}
}
/**
* Implements hook_block_view_BASE_BLOCK_ID_alter().
*/
function help_block_view_help_block_alter(array &$build, BlockPluginInterface $block) {
// Assume that most users do not need or want to perform contextual actions on
// the help block, so don't needlessly draw attention to it.
unset($build['#contextual_links']);
}
/**
* Implements hook_modules_uninstalled().
*/
function help_modules_uninstalled(array $modules) {
_help_search_update($modules);
}
/**
* Implements hook_themes_uninstalled().
*/
function help_themes_uninstalled(array $themes) {
\Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
_help_search_update();
}
/**
* Implements hook_modules_installed().
*/
function help_modules_installed(array $modules, $is_syncing) {
_help_search_update();
}
/**
* Implements hook_themes_installed().
*/
function help_themes_installed(array $themes) {
\Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
_help_search_update();
}
/**
* Ensure that search is updated when extensions are installed or uninstalled.
*
* @param string[] $extensions
* (optional) If modules are being uninstalled, the names of the modules
* being uninstalled. For themes being installed/uninstalled, or modules
* being installed, omit this parameter.
*/
function _help_search_update(array $extensions = []): void {
// Early return if search is not installed or if we're uninstalling this
// module.
if (!\Drupal::hasService('plugin.manager.search') ||
in_array('help', $extensions)) {
return;
}
if (\Drupal::service('update.update_hook_registry')->getInstalledVersion('help') >= 10200) {
// Ensure that topics for extensions that have been uninstalled are removed
// and that the index state variable is updated.
$help_search = \Drupal::service('plugin.manager.search')->createInstance('help_search');
$help_search->updateTopicList();
$help_search->updateIndexState();
}
}

View File

@@ -0,0 +1,2 @@
access help pages:
title: 'Use help pages'

View File

@@ -0,0 +1,112 @@
<?php
/**
* @file
* Post update functions for the Help module.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\search\Entity\SearchPage;
use Drupal\user\RoleInterface;
/**
* Install or update config for help topics if the search module installed.
*/
function help_post_update_help_topics_search() {
$module_handler = \Drupal::moduleHandler();
if (!$module_handler->moduleExists('search')) {
// No dependencies to update or install.
return;
}
if ($module_handler->moduleExists('help_topics')) {
if ($page = SearchPage::load('help_search')) {
// Resave to update module dependency.
$page->save();
}
}
else {
$factory = \Drupal::configFactory();
// Install optional config for the search page.
$config = $factory->getEditable('search.page.help_search');
$config->setData([
'langcode' => 'en',
'status' => TRUE,
'dependencies' => [
'module' => [
'help',
],
],
'id' => 'help_search',
'label' => 'Help',
'path' => 'help',
'weight' => 0,
'plugin' => 'help_search',
'configuration' => [],
])->save(TRUE);
if (\Drupal::service('theme_handler')->themeExists('claro') && $factory->get('block.block.claro_help_search')->isNew()) {
// Optional block only if it's not created manually earlier.
$config = $factory->getEditable('block.block.claro_help_search');
$config->setData([
'langcode' => 'en',
'status' => TRUE,
'dependencies' => [
'module' => [
'search',
'system',
],
'theme' => [
'claro',
],
'enforced' => [
'config' => [
'search.page.help_search',
],
],
],
'id' => 'claro_help_search',
'theme' => 'claro',
'region' => 'help',
'weight' => -4,
'provider' => NULL,
'plugin' => 'search_form_block',
'settings' => [
'id' => 'search_form_block',
'label' => 'Search help',
'label_display' => 'visible',
'provider' => 'search',
'page_id' => 'help_search',
],
'visibility' => [
'request_path' => [
'id' => 'request_path',
'negate' => FALSE,
'context_mapping' => [],
'pages' => '/admin/help',
],
],
])->save(TRUE);
}
}
}
/**
* Uninstall the help_topics module if installed.
*/
function help_post_update_help_topics_uninstall() {
if (\Drupal::moduleHandler()->moduleExists('help_topics')) {
\Drupal::service('module_installer')->uninstall(['help_topics'], FALSE);
}
}
/**
* Grant all admin roles the 'access help pages' permission.
*/
function help_post_update_add_permissions_to_roles(?array &$sandbox = []): void {
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'user_role', function (RoleInterface $role): bool {
if ($role->isAdmin() || !$role->hasPermission('access administration pages')) {
return FALSE;
}
$role->grantPermission('access help pages');
return TRUE;
});
}

View File

@@ -0,0 +1,22 @@
help.main:
path: '/admin/help'
defaults:
_controller: '\Drupal\help\Controller\HelpController::helpMain'
_title: 'Help'
requirements:
_permission: 'access help pages'
help.page:
path: '/admin/help/{name}'
defaults:
_controller: '\Drupal\help\Controller\HelpController::helpPage'
_title: 'Help'
requirements:
_permission: 'access help pages'
help.help_topic:
path: '/admin/help/topic/{id}'
defaults:
_controller: '\Drupal\help\Controller\HelpTopicPluginController::viewHelpTopic'
requirements:
_permission: 'access help pages'

View File

@@ -0,0 +1,29 @@
services:
plugin.manager.help_section:
class: Drupal\help\HelpSectionManager
parent: default_plugin_manager
calls:
- [setSearchManager, ['@?plugin.manager.search']]
tags:
- { name: plugin_manager_cache_clear }
help.breadcrumb:
class: Drupal\help\HelpBreadcrumbBuilder
tags:
- { name: breadcrumb_builder, priority: 900 }
public: false
plugin.manager.help_topic:
class: Drupal\help\HelpTopicPluginManager
arguments: ['@module_handler', '@theme_handler', '@cache.discovery', '%app.root%']
Drupal\help\HelpTopicPluginManagerInterface: '@plugin.manager.help_topic'
help.twig.loader:
class: Drupal\help\HelpTopicTwigLoader
arguments: ['%app.root%', '@module_handler', '@theme_handler']
# Lowest core priority because loading help topics is not the usual case.
tags:
- { name: twig.loader, priority: -200 }
public: false
help_twig.extension:
class: Drupal\help\HelpTwigExtension
arguments: ['@access_manager', '@plugin.manager.help_topic', '@string_translation']
tags:
- { name: twig.extension }

View File

@@ -0,0 +1,20 @@
---
label: 'Changing the appearance of your site'
top_level: true
related:
- core.content_structure
---
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}What is a theme?{% endtrans %}</h2>
<p>{% trans %}A <em>theme</em> is a set of files that define the visual look and feel of your site. The core software and modules that run on your site determine which content (including HTML text and other data stored in the database, uploaded images, and any other asset files) is displayed on the pages of your site. The theme determines the HTML markup and CSS styling that wraps the content. Several basic themes are supplied with the core software; additional <em>contributed themes</em> can be downloaded separately from the <a href="https://www.drupal.org/project/project_theme">Download &amp; Extend page on drupal.org</a>, or you can create your own theme.{% endtrans %}</p>
<h2>{% trans %}What is a base theme?{% endtrans %}</h2>
<p>{% trans %}A base theme is a theme that is not meant to be used directly on a site, but instead acts as a scaffolding for building other themes. The core Stable 9 theme is one example; other base themes can be downloaded from the <a href="https://www.drupal.org/project/project_theme">Download &amp; Extend page on drupal.org</a>.{% endtrans %}</p>
<h2>{% trans %}What is a layout?{% endtrans %}</h2>
<p>{% trans %}A <em>layout</em> is a template that defines where blocks and other pieces of content should be displayed. The core Layout Discovery module allows modules and themes to register layouts, and the core Layout Builder module provides a visual interface for placing fields and blocks in layouts for entity sub-types and individual entity items (see {{ content_structure_topic }} for more on entities and fields).{% endtrans %}</p>
<h2>{% trans %}Changing site appearance overview{% endtrans %}</h2>
<p>{% trans %}The main way to change the overall appearance of your site is to switch the default theme. The core Layout Builder and Layout Discovery modules allow you to define layouts for your site's content, and the core Breakpoint module helps themes change appearance for different-sized devices. See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/user_guide/en/extend-chapter.html">{% trans %}Extending and Customizing Your Site (Drupal User Guide){% endtrans %}</a></li>
<li><a href="https://www.drupal.org/docs/develop/theming-drupal">{% trans %}Theming Drupal{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,50 @@
---
label: 'Managing and deploying configuration'
top_level: true
related:
- config.export_full
- config.import_full
- config.export_single
- config.import_single
---
<h2>{% trans %}What is the configuration system?{% endtrans %}</h2>
<p>{% trans %}The configuration system provides the ability for administrators to customize the site, and to move and synchronize configuration changes between development sites and the live site. It does this in 2 ways:{% endtrans %}</p>
<ol>
<li>{% trans %}Providing storage for configuration{% endtrans %}</li>
<li>{% trans %}Providing a process in which configuration changes can be imported and exported between instances of the same site; for example, from "dev" to "staging" to "live"{% endtrans %}</li>
</ol>
<h2>{% trans %}What is configuration data?{% endtrans %}</h2>
<p>{% trans %}Configuration data describes settings that define how your site behaves or is displayed. For example, when a site administrator updates settings using an administrative form, these settings are stored as configuration data. Configuration data describes settings as simple as a site name and as complex as a view or image style.{% endtrans %}</p>
<h2>{% trans %}What kinds of configuration are there?{% endtrans %}</h2>
<dl>
<dt>{% trans %}Active configuration{% endtrans %}</dt>
<dd>{% trans %}Active configuration is the current working configuration of a site. Storage of active configuration is defined by the site, and resides in the database by default.{% endtrans %}</dd>
<dt>{% trans %}Simple configuration{% endtrans %}</dt>
<dd>{% trans %}A simple configuration item is a group of settings, such as the settings for a module or theme. Each simple configuration item has its own unique structure.{% endtrans %}</dd>
<dt>{% trans %}Configuration entities{% endtrans %}</dt>
<dd>{% trans %}Configuration entities are user-defined configuration items grouped by type, such as views, image styles, and content types. Each configuration entity within a type has a similar structure.{% endtrans %}</dd>
<dt>{% trans %}Default configuration{% endtrans %}</dt>
<dd>{% trans %}Default configuration can be defined by a module, theme, or installation profile in its <em>config/install</em> or <em>config/optional</em> directories. Configuration is provided in YAML files (file extension .yml); YAML is a human-readable data serialization standard that is used by the core software for several purposes. Once the default configuration has been imported into the site's active configuration (through installing the extension), that configuration is owned by the site, not the extension. This means that future updates of the extension will not override the site's active configuration for that extension.{% endtrans %}</dd>
</dl>
<h2>{% trans %}What is configuration synchronization?{% endtrans %}</h2>
<p>{% trans %}Configuration synchronization is the process of exporting and importing configuration to keep configuration synchronized between different versions of a site; for example, between a development site and the live site.{% endtrans %}</p>
<p>{% trans %}Each site has unique identifier, also called a <em>UUID</em>, which identifies the site to the system in any instance of the site, as long as the site instances have been reproduced as clones (cloning is when the codebase and database are copied to create a new site instance). When site instances are cloned, a "dev" instance of the site has the same UUID as the "live" instance. When site instances share the same UUID, configuration can be exported from one instance to another.{% endtrans %}</p>
<p>{% trans %}The following list contains terms and concepts related to configuration synchronization:{% endtrans %}</p>
<dl>
<dt>{% trans %}Exported configuration{% endtrans %}</dt>
<dd>{% trans %}When configuration is exported, the active configuration is exported as a set of files in YAML format. When using the <em>Configuration synchronization</em> administrative UI, configuration can be exported as a full-export or single-item archive. This archive can then be imported into the destination site instance.{% endtrans %}</dd>
<dt>{% trans %}Imported configuration{% endtrans %}</dt>
<dd>{% trans %}Imported configuration is configuration that has been exported from another instance of the site (the "source") and is now being imported into another site instance (the "destination"), thereby updating its active configuration to match the imported configuration data set.{% endtrans %}</dd>
<dt>{% trans %}Configuration sync directory{% endtrans %}</dt>
<dd>{% trans %}The configuration sync directory location is set in the site's <em>settings.php</em> file. When configuration is exported, the active configuration is exported and described in YAML files which are stored in the configuration sync directory. After the first export, the system compares the site's active configuration with the configuration data in the sync directory and will only export active configuration items that are different than their counterparts in the sync directory.{% endtrans %}</dd>
</dl>
<h2>{% trans %}Managing configuration overview{% endtrans %}</h2>
<p>{% trans %}Configuration management tasks, such as exporting or importing configuration and synchronizing configuration, can be done either through the administrative UI provided by the core Configuration Manager module or a command-line interface (CLI) tool. Defining a configuration sync directory path other than the default value requires read/write access to the site's <em>settings.php</em> file.{% endtrans %}</p>
<p>{% trans %}Most modules and themes also provide settings forms for updating the configuration they provide. See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/configuration-management/workflow-using-drush">Configuration Management: Workflow using Drush</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/understanding-data.html">Concept: Types of Data (Drupal User Guide)</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/install-dev-sites.html">Concept: Development Sites (Drupal User Guide)</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/install-dev-making.html">Making a Development Site (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,51 @@
---
label: 'Managing content structure'
top_level: true
---
{% set help_link_text %}{% trans %}Help{% endtrans %}{% endset %}
{% set help_link = render_var(help_route_link(help_link_text, 'help.main')) %}
<h2>{% trans %}What types of data does a site have?{% endtrans %}</h2>
<p>{% trans %}There are four main types of data. <em>Content</em> is the information (text, images, etc.) meant to be displayed to website visitors. <em>Configuration</em> is data that defines how the content is displayed; some configuration (such as field labels) may also be visible to site visitors. <em>State</em> is temporary data about the state of your site, such as the last time the system <em>cron</em> jobs ran. <em>Session</em> is a subset of State information, related to users' interactions with the site, such as site cookies and whether or not they are logged in.{% endtrans %}</p>
<h2>{% trans %}What is a content entity?{% endtrans %}</h2>
<p>{% trans %}A <em>content entity</em> (or more commonly, <em>entity</em>) is an item of content data, which can consist of text, HTML markup, images, attached files, and other data. Content entities are grouped into <em>entity types</em>, which have different purposes and are displayed in very different ways on the site. Most entity types are also divided into <em>entity sub-types</em>, which are divisions within an entity type to allow for smaller variations in how the entities are used and displayed. For example, the <em>Content item</em> entity type that stores page-level content is divided into <em>content type</em> sub-types; the <em>Content block</em> entity type has <em>block types</em>; but the <em>User</em> entity type (for user profile information) does not have sub-types.{% endtrans %}</p>
<h2>{% trans %}What is a field?{% endtrans %}</h2>
<p>{% trans %}Within entity items, the data is stored in individual <em>fields</em>, each of which holds one type of data, such as formatted or plain text, images or other files, or dates. Fields can be added by an administrator on entity sub-types, so that all entity items of a given entity sub-type have the same collection of fields available, and they can be single-valued or multiple-valued. When you create or edit entity items, you are specifying the values for the fields on the entity item.{% endtrans %}</p>
<h2>{% trans %}What is a reference field?{% endtrans %}</h2>
<p>{% trans %}A <em>reference field</em> is a field that stores a relationship between an entity and one or more other entities, which may belong to the same or different entity type. For example, a <em>Content reference</em> field on a content type stores a relationship between one content item and one or more other content items.{% endtrans %}</p>
<h2>{% trans %}What field types are available?{% endtrans %}</h2>
<p>{% trans %}The following field types are provided by the core system and core modules (many more are provided by contributed modules):{% endtrans %}</p>
<ul>
<li>{% trans %}Boolean, Number (provided by the core system): Stores true/false values and numbers{% endtrans %}</li>
<li>{% trans %}Comment (provided by the core Comment module): Allows users to add comments to an entity{% endtrans %}</li>
<li>{% trans %}Date, Timestamp (Datetime module): Stores dates and times{% endtrans %}</li>
<li>{% trans %}Date range (Datetime range module): Stores time/date periods with a start and an end{% endtrans %}</li>
<li>{% trans %}Email (core system): Stores email addresses{% endtrans %}</li>
<li>{% trans %}Link (Link module): Stores URLs and link text{% endtrans %}</li>
<li>{% trans %}List (Options module): Stores values chosen from pre-defined lists, where the values can be numbers or text; see section below for more on list fields.{% endtrans %}</li>
<li>{% trans %}Reference (core system): Stores entity references; see section above{% endtrans %}</li>
<li>{% trans %}Telephone (Telephone module): Stores telephone numbers{% endtrans %}</li>
<li>{% trans %}Text (Text module): Stores formatted and unformatted text; see section below for more on text fields.{% endtrans %}</li>
</ul>
<h2>{% trans %}What settings are available for List field types?{% endtrans %}</h2>
<p>{% trans %}List fields associate pre-defined <em>keys</em> (or value codes) with <em>labels</em> that the user sees. For example, you might define a list field that shows the user the names of several locations, while behind the scenes a location code is stored in the database. Each list field type corresponds to one type of stored key. For example, a <em>List (integer)</em> field stores integers, while the <em>List (text)</em> field stores text strings. Once you have chosen the field type, the main setting for a list field is the <em>Allowed values</em> list, which associates the keys with the labels.{% endtrans %}</p>
<h2>{% trans %}What types of Text fields are available?{% endtrans %}</h2>
<p>{% trans %}There are several types of text fields, with different characteristics. Text fields can be either <em>plain</em> or <em>formatted</em>: plain text fields do not contain HTML, while formatted fields can contain HTML and are processed through <em>text filters</em> (these are provided by the core Filter module; if you have that module enabled, see the related topic below on filters for more information). Text fields can also be regular-length (with a limit of 255 characters) or <em>long</em> (with a very large character limit), and long formatted text fields can include a <em>summary</em> attribute. All possible combinations of these characteristics exist as text field types; for example, <em>Text (plain)</em> and <em>Text (formatted, long, with summary)</em> are two examples of text field types. {% endtrans %}</p>
<h2>{% trans %}What is a formatter?{% endtrans %}</h2>
<p>{% trans %}A <em>formatter</em> is a way to display a field; most field types offer several types of formatters, and most formatters have settings that further define how the field is displayed. It is also possible to completely hide a field from display, and you have the option of showing or hiding the field's label when it is displayed.{% endtrans %}</p>
<h2>{% trans %}What is a widget?{% endtrans %}</h2>
<p>{% trans %}A <em>widget</em> is a way to edit a field. Some field types, such as plain text single-line fields, have only one widget available (in this case, a single-line text input field). Other field types offer choices for the widget; for example, single-valued <em>List</em> fields can use a <em>Select</em> or <em>Radio button</em> widget for editing. Many widget types have settings that further define how the field can be edited.{% endtrans %}</p>
<h2>{% trans %}Managing content structure overview{% endtrans %}</h2>
<p>{% trans %}Besides the field modules listed in the previous section, there are additional core modules that you can use to manage your content structure:{% endtrans %}</p>
<ul>
<li>{% trans %}The core Node, Comment, Content Block, Custom Menu Links, User, File, Image, Media, Taxonomy, and Contact modules all provide content entity types.{% endtrans %}</li>
<li>{% trans %}The core Field UI module provides a user interface for managing fields and their display on entities.{% endtrans %}</li>
<li>{% trans %}The core Layout Builder module provides a more flexible user interface for configuring the display of entities.{% endtrans %}</li>
<li>{% trans %}The core Filter, Responsive Image, and Path modules provide settings and display options for entities and fields.{% endtrans %}</li>
</ul>
<p>{% trans %}Depending on the core and contributed modules that you currently have installed on your site, the related topics below and other topics listed on the main help page (see {{ help_link }}) will help you with tasks related to content structure.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/understanding-data.html">Concept: Types of Data (Drupal User Guide)</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/planning-chapter.html">Planning your Site (Drupal User Guide)</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/structure-reference-fields.html">Concept: Reference Fields (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,29 @@
---
label: 'Running and configuring cron'
related:
- core.maintenance
---
{% set cron_link_text %}{% trans %}Cron{% endtrans %}{% endset %}
{% set cron_link = render_var(help_route_link(cron_link_text, 'system.cron_settings')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure your system so that cron will run automatically.{% endtrans %}</p>
<h2>{% trans %}What are cron tasks?{% endtrans %}</h2>
<p>{% trans %}To ensure that your site and its modules continue to function well, a group of administrative operations should be run periodically. These operations are called <em>cron</em> tasks, and running the tasks is known as <em>running cron</em>. Depending on how often content is updated on your site, you might need to run cron on a schedule ranging from hourly to weekly to keep your site running well.{% endtrans %}</p>
<h2>{% trans %}What options are available for running cron?{% endtrans %}</h2>
<ul>
<li>{% trans %}If the core Automated Cron module is installed, your site will run cron periodically, on a schedule you can configure.{% endtrans %}</li>
<li>{% trans %}You can set up a task on your web server to visit the <em> cron URL</em>, which is unique to your site, on a schedule.{% endtrans %}</li>
<li>{% trans %}You can also run cron manually, but this is not the recommended way to make sure it is run periodically.{% endtrans %}</li>
</ul>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administration menu, navigate to <em>Configuration</em> &gt; <em>System</em> &gt; <em>{{ cron_link }}</em>. Note the <em>Last run</em> time on the page.{% endtrans %}</li>
<li>{% trans %}If you want to run cron right now, click <em>Run cron</em> and wait for cron to finish.{% endtrans %}</li>
<li>{% trans %}If you have a way to configure tasks on your web server, copy the link where it says <em>To run cron from outside the site, go to</em>. Set up a task to visit that URL on your desired cron schedule, such as once an hour or once a week. (On Linux-like servers, you can use the <em>wget</em> command to visit a URL.) If you configure an outside task, you should uninstall the Automated Cron module.{% endtrans %}</li>
<li>{% trans %}If you are not configuring an outside task, and you have the core Automated Cron module installed, select a schedule for automated cron runs in <em>Cron settings</em> &gt; <em>Run cron every</em>. Click <em>Save configuration</em>.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/security-cron-concept.html">Concept: Cron (Drupal User Guide)</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/security-cron.html">Configuring Cron Maintenance Tasks (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,17 @@
---
label: 'Extending and modifying your site functionality'
top_level: true
---
<h2>{% trans %}What is a module?{% endtrans %}</h2>
<p>{% trans %}A <em>module</em> is a set of PHP, JavaScript, and/or CSS files that extends site features and adds functionality. A set of <em>Core modules</em> is distributed as part of the core software download. Additional <em>Contributed modules</em> can be downloaded separately from the <a href="https://www.drupal.org/project/project_module">Download &amp; Extend page on drupal.org</a>.{% endtrans %}</p>
<h2>{% trans %}What is an Experimental module?{% endtrans %}</h2>
<p>{% trans %}An <em>Experimental</em> module is a module that is still in development and is not yet stable. Using Experimental modules on production sites is not recommended.{% endtrans %}</p>
<h2>{% trans %}What are installing and uninstalling?{% endtrans %}</h2>
<p>{% trans %}Installing a core or downloaded contributed module means turning it on, so that you can use its features and functionality. Uninstalling means turning it off and removing all of its configuration. A module cannot be uninstalled if another installed module depends on it, or if you have created content on your site using the module -- you would need to delete the content and uninstall dependent modules first.{% endtrans %}</p>
<h2>{% trans %}Extending overview{% endtrans %}</h2>
<p>{% trans %}See the related topics listed below for help performing tasks related to extending the functionality of your site.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/understanding-modules.html">Concept: Modules (Drupal User Guide)</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/extend-chapter.html">Extending and Customizing Your Site (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,24 @@
---
label: 'Maintaining and troubleshooting your site'
top_level: true
related:
- core.cron
- core.extending
- core.security
- system.cache
- system.config_error
- system.maintenance_mode
---
<h2>{% trans %}Maintaining and troubleshooting overview{% endtrans %}</h2>
<p>{% trans %}Here are some tasks and hints related to maintaining your site, and troubleshooting problems that may come up on your site. See the related topics below for more information.{% endtrans %}</p>
<ul>
<li>{% trans %}When performing maintenance, such as installing, uninstalling, or updating a module, put your site in maintenance mode.{% endtrans %}</li>
<li>{% trans %}Configure your site so that cron runs periodically.{% endtrans %}</li>
<li>{% trans %}If your site is not behaving as expected, clear the cache before trying to diagnose the problem.{% endtrans %}</li>
<li>{% trans %}There are several site reports that can help you diagnose problems with your site. There are also two core modules that can be used for error logging: Database Logging and Syslog.{% endtrans %}</li>
</ul>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/prevent-chapter.html">Preventing and Fixing Problems (Drupal User Guide)</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/security-chapter.html">Security and Maintenance (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,30 @@
---
label: 'Managing media'
top_level: true
related:
- core.content_structure
- field_ui.add_field
- field_ui.reference_field
- field_ui.manage_form
- breakpoint.overview
---
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}What are media items?{% endtrans %}</h2>
<p>{% trans %}Core media items include audio, images, documents, and videos. You can add other media types, such as social media posts, through the use of contributed modules. Media items may be files located in your site's file system, or remote items referenced by a URL. Media items are content entities, and they are divided into media types (which are entity sub-types); media types can have fields. See {{ content_structure_topic }} for more information on content entities and fields.{% endtrans %}</p>
<h2>{% trans %}What is the media library?{% endtrans %}</h2>
<p>{% trans %}The media library is a visual user interface for managing and reusing media items. Add media items to content using Media reference fields and the Media library field widget.{% endtrans %}</p>
<h2>{% trans %}What is an image style?{% endtrans %}</h2>
<p>{% trans %}An image style is a set of processing steps, known as <em>effects</em>, that can be applied to images. Examples of effects include scaling and cropping images to different sizes. Responsive image styles can associate image styles with your theme's size breakpoints. This allows serving images sized for the browser width.{% endtrans %}</p>
<h2>{% trans %}Overview of managing media{% endtrans %}</h2>
<p>{% trans %}The following modules provide media-related functionality:{% endtrans %}</p>
<ul>
<li>{% trans %}Media items and media types are managed by the core Media module.{% endtrans %}</li>
<li>{% trans %}The core Media module provides a Media reference field to add media to content entities. The core File and Image modules also provide reference fields. It is recommended to use the Media reference field because it is more versatile.{% endtrans %}</li>
<li>{% trans %}The core Media Library module provides the media library and the Media library field widget. With this module installed, the Media library field widget becomes the default widget for editing Media reference fields.{% endtrans %}</li>
<li>{% trans %}The core Image module provides a user interface for defining image styles. The core Responsive Image module provides responsive image styles. Using the core Breakpoint module, and a breakpoint-enabled theme, these responsive styles can serve images sized for the browser.{% endtrans %}</li>
</ul>
<p>{% trans %}See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/8/core/modules/media">Media module</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,14 @@
---
label: 'Managing menus'
top_level: true
related:
- block.place
---
<h2>{% trans %}What is a menu?{% endtrans %}</h2>
<p>{% trans %}A menu is a collection of <em>menu links</em> used to navigate a web site. Menus and menu links can be provided by modules or site administrators.{% endtrans %}</p>
<h2>{% trans %}Managing menus overview{% endtrans %}</h2>
<p>{% trans %}The core Menu UI module provides a user interface for managing menus, including creating new menus, reordering menu links, and disabling links provided by modules. It also provides the ability for links to content items to be added to menus while editing, if configured on the content type. The core Custom Menu Links module provides the ability to add custom links to menus. Each menu can be displayed by placing a block in a theme region; some themes also can display a menu outside of the block system. See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional Resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/menu-concept.html">Concept: Menu (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,29 @@
---
label: 'Optimizing site performance'
top_level: true
---
<h2>{% trans %}What is site performance?{% endtrans %}</h2>
<p>{% trans %}Site performance, in this context, refers to speed factors such as the page load time and the response time after a user action on a page.{% endtrans %}</p>
<h2>{% trans %}What is caching?{% endtrans %}</h2>
<p>{% trans %}Caching is saving already-rendered HTML output and other calculated data for later use the first time it is needed. This saves time, because the next time the same data is needed it can be quickly retrieved instead of recalculated. Automatic caching systems also include mechanisms to delete cached calculations or mark them as no longer valid when the underlying data changes. To facilitate that, cached data has a <em>lifetime</em>, which is the maximum time before the data will be deleted from the cache (forcing recalculation).{% endtrans %}</p>
<h2>{% trans %}What is file aggregation?{% endtrans %}</h2>
<p>{% trans %}Aggregation is when CSS and JavaScript files are merged together and compressed into a format that is much smaller than the original. This allows for faster transmission and faster rendering on the other end.{% endtrans %}</p>
<h2>{% trans %}What can I do to improve my site's performance?{% endtrans %}</h2>
<p>{% trans %}The following core software modules and mechanisms can improve your site's performance:{% endtrans %}</p>
<dl>
<dt>{% trans %}Internal Page Cache module{% endtrans %}</dt>
<dd>{% trans %}Caches pages requested by users who are not logged in (anonymous users). Do not use if your site needs to send different output to different anonymous users.{% endtrans %}</dd>
<dt>{% trans %}Internal Dynamic Page Cache module{% endtrans %}</dt>
<dd>{% trans %}Caches data for both authenticated and anonymous users, with non-cacheable data in the page converted to placeholders and calculated when the page is requested.{% endtrans %}</dd>
<dt>{% trans %}Big Pipe module{% endtrans %}</dt>
<dd>{% trans %}Changes the way pages are sent to users, so that cacheable parts are sent out first with placeholders, and the uncacheable or personalized parts of the page are streamed afterwards. This allows the browser to render the bulk of the page quickly and fill in the details later.{% endtrans %}</dd>
<dt>{% trans %}Performance page settings{% endtrans %}</dt>
<dd>{% trans %}In the <em>Manage</em> administrative menu, if you navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em>Performance</em>, you will find a setting for the maximum cache lifetime, as well as the ability to turn on CSS and JavaScript file aggregation.{% endtrans %}</dd>
</dl>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/documentation/modules/internal_page_cache">{% trans %}Online documentation for the Internal Page Cache module{% endtrans %}</a></li>
<li><a href="https://www.drupal.org/documentation/modules/dynamic_page_cache">{% trans %}Online documentation for the Internal Dynamic Page Cache module{% endtrans %}</a></li>
<li><a href="https://www.drupal.org/documentation/modules/big_pipe">{% trans %}Online documentation for the BigPipe module{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,15 @@
---
label: 'Making your site secure'
top_level: true
---
<h2>{% trans %}What are security updates?{% endtrans %}</h2>
<p>{% trans %}Any software occasionally has bugs, and sometimes these bugs have security implications. When security bugs are fixed in the core software, modules, or themes that your site uses, they are released in a <em>security update</em>. You will need to apply security updates in order to keep your site secure.{% endtrans %}</p>
<h2>{% trans %}What are security advisories?{% endtrans %}</h2>
<p>{% trans %}A security advisory is a public announcement about a reported security problem in the core software. Contributed projects with a shield icon and "Stable releases for this project are covered by the security advisory policy" on their project page are also covered by Drupal's security advisory policy. Security advisories are managed by the <a href="https://www.drupal.org/drupal-security-team">Drupal Security Team</a>.{% endtrans %}</p>
<h2>{% trans %}Security tasks{% endtrans %}</h2>
<p>{% trans %}Keeping track of updates, updating the core software, and updating contributed modules and/or themes are all part of keeping your site secure. See the related topics listed below for specific tasks.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/security-chapter.html">Security and Maintenance (Drupal User Guide)</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/drupal-security-team/security-advisory-process-and-permissions-policy">Security advisory process and permissions policy</a>{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,19 @@
---
label: 'Using in-line (quick) settings editing'
related:
- core.ui_components
---
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Edit settings in place.{% endtrans %}</p>
<h2>{% trans %}What is quick editing?{% endtrans %}</h2>
<p>{% trans %}The core Settings Tray module provides the ability to quickly edit settings inline. It requires the core Contextual Links module in order to expose the links that let you edit in place.{% endtrans %}</p>
<h2>{% trans %}Who can edit settings in place?{% endtrans %}</h2>
<p>{% trans %}In order to follow these steps to edit settings in place, the core Settings Tray module must be installed. Also, either the core Toolbar module or a contributed replacement must be installed. You will need to have <em>Use contextual links</em> permission, as well as permission to edit the particular content or settings.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Find and visit a page on your site that has the settings that you would like to edit.{% endtrans %}</li>
<li>{% trans %}Click the contextual links <em>Edit</em> button on the toolbar (in most themes, it looks like a pencil). Contextual <em>Edit</em> links with the same icon will appear all over your page.{% endtrans %}</li>
<li>{% trans %}Find the contextual link for the part of the page you want to edit. For example, if you want to edit the settings for a block, the link should be in the top-right corner of the block, or top-left for right-to-left languages.{% endtrans %}</li>
<li>{% trans %}Click the link to open the contextual links menu, and click <em>Quick edit</em>. An editing form for the settings should appear on the page.{% endtrans %}</li>
<li>{% trans %}Make your edits and submit the form.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,7 @@
---
label: 'Tracking the content of your website'
top_level: true
---
<h2>{% trans %}Tracking overview{% endtrans %}</h2>
<p>{% trans %}The core History module tracks how recently users have viewed content items, and provides a Views field and filter that can be used to show users content that they haven't yet seen.{% endtrans %}</p>
<p>{% trans %}If you have one or more tracking modules installed on your site, see the related topics listed below for specific tasks.{% endtrans %}</p>

View File

@@ -0,0 +1,18 @@
---
label: 'Working with languages and translations'
top_level: true
related:
- block.place
- block.configure
---
{% set config_overview_topic = render_var(help_topic_link('core.config_overview')) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}What text can be translated in your site?{% endtrans %}</h2>
<p>{% trans %}There are three types of text that can be translated:{% endtrans %}</p>
<ul>
<li>{% trans %}Content (blocks, content items, etc.) can be written in English or another language, and can be translated into additional languages. See {{ content_structure_topic }} to learn more about content.{% endtrans %}</li>
<li>{% trans %}Many configuration items also include text that can be translated. Default configuration provided by your site's software is provided in English; you can also download community-provided translations. See {{ config_overview_topic }} to learn more about configuration.{% endtrans %}</li>
<li>{% trans %}User interface text that is provided by the core software, your install profile, themes, and modules is provided in English, but can be translated into other languages. You can also download translations that community-members have provided.{% endtrans %}</li>
</ul>
<h2>{% trans %}Working with languages overview{% endtrans %}</h2>
<p>{% trans %}The core Language module lets you add new languages to your site, provides the <em>Language switcher</em> block, and provides the ability to configure block visibility by language; the block and block visibility settings are only available if you have multiple languages configured. The core Content Translation, Configuration Translation, and Interface Translation modules let you translate content, configuration, and the built-in user interface, respectively. The core Update Manager module manages automatic downloads of community-provided translations of default configuration and user-interface text. See the related topics listed below for specific tasks.{% endtrans %}</p>

View File

@@ -0,0 +1,13 @@
---
label: 'Accessibility of the administrative interface'
related:
- core.ui_components
---
<h2>{% trans %}Overview of accessibility{% endtrans %}</h2>
<p>{% trans %}The core administrative interface has built-in compliance with many accessibility standards so that most pages are accessible to most users in their default state. However, certain pages become more accessible to some users through the use of a non-default or improved interface. These interfaces include:{% endtrans %}</p>
<dl>
<dt>{% trans %}Disabling drag-and-drop functionality{% endtrans %}</dt>
<dd>{% trans %}The default drag-and-drop user interface for ordering tables in the administrative interface presents a challenge for some users, including keyboard-only users and users of screen readers and other assistive technology. The drag-and-drop interface can be disabled in a table by clicking a link labeled <em>Show row weights</em> above the table. The replacement interface allows users to order the table by choosing numerical weights (with increasing numbers) instead of dragging table rows.{% endtrans %}</dd>
<dt>{% trans %}Enabling inline form errors{% endtrans %}</dt>
<dd>{% trans %}Errors that occur when you submit a form, such as not filling in a required field, are sometimes difficult for users to understand and locate. In order to make these errors easier to find, the best practice is to put a summary of the errors at the top of the form page. To make them easier to understand, the best practice is to display error messages with the form fields they are related to. Both of these practices are implemented by the core Inline Form Errors module.{% endtrans %}</dd>
</dl>

View File

@@ -0,0 +1,33 @@
---
label: 'Using the administrative interface'
top_level: true
related:
- block.overview
---
{% set accessibility_topic = render_var(help_topic_link('core.ui_accessibility')) %}
{% set settings_tray_topic = render_var(help_topic_link('core.settings_tray')) %}
{% set admin_link = render_var(help_route_link('/admin', 'system.admin')) %}
<h2>{% trans %}What administrative interface components are available?{% endtrans %}</h2>
<p>{% trans %}The following administrative interface components are provided by the core software and its modules (some contributed modules offer additional functionality):{% endtrans %}</p>
<ul>
<li>{% trans %}Accessibility features, to enable all users to perform administrative tasks. See {{ accessibility_topic }} for more information.{% endtrans %}</li>
<li>{% trans %}A menu system, which you can navigate to find pages for administrative tasks. The core Toolbar module displays this menu on the top or left side of the page (right side in right-to-left languages). There are also contributed module replacements for the core Toolbar module, with additional features, such as the <a href="https://www.drupal.org/project/admin_toolbar">Admin Toolbar module</a>.{% endtrans %}</li>
<li>{% trans %}The core Shortcuts module enhances the toolbar with a configurable list of links to commonly-used tasks.{% endtrans %}</li>
<li>{% trans %}If you install the core Contextual Links module, non-administrative pages will contain links leading to related administrative tasks.{% endtrans %}</li>
<li>{% trans %}In-place or <em>quick</em> editing. In-place editing of configuration is provided by the core Settings Tray module. See {{ settings_tray_topic }} for more information.{% endtrans %}</li>
<li>{% trans %}The core Help module displays help topics, and provides a Help block that can be placed on administrative pages to provide an overview of their functionality.{% endtrans %}</li>
</ul>
<h2>{% trans %}What are the sections of the administrative menu?{% endtrans %}</h2>
<p>{% trans %}The administrative menu, which you can navigate by visiting <em>{{ admin_link }}</em> on your site or by using an administrative toolbar, has the following sections (some may not be available, depending on which modules are currently installed on your site, and your permissions):{% endtrans %}</p>
<ul>
<li>{% trans %}<strong>Content:</strong> Find, manage, and create new pages; manage comments and files.{% endtrans %}</li>
<li>{% trans %}<strong>Structure:</strong> Place and edit blocks, set up content types and fields, configure menus, administer taxonomy, and configure some contributed modules.{% endtrans %}</li>
<li>{% trans %}<strong>Appearance:</strong> Switch between themes, install themes, and update existing themes.{% endtrans %}</li>
<li>{% trans %}<strong>Extend:</strong> Update, install, and uninstall modules.{% endtrans %}</li>
<li>{% trans %}<strong>Configuration:</strong> Configure the settings for various site functionality, including some contributed modules.{% endtrans %}</li>
<li>{% trans %}<strong>People:</strong> Manage user accounts and permissions.{% endtrans %}</li>
<li>{% trans %}<strong>Reports:</strong> Display information about site security, necessary updates, and site activity.{% endtrans %}</li>
<li>{% trans %}<strong>Help:</strong> Get help on using the administrative interface.{% endtrans %}</li>
</ul>
<h2>{% trans %}Administrative interface overview{% endtrans %}</h2>
<p>{% trans %}Install the core modules mentioned above to use the corresponding aspect of the administrative interface. See the related topics listed below for more details on some aspects of the administrative interface.{% endtrans %}</p>

View File

@@ -0,0 +1,32 @@
---
label: 'Enabling web services'
top_level: true
related:
- core.content_structure
---
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}What is a web service?{% endtrans %}</h2>
<p>{% trans %}A web service allows your site to provide its content and data to other web sites and applications. Typically, the data is transported via <a href="https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol">HTTP</a> in a serialized machine-readable format.{% endtrans %}</p>
<h2>{% trans %}What is serialization?{% endtrans %}</h2>
<p>{% trans %}Serialization is the process of converting complex data structures into text strings, so that they can be exchanged and stored. The reverse process is called <em>deserialization</em>. JSON and XML are the two most-commonly-used data serialization formats for web services.{% endtrans %}</p>
<h2>{% trans %}What is HTTP Basic authentication?{% endtrans %}</h2>
<p>{% trans %}<a href="http://en.wikipedia.org/wiki/Basic_access_authentication">HTTP Basic authentication</a> is a method for authenticating requests by sending a user name and password along with the request.{% endtrans %}</p>
<h2>{% trans %}What modules provide web services?{% endtrans %}</h2>
<p>{% trans %}The following core software modules provide web services:{% endtrans %}</p>
<dl>
<dt>{% trans %}JSON:API module{% endtrans %}</dt>
<dd>{% trans %}Exposes <em>entities</em> to other applications using a fully compliant implementation of the <a href="https://jsonapi.org">JSON:API Specification</a>. See {{ content_structure_topic }} for more information on content entities and fields.{% endtrans %}</dd>
<dt>{% trans %}RESTful Web Services module{% endtrans %}</dt>
<dd>{% trans %}Exposes entities and other resources to other applications using a <a href="https://en.wikipedia.org/wiki/Representational_state_transfer">REST</a> implementation. Data is exchanged using a serialization format such as JSON, and transferred using an authentication method such as HTTP Basic Authentication.{% endtrans %}</dd>
<dt>{% trans %}Serialization module{% endtrans %}</dt>
<dd>{% trans %}Provides a framework for adding specific serialization formats for other modules to use.{% endtrans %}</dd>
<dt>{% trans %}HTTP Basic Authentication module{% endtrans %}</dt>
<dd>{% trans %}Provides a way for web services to be authenticated using HTTP Basic authentication against site user accounts.{% endtrans %}</dd>
</dl>
<p>{% trans %}There are also contributed modules that provide web services.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/8/core/modules/rest">{% trans %}Online documentation for the RESTful Web Services module{% endtrans %}</a></li>
<li><a href="https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module">{% trans %}Online documentation for the JSON:API module{% endtrans %}</a></li>
<li><a href="https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/jsonapi-vs-cores-rest-module">{% trans %}Comparison of the RESTFul Web Services and JSON:API modules{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,27 @@
---
label: 'Configuring help search'
related:
- block.place
- system.cache
- core.cron
- search.overview
---
{% set extend_link_text %}{% trans %}Extend{% endtrans %}{% endset %}
{% set help_link_text %}{% trans %}Help{% endtrans %}{% endset %}
{% set extend_link = render_var(help_route_link(extend_link_text, 'system.modules_list')) %}
{% set help_link = render_var(help_route_link(help_link_text, 'help.main')) %}
{% set cache_topic = render_var(help_topic_link('system.cache')) %}
{% set cron_topic = render_var(help_topic_link('core.cron')) %}
<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 %}In the <em>Manage</em> administrative menu, navigate to <em>{{ extend_link }}</em>. Verify that the Search, Help, 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> &gt; <em>Search and metadata</em> &gt; <em>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, see {{ cron_topic }} about how to 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 {{ cache_topic }}, 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> &gt; <em>Block layout</em>.{% endtrans %}</li>
<li>{% trans %}Click the link for your administrative theme (such as the core Claro theme), near the top of the page, and verify that there is already a search block for help located in the Help region. If not, follow the steps in the related topic to place the <em>Search form</em> block in the Help region. When configuring the block, choose <em>Help</em> as the search page, and in the <em>Pages</em> tab under <em>Visibility</em>, enter <em>/admin/help</em> to make the search form only visible on the main <em>Help</em> page.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>{{ help_link }}</em>. Verify that the search block is visible, and try a search.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,26 @@
---
label: 'Working with help topics'
top_level: true
related:
- help.help_topic_search
- locale.translate_strings
---
{% set help_link_text %}{% trans %}Help{% endtrans %}{% endset %}
{% set help_link = render_var(help_route_link(help_link_text, 'help.main')) %}
{% set translate_text %}{% trans %}User interface translation{% endtrans %}{% endset %}
{% set translate_link = render_var(help_route_link(translate_text, 'locale.translate_page')) %}
{% set help_search_topic = render_var(help_topic_link('help.help_topic_search')) %}
<h2>{% trans %}What is a help topic?{% endtrans %}</h2>
<p>{% trans %}A help topic describes a concept, or steps to accomplish a task, related to a feature provided by one or more modules or themes. If the core Search module is enabled, these topics are also searchable.{% endtrans %}</p>
<h2>{% trans %}Where are help topics listed?{% endtrans %}</h2>
<p>{% trans %}The top-level help topics are listed at {{ help_link }}. Links to other topics, including non-top-level help topics, can be found under the "Related" heading when viewing a topic page.{% endtrans %}</p>
<h2>{% trans %}How are help topics provided?{% endtrans %}</h2>
<p>{% trans %}Modules and themes can provide help topics as Twig-file-based plugins in a project sub-directory called <em>help_topics</em>; plugin metadata 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/help_topics</em> as a guide when writing and formatting a help topic plugin for your theme or module.{% endtrans %}</p>
<h2>{% trans %}How are help topics translated?{% endtrans %}</h2>
<p>{% trans %}The title and body text of help topics provided by contributed modules and themes are translatable using {{ translate_link }} (provided by Interface Translation module). 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.{% endtrans %}</p>
<h2>{% trans %}How can users search for help topics?{% endtrans %}</h2>
<p>{% trans %}To enable users to search help, including help topics, 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 Claro 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 related topic, {{ help_search_topic }}, for step-by-step instructions.{% endtrans %}</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/node/3074421">{% trans %}Help Topics Standards{% endtrans %}</a></li>
</ul>

View File

@@ -0,0 +1,69 @@
<?php
namespace Drupal\help\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Plugin annotation object for help page section plugins.
*
* Plugin Namespace: Plugin\HelpSection
*
* For a working example, see \Drupal\help\Plugin\HelpSection\HookHelpSection.
*
* @see \Drupal\help\HelpSectionPluginInterface
* @see \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase
* @see \Drupal\help\HelpSectionManager
* @see hook_help_section_info_alter()
* @see plugin_api
*
* @Annotation
*/
class HelpSection extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The text to use as the title of the help page section.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $title;
/**
* The description of the help page section.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
/**
* The (optional) permission needed to view the help section.
*
* Only set if this section needs its own permission, beyond the generic
* 'access help pages' permission needed to see the /admin/help
* page itself.
*
* @var string
*/
public $permission = '';
/**
* An optional weight for the help section.
*
* The sections will be ordered by this weight on the help page.
*
* @var int
*/
public $weight = 0;
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\help\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a HelpSection attribute object for plugin discovery.
*
* Plugin Namespace: Plugin\HelpSection
*
* For a working example, see \Drupal\help\Plugin\HelpSection\HookHelpSection.
*
* @see \Drupal\help\HelpSectionPluginInterface
* @see \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase
* @see \Drupal\help\HelpSectionManager
* @see hook_help_section_info_alter()
* @see plugin_api
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class HelpSection extends Plugin {
/**
* Constructs a HelpSection attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $title
* The text to use as the title of the help page section.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $description
* (optional) The description of the help page section.
* @param string|null $permission
* (optional) The permission required to access the help page section.
*
* Only set if this section needs its own permission, beyond the generic
* 'access help pages' permission needed to see the /admin/help
* page itself.
* @param int|null $weight
* (optional) The weight of the help page section.
* @param class-string|null $deriver
* (optional) The deriver class.
*
* The sections will be ordered by this weight on the help page.
*/
public function __construct(
public readonly string $id,
public readonly TranslatableMarkup $title,
public readonly ?TranslatableMarkup $description = NULL,
public readonly ?string $permission = NULL,
public readonly ?int $weight = NULL,
public readonly ?string $deriver = NULL,
) {}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace Drupal\help\Controller;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\help\HelpSectionManager;
use Drupal\system\ModuleAdminLinksHelper;
use Drupal\user\ModulePermissionsLinkHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Controller routines for help routes.
*/
class HelpController extends ControllerBase {
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The help section plugin manager.
*
* @var \Drupal\help\HelpSectionManager
*/
protected $helpManager;
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* The module admin links service.
*
* @var \Drupal\system\ModuleAdminLinksHelper
*/
protected $moduleAdminLinks;
/**
* The module permissions link service.
*
* @var \Drupal\user\ModulePermissionsLinkHelper
*/
protected $modulePermissionsLinks;
/**
* Creates a new HelpController.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\help\HelpSectionManager $help_manager
* The help section manager.
* @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
* The module extension list.
* @param \Drupal\system\ModuleAdminLinksHelper|null $module_admin_links
* The module admin links.
* @param \Drupal\user\ModulePermissionsLinkHelper|null $module_permissions_link
* The module permissions link.
*/
public function __construct(RouteMatchInterface $route_match, HelpSectionManager $help_manager, ModuleExtensionList $module_extension_list, ?ModuleAdminLinksHelper $module_admin_links = NULL, ?ModulePermissionsLinkHelper $module_permissions_link = NULL) {
$this->routeMatch = $route_match;
$this->helpManager = $help_manager;
$this->moduleExtensionList = $module_extension_list;
if (!isset($module_admin_links)) {
@trigger_error('Calling ' . __METHOD__ . ' without the $module_admin_tasks_helper argument is deprecated in drupal:10.2.0 and the $module_admin_tasks_helper argument will be required in drupal:11.0.0. See https://www.drupal.org/node/3038972', E_USER_DEPRECATED);
$module_admin_links = \Drupal::service('system.module_admin_links_helper');
}
$this->moduleAdminLinks = $module_admin_links;
if (!isset($module_permissions_link)) {
@trigger_error('Calling HelpController::__construct() without the $module_permissions_link argument is deprecated in drupal:9.3.0 and the $module_permissions_link argument will be required in drupal:10.0.0. See https://www.drupal.org/node/3038972', E_USER_DEPRECATED);
$module_permissions_link = \Drupal::service('user.module_permissions_link_helper');
}
$this->modulePermissionsLinks = $module_permissions_link;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('current_route_match'),
$container->get('plugin.manager.help_section'),
$container->get('extension.list.module'),
$container->get('system.module_admin_links_helper'),
$container->get('user.module_permissions_link_helper')
);
}
/**
* Prints a page listing various types of help.
*
* The page has sections defined by \Drupal\help\HelpSectionPluginInterface
* plugins.
*
* @return array
* A render array for the help page.
*/
public function helpMain() {
$output = [];
// We are checking permissions, so add the user.permissions cache context.
$cacheability = new CacheableMetadata();
$cacheability->addCacheContexts(['user.permissions']);
$plugins = $this->helpManager->getDefinitions();
$cacheability->addCacheableDependency($this->helpManager);
foreach ($plugins as $plugin_id => $plugin_definition) {
// Check the provided permission.
if (!empty($plugin_definition['permission']) && !$this->currentUser()->hasPermission($plugin_definition['permission'])) {
continue;
}
// Add the section to the page.
/** @var \Drupal\help\HelpSectionPluginInterface $plugin */
$plugin = $this->helpManager->createInstance($plugin_id);
$this_output = [
'#theme' => 'help_section',
'#title' => $plugin->getTitle(),
'#description' => $plugin->getDescription(),
'#empty' => $this->t('There is currently nothing in this section.'),
'#links' => [],
'#weight' => $plugin_definition['weight'],
];
$links = $plugin->listTopics();
if (is_array($links) && count($links)) {
$this_output['#links'] = $links;
}
$cacheability->addCacheableDependency($plugin);
$output[$plugin_id] = $this_output;
}
$cacheability->applyTo($output);
return $output;
}
/**
* Prints a page listing general help for a module.
*
* @param string $name
* A module name to display a help page for.
*
* @return array
* A render array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function helpPage($name) {
$build = [];
if ($this->moduleHandler()->hasImplementations('help', $name)) {
$module_name = $this->moduleExtensionList->getName($name);
$build['#title'] = $module_name;
$info = $this->moduleExtensionList->getExtensionInfo($name);
if ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
$this->messenger()->addWarning($this->t('This module is experimental. <a href=":url">Experimental modules</a> are provided for testing purposes only. Use at your own risk.', [':url' => 'https://www.drupal.org/core/experimental']));
}
$temp = $this->moduleHandler()->invoke($name, 'help', ["help.page.$name", $this->routeMatch]);
if (empty($temp)) {
$build['top'] = ['#markup' => $this->t('No help is available for module %module.', ['%module' => $module_name])];
}
else {
if (!is_array($temp)) {
$temp = ['#markup' => $temp];
}
$build['top'] = $temp;
}
// Only print list of administration pages if the module in question has
// any such pages associated with it.
$admin_tasks = $this->moduleAdminLinks->getModuleAdminLinks($name);
if ($module_permissions_link = $this->modulePermissionsLinks->getModulePermissionsLink($name, $info['name'])) {
$admin_tasks["user.admin_permissions.{$name}"] = $module_permissions_link;
}
if (!empty($admin_tasks)) {
$links = [];
foreach ($admin_tasks as $task) {
$link['url'] = $task['url'];
$link['title'] = $task['title'];
$links[] = $link;
}
$build['links'] = [
'#theme' => 'links__help',
'#heading' => [
'level' => 'h3',
'text' => $this->t('@module administration pages', ['@module' => $module_name]),
],
'#links' => $links,
];
}
return $build;
}
else {
throw new NotFoundHttpException();
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\help\Controller;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\help\HelpTopicPluginManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Controller for help topic plugins.
*
* @internal
* Controller classes are internal.
*/
class HelpTopicPluginController extends ControllerBase {
/**
* Constructs a HelpTopicPluginController object.
*
* @param \Drupal\help\HelpTopicPluginManagerInterface $helpTopicPluginManager
* The help topic plugin manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(protected HelpTopicPluginManagerInterface $helpTopicPluginManager, protected RendererInterface $renderer) {
}
/**
* Displays a help topic page.
*
* @param string $id
* The plugin ID. Maps to the {id} placeholder in the
* help.help_topic route.
*
* @return array
* A render array with the contents of a help topic page.
*/
public function viewHelpTopic($id) {
$build = [];
if (!$this->helpTopicPluginManager->hasDefinition($id)) {
throw new NotFoundHttpException();
}
/** @var \Drupal\help\HelpTopicPluginInterface $help_topic */
$help_topic = $this->helpTopicPluginManager->createInstance($id);
$build['#body'] = $help_topic->getBody();
$this->renderer->addCacheableDependency($build, $help_topic);
// Build the related topics section, starting with the list this topic
// says are related.
$links = [];
$related = $help_topic->getRelated();
foreach ($related as $other_id) {
if ($other_id !== $id) {
/** @var \Drupal\help\HelpTopicPluginInterface $topic */
$topic = $this->helpTopicPluginManager->createInstance($other_id);
$links[$other_id] = [
'title' => $topic->getLabel(),
'url' => Url::fromRoute('help.help_topic', ['id' => $other_id]),
];
$this->renderer->addCacheableDependency($build, $topic);
}
}
if (count($links)) {
uasort($links, [SortArray::class, 'sortByTitleElement']);
$build['#related'] = [
'#theme' => 'links__related',
'#heading' => [
'text' => $this->t('Related topics'),
'level' => 'h2',
],
'#links' => $links,
];
}
$build['#theme'] = 'help_topic';
$build['#title'] = $help_topic->getLabel();
return $build;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\help;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a breadcrumb builder for help topic pages.
*
* @internal
* Tagged services are internal.
*/
class HelpBreadcrumbBuilder implements BreadcrumbBuilderInterface {
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return $route_match->getRouteName() == 'help.help_topic';
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$breadcrumb = new Breadcrumb();
$breadcrumb->addCacheContexts(['url.path.parent']);
$breadcrumb->addLink(Link::createFromRoute(new TranslatableMarkup('Home'), '<front>'));
$breadcrumb->addLink(Link::createFromRoute(new TranslatableMarkup('Administration'), 'system.admin'));
$breadcrumb->addLink(Link::createFromRoute(new TranslatableMarkup('Help'), 'help.main'));
return $breadcrumb;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Drupal\help;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\help\Attribute\HelpSection;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages help page section plugins.
*
* @see \Drupal\help\HelpSectionPluginInterface
* @see \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase
* @see \Drupal\help\Annotation\HelpSection
* @see hook_help_section_info_alter()
*/
class HelpSectionManager extends DefaultPluginManager {
/**
* The search manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected ?PluginManagerInterface $searchManager = NULL;
/**
* Constructs a new HelpSectionManager.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler for the alter hook.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/HelpSection', $namespaces, $module_handler, 'Drupal\help\HelpSectionPluginInterface', HelpSection::class, 'Drupal\help\Annotation\HelpSection');
$this->alterInfo('help_section_info');
$this->setCacheBackend($cache_backend, 'help_section_plugins');
}
/**
* 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();
$version = \Drupal::service('update.update_hook_registry')->getInstalledVersion('help');
if ($this->searchManager && $version >= 10200) {
// 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();
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\help;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
/**
* Provides an interface for a plugin for a section of the /admin/help page.
*
* Plugins of this type need to be annotated with
* \Drupal\help\Annotation\HelpSection annotation, and placed in the
* Plugin\HelpSection namespace directory. They are managed by the
* \Drupal\help\HelpSectionManager plugin manager class. There is a base
* class that may be helpful:
* \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase.
*/
interface HelpSectionPluginInterface extends PluginInspectionInterface, CacheableDependencyInterface {
/**
* Returns the title of the help section.
*
* @return string
* The title text, which could be a plain string or an object that can be
* cast to a string.
*/
public function getTitle();
/**
* Returns the description text for the help section.
*
* @return string
* The description text, which could be a plain string or an object that
* can be cast to a string.
*/
public function getDescription();
/**
* Returns a list of topics to show in the help section.
*
* @return array
* A sorted list of topic links or render arrays for topic links. The links
* will be shown in the help section; if the returned array of links is
* empty, the section will be shown with some generic empty text.
*/
public function listTopics();
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Drupal\help;
use Drupal\Component\Discovery\DiscoveryException;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\FileSystem\RegexDirectoryIterator;
use Drupal\Component\FrontMatter\FrontMatter;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Discovers help topic plugins from Twig files in help_topics directories.
*
* @see \Drupal\help\HelpTopicTwig
* @see \Drupal\help\HelpTopicTwigLoader
*
* @internal
* Tagged services are internal.
*/
class HelpTopicDiscovery implements DiscoveryInterface {
use DiscoveryTrait;
/**
* Defines the key in the discovered data where the file path is stored.
*/
const FILE_KEY = '_discovered_file_path';
/**
* An array of directories to scan, keyed by the provider.
*
* The value can either be a string or an array of strings. The string values
* should be the path of a directory to scan.
*
* @var array
*/
protected $directories = [];
/**
* Constructs a HelpTopicDiscovery object.
*
* @param array $directories
* An array of directories to scan, keyed by the provider. The value can
* either be a string or an array of strings. The string values should be
* the path of a directory to scan.
*/
public function __construct(array $directories) {
$this->directories = $directories;
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
$plugins = $this->findAll();
// Flatten definitions into what's expected from plugins.
$definitions = [];
foreach ($plugins as $list) {
foreach ($list as $id => $definition) {
$definitions[$id] = $definition;
}
}
return $definitions;
}
/**
* Returns an array of discoverable items.
*
* @return array
* An array of discovered data keyed by provider.
*
* @throws \Drupal\Component\Discovery\DiscoveryException
* Exception thrown if there is a problem during discovery.
*/
public function findAll() {
$all = [];
$files = $this->findFiles();
$file_cache = FileCacheFactory::get('help_topic_discovery:help_topics');
// Try to load from the file cache first.
foreach ($file_cache->getMultiple(array_keys($files)) as $file => $data) {
$all[$files[$file]][$data['id']] = $data;
unset($files[$file]);
}
// If there are files left that were not returned from the cache, load and
// parse them now. This list was flipped above and is keyed by filename.
if ($files) {
foreach ($files as $file => $provider) {
$plugin_id = substr(basename($file), 0, -10);
// The plugin ID begins with provider.
[$file_name_provider] = explode('.', $plugin_id, 2);
$data = [
// The plugin ID is derived from the filename. The extension
// '.html.twig' is removed.
'id' => $plugin_id,
'provider' => $file_name_provider,
'class' => HelpTopicTwig::class,
static::FILE_KEY => $file,
];
// Get the rest of the plugin definition from front matter contained in
// the help topic Twig file.
try {
$front_matter = FrontMatter::create(file_get_contents($file), Yaml::class)->getData();
}
catch (InvalidDataTypeException $e) {
throw new DiscoveryException(sprintf('Malformed YAML in help topic "%s": %s.', $file, $e->getMessage()));
}
foreach ($front_matter as $key => $value) {
switch ($key) {
case 'related':
if (!is_array($value)) {
throw new DiscoveryException("$file contains invalid value for 'related' key, the value must be an array of strings");
}
$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'");
}
}
if (!isset($data['label'])) {
throw new DiscoveryException("$file does not contain the required key with name='label'");
}
$all[$provider][$data['id']] = $data;
$file_cache->set($file, $data);
}
}
return $all;
}
/**
* Returns an array of providers keyed by file path.
*
* @return array
* An array of providers keyed by file path.
*/
protected function findFiles() {
$file_list = [];
foreach ($this->directories as $provider => $directories) {
$directories = (array) $directories;
foreach ($directories as $directory) {
if (is_dir($directory)) {
/** @var \SplFileInfo $fileInfo */
$iterator = new RegexDirectoryIterator($directory, '/\.html\.twig$/i');
foreach ($iterator as $fileInfo) {
$file_list[$fileInfo->getPathname()] = $provider;
}
}
}
}
return $file_list;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\help;
use Drupal\Core\Link;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Url;
/**
* Base class for help topic plugins.
*
* @internal
* Plugin classes are internal.
*/
abstract class HelpTopicPluginBase extends PluginBase implements HelpTopicPluginInterface {
/**
* The name of the module or theme providing the help topic.
*/
public function getProvider() {
return $this->pluginDefinition['provider'];
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function isTopLevel() {
return $this->pluginDefinition['top_level'];
}
/**
* {@inheritdoc}
*/
public function getRelated() {
return $this->pluginDefinition['related'];
}
/**
* {@inheritdoc}
*/
public function toUrl(array $options = []) {
return Url::fromRoute('help.help_topic', ['id' => $this->getPluginId()], $options);
}
/**
* {@inheritdoc}
*/
public function toLink($text = NULL, array $options = []) {
if (!$text) {
$text = $this->getLabel();
}
return Link::createFromRoute($text, 'help.help_topic', ['id' => $this->getPluginId()], $options);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\help;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
/**
* Defines an interface for help topic plugin classes.
*
* @see \Drupal\help\HelpTopicPluginManager
*/
interface HelpTopicPluginInterface extends PluginInspectionInterface, DerivativeInspectionInterface, CacheableDependencyInterface {
/**
* Returns the label of the topic.
*
* @return string
* The label of the topic.
*/
public function getLabel();
/**
* Returns the body of the topic.
*
* @return array
* A render array representing the body.
*/
public function getBody();
/**
* Returns whether this is a top-level topic or not.
*
* @return bool
* TRUE if this is a topic that should be displayed on the Help topics
* list; FALSE if not.
*/
public function isTopLevel();
/**
* Returns the IDs of related topics.
*
* @return string[]
* Array of the IDs of related topics.
*/
public function getRelated();
/**
* Returns the URL for viewing the help topic.
*
* @param array $options
* (optional) See
* \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for the
* available options.
*
* @return \Drupal\Core\Url
* A URL object containing the URL for viewing the help topic.
*/
public function toUrl(array $options = []);
/**
* Returns a link for viewing the help topic.
*
* @param string|null $text
* (optional) Link text to use for the link. If NULL, defaults to the
* topic title.
* @param array $options
* (optional) See
* \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for the
* available options.
*
* @return \Drupal\Core\Link
* A link object for viewing the topic.
*/
public function toLink($text = NULL, array $options = []);
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Drupal\help;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
/**
* Provides the default help_topic manager.
*
* Modules and themes can provide help topics in .html.twig files called
* provider.name_of_topic.html.twig inside the module or theme sub-directory
* help_topics. The provider is validated to be the extension that provides the
* help topic.
*
* The Twig file must contain YAML front matter with a key named 'label'. It can
* also contain keys named 'top_level' and 'related'. For example:
* @code
* ---
* label: 'Configuring error responses, including 403/404 pages'
*
* # Related help topics in an array.
* related:
* - core.config_basic
* - core.maintenance
*
* # If the value is true then the help topic will appear on admin/help.
* top_level: true
* ---
* @endcode
*
* In addition, modules wishing to add plugins can define them in a
* module_name.help_topics.yml file, with the plugin ID as the heading for
* each entry, and these properties:
* - id: The plugin ID.
* - class: The name of your plugin class, implementing
* \Drupal\help\HelpTopicPluginInterface.
* - top_level: TRUE if the topic is top-level.
* - related: Array of IDs of topics this one is related to.
* - Additional properties that your plugin class needs, such as 'label'.
*
* You can also provide an entry that designates a plugin deriver class in your
* help_topics.yml file, with a heading giving a prefix ID for your group of
* derived plugins, and a 'deriver' property giving the name of a class
* implementing \Drupal\Component\Plugin\Derivative\DeriverInterface. Example:
* @code
* my_module_prefix:
* deriver: 'Drupal\my_module\Plugin\Deriver\HelpTopicDeriver'
* @endcode
*
* @ingroup help_docs
*
* @see \Drupal\help\HelpTopicDiscovery
* @see \Drupal\help\HelpTopicTwig
* @see \Drupal\help\HelpTopicTwigLoader
* @see \Drupal\help\HelpTopicPluginInterface
* @see \Drupal\help\HelpTopicPluginBase
* @see hook_help_topics_info_alter()
* @see plugin_api
* @see \Drupal\Component\Plugin\Derivative\DeriverInterface
*/
class HelpTopicPluginManager extends DefaultPluginManager implements HelpTopicPluginManagerInterface {
/**
* Provides default values for all help topic plugins.
*
* @var array
*/
protected $defaults = [
// The plugin ID.
'id' => '',
// The title of the help topic plugin.
'label' => '',
// Whether or not the topic should appear on the help topics list.
'top_level' => '',
// List of related topic machine names.
'related' => [],
// The class used to instantiate the plugin.
'class' => '',
];
/**
* Constructs a new HelpTopicManager object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $themeHandler
* The theme handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param string $root
* The app root.
*/
public function __construct(ModuleHandlerInterface $module_handler, protected ThemeHandlerInterface $themeHandler, CacheBackendInterface $cache_backend, protected string $root) {
// Note that the parent construct is not called because this class does not use
// annotated class discovery.
$this->moduleHandler = $module_handler;
$this->alterInfo('help_topics_info');
$this->setCacheBackend($cache_backend, 'help_topics');
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$module_directories = $this->moduleHandler->getModuleDirectories();
$all_directories = array_merge(
['core' => $this->root . '/core'],
$module_directories,
$this->themeHandler->getThemeDirectories()
);
// Search for Twig help topics in subdirectory help_topics, under
// modules/profiles, themes, and the core directory.
$all_directories = array_map(function ($dir) {
return [$dir . '/help_topics'];
}, $all_directories);
$discovery = new HelpTopicDiscovery($all_directories);
// Also allow modules/profiles to extend help topic discovery to their
// own plugins and derivers, in my_module.help_topics.yml files.
$discovery = new YamlDiscoveryDecorator($discovery, 'help_topics', $module_directories);
$discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
$this->discovery = $discovery;
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
protected function providerExists($provider) {
return $this->moduleHandler->moduleExists($provider) || $this->themeHandler->themeExists($provider);
}
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = parent::findDefinitions();
// At this point the plugin list only contains valid plugins. Ensure all
// related plugins exist and the relationship is bi-directional. This
// ensures topics are listed on their related topics.
foreach ($definitions as $plugin_id => $plugin_definition) {
foreach ($plugin_definition['related'] as $key => $related_id) {
// If the related help topic does not exist it might be for a module
// that is not installed. Remove it.
// @todo Discuss this more as this could cause silent errors but it
// offers useful functionality to relate to a help topic provided by
// extensions that are yet to be installed.
// https://www.drupal.org/i/3360133
if (!isset($definitions[$related_id])) {
unset($definitions[$plugin_id]['related'][$key]);
continue;
}
// Make the related relationship bi-directional.
if (isset($definitions[$related_id]) && !in_array($plugin_id, $definitions[$related_id]['related'], TRUE)) {
$definitions[$related_id]['related'][] = $plugin_id;
}
}
}
return $definitions;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Drupal\help;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Defines an interface for managing help topics and storing their definitions.
*/
interface HelpTopicPluginManagerInterface extends PluginManagerInterface {
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Drupal\help;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Template\TwigEnvironment;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Represents a help topic plugin whose definition comes from a Twig file.
*
* @see \Drupal\help\HelpTopicDiscovery
* @see \Drupal\help\HelpTopicTwigLoader
* @see \Drupal\help\HelpTopicPluginManager
*
* @internal
* Plugin classes are internal.
*/
class HelpTopicTwig extends HelpTopicPluginBase implements ContainerFactoryPluginInterface {
/**
* HelpTopicPluginBase constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Template\TwigEnvironment $twig
* The Twig environment.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, protected TwigEnvironment $twig) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('twig')
);
}
/**
* {@inheritdoc}
*/
public function getBody() {
return [
'#markup' => $this->twig->load('@help_topics/' . $this->getPluginId() . '.html.twig')->render(),
];
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return ['core.extension'];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Drupal\help;
use Drupal\Component\FrontMatter\FrontMatter;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Serialization\Yaml;
use Twig\Error\LoaderError;
use Twig\Loader\FilesystemLoader;
use Twig\Source;
/**
* Loads help topic Twig files from the filesystem.
*
* This loader adds module and theme help topic paths to a help_topics namespace
* to the Twig filesystem loader so that help_topics can be referenced, using
* '@help-topic/pluginId.html.twig'.
*
* @see \Drupal\help\HelpTopicDiscovery
* @see \Drupal\help\HelpTopicTwig
*
* @internal
* Tagged services are internal.
*/
class HelpTopicTwigLoader extends FilesystemLoader {
/**
* {@inheritdoc}
*/
const MAIN_NAMESPACE = 'help_topics';
/**
* Constructs a new HelpTopicTwigLoader object.
*
* @param string $root_path
* The root path.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler service.
*/
public function __construct($root_path, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
parent::__construct([], $root_path);
// Add help_topics directories for modules and themes in the 'help_topic'
// namespace, plus core.
$this->addExtension($root_path . '/core');
array_map([$this, 'addExtension'], $module_handler->getModuleDirectories());
array_map([$this, 'addExtension'], $theme_handler->getThemeDirectories());
}
/**
* Adds an extensions help_topics directory to the Twig loader.
*
* @param $path
* The path to the extension.
*/
protected function addExtension($path) {
$path .= DIRECTORY_SEPARATOR . 'help_topics';
if (is_dir($path)) {
$this->cache = $this->errorCache = [];
$this->paths[self::MAIN_NAMESPACE][] = rtrim($path, '/\\');
}
}
/**
* {@inheritdoc}
*/
public function getSourceContext(string $name): Source {
$path = $this->findTemplate($name);
$contents = file_get_contents($path);
try {
// Note: always use \Drupal\Core\Serialization\Yaml here instead of the
// "serializer.yaml" service. This allows the core serializer to utilize
// core related functionality which isn't available as the standalone
// component based serializer.
$front_matter = new FrontMatter($contents, Yaml::class);
// Reconstruct the content if there is front matter data detected. Prepend
// the source with {% line \d+ %} to inform Twig that the source code
// actually starts on a different line past the front matter data. This is
// particularly useful when used in error reporting.
if ($front_matter->getData() && ($line = $front_matter->getLine())) {
$contents = "{% line $line %}" . $front_matter->getContent();
}
}
catch (InvalidDataTypeException $e) {
throw new LoaderError(sprintf('Malformed YAML in help topic "%s": %s.', $path, $e->getMessage()));
}
return new Source($contents, $name, $path);
}
/**
* {@inheritdoc}
*/
protected function findTemplate($name, $throw = TRUE) {
if (!str_ends_with($name, '.html.twig')) {
if (!$throw) {
return NULL;
}
$extension = pathinfo($name, PATHINFO_EXTENSION);
throw new LoaderError(sprintf("Help topic %s has an invalid file extension (%s). Only help topics ending .html.twig are allowed.", $name, $extension));
}
return parent::findTemplate($name, $throw);
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Drupal\help;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Defines and registers Drupal Twig extensions for rendering help topics.
*
* @internal
* Tagged services are internal.
*/
class HelpTwigExtension extends AbstractExtension {
use StringTranslationTrait;
/**
* Constructs a \Drupal\help\HelpTwigExtension.
*
* @param \Drupal\Core\Access\AccessManagerInterface $accessManager
* The access manager.
* @param \Drupal\help\HelpTopicPluginManagerInterface $pluginManager
* The help topic plugin manager service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(protected AccessManagerInterface $accessManager, protected HelpTopicPluginManagerInterface $pluginManager, TranslationInterface $string_translation) {
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function getFunctions() {
return [
new TwigFunction('help_route_link', [$this, 'getRouteLink']),
new TwigFunction('help_topic_link', [$this, 'getTopicLink']),
];
}
/**
* Returns a link or plain text, given text, route name, and parameters.
*
* @param string $text
* The link text.
* @param string $route
* The name of the route.
* @param array $parameters
* (optional) An associative array of route parameter names and values.
* @param array $options
* (optional) An associative array of additional options. The 'absolute'
* option is forced to be TRUE.
*
* @return array
* A render array with a generated absolute link to the given route. If
* the user does not have permission for the route, or an exception occurs,
* such as a missing route or missing parameters, the render array is for
* the link text as a plain string instead.
*
* @see \Drupal\Core\Template\TwigExtension::getUrl()
*/
public function getRouteLink(string $text, string $route, array $parameters = [], array $options = []): array {
assert($this->accessManager instanceof AccessManagerInterface, "The access manager hasn't been set up. Any configuration YAML file with a service directive dealing with the Twig configuration can cause this, most likely found in a recently installed or changed module.");
$bubbles = new BubbleableMetadata();
$bubbles->addCacheTags(['route_match']);
try {
$access_object = $this->accessManager->checkNamedRoute($route, $parameters, NULL, TRUE);
$bubbles->addCacheableDependency($access_object);
if ($access_object->isAllowed()) {
$options['absolute'] = TRUE;
$url = Url::fromRoute($route, $parameters, $options);
// Generate the URL to check for parameter problems and collect
// cache metadata.
$generated = $url->toString(TRUE);
$bubbles->addCacheableDependency($generated);
$build = [
'#title' => $text,
'#type' => 'link',
'#url' => $url,
];
}
else {
// If the user doesn't have access, return the link text.
$build = ['#markup' => $text];
}
}
catch (RouteNotFoundException | MissingMandatoryParametersException | InvalidParameterException $e) {
// If the route had one of these exceptions, return the link text.
$build = ['#markup' => $text];
}
$bubbles->applyTo($build);
return $build;
}
/**
* Returns a link to a help topic, or the title of the topic.
*
* @param string $topic_id
* The help topic ID.
*
* @return array
* A render array with a generated absolute link to the given topic. If
* the user does not have permission to view the topic, or an exception
* occurs, such as the topic not being defined due to a module not being
* installed, a default string is returned.
*
* @see \Drupal\Core\Template\TwigExtension::getUrl()
*/
public function getTopicLink(string $topic_id): array {
assert($this->pluginManager instanceof HelpTopicPluginManagerInterface, "The plugin manager hasn't been set up. Any configuration YAML file with a service directive dealing with the Twig configuration can cause this, most likely found in a recently installed or changed module.");
$bubbles = new BubbleableMetadata();
$bubbles->addCacheableDependency($this->pluginManager);
try {
$plugin = $this->pluginManager->createInstance($topic_id);
}
catch (PluginNotFoundException $e) {
// Not a topic.
$plugin = FALSE;
}
if ($plugin) {
$parameters = ['id' => $topic_id];
$route = 'help.help_topic';
$build = $this->getRouteLink($plugin->getLabel(), $route, $parameters);
$bubbles->addCacheableDependency($plugin);
}
else {
$build = [
'#markup' => $this->t('Missing help topic %topic', [
'%topic' => $topic_id,
]),
];
}
$bubbles->applyTo($build);
return $build;
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Drupal\help\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a 'Help' block.
*/
#[Block(
id: "help_block",
admin_label: new TranslatableMarkup("Help"),
forms: ['settings_tray' => FALSE]
)]
class HelpBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Creates a HelpBlock instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, ModuleHandlerInterface $module_handler, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->request = $request;
$this->moduleHandler = $module_handler;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('request_stack')->getCurrentRequest(),
$container->get('module_handler'),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function build() {
// Do not show on a 403 or 404 page.
if ($this->request->attributes->has('exception')) {
return [];
}
$build = [];
$this->moduleHandler->invokeAllWith('help', function (callable $hook, string $module) use (&$build) {
// Don't add empty strings to $build array.
if ($help = $hook($this->routeMatch->getRouteName(), $this->routeMatch)) {
// Convert strings to #markup render arrays so that they will XSS admin
// filtered.
$build[] = is_array($help) ? $help : ['#markup' => $help];
}
});
return $build;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Drupal\help\Plugin\HelpSection;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Plugin\PluginBase;
use Drupal\help\HelpSectionPluginInterface;
/**
* Provides a base class for help section plugins.
*
* @see \Drupal\help\HelpSectionPluginInterface
* @see \Drupal\help\Annotation\HelpSection
* @see \Drupal\help\HelpSectionManager
*/
abstract class HelpSectionPluginBase extends PluginBase implements HelpSectionPluginInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function getTitle() {
return $this->getPluginDefinition()['title'];
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->getPluginDefinition()['description'];
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Drupal\help\Plugin\HelpSection;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\help\Attribute\HelpSection;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\help\SearchableHelpInterface;
use Drupal\help\HelpTopicPluginInterface;
use Drupal\help\HelpTopicPluginManagerInterface;
use Drupal\Core\Language\LanguageDefault;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\StringTranslation\TranslationManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the help topics list section for the help page.
*
* @internal
* Plugin classes are internal.
*/
#[HelpSection(
id: 'help_topics',
title: new TranslatableMarkup('Topics'),
description: new TranslatableMarkup('Topics can be provided by modules or themes. Top-level help topics on your site:'),
weight: -10
)]
class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface, SearchableHelpInterface {
/**
* The top level help topic plugins.
*
* @var \Drupal\help\HelpTopicPluginInterface[]
*/
protected $topLevelPlugins;
/**
* The merged top level help topic plugins cache metadata.
*
* @var \Drupal\Core\Cache\CacheableMetadata
*/
protected $cacheableMetadata;
/**
* Constructs a HelpTopicSection object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\help\HelpTopicPluginManagerInterface $pluginManager
* The help topic plugin manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Language\LanguageDefault $defaultLanguage
* The default language object.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* The language manager.
* @param \Drupal\Core\StringTranslation\TranslationManager $translationManager
* 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, protected HelpTopicPluginManagerInterface $pluginManager, protected RendererInterface $renderer, protected LanguageDefault $defaultLanguage, protected LanguageManagerInterface $languageManager, protected TranslationManager $translationManager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.help_topic'),
$container->get('renderer'),
$container->get('language.default'),
$container->get('language_manager'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->getCacheMetadata()->getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->getCacheMetadata()->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return $this->getCacheMetadata()->getCacheMaxAge();
}
/**
* {@inheritdoc}
*/
public function listTopics() {
// Map the top level help topic plugins to a list of topic links.
return array_map(function (HelpTopicPluginInterface $topic) {
return $topic->toLink();
}, $this->getPlugins());
}
/**
* Gets the top level help topic plugins.
*
* @return \Drupal\help\HelpTopicPluginInterface[]
* The top level help topic plugins.
*/
protected function getPlugins() {
if (!isset($this->topLevelPlugins)) {
$definitions = $this->pluginManager->getDefinitions();
$this->topLevelPlugins = [];
// Get all the top level topics and merge their list cache tags.
foreach ($definitions as $definition) {
if ($definition['top_level']) {
$this->topLevelPlugins[$definition['id']] = $this->pluginManager->createInstance($definition['id']);
}
}
// Sort the top level topics by label and, if the labels match, then by
// plugin ID.
usort($this->topLevelPlugins, function (HelpTopicPluginInterface $a, HelpTopicPluginInterface $b) {
$a_label = (string) $a->getLabel();
$b_label = (string) $b->getLabel();
if ($a_label === $b_label) {
return $a->getPluginId() <=> $b->getPluginId();
}
return strnatcasecmp($a_label, $b_label);
});
}
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->renderInIsolation($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->renderInIsolation($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.
*
* @return \Drupal\Core\Cache\CacheableMetadata
* The merged CacheableMetadata for all the top level help topic plugins.
*/
protected function getCacheMetadata() {
if (!isset($this->cacheableMetadata)) {
$this->cacheableMetadata = new CacheableMetadata();
foreach ($this->getPlugins() as $plugin) {
$this->cacheableMetadata->addCacheableDependency($plugin);
}
}
return $this->cacheableMetadata;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\help\Plugin\HelpSection;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Link;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\help\Attribute\HelpSection;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the module topics list section for the help page.
*/
#[HelpSection(
id: 'hook_help',
title: new TranslatableMarkup('Module overviews'),
description: new TranslatableMarkup('Module overviews are provided by modules. Overviews available for your installed modules:')
)]
class HookHelpSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a HookHelpSection object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Extension\ModuleExtensionList|null $moduleExtensionList
* The module extension list.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, protected ?ModuleExtensionList $moduleExtensionList = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
if ($this->moduleExtensionList === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $moduleExtensionList argument is deprecated in drupal:10.3.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$this->moduleExtensionList = \Drupal::service('extension.list.module');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('extension.list.module'),
);
}
/**
* {@inheritdoc}
*/
public function listTopics() {
$topics = [];
$this->moduleHandler->invokeAllWith(
'help',
function (callable $hook, string $module) use (&$topics) {
$title = $this->moduleExtensionList->getName($module);
$topics[$title] = Link::createFromRoute($title, 'help.page', ['name' => $module]);
}
);
// Sort topics by title, which is the array key above.
ksort($topics);
return $topics;
}
}

View File

@@ -0,0 +1,526 @@
<?php
namespace Drupal\help\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\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\Core\StringTranslation\TranslatableMarkup;
use Drupal\help\HelpSectionManager;
use Drupal\help\SearchableHelpInterface;
use Drupal\search\Attribute\Search;
use Drupal\search\Plugin\SearchIndexingInterface;
use Drupal\search\Plugin\SearchPluginBase;
use Drupal\search\SearchIndexInterface;
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
*
* @internal
* Plugin classes are internal.
*/
#[Search(
id: 'help_search',
title: new TranslatableMarkup('Help'),
use_admin_theme: TRUE,
)]
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;
/**
* The search index.
*
* @var \Drupal\search\SearchIndexInterface
*/
protected $searchIndex;
/**
* {@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'),
$container->get('search.index')
);
}
/**
* 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.
* @param \Drupal\search\SearchIndexInterface $search_index
* The search index.
*/
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, SearchIndexInterface $search_index) {
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;
$this->searchIndex = $search_index;
}
/**
* {@inheritdoc}
*/
public function access($operation = 'view', ?AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIfHasPermission($account, 'access help 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 help 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 help 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.'));
}
$unindexed = $this->state->get('help_search_unindexed_count', 1);
if ($unindexed) {
$this->messenger()->addWarning($this->t('Help search is not fully indexed. Some results may be missing or incorrect.'));
}
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 = [];
$words = [];
try {
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];
$this->searchIndex->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'];
$words += $this->searchIndex->index($this->getType(), $item->sid, $langcode, $text, FALSE);
}
}
}
}
finally {
$this->searchIndex->updateWordWeights($words);
$this->updateIndexState();
}
}
/**
* {@inheritdoc}
*/
public function indexClear() {
$this->searchIndex->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);
}
/**
* Updates the 'help_search_unindexed_count' state variable.
*
* The state variable is a count of help topics that have never been indexed.
*/
public function updateIndexState() {
$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()]);
$query->isNull('sd.sid');
$never_indexed = $query->execute()->fetchField();
$this->state->set('help_search_unindexed_count', $never_indexed);
}
/**
* {@inheritdoc}
*/
public function markForReindex() {
$this->updateTopicList();
$this->searchIndex->markForReindex($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 = $this->database->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) {
$this->searchIndex->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\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;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\help;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides an interface for a HelpSection plugin that also supports search.
*
* @see \Drupal\help\HelpSectionPluginInterface
*/
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);
}

View File

@@ -0,0 +1,25 @@
{#
/**
* @file
* Default theme implementation for a section of the help page.
*
* Available variables:
* - title: The section title.
* - description: The description text for the section.
* - links: Links to display in the section.
* - empty: Text to display if there are no links.
*
* @ingroup themeable
*/
#}
<h2>{{ title }}</h2>
<p>{{ description }}</p>
{% if links %}
<ul>
{% for link in links %}
<li>{{ link }}</li>
{% endfor %}
</ul>
{% else %}
<p>{{ empty }}</p>
{% endif %}

View File

@@ -0,0 +1,16 @@
{#
/**
* @file
* Default theme implementation to display a help topic.
*
* Available variables:
* - body: The body of the topic.
* - related: List of related topic links.
*
* @ingroup themeable
*/
#}
<article>
{{ body }}
{{ related }}
</article>

View File

@@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains database additions for testing the help module permission.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
$role = Yaml::decode(file_get_contents(__DIR__ . '/drupal-10.access-help-pages.yml'));
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'user.role.content_editor',
'data' => serialize($role),
])
->execute();

View File

@@ -0,0 +1,46 @@
uuid: 91cd52b8-a979-426b-9761-039a5a184b74
langcode: en
status: true
dependencies:
config:
- node.type.article
- node.type.page
- taxonomy.vocabulary.tags
module:
- comment
- contextual
- file
- node
- path
- system
- taxonomy
- toolbar
_core:
default_config_hash: Wur9kcEOwY1Jal81NssKnz3RhVJxAvBwyWQBGcA_1Go
id: content_editor
label: 'Content editor'
weight: 2
is_admin: false
permissions:
- 'access administration pages'
- 'access content overview'
- 'access contextual links'
- 'access files overview'
- 'access toolbar'
- 'administer url aliases'
- 'create article content'
- 'create page content'
- 'create terms in tags'
- 'create url aliases'
- 'delete article revisions'
- 'delete own article content'
- 'delete own page content'
- 'delete page revisions'
- 'edit own article content'
- 'edit own comments'
- 'edit own page content'
- 'edit terms in tags'
- 'revert all revisions'
- 'view all revisions'
- 'view own unpublished content'
- 'view the administration theme'

View File

@@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains database additions for testing the upgrade path for help topics.
*
* @see https://www.drupal.org/node/3087499
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
// Enable experimental help_topics module.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['help_topics'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
// Structure of configured block and search page.
$search_page = Yaml::decode(file_get_contents(__DIR__ . '/search.page.help_search.yml'));
$connection->insert('config')
->fields([
'collection',
'name',
'data',
])
->values([
'collection' => '',
'name' => 'search.page.help_search',
'data' => serialize($search_page),
])
->execute();

View File

@@ -0,0 +1,14 @@
uuid: c0c6e5d5-c0b1-4874-8c94-ffed8929f5d7
langcode: en
status: true
dependencies:
module:
- help_topics
_core:
default_config_hash: RZ-bcSekNSsAbIPLW7Gmyd3uUjIOSrPvnb8RCCZYJm8
id: help_search
label: Help
path: help
weight: 0
plugin: help_search
configuration: { }

View File

@@ -0,0 +1,11 @@
name: 'Help Page Test'
type: module
description: 'Module to test the help page.'
package: Testing
# version: VERSION
hidden: true
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,31 @@
<?php
/**
* @file
* Help Page Test module to test the help page.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function help_page_test_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.help_page_test':
// Make the help text conform to core standards. See
// \Drupal\system\Tests\Functional\GenericModuleTestBase::assertHookHelp().
return t('Read the <a href=":url">online documentation for the Help Page Test module</a>.', [':url' => 'http://www.example.com']);
case 'help_page_test.has_help':
return t('I have help!');
case 'help_page_test.test_array':
return ['#markup' => 'Help text from help_page_test_help module.'];
}
// Ensure that hook_help() can return an empty string and not cause the block
// to display.
return '';
}

View File

@@ -0,0 +1,20 @@
help_page_test.has_help:
path: '/help_page_test/has_help'
defaults:
_controller: '\Drupal\help_page_test\HelpPageTestController::hasHelp'
requirements:
_access: 'TRUE'
help_page_test.no_help:
path: '/help_page_test/no_help'
defaults:
_controller: '\Drupal\help_page_test\HelpPageTestController::noHelp'
requirements:
_access: 'TRUE'
help_page_test.test_array:
path: '/help_page_test/test_array'
defaults:
_controller: '\Drupal\help_page_test\HelpPageTestController::testArray'
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\help_page_test;
/**
* Provides controllers for testing the help block.
*/
class HelpPageTestController {
/**
* Provides a route with help.
*
* @return array
* A render array.
*/
public function hasHelp() {
return ['#markup' => 'A route with help.'];
}
/**
* Provides a route with no help.
*
* @return array
* A render array.
*/
public function noHelp() {
return ['#markup' => 'A route without help.'];
}
/**
* Provides a route which has multiple array returns from hook_help().
*
* @return array
* A render array.
*/
public function testArray() {
return ['#markup' => 'A route which has multiple array returns from hook_help().'];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\help_page_test\Plugin\HelpSection;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
use Drupal\help\Attribute\HelpSection;
/**
* Provides an empty section for the help page, for testing.
*/
#[HelpSection(
id: 'empty_section',
title: new TranslatableMarkup('Empty section'),
description: new TranslatableMarkup('This description should appear.')
)]
class EmptyHelpSection extends HelpSectionPluginBase {
/**
* {@inheritdoc}
*/
public function listTopics() {
return [];
}
}

View File

@@ -0,0 +1,10 @@
name: help_test
type: module
package: Testing
dependencies:
- drupal:help
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,16 @@
<?php
/**
* @file
* Test Help module.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function help_test_help($route_name, RouteMatchInterface $route_match) {
// Do not implement a module overview page to test an empty implementation.
// @see \Drupal\help\Tests\HelpTest
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\help_test;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
/**
* Implements a URL generator which always thrown an exception.
*/
class SupernovaGenerator implements UrlGeneratorInterface {
/**
* {@inheritdoc}
*/
public function setContext(RequestContext $context) {
throw new \Exception();
}
/**
* {@inheritdoc}
*/
public function getContext(): RequestContext {
throw new \Exception();
}
/**
* {@inheritdoc}
*/
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH): string {
throw new \Exception();
}
/**
* {@inheritdoc}
*/
public function getPathFromRoute($name, $parameters = []) {
throw new \Exception();
}
/**
* {@inheritdoc}
*/
public function generateFromRoute($name, $parameters = [], $options = [], $collect_bubbleable_metadata = FALSE) {
throw new \Exception();
}
}

View File

@@ -0,0 +1,5 @@
---
label: 'Help topic with bad HTML syntax'
top_level: true
---
<p>{% trans %}Body goes here{% endtrans %}</h3>

View File

@@ -0,0 +1,6 @@
---
label: 'Bad HTML syntax within trans section'
top_level: true
---
<p>{% trans %}<a href="/foo">Text here{% endtrans %}</a></p>

View File

@@ -0,0 +1,5 @@
---
label: 'Unclosed HTML tag'
top_level: true
---
<p>{% trans %}<a href="/foo">Text here{% endtrans %}</p>

View File

@@ -0,0 +1,4 @@
---
label: 'Help topic containing no body'
top_level: true
---

View File

@@ -0,0 +1,5 @@
---
label: 'Help topic with H1 header'
top_level: true
---
<h1>{% trans %}Body goes here{% endtrans %}</h1>

View File

@@ -0,0 +1,5 @@
---
label: 'Help topic with h3 without an h2'
top_level: true
---
<h3>{% trans %}Body goes here{% endtrans %}</h3>

View File

@@ -0,0 +1,5 @@
---
label: 'Help topic with locale-unsafe tag'
top_level: true
---
<p>{% trans %}some translated text and a <script>alert('hello')</script>{% endtrans %}</p>

View File

@@ -0,0 +1,7 @@
---
label: 'Help topic related to nonexistent topic'
top_level: true
related:
- this.is.not.a.valid.help_topic.id
---
<p>{% trans %}Body goes here{% trans %}</p>

View File

@@ -0,0 +1,4 @@
---
label: 'Help topic not top level or related to top level'
---
<p>{% trans %}Body goes here{% endtrans %}</p>

View File

@@ -0,0 +1,6 @@
---
label: 'Help topic with untranslated text'
top_level: true
---
<p>Body goes here</p>
<p>{% trans %}some translated text too{% endtrans %}</p>

View File

@@ -0,0 +1,9 @@
---
label: 'URL test topic that uses outdated url function'
top_level: true
---
{% set link_uses_url_func = render_var(url('valid.route')) %}
<p>{% trans %}This topic should be top-level. It is used to test URLs{% endtrans %}</p>
<ul>
<li>{% trans %}Valid link, but generated with <code>url()</code> instead of <code>help_route_link()</code> or <code>help_topic_link()</code>: {{ link_uses_url_func }}{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,6 @@
---
label: 'Additional topic'
related:
- help_topics_test.test
---
<p>{% trans %}This topic should get listed automatically on the Help test topic.{% endtrans %}</p>

View File

@@ -0,0 +1,4 @@
---
label: 'Linked topic'
---
<p>{% trans %}This topic is not supposed to be top-level.{% endtrans %}</p>

View File

@@ -0,0 +1,11 @@
---
label: "ABC Help Test module"
top_level: true
related:
- help_topics_test.linked
- does_not_exist.and_no_error
---
{% set help_topic_link = render_var(help_topic_link('help_topics_test.test_urls')) %}
<p>{% trans %}This is a test. It should link to the URL test topic {{ help_topic_link }}. Also there should be a related topic link below to the Help module topic page and the linked topic.{% endtrans %}</p>
<p>{% trans %}Non-word-item to translate.{% endtrans %}</p>
<p>{% trans %}Test translation.{% endtrans %}</p>

View File

@@ -0,0 +1,19 @@
---
label: 'URL test topic'
top_level: true
---
{% set non_route_link = render_var(help_route_link('not a route', 'not_a_real_route')) %}
{% set missing_params_link = render_var(help_route_link('missing params', 'help_topics_test.test_route')) %}
{% set invalid_params_link = render_var(help_route_link('invalid params', 'help_topics_test.test_route', {'int_param': 'not_an_int'})) %}
{% set valid_link = render_var(help_route_link('valid link', 'help_topics_test.test_route', {'int_param': 2})) %}
{% set topic_link = render_var(help_topic_link('help_topics_test.additional')) %}
{% set not_a_topic = render_var(help_topic_link('not_a_topic')) %}
<p>{% trans %}This topic should be top-level. It is used to test URLs{% endtrans %}</p>
<ul>
<li>{% trans %}Should not be a link: {{ non_route_link }}{% endtrans %}</li>
<li>{% trans %}Should not be a link: {{ missing_params_link }}{% endtrans %}</li>
<li>{% trans %}Should not be a link: {{ invalid_params_link }}{% endtrans %}</li>
<li>{% trans %}Should be a link if user has access: {{ valid_link }}{% endtrans %}</li>
<li>{% trans %}Should be a link: {{ topic_link }}{% endtrans %}</li>
<li>{% trans %}Should not be a link: {{ not_a_topic }}{% endtrans %}</li>
</ul>

View File

@@ -0,0 +1,9 @@
help_topics_test_direct_yml:
class: 'Drupal\help_topics_test\Plugin\HelpTopic\TestHelpTopicPlugin'
top_level: true
related: {}
label: "Test direct yaml topic label"
body: "Test direct yaml body"
help_topics_derivatives:
deriver: 'Drupal\help_topics_test\Plugin\Deriver\TestHelpTopicDeriver'

View File

@@ -0,0 +1,13 @@
# The name of this module is deliberately different from its machine
# name to test the presented order of help topics.
name: 'ABC Help Test'
type: module
description: 'Support module for help testing.'
package: Testing
dependencies:
- drupal:help
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,31 @@
<?php
/**
* @file
* Test module for help.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function help_topics_test_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.help_topics_test':
return 'Some kind of non-empty output for testing';
}
}
/**
* Implements hook_help_topics_info_alter().
*/
function help_topics_test_help_topics_info_alter(array &$info) {
// To prevent false positive search results limit list to testing topis only.
$filter = fn(string $key) => str_starts_with($key, 'help_topics_test') || in_array($key, [
'help_topics_test_direct_yml',
'help_topics_derivatives:test_derived_topic',
], TRUE);
$info = array_filter($info, $filter, ARRAY_FILTER_USE_KEY);
$info['help_topics_test.test']['top_level'] = \Drupal::state()->get('help_topics_test.test:top_level', TRUE);
}

View File

@@ -0,0 +1,2 @@
access test help:
title: 'Access the test help section'

View File

@@ -0,0 +1,11 @@
help_topics_test.test_route:
path: '/help_topics_test/{int_param<\d+>}'
defaults:
_controller: '\Drupal\help_topics_test\Controller\HelpTopicsTestController::testPage'
_title: 'Page for testing URLs in topics'
requirements:
_permission: 'access test help'
options:
parameters:
required_param:
type: int

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\help_topics_test\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Returns the response for help_topics_test routes.
*/
class HelpTopicsTestController extends ControllerBase {
/**
* Displays a dummy page for testing.
*
* @param int $int_param
* Required parameter (ignored).
*
* @return array
* Render array for the dummy page.
*/
public function testPage(int $int_param) {
$build = [
'#markup' => 'You have reached the help topics test routing page.',
];
return $build;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\help_topics_test\Plugin\Deriver;
use Drupal\Component\Plugin\Derivative\DeriverInterface;
/**
* A test discovery deriver for fake help topics.
*/
class TestHelpTopicDeriver implements DeriverInterface {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$prefix = $base_plugin_definition['id'];
$id = 'test_derived_topic';
$plugin_id = $prefix . ':' . $id;
$definitions[$id] = [
'plugin_id' => $plugin_id,
'id' => $plugin_id,
'class' => 'Drupal\\help_topics_test\\Plugin\\HelpTopic\\TestHelpTopicPlugin',
'label' => 'Label for ' . $id,
'body' => 'Body for ' . $id,
'top_level' => TRUE,
'related' => [],
'provider' => 'help_topics_test',
];
return $definitions;
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
return $base_plugin_definition;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\help_topics_test\Plugin\HelpSection;
use Drupal\help\SearchableHelpInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;
use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
use Drupal\help\Attribute\HelpSection;
use Drupal\Core\StringTranslation\TranslatableMarkup;
// cspell:ignore asdrsad barmm foomm sqruct wcsrefsdf sdeeeee
/**
* Provides a searchable help section for testing.
*/
#[HelpSection(
id: 'help_topics_test',
title: new TranslatableMarkup('Test section'),
description: new TranslatableMarkup('For testing search'),
permission: 'access test help',
weight: 100
)]
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 not-a-word-english sqruct',
'url' => Url::fromUri('https://foo.com'),
];
}
return [
'title' => 'Foomm Foreign heading',
'text' => 'Fake foreign foo text not-a-word-german asdrsad',
'url' => Url::fromUri('https://mm.foo.com'),
];
case 'bar':
if ($language->getId() == 'en') {
return [
'title' => 'Bar in English',
'text' => 'Something about bar another-word-english asdrsad',
'url' => Url::fromUri('https://bar.com'),
];
}
return [
'title' => \Drupal::state()->get('help_topics_test:translated_title', 'Barmm Foreign sdeeeee'),
'text' => 'Fake foreign barmm another-word-german sqruct',
'url' => Url::fromUri('https://mm.bar.com'),
];
default:
throw new \InvalidArgumentException('Unexpected ID encountered');
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\help_topics_test\Plugin\HelpTopic;
use Drupal\Core\Cache\Cache;
use Drupal\help\HelpTopicPluginBase;
/**
* A fake help topic plugin for testing.
*/
class TestHelpTopicPlugin extends HelpTopicPluginBase {
/**
* {@inheritdoc}
*/
public function getBody() {
return [
'#type' => 'markup',
'#markup' => $this->pluginDefinition['body'],
];
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return ['foobar'];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
}

View File

@@ -0,0 +1,11 @@
name: 'Help Topics Twig Tester'
type: module
description: 'Support module for help testing.'
package: Testing
dependencies:
- drupal:help
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
services:
help_test_twig.extension:
class: Drupal\help_topics_twig_tester\HelpTestTwigExtension
arguments: []
tags:
- { name: twig.extension, priority: 500 }

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\help_topics_twig_tester;
use Twig\Extension\AbstractExtension;
/**
* Defines and registers Drupal Twig extensions for testing help topics.
*/
class HelpTestTwigExtension extends AbstractExtension {
/**
* {@inheritdoc}
*/
public function getNodeVisitors() {
return [
new HelpTestTwigNodeVisitor(),
];
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Drupal\help_topics_twig_tester;
use Drupal\Core\Template\TwigNodeTrans;
use Twig\Environment;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
use Twig\Node\TextNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface;
/**
* Defines a Twig node visitor for testing help topics.
*
* See static::setStateValue() for information on the special processing
* this class can do.
*/
class HelpTestTwigNodeVisitor implements NodeVisitorInterface {
/**
* Delimiter placed around single translated chunks.
*/
public const DELIMITER = 'Not Likely To Be Inside A Template';
/**
* Name used in \Drupal::state() for saving state information.
*/
protected const STATE_NAME = 'help_test_twig_node_visitor';
/**
* {@inheritdoc}
*/
public function enterNode(Node $node, Environment $env): Node {
return $node;
}
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node, Environment $env): ?Node {
$processing = static::getState();
if (!$processing['manner']) {
return $node;
}
// For all special processing, we want to remove variables, set statements,
// and assorted Twig expression calls (if, do, etc.).
if ($node instanceof SetNode || $node instanceof PrintNode ||
$node instanceof AbstractExpression) {
return NULL;
}
if ($node instanceof TwigNodeTrans) {
// Count the number of translated chunks.
$this_chunk = $processing['chunk_count'] + 1;
static::setStateValue('chunk_count', $this_chunk);
if ($this_chunk > $processing['max_chunk']) {
static::setStateValue('max_chunk', $this_chunk);
}
if ($processing['manner'] == 'remove_translated') {
// Remove all translated text.
return NULL;
}
elseif ($processing['manner'] == 'replace_translated') {
// Replace with a dummy string.
$node = new TextNode('dummy', 0);
}
elseif ($processing['manner'] == 'translated_chunk') {
// Return the text only if it's the next chunk we're supposed to return.
// Add a wrapper, because non-translated nodes will still be returned.
if ($this_chunk == $processing['return_chunk']) {
return new TextNode(static::DELIMITER . $this->extractText($node) . static::DELIMITER, 0);
}
else {
return NULL;
}
}
}
if ($processing['manner'] == 'remove_translated' && $node instanceof TextNode) {
// For this processing, we also want to remove all HTML tags and
// whitespace from TextNodes.
$text = $node->getAttribute('data');
$text = strip_tags($text);
$text = preg_replace('|\s+|', '', $text);
return new TextNode($text, 0);
}
return $node;
}
/**
* {@inheritdoc}
*/
public function getPriority() {
return -100;
}
/**
* Extracts the text from a translated text object.
*
* @param \Drupal\Core\Template\TwigNodeTrans $node
* Translated text node.
*
* @return string
* Text in the node.
*/
protected function extractText(TwigNodeTrans $node) {
// Extract the singular/body and optional plural text from the
// TwigNodeTrans object.
$bodies = $node->getNode('body');
if (!count($bodies)) {
$bodies = [$bodies];
}
if ($node->hasNode('plural')) {
$plural = $node->getNode('plural');
if (!count($plural)) {
$bodies[] = $plural;
}
else {
foreach ($plural as $item) {
$bodies[] = $item;
}
}
}
// Extract the text from each component of the singular/plural strings.
$text = '';
foreach ($bodies as $body) {
if ($body->hasAttribute('data')) {
$text .= $body->getAttribute('data');
}
}
return trim($text);
}
/**
* Returns the state information.
*
* @return array
* The state information.
*/
public static function getState() {
return \Drupal::state()->get(static::STATE_NAME, ['manner' => 0]);
}
/**
* Sets state information.
*
* @param string $key
* Key to set. Possible keys:
* - manner: Type of special processing to do when rendering. Values:
* - 0: No processing.
* - remove_translated: Remove all translated text, HTML tags, and
* whitespace.
* - replace_translated: Replace all translated text with dummy text.
* - translated_chunk: Remove all translated text except one designated
* chunk (see return_chunk below).
* - bare_body (or any other non-zero value): Remove variables, set
* statements, and Twig programming, but leave everything else intact.
* - chunk_count: Current index of translated chunks. Reset to -1 before
* each rendering run. (Used internally by this class.)
* - max_chunk: Maximum index of translated chunks. Reset to -1 before
* each rendering run.
* - return_chunk: Chunk index to keep intact for translated_chunk
* processing. All others are removed.
* @param $value
* Value to set for $key.
*/
public static function setStateValue(string $key, $value) {
$state = \Drupal::state();
$values = $state->get(static::STATE_NAME, ['manner' => 0]);
$values[$key] = $value;
$state->set(static::STATE_NAME, $values);
}
}

View File

@@ -0,0 +1,11 @@
name: 'More Help Page Test'
type: module
description: 'Module to test the help page.'
package: Testing
# version: VERSION
hidden: true
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,27 @@
<?php
/**
* @file
* More Help Page Test module to test the help blocks.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function more_help_page_test_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Return help for the same route as the help_page_test module.
case 'help_page_test.test_array':
return ['#markup' => 'Help text from more_help_page_test_help module.'];
}
}
/**
* Implements hook_help_section_info_alter().
*/
function more_help_page_test_help_section_info_alter(array &$info) {
$info['hook_help']['weight'] = 500;
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\user\Entity\Role;
/**
* Tests help_post_update_add_permissions_to_roles().
*
* @group help
* @group legacy
* @group #slow
*
* @see help_post_update_add_permissions_to_roles()
*/
class AddPermissionsUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles(): void {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz',
__DIR__ . '/../../fixtures/update/drupal-10.access-help-pages.php',
];
}
/**
* Tests adding 'access help pages' permission.
*/
public function testUpdate(): void {
$roles = Role::loadMultiple();
$this->assertGreaterThan(2, count($roles));
foreach ($roles as $role) {
$permissions = $role->toArray()['permissions'];
$this->assertNotContains('access help pages', $permissions);
}
$this->runUpdates();
$role = Role::load(Role::ANONYMOUS_ID);
$permissions = $role->toArray()['permissions'];
$this->assertNotContains('access help pages', $permissions);
$role = Role::load(Role::AUTHENTICATED_ID);
$permissions = $role->toArray()['permissions'];
$this->assertNotContains('access help pages', $permissions);
// Admin roles have the permission and do not need assigned.
$role = Role::load('administrator');
$permissions = $role->toArray()['permissions'];
$this->assertNotContains('access help pages', $permissions);
$role = Role::load('content_editor');
$permissions = $role->toArray()['permissions'];
$this->assertContains('access help pages', $permissions);
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Verifies help for experimental modules.
*
* @group help
*/
class ExperimentalHelpTest extends BrowserTestBase {
/**
* Modules to enable.
*
* The experimental_module_test module implements hook_help() and is in the
* Core (Experimental) package.
*
* @var array
*/
protected static $modules = [
'help',
'experimental_module_test',
'help_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['access help pages']);
}
/**
* Verifies that a warning message is displayed for experimental modules.
*/
public function testExperimentalHelp(): void {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/help/experimental_module_test');
$this->assertSession()->statusMessageContains('This module is experimental.', 'warning');
// Regular modules should not display the message.
$this->drupalGet('admin/help/help_page_test');
$this->assertSession()->statusMessageNotContains('This module is experimental.');
// Ensure the actual help page is displayed to avoid a false positive.
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('online documentation for the Help Page Test module');
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for help.
*
* @group help
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests display of help block.
*
* @group help
*/
class HelpBlockTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'help',
'help_page_test',
'block',
'more_help_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The help block instance.
*
* @var \Drupal\block\Entity\Block
*/
protected $helpBlock;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->helpBlock = $this->placeBlock('help_block');
}
/**
* Logs in users, tests help pages.
*/
public function testHelp(): void {
$this->drupalGet('help_page_test/has_help');
$this->assertSession()->pageTextContains('I have help!');
$this->assertSession()->pageTextContains($this->helpBlock->label());
$this->drupalGet('help_page_test/no_help');
// The help block should not appear when there is no help.
$this->assertSession()->pageTextNotContains($this->helpBlock->label());
// Ensure that if two hook_help() implementations both return a render array
// the output is as expected.
$this->drupalGet('help_page_test/test_array');
$this->assertSession()->pageTextContains('Help text from more_help_page_test_help module.');
$this->assertSession()->pageTextContains('Help text from help_page_test_help module.');
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Verify the order of the help page.
*
* @group help
*/
class HelpPageOrderTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['help', 'help_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Strings to search for on admin/help, in order.
*
* @var string[]
*/
protected $stringOrder = [
'Module overviews are provided',
'This description should appear',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create and log in user.
$account = $this->drupalCreateUser([
'access help pages',
'view the administration theme',
'administer permissions',
]);
$this->drupalLogin($account);
}
/**
* Tests the order of the help page.
*/
public function testHelp(): void {
$pos = 0;
$this->drupalGet('admin/help');
$page_text = $this->getTextContent();
foreach ($this->stringOrder as $item) {
$new_pos = strpos($page_text, $item, $pos);
$this->assertGreaterThan($pos, $new_pos, "Order of $item is not correct on help page");
$pos = $new_pos;
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
/**
* Verify the order of the help page with an alter hook.
*
* @group help
*/
class HelpPageReverseOrderTest extends HelpPageOrderTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['more_help_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Strings to search for on admin/help, in order.
*
* These are reversed, due to the alter hook.
*
* @var string[]
*/
protected $stringOrder = [
'This description should appear',
'Module overviews are provided',
];
}

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Tests\BrowserTestBase;
/**
* Verify help display and user access to help based on permissions.
*
* @group help
*/
class HelpTest extends BrowserTestBase {
/**
* Modules to enable.
*
* The help_test module implements hook_help() but does not provide a module
* overview page. The help_page_test module has a page section plugin that
* returns no links.
*
* @var array
*/
protected static $modules = [
'block_content',
'breakpoint',
'editor',
'help',
'help_page_test',
'help_test',
'history',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'claro';
/**
* The admin user that will be created.
*/
protected $adminUser;
/**
* The anonymous user that will be created.
*/
protected $anyUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create users.
$this->adminUser = $this->drupalCreateUser([
'access help pages',
'view the administration theme',
'administer permissions',
]);
$this->anyUser = $this->drupalCreateUser([]);
}
/**
* Logs in users, tests help pages.
*/
public function testHelp(): void {
// Log in the root user to ensure as many admin links appear as possible on
// the module overview pages.
$this->drupalLogin($this->drupalCreateUser([
'access help pages',
'access administration pages',
]));
$this->verifyHelp();
// Log in the regular user.
$this->drupalLogin($this->anyUser);
$this->verifyHelp(403);
// Verify that introductory help text exists, goes for 100% module coverage.
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/help');
$this->assertSession()->responseContains('For more information, refer to the help listed on this page or to the <a href="https://www.drupal.org/documentation">online documentation</a> and <a href="https://www.drupal.org/support">support</a> pages at <a href="https://www.drupal.org">drupal.org</a>.');
// Verify that hook_help() section title and description appear.
$this->assertSession()->responseContains('<h2>Module overviews</h2>');
$this->assertSession()->responseContains('<p>Module overviews are provided by modules. Overviews available for your installed modules:</p>');
// Verify that an empty section is handled correctly.
$this->assertSession()->responseContains('<h2>Empty section</h2>');
$this->assertSession()->responseContains('<p>This description should appear.</p>');
$this->assertSession()->pageTextContains('There is currently nothing in this section.');
// Make sure links are properly added for modules implementing hook_help().
foreach ($this->getModuleList() as $module => $name) {
$this->assertSession()->linkExists($name, 0, new FormattableMarkup('Link properly added to @name (admin/help/@module)', ['@module' => $module, '@name' => $name]));
}
// Ensure a module which does not provide a module overview page is handled
// correctly.
$module_name = \Drupal::service('extension.list.module')->getName('help_test');
$this->clickLink($module_name);
$this->assertSession()->pageTextContains('No help is available for module ' . $module_name);
// Verify that the order of topics is alphabetical by displayed module
// name, by checking the order of some modules, including some that would
// have a different order if it was done by machine name instead.
$this->drupalGet('admin/help');
$page_text = $this->getTextContent();
$start = strpos($page_text, 'Module overviews');
$pos = $start;
$list = ['Block', 'Block Content', 'Breakpoint', 'History', 'Text Editor'];
foreach ($list as $name) {
$this->assertSession()->linkExists($name);
$new_pos = strpos($page_text, $name, $start);
$this->assertGreaterThan($pos, $new_pos, "Order of $name is not correct on page");
$pos = $new_pos;
}
}
/**
* Verifies the logged in user has access to the various help pages.
*
* @param int $response
* (optional) An HTTP response code. Defaults to 200.
*/
protected function verifyHelp($response = 200) {
$this->drupalGet('admin/index');
$this->assertSession()->statusCodeEquals($response);
if ($response == 200) {
$this->assertSession()->pageTextContains('This page shows you all available administration tasks for each module.');
}
else {
$this->assertSession()->pageTextNotContains('This page shows you all available administration tasks for each module.');
}
$module_list = \Drupal::service('extension.list.module');
foreach ($this->getModuleList() as $module => $name) {
// View module help page.
$this->drupalGet('admin/help/' . $module);
$this->assertSession()->statusCodeEquals($response);
if ($response == 200) {
$this->assertSession()->titleEquals("$name | Drupal");
$this->assertEquals($name, $this->cssSelect('h1.page-title')[0]->getText(), "$module heading was displayed");
$info = $module_list->getExtensionInfo($module);
$admin_tasks = \Drupal::service('system.module_admin_links_helper')->getModuleAdminLinks($module);
if ($module_permissions_link = \Drupal::service('user.module_permissions_link_helper')->getModulePermissionsLink($module, $info['name'])) {
$admin_tasks["user.admin_permissions.{$module}"] = $module_permissions_link;
}
if (!empty($admin_tasks)) {
$this->assertSession()->pageTextContains($name . ' administration pages');
}
foreach ($admin_tasks as $task) {
$this->assertSession()->linkExists($task['title']);
// Ensure there are no double escaped '&' or '<' characters.
$this->assertSession()->assertNoEscaped('&amp;');
$this->assertSession()->assertNoEscaped('&lt;');
// Ensure there are no escaped '<' characters.
$this->assertSession()->assertNoEscaped('<');
}
// Ensure there are no double escaped '&' or '<' characters.
$this->assertSession()->assertNoEscaped('&amp;');
$this->assertSession()->assertNoEscaped('&lt;');
// The help for CKEditor 5 intentionally has escaped '<' so leave this
// iteration before the assertion below.
if ($module === 'ckeditor5') {
continue;
}
// Ensure there are no escaped '<' characters.
$this->assertSession()->assertNoEscaped('<');
}
}
}
/**
* Gets the list of enabled modules that implement hook_help().
*
* @return array
* A list of enabled modules.
*/
protected function getModuleList() {
$modules = [];
$module_data = $this->container->get('extension.list.module')->getList();
\Drupal::moduleHandler()->invokeAllWith(
'help',
function (callable $hook, string $module) use (&$modules, $module_data) {
$modules[$module] = $module_data[$module]->info['name'];
}
);
return $modules;
}
}

View File

@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\help\Functional;
use Drupal\Tests\Traits\Core\CronRunTrait;
use Drupal\help\Plugin\Search\HelpSearch;
// cspell:ignore asdrsad barmm foomm hilfetestmodul sdeeeee sqruct testen
// cspell:ignore wcsrefsdf übersetzung
/**
* Verifies help topic search.
*
* @group help
* @group #slow
*/
class HelpTopicSearchTest extends HelpTopicTranslatedTestBase {
use CronRunTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'search',
'locale',
'language',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Log in.
$this->drupalLogin($this->createUser([
'access help 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->drupalGet('admin/config/regional/language/add');
$this->submitForm([
'predefined_langcode' => 'en',
], 'Add language');
$this->drupalGet('admin/config/regional/language');
$this->submitForm([
'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 and shows
// warning.
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$this->assertSearchResultsCount(0);
$this->assertSession()->statusMessageContains('Help search is not fully indexed', 'warning');
// 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');
// Search and verify there is no warning.
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$this->assertSearchResultsCount(1);
$this->assertSession()->statusMessageNotContains('Help search is not fully indexed');
}
/**
* Tests help topic search.
*/
public function testHelpSearch(): void {
$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->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
// Same for German.
$this->drupalGet('search/help', ['language' => $german]);
$this->submitForm(['keys' => 'not-a-word-german'], 'Search');
$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->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-german'], 'Search');
$this->assertSearchResultsCount(0);
$session->pageTextContains('no results');
// Same for German.
$this->drupalGet('search/help', ['language' => $german]);
$this->submitForm(['keys' => 'not-a-word-english'], 'Search');
$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->drupalGet('search/help');
$this->submitForm(['keys' => 'sqruct'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
// Same for German.
$this->drupalGet('search/help', ['language' => $german]);
$this->submitForm(['keys' => 'asdrsad'], 'Search');
$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->drupalGet('search/help');
$this->submitForm(['keys' => 'non-word-item'], '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->drupalGet('search/help', ['language' => $german]);
$this->submitForm(['keys' => 'non-word-german'], 'Search');
$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->submitForm(['keys' => 'non-word-item'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC Help Test module');
// Same for German.
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'non-word-german'], 'Search');
$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->drupalGet('search/help');
$this->submitForm(['keys' => 'wcsrefsdf'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'sdeeeee'], 'Search');
$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->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'sdeeeee'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Updated translated title');
// Searching for the updated test shouldn't produce a match.
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'translated title'], 'Search');
$this->assertSearchResultsCount(0);
// Clear the caches and re-run cron - this should re-index the help.
$this->rebuildAll();
$this->cronRun();
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'sdeeeee'], 'Search');
$this->assertSearchResultsCount(0);
$this->drupalGet('admin/help', ['language' => $german]);
$this->submitForm(['keys' => 'translated title'], 'Search');
$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 help 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->drupalGet('search/help');
$this->submitForm(['keys' => 'non-word-item'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC Help Test module');
$this->drupalGet('search/help');
$this->submitForm(['keys' => 'not-a-word-english'], '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->drupalGet('search/help');
$this->submitForm(['keys' => 'non-word-item'], 'Search');
$this->assertSearchResultsCount(0);
}
/**
* Tests uninstalling the help_topics module.
*/
public function testUninstall(): void {
\Drupal::service('module_installer')->uninstall(['help_topics_test']);
// Ensure we can uninstall help_topics and use the help system without
// breaking.
$this->drupalLogin($this->createUser([
'administer modules',
'access help pages',
]));
$edit = [];
$edit['uninstall[help]'] = TRUE;
$this->drupalGet('admin/modules/uninstall');
$this->submitForm($edit, 'Uninstall');
$this->submitForm([], 'Uninstall');
$this->assertSession()->statusMessageContains('The selected modules have been uninstalled.', 'status');
$this->drupalGet('admin/help');
$this->assertSession()->statusCodeEquals(404);
}
/**
* Tests uninstalling the search module.
*/
public function testUninstallSearch(): void {
// Ensure we can uninstall search and use the help system without
// breaking.
$this->drupalLogin($this->createUser([
'administer modules',
'access help pages',
]));
$edit = [];
$edit['uninstall[search]'] = TRUE;
$this->drupalGet('admin/modules/uninstall');
$this->submitForm($edit, 'Uninstall');
$this->submitForm([], 'Uninstall');
$this->assertSession()->statusMessageContains('The selected modules have been uninstalled.', 'status');
$this->drupalGet('admin/help');
$this->assertSession()->statusCodeEquals(200);
// Rebuild the container to reflect the latest changes.
$this->rebuildContainer();
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('help'), 'The help module is still installed.');
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('search'), 'The search module is uninstalled.');
}
/**
* Asserts that help search returned the expected number of results.
*
* @param int $count
* The expected number of search results.
*
* @internal
*/
protected function assertSearchResultsCount(int $count): void {
$this->assertSession()->elementsCount('css', '.help_search-results > li', $count);
}
}

Some files were not shown because too many files have changed in this diff Show More