Initial Drupal 11 with DDEV setup
This commit is contained in:
69
web/core/modules/help/src/Annotation/HelpSection.php
Normal file
69
web/core/modules/help/src/Annotation/HelpSection.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
|
||||
/**
|
||||
* Defines a Plugin annotation object for help page section plugins.
|
||||
*
|
||||
* Plugin Namespace: Plugin\HelpSection
|
||||
*
|
||||
* For a working example, see \Drupal\help\Plugin\HelpSection\HookHelpSection.
|
||||
*
|
||||
* @see \Drupal\help\HelpSectionPluginInterface
|
||||
* @see \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase
|
||||
* @see \Drupal\help\HelpSectionManager
|
||||
* @see hook_help_section_info_alter()
|
||||
* @see plugin_api
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class HelpSection extends Plugin {
|
||||
|
||||
/**
|
||||
* The plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* The text to use as the title of the help page section.
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*/
|
||||
public $title;
|
||||
|
||||
/**
|
||||
* The description of the help page section.
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* The (optional) permission needed to view the help section.
|
||||
*
|
||||
* Only set if this section needs its own permission, beyond the generic
|
||||
* 'access help pages' permission needed to see the /admin/help
|
||||
* page itself.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $permission = '';
|
||||
|
||||
/**
|
||||
* An optional weight for the help section.
|
||||
*
|
||||
* The sections will be ordered by this weight on the help page.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $weight = 0;
|
||||
|
||||
}
|
||||
57
web/core/modules/help/src/Attribute/HelpSection.php
Normal file
57
web/core/modules/help/src/Attribute/HelpSection.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\help\Attribute;
|
||||
|
||||
use Drupal\Component\Plugin\Attribute\Plugin;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Defines a HelpSection attribute object for plugin discovery.
|
||||
*
|
||||
* Plugin Namespace: Plugin\HelpSection
|
||||
*
|
||||
* For a working example, see \Drupal\help\Plugin\HelpSection\HookHelpSection.
|
||||
*
|
||||
* @see \Drupal\help\HelpSectionPluginInterface
|
||||
* @see \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase
|
||||
* @see \Drupal\help\HelpSectionManager
|
||||
* @see hook_help_section_info_alter()
|
||||
* @see plugin_api
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class HelpSection extends Plugin {
|
||||
|
||||
/**
|
||||
* Constructs a HelpSection attribute.
|
||||
*
|
||||
* @param string $id
|
||||
* The plugin ID.
|
||||
* @param \Drupal\Core\StringTranslation\TranslatableMarkup $title
|
||||
* The text to use as the title of the help page section.
|
||||
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $description
|
||||
* (optional) The description of the help page section.
|
||||
* @param string|null $permission
|
||||
* (optional) The permission required to access the help page section.
|
||||
*
|
||||
* Only set if this section needs its own permission, beyond the generic
|
||||
* 'access help pages' permission needed to see the /admin/help
|
||||
* page itself.
|
||||
* @param int|null $weight
|
||||
* (optional) The weight of the help page section.
|
||||
* @param class-string|null $deriver
|
||||
* (optional) The deriver class.
|
||||
*
|
||||
* The sections will be ordered by this weight on the help page.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly TranslatableMarkup $title,
|
||||
public readonly ?TranslatableMarkup $description = NULL,
|
||||
public readonly ?string $permission = NULL,
|
||||
public readonly ?int $weight = NULL,
|
||||
public readonly ?string $deriver = NULL,
|
||||
) {}
|
||||
|
||||
}
|
||||
170
web/core/modules/help/src/Controller/HelpController.php
Normal file
170
web/core/modules/help/src/Controller/HelpController.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Controller;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Extension\ExtensionLifecycle;
|
||||
use Drupal\Core\Extension\ModuleExtensionList;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\help\HelpSectionManager;
|
||||
use Drupal\system\ModuleAdminLinksHelper;
|
||||
use Drupal\user\ModulePermissionsLinkHelper;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Controller routines for help routes.
|
||||
*/
|
||||
class HelpController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Creates a new HelpController.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
|
||||
* The current route match.
|
||||
* @param \Drupal\help\HelpSectionManager $helpManager
|
||||
* The help section manager.
|
||||
* @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
|
||||
* The module extension list.
|
||||
* @param \Drupal\system\ModuleAdminLinksHelper $moduleAdminLinks
|
||||
* The module admin links.
|
||||
* @param \Drupal\user\ModulePermissionsLinkHelper $modulePermissionsLinks
|
||||
* The module permissions link.
|
||||
*/
|
||||
public function __construct(
|
||||
protected RouteMatchInterface $routeMatch,
|
||||
protected HelpSectionManager $helpManager,
|
||||
protected ModuleExtensionList $moduleExtensionList,
|
||||
protected ModuleAdminLinksHelper $moduleAdminLinks,
|
||||
protected ModulePermissionsLinkHelper $modulePermissionsLinks,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('current_route_match'),
|
||||
$container->get('plugin.manager.help_section'),
|
||||
$container->get('extension.list.module'),
|
||||
$container->get('system.module_admin_links_helper'),
|
||||
$container->get('user.module_permissions_link_helper')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a page listing various types of help.
|
||||
*
|
||||
* The page has sections defined by \Drupal\help\HelpSectionPluginInterface
|
||||
* plugins.
|
||||
*
|
||||
* @return array
|
||||
* A render array for the help page.
|
||||
*/
|
||||
public function helpMain() {
|
||||
$output = [];
|
||||
|
||||
// We are checking permissions, so add the user.permissions cache context.
|
||||
$cacheability = new CacheableMetadata();
|
||||
$cacheability->addCacheContexts(['user.permissions']);
|
||||
|
||||
$plugins = $this->helpManager->getDefinitions();
|
||||
$cacheability->addCacheableDependency($this->helpManager);
|
||||
|
||||
foreach ($plugins as $plugin_id => $plugin_definition) {
|
||||
// Check the provided permission.
|
||||
if (!empty($plugin_definition['permission']) && !$this->currentUser()->hasPermission($plugin_definition['permission'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the section to the page.
|
||||
/** @var \Drupal\help\HelpSectionPluginInterface $plugin */
|
||||
$plugin = $this->helpManager->createInstance($plugin_id);
|
||||
$this_output = [
|
||||
'#theme' => 'help_section',
|
||||
'#title' => $plugin->getTitle(),
|
||||
'#description' => $plugin->getDescription(),
|
||||
'#empty' => $this->t('There is currently nothing in this section.'),
|
||||
'#links' => [],
|
||||
'#weight' => $plugin_definition['weight'],
|
||||
];
|
||||
|
||||
$links = $plugin->listTopics();
|
||||
if (is_array($links) && count($links)) {
|
||||
$this_output['#links'] = $links;
|
||||
}
|
||||
|
||||
$cacheability->addCacheableDependency($plugin);
|
||||
$output[$plugin_id] = $this_output;
|
||||
}
|
||||
|
||||
$cacheability->applyTo($output);
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a page listing general help for a module.
|
||||
*
|
||||
* @param string $name
|
||||
* A module name to display a help page for.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by
|
||||
* \Drupal\Core\Render\RendererInterface::render().
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function helpPage($name) {
|
||||
$build = [];
|
||||
if ($this->moduleHandler()->hasImplementations('help', $name)) {
|
||||
$module_name = $this->moduleExtensionList->getName($name);
|
||||
$build['#title'] = $module_name;
|
||||
|
||||
$info = $this->moduleExtensionList->getExtensionInfo($name);
|
||||
if ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
|
||||
$this->messenger()->addWarning($this->t('This module is experimental. <a href=":url">Experimental modules</a> are provided for testing purposes only. Use at your own risk.', [':url' => 'https://www.drupal.org/core/experimental']));
|
||||
}
|
||||
|
||||
$temp = $this->moduleHandler()->invoke($name, 'help', ["help.page.$name", $this->routeMatch]);
|
||||
if (empty($temp)) {
|
||||
$build['top'] = ['#markup' => $this->t('No help is available for module %module.', ['%module' => $module_name])];
|
||||
}
|
||||
else {
|
||||
if (!is_array($temp)) {
|
||||
$temp = ['#markup' => $temp];
|
||||
}
|
||||
$build['top'] = $temp;
|
||||
}
|
||||
|
||||
// Only print list of administration pages if the module in question has
|
||||
// any such pages associated with it.
|
||||
$admin_tasks = $this->moduleAdminLinks->getModuleAdminLinks($name);
|
||||
if ($module_permissions_link = $this->modulePermissionsLinks->getModulePermissionsLink($name, $info['name'])) {
|
||||
$admin_tasks["user.admin_permissions.{$name}"] = $module_permissions_link;
|
||||
}
|
||||
if (!empty($admin_tasks)) {
|
||||
$links = [];
|
||||
foreach ($admin_tasks as $task) {
|
||||
$link['url'] = $task['url'];
|
||||
$link['title'] = $task['title'];
|
||||
$links[] = $link;
|
||||
}
|
||||
$build['links'] = [
|
||||
'#theme' => 'links__help',
|
||||
'#heading' => [
|
||||
'level' => 'h3',
|
||||
'text' => $this->t('@module administration pages', ['@module' => $module_name]),
|
||||
],
|
||||
'#links' => $links,
|
||||
];
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
else {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Controller;
|
||||
|
||||
use Drupal\Component\Utility\SortArray;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\help\HelpTopicPluginManagerInterface;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Controller for help topic plugins.
|
||||
*
|
||||
* @internal
|
||||
* Controller classes are internal.
|
||||
*/
|
||||
class HelpTopicPluginController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Constructs a HelpTopicPluginController object.
|
||||
*
|
||||
* @param \Drupal\help\HelpTopicPluginManagerInterface $helpTopicPluginManager
|
||||
* The help topic plugin manager service.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer service.
|
||||
*/
|
||||
public function __construct(protected HelpTopicPluginManagerInterface $helpTopicPluginManager, protected RendererInterface $renderer) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a help topic page.
|
||||
*
|
||||
* @param string $id
|
||||
* The plugin ID. Maps to the {id} placeholder in the
|
||||
* help.help_topic route.
|
||||
*
|
||||
* @return array
|
||||
* A render array with the contents of a help topic page.
|
||||
*/
|
||||
public function viewHelpTopic($id) {
|
||||
$build = [];
|
||||
|
||||
if (!$this->helpTopicPluginManager->hasDefinition($id)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
/** @var \Drupal\help\HelpTopicPluginInterface $help_topic */
|
||||
$help_topic = $this->helpTopicPluginManager->createInstance($id);
|
||||
|
||||
$build['#body'] = $help_topic->getBody();
|
||||
|
||||
$this->renderer->addCacheableDependency($build, $help_topic);
|
||||
|
||||
// Build the related topics section, starting with the list this topic
|
||||
// says are related.
|
||||
$links = [];
|
||||
|
||||
$related = $help_topic->getRelated();
|
||||
foreach ($related as $other_id) {
|
||||
if ($other_id !== $id) {
|
||||
/** @var \Drupal\help\HelpTopicPluginInterface $topic */
|
||||
$topic = $this->helpTopicPluginManager->createInstance($other_id);
|
||||
$links[$other_id] = [
|
||||
'title' => $topic->getLabel(),
|
||||
'url' => Url::fromRoute('help.help_topic', ['id' => $other_id]),
|
||||
];
|
||||
$this->renderer->addCacheableDependency($build, $topic);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($links)) {
|
||||
uasort($links, [SortArray::class, 'sortByTitleElement']);
|
||||
$build['#related'] = [
|
||||
'#theme' => 'links__related',
|
||||
'#heading' => [
|
||||
'text' => $this->t('Related topics'),
|
||||
'level' => 'h2',
|
||||
],
|
||||
'#links' => $links,
|
||||
];
|
||||
}
|
||||
|
||||
$build['#theme'] = 'help_topic';
|
||||
$build['#title'] = $help_topic->getLabel();
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
||||
43
web/core/modules/help/src/HelpBreadcrumbBuilder.php
Normal file
43
web/core/modules/help/src/HelpBreadcrumbBuilder.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Core\Breadcrumb\Breadcrumb;
|
||||
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Provides a breadcrumb builder for help topic pages.
|
||||
*
|
||||
* @internal
|
||||
* Tagged services are internal.
|
||||
*/
|
||||
class HelpBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies(RouteMatchInterface $route_match, ?CacheableMetadata $cacheable_metadata = NULL) {
|
||||
// @todo Remove null safe operator in Drupal 12.0.0, see
|
||||
// https://www.drupal.org/project/drupal/issues/3459277.
|
||||
$cacheable_metadata?->addCacheContexts(['route']);
|
||||
return $route_match->getRouteName() == 'help.help_topic';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build(RouteMatchInterface $route_match) {
|
||||
$breadcrumb = new Breadcrumb();
|
||||
$breadcrumb->addCacheContexts(['url.path.parent']);
|
||||
$breadcrumb->addLink(Link::createFromRoute(new TranslatableMarkup('Home'), '<front>'));
|
||||
$breadcrumb->addLink(Link::createFromRoute(new TranslatableMarkup('Administration'), 'system.admin'));
|
||||
$breadcrumb->addLink(Link::createFromRoute(new TranslatableMarkup('Help'), 'help.main'));
|
||||
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
}
|
||||
72
web/core/modules/help/src/HelpSectionManager.php
Normal file
72
web/core/modules/help/src/HelpSectionManager.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
use Drupal\help\Attribute\HelpSection;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
|
||||
/**
|
||||
* Manages help page section plugins.
|
||||
*
|
||||
* @see \Drupal\help\HelpSectionPluginInterface
|
||||
* @see \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase
|
||||
* @see \Drupal\help\Annotation\HelpSection
|
||||
* @see hook_help_section_info_alter()
|
||||
*/
|
||||
class HelpSectionManager extends DefaultPluginManager {
|
||||
|
||||
/**
|
||||
* The search manager.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected ?PluginManagerInterface $searchManager = NULL;
|
||||
|
||||
/**
|
||||
* Constructs a new HelpSectionManager.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler for the alter hook.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/HelpSection', $namespaces, $module_handler, HelpSectionPluginInterface::class, HelpSection::class, 'Drupal\help\Annotation\HelpSection');
|
||||
|
||||
$this->alterInfo('help_section_info');
|
||||
$this->setCacheBackend($cache_backend, 'help_section_plugins');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the search manager.
|
||||
*
|
||||
* @param \Drupal\Component\Plugin\PluginManagerInterface|null $search_manager
|
||||
* The search manager if the Search module is installed.
|
||||
*/
|
||||
public function setSearchManager(?PluginManagerInterface $search_manager = NULL) {
|
||||
$this->searchManager = $search_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function clearCachedDefinitions() {
|
||||
parent::clearCachedDefinitions();
|
||||
// Search module may be missing. Help module might be installing,
|
||||
// so its search plugin may not be discovered yet.
|
||||
if ($this->searchManager && $this->searchManager->hasDefinition('help_search')) {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
web/core/modules/help/src/HelpSectionPluginInterface.php
Normal file
48
web/core/modules/help/src/HelpSectionPluginInterface.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for a plugin for a section of the /admin/help page.
|
||||
*
|
||||
* Plugins of this type need to be annotated with
|
||||
* \Drupal\help\Annotation\HelpSection annotation, and placed in the
|
||||
* Plugin\HelpSection namespace directory. They are managed by the
|
||||
* \Drupal\help\HelpSectionManager plugin manager class. There is a base
|
||||
* class that may be helpful:
|
||||
* \Drupal\help\Plugin\HelpSection\HelpSectionPluginBase.
|
||||
*/
|
||||
interface HelpSectionPluginInterface extends PluginInspectionInterface, CacheableDependencyInterface {
|
||||
|
||||
/**
|
||||
* Returns the title of the help section.
|
||||
*
|
||||
* @return string
|
||||
* The title text, which could be a plain string or an object that can be
|
||||
* cast to a string.
|
||||
*/
|
||||
public function getTitle();
|
||||
|
||||
/**
|
||||
* Returns the description text for the help section.
|
||||
*
|
||||
* @return string
|
||||
* The description text, which could be a plain string or an object that
|
||||
* can be cast to a string.
|
||||
*/
|
||||
public function getDescription();
|
||||
|
||||
/**
|
||||
* Returns a list of topics to show in the help section.
|
||||
*
|
||||
* @return array
|
||||
* A sorted list of topic links or render arrays for topic links. The links
|
||||
* will be shown in the help section; if the returned array of links is
|
||||
* empty, the section will be shown with some generic empty text.
|
||||
*/
|
||||
public function listTopics();
|
||||
|
||||
}
|
||||
178
web/core/modules/help/src/HelpTopicDiscovery.php
Normal file
178
web/core/modules/help/src/HelpTopicDiscovery.php
Normal file
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Component\Discovery\DiscoveryException;
|
||||
use Drupal\Component\FileCache\FileCacheFactory;
|
||||
use Drupal\Component\FileSystem\RegexDirectoryIterator;
|
||||
use Drupal\Component\FrontMatter\FrontMatter;
|
||||
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
|
||||
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
|
||||
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
|
||||
use Drupal\Core\Serialization\Yaml;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Discovers help topic plugins from Twig files in help_topics directories.
|
||||
*
|
||||
* @see \Drupal\help\HelpTopicTwig
|
||||
* @see \Drupal\help\HelpTopicTwigLoader
|
||||
*
|
||||
* @internal
|
||||
* Tagged services are internal.
|
||||
*/
|
||||
class HelpTopicDiscovery implements DiscoveryInterface {
|
||||
|
||||
use DiscoveryTrait;
|
||||
|
||||
/**
|
||||
* Defines the key in the discovered data where the file path is stored.
|
||||
*/
|
||||
const FILE_KEY = '_discovered_file_path';
|
||||
|
||||
/**
|
||||
* An array of directories to scan, keyed by the provider.
|
||||
*
|
||||
* The value can either be a string or an array of strings. The string values
|
||||
* should be the path of a directory to scan.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $directories = [];
|
||||
|
||||
/**
|
||||
* Constructs a HelpTopicDiscovery object.
|
||||
*
|
||||
* @param array $directories
|
||||
* An array of directories to scan, keyed by the provider. The value can
|
||||
* either be a string or an array of strings. The string values should be
|
||||
* the path of a directory to scan.
|
||||
*/
|
||||
public function __construct(array $directories) {
|
||||
$this->directories = $directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefinitions() {
|
||||
$plugins = $this->findAll();
|
||||
|
||||
// Flatten definitions into what's expected from plugins.
|
||||
$definitions = [];
|
||||
foreach ($plugins as $list) {
|
||||
foreach ($list as $id => $definition) {
|
||||
$definitions[$id] = $definition;
|
||||
}
|
||||
}
|
||||
|
||||
return $definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of discoverable items.
|
||||
*
|
||||
* @return array
|
||||
* An array of discovered data keyed by provider.
|
||||
*
|
||||
* @throws \Drupal\Component\Discovery\DiscoveryException
|
||||
* Exception thrown if there is a problem during discovery.
|
||||
*/
|
||||
public function findAll() {
|
||||
$all = [];
|
||||
|
||||
$files = $this->findFiles();
|
||||
|
||||
$file_cache = FileCacheFactory::get('help_topic_discovery:help_topics');
|
||||
|
||||
// Try to load from the file cache first.
|
||||
foreach ($file_cache->getMultiple(array_keys($files)) as $file => $data) {
|
||||
$all[$files[$file]][$data['id']] = $data;
|
||||
unset($files[$file]);
|
||||
}
|
||||
|
||||
// If there are files left that were not returned from the cache, load and
|
||||
// parse them now. This list was flipped above and is keyed by filename.
|
||||
if ($files) {
|
||||
foreach ($files as $file => $provider) {
|
||||
$plugin_id = substr(basename($file), 0, -10);
|
||||
// The plugin ID begins with provider.
|
||||
[$file_name_provider] = explode('.', $plugin_id, 2);
|
||||
$data = [
|
||||
// The plugin ID is derived from the filename. The extension
|
||||
// '.html.twig' is removed.
|
||||
'id' => $plugin_id,
|
||||
'provider' => $file_name_provider,
|
||||
'class' => HelpTopicTwig::class,
|
||||
static::FILE_KEY => $file,
|
||||
];
|
||||
|
||||
// Get the rest of the plugin definition from front matter contained in
|
||||
// the help topic Twig file.
|
||||
try {
|
||||
$front_matter = FrontMatter::create(file_get_contents($file), Yaml::class)->getData();
|
||||
}
|
||||
catch (InvalidDataTypeException $e) {
|
||||
throw new DiscoveryException(sprintf('Malformed YAML in help topic "%s": %s.', $file, $e->getMessage()));
|
||||
}
|
||||
foreach ($front_matter as $key => $value) {
|
||||
switch ($key) {
|
||||
case 'related':
|
||||
if (!is_array($value)) {
|
||||
throw new DiscoveryException("$file contains invalid value for 'related' key, the value must be an array of strings");
|
||||
}
|
||||
$data[$key] = $value;
|
||||
break;
|
||||
|
||||
case 'top_level':
|
||||
if (!is_bool($value)) {
|
||||
throw new DiscoveryException("$file contains invalid value for 'top_level' key, the value must be a Boolean");
|
||||
}
|
||||
$data[$key] = $value;
|
||||
break;
|
||||
|
||||
case 'label':
|
||||
// phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
||||
62
web/core/modules/help/src/HelpTopicPluginBase.php
Normal file
62
web/core/modules/help/src/HelpTopicPluginBase.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Base class for help topic plugins.
|
||||
*
|
||||
* @internal
|
||||
* Plugin classes are internal.
|
||||
*/
|
||||
abstract class HelpTopicPluginBase extends PluginBase implements HelpTopicPluginInterface {
|
||||
|
||||
/**
|
||||
* The name of the module or theme providing the help topic.
|
||||
*/
|
||||
public function getProvider() {
|
||||
return $this->pluginDefinition['provider'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLabel() {
|
||||
return $this->pluginDefinition['label'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isTopLevel() {
|
||||
return $this->pluginDefinition['top_level'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRelated() {
|
||||
return $this->pluginDefinition['related'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toUrl(array $options = []) {
|
||||
return Url::fromRoute('help.help_topic', ['id' => $this->getPluginId()], $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toLink($text = NULL, array $options = []) {
|
||||
if (!$text) {
|
||||
$text = $this->getLabel();
|
||||
}
|
||||
return Link::createFromRoute($text, 'help.help_topic', ['id' => $this->getPluginId()], $options);
|
||||
}
|
||||
|
||||
}
|
||||
78
web/core/modules/help/src/HelpTopicPluginInterface.php
Normal file
78
web/core/modules/help/src/HelpTopicPluginInterface.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Component\Plugin\DerivativeInspectionInterface;
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
|
||||
/**
|
||||
* Defines an interface for help topic plugin classes.
|
||||
*
|
||||
* @see \Drupal\help\HelpTopicPluginManager
|
||||
*/
|
||||
interface HelpTopicPluginInterface extends PluginInspectionInterface, DerivativeInspectionInterface, CacheableDependencyInterface {
|
||||
|
||||
/**
|
||||
* Returns the label of the topic.
|
||||
*
|
||||
* @return string
|
||||
* The label of the topic.
|
||||
*/
|
||||
public function getLabel();
|
||||
|
||||
/**
|
||||
* Returns the body of the topic.
|
||||
*
|
||||
* @return array
|
||||
* A render array representing the body.
|
||||
*/
|
||||
public function getBody();
|
||||
|
||||
/**
|
||||
* Returns whether this is a top-level topic or not.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this is a topic that should be displayed on the Help topics
|
||||
* list; FALSE if not.
|
||||
*/
|
||||
public function isTopLevel();
|
||||
|
||||
/**
|
||||
* Returns the IDs of related topics.
|
||||
*
|
||||
* @return string[]
|
||||
* Array of the IDs of related topics.
|
||||
*/
|
||||
public function getRelated();
|
||||
|
||||
/**
|
||||
* Returns the URL for viewing the help topic.
|
||||
*
|
||||
* @param array $options
|
||||
* (optional) See
|
||||
* \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for the
|
||||
* available options.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* A URL object containing the URL for viewing the help topic.
|
||||
*/
|
||||
public function toUrl(array $options = []);
|
||||
|
||||
/**
|
||||
* Returns a link for viewing the help topic.
|
||||
*
|
||||
* @param string|null $text
|
||||
* (optional) Link text to use for the link. If NULL, defaults to the
|
||||
* topic title.
|
||||
* @param array $options
|
||||
* (optional) See
|
||||
* \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for the
|
||||
* available options.
|
||||
*
|
||||
* @return \Drupal\Core\Link
|
||||
* A link object for viewing the topic.
|
||||
*/
|
||||
public function toLink($text = NULL, array $options = []);
|
||||
|
||||
}
|
||||
171
web/core/modules/help/src/HelpTopicPluginManager.php
Normal file
171
web/core/modules/help/src/HelpTopicPluginManager.php
Normal file
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Extension\ThemeHandlerInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
|
||||
|
||||
/**
|
||||
* Provides the default help_topic manager.
|
||||
*
|
||||
* Modules and themes can provide help topics in .html.twig files called
|
||||
* provider.name_of_topic.html.twig inside the module or theme sub-directory
|
||||
* help_topics. The provider is validated to be the extension that provides the
|
||||
* help topic.
|
||||
*
|
||||
* The Twig file must contain YAML front matter with a key named 'label'. It can
|
||||
* also contain keys named 'top_level' and 'related'. For example:
|
||||
* @code
|
||||
* ---
|
||||
* label: 'Configuring error responses, including 403/404 pages'
|
||||
*
|
||||
* # Related help topics in an array.
|
||||
* related:
|
||||
* - core.config_basic
|
||||
* - core.maintenance
|
||||
*
|
||||
* # If the value is true then the help topic will appear on admin/help.
|
||||
* top_level: true
|
||||
* ---
|
||||
* @endcode
|
||||
*
|
||||
* In addition, modules wishing to add plugins can define them in a
|
||||
* module_name.help_topics.yml file, with the plugin ID as the heading for
|
||||
* each entry, and these properties:
|
||||
* - id: The plugin ID.
|
||||
* - class: The name of your plugin class, implementing
|
||||
* \Drupal\help\HelpTopicPluginInterface.
|
||||
* - top_level: TRUE if the topic is top-level.
|
||||
* - related: Array of IDs of topics this one is related to.
|
||||
* - Additional properties that your plugin class needs, such as 'label'.
|
||||
*
|
||||
* You can also provide an entry that designates a plugin deriver class in your
|
||||
* help_topics.yml file, with a heading giving a prefix ID for your group of
|
||||
* derived plugins, and a 'deriver' property giving the name of a class
|
||||
* implementing \Drupal\Component\Plugin\Derivative\DeriverInterface. Example:
|
||||
* @code
|
||||
* my_module_prefix:
|
||||
* deriver: 'Drupal\my_module\Plugin\Deriver\HelpTopicDeriver'
|
||||
* @endcode
|
||||
*
|
||||
* @ingroup help_docs
|
||||
*
|
||||
* @see \Drupal\help\HelpTopicDiscovery
|
||||
* @see \Drupal\help\HelpTopicTwig
|
||||
* @see \Drupal\help\HelpTopicTwigLoader
|
||||
* @see \Drupal\help\HelpTopicPluginInterface
|
||||
* @see \Drupal\help\HelpTopicPluginBase
|
||||
* @see hook_help_topics_info_alter()
|
||||
* @see plugin_api
|
||||
* @see \Drupal\Component\Plugin\Derivative\DeriverInterface
|
||||
*/
|
||||
class HelpTopicPluginManager extends DefaultPluginManager implements HelpTopicPluginManagerInterface {
|
||||
|
||||
/**
|
||||
* Provides default values for all help topic plugins.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaults = [
|
||||
// The plugin ID.
|
||||
'id' => '',
|
||||
// The title of the help topic plugin.
|
||||
'label' => '',
|
||||
// Whether or not the topic should appear on the help topics list.
|
||||
'top_level' => '',
|
||||
// List of related topic machine names.
|
||||
'related' => [],
|
||||
// The class used to instantiate the plugin.
|
||||
'class' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructs a new HelpTopicManager object.
|
||||
*
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
* @param \Drupal\Core\Extension\ThemeHandlerInterface $themeHandler
|
||||
* The theme handler.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param string $root
|
||||
* The app root.
|
||||
*/
|
||||
public function __construct(ModuleHandlerInterface $module_handler, protected ThemeHandlerInterface $themeHandler, CacheBackendInterface $cache_backend, protected string $root) {
|
||||
// Note that the parent construct is not called because this class does not
|
||||
// use annotated class discovery.
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->alterInfo('help_topics_info');
|
||||
$this->setCacheBackend($cache_backend, 'help_topics');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getDiscovery() {
|
||||
if (!isset($this->discovery)) {
|
||||
$module_directories = $this->moduleHandler->getModuleDirectories();
|
||||
$all_directories = array_merge(
|
||||
['core' => $this->root . '/core'],
|
||||
$module_directories,
|
||||
$this->themeHandler->getThemeDirectories()
|
||||
);
|
||||
|
||||
// Search for Twig help topics in subdirectory help_topics, under
|
||||
// modules/profiles, themes, and the core directory.
|
||||
$all_directories = array_map(function ($dir) {
|
||||
return [$dir . '/help_topics'];
|
||||
}, $all_directories);
|
||||
$discovery = new HelpTopicDiscovery($all_directories);
|
||||
|
||||
// Also allow modules/profiles to extend help topic discovery to their
|
||||
// own plugins and derivers, in my_module.help_topics.yml files.
|
||||
$discovery = new YamlDiscoveryDecorator($discovery, 'help_topics', $module_directories);
|
||||
$discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
|
||||
$this->discovery = $discovery;
|
||||
}
|
||||
return $this->discovery;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function providerExists($provider) {
|
||||
return $this->moduleHandler->moduleExists($provider) || $this->themeHandler->themeExists($provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function findDefinitions() {
|
||||
$definitions = parent::findDefinitions();
|
||||
|
||||
// At this point the plugin list only contains valid plugins. Ensure all
|
||||
// related plugins exist and the relationship is bi-directional. This
|
||||
// ensures topics are listed on their related topics.
|
||||
foreach ($definitions as $plugin_id => $plugin_definition) {
|
||||
foreach ($plugin_definition['related'] as $key => $related_id) {
|
||||
// If the related help topic does not exist it might be for a module
|
||||
// that is not installed. Remove it.
|
||||
// @todo Discuss this more as this could cause silent errors but it
|
||||
// offers useful functionality to relate to a help topic provided by
|
||||
// extensions that are yet to be installed.
|
||||
// https://www.drupal.org/i/3360133
|
||||
if (!isset($definitions[$related_id])) {
|
||||
unset($definitions[$plugin_id]['related'][$key]);
|
||||
continue;
|
||||
}
|
||||
// Make the related relationship bi-directional.
|
||||
if (isset($definitions[$related_id]) && !in_array($plugin_id, $definitions[$related_id]['related'], TRUE)) {
|
||||
$definitions[$related_id]['related'][] = $plugin_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $definitions;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
|
||||
/**
|
||||
* Defines an interface for managing help topics and storing their definitions.
|
||||
*/
|
||||
interface HelpTopicPluginManagerInterface extends PluginManagerInterface {
|
||||
}
|
||||
80
web/core/modules/help/src/HelpTopicTwig.php
Normal file
80
web/core/modules/help/src/HelpTopicTwig.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Template\TwigEnvironment;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Represents a help topic plugin whose definition comes from a Twig file.
|
||||
*
|
||||
* @see \Drupal\help\HelpTopicDiscovery
|
||||
* @see \Drupal\help\HelpTopicTwigLoader
|
||||
* @see \Drupal\help\HelpTopicPluginManager
|
||||
*
|
||||
* @internal
|
||||
* Plugin classes are internal.
|
||||
*/
|
||||
class HelpTopicTwig extends HelpTopicPluginBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* HelpTopicPluginBase constructor.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Template\TwigEnvironment $twig
|
||||
* The Twig environment.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, protected TwigEnvironment $twig) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('twig')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBody() {
|
||||
return [
|
||||
'#markup' => $this->twig->load('@help_topics/' . $this->getPluginId() . '.html.twig')->render(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheTags() {
|
||||
return ['core.extension'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheMaxAge() {
|
||||
return Cache::PERMANENT;
|
||||
}
|
||||
|
||||
}
|
||||
110
web/core/modules/help/src/HelpTopicTwigLoader.php
Normal file
110
web/core/modules/help/src/HelpTopicTwigLoader.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Component\FrontMatter\FrontMatter;
|
||||
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Extension\ThemeHandlerInterface;
|
||||
use Drupal\Core\Serialization\Yaml;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\Source;
|
||||
|
||||
/**
|
||||
* Loads help topic Twig files from the filesystem.
|
||||
*
|
||||
* This loader adds module and theme help topic paths to a help_topics namespace
|
||||
* to the Twig filesystem loader so that help_topics can be referenced, using
|
||||
* '@help-topic/pluginId.html.twig'.
|
||||
*
|
||||
* @see \Drupal\help\HelpTopicDiscovery
|
||||
* @see \Drupal\help\HelpTopicTwig
|
||||
*
|
||||
* @internal
|
||||
* Tagged services are internal.
|
||||
*/
|
||||
class HelpTopicTwigLoader extends FilesystemLoader {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
const MAIN_NAMESPACE = 'help_topics';
|
||||
|
||||
/**
|
||||
* Constructs a new HelpTopicTwigLoader object.
|
||||
*
|
||||
* @param string $root_path
|
||||
* The root path.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler service.
|
||||
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
|
||||
* The theme handler service.
|
||||
*/
|
||||
public function __construct($root_path, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
|
||||
parent::__construct([], $root_path);
|
||||
// Add help_topics directories for modules and themes in the 'help_topic'
|
||||
// namespace, plus core.
|
||||
$this->addExtension($root_path . '/core');
|
||||
array_map([$this, 'addExtension'], $module_handler->getModuleDirectories());
|
||||
array_map([$this, 'addExtension'], $theme_handler->getThemeDirectories());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an extensions help_topics directory to the Twig loader.
|
||||
*
|
||||
* @param string $path
|
||||
* The path to the extension.
|
||||
*/
|
||||
protected function addExtension($path) {
|
||||
$path .= DIRECTORY_SEPARATOR . 'help_topics';
|
||||
if (is_dir($path)) {
|
||||
$this->cache = $this->errorCache = [];
|
||||
$this->paths[self::MAIN_NAMESPACE][] = rtrim($path, '/\\');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSourceContext(string $name): Source {
|
||||
$path = $this->findTemplate($name);
|
||||
|
||||
$contents = file_get_contents($path);
|
||||
try {
|
||||
// Note: always use \Drupal\Core\Serialization\Yaml here instead of the
|
||||
// "serializer.yaml" service. This allows the core serializer to utilize
|
||||
// core related functionality which isn't available as the standalone
|
||||
// component based serializer.
|
||||
$front_matter = new FrontMatter($contents, Yaml::class);
|
||||
|
||||
// Reconstruct the content if there is front matter data detected. Prepend
|
||||
// the source with {% line \d+ %} to inform Twig that the source code
|
||||
// actually starts on a different line past the front matter data. This is
|
||||
// particularly useful when used in error reporting.
|
||||
if ($front_matter->getData() && ($line = $front_matter->getLine())) {
|
||||
$contents = "{% line $line %}" . $front_matter->getContent();
|
||||
}
|
||||
}
|
||||
catch (InvalidDataTypeException $e) {
|
||||
throw new LoaderError(sprintf('Malformed YAML in help topic "%s": %s.', $path, $e->getMessage()));
|
||||
}
|
||||
|
||||
return new Source($contents, $name, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function findTemplate($name, $throw = TRUE): ?string {
|
||||
if (!str_ends_with($name, '.html.twig')) {
|
||||
if (!$throw) {
|
||||
return NULL;
|
||||
}
|
||||
$extension = pathinfo($name, PATHINFO_EXTENSION);
|
||||
throw new LoaderError(sprintf("Help topic %s has an invalid file extension (%s). Only help topics ending .html.twig are allowed.", $name, $extension));
|
||||
}
|
||||
return parent::findTemplate($name, $throw);
|
||||
}
|
||||
|
||||
}
|
||||
153
web/core/modules/help/src/HelpTwigExtension.php
Normal file
153
web/core/modules/help/src/HelpTwigExtension.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
|
||||
use Drupal\Core\Access\AccessManagerInterface;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Symfony\Component\Routing\Exception\InvalidParameterException;
|
||||
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
|
||||
use Symfony\Component\Routing\Exception\RouteNotFoundException;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* Defines and registers Drupal Twig extensions for rendering help topics.
|
||||
*
|
||||
* @internal
|
||||
* Tagged services are internal.
|
||||
*/
|
||||
class HelpTwigExtension extends AbstractExtension {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Constructs a \Drupal\help\HelpTwigExtension.
|
||||
*
|
||||
* @param \Drupal\Core\Access\AccessManagerInterface $accessManager
|
||||
* The access manager.
|
||||
* @param \Drupal\help\HelpTopicPluginManagerInterface $pluginManager
|
||||
* The help topic plugin manager service.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The string translation service.
|
||||
*/
|
||||
public function __construct(protected AccessManagerInterface $accessManager, protected HelpTopicPluginManagerInterface $pluginManager, TranslationInterface $string_translation) {
|
||||
$this->stringTranslation = $string_translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions(): array {
|
||||
return [
|
||||
new TwigFunction('help_route_link', [$this, 'getRouteLink']),
|
||||
new TwigFunction('help_topic_link', [$this, 'getTopicLink']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link or plain text, given text, route name, and parameters.
|
||||
*
|
||||
* @param string $text
|
||||
* The link text.
|
||||
* @param string $route
|
||||
* The name of the route.
|
||||
* @param array $parameters
|
||||
* (optional) An associative array of route parameter names and values.
|
||||
* @param array $options
|
||||
* (optional) An associative array of additional options. The 'absolute'
|
||||
* option is forced to be TRUE.
|
||||
*
|
||||
* @return array
|
||||
* A render array with a generated absolute link to the given route. If
|
||||
* the user does not have permission for the route, or an exception occurs,
|
||||
* such as a missing route or missing parameters, the render array is for
|
||||
* the link text as a plain string instead.
|
||||
*
|
||||
* @see \Drupal\Core\Template\TwigExtension::getUrl()
|
||||
*/
|
||||
public function getRouteLink(string $text, string $route, array $parameters = [], array $options = []): array {
|
||||
assert($this->accessManager instanceof AccessManagerInterface, "The access manager hasn't been set up. Any configuration YAML file with a service directive dealing with the Twig configuration can cause this, most likely found in a recently installed or changed module.");
|
||||
|
||||
$bubbles = new BubbleableMetadata();
|
||||
$bubbles->addCacheTags(['route_match']);
|
||||
|
||||
try {
|
||||
$access_object = $this->accessManager->checkNamedRoute($route, $parameters, NULL, TRUE);
|
||||
$bubbles->addCacheableDependency($access_object);
|
||||
|
||||
if ($access_object->isAllowed()) {
|
||||
$options['absolute'] = TRUE;
|
||||
$url = Url::fromRoute($route, $parameters, $options);
|
||||
// Generate the URL to check for parameter problems and collect
|
||||
// cache metadata.
|
||||
$generated = $url->toString(TRUE);
|
||||
$bubbles->addCacheableDependency($generated);
|
||||
$build = [
|
||||
'#title' => $text,
|
||||
'#type' => 'link',
|
||||
'#url' => $url,
|
||||
];
|
||||
}
|
||||
else {
|
||||
// If the user doesn't have access, return the link text.
|
||||
$build = ['#markup' => $text];
|
||||
}
|
||||
}
|
||||
catch (RouteNotFoundException | MissingMandatoryParametersException | InvalidParameterException) {
|
||||
// If the route had one of these exceptions, return the link text.
|
||||
$build = ['#markup' => $text];
|
||||
}
|
||||
$bubbles->applyTo($build);
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to a help topic, or the title of the topic.
|
||||
*
|
||||
* @param string $topic_id
|
||||
* The help topic ID.
|
||||
*
|
||||
* @return array
|
||||
* A render array with a generated absolute link to the given topic. If
|
||||
* the user does not have permission to view the topic, or an exception
|
||||
* occurs, such as the topic not being defined due to a module not being
|
||||
* installed, a default string is returned.
|
||||
*
|
||||
* @see \Drupal\Core\Template\TwigExtension::getUrl()
|
||||
*/
|
||||
public function getTopicLink(string $topic_id): array {
|
||||
assert($this->pluginManager instanceof HelpTopicPluginManagerInterface, "The plugin manager hasn't been set up. Any configuration YAML file with a service directive dealing with the Twig configuration can cause this, most likely found in a recently installed or changed module.");
|
||||
|
||||
$bubbles = new BubbleableMetadata();
|
||||
$bubbles->addCacheableDependency($this->pluginManager);
|
||||
try {
|
||||
$plugin = $this->pluginManager->createInstance($topic_id);
|
||||
}
|
||||
catch (PluginNotFoundException) {
|
||||
// Not a topic.
|
||||
$plugin = FALSE;
|
||||
}
|
||||
|
||||
if ($plugin) {
|
||||
$parameters = ['id' => $topic_id];
|
||||
$route = 'help.help_topic';
|
||||
$build = $this->getRouteLink($plugin->getLabel(), $route, $parameters);
|
||||
$bubbles->addCacheableDependency($plugin);
|
||||
}
|
||||
else {
|
||||
$build = [
|
||||
'#markup' => $this->t('Missing help topic %topic', [
|
||||
'%topic' => $topic_id,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
$bubbles->applyTo($build);
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
||||
160
web/core/modules/help/src/Hook/HelpHooks.php
Normal file
160
web/core/modules/help/src/Hook/HelpHooks.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Hook;
|
||||
|
||||
use Drupal\Core\Block\BlockPluginInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for help.
|
||||
*/
|
||||
class HelpHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
#[Hook('help')]
|
||||
public function help($route_name, RouteMatchInterface $route_match): string|array|null {
|
||||
switch ($route_name) {
|
||||
case 'help.main':
|
||||
$output = '<h2>' . $this->t('Getting Started') . '</h2>';
|
||||
$output .= '<p>' . $this->t('Follow these steps to set up and start using your website:') . '</p>';
|
||||
$output .= '<ol>';
|
||||
$output .= '<li>' . $this->t('<strong>Configure your website</strong> Once logged in, visit the <a href=":admin">Administration page</a>, where you may <a href=":config">customize and configure</a> all aspects of your website.', [
|
||||
':admin' => Url::fromRoute('system.admin')->toString(),
|
||||
':config' => Url::fromRoute('system.admin_config')->toString(),
|
||||
]) . '</li>';
|
||||
$output .= '<li>' . $this->t('<strong>Enable additional functionality</strong> Next, visit the <a href=":modules">Extend page</a> and install modules that suit your specific needs. You can find additional modules at the <a href=":download_modules">Drupal.org modules page</a>.', [
|
||||
':modules' => Url::fromRoute('system.modules_list')->toString(),
|
||||
':download_modules' => 'https://www.drupal.org/project/modules',
|
||||
]) . '</li>';
|
||||
$output .= '<li>' . $this->t('<strong>Customize your website design</strong> To change the "look and feel" of your website, visit the <a href=":themes">Appearance page</a>. You may choose from one of the included themes or download additional themes from the <a href=":download_themes">Drupal.org themes page</a>.', [
|
||||
':themes' => Url::fromRoute('system.themes_page')->toString(),
|
||||
':download_themes' => 'https://www.drupal.org/project/themes',
|
||||
]) . '</li>';
|
||||
// Display a link to the create content page if Node module is
|
||||
// installed.
|
||||
if (\Drupal::moduleHandler()->moduleExists('node')) {
|
||||
$output .= '<li>' . $this->t('<strong>Start posting content</strong> Finally, you may <a href=":content">add new content</a> to your website.', [':content' => Url::fromRoute('node.add_page')->toString()]) . '</li>';
|
||||
}
|
||||
$output .= '</ol>';
|
||||
$output .= '<p>' . $this->t('For more information, refer to the help listed on this page or to the <a href=":docs">online documentation</a> and <a href=":support">support</a> pages at <a href=":drupal">drupal.org</a>.', [
|
||||
':docs' => 'https://www.drupal.org/documentation',
|
||||
':support' => 'https://www.drupal.org/support',
|
||||
':drupal' => 'https://www.drupal.org',
|
||||
]) . '</p>';
|
||||
return ['#markup' => $output];
|
||||
|
||||
case 'help.page.help':
|
||||
$help_home = Url::fromRoute('help.main')->toString();
|
||||
$module_handler = \Drupal::moduleHandler();
|
||||
$locale_help = $module_handler->moduleExists('locale') ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#';
|
||||
$search_help = $module_handler->moduleExists('search') ? Url::fromRoute('help.page', ['name' => 'search'])->toString() : '#';
|
||||
$output = '<h2>' . $this->t('About') . '</h2>';
|
||||
$output .= '<p>' . $this->t('The Help module generates <a href=":help-page">Help topics and reference pages</a> to guide you through the use and configuration of modules, and provides a Help block with page-level help. The reference pages are a starting point for <a href=":handbook">Drupal.org online documentation</a> pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the <a href=":help">online documentation for the Help module</a>.', [
|
||||
':help' => 'https://www.drupal.org/documentation/modules/help/',
|
||||
':handbook' => 'https://www.drupal.org/documentation',
|
||||
':help-page' => Url::fromRoute('help.main')->toString(),
|
||||
]) . '</p>';
|
||||
$output .= '<p>' . $this->t('Help topics provided by modules and themes are also part of the Help module. If the core Search module is installed, these topics are searchable. For more information, see the <a href=":online">online documentation, Help Topic Standards</a>.', [
|
||||
':online' => 'https://www.drupal.org/docs/develop/managing-a-drupalorg-theme-module-or-distribution-project/documenting-your-project/help-topic-standards',
|
||||
]) . '</p>';
|
||||
$output .= '<h2>' . $this->t('Uses') . '</h2>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . $this->t('Providing a help reference') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('The Help module displays explanations for using each module listed on the main <a href=":help">Help reference page</a>.', [':help' => Url::fromRoute('help.main')->toString()]) . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Providing page-specific help') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('Page-specific help text provided by modules is displayed in the Help block. This block can be placed and configured on the <a href=":blocks">Block layout page</a>.', [
|
||||
':blocks' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#',
|
||||
]) . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Viewing help topics') . '</dt>';
|
||||
$output .= '<dd>' . $this->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>' . $this->t('Providing help topics') . '</dt>';
|
||||
$output .= '<dd>' . $this->t("Modules and themes can provide help topics as Twig-file-based plugins in a project sub-directory called <em>help_topics</em>; plugin meta-data is provided in YAML front matter within each Twig file. Plugin-based help topics provided by modules and themes will automatically be updated when a module or theme is updated. Use the plugins in <em>core/modules/help/help_topics</em> as a guide when writing and formatting a help topic plugin for your theme or module.") . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Translating help topics') . '</dt>';
|
||||
$output .= '<dd>' . $this->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>' . $this->t('Configuring help search') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('To search help, you will need to install the core Search module, configure a search page, and add a search block to the Help page or another administrative page. (A search page is provided automatically, and if you use the core Claro administrative theme, a help search block is shown on the main Help page.) Then users with search permissions, and permission to view help, will be able to search help. See the <a href=":search_help">Search module help page</a> for more information.', [':search_help' => $search_help]) . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return ['#markup' => $output];
|
||||
|
||||
case 'help.help_topic':
|
||||
$help_home = Url::fromRoute('help.main')->toString();
|
||||
return '<p>' . $this->t('See the <a href=":help_page">Help page</a> for more topics.', [':help_page' => $help_home]) . '</p>';
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
#[Hook('theme')]
|
||||
public function theme($existing, $type, $theme, $path) : array {
|
||||
return [
|
||||
'help_section' => [
|
||||
'variables' => [
|
||||
'title' => NULL,
|
||||
'description' => NULL,
|
||||
'links' => NULL,
|
||||
'empty' => NULL,
|
||||
],
|
||||
],
|
||||
'help_topic' => [
|
||||
'variables' => [
|
||||
'body' => [],
|
||||
'related' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_view_BASE_BLOCK_ID_alter().
|
||||
*/
|
||||
#[Hook('block_view_help_block_alter')]
|
||||
public function blockViewHelpBlockAlter(array &$build, BlockPluginInterface $block): void {
|
||||
// Assume that most users do not need or want to perform contextual actions
|
||||
// on the help block, so don't needlessly draw attention to it.
|
||||
unset($build['#contextual_links']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_modules_uninstalled().
|
||||
*/
|
||||
#[Hook('modules_uninstalled')]
|
||||
public function modulesUninstalled(array $modules): void {
|
||||
_help_search_update($modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_themes_uninstalled().
|
||||
*/
|
||||
#[Hook('themes_uninstalled')]
|
||||
public function themesUninstalled(array $themes): void {
|
||||
\Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
|
||||
_help_search_update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_modules_installed().
|
||||
*/
|
||||
#[Hook('modules_installed')]
|
||||
public function modulesInstalled(array $modules, $is_syncing): void {
|
||||
_help_search_update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_themes_installed().
|
||||
*/
|
||||
#[Hook('themes_installed')]
|
||||
public function themesInstalled(array $themes): void {
|
||||
\Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
|
||||
_help_search_update();
|
||||
}
|
||||
|
||||
}
|
||||
112
web/core/modules/help/src/Plugin/Block/HelpBlock.php
Normal file
112
web/core/modules/help/src/Plugin/Block/HelpBlock.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Plugin\Block;
|
||||
|
||||
use Drupal\Core\Block\Attribute\Block;
|
||||
use Drupal\Core\Block\BlockBase;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Provides a 'Help' block.
|
||||
*/
|
||||
#[Block(
|
||||
id: "help_block",
|
||||
admin_label: new TranslatableMarkup("Help"),
|
||||
forms: ['settings_tray' => FALSE]
|
||||
)]
|
||||
class HelpBlock extends BlockBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* The current request.
|
||||
*
|
||||
* @var \Symfony\Component\HttpFoundation\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* The current route match.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteMatchInterface
|
||||
*/
|
||||
protected $routeMatch;
|
||||
|
||||
/**
|
||||
* Creates a HelpBlock instance.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The current request.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The current route match.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, ModuleHandlerInterface $module_handler, RouteMatchInterface $route_match) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
$this->request = $request;
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->routeMatch = $route_match;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('request_stack')->getCurrentRequest(),
|
||||
$container->get('module_handler'),
|
||||
$container->get('current_route_match')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build() {
|
||||
// Do not show on a 403 or 404 page.
|
||||
if ($this->request->attributes->has('exception')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$build = [];
|
||||
$this->moduleHandler->invokeAllWith('help', function (callable $hook, string $module) use (&$build) {
|
||||
// Don't add empty strings to $build array.
|
||||
if ($help = $hook($this->routeMatch->getRouteName(), $this->routeMatch)) {
|
||||
// Convert strings to #markup render arrays so that they will XSS admin
|
||||
// filtered.
|
||||
$build[] = is_array($help) ? $help : ['#markup' => $help];
|
||||
}
|
||||
});
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Plugin\HelpSection;
|
||||
|
||||
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\help\HelpSectionPluginInterface;
|
||||
|
||||
/**
|
||||
* Provides a base class for help section plugins.
|
||||
*
|
||||
* @see \Drupal\help\HelpSectionPluginInterface
|
||||
* @see \Drupal\help\Annotation\HelpSection
|
||||
* @see \Drupal\help\HelpSectionManager
|
||||
*/
|
||||
abstract class HelpSectionPluginBase extends PluginBase implements HelpSectionPluginInterface {
|
||||
|
||||
use UnchangingCacheableDependencyTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTitle() {
|
||||
return $this->getPluginDefinition()['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDescription() {
|
||||
return $this->getPluginDefinition()['description'];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Plugin\HelpSection;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\help\Attribute\HelpSection;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\help\SearchableHelpInterface;
|
||||
use Drupal\help\HelpTopicPluginInterface;
|
||||
use Drupal\help\HelpTopicPluginManagerInterface;
|
||||
use Drupal\Core\Language\LanguageDefault;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\StringTranslation\TranslationManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides the help topics list section for the help page.
|
||||
*
|
||||
* @internal
|
||||
* Plugin classes are internal.
|
||||
*/
|
||||
#[HelpSection(
|
||||
id: 'help_topics',
|
||||
title: new TranslatableMarkup('Topics'),
|
||||
description: new TranslatableMarkup('Topics can be provided by modules or themes. Top-level help topics on your site:'),
|
||||
weight: -10
|
||||
)]
|
||||
class HelpTopicSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface, SearchableHelpInterface {
|
||||
|
||||
/**
|
||||
* The top level help topic plugins.
|
||||
*
|
||||
* @var \Drupal\help\HelpTopicPluginInterface[]
|
||||
*/
|
||||
protected $topLevelPlugins;
|
||||
|
||||
/**
|
||||
* The merged top level help topic plugins cache metadata.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheableMetadata
|
||||
*/
|
||||
protected $cacheableMetadata;
|
||||
|
||||
/**
|
||||
* Constructs a HelpTopicSection object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\help\HelpTopicPluginManagerInterface $pluginManager
|
||||
* The help topic plugin manager service.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer.
|
||||
* @param \Drupal\Core\Language\LanguageDefault $defaultLanguage
|
||||
* The default language object.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
|
||||
* The language manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationManager $translationManager
|
||||
* The translation manager. We are using a method that doesn't exist on an
|
||||
* interface, so require this class.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, protected HelpTopicPluginManagerInterface $pluginManager, protected RendererInterface $renderer, protected LanguageDefault $defaultLanguage, protected LanguageManagerInterface $languageManager, protected TranslationManager $translationManager) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('plugin.manager.help_topic'),
|
||||
$container->get('renderer'),
|
||||
$container->get('language.default'),
|
||||
$container->get('language_manager'),
|
||||
$container->get('string_translation')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheTags() {
|
||||
return $this->getCacheMetadata()->getCacheTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
return $this->getCacheMetadata()->getCacheContexts();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheMaxAge() {
|
||||
return $this->getCacheMetadata()->getCacheMaxAge();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function listTopics() {
|
||||
// Map the top level help topic plugins to a list of topic links.
|
||||
return array_map(function (HelpTopicPluginInterface $topic) {
|
||||
return $topic->toLink();
|
||||
}, $this->getPlugins());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the top level help topic plugins.
|
||||
*
|
||||
* @return \Drupal\help\HelpTopicPluginInterface[]
|
||||
* The top level help topic plugins.
|
||||
*/
|
||||
protected function getPlugins() {
|
||||
if (!isset($this->topLevelPlugins)) {
|
||||
$definitions = $this->pluginManager->getDefinitions();
|
||||
|
||||
$this->topLevelPlugins = [];
|
||||
// Get all the top level topics and merge their list cache tags.
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition['top_level']) {
|
||||
$this->topLevelPlugins[$definition['id']] = $this->pluginManager->createInstance($definition['id']);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the top level topics by label and, if the labels match, then by
|
||||
// plugin ID.
|
||||
usort($this->topLevelPlugins, function (HelpTopicPluginInterface $a, HelpTopicPluginInterface $b) {
|
||||
$a_label = (string) $a->getLabel();
|
||||
$b_label = (string) $b->getLabel();
|
||||
if ($a_label === $b_label) {
|
||||
return $a->getPluginId() <=> $b->getPluginId();
|
||||
}
|
||||
return strnatcasecmp($a_label, $b_label);
|
||||
});
|
||||
}
|
||||
return $this->topLevelPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function listSearchableTopics() {
|
||||
$definitions = $this->pluginManager->getDefinitions();
|
||||
return array_column($definitions, 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function renderTopicForSearch($topic_id, LanguageInterface $language) {
|
||||
$plugin = $this->pluginManager->createInstance($topic_id);
|
||||
if (!$plugin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// We are rendering this topic for search indexing or search results,
|
||||
// possibly in a different language than the current language. The topic
|
||||
// title and body come from translatable things in the Twig template, so we
|
||||
// need to set the default language to the desired language, render them,
|
||||
// then restore the default language so we do not affect other cron
|
||||
// processes. Also, just in case there is an exception, wrap the whole
|
||||
// thing in a try/finally block, and reset the language in the finally part.
|
||||
$old_language = $this->defaultLanguage->get();
|
||||
try {
|
||||
if ($old_language->getId() !== $language->getId()) {
|
||||
$this->defaultLanguage->set($language);
|
||||
$this->translationManager->setDefaultLangcode($language->getId());
|
||||
$this->languageManager->reset();
|
||||
}
|
||||
$topic = [];
|
||||
|
||||
// Render the title in this language.
|
||||
$title_build = [
|
||||
'title' => [
|
||||
'#type' => '#markup',
|
||||
'#markup' => $plugin->getLabel(),
|
||||
],
|
||||
];
|
||||
$topic['title'] = $this->renderer->renderInIsolation($title_build);
|
||||
$cacheable_metadata = CacheableMetadata::createFromRenderArray($title_build);
|
||||
|
||||
// Render the body in this language. For this, we need to set up a render
|
||||
// context, because the Twig plugins that provide the body assumes one
|
||||
// is present.
|
||||
$context = new RenderContext();
|
||||
$build = [
|
||||
'body' => $this->renderer->executeInRenderContext($context, [$plugin, 'getBody']),
|
||||
];
|
||||
$topic['text'] = $this->renderer->renderInIsolation($build);
|
||||
$cacheable_metadata->addCacheableDependency(CacheableMetadata::createFromRenderArray($build));
|
||||
$cacheable_metadata->addCacheableDependency($plugin);
|
||||
if (!$context->isEmpty()) {
|
||||
$cacheable_metadata->addCacheableDependency($context->pop());
|
||||
}
|
||||
|
||||
// Add the other information.
|
||||
$topic['url'] = $plugin->toUrl();
|
||||
$topic['cacheable_metadata'] = $cacheable_metadata;
|
||||
}
|
||||
finally {
|
||||
// Restore the original language.
|
||||
if ($old_language->getId() !== $language->getId()) {
|
||||
$this->defaultLanguage->set($old_language);
|
||||
$this->translationManager->setDefaultLangcode($old_language->getId());
|
||||
$this->languageManager->reset();
|
||||
}
|
||||
}
|
||||
|
||||
return $topic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the merged CacheableMetadata for all the top level help topic plugins.
|
||||
*
|
||||
* @return \Drupal\Core\Cache\CacheableMetadata
|
||||
* The merged CacheableMetadata for all the top level help topic plugins.
|
||||
*/
|
||||
protected function getCacheMetadata() {
|
||||
if (!isset($this->cacheableMetadata)) {
|
||||
$this->cacheableMetadata = new CacheableMetadata();
|
||||
foreach ($this->getPlugins() as $plugin) {
|
||||
$this->cacheableMetadata->addCacheableDependency($plugin);
|
||||
}
|
||||
}
|
||||
return $this->cacheableMetadata;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Plugin\HelpSection;
|
||||
|
||||
use Drupal\Core\Extension\ModuleExtensionList;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\help\Attribute\HelpSection;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides the module topics list section for the help page.
|
||||
*/
|
||||
#[HelpSection(
|
||||
id: 'hook_help',
|
||||
title: new TranslatableMarkup('Module overviews'),
|
||||
description: new TranslatableMarkup('Module overviews are provided by modules. Overviews available for your installed modules:')
|
||||
)]
|
||||
class HookHelpSection extends HelpSectionPluginBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* Constructs a HookHelpSection object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler service.
|
||||
* @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
|
||||
* The module extension list.
|
||||
*/
|
||||
public function __construct(
|
||||
array $configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
ModuleHandlerInterface $module_handler,
|
||||
protected ModuleExtensionList $moduleExtensionList,
|
||||
) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->moduleHandler = $module_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('module_handler'),
|
||||
$container->get('extension.list.module'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function listTopics() {
|
||||
$topics = [];
|
||||
$this->moduleHandler->invokeAllWith(
|
||||
'help',
|
||||
function (callable $hook, string $module) use (&$topics) {
|
||||
$title = $this->moduleExtensionList->getName($module);
|
||||
$topics[$title] = Link::createFromRoute($title, 'help.page', ['name' => $module]);
|
||||
}
|
||||
);
|
||||
|
||||
// Sort topics by title, which is the array key above.
|
||||
ksort($topics);
|
||||
return $topics;
|
||||
}
|
||||
|
||||
}
|
||||
526
web/core/modules/help/src/Plugin/Search/HelpSearch.php
Normal file
526
web/core/modules/help/src/Plugin/Search/HelpSearch.php
Normal file
@ -0,0 +1,526 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help\Plugin\Search;
|
||||
|
||||
use Drupal\Core\Access\AccessibleInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Config\Config;
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Database\Query\PagerSelectExtender;
|
||||
use Drupal\Core\Database\StatementInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\help\HelpSectionManager;
|
||||
use Drupal\help\SearchableHelpInterface;
|
||||
use Drupal\search\Attribute\Search;
|
||||
use Drupal\search\Plugin\SearchIndexingInterface;
|
||||
use Drupal\search\Plugin\SearchPluginBase;
|
||||
use Drupal\search\SearchIndexInterface;
|
||||
use Drupal\search\SearchQuery;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Handles searching for help using the Search module index.
|
||||
*
|
||||
* Help items are indexed if their HelpSection plugin implements
|
||||
* \Drupal\help\HelpSearchInterface.
|
||||
*
|
||||
* @see \Drupal\help\HelpSearchInterface
|
||||
* @see \Drupal\help\HelpSectionPluginInterface
|
||||
*
|
||||
* @internal
|
||||
* Plugin classes are internal.
|
||||
*/
|
||||
#[Search(
|
||||
id: 'help_search',
|
||||
title: new TranslatableMarkup('Help'),
|
||||
use_admin_theme: TRUE,
|
||||
)]
|
||||
class HelpSearch extends SearchPluginBase implements AccessibleInterface, SearchIndexingInterface {
|
||||
|
||||
/**
|
||||
* The current database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* A config object for 'search.settings'.
|
||||
*
|
||||
* @var \Drupal\Core\Config\Config
|
||||
*/
|
||||
protected $searchSettings;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*
|
||||
* @var \Drupal\Core\Language\LanguageManagerInterface
|
||||
*/
|
||||
protected $languageManager;
|
||||
|
||||
/**
|
||||
* The Drupal account to use for checking for access to search.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $account;
|
||||
|
||||
/**
|
||||
* The messenger.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* The state object.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The help section plugin manager.
|
||||
*
|
||||
* @var \Drupal\help\HelpSectionManager
|
||||
*/
|
||||
protected $helpSectionManager;
|
||||
|
||||
/**
|
||||
* The search index.
|
||||
*
|
||||
* @var \Drupal\search\SearchIndexInterface
|
||||
*/
|
||||
protected $searchIndex;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('database'),
|
||||
$container->get('config.factory')->get('search.settings'),
|
||||
$container->get('language_manager'),
|
||||
$container->get('messenger'),
|
||||
$container->get('current_user'),
|
||||
$container->get('state'),
|
||||
$container->get('plugin.manager.help_section'),
|
||||
$container->get('search.index')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a \Drupal\help_search\Plugin\Search\HelpSearch object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* Configuration for the plugin.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Database\Connection $database
|
||||
* The current database connection.
|
||||
* @param \Drupal\Core\Config\Config $search_settings
|
||||
* A config object for 'search.settings'.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
* The language manager.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The $account object to use for checking for access to view help.
|
||||
* @param \Drupal\Core\State\StateInterface $state
|
||||
* The state object.
|
||||
* @param \Drupal\help\HelpSectionManager $help_section_manager
|
||||
* The help section manager.
|
||||
* @param \Drupal\search\SearchIndexInterface $search_index
|
||||
* The search index.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, Config $search_settings, LanguageManagerInterface $language_manager, MessengerInterface $messenger, AccountInterface $account, StateInterface $state, HelpSectionManager $help_section_manager, SearchIndexInterface $search_index) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->database = $database;
|
||||
$this->searchSettings = $search_settings;
|
||||
$this->languageManager = $language_manager;
|
||||
$this->messenger = $messenger;
|
||||
$this->account = $account;
|
||||
$this->state = $state;
|
||||
$this->helpSectionManager = $help_section_manager;
|
||||
$this->searchIndex = $search_index;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function access($operation = 'view', ?AccountInterface $account = NULL, $return_as_object = FALSE) {
|
||||
$result = AccessResult::allowedIfHasPermission($account, 'access help pages');
|
||||
return $return_as_object ? $result : $result->isAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getType() {
|
||||
return $this->getPluginId();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
if ($this->isSearchExecutable()) {
|
||||
$results = $this->findResults();
|
||||
|
||||
if ($results) {
|
||||
return $this->prepareResults($results);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the search results.
|
||||
*
|
||||
* @return \Drupal\Core\Database\StatementInterface|null
|
||||
* Results from search query execute() method, or NULL if the search
|
||||
* failed.
|
||||
*/
|
||||
protected function findResults() {
|
||||
// We need to check access for the current user to see the topics that
|
||||
// could be returned by search. Each entry in the help_search_items
|
||||
// database has an optional permission that comes from the HelpSection
|
||||
// plugin, in addition to the generic 'access help pages'
|
||||
// permission. In order to enforce these permissions so only topics that
|
||||
// the current user has permission to view are selected by the query, make
|
||||
// a list of the permission strings and pre-check those permissions.
|
||||
$this->addCacheContexts(['user.permissions']);
|
||||
if (!$this->account->hasPermission('access help pages')) {
|
||||
return NULL;
|
||||
}
|
||||
$permissions = $this->database
|
||||
->select('help_search_items', 'hsi')
|
||||
->distinct()
|
||||
->fields('hsi', ['permission'])
|
||||
->condition('permission', '', '<>')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
$denied_permissions = array_filter($permissions, function ($permission) {
|
||||
return !$this->account->hasPermission($permission);
|
||||
});
|
||||
|
||||
$query = $this->database
|
||||
->select('search_index', 'i')
|
||||
// Restrict the search to the current interface language.
|
||||
->condition('i.langcode', $this->languageManager->getCurrentLanguage()->getId())
|
||||
->extend(SearchQuery::class)
|
||||
->extend(PagerSelectExtender::class);
|
||||
$query->innerJoin('help_search_items', 'hsi', '[i].[sid] = [hsi].[sid] AND [i].[type] = :type', [':type' => $this->getType()]);
|
||||
if ($denied_permissions) {
|
||||
$query->condition('hsi.permission', $denied_permissions, 'NOT IN');
|
||||
}
|
||||
$query->searchExpression($this->getKeywords(), $this->getType());
|
||||
|
||||
$find = $query
|
||||
->fields('i', ['langcode'])
|
||||
->fields('hsi', ['section_plugin_id', 'topic_id'])
|
||||
// Since SearchQuery makes these into GROUP BY queries, if we add
|
||||
// a field, for PostgreSQL we also need to make it an aggregate or a
|
||||
// GROUP BY. In this case, we want GROUP BY.
|
||||
->groupBy('i.langcode')
|
||||
->groupBy('hsi.section_plugin_id')
|
||||
->groupBy('hsi.topic_id')
|
||||
->limit(10)
|
||||
->execute();
|
||||
|
||||
// Check query status and set messages if needed.
|
||||
$status = $query->getStatus();
|
||||
|
||||
if ($status & SearchQuery::EXPRESSIONS_IGNORED) {
|
||||
$this->messenger->addWarning($this->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', ['@count' => $this->searchSettings->get('and_or_limit')]));
|
||||
}
|
||||
|
||||
if ($status & SearchQuery::LOWER_CASE_OR) {
|
||||
$this->messenger->addWarning($this->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'));
|
||||
}
|
||||
|
||||
if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) {
|
||||
$this->messenger->addWarning($this->formatPlural($this->searchSettings->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
|
||||
}
|
||||
|
||||
$unindexed = $this->state->get('help_search_unindexed_count', 1);
|
||||
if ($unindexed) {
|
||||
$this->messenger()->addWarning($this->t('Help search is not fully indexed. Some results may be missing or incorrect.'));
|
||||
}
|
||||
|
||||
return $find;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares search results for display.
|
||||
*
|
||||
* @param \Drupal\Core\Database\StatementInterface $found
|
||||
* Results found from a successful search query execute() method.
|
||||
*
|
||||
* @return array
|
||||
* List of search result render arrays, with links, snippets, etc.
|
||||
*/
|
||||
protected function prepareResults(StatementInterface $found) {
|
||||
$results = [];
|
||||
$plugins = [];
|
||||
$languages = [];
|
||||
$keys = $this->getKeywords();
|
||||
foreach ($found as $item) {
|
||||
$section_plugin_id = $item->section_plugin_id;
|
||||
if (!isset($plugins[$section_plugin_id])) {
|
||||
$plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id);
|
||||
}
|
||||
if ($plugins[$section_plugin_id]) {
|
||||
$langcode = $item->langcode;
|
||||
if (!isset($languages[$langcode])) {
|
||||
$languages[$langcode] = $this->languageManager->getLanguage($item->langcode);
|
||||
}
|
||||
$topic = $plugins[$section_plugin_id]->renderTopicForSearch($item->topic_id, $languages[$langcode]);
|
||||
if ($topic) {
|
||||
if (isset($topic['cacheable_metadata'])) {
|
||||
$this->addCacheableDependency($topic['cacheable_metadata']);
|
||||
}
|
||||
$results[] = [
|
||||
'title' => $topic['title'],
|
||||
'link' => $topic['url']->toString(),
|
||||
'snippet' => search_excerpt($keys, $topic['title'] . ' ' . $topic['text'], $item->langcode),
|
||||
'langcode' => $item->langcode,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function updateIndex() {
|
||||
// Update the list of items to be indexed.
|
||||
$this->updateTopicList();
|
||||
|
||||
// Find some items that need to be updated. Start with ones that have
|
||||
// never been indexed.
|
||||
$limit = (int) $this->searchSettings->get('index.cron_limit');
|
||||
|
||||
$query = $this->database->select('help_search_items', 'hsi');
|
||||
$query->fields('hsi', ['sid', 'section_plugin_id', 'topic_id']);
|
||||
$query->leftJoin('search_dataset', 'sd', '[sd].[sid] = [hsi].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]);
|
||||
$query->where('[sd].[sid] IS NULL');
|
||||
$query->groupBy('hsi.sid')
|
||||
->groupBy('hsi.section_plugin_id')
|
||||
->groupBy('hsi.topic_id')
|
||||
->range(0, $limit);
|
||||
$items = $query->execute()->fetchAll();
|
||||
|
||||
// If there is still space in the indexing limit, index items that have
|
||||
// been indexed before, but are currently marked as needing a re-index.
|
||||
if (count($items) < $limit) {
|
||||
$query = $this->database->select('help_search_items', 'hsi');
|
||||
$query->fields('hsi', ['sid', 'section_plugin_id', 'topic_id']);
|
||||
$query->leftJoin('search_dataset', 'sd', '[sd].[sid] = [hsi].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]);
|
||||
$query->condition('sd.reindex', 0, '<>');
|
||||
$query->groupBy('hsi.sid')
|
||||
->groupBy('hsi.section_plugin_id')
|
||||
->groupBy('hsi.topic_id')
|
||||
->range(0, $limit - count($items));
|
||||
$items = $items + $query->execute()->fetchAll();
|
||||
}
|
||||
|
||||
// Index the items we have chosen, in all available languages.
|
||||
$language_list = $this->languageManager->getLanguages(LanguageInterface::STATE_CONFIGURABLE);
|
||||
$section_plugins = [];
|
||||
|
||||
$words = [];
|
||||
try {
|
||||
foreach ($items as $item) {
|
||||
$section_plugin_id = $item->section_plugin_id;
|
||||
if (!isset($section_plugins[$section_plugin_id])) {
|
||||
$section_plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id);
|
||||
}
|
||||
|
||||
if (!$section_plugins[$section_plugin_id]) {
|
||||
$this->removeItemsFromIndex($item->sid);
|
||||
continue;
|
||||
}
|
||||
|
||||
$section_plugin = $section_plugins[$section_plugin_id];
|
||||
$this->searchIndex->clear($this->getType(), $item->sid);
|
||||
foreach ($language_list as $langcode => $language) {
|
||||
$topic = $section_plugin->renderTopicForSearch($item->topic_id, $language);
|
||||
if ($topic) {
|
||||
// Index the title plus body text.
|
||||
$text = '<h1>' . $topic['title'] . '</h1>' . "\n" . $topic['text'];
|
||||
$words += $this->searchIndex->index($this->getType(), $item->sid, $langcode, $text, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$this->searchIndex->updateWordWeights($words);
|
||||
$this->updateIndexState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function indexClear() {
|
||||
$this->searchIndex->clear($this->getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the database table containing topics to be indexed.
|
||||
*/
|
||||
public function updateTopicList() {
|
||||
// Start by fetching the existing list, so we can remove items not found
|
||||
// at the end.
|
||||
$old_list = $this->database->select('help_search_items', 'hsi')
|
||||
->fields('hsi', ['sid', 'topic_id', 'section_plugin_id', 'permission'])
|
||||
->execute();
|
||||
$old_list_ordered = [];
|
||||
$sids_to_remove = [];
|
||||
foreach ($old_list as $item) {
|
||||
$old_list_ordered[$item->section_plugin_id][$item->topic_id] = $item;
|
||||
$sids_to_remove[$item->sid] = $item->sid;
|
||||
}
|
||||
|
||||
$section_plugins = $this->helpSectionManager->getDefinitions();
|
||||
foreach ($section_plugins as $section_plugin_id => $section_plugin_definition) {
|
||||
$plugin = $this->getSectionPlugin($section_plugin_id);
|
||||
if (!$plugin) {
|
||||
continue;
|
||||
}
|
||||
$permission = $section_plugin_definition['permission'] ?? '';
|
||||
foreach ($plugin->listSearchableTopics() as $topic_id) {
|
||||
if (isset($old_list_ordered[$section_plugin_id][$topic_id])) {
|
||||
$old_item = $old_list_ordered[$section_plugin_id][$topic_id];
|
||||
if ($old_item->permission == $permission) {
|
||||
// Record has not changed.
|
||||
unset($sids_to_remove[$old_item->sid]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Permission has changed, update record.
|
||||
$this->database->update('help_search_items')
|
||||
->condition('sid', $old_item->sid)
|
||||
->fields(['permission' => $permission])
|
||||
->execute();
|
||||
unset($sids_to_remove[$old_item->sid]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// New record, create it.
|
||||
$this->database->insert('help_search_items')
|
||||
->fields([
|
||||
'section_plugin_id' => $section_plugin_id,
|
||||
'permission' => $permission,
|
||||
'topic_id' => $topic_id,
|
||||
])
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove remaining items from the index.
|
||||
$this->removeItemsFromIndex($sids_to_remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the 'help_search_unindexed_count' state variable.
|
||||
*
|
||||
* The state variable is a count of help topics that have never been indexed.
|
||||
*/
|
||||
public function updateIndexState() {
|
||||
$query = $this->database->select('help_search_items', 'hsi');
|
||||
$query->addExpression('COUNT(DISTINCT([hsi].[sid]))');
|
||||
$query->leftJoin('search_dataset', 'sd', '[hsi].[sid] = [sd].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]);
|
||||
$query->isNull('sd.sid');
|
||||
$never_indexed = $query->execute()->fetchField();
|
||||
$this->state->set('help_search_unindexed_count', $never_indexed);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function markForReindex() {
|
||||
$this->updateTopicList();
|
||||
$this->searchIndex->markForReindex($this->getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function indexStatus() {
|
||||
$this->updateTopicList();
|
||||
$total = $this->database->select('help_search_items', 'hsi')
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
|
||||
$query = $this->database->select('help_search_items', 'hsi');
|
||||
$query->addExpression('COUNT(DISTINCT([hsi].[sid]))');
|
||||
$query->leftJoin('search_dataset', 'sd', '[hsi].[sid] = [sd].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]);
|
||||
$condition = $this->database->condition('OR');
|
||||
$condition->condition('sd.reindex', 0, '<>')
|
||||
->isNull('sd.sid');
|
||||
$query->condition($condition);
|
||||
$remaining = $query->execute()->fetchField();
|
||||
|
||||
return [
|
||||
'remaining' => $remaining,
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item or items from the search index.
|
||||
*
|
||||
* @param int|int[] $sids
|
||||
* Search ID (sid) of item or items to remove.
|
||||
*/
|
||||
protected function removeItemsFromIndex($sids) {
|
||||
$sids = (array) $sids;
|
||||
|
||||
// Remove items from our table in batches of 100, to avoid problems
|
||||
// with having too many placeholders in database queries.
|
||||
foreach (array_chunk($sids, 100) as $this_list) {
|
||||
$this->database->delete('help_search_items')
|
||||
->condition('sid', $this_list, 'IN')
|
||||
->execute();
|
||||
}
|
||||
// Remove items from the search tables individually, as there is no bulk
|
||||
// function to delete items from the search index.
|
||||
foreach ($sids as $sid) {
|
||||
$this->searchIndex->clear($this->getType(), $sid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a help section plugin and verifies it is searchable.
|
||||
*
|
||||
* @param string $section_plugin_id
|
||||
* Type of plugin to instantiate.
|
||||
*
|
||||
* @return \Drupal\help\SearchableHelpInterface|false
|
||||
* Plugin object, or FALSE if it is not searchable.
|
||||
*/
|
||||
protected function getSectionPlugin($section_plugin_id) {
|
||||
/** @var \Drupal\help\HelpSectionPluginInterface $section_plugin */
|
||||
$section_plugin = $this->helpSectionManager->createInstance($section_plugin_id);
|
||||
// Intentionally return boolean to allow caching of results.
|
||||
return $section_plugin instanceof SearchableHelpInterface ? $section_plugin : FALSE;
|
||||
}
|
||||
|
||||
}
|
||||
41
web/core/modules/help/src/SearchableHelpInterface.php
Normal file
41
web/core/modules/help/src/SearchableHelpInterface.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\help;
|
||||
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for a HelpSection plugin that also supports search.
|
||||
*
|
||||
* @see \Drupal\help\HelpSectionPluginInterface
|
||||
*/
|
||||
interface SearchableHelpInterface {
|
||||
|
||||
/**
|
||||
* Returns the IDs of topics that should be indexed for searching.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of topic IDs that should be searchable. IDs need to be
|
||||
* unique within this HelpSection plugin.
|
||||
*/
|
||||
public function listSearchableTopics();
|
||||
|
||||
/**
|
||||
* Renders one topic for search indexing or search results.
|
||||
*
|
||||
* @param string $topic_id
|
||||
* The ID of the topic to be indexed.
|
||||
* @param \Drupal\Core\Language\LanguageInterface $language
|
||||
* The language to render the topic in.
|
||||
*
|
||||
* @return array
|
||||
* An array of information about the topic, with elements:
|
||||
* - title: The title of the topic in this language.
|
||||
* - text: The text of the topic in this language.
|
||||
* - url: The URL of the topic as a \Drupal\Core\Url object.
|
||||
* - cacheable_metadata: (optional) An object to add as a cache dependency
|
||||
* if this topic is shown in search results.
|
||||
*/
|
||||
public function renderTopicForSearch($topic_id, LanguageInterface $language);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user