diff --git a/core/modules/help/config/install/filter.format.help.yml b/core/modules/help/config/install/filter.format.help.yml new file mode 100644 index 0000000..7f87b2e --- /dev/null +++ b/core/modules/help/config/install/filter.format.help.yml @@ -0,0 +1,15 @@ +langcode: en +status: true +name: Help +format: help +weight: 0 +filters: + filter_html: + id: filter_html + provider: filter + status: true + weight: -10 + settings: + allowed_html: '
    1. ' + filter_html_help: true + filter_html_nofollow: false diff --git a/core/modules/help/config/install/help.topic.help.yml b/core/modules/help/config/install/help.topic.help.yml new file mode 100644 index 0000000..b898631 --- /dev/null +++ b/core/modules/help/config/install/help.topic.help.yml @@ -0,0 +1,11 @@ +langcode: en +status: true +dependencies: { } +id: help +label: 'Help module' +body: + value: "

      About

      \r\n

      The Help module provides Help reference pages to guide you through the use and configuration of modules. It is a starting point for Drupal.org online documentation 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 online documentation for the Help module.

      \r\n

      Uses

      \r\n
      \r\n
      Providing a help reference
      \r\n
      The Help module displays both static module-provided help and configured help topics on the main Help page.
      \r\n
      Configuring help topics
      \r\n
      You can add, edit, delete, and translate configure help topics on the Help topics administration page. The help topics that are listed in the Module help section of the main Help page cannot be edited or deleted.
      \r\n
      \r\n" + format: help +top_level: true +related: { } +list_on: { } diff --git a/core/modules/help/config/schema/help.schema.yml b/core/modules/help/config/schema/help.schema.yml new file mode 100644 index 0000000..4922897 --- /dev/null +++ b/core/modules/help/config/schema/help.schema.yml @@ -0,0 +1,33 @@ +help.topic.*: + type: config_entity + label: 'Help topic' + mapping: + id: + type: string + label: 'Machine-readable name' + label: + type: label + label: 'Title' + body: + label: 'Body' + type: mapping + mapping: + value: + type: text + label: 'Body' + format: + type: string + label: 'Text format' + top_level: + type: boolean + label: 'Top-level topic' + related: + type: sequence + label: 'Related topics' + sequence: + - type: string + list_on: + type: sequence + label: 'List on topics' + sequence: + - type: string diff --git a/core/modules/help/help.links.action.yml b/core/modules/help/help.links.action.yml new file mode 100644 index 0000000..3b452f0 --- /dev/null +++ b/core/modules/help/help.links.action.yml @@ -0,0 +1,5 @@ +entity.help_topic.add_form: + route_name: entity.help_topic.add_form + title: 'Add new help topic' + appears_on: + - help.topic_admin diff --git a/core/modules/help/help.links.menu.yml b/core/modules/help/help.links.menu.yml index aa85add..aa9c469 100644 --- a/core/modules/help/help.links.menu.yml +++ b/core/modules/help/help.links.menu.yml @@ -4,3 +4,9 @@ help.main: route_name: help.main weight: 9 parent: system.admin + +help.topic_admin: + title: Help topics + description: Add, delete, and edit help topics. + route_name: help.topic_admin + parent: system.admin_config_development diff --git a/core/modules/help/help.links.task.yml b/core/modules/help/help.links.task.yml new file mode 100644 index 0000000..e2ce418 --- /dev/null +++ b/core/modules/help/help.links.task.yml @@ -0,0 +1,9 @@ +entity.help_topic.canonical: + title: 'View' + route_name: entity.help_topic.canonical + base_route: entity.help_topic.canonical + +entity.help_topic.edit_form: + title: 'Edit' + route_name: entity.help_topic.edit_form + base_route: entity.help_topic.canonical diff --git a/core/modules/help/help.module b/core/modules/help/help.module index 3539660..81dfdad 100644 --- a/core/modules/help/help.module +++ b/core/modules/help/help.module @@ -26,17 +26,6 @@ function help_help($route_name, RouteMatchInterface $route_match) { $output .= '
    '; $output .= '

    ' . t('For more information, refer to the subjects listed in the Help Topics section or to the online documentation and support pages at drupal.org.', array('!docs' => 'https://drupal.org/documentation', '!support' => 'https://drupal.org/support', '!drupal' => 'https://drupal.org')) . '

    '; return $output; - - case 'help.page.help': - $output = ''; - $output .= '

    ' . t('About') . '

    '; - $output .= '

    ' . t('The Help module provides Help reference pages to guide you through the use and configuration of modules. It is a starting point for Drupal.org online documentation 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 online documentation for the Help module.', array('!help' => 'https://drupal.org/documentation/modules/help/', '!handbook' => 'https://drupal.org/documentation', '!help-page' => \Drupal::url('help.main'))) . '

    '; - $output .= '

    ' . t('Uses') . '

    '; - $output .= '
    '; - $output .= '
    ' . t('Providing a help reference') . '
    '; - $output .= '
    ' . t('The Help module displays explanations for using each module listed on the main Help reference page.', array('!help' => \Drupal::url('help.main'))) . '
    '; - $output .= '
    '; - return $output; } } diff --git a/core/modules/help/help.permissions.yml b/core/modules/help/help.permissions.yml new file mode 100644 index 0000000..c1bc85a --- /dev/null +++ b/core/modules/help/help.permissions.yml @@ -0,0 +1,3 @@ +administer help topics: + title: 'Administer help topics' + description: 'Create, edit, and delete help topics.' diff --git a/core/modules/help/help.routing.yml b/core/modules/help/help.routing.yml index a393eb8..3540943 100644 --- a/core/modules/help/help.routing.yml +++ b/core/modules/help/help.routing.yml @@ -13,3 +13,43 @@ help.page: _title: 'Help' requirements: _permission: 'access administration pages' + +entity.help_topic.canonical: + path: '/admin/help-topic/{help_topic}' + defaults: + _entity_view: 'help_topic.full' + _title: 'Help' + requirements: + _entity_access: 'help_topic.view' + +help.topic_admin: + path: '/admin/config/development/help' + defaults: + _entity_list: 'help_topic' + _title: 'Help topics' + requirements: + _permission: 'administer help topics' + +entity.help_topic.add_form: + path: '/admin/config/development/help/add' + defaults: + _entity_form: 'help_topic.add' + _title: 'Add help topic' + requirements: + _entity_create_access: 'help_topic' + +entity.help_topic.edit_form: + path: '/admin/config/development/help/{help_topic}/edit' + defaults: + _entity_form: 'help_topic.edit' + _title: 'Edit help topic' + requirements: + _entity_access: 'help_topic.edit' + +entity.help_topic.delete_form: + path: '/admin/config/development/help/{help_topic}/delete' + defaults: + _entity_form: 'help_topic.delete' + _title: 'Delete help topic' + requirements: + _entity_access: 'help_topic.delete' diff --git a/core/modules/help/help.services.yml b/core/modules/help/help.services.yml new file mode 100644 index 0000000..5a43fbf --- /dev/null +++ b/core/modules/help/help.services.yml @@ -0,0 +1,6 @@ +services: + help.breadcrumb: + class: Drupal\help\HelpBreadcrumbBuilder + arguments: ['@string_translation'] + tags: + - { name: breadcrumb_builder, priority: 900 } diff --git a/core/modules/help/help.tokens.inc b/core/modules/help/help.tokens.inc new file mode 100644 index 0000000..9854c62 --- /dev/null +++ b/core/modules/help/help.tokens.inc @@ -0,0 +1,53 @@ + t("Help Topics"), + 'description' => t("Provides tokens for help topics."), + ); + + $topics = array(); + /* @var \Drupal\help\HelpTopicInterface $help_topic */ + foreach (HelpTopic::loadMultiple() as $help_topic_id => $help_topic) { + $topics[$help_topic_id] = array( + 'name' => $help_topic->label(), + 'description' => t('URL to the @label help topic', array('@label' => $help_topic->label())), + ); + } + + return array( + 'types' => $types, + 'tokens' => array( + 'help_topic' => $topics, + ), + ); +} + +/** + * Implements hook_tokens(). + */ +function help_tokens($type, $tokens, array $data = array(), array $options = array()) { + $replacements = array(); + + if ($type == 'help_topic') { + foreach ($tokens as $name => $original) { + if ($topic = HelpTopic::load($name)) { + $replacements[$original] = $topic->url('canonical'); + } + } + } + + return $replacements; +} diff --git a/core/modules/help/src/Controller/HelpController.php b/core/modules/help/src/Controller/HelpController.php index 2de0a53..0453375 100644 --- a/core/modules/help/src/Controller/HelpController.php +++ b/core/modules/help/src/Controller/HelpController.php @@ -8,11 +8,12 @@ namespace Drupal\help\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; +use Drupal\Component\Utility\String; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Drupal\Component\Utility\String; /** * Controller routines for help routes. @@ -27,13 +28,23 @@ class HelpController extends ControllerBase { protected $routeMatch; /** + * The help topic entity storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $helpStorage; + + /** * Creates a new HelpController. * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The current route match. + * @param \Drupal\Core\Entity\EntityStorageInterface $help_storage + * The entity storage for help topic entities. */ - public function __construct(RouteMatchInterface $route_match) { + public function __construct(RouteMatchInterface $route_match, EntityStorageInterface $help_storage) { $this->routeMatch = $route_match; + $this->helpStorage = $help_storage; } /** @@ -41,50 +52,91 @@ public function __construct(RouteMatchInterface $route_match) { */ public static function create(ContainerInterface $container) { return new static( - $container->get('current_route_match') + $container->get('current_route_match'), + $container->get('entity.manager')->getStorage('help_topic') ); } /** - * Prints a page listing a glossary of Drupal terminology. + * Prints a page listing help topics. * - * @return string - * An HTML string representing the contents of help page. + * @return array + * Render array for the help topics list. */ public function helpMain() { $output = array( '#attached' => array( 'css' => array(drupal_get_path('module', 'help') . '/css/help.module.css'), ), - '#markup' => '

    ' . $this->t('Help topics') . '

    ' . $this->t('Help is available on the following items:') . '

    ' . $this->helpLinksAsList(), ); + + $template = '

    {{ title }}

    {{ header }}

    {{ links }}'; + + $output['modules'] = array( + '#type' => 'inline_template', + '#template' => $template, + '#context' => array( + 'title' => $this->t('Module help'), + 'header' => $this->t('Help pages are available for the following modules:'), + 'links' => array('#markup' => $this->moduleHelpLinksAsList()), + ), + ); + + $output['topics'] = array( + '#type' => 'inline_template', + '#template' => $template, + '#context' => array( + 'title' => $this->t('Configured topics'), + 'header' => $this->t('Additional help topics configured on your site:'), + 'links' => array('#markup' => $this->helpTopicsList()), + ), + ); + return $output; } /** - * Provides a formatted list of available help topics. + * Provides a formatted list of available module help topics from hook_help(). * * @return string * A string containing the formatted list. */ - protected function helpLinksAsList() { + protected function moduleHelpLinksAsList() { $module_info = system_rebuild_module_data(); $modules = array(); foreach ($this->moduleHandler()->getImplementations('help') as $module) { if ($this->moduleHandler()->invoke($module, 'help', array("help.page.$module", $this->routeMatch))) { - $modules[$module] = $module_info[$module]->info['name']; + $modules[$module] = array( + 'title' => $module_info[$module]->info['name'], + 'url' => new Url('help.page', array('name' => $module)), + ); } } asort($modules); + return $this->fourColumnList($modules); + } + + /** + * Makes a four-column list of links. + * + * @param array $links + * Array whose elements are arrays with: + * - title: Link title. + * - url: URL object. + * + * @return string + * Markup for a four-column UL list of links. + */ + protected function fourColumnList($links) { // Output pretty four-column list. - $count = count($modules); + $count = count($links); $break = ceil($count / 4); $output = '
      '; $i = 0; - foreach ($modules as $module => $name) { - $output .= '
    • ' . $this->l($name, new Url('help.page', array('name' => $module))) . '
    • '; + foreach ($links as $info) { + $output .= '
    • ' . $this->l($info['title'], $info['url']) . '
    • '; if (($i + 1) % $break == 0 && ($i + 1) != $count) { $output .= '
      '; } @@ -96,13 +148,37 @@ protected function helpLinksAsList() { } /** - * Prints a page listing general help for a module. + * Provides a formatted list of configured help topics. + * + * @return string + * A string containing the formatted list. + */ + protected function helpTopicsList() { + $entities = $this->helpStorage->loadMultiple(); + uasort($entities, array('Drupal\help\Entity\HelpTopic', 'sort')); + + $topics = array(); + /* @var \Drupal\help\HelpTopicInterface $entity */ + foreach ($entities as $entity) { + if ($entity->isTopLevel()) { + $topics[] = array( + 'title' => $entity->label(), + 'url' => $entity->urlInfo('canonical'), + ); + } + } + + return $this->fourColumnList($topics); + } + + /** + * Renders a help page from a module's hook_help(). * * @param string $name * A module name to display a help page for. * * @return array - * A render array as expected by drupal_render(). + * A render array for the help page. * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ @@ -144,5 +220,4 @@ public function helpPage($name) { throw new NotFoundHttpException(); } } - } diff --git a/core/modules/help/src/Entity/HelpTopic.php b/core/modules/help/src/Entity/HelpTopic.php new file mode 100644 index 0000000..7c7131d --- /dev/null +++ b/core/modules/help/src/Entity/HelpTopic.php @@ -0,0 +1,180 @@ +get('body'); + } + + /** + * {@inheritdoc} + */ + public function isTopLevel() { + return $this->get('top_level'); + } + + /** + * {@inheritdoc} + */ + public function getRelated() { + return $this->get('related'); + } + + /** + * {@inheritdoc} + */ + public function setRelated($topics) { + $topics = $this->cleanTopicList($topics); + $this->set('related', $topics); + + return $this; + } + + /** + * Cleans a list of topic IDs. + * + * @param string|array $topics + * Either an array of topic IDs or a comma-separated list in a string. + * + * @return array + * List of non-empty IDs from the input. + */ + protected function cleanTopicList($topics) { + if (is_string($topics)) { + $topics = explode(',', $topics); + } + + // Make a list of non-empty trimmed IDs. + $tosave = array(); + foreach ($topics as $item) { + $item = trim($item); + if ($item) { + $tosave[] = $item; + } + } + + return $tosave; + } + + /** + * {@inheritdoc} + */ + public function getListOn() { + return $this->get('list_on'); + } + + /** + * {@inheritdoc} + */ + public function setListOn($topics) { + $topics = $this->cleanTopicList($topics); + $this->set('list_on', $topics); + + return $this; + } +} + diff --git a/core/modules/help/src/Form/HelpDeleteForm.php b/core/modules/help/src/Form/HelpDeleteForm.php new file mode 100644 index 0000000..f98163f --- /dev/null +++ b/core/modules/help/src/Form/HelpDeleteForm.php @@ -0,0 +1,49 @@ +t('Are you sure you want to delete the topic %name?', array('%name' => $this->entity->label())); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('help.topic_admin'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + drupal_set_message(t('Deleted help topic %name.', array('%name' => $this->entity->label()))); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } +} diff --git a/core/modules/help/src/Form/HelpTopicForm.php b/core/modules/help/src/Form/HelpTopicForm.php new file mode 100644 index 0000000..3b2c9b3 --- /dev/null +++ b/core/modules/help/src/Form/HelpTopicForm.php @@ -0,0 +1,155 @@ +helpStorage = $help_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager')->getStorage('help_topic') + ); + } + + /** + * Checks for an existing help topic. + * + * @param string $entity_id + * The entity ID. + * + * @return bool + * TRUE if this topic already exists, FALSE otherwise. + */ + public function exists($entity_id) { + return (bool) $this->helpStorage + ->getQuery() + ->condition('id', $entity_id) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Title'), + '#maxlength' => 100, + '#default_value' => $this->entity->label(), + '#required' => TRUE, + ); + + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $this->entity->id(), + '#machine_name' => array( + 'exists' => array($this, 'exists'), + 'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores.'), + ), + ); + + $form['top_level'] = array( + '#type' => 'checkbox', + '#default_value' => $this->entity->isTopLevel(), + '#title' => $this->t('Top-level topic'), + '#description' => $this->t('Check box if this topic should be displayed on the topics list'), + ); + + $body = $this->entity->getBody(); + if (!isset($body['format'])) { + $body = array('value' => '', 'format' => 'help'); + } + + $form['body'] = array( + '#type' => 'text_format', + '#title' => $this->t('Body'), + '#default_value' => $body['value'], + '#format' => $body['format'], + ); + + $form['related'] = array( + '#title' => $this->t('Related topics'), + '#description' => $this->t('Comma-separated list of machine names of related topics.'), + '#type' => 'textarea', + '#default_value' => implode(',', $this->entity->getRelated()), + ); + + $form['list_on'] = array( + '#title' => $this->t('Topics to list this topic on'), + '#description' => $this->t('Comma-separated list of machine names of topics. This topic will be listed as Related on those topic pages.'), + '#type' => 'textarea', + '#default_value' => implode(',', $this->entity->getListOn()), + ); + + $form['#entity_builders'][] = array($this, 'copyTopicFieldsToEntity'); + + return parent::form($form, $form_state); + } + + /** + * Copies the topics field values to the entity properties. + * + * This is added to $form['#entity_builders'] in the form builder method. + * + * @param string $type + * Type of entity. + * @param \Drupal\Core\Entity\EntityInterface $entity + * Entity to copy property values to. + * @param array $form + * Form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + */ + protected function copyTopicFieldsToEntity($type, EntityInterface $entity, array &$form, FormStateInterface &$form_state) { + $entity->setRelated($form_state->getValue('related')); + $entity->setListOn($form_state->getValue('list_on')); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $form_state->setRedirect('help.topic_admin'); + + $status = $this->entity->save(); + if ($status == SAVED_UPDATED) { + drupal_set_message($this->t('Help topic updated.')); + } + else { + drupal_set_message($this->t('Help topic added.')); + } + } +} diff --git a/core/modules/help/src/HelpAccessControlHandler.php b/core/modules/help/src/HelpAccessControlHandler.php new file mode 100644 index 0000000..43043dc --- /dev/null +++ b/core/modules/help/src/HelpAccessControlHandler.php @@ -0,0 +1,33 @@ +stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function applies(RouteMatchInterface $route_match) { + return $route_match->getRouteName() == 'entity.help_topic.canonical'; + } + + /** + * {@inheritdoc} + */ + public function build(RouteMatchInterface $route_match) { + $links = array( + Link::createFromRoute($this->t('Home'), ''), + Link::createFromRoute($this->t('Administration'), 'system.admin'), + Link::createFromRoute($this->t('Help'), 'help.main'), + ); + + return $links; + } + +} diff --git a/core/modules/help/src/HelpListBuilder.php b/core/modules/help/src/HelpListBuilder.php new file mode 100644 index 0000000..833756a --- /dev/null +++ b/core/modules/help/src/HelpListBuilder.php @@ -0,0 +1,46 @@ +t('Title'); + $header['id'] = $this->t('Machine name'); + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row = array(); + + $row['label']['data'] = array( + '#type' => 'link', + '#title' => $this->getLabel($entity), + '#url' => $entity->urlInfo('canonical'), + ); + + $row['id'] = $entity->id(); + + return $row + parent::buildRow($entity); + } +} diff --git a/core/modules/help/src/HelpTopicInterface.php b/core/modules/help/src/HelpTopicInterface.php new file mode 100644 index 0000000..978aec6 --- /dev/null +++ b/core/modules/help/src/HelpTopicInterface.php @@ -0,0 +1,74 @@ +entityTypeId = $entity_type->id(); + $this->entityType = $entity_type; + $this->entityManager = $entity_manager; + $this->languageManager = $language_manager; + $this->token = $token; + $this->queryFactory = $query_factory; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static($entity_type, $container->get('entity.manager'), $container->get('language_manager'), $container->get('token'), $container->get('entity.query')); + } + + /** + * {@inheritdoc} + */ + public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) { + $output = array(); + + /** @var \Drupal\help\HelpTopicInterface[] $entities */ + foreach ($entities as $entity_id => $help_topic) { + $build = array( + '#langcode' => $langcode, + '#title' => String::checkPlain($help_topic->label()), + ); + + $body = $help_topic->getBody(); + $build['body'] = array( + '#type' => 'processed_text', + '#text' => $this->token->replace($body['value']), + '#format' => $body['format'], + ); + + // The list should include topics this entity lists as related, plus + // topics that have said "Add me to this topic's related list". + $related = $help_topic->getRelated() + + $this->queryFactory->get('help_topic') + ->condition('list_on.*', $help_topic->id()) + ->execute(); + + $links = array(); + $storage = $this->entityManager->getStorage('help_topic'); + + foreach ($related as $other_id) { + if ($other_id != $help_topic->id()) { + $topic = $storage->load($other_id); + if ($topic) { + $links[$other_id] = array( + 'title' => $topic->label(), + 'url' => $topic->urlInfo('canonical'), + ); + } + } + } + + if (count($links)) { + ksort($links); + $build['related'] = array( + '#theme' => 'links', + '#heading' => array( + 'text' => $this->t('Related topics'), + 'level' => 'h3', + ), + '#links' => $links, + ); + } + + $output[$entity_id] = $build; + } + + return $output; + } +} diff --git a/core/modules/help/src/Tests/HelpTest.php b/core/modules/help/src/Tests/HelpTest.php index 7010f74..90ae0a4 100644 --- a/core/modules/help/src/Tests/HelpTest.php +++ b/core/modules/help/src/Tests/HelpTest.php @@ -16,15 +16,16 @@ */ class HelpTest extends WebTestBase { + // Install with the standard profile, because it has the help block + // enabled and admin theme, etc. + protected $profile = 'standard'; + /** * Modules to enable. * * @var array. */ - public static $modules = array('shortcut'); - - // Tests help implementations of many arbitrary core modules. - protected $profile = 'standard'; + public static $modules = array('help_test'); /** * The admin user that will be created. @@ -39,20 +40,19 @@ class HelpTest extends WebTestBase { protected function setUp() { parent::setUp(); - $this->getModuleList(); - // Create users. - $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer permissions')); + $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer permissions', 'administer help topics')); $this->anyUser = $this->drupalCreateUser(array()); } /** - * Logs in users, creates dblog events, and tests dblog functionality. + * Logs in users, tests help pages. */ public function testHelp() { // Login the admin user. $this->drupalLogin($this->adminUser); $this->verifyHelp(); + $this->verifyHelpLinks(); // Login the regular user. $this->drupalLogin($this->anyUser); @@ -67,12 +67,18 @@ public function testHelp() { $this->assertRaw(t('For more information, refer to the subjects listed in the Help Topics section or to the online documentation and support pages at drupal.org.', array('!docs' => 'https://drupal.org/documentation', '!support' => 'https://drupal.org/support', '!drupal' => 'https://drupal.org')), 'Help intro text correctly appears.'); // Verify that help topics text appears. - $this->assertRaw('

      ' . t('Help topics') . '

      ' . t('Help is available on the following items:') . '

      ', 'Help topics text correctly appears.'); + $this->assertRaw('

      ' . t('Module help') . '

      ' . t('Help pages are available for the following modules:') . '

      ', 'Help module topics text correctly appears.'); + $this->assertRaw('

      ' . t('Configured topics') . '

      ' . t('Additional help topics configured on your site:') . '

      ', 'Help configured topics text correctly appears.'); // Make sure links are properly added for modules implementing hook_help(). foreach ($this->getModuleList() as $module => $name) { $this->assertLink($name, 0, format_string('Link properly added to @name (admin/help/@module)', array('@module' => $module, '@name' => $name))); } + + // Make sure links are properly added for topics. + foreach ($this->getTopicList() as $topic => $name) { + $this->assertLink($name, 0, format_string('Link properly added to @name (admin/help-topic/@topic)', array('@topic' => $topic, '@name' => $name))); + } } /** @@ -100,20 +106,68 @@ protected function verifyHelp($response = 200) { $this->assertRaw('

      ' . t($name) . '

      ', format_string('%module heading was displayed', array('%module' => $module))); } } + + foreach ($this->getTopicList() as $topic => $name) { + // View module help node. + $this->drupalGet('admin/help-topic/' . $topic); + $this->assertResponse($response); + if ($response == 200) { + $this->assertTitle($name . ' | Drupal', format_string('%topic title was displayed', array('%topic' => $topic))); + $this->assertRaw('

      ' . t($name) . '

      ', format_string('%topic heading was displayed', array('%topic' => $topic))); + } + } } /** - * Gets the list of enabled modules that implement hook_help(). + * Verifies links on the test help topic page and other pages. + * + * Assumes an admin user is logged in. + */ + protected function verifyHelpLinks() { + // Verify links on the test top-level page. + $page = 'admin/help-topic/help_test'; + $links = array( + 'link to the Help module topic' => 'The Help module provides', + 'link to the help admin page' => 'Add new help topic', + 'Help module' => 'The Help module provides', + 'Linked topic' => 'This topic is not supposed to be top-level', + 'Additional topic' => 'This topic should get listed automatically', + ); + foreach ($links as $link_text => $page_text) { + $this->drupalGet($page); + $this->clickLink($link_text); + $this->assertText($page_text); + } + + // Verify that the non-top-level topics do not appear on the Help page. + $this->drupalGet('admin/help'); + $this->assertNoLink('Linked topic'); + $this->assertNoLink('Additional topic'); + } + + /** + * Gets a list of modules to test for hook_help() pages. * * @return array - * A list of enabled modules. + * A list of modules to test, machine name => displayed name. */ protected function getModuleList() { - $modules = array(); - $module_data = system_rebuild_module_data(); - foreach (\Drupal::moduleHandler()->getImplementations('help') as $module) { - $modules[$module] = $module_data[$module]->info['name']; - } - return $modules; + return array( + 'help_test' => 'Help Test', + ); } + + /** + * Gets a list of topic IDs to test. + * + * @return array + * A list of topics to test, machine name => displayed name. + */ + protected function getTopicList() { + return array( + 'help' => t('Help module'), + 'help_test' => t('Help Test module'), + ); + } + } diff --git a/core/modules/help/src/Tests/HelpTopicAdminTest.php b/core/modules/help/src/Tests/HelpTopicAdminTest.php new file mode 100644 index 0000000..291fe47 --- /dev/null +++ b/core/modules/help/src/Tests/HelpTopicAdminTest.php @@ -0,0 +1,132 @@ +adminUser = $this->drupalCreateUser(array('access administration pages', 'administer help topics', 'use text format help')); + $this->nonAdminUser = $this->drupalCreateUser(array('access administration pages')); + } + + /** + * Logs in users, tests help admin pages. + */ + public function testHelpAdmin() { + $this->drupalLogin($this->adminUser); + $this->verifyHelpAdmin(); + + $this->drupalLogin($this->nonAdminUser); + $this->verifyHelpAdmin(403); + } + + /** + * Verifies the logged in user has the correct access to help admin. + * + * @param integer $response + * An HTTP response code to verify. + */ + protected function verifyHelpAdmin($response = 200) { + // Verify admin links. + foreach(array('admin/config', 'admin/config/development', 'admin/index') as $page) { + $this->drupalGet($page); + if ($response == 200) { + $this->assertText('Add, delete, and edit help topics'); + $this->assertLink('Help topics'); + } + else { + $this->assertNoText('Add, delete, and edit help topics'); + $this->assertNoLink('Help topics'); + } + } + + // Verify CRUD and listing page. + $this->drupalGet('admin/config/development/help'); + $this->assertResponse($response); + if ($response == 200) { + $this->assertLink('Add new help topic'); + $this->assertText('Help topics'); + $this->assertText('Title'); + $this->assertText('Machine name'); + $this->assertText('Operations'); + } + + $this->drupalGet('admin/config/development/help/add'); + $this->assertResponse($response); + + // Everything after this point, just do for the admin user. + if ($response != 200) { + return; + } + + // Create a new help topic from the UI. + $body = 'This text is for the foo topic'; + $title = 'Foo topic'; + $this->drupalPostForm(NULL, array( + 'label' => $title, + 'id' => 'foo', + 'top_level' => TRUE, + 'body[value]' => $body, + ), t('Save')); + $this->assertText('Help topic added'); + + // Click to view the topic and verify the edit link works too. + $this->clickLink($title); + $this->assertText($title); + $this->assertText($body); + + $this->clickLink(t('Edit')); + $new_title = 'Foo longer topic'; + $this->drupalPostForm(NULL, array('label' => $new_title), t('Save')); + $this->assertText('Help topic updated'); + $this->assertLink($new_title); + + // Verify the link is on the Help page. + $this->drupalGet('admin/help'); + $this->assertLink($new_title); + + // Test deleting. + $this->drupalGet('admin/config/development/help/foo/delete'); + $this->assertText('This action cannot be undone.'); + $this->assertText('Are you sure you want to delete the topic'); + $this->assertText($new_title); + $this->drupalPostForm(NULL, array(), t('Delete')); + $this->assertText('Deleted help topic'); + $this->assertText($new_title); + $this->assertNoLink($new_title); + $this->drupalGet('admin/help'); + $this->assertNoLink($new_title); + } + +} diff --git a/core/modules/help/src/Tests/HelpTopicTokensTest.php b/core/modules/help/src/Tests/HelpTopicTokensTest.php new file mode 100644 index 0000000..95a2405 --- /dev/null +++ b/core/modules/help/src/Tests/HelpTopicTokensTest.php @@ -0,0 +1,53 @@ +installSchema('system', array('router', 'sequences')); + $this->installConfig(array('help')); + \Drupal::service('router.builder')->rebuild(); + + } + + /** + * Tests that help topic tokens work. + */ + public function testHelpTopicTokens() { + $text = 'This should Link to help topic'; + $replaced = \Drupal::token()->replace($text); + $this->assertTrue(strpos($replaced, 'replace($text); + $this->assertTrue(strpos($replaced, '[help_topic:nonexistant]') !== FALSE); + } + +} diff --git a/core/modules/help/src/Tests/NoHelpTest.php b/core/modules/help/src/Tests/NoHelpTest.php index cd4d460..caaf75b 100644 --- a/core/modules/help/src/Tests/NoHelpTest.php +++ b/core/modules/help/src/Tests/NoHelpTest.php @@ -43,7 +43,7 @@ public function testMainPageNoHelp() { $this->drupalGet('admin/help'); $this->assertResponse(200); - $this->assertText('Help is available on the following items', 'Help page is found.'); + $this->assertText('Help pages are available for the following modules', 'Help page is found.'); $this->assertNoText('Hook menu tests', 'Making sure the test module menu_test does not display a help link on admin/help.'); } } diff --git a/core/modules/help/test/modules/help_test/config/install/help.topic.help_test.yml b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test.yml new file mode 100644 index 0000000..f7ad029 --- /dev/null +++ b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test.yml @@ -0,0 +1,13 @@ +langcode: en +status: true +dependencies: { } +id: help_test +label: 'Help Test module' +body: + value: 'This is a test. It should link to the Help module topic, and it should link to the help admin page. Also there should be a related topic link below to the Help module topic page and the linked topic.' + format: help +top_level: true +related: + - help + - help_test_linked +list_on: { } diff --git a/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_additional.yml b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_additional.yml new file mode 100644 index 0000000..d638cb3 --- /dev/null +++ b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_additional.yml @@ -0,0 +1,12 @@ +langcode: en +status: true +dependencies: { } +id: help_test_additional +label: 'Additional topic' +body: + value: 'This topic should get listed automatically on the Help test topic.' + format: help +top_level: false +related: { } +list_on: + - help_test diff --git a/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_linked.yml b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_linked.yml new file mode 100644 index 0000000..8d3674c --- /dev/null +++ b/core/modules/help/test/modules/help_test/config/install/help.topic.help_test_linked.yml @@ -0,0 +1,11 @@ +langcode: en +status: true +dependencies: { } +id: help_test_linked +label: 'Linked topic' +body: + value: 'This topic is not supposed to be top-level.' + format: help +top_level: false +related: { } +list_on: { } diff --git a/core/modules/help/test/modules/help_test/help_test.info.yml b/core/modules/help/test/modules/help_test/help_test.info.yml new file mode 100644 index 0000000..e67f988 --- /dev/null +++ b/core/modules/help/test/modules/help_test/help_test.info.yml @@ -0,0 +1,6 @@ +name: 'Help Test' +type: module +description: 'Support module for help testing.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/help/test/modules/help_test/help_test.module b/core/modules/help/test/modules/help_test/help_test.module new file mode 100644 index 0000000..f7a918b --- /dev/null +++ b/core/modules/help/test/modules/help_test/help_test.module @@ -0,0 +1,18 @@ +container->get('module_handler')->moduleExists($module)) { + if ($module != 'core' && !$this->container->get('module_handler')->moduleExists($module)) { throw new \RuntimeException(format_string("'@module' module is not enabled.", array( '@module' => $module, ))); diff --git a/core/modules/system/src/Tests/Token/RouteTokensTest.php b/core/modules/system/src/Tests/Token/RouteTokensTest.php new file mode 100644 index 0000000..ec19325 --- /dev/null +++ b/core/modules/system/src/Tests/Token/RouteTokensTest.php @@ -0,0 +1,51 @@ +installSchema('system', array('router', 'sequences')); + $this->installConfig(array('core')); + \Drupal::service('router.builder')->rebuild(); + + } + + /** + * Tests that default route tokens work. + */ + public function testRouteTokens() { + $text = 'This should Link to admin'; + $replaced = \Drupal::token()->replace($text); + $this->assertTrue(strpos($replaced, 'replace($text); + $this->assertTrue(strpos($replaced, '[route:system.nonexistant]') !== FALSE); + } + +} diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index cd2e6cf..3ac1136 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -883,10 +883,13 @@ function hook_system_info_alter(array &$info, \Drupal\Core\Extension\Extension $ * developers should usually be provided via function header comments in the * code, or in special API example files. * + * Module overviews and other topic pages can also be provided as configurable + * help topics, using the \Drupal\help\Entity\Help config entity, outside of + * this hook. See the Help class documentation header for details. + * * The page-specific help information provided by this hook appears as a system - * help block on that page. The module overview help information is displayed - * by the Help module. It can be accessed from the page at admin/help or from - * the Extend page. + * help block on that page. The module overview help and configurable help + * topics are displayed by the Help module; topics are listed on admin/help. * * For detailed usage examples of: * - Module overview help, see content_translation_help(). Module overview diff --git a/core/modules/system/system.tokens.inc b/core/modules/system/system.tokens.inc index ff78335..6133653 100644 --- a/core/modules/system/system.tokens.inc +++ b/core/modules/system/system.tokens.inc @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\Xss; +use Drupal\Core\Url; /** * Implements hook_token_info(). @@ -22,7 +23,10 @@ function system_token_info() { 'name' => t("Dates"), 'description' => t("Tokens related to times and dates."), ); - + $types['route'] = array( + 'name' => t("Route information"), + 'description' => t("Tokens for route names."), + ); // Site-wide global tokens. $site['name'] = array( 'name' => t("Name"), @@ -75,11 +79,25 @@ function system_token_info() { 'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)), ); + $routes = array(); + /* @var \Symfony\Component\Routing\Route $route */ + foreach (\Drupal::service('router.route_provider')->getAllRoutes() as $route_name => $route) { + if (strpos($route->getPath(), '}') !== FALSE) { + // Route tokens don't support route parameters. + continue; + } + $routes[$route_name] = array( + 'name' => t('URL to !route_name', ['!route_name' => $route_name]), + 'description' => t('The URL to the !route_name route.', array('!route_name' => $route_name)), + ); + } + return array( 'types' => $types, 'tokens' => array( 'site' => $site, 'date' => $date, + 'route' => $routes, ), ); } @@ -124,7 +142,10 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a break; case 'url-brief': - $replacements[$original] = preg_replace(array('!^https?://!', '!/$!'), '', \Drupal::url('', array(), $url_options)); + $replacements[$original] = preg_replace(array( + '!^https?://!', + '!/$!' + ), '', \Drupal::url('', array(), $url_options)); break; case 'login-url': @@ -173,5 +194,20 @@ function system_tokens($type, $tokens, array $data = array(), array $options = a } } + elseif ($type == 'route') { + foreach ($tokens as $route_name => $original) { + try { + $url = Url::fromRoute($route_name)->toString(); + } + catch (\Exception $e) { + // Invalid route, do nothing. + $url = FALSE; + } + if ($url) { + $replacements[$original] = $url; + } + } + } + return $replacements; }