first commit

This commit is contained in:
2020-06-08 23:57:36 +02:00
commit 6277454f7a
16057 changed files with 1715382 additions and 0 deletions
@@ -0,0 +1,29 @@
langcode: en
status: true
dependencies:
enforced:
config:
- search.page.help_search
module:
- search
- system
theme:
- seven
id: seven_help_search
theme: seven
region: help
weight: -4
provider: null
plugin: search_form_block
settings:
id: search_form_block
label: 'Search help'
provider: search
label_display: visible
page_id: help_search
visibility:
request_path:
id: request_path
pages: /admin/help
negate: false
context_mapping: { }
@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
module:
- help_topics
id: help_search
label: Help
path: help
weight: 0
plugin: help_search
configuration: { }
@@ -0,0 +1,3 @@
search.plugin.help_search:
type: sequence
label: 'Help search'
@@ -0,0 +1,31 @@
<?php
/**
* @file
* Hooks provided by the Help Topics module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Perform alterations on help topic definitions.
*
* @param array $info
* Array of help topic plugin definitions keyed by their plugin ID.
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
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".
*/
@@ -0,0 +1,8 @@
name: Help Topics
type: module
description: 'Displays help topics provided by themes and modules.'
core: 8.x
package: Core (Experimental)
version: VERSION
dependencies:
- drupal:help
@@ -0,0 +1,51 @@
<?php
/**
* @file
* Install and uninstall functions for help_topics module.
*/
/**
* Implements hook_schema().
*/
function help_topics_schema() {
$schema['help_search_items'] = [
'description' => 'Stores information about indexed help search items',
'fields' => [
'sid' => [
'description' => 'Numeric index of this item in the search index',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'section_plugin_id' => [
'description' => 'The help section the item comes from',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'permission' => [
'description' => 'The permission needed to view this item',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'topic_id' => [
'description' => 'The topic ID of the item',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
],
'primary key' => ['sid'],
'indexes' => [
'section_plugin_id' => ['section_plugin_id'],
'topic_id' => ['topic_id'],
],
];
return $schema;
}
@@ -0,0 +1,84 @@
<?php
/**
* @file
* Displays help topics provided by modules and themes.
*/
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
/**
* Implements hook_help().
*/
function help_topics_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.help_topics':
$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 = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Help Topics module adds module- and theme-provided help topics to the module overviews from the core Help module. If the core Search module is enabled, these topics are also searchable. For more information, see the <a href=":online">online documentation for the Help Topics module</a>.', [':online' => 'https://www.drupal.org/documentation/modules/help_topics']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$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_topics/help_topics</em> as a guide when writing and formatting a help topic plugin for your theme or module.") . '</dd>';
$output .= '<dt>' . t('Translating help topics') . '</dt>';
$output .= '<dd>' . t('The title and body text of help topics provided by contributed modules and themes are translatable using the <a href=":locale_help">Interface Translation module</a>. Topics provided by custom modules and themes are also translatable if they have been viewed at least once in a non-English language, which triggers putting their translatable text into the translation database.', [':locale_help' => $locale_help]) . '</dd>';
$output .= '<dt>' . t('Configuring help search') . '</dt>';
$output .= '<dd>' . t('To search help, you will need to install the core Search module, configure a search page, and add a search block to the Help page or another administrative page. (A search page is provided automatically, and if you use the core Seven administrative theme, a help search block is shown on the main Help page.) Then users with search permissions, and permission to view help, will be able to search help. See the <a href=":search_help">Search module help page</a> for more information.', [':search_help' => $search_help]) . '</dd>';
$output .= '</dl>';
return ['#markup' => $output];
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_topics_theme() {
return [
'help_topic' => [
'variables' => [
'body' => [],
'related' => [],
],
],
];
}
/**
* Implements hook_modules_uninstalled().
*/
function help_topics_modules_uninstalled(array $modules) {
// Early return if search is not installed or if we're uninstalling this
// module.
if (!\Drupal::hasService('plugin.manager.search') ||
in_array('help_topics', $modules)) {
return;
}
$search_plugin_manager = \Drupal::service('plugin.manager.search');
if ($search_plugin_manager->hasDefinition('help_search')) {
// Ensure that topics for extensions that have been uninstalled are removed.
$help_search = $search_plugin_manager->createInstance('help_search');
$help_search->updateTopicList();
}
}
/**
* Implements hook_themes_uninstalled().
*/
function help_topics_themes_uninstalled(array $themes) {
// Use the same code as module uninstall to ensure that theme help is removed
// when a theme is uninstalled.
help_topics_modules_uninstalled([]);
}
@@ -0,0 +1,6 @@
help.help_topic:
path: '/admin/help/topic/{id}'
defaults:
_controller: '\Drupal\help_topics\Controller\HelpTopicPluginController::viewHelpTopic'
requirements:
_permission: 'access administration pages'
@@ -0,0 +1,25 @@
services:
help.breadcrumb:
class: Drupal\help_topics\HelpBreadcrumbBuilder
arguments: ['@string_translation']
tags:
- { name: breadcrumb_builder, priority: 900 }
public: false
plugin.manager.help_topic:
class: Drupal\help_topics\HelpTopicPluginManager
arguments: ['@module_handler', '@theme_handler', '@cache.discovery', '@app.root']
help.twig.loader:
class: Drupal\help_topics\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
plugin.manager.help_section_topics:
class: Drupal\help_topics\HelpSectionManager
decorates: plugin.manager.help_section
parent: plugin.manager.help_section
calls:
- [setSearchManager, ['@?plugin.manager.search']]
tags:
- { name: plugin_manager_cache_clear }
@@ -0,0 +1,14 @@
---
label: 'Banning IP addresses'
related:
- user.overview
---
{% set ban = render_var(url('ban.admin_page')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Ban visitors from one or more IP addresses from accessing and viewing your site.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>People</em> &gt; <a href="{{ ban }}"><em>IP address bans</em></a>{% endtrans %}</li>
<li>{% trans %}Enter an <em>IP address</em> and click <em>Add</em>.{% endtrans %}</li>
<li>{% trans %}You should see the IP address you entered listed under <em>Banned IP addresses</em>. Repeat the above steps to ban additional IP addresses.{% endtrans %}</li>
</ol>
@@ -0,0 +1,26 @@
---
label: 'Configuring a previously-placed block'
related:
- block.overview
- core.ui_accessibility
---
{% set layout_url = render_var(url('block.admin_display')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure the settings of a block that was previously placed in a region of a theme.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <a href="{{ layout_url }}"><em>Block layout</em></a>.{% endtrans %}</li>
<li>{% trans %}Click the name of the theme that contains the block.{% endtrans %}</li>
<li>{% trans %}Optionally, click <em>Demonstrate block regions</em> to see the regions of the theme.{% endtrans %}</li>
<li>{% trans %}If you only want to change the region where a block is located, or the ordering of blocks within a region, drag blocks to their desired positions and click <em>Save blocks</em>.{% endtrans %}</li>
<li>{% trans %}If you want to change additional settings, find the region where the block you want to update is currently located, and click <em>Configure</em> in the line of the block description.{% endtrans %}</li>
<li>{% trans %}Edit the block's settings. The available settings vary depending on the module that provides the block, but for all blocks you can change:{% endtrans %}
<ul>
<li>{% trans %}<em>Block title</em>: The heading for the block on your site -- for some blocks, you will need to check the <em>Override title</em> checkbox in order to enter a title{% endtrans %}</li>
<li>{% trans %}<em>Display title</em>: Check the box if you want the title displayed{% endtrans %}</li>
<li>{% trans %}<em>Visibility</em>: Add conditions for when the block should be displayed{% endtrans %}</li>
<li>{% trans %}<em>Region</em>: Change the theme region the block is displayed in{% endtrans %}</li>
</ul>
</li>
<li>{% trans %}Click <em>Save block</em>.{% endtrans %}</li>
</ol>
@@ -0,0 +1,20 @@
---
label: 'Managing blocks'
top_level: true
related:
- core.content_structure
---
<h2>{% trans %}What are blocks?{% endtrans %}</h2>
<p>{% trans %}Blocks are boxes of content rendered into an area, or region, of a web page of your site. Blocks are placed and configured specifically for each theme.{% endtrans %}</p>
<h2>{% trans %}What are custom blocks?{% endtrans %}</h2>
<p>{% trans %}Custom blocks are blocks whose content you can edit. You can define one or more <em>custom block types</em>, and attach fields to each custom block type. Custom blocks can be placed just like blocks provided by other modules.{% endtrans %}</p>
<h2>{% trans %}What is the block description?{% endtrans %}</h2>
<p>{% trans %}The block description is an identification name for a block, which is shown in the administrative interface. It is not displayed on the site.{% endtrans %}</p>
<h2>{% trans %}What is the block title?{% endtrans %}</h2>
<p>{% trans %}The block title is the heading that is optionally shown to site visitors when the block is placed in a region.{% endtrans %}</p>
<h2>{% trans %}Managing blocks overview{% endtrans %}</h2>
<p>{% trans %}The <em>Block</em> module allows you to place blocks in regions of your installed themes, and configure block settings. The <em>Custom Block</em> module allows you to custom block types and custom blocks. 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/blocks-chapter.html">Blocks chapter of the User Guide</a>{% endtrans %}</li>
</ul>
@@ -0,0 +1,19 @@
---
label: 'Placing a block'
related:
- block.overview
- block.configure
---
{% set layout_url = render_var(url('block.admin_display')) %}
{% set configure = render_var(url('help.help_topic', {'id': 'block.configure'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Place a block into a theme's region. {% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <a href="{{ layout_url }}"><em>Block layout</em></a>.{% endtrans %}</li>
<li>{% trans %}Click the name of the theme that you want to place the block in.{% endtrans %}</li>
<li>{% trans %}Optionally, click <em>Demonstrate block regions</em> to see the regions of the theme.{% endtrans %}</li>
<li>{% trans %}Find the region where you want the block, and click <em>Place block</em> in that region. A modal dialog will pop up.{% endtrans %}</li>
<li>{% trans %}Find the block you want to place and click <em>Place block</em>. A <em>Configure block</em> modal dialog will pop up.{% endtrans %}</li>
<li>{% trans %}Configure the block and click <em>Save block</em>; see <a href="{{ configure }}">Configuring a previously-placed block</a> for configuration details.{% endtrans %}</li>
</ol>
@@ -0,0 +1,18 @@
---
label: 'Creating a custom block'
related:
- block.overview
- block.configure
- block.place
- block_content.type
---
{% set content_url = render_var(url('entity.block_content.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a custom block, which can later be placed on the site.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>Block layout</em> &gt; <a href="{{ content_url }}"><em>Custom block library</em></a>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add custom block</em>. If you have more than one custom block type defined on your site, click the name of the type you want to create.{% endtrans %}</li>
<li>{% trans %}Enter a description of your block (to be shown to administrators) and the body text for your block.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>.{% endtrans %}</li>
</ol>
@@ -0,0 +1,23 @@
---
label: 'Defining a custom block type'
related:
- block.overview
- block.configure
- block.place
- block_content.add
- field_ui.add_field
- field_ui.manage_form
- field_ui.manage_display
---
{% set types_url = render_var(url('entity.block_content_type.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Define a custom block type and its fields.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>Block layout</em> &gt; <em>Custom block library</em> &gt; <a href="{{ types_url }}"><em>Block types</em></a>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add custom block type</em>.{% endtrans %}</li>
<li>{% trans %}Enter a label for this block type (shown in the administrative interface). Optionally, edit the automatically-generated machine name or the description.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. You will be returned to the <em>Block types</em> page.{% endtrans %}</li>
<li>{% trans %}Click <em>Manage fields</em> in the row of your new block type, and add the desired fields to your block type.{% endtrans %}</li>
<li>{% trans %}Optionally, click <em>Manage form display</em> or <em>Manage display</em> to change the editing form or field display for your block type.{% endtrans %}</li>
</ol>
@@ -0,0 +1,28 @@
---
label: 'Managing books'
top_level: true
---
{% set user_topic = render_var(url('help.help_topic', {'id': 'user.overview'})) %}
<h2>{% trans %}What is a book?{% endtrans %}</h2>
<p>{% trans %}A book is a structured group of content pages, arranged in a hierarchical structure called a <em>book outline</em>. A book hierarchy can be up to nine levels deep, and a book can include <em>Book page</em> content items or other content items. Every book has a default book-specific navigation block, which contains links that lead to the previous and next pages in the book and to the level above the current page in the book's structure.{% endtrans %}</p>
<h2>{% trans %}What are the permissions for books?{% endtrans %}</h2>
<p>{% trans %}The following permissions are needed to create and manage books; see <a href="{{ user_topic }}">Managing user accounts and site visitors</a> and its related topics for more about permissions.{% endtrans %}</p>
<dl>
<dt>{% trans %}Create new books{% endtrans %}</dt>
<dd>{% trans %}Allows users to add new books to the site.{% endtrans %}</dd>
<dt>{% trans %}Add content and child pages to books and manage their hierarchies{% endtrans %}</dt>
<dd>{% trans %}Allows users to add configured types of content to existing books.{% endtrans %}</dd>
<dt>{% trans %}Administer book outlines{% endtrans %}</dt>
<dd>{% trans %}Allows users to add <em>any</em> type of content to a book, use the book overview administration page, and rearrange the pages of a book from the book outline page.{% endtrans %}
<dt>{% trans %}Administer site configuration (in the System module section){% endtrans %}</dt>
<dd>{% trans %}Allows users to do many site configuration tasks, including configuring books. This permission has security implications.{% endtrans %}
</dd>
<dt>{% trans %}View printer-friendly books{% endtrans %}</dt>
<dd>{% trans %}Allows users to click the <em>printer-friendly version</em> link to generate a printer-friendly display of the page, which includes pages below it in the book outline.{% endtrans %}
</dd>
<dt>{% trans %}<em>Book page</em> content permissions (in the Node module section){% endtrans %}</dt>
<dd>{% trans %}Like other content types, the <em>Book page</em> content type has separate permissions for creating, editing, and deleting a user's own and any content items of this type.{% endtrans %}
</dd>
</dl>
<h2>{% trans %}Managing books{% endtrans %}</h2>
<p>{% trans %}Book management is handled by the core Book module. The topics listed below will help you create, edit, and configure books.{% endtrans %}</p>
@@ -0,0 +1,20 @@
---
label: 'Adding content to a book'
related:
- book.about
- book.configuring
- book.creating
- book.organizing
---
{% set node_add = render_var(url('node.add_page')) %}
{% set config = render_var(url('help.help_topic', {'id': 'book.configuring'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Add a page to an existing book.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Content</em> &gt; <a href="{{ node_add }}"><em>Add content</em></a> &gt; <em>Book page</em>. If you have configured additional content types that can be added to books, you can substitute a different content type for <em>Book page</em> (see the <a href="{{ config }}">Configuring books</a> topic for more information).{% endtrans %}</li>
<li>{% trans %}Enter a title for the page and some text for the body of the page.{% endtrans %}</li>
<li>{% trans %}In the vertical tabs area, click <em>Book Outline</em>. Select the book you want to add the page to in the <em>Book</em> select list. If you want to insert this page into the book hierarchy, also select the desired parent page in the <em>Parent item</em> select list.{% endtrans %}</li>
<li>{% trans %}Select the desired weight for the page in the <em>Weight</em> select list (pages with the same parent item are ordered from lowest to highest weight).{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em> to add the page to the book.{% endtrans %}</li>
</ol>
@@ -0,0 +1,18 @@
---
label: 'Configuring books'
related:
- book.about
- book.adding
- book.creating
- book.organizing
---
{% set settings = render_var(url('book.settings')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure settings related to books.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>Books</em> &gt; <a href="{{ settings }}"><em>Settings</em></a>.{% endtrans %}</li>
<li>{% trans %}Check all of the content types that you would like to use as book pages in the <em>Content types allowed in book outlines</em> field.{% endtrans %}</li>
<li>{% trans %}In the <em>Content type for the Add child page link</em> field, select the content type that will be created from the <em>Add child page</em> link on a book page.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>.{% endtrans %}</li>
</ol>
@@ -0,0 +1,17 @@
---
label: 'Creating a book'
related:
- book.about
- book.adding
- book.organizing
- book.configuring
---
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a new book.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Content</em> &gt; <em>Add content</em> &gt; <em>Book page</em>. If you have configured additional content types that can be added to books, you can substitute a different content type for <em>Book page</em>.{% endtrans %}</li>
<li>{% trans %}Enter a title for the book, and if desired, some text for the body of the book's title page.{% endtrans %}</li>
<li>{% trans %}In the vertical tabs area, click <em>Book Outline</em>. Select <em>- Create a new book -</em> from the <em>Book</em> select list.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em> to create the book.{% endtrans %}</li>
</ol>
@@ -0,0 +1,19 @@
---
label: 'Changing the outline of a book'
related:
- book.about
- book.adding
- book.creating
- book.configuring
---
{% set overview = render_var(url('book.admin')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Change the order and titles of pages within a book.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <a href="{{ overview }}"><em>Books</em></a>.{% endtrans %}</li>
<li>{% trans %}Click <em>Edit order and titles</em> for the book you would like to change.{% endtrans %}</li>
<li>{% trans %}Drag the book pages to the desired order.{% endtrans %}</li>
<li>{% trans %}If desired, enter new text for one or more of the page titles within the book.{% endtrans %}</li>
<li>{% trans %}Click <em>Save book pages</em>.{% endtrans %}</li>
</ol>
@@ -0,0 +1,21 @@
---
label: 'Managing height, width, and resolution breakpoints'
related:
- core.appearance
---
<h2>{% trans %}What are breakpoints?{% endtrans %}</h2>
<p>{% trans %}Breakpoints are the point at which your site's content will respond to provide the user with the best possible layout to consume the information. A breakpoint separates the height or width of viewports (screens, printers, and other media output types) into steps. For instance, a width breakpoint of 40em creates two steps: one for widths up to 40em and one for widths above 40em. Breakpoints can be used to define when layouts should shift from one form to another, when images should be resized, and other changes that need to respond to changes in viewport height or width.{% endtrans %}</p>
<h2>{% trans %}What are media queries?{% endtrans %}</h2>
<p>{% trans %}Media queries are a formal way to encode breakpoints. For instance, a width breakpoint at 40em would be written as the media query "(min-width: 40em)". Breakpoints are really just media queries with some additional meta-data, such as a name and multiplier information.{% endtrans %}</p>
<h2>{% trans %}What are resolution multipliers?{% endtrans %}</h2>
<p>{% trans %}Resolution multipliers are a measure of the viewport's device resolution, defined to be the ratio between the physical pixel size of the active device and the <a href="http://en.wikipedia.org/wiki/Device_independent_pixel">device-independent pixel</a> size. The Breakpoint module defines multipliers of 1, 1.5, and 2; when defining breakpoints, modules and themes can define which multipliers apply to each breakpoint.{% endtrans %}</p>
<h2>{% trans %}What is a breakpoint group?{% endtrans %}</h2>
<p>{% trans %}Breakpoints can be organized into groups. Modules and themes should use groups to separate out breakpoints that are meant to be used for different purposes, such as breakpoints for layouts or breakpoints for image sizing.{% endtrans %}</p>
<h2>{% trans %}Managing breakpoints and breakpoint groups overview{% endtrans %}</h2>
<p>{% trans %}The <em>Breakpoint</em> module allows you to define breakpoints and breakpoint groups in YAML files. Modules and themes can use the API provided by the <em>Breakpoint</em> module to define breakpoints and breakpoint groups, and to assign resolution multipliers to breakpoints.{% endtrans %}
</p>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/documentation/modules/breakpoint">{% trans %}Working with breakpoints in Drupal 8{% endtrans %}</a></li>
<li><a href="http://www.w3.org/TR/css3-mediaqueries/">{% trans %}W3C standards for media queries{% endtrans %}</a></li>
</ul>
@@ -0,0 +1,20 @@
---
label: 'Changing the color palette of a theme'
related:
- core.appearance
---
{% set appearance = render_var(url('system.themes_page')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Change the colors for links, backgrounds, and text in a theme that supports the Color module. Color-specific stylesheets will be generated and saved; you will need to follow these steps again to regenerate the stylesheets if you make any changes to the base stylesheets of your theme.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the Manage administrative menu, navigate to <a href="{{ appearance }}">Appearance</a>.{% endtrans %}</li>
<li>{% trans %}Click the <em>Settings</em> link for the theme you want to change the colors of.{% endtrans %}</li>
<li>{% trans %}In the <em>Color scheme</em> section, choose new colors for the backgrounds, text, and links that your theme defines colors for. However, if you do not see color settings, then your theme does not support the Color module.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. Color-specific stylesheets will be generated and saved in the file system.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/8/core/modules/color/overview">{% trans %}Color module overview{% endtrans %}</a></li>
</ul>
@@ -0,0 +1,20 @@
---
label: 'Using contextual links'
related:
- core.ui_components
- block.overview
---
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Use contextual links to access administrative tasks without navigating the administrative menu.{%
endtrans %}</p>
<h2>{% trans %}What are contextual links?{% endtrans %}</h2>
<p>{% trans %}<em>Contextual links</em> give users with the <em>Use contextual links</em> permission quick access to administrative tasks related to areas of non-administrative pages. For example, if a page on your site displays a block, the block would have a contextual link that would allow users with permission to configure the block. If the block contains a menu or a view, it would also have a contextual link for editing the menu links or the view. Clicking a contextual link takes you to the related administrative page directly, without needing to navigate through the administrative menu system.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Make sure that the core Contextual Links module is installed, and that you have a role with the <em>Use contextual links</em> permission. Optionally, make sure that a toolbar module is installed (either the core Toolbar module or a contributed module replacement).{% endtrans %}</li>
<li>{% trans %}Visit a non-administrative page on your site, such as the home page.{% endtrans %}</li>
<li>{% trans %}Locate a block or another area on the page that you want to edit or configure.{% endtrans %}</li>
<li>{% trans %}Make the contextual links button visible by hovering your mouse over that area in the page. In most themes, this button looks like a pencil and is placed in the upper right corner of the page area (upper left for right-to-left languages), and hovering will also temporarily outline the affected area. Alternatively, click the contextual links toggle button on the right end of the toolbar (left end for right-to-left languages), which will make all contextual link buttons on the page visible until it is clicked again.{% endtrans %}</li>
<li>{% trans %}While the contextual links button for the area of interest is visible, click the button to display the list of links for that area. Click a link in the list to visit the corresponding administrative page.{% endtrans %}</li>
<li>{% trans %}Complete your administrative task and save your settings, or cancel the action. You should be returned to the page you started from.{% endtrans %}</li>
</ol>
@@ -0,0 +1,20 @@
---
label: 'Changing the appearance of your site'
top_level: true
related:
- core.content_structure
---
{% set entities = render_var(url('help.help_topic', {'id': '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 Classy 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 <a href="{{ entities }}">Managing content structure</a> 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. You can also change the color palette of some themes, if the core Color module is installed and the theme supports it; some themes also have other settings. 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 chapter in the User Guide{% endtrans %}</a></li>
<li><a href="https://www.drupal.org/docs/8/theming">{% trans %}Theming Drupal 8{% endtrans %}</a></li>
</ul>
@@ -0,0 +1,46 @@
---
label: 'Managing content structure'
top_level: true
---
{% set help_url = render_var(url('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 web site 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>Custom block</em> entity type has <em>custom 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 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; the values can be numbers or text{% endtrans %}</li>
<li>{% trans %}Reference (core system): Stores entity references{% endtrans %}</li>
<li>{% trans %}Telephone (Telephone module): Stores telephone numbers{% endtrans %}</li>
<li>{% trans %}Text (Text module): Stores formatted and unformatted text{% endtrans %}</li>
</ul>
<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, Custom Block, Custom Menu Links, User, File, Image, Media, Taxonomy, Contact, and Aggregator 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, RDF, 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 <a href="{{ help_url }}">Help page</a>, 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 topic in the User Guide</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/planning-chapter.html">Planning your Site chapter in the User Guide</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/structure-reference-fields.html">Concept: Reference Fields topic in the User Guide</a>{% endtrans %}</li>
</ul>
@@ -0,0 +1,28 @@
---
label: 'Running and configuring cron'
related:
- core.maintenance
---
{% set cron = render_var(url('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; <a href="{{ cron }}"><em>Cron</em></a>. 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 in the User Guide</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/security-cron.html">Configuring Cron Maintenance Tasks in the User Guide</a>{% endtrans %}</li>
</ul>
@@ -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 topic in the 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 chapter in the User Guide</a>{% endtrans %}</li>
</ul>
@@ -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 upgrading 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 chapter in the User Guide</a>{% endtrans %}</li>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/security-chapter.html">Security and Maintenance chapter in the User Guide</a>{% endtrans %}</li>
</ul>
@@ -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 Drupal core 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>
@@ -0,0 +1,12 @@
---
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 %}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 chapter in the User Guide</a>{% endtrans %}</li>
</ul>
@@ -0,0 +1,11 @@
---
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 interface. These replacement 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>
</dl>
@@ -0,0 +1,18 @@
---
label: 'Using the administrative interface'
top_level: true
related:
- block.overview
---
<h2>{% trans %}Administrative interface overview{% endtrans %}</h2>
<p>{% trans %}The administrative interface has several components:{% endtrans %}</p>
<ul>
<li>{% trans %}Accessibility features, to enable all users to perform administrative tasks.{% 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 %}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>
<li>{% trans %}The core Tour module allows modules to provide interactive tours of administrative pages for more detailed help.{% endtrans %}</li>
</ul>
<p>{% trans %}See the related topics listed below for specific tasks.{% endtrans %}</p>
@@ -0,0 +1,23 @@
---
label: 'Adding a field to an entity sub-type'
related:
- core.content_structure
- field_ui.manage_display
- field_ui.manage_form
---
{% set content_types = render_var(url('entity.node_type.collection')) %}
{% set content_structure = render_var(url('help.help_topic', {'id': 'core.content_structure'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Add a field to an entity sub-type; see <a href="{{ content_structure }}">Managing content structure</a> for an overview of entity types and sub-types.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity sub-type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <a href="{{ content_types }}"><em>Content types</em></a>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to add the field to, and click <em>Manage fields</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add field</em>.{% endtrans %}</li>
<li>{% trans %}In <em>Add a new field</em>, select the type of field you want to add; see <a href="{{ content_structure }}">Managing content structure</a> for an overview of field types.{% endtrans %}</li>
<li>{% trans %}The <em>Label</em> field should now be visible; enter a label for the field, which is used as the field label for both content editing and content display.{% endtrans %}</li>
<li>{% trans %}Click <em>Save and continue</em>.{% endtrans %}</li>
<li>{% trans %}On the next screen, enter a value for <em>Allowed number of values</em>. You can limit the field to one value per entity item, a set number of values, or set it to have unlimited values. Click <em>Save field settings</em>.{% endtrans %}</li>
<li>{% trans %}On the next screen, optionally edit the settings for the field, which vary depending on what field type you are creating. For all fields, you can edit the <em>Label</em>, <em>Help text</em> (text to be displayed below the field on the content editing page), and <em>Required field</em> (to make it so a value must be entered in order to save the content when editing). You can also configure a default value for the field.{% endtrans %}</li>
<li>{% trans %}Click <em>Save settings</em>. You should be returned to the <em>Manage fields</em> page, with your new field in the list.{% endtrans %}</li>
</ol>
@@ -0,0 +1,24 @@
---
label: 'Configuring field display for an entity sub-type'
related:
- core.content_structure
- field_ui.add_field
- field_ui.manage_form
- core.ui_accessibility
---
{% set content_types = render_var(url('entity.node_type.collection')) %}
{% set content_structure = render_var(url('help.help_topic', {'id': 'core.content_structure'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure the <em>formatters</em> used to display the fields of an entity sub-type, their order in the display, and the formatter settings. See <a href="{{ content_structure }}">Managing content structure</a> for background information.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <a href="{{ content_types }}"><em>Content types</em></a>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to configure the display of, and click <em>Manage display</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Use the drag arrows to order the fields in your preferred order.{% endtrans %}</li>
<li>{% trans %}Drag any fields that you do not wish to see in the display to the <em>Disabled</em> section.{% endtrans %}</li>
<li>{% trans %}In the <em>Label</em> column, select the position for each field label in the display, or <em>- Hidden -</em> to hide a label. You can also choose <em>- Visually Hidden-</em> if you want the label's text to appear in the HTML page, so that screen readers and search engines can read it, but it will not be visible.{% endtrans %}</li>
<li>{% trans %}In the <em>Format</em> column, select the formatter for displaying each field.{% endtrans %}</li>
<li>{% trans %}After selecting the desired formatters, click the settings gear in each row to change the settings for the formatter.{% endtrans %}</li>
<li>{% trans %}When you are done making changes, click <em>Save</em>.{% endtrans %}</li>
<li>{% trans %}Test the display for your entity sub-type by viewing an entity. If needed, return to these steps to further refine the display.{% endtrans %}</li>
</ol>
@@ -0,0 +1,23 @@
---
label: 'Configuring the edit form for an entity sub-type'
related:
- core.content_structure
- field_ui.add_field
- field_ui.manage_display
- core.ui_accessibility
---
{% set content_types = render_var(url('entity.node_type.collection')) %}
{% set content_structure = render_var(url('help.help_topic', {'id': 'core.content_structure'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure the <em>widgets</em> used to edit the fields of an entity sub-type, their order on the form, and the widget settings. See <a href="{{ content_structure }}">Managing content structure</a> for background information.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <a href="{{ content_types }}"><em>Content types</em></a>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to configure the editing form for, and click <em>Manage form display</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Use the drag arrows to order the fields in your preferred order.{% endtrans %}</li>
<li>{% trans %}Drag any fields that you do not wish to see on the editing form to the <em>Disabled</em> section.{% endtrans %}</li>
<li>{% trans %}In the <em>Widget</em> column, select the widget for editing each field.{% endtrans %}</li>
<li>{% trans %}After selecting the desired widgets, click the settings gear in each row to change the settings for the widget.{% endtrans %}</li>
<li>{% trans %}When you are done making changes, click <em>Save</em>.{% endtrans %}</li>
<li>{% trans %}Test the editing form for your entity sub-type by editing or creating an entity. If needed, return to these steps to further refine the form.{% endtrans %}</li>
</ol>
@@ -0,0 +1,25 @@
---
label: 'Adding a reference field to an entity sub-type'
related:
- core.content_structure
- field_ui.add_field
- field_ui.manage_display
- field_ui.manage_form
---
{% set content_structure = render_var(url('help.help_topic', {'id': 'core.content_structure'})) %}
{% set content_types = render_var(url('entity.node_type.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Add an entity reference field to an entity sub-type; see <a href="{{ content_structure}}">Managing content structure</a> for more information on entities and reference fields.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity sub-type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <a href="{{ content_types }}"><em>Content types</em></a>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to add the field to, and click <em>Manage fields</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add field</em>.{% endtrans %}</li>
<li>{% trans %}In <em>Add a new field</em>, select the type of reference field you want to add. The <em>Reference</em> section of the select list shows the most common types of reference field; choose <em>Other...</em> if the entity type you want to reference is not listed.{% endtrans %}</li>
<li>{% trans %}The <em>Label</em> field should now be visible; enter a label for the field, which is used as the field label for both content editing and content display.{% endtrans %}</li>
<li>{% trans %}Click <em>Save and continue</em>.{% endtrans %}</li>
<li>{% trans %}On the next screen, verify that the type of entity you want to reference is shown in <em>Type of item to reference</em>, or select it if not. Enter a value for <em>Allowed number of values</em>. You can limit the field to one value per entity item, a set number of values, or set it to have unlimited values. Click <em>Save field settings</em>.{% endtrans %}</li>
<li>{% trans %}On the next screen, optionally edit the settings for <em>Label</em>, <em>Help text</em> (text to be displayed below the field on the content editing page), and <em>Required field</em> (to make it so a value must be entered in order to save the content when editing).{% endtrans %}</li>
<li>{% trans %}In the <em>Reference type</em> section, you will usually want to limit the entity sub-types that can be referenced; for example, if you are creating a <em>Content</em> reference, you can check one or two <em>Content type</em> choices. The choices will be easier for content editors to scan if you also choose a sort value (normally the entity title or label field).{% endtrans %}</li>
<li>{% trans %}Click <em>Save settings</em>. You should be returned to the <em>Manage fields</em> page, with your new field in the list.{% endtrans %}</li>
</ol>
@@ -0,0 +1,25 @@
---
label: 'Configuring help search'
top_level: true
related:
- block.place
- system.cache
- core.cron
---
{% set extend_url = render_var(url('system.modules_list')) %}
{% set help_url = render_var(url('help.main')) %}
{% set cache_help = render_var(url('help.help_topic', {'id': 'system.cache'})) %}
{% set cron_help = render_var(url('help.help_topic', {'id': '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><a href="{{ extend_url }}">Extend</a></em>. Verify that the Search, Help, Help Topics, and Block modules are installed (or install them if they are not already installed).{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &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, <a href="{{ cron_help }}">run Cron</a> until indexing is complete.{% endtrans %}</li>
<li>{% trans %}In the future, you can click <em>Rebuild search index</em> on this page, or <a href="{{ cache_help }}">clear the site cache</a>, 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 Seven 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><a href="{{ help_url }}">Help</a></em>. Verify that the search block is visible, and try a search.{% endtrans %}</li>
</ol>
@@ -0,0 +1,35 @@
---
label: 'Changing the layout for an entity'
related:
- core.appearance
- core.content_structure
- field_ui.manage_display
- block.overview
---
{% set content_types = render_var(url('entity.node_type.collection')) %}
{% set entities = render_var(url('help.help_topic', {'id': 'core.content_structure'})) %}
{% set blocks = render_var(url('help.help_topic', {'id': 'block.overview'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure an entity sub-type to have its fields displayed using a layout (see <a href="{{ entities }}">Managing content structure</a> for more on entities and fields).{% endtrans %}</p>
<h2>{% trans %}What are the parts of a layout?{% endtrans %}</h2>
<p>{% trans %}A layout consists of one or more <em>sections</em>. Each section can have from one to four <em>columns</em>. You can place blocks, including special blocks for the fields on the entity sub-type, in each column of each section (see <a href="{{ blocks }}">Managing blocks</a> for more on blocks).{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <a href="{{ content_types }}"><em>Content types</em></a>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to create a layout for, and click <em>Manage display</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Under <em>Layout options</em>, check <em>Use Layout Builder</em>. You can also check the box below to allow each entity item to have its layout individually customized (if it is left unchecked, the site will use the same layout for all items of this entity sub-type).{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. You will be returned to the <em>Manage display</em> page, but you will no longer see the table of fields of the classic display manager.{% endtrans %}</li>
<li>{% trans %}Click <em>Manage layout</em> to enter layout management view. A default layout will be set up for you, with a single one-column section containing the fields on your entity sub-type.{% endtrans %}</li>
<li>{% trans %}To remove the default section and start from an empty layout, find and click the <em>Remove</em> button for the default section, which looks like an X. Confirm by clicking <em>Remove</em> in the pop-up dialog.{% endtrans %}</li>
<li>{% trans %}Add new sections, each with one to four columns, to your layout. For instance, you might want a one-column section at the top, a two-column section in the middle, and then a one-column section at the bottom. To add a section, click <em>Add section</em> and click the desired number of columns. For multi-column sections, set the column width percentages and click <em>Add section</em> in the pop-up dialog.{% endtrans %}</li>
<li>{% trans %}In each section, click <em>Add block</em> to add a block. You will see a list of the blocks available on your site, plus a section called <em>Content fields</em> with a block for each field on your content item. Each block can be configured, if desired, with a <em>Title</em>, and for content field blocks, you can also configure the field formatter. Continue to add blocks to your sections until all the desired blocks and fields are displayed.{% endtrans %}</li>
<li>{% trans %}Verify your layout. You can check <em>Show content preview</em> to show a preview of what your layout will look like, or uncheck it to see the names of the fields and blocks in each section.{% endtrans %}</li>
<li>{% trans %}If needed, reorder the blocks by dragging them to new locations. If you hover over a block, a contextual menu will appear that will let you change the configuration of the block, remove the block, or <em>Move</em> blocks within the section using a more compact interface.{% endtrans %}</li>
<li>{% trans %}When you are satisfied with your layout, click <em>Save layout</em>.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li><a href="https://www.drupal.org/docs/8/core/modules/layout-builder/creating-layout-defaults">{% trans %}Creating layout defaults{% endtrans %}</a></li>
<li><a href="https://www.drupal.org/docs/8/core/modules/layout-builder/building-layouts-using-the-layout-builder-ui">{% trans %}Building Layouts Using the Layout Builder UI{% endtrans %}</a></li>
</ul>
@@ -0,0 +1,19 @@
---
label: 'Creating and using shortcut administrative links'
related:
- core.ui_components
---
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create, view, and use a set of shortcuts to access administrative pages.{% endtrans %}</p>
<h2>{% trans %}What are shortcuts?{% endtrans %}</h2>
<p>{% trans %}<em>Shortcuts</em> are quick links to administrative pages; they are managed by the core Shortcut module. A site can have one or more <em>shortcut sets</em>, which can be shared by one or more users (by default, there is only one set shared by all users); each set contains a limited number of shortcuts. Users need <em>Use shortcuts</em> permission to view shortcuts; <em>Edit current shortcut set</em> permission to add, delete, or edit the shortcuts in the set assigned to them; and <em>Select any shortcut set</em> permission to select a different shortcut set when editing their user profile. There is also an <em>Administer shortcuts</em> permission, which allows an administrator to do any of these actions, as well as select shortcut sets for other users.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Make sure that the core Shortcut module is enabled, and that you have a role with <em>Edit current shortcut set</em> or <em>Administer shortcuts</em> permission. Also, make sure that a toolbar module is installed (either the core Toolbar module or a contributed module replacement).{% endtrans %}</li>
<li>{% trans %}Navigate to an administrative page that you want in your shortcut list.{% endtrans %}</li>
<li>{% trans %}Click the shortcut link to add the page to your shortcut list -- in the core Seven administrative theme, the link looks like a star, and is displayed next to the page title. However, if the page is already in your shortcut set, clicking the shortcut link will remove it from your shortcut set.{% endtrans %}</li>
<li>{% trans %}Repeat until all the desired links have been added to your shortcut set.{% endtrans %}</li>
<li>{% trans %}Click <em>Shortcuts</em> in the toolbar to display your shortcuts, and verify that the list is complete.{% endtrans %}</li>
<li>{% trans %}Optionally, click <em>Edit shortcuts</em> at the right end of the shortcut list (left end in right-to-left languages), to remove links or change their order.{% endtrans %}</li>
<li>{% trans %}Click any link in the shortcut bar to go directly to the administrative page.{% endtrans %}</li>
</ol>
@@ -0,0 +1,19 @@
---
label: 'Clearing the site cache'
related:
- core.maintenance
---
{% set performance_url = render_var(url('system.performance_settings')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Clear the data in the site cache.{% endtrans %}</p>
<h2>{% trans %}What is the cache?{% endtrans %}</h2>
<p>{% trans %}Some of the calculations that are done when your site loads a page take a long time to run. To save time when these calculations would need to be done again, their results can be <em>cached</em> in your site's database. There are internal mechanisms to <em>clear</em> cached data when the conditions or assumptions that went into the calculation have changed, but you can also clear cached data manually. When your site is misbehaving, a good first step is to clear the cache and see if the problem goes away.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em><a href="{{ performance_url }}"><em>Performance</em></a></em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Clear all caches</em>. Your site's cached data will be cleared.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/prevent-cache.html">Concept: Cache in the User Guide</a>{% endtrans %}</li>
</ul>
@@ -0,0 +1,27 @@
---
label: 'Changing basic site settings'
top_level: true
related:
- user.security_account_settings
---
{% set regional_url = render_var(url('system.regional_settings')) %}
{% set information_url = render_var(url('system.site_information_settings')) %}
{% set datetime_url = render_var(url('entity.date_format.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure the basic settings of your site, including the site name, slogan, main email address, default time zone, default country, and the date formats to use.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>System</em> &gt; <em><a href="{{ information_url }}">Basic site settings</a></em>.{% endtrans %}</li>
<li>{% trans %}Enter the site name, slogan, and main email address for your site. {% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Regional and language</em> &gt; <em><a href="{{ regional_url }}">Regional settings</a></em>.{% endtrans %}</li>
<li>{% trans %}Select the default country and default time zone for your site.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Regional and language</em> &gt; <em><a href="{{ datetime_url }}">Date and time formats</a></em>.{% endtrans %}</li>
<li>{% trans %}Look at the <em>Patterns</em> for the Default long, medium, and short date formats. If any of them does not match the date format you want to use on your site, click <em>Edit</em> in that row to edit the format.{% endtrans %}</li>
<li>{% trans %}Adjust the <em>Format string</em> until the <em>Displayed</em> format matches what you want. (Date format strings are composed of PHP date format codes.){% endtrans %}</li>
<li>{% trans %}Click <em>Save format</em>. You should see a message indicating that the format was saved.{% endtrans %}</li>
<li>{% trans %}Repeat the previous three steps for any other date formats that need to be changed.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<p>{% trans %}<a href="https://php.net/manual/function.date.php">PHP date format codes reference</a>{% endtrans %}</p>
@@ -0,0 +1,24 @@
---
label: 'Configuring error responses, including 403/404 pages'
related:
- system.config_basic
- core.maintenance
---
{% set log_settings_url = render_var(url('system.logging_settings')) %}
{% set information_url = render_var(url('system.site_information_settings')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Set up your site to respond appropriately to site errors, including 403 and 404 page responses.{% endtrans %}</p>
<h2>{% trans %}What are 403 and 404 responses?{% endtrans %}</h2>
<p>{% trans %}When a user visits a web page, the web server sends a response code in addition to the page content. A normal, non-error response has code 200. If the page does not exist on the site, the response code is 404. If the page exists, but the user is not authorized to visit the page, the response code is 403. The core software provides default responses for both 403 and 404 codes, but if you prefer, you can create your own pages for each.{% endtrans %}</p>
<h2>{% trans %}What other errors can occur?{% endtrans %}</h2>
<p>{% trans %}Under some situations, your site can generate error messages. These can be due to user errors (such as entering invalid values in a form, or incorrect configuration), PHP runtime errors, or software bugs. Some errors may result in a <em>white screen of death</em> (a totally blank web page response); less drastic errors will generate error messages. You can configure what happens when an error message is generated.{% endtrans %}</p>
<h2>{% trans %}Steps {% endtrans %}</h2>
<ol>
<li>{% trans %}If desired, create pages to use for 403 and 404 responses. Note the URLs for these pages.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>System</em> &gt; <em><a href="{{ information_url }}">Basic site settings</a></em>.{% endtrans %}</li>
<li>{% trans %}In the <em>Error pages</em> section, enter the URL for your 403/403 pages, starting after the site home page URL. For example, if your site URL is <em>https://example.com</em> and your 404 page is <em>https://example.com/not-found</em>, you would enter <em>/not-found</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em><a href="{{ log_settings_url }}">Logging and errors</a></em>.{% endtrans %}</li>
<li>{% trans %}For a production site, select <em>None</em> under <em>Error messages to display</em>. For a site that is in development, select one of the other options, so that you are more aware of the errors the site is generating.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
</ol>
@@ -0,0 +1,21 @@
---
label: 'Enabling and disabling maintenance mode'
related:
- core.maintenance
- system.cache
---
{% set maintenance_url = render_var(url('system.site_maintenance_mode')) %}
{% set cache_help = render_var(url('help.help_topic', {'id': 'system.cache'})) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Put your site in maintenance mode to perform maintenance operations, and then return to normal mode when finished.{% endtrans %}</p>
<h2>{% trans %}What is maintenance mode?{% endtrans %}</h2>
<p>{% trans %}When your site is in maintenance mode, most site visitors will see a simple maintenance mode message page, rather than being able to use the full functionality of the site. Users with <em>Use the site in maintenance mode</em> permission who are already logged in will be able to use the full site, and the log in page at <em>/user</em> will also be accessible to anyone.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <a href="{{ maintenance_url }}"><em>Maintenance mode</em></a>.{% endtrans %}</li>
<li>{% trans %}Check <em>Put site into maintenance mode</em>, optionally change the <em>Message to display when in maintenance mode</em>, and click <em>Save configuration</em>. Your site will be in maintenance mode.{% endtrans %}</li>
<li>{% trans %}Perform your maintenance operations.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em><a href="{{ maintenance_url }}">Maintenance mode</a></em>.{% endtrans %}</li>
<li>{% trans %}Uncheck <em>Put site into maintenance mode</em> and click <em>Save configuration</em>. Your site will be back in normal operation mode.{% endtrans %}</li>
<li>{% trans %}Clear the site cache. See <a href="{{ cache_help }}">Clearing the site cache</a> for instructions.{% endtrans %}</li>
</ol>
@@ -0,0 +1,17 @@
---
label: 'Installing a module'
related:
- core.extending
- system.module_uninstall
---
{% set extend_url = render_var(url('system.modules_list')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Install a core module, or a contributed module that has already been downloaded.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <a href="{{ extend_url }}"><em>Extend</em></a>.{% endtrans %}</li>
<li>{% trans %}Enter a word from the module name or description into the filter box, to make the list of modules smaller. Locate the module you want to install.{% endtrans %}</li>
<li>{% trans %}Check the box next to the name of the module you want to install; you can also check more than one box to install multiple modules at the same time. If the checkbox is disabled for the module you are trying to install, expand the information to see why -- you may need to download an additional module that your module requires.{% endtrans %}</li>
<li>{% trans %}Click <em>Install</em> at the bottom of the page. If you chose to install a module with dependencies that were not already installed, or if you chose an Experimental module, confirm your choice on the next page.{% endtrans %}</li>
<li>{% trans %}Wait for the module (or modules) to be installed. You should be returned to the <em>Extend</em> page with a message saying the module or modules were installed.{% endtrans %}</li>
</ol>
@@ -0,0 +1,19 @@
---
label: 'Uninstalling a module'
related:
- core.extending
- system.module_install
- system.maintenance_mode
---
{% set uninstall_url = render_var(url('system.modules_uninstall')) %}
{% set maintenance_topic = render_var(url('help.help_topic', {'id': 'system.maintenance_mode'})) %}<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Uninstall a module. Your site should be in <a href="{{ maintenance_topic }}">maintenance mode</a> when you uninstall modules.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Extend</em> &gt; <a href="{{ uninstall_url }}"><em>Uninstall</em></a>.{% endtrans %}</li>
<li>{% trans %}Enter a word from the module name or description into the filter box, to make the list of modules smaller. Locate the module you want to uninstall.{% endtrans %}</li>
<li>{% trans %}In the <em>Description</em> column, see if there are reasons that this module cannot be uninstalled. For example, you may have created content using this module (which you would need to delete first), or there may be another module installed that requires this module to be installed (you would need to uninstall the other module first).{% endtrans %}</li>
<li>{% trans %}If there are no reasons listed, the module can be uninstalled. Check the box in the <em>Uninstall</em> column, next to the module's name.{% endtrans %}</li>
<li>{% trans %}Click <em>Uninstall</em> at the bottom of the page. Verify the list of modules to be uninstalled and configuration to be deleted on the confirmation page, and click <em>Uninstall</em>.{% endtrans %}</li>
<li>{% trans %}Wait for the module to be uninstalled. You should be returned to the <em>Uninstall</em> page with a message saying the module was uninstalled.{% endtrans %}</li>
</ol>
@@ -0,0 +1,20 @@
---
label: 'Running reports on your site'
related:
- core.maintenance
- core.security
- system.config_error
---
{% set status_url = render_var(url('system.status')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Run reports to learn about the status and health of your site.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Reports</em> &gt; <a href="{{ status_url }}"><em>Status report</em></a> to see a report that summarizes the health and status of your site. If there are any warnings or errors, you will need to fix them.{% endtrans %}</li>
<li>{% trans %}If you have the core Database Logging module installed, in the <em>Manage</em> administrative menu, navigate to <em>Reports</em> &gt; <em>Recent log messages</em> to see a report of the error and informational messages your site has generated. You can filter the report by <em>Severity</em> to see only the most critical messages, if desired.{% endtrans %}</li>
<li>{% trans %}If you have the core Update Manager module installed, in the <em>Manage</em> administrative menu, navigate to <em>Reports</em> &gt; <em>Available updates</em> to see a report of the updates that are available for your site software. If <em>Last checked</em> is far in the past, click <em>Check manually</em> to update the report. Scan the report; if Drupal core or any modules or themes have security updates available, you should update them as soon as possible.{% endtrans %}</li>
</ol>
<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 chapter in the User Guide</a>, which includes information on how to update your site's core software, modules, and themes{% endtrans %}</li>
</ul>
@@ -0,0 +1,18 @@
---
label: 'Installing a theme and setting default themes'
related:
- core.appearance
- system.theme_uninstall
---
{% set themes_url = render_var(url('system.themes_page')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Install a core theme, or a contributed theme that has already been downloaded. Choose the default themes to use for the site and for administrative pages.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <a href="{{ themes_url }}"><em>Appearance</em></a>.{% endtrans %}</li>
<li>{% trans %}Locate the themes that you want to use as the site default theme and for administrative pages.{% endtrans %}</li>
<li>{% trans %}For each of these themes, if the theme is in the <em>Uninstalled themes</em> section, click the <em>Install</em> link to install the theme. Wait for the theme to be installed (translations might be downloaded). You should be returned to the <em>Appearance</em> page.{% endtrans %}</li>
<li>{% trans %}Locate the theme that you want to be your default theme, which should now be in the <em>Installed themes</em> section. If it is not already labeled as the <em>default theme</em>, click the <em>Set as default</em> link.{% endtrans %}</li>
<li>{% trans %}At the bottom of the page, select the <em>Administration theme</em> that you want to use on administrative pages. Click <em>Save configuration</em> if you selected a new theme.{% endtrans %}</li>
<li>{% trans %}If you changed the default theme for your site, visit the site home page or another page on the non-administration part of your site and verify that the site is using the new theme. If you changed the administration theme, verify that the new theme is used on administrative pages.{% endtrans %}</li>
</ol>
@@ -0,0 +1,15 @@
---
label: 'Uninstalling an unused theme'
related:
- core.appearance
- system.theme_install
---
{% set themes_url = render_var(url('system.themes_page')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Uninstall a theme that was previously installed, but is no longer being used on the site.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <a href="{{ themes_url }}"><em>Appearance</em></a>.{% endtrans %}</li>
<li>{% trans %}Locate the theme that you want to uninstall, in the <em>Installed themes</em> section.{% endtrans %}</li>
<li>{% trans %}Click the <em>Uninstall</em> link to install the theme. If there is not an <em>Uninstall</em> link, the theme cannot be uninstalled because it is either being used as the site default theme, being used as the <em>Administration theme</em>, or is the base theme for another installed theme.{% endtrans %}</li>
</ol>
@@ -0,0 +1,16 @@
---
label: 'Taking tours of administrative pages'
related:
- core.ui_components
---
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Take a tour of an administrative page.{% endtrans %}</p>
<h2>{% trans %}What are tours?{% endtrans %}</h2>
<p>{% trans %}The core Tour module provides users with <em>tours</em>, which are guided tours of the administrative interface. Each tour starts on a particular administrative page, and consists of one or more <em>tips</em> that highlight elements of the page, guide you through a workflow, or explain key concepts. Users need <em>Access tour</em> permission to view tours, and JavaScript must be enabled in their browsers.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Make sure that the core Tour module is installed, and that you have a role with the <em>Access tour</em> permission. Also, make sure that a toolbar module is installed (either the core Toolbar module or a contributed module replacement).{% endtrans %}</li>
<li>{% trans %}Visit an administrative page that has a tour, such as the edit view page provided by the core Views UI module.{% endtrans %}</li>
<li>{% trans %}Click the <em>Tour</em> button at the right end of the toolbar (left end for right-to-left languages). The first tip of the tour should appear.{% endtrans %}</li>
<li>{% trans %}Click the <em>Next</em> button to advance to the next tip, and <em>End tour</em> at the end to close the tour.{% endtrans %}</li>
</ol>
@@ -0,0 +1,20 @@
---
label: 'Creating a user account'
related:
- user.security_account_settings
- user.overview
---
{% set people_url = render_var(url('entity.user.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a new user account.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <a href="{{ people_url}}"><em>People</em></a>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add user</em>.{% endtrans %}</li>
<li>{% trans %}Enter the <em>Email address</em>, <em>Username</em>, and <em>Password</em> (twice) for the new user.{% endtrans %}</li>
<li>{% trans %}Verify that the <em>Roles</em> checked for the new user are correct.{% endtrans %}</li>
<li>{% trans %}If you want the new user to receive an email message notifying them of the new account, check <em>Notify user of new account</em>.{% endtrans %}</li>
<li>{% trans %}Optionally, change other settings on the form.{% endtrans %}</li>
<li>{% trans %}Click <em>Create new account</em>.{% endtrans %}</li>
<li>{% trans %}You will be left on the <em>Add user</em> page; repeat these steps if you have more user accounts to create.{% endtrans %}</li>
</ol>
@@ -0,0 +1,16 @@
---
label: 'Adding a new role'
related:
- user.overview
- user.permissions
---
{% set roles_url = render_var(url('entity.user_role.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a new role.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>People</em> &gt; <a href="{{ roles_url}}"><em>Roles</em></a>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add role</em>.{% endtrans %}</li>
<li>{% trans %}Enter the desired <em>Role name</em>. If desired, click <em>Edit</em> to change the <em>Machine name</em> for the role.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. You should be returned to the <em>Roles</em> page and your new role should be in the role list.{% endtrans %}</li>
</ol>
@@ -0,0 +1,12 @@
---
label: 'Managing user accounts and site visitors'
top_level: true
---
<h2>{% trans %}What is a user?{% endtrans %}</h2>
<p>{% trans %}A user is anyone accessing or viewing your site. <em>Anonymous</em> users are users who are not logged in, and <em>Authenticated</em> users are users who are logged in.{% endtrans %}</p>
<h2>{% trans %}What is a role?{% endtrans %}</h2>
<p>{% trans %}<em>Roles</em> are used to group and classify users; each user can be assigned one or more roles. There are also special roles for all anonymous and all authenticated users.{% endtrans %}</p>
<h2>{% trans %}What is a permission?{% endtrans %}</h2>
<p>{% trans %}Granting a <em>permission</em> to a role allows users who have been assigned that role to perform an action on the site, such as viewing content, editing or creating a particular type of content, administering settings for a particular module, or using a particular function of the site (such as search).{% endtrans %}</p>
<h2>{% trans %}Overview of managing user accounts and visitors{% endtrans %}</h2>
<p>{% trans %}The core User module allows users to register, log in, and log out, and administrators to manage user roles and permissions. The core Ban module allows administrators to ban certain IP addresses from accessing the site. Depending on which modules you have installed on your site, the related topics below will help you with tasks related to managing user accounts and visitors.{% endtrans %}</p>
@@ -0,0 +1,16 @@
---
label: 'Modifying the permissions for a role'
related:
- user.overview
- user.new_role
- core.security
---
{% set permissions_url = render_var(url('user.admin_permissions')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Modify the permissions for an existing role.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>People</em> &gt; <a href="{{ permissions_url}}"><em>Permissions</em></a>.{% endtrans %}</li>
<li>{% trans %}Review the permissions for the role, paying particular attention to the permissions marked with <em>Warning: Give to trusted roles only; this permission has security implications.</em> Uncheck permissions that this role should not have, in the row of the permission and the column of the role; check permissions that this role should have.{% endtrans %}</li>
<li>{% trans %}Click <em>Save permissions</em>.{% endtrans %}</li>
</ol>
@@ -0,0 +1,35 @@
---
label: 'Configuring how user accounts are created and deleted'
related:
- core.security
- user.overview
---
{% set account_settings_url = render_var(url('entity.user.admin_form')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure settings related to how user accounts are created and deleted.{% endtrans %}</p>
<h2>{% trans %}What are the settings related to user account creation and deletion?{% endtrans %}</h2>
<ul>
<li>{% trans %}You can make it possible for new users to register themselves for accounts, with or without email verification or administrative approval. Or, you can make it so only administrators with <em>Administer users</em> permission can register new users.{% endtrans %}</li>
<li>{% trans %}You can configure what happens to content that a user created, if their account is <em>canceled</em> (deleted).{% endtrans %}</li>
<li>{% trans %}You can edit the email messages that are sent to users when their accounts are pending, approved, created, blocked, or canceled, or when they request a password reset.{% endtrans %}</li>
</ul>
<h2>{% trans %}What are variables in email message text?{% endtrans %}</h2>
<p>{% trans %}<em>Variables</em> are short text strings, enclosed in square brackets [], that you can insert into configured email message text. When an individual message is generated, data from your site is substituted for the variables. Some commonly-used variables are:{% endtrans %}</p>
<ul>
<li>{% trans %}[site:name]: The name of your web site.{% endtrans %}</li>
<li>{% trans %}[site:url]: The URL of your web site.{% endtrans %}</li>
<li>{% trans %}[site:login-url]: The URL where users can log in to your site.{% endtrans %}</li>
<li>{% trans %}[user:display-name]: The user's displayed name.{% endtrans %}</li>
<li>{% trans %}[user:account-name]: The users's account name.{% endtrans %}</li>
<li>{% trans %}[user:mail]: The user's email alias.{% endtrans %}</li>
<li>{% trans %}[user:one-time-login-url]: An expiring URL that a user can use to log in once, if they need to reset their password.{% endtrans %}</li>
</ul>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>People</em> &gt; <a href="{{ account_settings_url}}"><em>Account settings</em></a>.{% endtrans %}</li>
<li>{% trans %}Select the method you want to use for creating user accounts, and check or uncheck the box that requires email verification, to match the settings you want for your site.{% endtrans %}</li>
<li>{% trans %}Select the desired option for what happens to content that a user created if their account is canceled.{% endtrans %}</li>
<li>{% trans %}Optionally, edit the text of email messages related to user accounts.{% endtrans %}</li>
<li>{% trans %}Verify that the other settings are correct.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
</ol>
@@ -0,0 +1,17 @@
---
label: 'Modifying or deleting a user account'
related:
- user.security_account_settings
- user.overview
---
{% set people_url = render_var(url('entity.user.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Update or delete an existing user account.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <a href="{{ people_url}}"><em>People</em></a>.{% endtrans %}</li>
<li>{% trans %}Enter all or part of the user name or email address of the user account you want to update or delete, and click <em>Filter</em>. A short list of user accounts, including the account of interest, should be shown in the table; if not, modify the filter text until you can find the account of interest.{% endtrans %}</li>
<li>{% trans %}Click <em>Edit</em> in the <em>Operations</em> area of the account of interest.{% endtrans %}</li>
<li>{% trans %}To delete the user account, scroll to the bottom and click <em>Cancel account</em>. Select what you want to happen to the user's content on the next screen, and click <em>Cancel account</em>.{% endtrans %}</li>
<li>{% trans %}To update the user account, enter new values in the form and click <em>Save</em>.{% endtrans %}</li>
</ol>
@@ -0,0 +1,117 @@
<?php
namespace Drupal\help_topics\Controller;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\help_topics\HelpTopicPluginManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Controller for help topic plugins.
*
* @internal
* Help Topic is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class HelpTopicPluginController extends ControllerBase {
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The Help Topic plugin manager.
*
* @var \Drupal\help_topics\HelpTopicPluginManagerInterface
*/
protected $helpTopicPluginManager;
/**
* Constructs a HelpTopicPluginController object.
*
* @param \Drupal\help_topics\HelpTopicPluginManagerInterface $help_topic_plugin_manager
* The help topic plugin manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(HelpTopicPluginManagerInterface $help_topic_plugin_manager, RendererInterface $renderer) {
$this->helpTopicPluginManager = $help_topic_plugin_manager;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.help_topic'),
$container->get('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_topics\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_topics\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;
}
}
@@ -0,0 +1,173 @@
<?php
namespace Drupal\help_topics;
/**
* Extracts Front Matter from the beginning of a source.
*
* @internal
* This front matter extractor only supports help topic discovery and is not
* part of the public API.
*/
final class FrontMatter {
/**
* The separator used to indicate front matter data.
*
* @var string
*/
const FRONT_MATTER_SEPARATOR = '---';
/**
* The regular expression used to extract the YAML front matter content.
*
* @var string
*/
const FRONT_MATTER_REGEXP = "{^(?:" . self::FRONT_MATTER_SEPARATOR . ")[\r\n|\n]*(.*?)[\r\n|\n]+(?:" . self::FRONT_MATTER_SEPARATOR . ")[\r\n|\n]*(.*)$}s";
/**
* The parsed source.
*
* @var array
*/
protected $parsed;
/**
* A serializer class.
*
* @var string
*/
protected $serializer;
/**
* The source.
*
* @var string
*/
protected $source;
/**
* FrontMatter constructor.
*
* @param string $source
* A string source.
* @param string $serializer
* A class that implements
* \Drupal\Component\Serialization\SerializationInterface.
*/
public function __construct($source, $serializer = '\Drupal\Component\Serialization\Yaml') {
assert(is_string($source), '$source must be a string');
assert(is_string($serializer), '$serializer must be a string');
if (!is_subclass_of($serializer, '\Drupal\Component\Serialization\SerializationInterface')) {
throw new \InvalidArgumentException('The $serializer parameter must reference a class that implements \Drupal\Component\Serialization\SerializationInterface.');
}
$this->serializer = $serializer;
$this->source = $source;
}
/**
* Creates a new FrontMatter instance.
*
* @param string $source
* A string source.
* @param string $serializer
* A class that implements
* \Drupal\Component\Serialization\SerializationInterface.
*
* @return static
*/
public static function load($source, $serializer = '\Drupal\Component\Serialization\Yaml') {
return new static($source, $serializer);
}
/**
* Parses the source.
*
* @return array
* An associative array containing:
* - code: The real source code.
* - data: The front matter data extracted and decoded.
* - line: The line number where the real source code starts.
*
* @throws \Drupal\Component\Serialization\Exception\InvalidDataTypeException
* Exception thrown when the Front Matter cannot be parsed.
*/
private function parse() {
if (!$this->parsed) {
$this->parsed = [
'code' => $this->source,
'data' => [],
'line' => 1,
];
// Check for front matter data.
$len = strlen(static::FRONT_MATTER_SEPARATOR);
$matches = [];
if (substr($this->parsed['code'], 0, $len + 1) === static::FRONT_MATTER_SEPARATOR . "\n" || substr($this->parsed['code'], 0, $len + 2) === static::FRONT_MATTER_SEPARATOR . "\r\n") {
preg_match(static::FRONT_MATTER_REGEXP, $this->parsed['code'], $matches);
$matches = array_map('trim', $matches);
}
// Immediately return if the code doesn't contain front matter data.
if (empty($matches)) {
return $this->parsed;
}
// Set the extracted source code.
$this->parsed['code'] = $matches[2];
// Set the extracted front matter data. Do not catch any exceptions here
// as doing so would only obfuscate any errors found in the front matter
// data. Typecast to an array to ensure top level scalars are in an array.
if ($matches[1]) {
$this->parsed['data'] = (array) $this->serializer::decode($matches[1]);
}
// Determine the real source line by counting newlines from the data and
// then adding 2 to account for the front matter separator (---) wrappers
// and then adding 1 more for the actual line number after the data.
$this->parsed['line'] = count(preg_split('/\r\n|\n/', $matches[1])) + 3;
}
return $this->parsed;
}
/**
* Retrieves the extracted source code.
*
* @return string
* The extracted source code.
*
* @throws \Drupal\Component\Serialization\Exception\InvalidDataTypeException
* Exception thrown when the Front Matter cannot be parsed.
*/
public function getCode() {
return $this->parse()['code'];
}
/**
* Retrieves the extracted front matter data.
*
* @return array
* The extracted front matter data.
*
* @throws \Drupal\Component\Serialization\Exception\InvalidDataTypeException
* Exception thrown when the Front Matter cannot be parsed.
*/
public function getData() {
return $this->parse()['data'];
}
/**
* Retrieves the line where the source code starts, after any data.
*
* @return int
* The source code line.
*
* @throws \Drupal\Component\Serialization\Exception\InvalidDataTypeException
* Exception thrown when the Front Matter cannot be parsed.
*/
public function getLine() {
return $this->parse()['line'];
}
}
@@ -0,0 +1,54 @@
<?php
namespace Drupal\help_topics;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Provides a breadcrumb builder for help topic pages.
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class HelpBreadcrumbBuilder implements BreadcrumbBuilderInterface {
use StringTranslationTrait;
/**
* Constructs the HelpBreadcrumbBuilder.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation service.
*/
public function __construct(TranslationInterface $string_translation) {
$this->stringTranslation = $string_translation;
}
/**
* {@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($this->t('Home'), '<front>'));
$breadcrumb->addLink(Link::createFromRoute($this->t('Administration'), 'system.admin'));
$breadcrumb->addLink(Link::createFromRoute($this->t('Help'), 'help.main'));
return $breadcrumb;
}
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\help_topics;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\help\HelpSectionManager as CoreHelpSectionManager;
/**
* Decorates the Help Section plugin manager to provide help topic search.
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class HelpSectionManager extends CoreHelpSectionManager {
/**
* The search manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $searchManager;
/**
* Sets the search manager.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface|null $search_manager
* The search manager if the Search module is installed.
*/
public function setSearchManager(PluginManagerInterface $search_manager = NULL) {
$this->searchManager = $search_manager;
}
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions() {
parent::clearCachedDefinitions();
if ($this->searchManager && $this->searchManager->hasDefinition('help_search') && $this->moduleHandler->moduleExists('help_topics')) {
// Rebuild the index on cache clear so that new help topics are indexed
// and any changes due to help topics edits or translation changes are
// picked up.
$help_search = $this->searchManager->createInstance('help_search');
$help_search->markForReindex();
}
}
}
@@ -0,0 +1,185 @@
<?php
namespace Drupal\help_topics;
use Drupal\Component\Discovery\DiscoveryException;
use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\FileSystem\RegexDirectoryIterator;
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_topics\HelpTopicTwig
* @see \Drupal\help_topics\HelpTopicTwigLoader
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class 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.
list($file_name_provider,) = explode('.', $plugin_id, 2);
// Only the Help Topics module can provide help for other extensions.
// @todo https://www.drupal.org/project/drupal/issues/3072312 Remove
// help_topics special case once Help Topics is stable and core
// modules can provide their own help topics.
if ($provider !== 'help_topics' && $provider !== $file_name_provider) {
throw new DiscoveryException("$file file name should begin with '$provider'");
}
$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::load(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;
}
}
@@ -0,0 +1,64 @@
<?php
namespace Drupal\help_topics;
use Drupal\Core\Link;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Url;
/**
* Base class for help topic plugins.
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
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);
}
}
@@ -0,0 +1,83 @@
<?php
namespace Drupal\help_topics;
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_topics\HelpTopicPluginManager
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
interface 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 = []);
}
@@ -0,0 +1,191 @@
<?php
namespace Drupal\help_topics;
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_topics\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
* mymodule_prefix:
* deriver: 'Drupal\mymodule\Plugin\Deriver\HelpTopicDeriver'
* @endcode
*
* @see \Drupal\help_topics\HelpTopicDiscovery
* @see \Drupal\help_topics\HelpTopicTwig
* @see \Drupal\help_topics\HelpTopicTwigLoader
* @see \Drupal\help_topics\HelpTopicPluginInterface
* @see \Drupal\help_topics\HelpTopicPluginBase
* @see hook_help_topics_info_alter()
* @see plugin_api
* @see \Drupal\Component\Plugin\Derivative\DeriverInterface
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class 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' => '',
];
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new HelpTopicManager object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* 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, ThemeHandlerInterface $theme_handler, CacheBackendInterface $cache_backend, $root) {
// Note that the parent construct is not called because this not use
// annotated class discovery.
$this->moduleHandler = $module_handler;
$this->themeHandler = $theme_handler;
$this->alterInfo('help_topics_info');
// Use the 'config:core.extension' cache tag so the plugin cache is
// invalidated on theme install and uninstall.
$this->setCacheBackend($cache_backend, 'help_topics', ['config:core.extension']);
$this->root = (string) $root;
}
/**
* {@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 mymodule.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 help topic provided by
// extensions that are yet to be installed.
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;
}
}
@@ -0,0 +1,16 @@
<?php
namespace Drupal\help_topics;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Defines an interface for managing help topics and storing their definitions.
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
interface HelpTopicPluginManagerInterface extends PluginManagerInterface {
}
@@ -0,0 +1,90 @@
<?php
namespace Drupal\help_topics;
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_topics\HelpTopicDiscovery
* @see \Drupal\help_topics\HelpTopicTwigLoader
* @see \Drupal\help_topics\HelpTopicPluginManager
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class HelpTopicTwig extends HelpTopicPluginBase implements ContainerFactoryPluginInterface {
/**
* The Twig environment.
*
* @var \Drupal\Core\Template\TwigEnvironment
*/
protected $twig;
/**
* 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, TwigEnvironment $twig) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->twig = $twig;
}
/**
* {@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;
}
}
@@ -0,0 +1,96 @@
<?php
namespace Drupal\help_topics;
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\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_topics\HelpTopicDiscovery
* @see \Drupal\help_topics\HelpTopicTwig
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class HelpTopicTwigLoader extends \Twig_Loader_Filesystem {
/**
* {@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($name) {
$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 = FrontMatter::load($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->getCode();
}
}
catch (InvalidDataTypeException $e) {
throw new LoaderError(sprintf('Malformed YAML in help topic "%s": %s.', $path, $e->getMessage()));
}
return new Source($contents, $name, $path);
}
}
@@ -0,0 +1,283 @@
<?php
namespace Drupal\help_topics\Plugin\HelpSection;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\help_topics\SearchableHelpInterface;
use Drupal\help_topics\HelpTopicPluginInterface;
use Drupal\help_topics\HelpTopicPluginManagerInterface;
use Drupal\Core\Language\LanguageDefault;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\StringTranslation\TranslationManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the help topics list section for the help page.
*
* @HelpSection(
* id = "help_topics",
* title = @Translation("Topics"),
* weight = -10,
* description = @Translation("Topics can be provided by modules or themes. Top-level help topics on your site:"),
* permission = "access administration pages"
* )
*
* @internal
* Help Topic is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface, SearchableHelpInterface {
/**
* The plugin manager.
*
* @var \Drupal\help_topics\HelpTopicPluginManagerInterface
*/
protected $pluginManager;
/**
* The top level help topic plugins.
*
* @var \Drupal\help_topics\HelpTopicPluginInterface[]
*/
protected $topLevelPlugins;
/**
* The merged top level help topic plugins cache metadata.
*
* @var \Drupal\Core\Cache\CacheableMetadata
*/
protected $cacheableMetadata;
/**
* The Renderer service to format the username and node.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The default language object.
*
* @var \Drupal\Core\Language\LanguageDefault
*/
protected $defaultLanguage;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The string translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationManager
*/
protected $stringTranslation;
/**
* Constructs a HelpTopicSection object.
*
* @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_topics\HelpTopicPluginManagerInterface $plugin_manager
* The help topic plugin manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Language\LanguageDefault $default_language
* The default language object.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\StringTranslation\TranslationManager $translation_manager
* The translation manager. We are using a method that doesn't exist on an
* interface, so require this class.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, HelpTopicPluginManagerInterface $plugin_manager, RendererInterface $renderer, LanguageDefault $default_language, LanguageManagerInterface $language_manager, TranslationManager $translation_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->pluginManager = $plugin_manager;
$this->renderer = $renderer;
$this->defaultLanguage = $default_language;
$this->languageManager = $language_manager;
$this->translationManager = $translation_manager;
}
/**
* {@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_topics\HelpTopicPluginInterface[]
* The top level help topic plugins.
*/
protected function getPlugins() {
if (!isset($this->topLevelPlugins)) {
$definitions = $this->pluginManager->getDefinitions();
// 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() ? -1 : 1;
}
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->renderPlain($title_build);
$cacheable_metadata = CacheableMetadata::createFromRenderArray($title_build);
// Render the body in this language. For this, we need to set up a render
// context, because the Twig plugins that provide the body assumes one
// is present.
$context = new RenderContext();
$build = [
'body' => $this->renderer->executeInRenderContext($context, [$plugin, 'getBody']),
];
$topic['text'] = $this->renderer->renderPlain($build);
$cacheable_metadata->addCacheableDependency(CacheableMetadata::createFromRenderArray($build));
$cacheable_metadata->addCacheableDependency($plugin);
if (!$context->isEmpty()) {
$cacheable_metadata->addCacheableDependency($context->pop());
}
// Add the other information.
$topic['url'] = $plugin->toUrl();
$topic['cacheable_metadata'] = $cacheable_metadata;
}
finally {
// Restore the original language.
if ($old_language->getId() !== $language->getId()) {
$this->defaultLanguage->set($old_language);
$this->translationManager->setDefaultLangcode($old_language->getId());
$this->languageManager->reset();
}
}
return $topic;
}
/**
* Gets the merged CacheableMetadata for all the top level help topic plugins.
*
* @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;
}
}
@@ -0,0 +1,507 @@
<?php
namespace Drupal\help_topics\Plugin\Search;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Config\Config;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\Query\PagerSelectExtender;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\State\StateInterface;
use Drupal\help\HelpSectionManager;
use Drupal\help_topics\SearchableHelpInterface;
use Drupal\search\Plugin\SearchIndexingInterface;
use Drupal\search\Plugin\SearchPluginBase;
use Drupal\search\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
*
* @SearchPlugin(
* id = "help_search",
* title = @Translation("Help")
* )
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class HelpSearch extends SearchPluginBase implements AccessibleInterface, SearchIndexingInterface {
/**
* The current database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* A config object for 'search.settings'.
*
* @var \Drupal\Core\Config\Config
*/
protected $searchSettings;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The Drupal account to use for checking for access to search.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The state object.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The help section plugin manager.
*
* @var \Drupal\help\HelpSectionManager
*/
protected $helpSectionManager;
/**
* 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 administration pages');
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function getType() {
return $this->getPluginId();
}
/**
* {@inheritdoc}
*/
public function execute() {
if ($this->isSearchExecutable()) {
$results = $this->findResults();
if ($results) {
return $this->prepareResults($results);
}
}
return [];
}
/**
* Finds the search results.
*
* @return \Drupal\Core\Database\StatementInterface|null
* Results from search query execute() method, or NULL if the search
* failed.
*/
protected function findResults() {
// We need to check access for the current user to see the topics that
// could be returned by search. Each entry in the help_search_items
// database has an optional permission that comes from the HelpSection
// plugin, in addition to the generic 'access administration pages'
// permission. In order to enforce these permissions so only topics that
// the current user has permission to view are selected by the query, make
// a list of the permission strings and pre-check those permissions.
$this->addCacheContexts(['user.permissions']);
if (!$this->account->hasPermission('access administration pages')) {
return NULL;
}
$permissions = $this->database
->select('help_search_items', 'hsi')
->distinct()
->fields('hsi', ['permission'])
->condition('permission', '', '<>')
->execute()
->fetchCol();
$denied_permissions = array_filter($permissions, function ($permission) {
return !$this->account->hasPermission($permission);
});
$query = $this->database
->select('search_index', 'i')
// Restrict the search to the current interface language.
->condition('i.langcode', $this->languageManager->getCurrentLanguage()->getId())
->extend(SearchQuery::class)
->extend(PagerSelectExtender::class);
$query->innerJoin('help_search_items', 'hsi', 'i.sid = hsi.sid AND i.type = :type', [':type' => $this->getType()]);
if ($denied_permissions) {
$query->condition('hsi.permission', $denied_permissions, 'NOT IN');
}
$query->searchExpression($this->getKeywords(), $this->getType());
$find = $query
->fields('i', ['langcode'])
->fields('hsi', ['section_plugin_id', 'topic_id'])
// Since SearchQuery makes these into GROUP BY queries, if we add
// a field, for PostgreSQL we also need to make it an aggregate or a
// GROUP BY. In this case, we want GROUP BY.
->groupBy('i.langcode')
->groupBy('hsi.section_plugin_id')
->groupBy('hsi.topic_id')
->limit(10)
->execute();
// Check query status and set messages if needed.
$status = $query->getStatus();
if ($status & SearchQuery::EXPRESSIONS_IGNORED) {
$this->messenger->addWarning($this->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', ['@count' => $this->searchSettings->get('and_or_limit')]));
}
if ($status & SearchQuery::LOWER_CASE_OR) {
$this->messenger->addWarning($this->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'));
}
if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) {
$this->messenger->addWarning($this->formatPlural($this->searchSettings->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
}
return $find;
}
/**
* Prepares search results for display.
*
* @param \Drupal\Core\Database\StatementInterface $found
* Results found from a successful search query execute() method.
*
* @return array
* List of search result render arrays, with links, snippets, etc.
*/
protected function prepareResults(StatementInterface $found) {
$results = [];
$plugins = [];
$languages = [];
$keys = $this->getKeywords();
foreach ($found as $item) {
$section_plugin_id = $item->section_plugin_id;
if (!isset($plugins[$section_plugin_id])) {
$plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id);
}
if ($plugins[$section_plugin_id]) {
$langcode = $item->langcode;
if (!isset($languages[$langcode])) {
$languages[$langcode] = $this->languageManager->getLanguage($item->langcode);
}
$topic = $plugins[$section_plugin_id]->renderTopicForSearch($item->topic_id, $languages[$langcode]);
if ($topic) {
if (isset($topic['cacheable_metadata'])) {
$this->addCacheableDependency($topic['cacheable_metadata']);
}
$results[] = [
'title' => $topic['title'],
'link' => $topic['url']->toString(),
'snippet' => search_excerpt($keys, $topic['title'] . ' ' . $topic['text'], $item->langcode),
'langcode' => $item->langcode,
];
}
}
}
return $results;
}
/**
* {@inheritdoc}
*/
public function updateIndex() {
// Update the list of items to be indexed.
$this->updateTopicList();
// Find some items that need to be updated. Start with ones that have
// never been indexed.
$limit = (int) $this->searchSettings->get('index.cron_limit');
$query = $this->database->select('help_search_items', 'hsi');
$query->fields('hsi', ['sid', 'section_plugin_id', 'topic_id']);
$query->leftJoin('search_dataset', 'sd', 'sd.sid = hsi.sid AND sd.type = :type', [':type' => $this->getType()]);
$query->where('sd.sid IS NULL');
$query->groupBy('hsi.sid')
->groupBy('hsi.section_plugin_id')
->groupBy('hsi.topic_id')
->range(0, $limit);
$items = $query->execute()->fetchAll();
// If there is still space in the indexing limit, index items that have
// been indexed before, but are currently marked as needing a re-index.
if (count($items) < $limit) {
$query = $this->database->select('help_search_items', 'hsi');
$query->fields('hsi', ['sid', 'section_plugin_id', 'topic_id']);
$query->leftJoin('search_dataset', 'sd', 'sd.sid = hsi.sid AND sd.type = :type', [':type' => $this->getType()]);
$query->condition('sd.reindex', 0, '<>');
$query->groupBy('hsi.sid')
->groupBy('hsi.section_plugin_id')
->groupBy('hsi.topic_id')
->range(0, $limit - count($items));
$items = $items + $query->execute()->fetchAll();
}
// Index the items we have chosen, in all available languages.
$language_list = $this->languageManager->getLanguages(LanguageInterface::STATE_CONFIGURABLE);
$section_plugins = [];
$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);
}
}
/**
* {@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);
}
/**
* {@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 = new Condition('OR');
$condition->condition('sd.reindex', 0, '<>')
->isNull('sd.sid');
$query->condition($condition);
$remaining = $query->execute()->fetchField();
return [
'remaining' => $remaining,
'total' => $total,
];
}
/**
* Removes an item or items from the search index.
*
* @param int|int[] $sids
* Search ID (sid) of item or items to remove.
*/
protected function removeItemsFromIndex($sids) {
$sids = (array) $sids;
// Remove items from our table in batches of 100, to avoid problems
// with having too many placeholders in database queries.
foreach (array_chunk($sids, 100) as $this_list) {
$this->database->delete('help_search_items')
->condition('sid', $this_list, 'IN')
->execute();
}
// Remove items from the search tables individually, as there is no bulk
// function to delete items from the search index.
foreach ($sids as $sid) {
$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_topics\SearchableHelpInterface|false
* Plugin object, or FALSE if it is not searchable.
*/
protected function getSectionPlugin($section_plugin_id) {
/** @var \Drupal\help\HelpSectionPluginInterface $section_plugin */
$section_plugin = $this->helpSectionManager->createInstance($section_plugin_id);
// Intentionally return boolean to allow caching of results.
return $section_plugin instanceof SearchableHelpInterface ? $section_plugin : FALSE;
}
}
@@ -0,0 +1,46 @@
<?php
namespace Drupal\help_topics;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides an interface for a HelpSection plugin that also supports search.
*
* @see \Drupal\help\HelpSectionPluginInterface
*
* @internal
* Help Topics is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
interface SearchableHelpInterface {
/**
* Returns the IDs of topics that should be indexed for searching.
*
* @return string[]
* An array of topic IDs that should be searchable. IDs need to be
* unique within this HelpSection plugin.
*/
public function listSearchableTopics();
/**
* Renders one topic for search indexing or search results.
*
* @param string $topic_id
* The ID of the topic to be indexed.
* @param \Drupal\Core\Language\LanguageInterface $language
* The language to render the topic in.
*
* @return array
* An array of information about the topic, with elements:
* - title: The title of the topic in this language.
* - text: The text of the topic in this language.
* - url: The URL of the topic as a \Drupal\Core\Url object.
* - cacheable_metadata: (optional) An object to add as a cache dependency
* if this topic is shown in search results.
*/
public function renderTopicForSearch($topic_id, LanguageInterface $language);
}
@@ -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>
@@ -0,0 +1,5 @@
---
label: 'Help topic with bad HTML syntax'
top_level: true
---
<p>{% trans %}Body goes here{% endtrans %}</h3>
@@ -0,0 +1,4 @@
---
label: 'Help topic containing no body'
top_level: true
---
@@ -0,0 +1,5 @@
---
label: 'Help topic with H1 header'
top_level: true
---
<h1>{% trans %}Body goes here{% endtrans %}</h1>
@@ -0,0 +1,5 @@
---
label: 'Help topic with h3 without an h2'
top_level: true
---
<h3>{% trans %}Body goes here{% endtrans %}</h3>
@@ -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>
@@ -0,0 +1,4 @@
---
label: 'Help topic not top level or related to top level'
---
<p>{% trans %}Body goes here{% endtrans %}</p>
@@ -0,0 +1,5 @@
---
label: 'Help topic with untranslated text'
top_level: true
---
<p>Body goes here</p>
@@ -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>
@@ -0,0 +1,4 @@
---
label: 'Linked topic'
---
<p>{% trans %}This topic is not supposed to be top-level.{% endtrans %}</p>
@@ -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_url = render_var(url('help.help_topic', {id: 'help_topics_test.additional'})) %}
<p>{% trans %}This is a test. It should <a href="{{ help_topic_url }}">link to the additional topic</a>. Also there should be a related topic link below to the Help module topic page and the linked topic.{% endtrans %}</p>
<p>{% trans %}Nonworditem totranslate.{% endtrans %}</p>
<p>{% trans %}Test translation.{% endtrans %}</p>
@@ -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'
@@ -0,0 +1,7 @@
# 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
core: 8.x
@@ -0,0 +1,25 @@
<?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) {
$info['help_topics_test.test']['top_level'] = \Drupal::state()->get('help_topics_test.test:top_level', TRUE);
}
@@ -0,0 +1,2 @@
access test help:
title: 'Access the test help section'
@@ -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;
}
}
@@ -0,0 +1,79 @@
<?php
namespace Drupal\help_topics_test\Plugin\HelpSection;
use Drupal\help_topics\SearchableHelpInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;
use Drupal\help\Plugin\HelpSection\HelpSectionPluginBase;
/**
* Provides a searchable help section for testing.
*
* @HelpSection(
* id = "help_topics_test",
* title = @Translation("Test section"),
* weight = 100,
* description = @Translation("For testing search"),
* permission = "access test help"
* )
*/
class TestHelpSection extends HelpSectionPluginBase implements SearchableHelpInterface {
/**
* {@inheritdoc}
*/
public function listTopics() {
return [
Link::fromTextAndUrl('Foo', Url::fromUri('https://foo.com')),
Link::fromTextAndUrl('Bar', Url::fromUri('https://bar.com')),
];
}
/**
* {@inheritdoc}
*/
public function listSearchableTopics() {
return ['foo', 'bar'];
}
/**
* {@inheritdoc}
*/
public function renderTopicForSearch($topic_id, LanguageInterface $language) {
switch ($topic_id) {
case 'foo':
if ($language->getId() == 'en') {
return [
'title' => 'Foo in English title wcsrefsdf',
'text' => 'Something about foo body notawordenglish sqruct',
'url' => Url::fromUri('https://foo.com'),
];
}
return [
'title' => 'Foomm Foreign heading',
'text' => 'Fake foreign foo text notawordgerman asdrsad',
'url' => Url::fromUri('https://mm.foo.com'),
];
case 'bar':
if ($language->getId() == 'en') {
return [
'title' => 'Bar in English',
'text' => 'Something about bar anotherwordenglish asdrsad',
'url' => Url::fromUri('https://bar.com'),
];
}
return [
'title' => \Drupal::state()->get('help_topics_test:translated_title', 'Barmm Foreign sdeeeee'),
'text' => 'Fake foreign barmm anotherwordgerman sqruct',
'url' => Url::fromUri('https://mm.bar.com'),
];
default:
throw new \InvalidArgumentException('Unexpected ID encountered');
}
}
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\help_topics_test\Plugin\HelpTopic;
use Drupal\Core\Cache\Cache;
use Drupal\help_topics\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;
}
}
@@ -0,0 +1,266 @@
<?php
namespace Drupal\Tests\help_topics\Functional;
use Drupal\Tests\Traits\Core\CronRunTrait;
use Drupal\help_topics\Plugin\Search\HelpSearch;
/**
* Verifies help topic search.
*
* @group help_topics
*/
class HelpTopicSearchTest extends HelpTopicTranslatedTestBase {
use CronRunTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'search',
'locale',
'language',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Log in.
$this->drupalLogin($this->createUser([
'access administration pages',
'administer site configuration',
'view the administration theme',
'administer permissions',
'administer languages',
'administer search',
'access test help',
'search content',
]));
// Add English language and set to default.
$this->drupalPostForm('admin/config/regional/language/add', [
'predefined_langcode' => 'en',
], 'Add language');
$this->drupalPostForm('admin/config/regional/language', [
'site_default_language' => 'en',
], 'Save configuration');
// When default language is changed, the container is rebuilt in the child
// site, so a rebuild in the main site is required to use the new container
// here.
$this->rebuildContainer();
// Before running cron, verify that a search returns no results.
$this->drupalPostForm('search/help', ['keys' => 'notawordenglish'], 'Search');
$this->assertSearchResultsCount(0);
// Run cron until the topics are fully indexed, with a limit of 100 runs
// to avoid infinite loops.
$num_runs = 100;
$plugin = HelpSearch::create($this->container, [], 'help_search', []);
do {
$this->cronRun();
$remaining = $plugin->indexStatus()['remaining'];
} while (--$num_runs && $remaining);
$this->assertNotEmpty($num_runs);
$this->assertEmpty($remaining);
// Visit the Search settings page and verify it says 100% indexed.
$this->drupalGet('admin/config/search/pages');
$this->assertSession()->pageTextContains('100% of the site has been indexed');
}
/**
* Tests help topic search.
*/
public function testHelpSearch() {
$german = \Drupal::languageManager()->getLanguage('de');
$session = $this->assertSession();
// Verify that when we search in English for a word that is only in
// English text, we find the topic. Note that these "words" are provided
// by the topics that come from
// \Drupal\help_topics_test\Plugin\HelpSection\TestHelpSection.
$this->drupalPostForm('search/help', ['keys' => 'notawordenglish'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
// Same for German.
$this->drupalPostForm('search/help', ['keys' => 'notawordgerman'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(1);
$session->linkExists('Foomm Foreign heading');
// Verify when we search in English for a word that only exists in German,
// we get no results.
$this->drupalPostForm('search/help', ['keys' => 'notawordgerman'], 'Search');
$this->assertSearchResultsCount(0);
$session->pageTextContains('no results');
// Same for German.
$this->drupalPostForm('search/help', ['keys' => 'notawordenglish'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(0);
$session->pageTextContains('no results');
// Verify when we search in English for a word that exists in one topic
// in English and a different topic in German, we only get the one English
// topic.
$this->drupalPostForm('search/help', ['keys' => 'sqruct'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
// Same for German.
$this->drupalPostForm('search/help', ['keys' => 'asdrsad'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(1);
$session->linkExists('Foomm Foreign heading');
// All of the above tests used the TestHelpSection plugin. Also verify
// that we can search for translated regular help topics, in both English
// and German.
$this->drupalPostForm('search/help', ['keys' => 'nonworditem'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC Help Test module');
// Click the link and verify we ended up on the topic page.
$this->clickLink('ABC Help Test module');
$session->pageTextContains('This is a test');
$this->drupalPostForm('search/help', ['keys' => 'nonwordgerman'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(1);
$session->linkExists('ABC-Hilfetestmodul');
$this->clickLink('ABC-Hilfetestmodul');
$session->pageTextContains('Übersetzung testen.');
// Verify that we can search from the admin/help page.
$this->drupalGet('admin/help');
$session->pageTextContains('Search help');
$this->drupalPostForm(NULL, ['keys' => 'nonworditem'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC Help Test module');
// Same for German.
$this->drupalPostForm('admin/help', ['keys' => 'nonwordgerman'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(1);
$session->linkExists('ABC-Hilfetestmodul');
// Verify we can search for title text (other searches used text
// that was part of the body).
$this->drupalPostForm('search/help', ['keys' => 'wcsrefsdf'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('Foo in English title wcsrefsdf');
$this->drupalPostForm('admin/help', ['keys' => 'sdeeeee'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(1);
$session->linkExists('Barmm Foreign sdeeeee');
// Just changing the title and running cron is not enough to reindex so
// 'sdeeeee' still hits a match. The content is updated because the help
// topic is rendered each time.
\Drupal::state()->set('help_topics_test:translated_title', 'Updated translated title');
$this->cronRun();
$this->drupalPostForm('admin/help', ['keys' => 'sdeeeee'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(1);
$session->linkExists('Updated translated title');
// Searching for the updated test shouldn't produce a match.
$this->drupalPostForm('admin/help', ['keys' => 'translated title'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(0);
// Clear the caches and re-run cron - this should re-index the help.
$this->rebuildAll();
$this->cronRun();
$this->drupalPostForm('admin/help', ['keys' => 'sdeeeee'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(0);
$this->drupalPostForm('admin/help', ['keys' => 'translated title'], 'Search', [
'language' => $german,
]);
$this->assertSearchResultsCount(1);
$session->linkExists('Updated translated title');
// Verify the cache tags and contexts.
$session->responseHeaderContains('X-Drupal-Cache-Tags', 'config:search.page.help_search');
$session->responseHeaderContains('X-Drupal-Cache-Tags', 'search_index:help_search');
$session->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.permissions');
$session->responseHeaderContains('X-Drupal-Cache-Contexts', 'languages:language_interface');
// Log in as a user that does not have permission to see TestHelpSection
// items, and verify they can still search for help topics but not see these
// items.
$this->drupalLogin($this->createUser([
'access administration pages',
'administer site configuration',
'view the administration theme',
'administer permissions',
'administer languages',
'administer search',
'search content',
]));
$this->drupalGet('admin/help');
$session->pageTextContains('Search help');
$this->drupalPostForm('search/help', ['keys' => 'nonworditem'], 'Search');
$this->assertSearchResultsCount(1);
$session->linkExists('ABC Help Test module');
$this->drupalPostForm('search/help', ['keys' => 'notawordenglish'], 'Search');
$this->assertSearchResultsCount(0);
$session->pageTextContains('no results');
// Uninstall the test module and verify its topics are immediately not
// searchable.
\Drupal::service('module_installer')->uninstall(['help_topics_test']);
$this->drupalPostForm('search/help', ['keys' => 'nonworditem'], 'Search');
$this->assertSearchResultsCount(0);
}
/**
* Tests uninstalling the help_topics module.
*/
public function testUninstall() {
// Ensure we can uninstall help_topics and use the help system without
// breaking.
$this->drupalLogin($this->rootUser);
$edit = [];
$edit['uninstall[help_topics]'] = TRUE;
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->drupalPostForm(NULL, NULL, t('Uninstall'));
$this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.');
$this->drupalGet('admin/help');
$this->assertSession()->statusCodeEquals(200);
}
/**
* Asserts that help search returned the expected number of results.
*
* @param int $count
* The expected number of search results.
*/
protected function assertSearchResultsCount($count) {
$this->assertSession()->elementsCount('css', '.help_search-results > li', $count);
}
}
@@ -0,0 +1,253 @@
<?php
namespace Drupal\Tests\help_topics\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Menu\AssertBreadcrumbTrait;
/**
* Verifies help topic display and user access to help based on permissions.
*
* @group help_topics
*/
class HelpTopicTest extends BrowserTestBase {
use AssertBreadcrumbTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'help_topics_test',
'help',
'help_topics',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The admin user that will be created.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The anonymous user that will be created.
*
* @var \Drupal\user\UserInterface
*/
protected $anyUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// These tests rely on some markup from the 'Seven' theme and we test theme
// provided help topics.
\Drupal::service('theme_installer')->install(['seven', 'help_topics_test_theme']);
\Drupal::service('config.factory')->getEditable('system.theme')->set('admin', 'seven')->save();
// Place various blocks.
$settings = [
'theme' => 'seven',
'region' => 'help',
];
$this->placeBlock('help_block', $settings);
$this->placeBlock('local_tasks_block', $settings);
$this->placeBlock('local_actions_block', $settings);
$this->placeBlock('page_title_block', $settings);
$this->placeBlock('system_breadcrumb_block', $settings);
// Create users.
$this->adminUser = $this->createUser([
'access administration pages',
'view the administration theme',
'administer permissions',
'administer site configuration',
]);
$this->anyUser = $this->createUser([]);
}
/**
* Tests the main help page and individual pages for topics.
*/
public function testHelp() {
$session = $this->assertSession();
// Log in the regular user.
$this->drupalLogin($this->anyUser);
$this->verifyHelp(403);
// Log in the admin user.
$this->drupalLogin($this->adminUser);
$this->verifyHelp();
$this->verifyHelpLinks();
$this->verifyBreadCrumb();
// Verify that help topics text appears on admin/help, and cache tags.
$this->drupalGet('admin/help');
$session->responseContains('<h2>Topics</h2>');
$session->pageTextContains('Topics can be provided by modules or themes');
$session->responseHeaderContains('X-Drupal-Cache-Tags', 'core.extension');
// Verify links for for help topics and order.
$page_text = $this->getTextContent();
$start = strpos($page_text, 'Topics can be provided');
$pos = $start;
foreach ($this->getTopicList() as $info) {
$name = $info['name'];
$session->linkExists($name);
$new_pos = strpos($page_text, $name, $start);
$this->assertTrue($new_pos > $pos, 'Order of ' . $name . ' is correct on page');
$pos = $new_pos;
}
// Ensure the plugin manager alter hook works as expected.
$session->linkExists('ABC Help Test module');
\Drupal::state()->set('help_topics_test.test:top_level', FALSE);
\Drupal::service('plugin.manager.help_topic')->clearCachedDefinitions();
$this->drupalGet('admin/help');
$session->linkNotExists('ABC Help Test module');
\Drupal::state()->set('help_topics_test.test:top_level', TRUE);
\Drupal::service('plugin.manager.help_topic')->clearCachedDefinitions();
$this->drupalGet('admin/help');
// Ensure all the expected links are present before uninstalling.
$session->linkExists('ABC Help Test module');
$session->linkExists('ABC Help Test');
$session->linkExists('XYZ Help Test theme');
// Uninstall the test module and verify the topics are gone, after
// reloading page.
$this->container->get('module_installer')->uninstall(['help_topics_test']);
$this->drupalGet('admin/help');
$session->linkNotExists('ABC Help Test module');
$session->linkNotExists('ABC Help Test');
$session->linkExists('XYZ Help Test theme');
// Uninstall the test theme and verify the topic is gone.
$this->container->get('theme_installer')->uninstall(['help_topics_test_theme']);
$this->drupalGet('admin/help');
$session->linkNotExists('XYZ Help Test theme');
}
/**
* Verifies the logged in user has access to various help links and pages.
*
* @param int $response
* (optional) The HTTP response code to test for. If it's 200 (default),
* the test verifies the user sees the help; if it's not, it verifies they
* are denied access.
*/
protected function verifyHelp($response = 200) {
// Verify access to help topic pages.
foreach ($this->getTopicList() as $topic => $info) {
// View help topic page.
$this->drupalGet('admin/help/topic/' . $topic);
$session = $this->assertSession();
$session->statusCodeEquals($response);
if ($response == 200) {
// Verify page information.
$name = $info['name'];
$session->titleEquals($name . ' | Drupal');
$session->responseContains('<h1 class="page-title">' . $name . '</h1>');
foreach ($info['tags'] as $tag) {
$session->responseHeaderContains('X-Drupal-Cache-Tags', $tag);
}
}
}
}
/**
* Verifies links on the test help topic page and other pages.
*
* Assumes an admin user is logged in.
*/
protected function verifyHelpLinks() {
$session = $this->assertSession();
// Verify links on the test top-level page.
$page = 'admin/help/topic/help_topics_test.test';
$links = [
'link to the additional topic' => 'Additional topic',
'Linked topic' => 'This topic is not supposed to be top-level',
'Additional topic' => 'This topic should get listed automatically',
];
foreach ($links as $link_text => $page_text) {
$this->drupalGet($page);
$this->clickLink($link_text);
$session->pageTextContains($page_text);
}
// Verify theme provided help topics work and can be related.
$this->drupalGet('admin/help/topic/help_topics_test_theme.test');
$session->pageTextContains('This is a theme provided topic.');
$this->assertStringContainsString('This is a theme provided topic.', $session->elementExists('css', 'article')->getText());
$this->clickLink('Additional topic');
$session->linkExists('XYZ Help Test theme');
// Verify that the non-top-level topics do not appear on the Help page.
$this->drupalGet('admin/help');
$session->linkNotExists('Linked topic');
$session->linkNotExists('Additional topic');
}
/**
* Gets a list of topic IDs to test.
*
* @return array
* A list of topics to test, in the order in which they should appear. The
* keys are the machine names of the topics. The values are arrays with the
* following elements:
* - name: Displayed name.
* - tags: Cache tags to test for.
*/
protected function getTopicList() {
return [
'help_topics_test.test' => [
'name' => 'ABC Help Test module',
'tags' => ['core.extension'],
],
'help_topics_derivatives:test_derived_topic' => [
'name' => 'Label for test_derived_topic',
'tags' => ['foobar'],
],
'help_topics_test_direct_yml' => [
'name' => 'Test direct yaml topic label',
'tags' => ['foobar'],
],
];
}
/**
* Tests breadcrumb on a help topic page.
*/
public function verifyBreadCrumb() {
// Verify Help Topics administration breadcrumbs.
$trail = [
'' => 'Home',
'admin' => 'Administration',
'admin/help' => 'Help',
];
$this->assertBreadcrumb('admin/help/topic/help_topics_test.test', $trail);
// Ensure we are on the expected help topic page.
$this->assertSession()->pageTextContains('Also there should be a related topic link below to the Help module topic page and the linked topic.');
// Verify that another page does not have the help breadcrumb.
$trail = [
'' => 'Home',
'admin' => 'Administration',
'admin/config' => 'Configuration',
'admin/config/system' => 'System',
];
$this->assertBreadcrumb('admin/config/system/site-information', $trail);
}
}
@@ -0,0 +1,86 @@
<?php
namespace Drupal\Tests\help_topics\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Provides a base class for functional help topic tests that use translation.
*
* Installs in German, with a small PO file, and sets up the task, help, and
* page title blocks.
*/
abstract class HelpTopicTranslatedTestBase extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'help_topics_test',
'help',
'help_topics',
'block',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// These tests rely on some markup from the 'Seven' theme.
\Drupal::service('theme_installer')->install(['seven']);
\Drupal::configFactory()->getEditable('system.theme')
->set('admin', 'seven')
->save(TRUE);
// Place various blocks.
$settings = [
'theme' => 'seven',
'region' => 'help',
];
$this->placeBlock('help_block', $settings);
$this->placeBlock('local_tasks_block', $settings);
$this->placeBlock('local_actions_block', $settings);
$this->placeBlock('page_title_block', $settings);
// Create user.
$this->drupalLogin($this->createUser([
'access administration pages',
'view the administration theme',
'administer permissions',
]));
}
/**
* {@inheritdoc}
*/
protected function installParameters() {
$parameters = parent::installParameters();
// Install in German. This will ensure the language and locale modules are
// installed.
$parameters['parameters']['langcode'] = 'de';
// Create a po file so we don't attempt to download one from
// localize.drupal.org and to have a test translation that will not change.
\Drupal::service('file_system')->mkdir($this->publicFilesDirectory . '/translations', NULL, TRUE);
$contents = <<<ENDPO
msgid ""
msgstr ""
msgid "ABC Help Test module"
msgstr "ABC-Hilfetestmodul"
msgid "Test translation."
msgstr "Übersetzung testen."
msgid "Nonworditem totranslate."
msgstr "Nonwordgerman sdfwedrsdf."
ENDPO;
include_once $this->root . '/core/includes/install.core.inc';
$version = _install_get_version_info(\Drupal::VERSION)['major'] . '.0.0';
file_put_contents($this->publicFilesDirectory . "/translations/drupal-{$version}.de.po", $contents);
return $parameters;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\help_topics\Functional;
/**
* Verifies help topic translations.
*
* @group help_topics
*/
class HelpTopicTranslationTest extends HelpTopicTranslatedTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create user and log in.
$this->drupalLogin($this->createUser([
'access administration pages',
'view the administration theme',
'administer permissions',
]));
}
/**
* Tests help topic translations.
*/
public function testHelpTopicTranslations() {
$session = $this->assertSession();
// Verify that help topic link is translated on admin/help.
$this->drupalGet('admin/help');
$session->linkExists('ABC-Hilfetestmodul');
// Verify that the language cache tag appears on admin/help.
$session->responseHeaderContains('X-Drupal-Cache-Contexts', 'languages:language_interface');
// Verify that help topic is translated.
$this->drupalGet('admin/help/topic/help_topics_test.test');
$session->pageTextContains('ABC-Hilfetestmodul');
$session->pageTextContains('Übersetzung testen.');
// Verify that the language cache tag appears on a topic page.
$session->responseHeaderContains('X-Drupal-Cache-Contexts', 'languages:language_interface');
}
}
@@ -0,0 +1,262 @@
<?php
namespace Drupal\Tests\help_topics\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\help_topics\HelpTopicDiscovery;
use Drupal\Tests\DeprecatedModulesTestTrait;
use PHPUnit\Framework\ExpectationFailedException;
/**
* Verifies that all core Help topics can be rendered and comply with standards.
*
* @todo This test should eventually be folded into
* Drupal\Tests\system\Functional\Module\InstallUninstallTest
* when help_topics becomes stable, so that it will test with only one module
* at a time installed and not duplicate the effort of installing. See issue
* https://www.drupal.org/project/drupal/issues/3074040
*
* @group help_topics
*/
class HelpTopicsSyntaxTest extends BrowserTestBase {
use DeprecatedModulesTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'help',
'help_topics',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* Tests that all Core help topics can be rendered and have good syntax.
*/
public function testHelpTopics() {
$this->drupalLogin($this->rootUser);
// Enable all modules and themes, so that all routes mentioned in topics
// will be defined.
$module_directories = $this->listDirectories('module');
$modules_to_install = array_keys($module_directories);
$modules_to_install = $this->removeDeprecatedModules($modules_to_install);
\Drupal::service('module_installer')->install($modules_to_install);
$theme_directories = $this->listDirectories('theme');
\Drupal::service('theme_installer')->install(array_keys($theme_directories));
$directories = $module_directories + $theme_directories +
$this->listDirectories('profile');
$directories['core'] = \Drupal::service('app.root') . '/core/help_topics';
$directories['bad_help_topics'] = \Drupal::service('extension.list.module')->getPath('help_topics_test') . '/bad_help_topics/syntax/';
// Filter out directories outside of core. If you want to run this test
// on a contrib/custom module, remove the next line.
$directories = array_filter($directories, function ($directory) {
return strpos($directory, 'core') === 0;
});
// Verify that a few key modules, themes, and profiles are listed, so that
// we can be certain our directory list is complete and we will be testing
// all existing help topics. If these lines in the test fail in the future,
// it is probably because something we chose to list here is being removed.
// Substitute another item of the same type that still exists, so that this
// test can continue.
$this->assertArrayHasKey('system', $directories, 'System module is being scanned');
$this->assertArrayHasKey('help', $directories, 'Help module is being scanned');
$this->assertArrayHasKey('seven', $directories, 'Seven theme is being scanned');
$this->assertArrayHasKey('standard', $directories, 'Standard profile is being scanned');
$definitions = (new HelpTopicDiscovery($directories))->getDefinitions();
$this->assertGreaterThan(0, count($definitions), 'At least 1 topic was found');
// Test each topic for compliance with standards, or for failing in the
// right way.
foreach (array_keys($definitions) as $id) {
if (strpos($id, 'bad_help_topics.') === 0) {
$this->verifyBadTopic($id, $definitions);
}
else {
$this->verifyTopic($id, $definitions);
}
}
}
/**
* Verifies rendering and standards compliance of one help topic.
*
* @param string $id
* ID of the topic to verify.
* @param array $definitions
* Array of all topic definitions, keyed by ID.
* @param int $response
* Expected response from visiting the page for the topic.
*/
protected function verifyTopic($id, $definitions, $response = 200) {
$definition = $definitions[$id];
// Visit the URL for the topic.
$this->drupalGet('admin/help/topic/' . $id);
// Verify the title and response.
$session = $this->assertSession();
$session->statusCodeEquals($response);
if ($response == 200) {
$session->titleEquals($definition['label'] . ' | Drupal');
}
// Verify that all the related topics exist. Also check to see if any of
// them are top-level (we will need that in the next section).
$has_top_level_related = FALSE;
if (isset($definition['related'])) {
foreach ($definition['related'] as $related_id) {
$this->assertArrayHasKey($related_id, $definitions, 'Topic ' . $id . ' is only related to topics that exist (' . $related_id . ')');
$has_top_level_related = $has_top_level_related || !empty($definitions[$related_id]['top_level']);
}
}
// Verify this is either top-level or related to a top-level topic.
$this->assertTrue(!empty($definition['top_level']) || $has_top_level_related, 'Topic ' . $id . ' is either top-level or related to at least one other top-level topic');
// Verify that the label is not empty.
$this->assertNotEmpty($definition['label'], 'Topic ' . $id . ' has a non-empty label');
// Read in the file so we can run some tests on that.
$body = file_get_contents($definition[HelpTopicDiscovery::FILE_KEY]);
$this->assertNotEmpty($body, 'Topic ' . $id . ' has a non-empty Twig file');
// Remove the front matter data (already tested above), and Twig set and
// variable printouts from the file.
$body = preg_replace('|---.*---|sU', '', $body);
$body = preg_replace('|\{\{.*\}\}|sU', '', $body);
$body = preg_replace('|\{\% set.*\%\}|sU', '', $body);
$body = trim($body);
$this->assertNotEmpty($body, 'Topic ' . $id . ' Twig file contains some text outside of front matter');
// Verify that if we remove all the translated text, whitespace, and
// HTML tags, there is nothing left (that is, all text is translated).
$text = preg_replace('|\{\% trans \%\}.*\{\% endtrans \%\}|sU', '', $body);
$text = strip_tags($text);
$text = preg_replace('|\s+|', '', $text);
$this->assertEmpty($text, 'Topic ' . $id . ' Twig file has all of its text translated');
// Load the topic body as HTML and verify that it parses.
$doc = new \DOMDocument();
$doc->strictErrorChecking = TRUE;
$doc->validateOnParse = TRUE;
libxml_use_internal_errors(TRUE);
if (!$doc->loadHTML($body)) {
foreach (libxml_get_errors() as $error) {
$this->fail($error->message);
}
libxml_clear_errors();
}
// Check for headings hierarchy.
$levels = [1, 2, 3, 4, 5, 6];
foreach ($levels as $level) {
$num_headings[$level] = $doc->getElementsByTagName('h' . $level)->length;
if ($level == 1) {
$this->assertSame(0, $num_headings[1], 'Topic ' . $id . ' has no H1 tag');
// Set num_headings to 1 for this level, so the rest of the hierarchy
// can be tested using simpler code.
$num_headings[1] = 1;
}
else {
// We should either not have this heading, or if we do have one at this
// level, we should also have the next-smaller level. That is, if we
// have an h3, we should have also had an h2.
$this->assertTrue($num_headings[$level - 1] > 0 || $num_headings[$level] == 0,
'Topic ' . $id . ' has the correct H2-H6 heading hierarchy');
}
}
}
/**
* Verifies that a bad topic fails in the expected way.
*
* @param string $id
* ID of the topic to verify. It should start with "bad_help_topics.".
* @param array $definitions
* Array of all topic definitions, keyed by ID.
*/
protected function verifyBadTopic($id, $definitions) {
$bad_topic_type = substr($id, 16);
// Topics should fail verifyTopic() in specific ways.
try {
$this->verifyTopic($id, $definitions, 404);
}
catch (ExpectationFailedException $e) {
$message = $e->getMessage();
switch ($bad_topic_type) {
case 'related':
$this->assertStringContainsString('only related to topics that exist', $message);
break;
case 'bad_html':
$this->assertStringContainsString('Unexpected end tag', $message);
break;
case 'top_level':
$this->assertStringContainsString('is either top-level or related to at least one other top-level topic', $message);
break;
case 'empty':
$this->assertStringContainsString('contains some text outside of front matter', $message);
break;
case 'translated':
$this->assertStringContainsString('Twig file has all of its text translated', $message);
break;
case 'h1':
$this->assertStringContainsString('has no H1 tag', $message);
break;
case 'hierarchy':
$this->assertStringContainsString('has the correct H2-H6 heading hierarchy', $message);
break;
default:
// This was an unexpected error.
throw $e;
}
}
}
/**
* Lists the extension help topic directories of a certain type.
*
* @param string $type
* The type of extension to list: module, theme, or profile.
*
* @return string[]
* An array of all of the help topic directories for this type of
* extension, keyed by extension short name.
*/
protected function listDirectories($type) {
$directories = [];
// Find the extensions of this type, even if they are not installed, but
// excluding test ones.
$lister = \Drupal::service('extension.list.' . $type);
foreach (array_keys($lister->getAllAvailableInfo()) as $name) {
$path = $lister->getPath($name);
// You can tell test modules because they are in package 'Testing', but
// test themes are only known by being found in test directories. So...
// exclude things in test directories.
if ((strpos($path, '/tests') === FALSE) &&
(strpos($path, '/testing') === FALSE)) {
$directories[$name] = $path . '/help_topics';
}
}
return $directories;
}
}
@@ -0,0 +1,20 @@
<?php
namespace Drupal\Tests\help_topics\Functional;
/**
* Extends HelpTopicsSyntaxTest to test with deprecated modules.
*
* @see \Drupal\Tests\DeprecatedModulesTestTrait::removeDeprecatedModules()
*
* @group help_topics
* @group legacy
*/
class LegacyHelpTopicsSyntaxTest extends HelpTopicsSyntaxTest {
/**
* {@inheritdoc}
*/
protected $excludeDeprecated = FALSE;
}
@@ -0,0 +1,266 @@
<?php
namespace Drupal\Tests\help_topics\Unit;
use Drupal\Component\Discovery\DiscoveryException;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\help_topics\HelpTopicDiscovery;
use Drupal\help_topics\HelpTopicTwig;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* @coversDefaultClass \Drupal\help_topics\HelpTopicDiscovery
* @group help_topics
*/
class HelpTopicDiscoveryTest extends UnitTestCase {
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionProviderMismatch() {
vfsStream::setup('root');
vfsStream::create([
'modules' => [
'foo' => [
'help_topics' => [
// The content of the help topic does not matter.
'test.topic.html.twig' => '',
],
],
],
]);
$discovery = new HelpTopicDiscovery(['foo' => vfsStream::url('root/modules/foo/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/foo/help_topics/test.topic.html.twig file name should begin with 'foo'");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionMissingLabel() {
vfsStream::setup('root');
vfsStream::create([
'modules' => [
'test' => [
'help_topics' => [
// The content of the help topic does not matter.
'test.topic.html.twig' => '',
],
],
],
]);
$discovery = new HelpTopicDiscovery(['test' => vfsStream::url('root/modules/test/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig does not contain the required key with name='label'");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionInvalidYamlKey() {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: 'A label'
foo: bar
---
EOF;
vfsStream::create([
'modules' => [
'test' => [
'help_topics' => [
'test.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['test' => vfsStream::url('root/modules/test/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig contains invalid key='foo'");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionInvalidTopLevel() {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: 'A label'
top_level: bar
---
EOF;
vfsStream::create([
'modules' => [
'test' => [
'help_topics' => [
'test.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['test' => vfsStream::url('root/modules/test/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig contains invalid value for 'top_level' key, the value must be a Boolean");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testDiscoveryExceptionInvalidRelated() {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: 'A label'
related: "one, two"
---
EOF;
vfsStream::create([
'modules' => [
'test' => [
'help_topics' => [
'test.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['test' => vfsStream::url('root/modules/test/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("vfs://root/modules/test/help_topics/test.topic.html.twig contains invalid value for 'related' key, the value must be an array of strings");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testHelpTopicsExtensionProviderSpecialCase() {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: Test
---
<h2>Test</h2>
EOF;
vfsStream::create([
'modules' => [
'help_topics' => [
'help_topics' => [
'core.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['help_topics' => vfsStream::url('root/modules/help_topics/help_topics')]);
$this->assertArrayHasKey('core.topic', $discovery->getDefinitions());
}
/**
* @covers ::findAll
*/
public function testHelpTopicsInCore() {
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: Test
---
<h2>Test</h2>
EOF;
vfsStream::create([
'core' => [
'help_topics' => [
'core.topic.html.twig' => $topic_content,
],
],
]);
$discovery = new HelpTopicDiscovery(['core' => vfsStream::url('root/core/help_topics')]);
$this->assertArrayHasKey('core.topic', $discovery->getDefinitions());
}
/**
* @covers ::findAll
*/
public function testHelpTopicsBrokenYaml() {
vfsStream::setup('root');
$topic_content = <<<EOF
---
foo : [bar}
---
<h2>Test</h2>
EOF;
vfsStream::create([
'modules' => [
'help_topics' => [
'help_topics' => [
'core.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['help_topics' => vfsStream::url('root/modules/help_topics/help_topics')]);
$this->expectException(DiscoveryException::class);
$this->expectExceptionMessage("Malformed YAML in help topic \"vfs://root/modules/help_topics/help_topics/core.topic.html.twig\":");
$discovery->getDefinitions();
}
/**
* @covers ::findAll
*/
public function testHelpTopicsDefinition() {
$container = new ContainerBuilder();
$container->set('string_translation', $this->getStringTranslationStub());
\Drupal::setContainer($container);
vfsStream::setup('root');
$topic_content = <<<EOF
---
label: 'Test'
top_level: true
related:
- one
- two
- three
---
<h2>Test</h2>
EOF;
vfsStream::create([
'modules' => [
'foo' => [
'help_topics' => [
'foo.topic.html.twig' => $topic_content,
],
],
],
]);
$discovery = new HelpTopicDiscovery(['foo' => vfsStream::url('root/modules/foo/help_topics')]);
$definition = $discovery->getDefinitions()['foo.topic'];
$this->assertEquals('Test', $definition['label']);
$this->assertInstanceOf(TranslatableMarkup::class, $definition['label']);
$this->assertSame(TRUE, $definition['top_level']);
// Each related plugin ID should be trimmed.
$this->assertSame(['one', 'two', 'three'], $definition['related']);
$this->assertSame('foo', $definition['provider']);
$this->assertSame(HelpTopicTwig::class, $definition['class']);
$this->assertSame(vfsStream::url('root/modules/foo/help_topics/foo.topic.html.twig'), $definition['_discovered_file_path']);
$this->assertSame('foo.topic', $definition['id']);
}
}
@@ -0,0 +1,150 @@
<?php
namespace Drupal\Tests\help_topics\Unit;
use Drupal\help_topics\HelpTopicTwigLoader;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
use Twig\Error\LoaderError;
/**
* Unit test for the HelpTopicTwigLoader class.
*
* @coversDefaultClass \Drupal\help_topics\HelpTopicTwigLoader
* @group help_topics
*/
class HelpTopicTwigLoaderTest extends UnitTestCase {
/**
* The help topic loader instance to test.
*
* @var \Drupal\help_topics\HelpTopicTwigLoader
*/
protected $helpLoader;
/**
* The virtual directories to use in testing.
*
* @var array
*/
protected $directories;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->setUpVfs();
$this->helpLoader = new HelpTopicTwigLoader('\fake\root\path',
$this->getHandlerMock('module'),
$this->getHandlerMock('theme')
);
}
/**
* @covers ::__construct
*/
public function testConstructor() {
// Verify that the module/theme directories were added in the constructor,
// and non-existent directories were omitted.
$paths = $this->helpLoader->getPaths(HelpTopicTwigLoader::MAIN_NAMESPACE);
$this->assertCount(2, $paths);
$this->assertContains($this->directories['module']['test'] . '/help_topics', $paths);
$this->assertContains($this->directories['theme']['test'] . '/help_topics', $paths);
}
/**
* @covers ::getSourceContext
*/
public function testGetSourceContext() {
$source = $this->helpLoader->getSourceContext('@' . HelpTopicTwigLoader::MAIN_NAMESPACE . '/test.topic.html.twig');
$this->assertEquals('{% line 4 %}<h2>Test</h2>', $source->getCode());
}
/**
* @covers ::getSourceContext
*/
public function testGetSourceContextException() {
$this->expectException(LoaderError::class);
$this->expectExceptionMessage("Malformed YAML in help topic \"vfs://root/modules/test/help_topics/test.invalid_yaml.html.twig\":");
$source = $this->helpLoader->getSourceContext('@' . HelpTopicTwigLoader::MAIN_NAMESPACE . '/test.invalid_yaml.html.twig');
}
/**
* Creates a mock module or theme handler class for the test.
*
* @param string $type
* Type of handler to return: 'module' or 'theme'.
*
* @return \PHPUnit\Framework\MockObject\MockObject
* The mock of module or theme handler.
*/
protected function getHandlerMock($type) {
if ($type == 'module') {
$class = 'Drupal\Core\Extension\ModuleHandlerInterface';
$method = 'getModuleDirectories';
}
else {
$class = 'Drupal\Core\Extension\ThemeHandlerInterface';
$method = 'getThemeDirectories';
}
$handler = $this
->getMockBuilder($class)
->disableOriginalConstructor()
->getMock();
$handler
->method($method)
->willReturn($this->directories[$type]);
return $handler;
}
/**
* Sets up the virtual file system.
*/
protected function setUpVfs() {
$content = <<<EOF
---
label: Test
---
<h2>Test</h2>
EOF;
$invalid_content = <<<EOF
---
foo : [bar}
---
<h2>Test</h2>
EOF;
$help_topics_dir = [
'help_topics' => [
'test.topic.html.twig' => $content,
'test.invalid_yaml.html.twig' => $invalid_content,
],
];
vfsStream::setup('root');
vfsStream::create([
'modules' => [
'test' => $help_topics_dir,
],
'themes' => [
'test' => $help_topics_dir,
],
]);
$this->directories = [
'root' => vfsStream::url('root'),
'module' => [
'test' => vfsStream::url('root/modules/test'),
'not_a_dir' => vfsStream::url('root/modules/not_a_dir'),
],
'theme' => [
'test' => vfsStream::url('root/themes/test'),
'not_a_dir' => vfsStream::url('root/themes/not_a_dir'),
],
];
}
}
@@ -0,0 +1,141 @@
<?php
namespace Drupal\Tests\help_topics\Unit;
use Drupal\Core\Cache\Cache;
use Drupal\help_topics\HelpTopicTwig;
use Drupal\Tests\UnitTestCase;
/**
* Unit test for the HelpTopicTwig class.
*
* Note that the toUrl() and toLink() methods are not covered, because they
* have calls to new Url() and new Link() in them, so they cannot be unit
* tested.
*
* @coversDefaultClass \Drupal\help_topics\HelpTopicTwig
* @group help_topics
*/
class HelpTopicTwigTest extends UnitTestCase {
/**
* The help topic instance to test.
*
* @var \Drupal\help_topics\HelpTopicTwig
*/
protected $helpTopic;
/**
* The plugin information to use for setting up a test topic.
*
* @var array
*/
const PLUGIN_INFORMATION = [
'id' => 'test.topic',
'provider' => 'test',
'label' => 'This is the topic label',
'top_level' => TRUE,
'related' => ['something'],
'body' => '<p>This is the topic body</p>',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->helpTopic = new HelpTopicTwig([],
self::PLUGIN_INFORMATION['id'],
self::PLUGIN_INFORMATION,
$this->getTwigMock());
}
/**
* @covers ::getBody
* @covers ::getLabel
*/
public function testText() {
$this->assertEquals($this->helpTopic->getBody(),
['#markup' => self::PLUGIN_INFORMATION['body']]);
$this->assertEquals($this->helpTopic->getLabel(),
self::PLUGIN_INFORMATION['label']);
}
/**
* @covers ::getProvider
* @covers ::isTopLevel
* @covers ::getRelated
*/
public function testDefinition() {
$this->assertEquals($this->helpTopic->getProvider(),
self::PLUGIN_INFORMATION['provider']);
$this->assertEquals($this->helpTopic->isTopLevel(),
self::PLUGIN_INFORMATION['top_level']);
$this->assertEquals($this->helpTopic->getRelated(),
self::PLUGIN_INFORMATION['related']);
}
/**
* @covers ::getCacheContexts
* @covers ::getCacheTags
* @covers ::getCacheMaxAge
*/
public function testCacheInfo() {
$this->assertEquals($this->helpTopic->getCacheContexts(), []);
$this->assertEquals($this->helpTopic->getCacheTags(), ['core.extension']);
$this->assertEquals($this->helpTopic->getCacheMaxAge(), Cache::PERMANENT);
}
/**
* Creates a mock Twig loader class for the test.
*/
protected function getTwigMock() {
$twig = $this
->getMockBuilder('Drupal\Core\Template\TwigEnvironment')
->disableOriginalConstructor()
->getMock();
$twig
->method('load')
->willReturn(new FakeTemplateWrapper(self::PLUGIN_INFORMATION['body']));
return $twig;
}
}
/**
* Defines a fake template class to mock \Twig_TemplateWrapper.
*
* We cannot use getMockBuilder() for this, because the Twig TemplateWrapper
* class is declared "final" and cannot be mocked.
*/
class FakeTemplateWrapper {
/**
* Body text to return from the render() method.
*
* @var string
*/
protected $body;
/**
* Constructor.
*
* @param string $body
* Body text to return from the render() method.
*/
public function __construct($body) {
$this->body = $body;
}
/**
* Mocks the \Twig_TemplateWrapper render() method.
*
* @param array $context
* (optional) Render context.
*/
public function render(array $context = []) {
return $this->body;
}
}
@@ -0,0 +1,7 @@
---
label: 'XYZ Help Test theme'
top_level: true
related:
- help_topics_test.additional
---
<p>{% trans %}This is a theme provided topic.{% endtrans %}</p>
@@ -0,0 +1,6 @@
name: Test Help Topics
type: theme
base theme: stable
description: A theme to test help topics.
version: VERSION
core: 8.x