diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 0867a84..d69df14 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -123,6 +123,10 @@ Comment - Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan - Andrey Postnikov 'andypost' https://www.drupal.org/u/andypost +Configurable Help +- Amber Matz 'Amber Himes Matz' https://www.drupal.org/u/amber-himes-matz +- Andrey Postnikov 'andypost' https://www.drupal.org/u/andypost + Configuration API - Alex Pott 'alexpott' https://www.drupal.org/u/alexpott - Matthew Tift 'mtift' https://www.drupal.org/u/mtift diff --git a/core/composer.json b/core/composer.json index 31bc51a..3d86c63 100644 --- a/core/composer.json +++ b/core/composer.json @@ -67,6 +67,7 @@ "drupal/color": "self.version", "drupal/comment": "self.version", "drupal/config": "self.version", + "drupal/config_help": "self.version", "drupal/config_translation": "self.version", "drupal/contact": "self.version", "drupal/content_moderation": "self.version", diff --git a/core/modules/config_help/config/install/config_help.topic.config_basic.yml b/core/modules/config_help/config/install/config_help.topic.config_basic.yml new file mode 100644 index 0000000..4b6b45e --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.config_basic.yml @@ -0,0 +1,42 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: config_basic +label: 'Changing basic site settings' +top_level: true +locked: true +related: { } +list_on: { } +body: + - + text: 'The settings for your site are configured on various administrative pages, as follows:' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Site name, slogan, and email address' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'On the Basic site settings page, which you can reach in the main Manage administrative menu, by navigating to Configuration > System > Basic site settings.' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'Time zone and country' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'On the Regional settings page, which you can reach in the main Manage administrative menu, by navigating to Configuration > Regional and language > Regional settings.' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'Date and time formats' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'On the Date and time formats page, which you can reach in the main Manage administrative menu, by navigating to Configuration > Regional and language > Date and time formats.' + prefix_tags: '
' + suffix_tags: '
' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.config_error.yml b/core/modules/config_help/config/install/config_help.topic.config_error.yml new file mode 100644 index 0000000..483e203 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.config_error.yml @@ -0,0 +1,44 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: config_error +label: 'Configuring error responses, including 403/404 pages' +top_level: false +locked: true +related: + - config_basic + - maintenance +list_on: + - config_basic + - maintenance + - menu_overview + - security +body: + - + text: 'Configuring 403/404 pages' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'The core software provides default responses for 403 response (Not Authorized: when someone tries to visit a page they do not have permission to see) and 404 response (Not Found: when someone tries to visit a page that does not exist). You can change what page is displayed for these responses on the Basic site settings page, which you can reach in the main Manage administrative menu, by navigating to Configuration > System > Basic site settings. Note that the pages you want to use must already exist as either system-provided pages or content that you have created.' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Responding to software errors' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Software errors on your site are logged, if you have a logging module installed (such as the core Database Logging module or the core Syslog module). You can configure whether or not error messages are also shown (to both administrators and other site visitors) on the Logging and errors configuration page, which you can reach in the main Manage administrative menu, by navigating to Configuration > Development > Logging and errors.' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Viewing the site log' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'If you have the core Database Logging module installed, you can view recent error and informational messages by navigating in the main Manage administrative menu to Reports > Recent log messages. If you are using the core Syslog module for logging, error messages will be logged in your web server''s log files.' + prefix_tags: '

' + suffix_tags: '

' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.config_help.yml b/core/modules/config_help/config/install/config_help.topic.config_help.yml new file mode 100644 index 0000000..2488e41 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.config_help.yml @@ -0,0 +1,40 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: config_help +label: 'Building a help system' +top_level: true +locked: true +related: + - config_help_form + - config_help_writing +list_on: { } +body: + - + text: 'Follow these steps to build a help system for different types/roles of users on your system to use:' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Plan your help system: make a list of the topics each type/role of user would benefit from.' + prefix_tags: '
  1. ' + suffix_tags: '
  2. ' + - + text: 'On the Help topics administration page, see which topics already exist (some topics may be provided by the core software or contributed modules and themes).' + prefix_tags: '
  3. ' + suffix_tags: '
  4. ' + - + text: 'Use the Add new help topic button to add the missing topics. Be sure to make links between related topics, but be careful not to make links that would take people to topics not relevant to their roles and permissions on the site. See the Help topic editing form topic for more information about the editing form, and the Writing good help topic for suggestions on making good topics.' + prefix_tags: '
  5. ' + suffix_tags: '
  6. ' + - + text: 'For each role on your site that needs a help system, make a top-level topic with a title like "Help for content editors" (for the content editor role, in this case). Leave the body of this topic blank (or maybe provide a short introduction to what the role''s responsibilities are). List all the topics that the person needs to see in the Related field.' + prefix_tags: '
  7. ' + suffix_tags: '
  8. ' + - + text: 'Make sure that each role has the View configured help topics permission, so that they will be able to see their help pages. Each top-level topic you add will be visible on the main Help page.' + prefix_tags: '
  9. ' + suffix_tags: '
' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.config_help_form.yml b/core/modules/config_help/config/install/config_help.topic.config_help_form.yml new file mode 100644 index 0000000..f377a65 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.config_help_form.yml @@ -0,0 +1,48 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: config_help_form +label: 'Help topic editing form' +top_level: false +locked: true +related: + - config_help + - config_help_writing +list_on: { } +body: + - + text: 'You can access the help topic editing form by adding a new configurable help topic or editing an existing help topic, from the Help topics administration page. Here is a list of the fields on the form (fields that are similar to those found on other forms, like Title and Machine name, are not described in this list):' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Top-level topic' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'To reduce clutter, only the topics that have this field checked are listed on the main Help page. Generally, the top-level topics should have one or more Related topics listed on their pages, so that they will lead the viewer to additional help.' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'Related topics and Topics to list this topic on' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'Use these two fields to make links between topics. If topic A lists topic B in Related topics, then someone viewing topic A will see a link to topic B in the Related topics section at the bottom. If topic A lists topic B in Topics to list this topic on, then someone viewing topic B will see a link to topic A in the Related topics section at the bottom.' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'You can use either of these fields to make links between topics. In general, Related topics is preferred. However, you may want to use Topics to list this topic on instead if you are creating a custom topics for your site, and the topic you want the link to appear on was provided by a module (you might want to avoid editing the module-provided topic, for easier updates). Another scenario would be that you are a module developer and are writing topics to provide as part of your module, and the topic you want to add a link to is not part of your module (so you cannot edit it), or has more module dependencies (so it may not be present on a given site).' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'Module and theme dependencies' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'These fields are primarily useful for module and theme developers who are writing topics that they plan to export and provide as configuration with their module or theme. Filling in the proper module and theme dependencies will ensure that the help topic is only added to a site''s configuration if all of the module or theme dependencies are present on the site. Leave these fields blank if you are writing custom topics for a site.' + prefix_tags: '
' + suffix_tags: '
' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.config_help_writing.yml b/core/modules/config_help/config/install/config_help.topic.config_help_writing.yml new file mode 100644 index 0000000..ac84f92 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.config_help_writing.yml @@ -0,0 +1,36 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: config_help_writing +label: 'Writing good help' +top_level: false +locked: true +related: + - config_help + - config_help_form +list_on: { } +body: + - + text: 'Here are some suggestions for how to make your help topics as useful as possible for readers:' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Choose short titles. If the topic describes a task, start with a verb in -ing form, like "Building a help system".' + prefix_tags: '' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.maintenance.yml b/core/modules/config_help/config/install/config_help.topic.maintenance.yml new file mode 100644 index 0000000..fa35de6 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.maintenance.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: maintenance +label: 'Maintaining and troubleshooting your site' +top_level: true +locked: true +related: { } +list_on: { } +body: + - + text: 'The related topics listed here will help you keep your site running and troubleshoot problems.' + prefix_tags: '

' + suffix_tags: '

' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.menu_overview.yml b/core/modules/config_help/config/install/config_help.topic.menu_overview.yml new file mode 100644 index 0000000..6d29ae1 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.menu_overview.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: menu_overview +label: 'Defining navigation and URLs' +top_level: true +locked: true +related: { } +list_on: { } +body: + - + text: 'The related topics listed here describe how to set up various aspects of site navigation and URLs.' + prefix_tags: '

' + suffix_tags: '

' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.security.yml b/core/modules/config_help/config/install/config_help.topic.security.yml new file mode 100644 index 0000000..d44e373 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.security.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: security +label: 'Making your site secure' +top_level: true +locked: true +related: { } +list_on: { } +body: + - + text: 'The topics listed here will help you make and keep your site secure.' + prefix_tags: '

' + suffix_tags: '

' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.security_account_settings.yml b/core/modules/config_help/config/install/config_help.topic.security_account_settings.yml new file mode 100644 index 0000000..6261d2e --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.security_account_settings.yml @@ -0,0 +1,37 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: security_account_settings +label: 'Defining how user accounts are created' +top_level: false +locked: true +related: + - security +list_on: + - config_basic + - security +body: + - + text: 'On the Account settings page, which you can reach from the Manage administrative menu, by navigating to Configuration > People > Account settings (requires the Administer account settings permission), you can configure several settings related to how user accounts are created:' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'You can make it possible for new users to register themselves, with or without administrator approval. Or, you can make it so only administrators with Administer users permission can register new users.' + prefix_tags: '' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.ui_accessibility.yml b/core/modules/config_help/config/install/config_help.topic.ui_accessibility.yml new file mode 100644 index 0000000..608dcf3 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.ui_accessibility.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: ui_accessibility +label: 'Accessibility features' +top_level: false +locked: true +related: + - ui_components +list_on: + - ui_components +body: + - + text: 'The following features of the administrative user interface may help administrative users with disabilities access your site:' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Disabling drag-and-drop functionality' + prefix_tags: '
' + suffix_tags: '
' + - + text: 'The default drag-and-drop user interface for ordering tables in the administrative interface presents a challenge for some users, including users of screen readers and other assistive technology. The drag-and-drop interface can be disabled in a table by clicking a link labeled Show row weights above the table. The replacement interface allows users to order the table by choosing numerical weights instead of dragging table rows.' + prefix_tags: '
' + suffix_tags: '
' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.ui_components.yml b/core/modules/config_help/config/install/config_help.topic.ui_components.yml new file mode 100644 index 0000000..e864d25 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.ui_components.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: ui_components +label: 'Using the administrative interface' +top_level: true +locked: true +related: { } +list_on: { } +body: + - + text: 'The related topics listed here describe various aspects of the administrative interface, and tell how to use them.' + prefix_tags: '

' + suffix_tags: '

' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.ui_contextual.yml b/core/modules/config_help/config/install/config_help.topic.ui_contextual.yml new file mode 100644 index 0000000..acbecc5 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.ui_contextual.yml @@ -0,0 +1,44 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: ui_contextual +label: 'Contextual links' +top_level: false +locked: true +related: + - ui_components +list_on: + - ui_components +body: + - + text: 'What are contextual links?' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Contextual links give users with the Use contextual links permission quick access to administrative tasks related to areas of non-administrative pages. For example, if a page on your site displays a block, the block would have a contextual link that would allow users with permission to configure the block. If the block contains a menu or a view, it would also have a contextual link for editing the menu links or the view. Clicking a contextual link takes you to the related administrative page directly, without needing to navigate through the administrative menu system.' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Displaying and using contextual links' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'If you have the core Contextual Links module installed, the contextual links related to an area on a page can be displayed by clicking the contextual links button in that area of the page. In most themes, this button looks like a pencil and is placed in the upper right corner of the page area (upper left for right-to-left languages); however, contextual links buttons are normally hidden. Here are two ways to make contextual links buttons visible:' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Hovering your mouse over an area on the page will temporarily make the contextual links button visible, if there is one for that area of the page. Also, in most themes, the page area that the contextual links pertain to will be outlined while you are hovering.' + prefix_tags: '' + - + text: 'While the contextual links button for the area of interest is visible, click the button to display the list of links for that area. Click a link in the list to perform the task.' + prefix_tags: '

' + suffix_tags: '

' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.ui_shortcuts.yml b/core/modules/config_help/config/install/config_help.topic.ui_shortcuts.yml new file mode 100644 index 0000000..001f69a --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.ui_shortcuts.yml @@ -0,0 +1,40 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: ui_shortcuts +label: Shortcuts +top_level: false +locked: true +related: + - ui_components +list_on: + - ui_components +body: + - + text: 'What are shortcuts?' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Shortcuts are quick links to administrative pages; they are managed by the core Shortcut module. A site can have one or more shortcut sets, which can be shared by one or more users; each set contains one or more shortcuts. Users need Use shortcuts permission to view shortcuts; Edit current shortcut set permission to add, delete, or edit the shortcuts in the set assigned to them; and Select any shortcut set permission to select a different shortcut set when editing their user profile. There is also an Administer shortcuts permission, which allows an administrator to do any of these actions, and also permits assigning shortcut sets to other users.' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Creating and deleting shortcuts' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'When viewing certain administrative pages, you will see a link that allows you to add the page to your current shortcut set. In the core Seven administrative theme, the link looks like a star, and is displayed next to the page title. If the page is already in your shortcut set, you will instead see a link that allows you to remove it.' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Viewing and using shortcuts' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'If you have the core Toolbar module installed, click Shortcuts in the toolbar to display your shortcuts. Once they are displayed, click any link in the shortcut bar to go directly to the administrative page. If you are not using the Toolbar module, you can display shortcuts by placing the Shortcuts block in a region of your theme.' + prefix_tags: '

' + suffix_tags: '

' +body_format: help diff --git a/core/modules/config_help/config/install/config_help.topic.ui_tours.yml b/core/modules/config_help/config/install/config_help.topic.ui_tours.yml new file mode 100644 index 0000000..ec45443 --- /dev/null +++ b/core/modules/config_help/config/install/config_help.topic.ui_tours.yml @@ -0,0 +1,32 @@ +langcode: en +status: true +dependencies: + enforced: + module: { } + theme: { } +id: ui_tours +label: Tours +top_level: false +locked: true +related: + - ui_components +list_on: + - ui_components +body: + - + text: 'What are tours?' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'The core Tour module provides users with tours, which are guided tours of the administrative interface. Each tour starts on a particular administrative page, and consists of one or more tips that highlight elements of the page, guide you through a workflow, or explain key concepts. Users need Access tour permission to view tours, and JavaScript must be enabled in their browsers.' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'Viewing tours' + prefix_tags: '

' + suffix_tags: '

' + - + text: 'If a tour is available on a page, and you have the core Toolbar module installed, a Tour button will appear on the right end of the toolbar (left end for right-to-left languages). Click this button to view the first tip of the tour; click the Next button to advance to the next tip, and End tour at the end to close the tour.' + prefix_tags: '

' + suffix_tags: '

' +body_format: help diff --git a/core/modules/config_help/config/install/filter.format.help.yml b/core/modules/config_help/config/install/filter.format.help.yml new file mode 100644 index 0000000..274682a --- /dev/null +++ b/core/modules/config_help/config/install/filter.format.help.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - config_help +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/config_help/config/optional/editor.editor.help.yml b/core/modules/config_help/config/optional/editor.editor.help.yml
      new file mode 100644
      index 0000000..66c2f58
      --- /dev/null
      +++ b/core/modules/config_help/config/optional/editor.editor.help.yml
      @@ -0,0 +1,52 @@
      +langcode: en
      +status: true
      +dependencies:
      +  config:
      +    - filter.format.help
      +  module:
      +    - ckeditor
      +format: help
      +editor: ckeditor
      +settings:
      +  toolbar:
      +    rows:
      +      -
      +        -
      +          name: Formatting
      +          items:
      +            - Bold
      +            - Italic
      +        -
      +          name: Links
      +          items:
      +            - DrupalLink
      +            - DrupalUnlink
      +        -
      +          name: Lists
      +          items:
      +            - BulletedList
      +            - NumberedList
      +            - Indent
      +            - Outdent
      +            - Table
      +        -
      +          name: Media
      +          items:
      +            - DrupalImage
      +        -
      +          name: Tools
      +          items:
      +            - Source
      +  plugins:
      +    stylescombo:
      +      styles: ''
      +    language:
      +      language_list: un
      +image_upload:
      +  status: false
      +  scheme: public
      +  directory: inline-images
      +  max_size: ''
      +  max_dimensions:
      +    width: null
      +    height: null
      diff --git a/core/modules/config_help/config/schema/config_help.schema.yml b/core/modules/config_help/config/schema/config_help.schema.yml
      new file mode 100644
      index 0000000..576ca10
      --- /dev/null
      +++ b/core/modules/config_help/config/schema/config_help.schema.yml
      @@ -0,0 +1,47 @@
      +config_help.topic.*:
      +  type: config_entity
      +  label: 'Help topic'
      +  mapping:
      +    id:
      +      type: string
      +      label: 'Machine-readable name'
      +    label:
      +      type: label
      +      label: 'Title'
      +    top_level:
      +      type: boolean
      +      label: 'Top-level topic'
      +    locked:
      +      type: boolean
      +      label: 'Locked'
      +    related:
      +      type: sequence
      +      label: 'Related topics'
      +      sequence:
      +        type: string
      +    list_on:
      +      type: sequence
      +      label: 'List on topics'
      +      sequence:
      +        type: string
      +    body:
      +      type: sequence
      +      label: 'Body'
      +      sequence:
      +        type: config_help_text
      +    body_format:
      +      type: string
      +      label: 'Body format'
      +
      +config_help_text:
      +  type: mapping
      +  mapping:
      +    prefix_tags:
      +      type: string
      +      label: 'Prefix tags'
      +    text:
      +      type: text
      +      label: 'Text'
      +    suffix_tags:
      +      type: string
      +      label: 'Suffix tags'
      diff --git a/core/modules/config_help/config_help.info.yml b/core/modules/config_help/config_help.info.yml
      new file mode 100644
      index 0000000..fbbffad
      --- /dev/null
      +++ b/core/modules/config_help/config_help.info.yml
      @@ -0,0 +1,10 @@
      +name: Configurable Help
      +type: module
      +description: 'Provides a configurable help system'
      +core: 8.x
      +package: Core (Experimental)
      +configure: entity.help_topic.collection
      +version: VERSION
      +dependencies:
      +  - drupal:help
      +  - drupal:filter
      diff --git a/core/modules/config_help/config_help.links.action.yml b/core/modules/config_help/config_help.links.action.yml
      new file mode 100644
      index 0000000..b4432d3
      --- /dev/null
      +++ b/core/modules/config_help/config_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:
      +    - entity.help_topic.collection
      diff --git a/core/modules/config_help/config_help.links.menu.yml b/core/modules/config_help/config_help.links.menu.yml
      new file mode 100644
      index 0000000..f62c4fb
      --- /dev/null
      +++ b/core/modules/config_help/config_help.links.menu.yml
      @@ -0,0 +1,5 @@
      +entity.help_topic.collection:
      +  title: Help topics
      +  description: Add, delete, and edit help topics.
      +  route_name: entity.help_topic.collection
      +  parent: system.admin_config_development
      diff --git a/core/modules/config_help/config_help.links.task.yml b/core/modules/config_help/config_help.links.task.yml
      new file mode 100644
      index 0000000..4dafd75
      --- /dev/null
      +++ b/core/modules/config_help/config_help.links.task.yml
      @@ -0,0 +1,16 @@
      +entity.help_topic.canonical:
      +  title: View
      +  route_name: entity.help_topic.canonical
      +  base_route: entity.help_topic.edit_form
      +  weight: -10
      +
      +entity.help_topic.edit_form:
      +  title: Edit
      +  route_name: entity.help_topic.edit_form
      +  base_route: entity.help_topic.edit_form
      +
      +entity.help_topic.delete_form:
      +  route_name: entity.help_topic.delete_form
      +  base_route: entity.help_topic.edit_form
      +  title: Delete
      +  weight: 10
      diff --git a/core/modules/config_help/config_help.module b/core/modules/config_help/config_help.module
      new file mode 100644
      index 0000000..b7e2319
      --- /dev/null
      +++ b/core/modules/config_help/config_help.module
      @@ -0,0 +1,48 @@
      +' . t('About') . '';
      +      $output .= '

      ' . t('The Configurable Help module adds configurable help topics to the module-provided help from the core Help module. For more information, see the online documentation for the Configurable Help module.', [':online' => 'https://www.drupal.org/modules/config_help']) . '

      '; + $output .= '

      ' . t('Uses') . '

      '; + $output .= '
      '; + $output .= '
      ' . t('Configuring help topics') . '
      '; + $output .= '
      ' . t('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. See the Building a help system topic for more information on configuring help topics.', [ + ':topic_admin' => Url::fromRoute('entity.help_topic.collection')->toString(), + ':main_topic' => Url::fromRoute('entity.help_topic.canonical', ['help_topic' => 'config_help'])->toString(), + ]) . '
      '; + $output .= '
      ' . t('Viewing configurable help topics') . '
      '; + $output .= '
      ' . t('The top-level configured help topics are listed on the main Help page.', [':help_page' => Url::fromRoute('help.main')->toString()]) . '
      '; + $output .= '
      ' . t('Updating help topics') . '
      '; + $output .= '
      ' . t('Help topics provided by modules and themes may be updated when a module or theme is updated, or new topics may be added. However, help topics are only imported into your site configuration when you first install the module or theme. The contributed Configuration Update Manager module can be used to check for updates, see differences, and import or update topics that have been added or have changed. It is advisable not to edit module- or theme-provided topics, to make updates easier.', [':config_update' => 'https://www.drupal.org/project/config_update']) . '
      '; + $output .= '
      '; + return ['#markup' => $output]; + } +} + +/** + * Implements hook_theme(). + */ +function config_help_theme($existing, $type, $theme, $path) { + return [ + 'help_topic' => [ + 'variables' => [ + 'body' => [], + 'related' => [], + ], + ], + ]; +} diff --git a/core/modules/config_help/config_help.permissions.yml b/core/modules/config_help/config_help.permissions.yml new file mode 100644 index 0000000..c072b6e --- /dev/null +++ b/core/modules/config_help/config_help.permissions.yml @@ -0,0 +1,7 @@ +administer help topics: + title: 'Administer configured help topics' + description: 'Create, edit, and delete unlocked help topics' +view help topics: + title: 'View configured help topics' +administer help topic locking: + title: 'Lock and unlock configured help topics' diff --git a/core/modules/config_help/config_help.routing.yml b/core/modules/config_help/config_help.routing.yml new file mode 100644 index 0000000..122da2b --- /dev/null +++ b/core/modules/config_help/config_help.routing.yml @@ -0,0 +1,20 @@ +config_help.topic_autocomplete: + path: '/config-help/autocomplete-topic' + defaults: + _controller: '\Drupal\config_help\Controller\AutocompleteController::topicAutocomplete' + requirements: + _permission: 'administer help topics' + +config_help.module_autocomplete: + path: '/config-help/autocomplete-module' + defaults: + _controller: '\Drupal\config_help\Controller\AutocompleteController::moduleAutocomplete' + requirements: + _permission: 'administer help topics' + +config_help.theme_autocomplete: + path: '/config-help/autocomplete-theme' + defaults: + _controller: '\Drupal\config_help\Controller\AutocompleteController::themeAutocomplete' + requirements: + _permission: 'administer help topics' diff --git a/core/modules/config_help/config_help.services.yml b/core/modules/config_help/config_help.services.yml new file mode 100644 index 0000000..07ec438 --- /dev/null +++ b/core/modules/config_help/config_help.services.yml @@ -0,0 +1,6 @@ +services: + config_help.breadcrumb: + class: Drupal\config_help\HelpBreadcrumbBuilder + arguments: ['@string_translation'] + tags: + - { name: breadcrumb_builder, priority: 900 } diff --git a/core/modules/config_help/config_help.tokens.inc b/core/modules/config_help/config_help.tokens.inc new file mode 100644 index 0000000..01c8277 --- /dev/null +++ b/core/modules/config_help/config_help.tokens.inc @@ -0,0 +1,81 @@ + t("Route information"), + 'description' => t("Tokens based on route names."), + ]; + + $routes = []; + $routes['url'] = [ + 'name' => t('URL'), + 'description' => t('The URL to the route. Provide the route name as route:url:ROUTE_NAME'), + ]; + + $types['help_topic'] = [ + 'name' => t("Help Topics"), + 'description' => t("Tokens for help topics."), + ]; + + $topics = []; + $topics['url'] = [ + 'name' => t('URL'), + 'description' => t('The URL to the topic. Provide the topic machine name as help_topic:url:MACHINE_NAME'), + ]; + + return [ + 'types' => $types, + 'tokens' => [ + 'help_topic' => $topics, + 'route' => $routes, + ], + ]; +} + +/** + * Implements hook_tokens(). + */ +function config_help_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + $replacements = []; + $token_service = \Drupal::token(); + + if ($type == 'help_topic') { + $topics = $token_service->findWithPrefix($tokens, 'url'); + foreach ($topics as $name => $original) { + if ($topic = HelpTopic::load($name)) { + $replacements[$original] = $topic->url('canonical'); + $bubbleable_metadata->addCacheableDependency($topic); + } + } + } + elseif ($type == 'route') { + $routes = $token_service->findWithPrefix($tokens, 'url'); + foreach ($routes as $route_name => $original) { + try { + $url = Url::fromRoute($route_name)->toString(); + } + catch (\Exception $e) { + // Invalid route or missing parameters or something like that. + // Do nothing. + $url = FALSE; + } + if ($url) { + $replacements[$original] = $url; + } + } + } + + return $replacements; +} diff --git a/core/modules/config_help/src/Controller/AutocompleteController.php b/core/modules/config_help/src/Controller/AutocompleteController.php new file mode 100644 index 0000000..3990c20 --- /dev/null +++ b/core/modules/config_help/src/Controller/AutocompleteController.php @@ -0,0 +1,214 @@ +storage = $entity_type_manager->getStorage('help_topic'); + $this->moduleHandler = $module_handler; + $this->themeHandler = $theme_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('module_handler'), + $container->get('theme_handler') + ); + } + + /** + * Retrieves suggestions for help topic autocomplete. + * + * The autocomplete suggestions search for matches by topic title and machine + * name, and are returned in a JSON response for use in an edit form field. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * A JSON response containing the autocomplete suggestions. + */ + public function topicAutocomplete(Request $request) { + $matches = []; + if ($input = $request->query->get('q')) { + $input = Tags::explode($input); + $input = Unicode::strtolower(array_pop($input)); + $query = $this->storage->getQuery(); + // Find matching topics by machine name or title. + $group = $query->orConditionGroup() + ->condition('id', $input, 'CONTAINS') + ->condition('label', $input, 'CONTAINS'); + $ids = $query + ->condition($group) + ->range(0, 10) + ->sort('label') + ->execute(); + + if (!empty($ids)) { + $topics = $this->storage->loadMultiple($ids); + foreach ($topics as $topic) { + $matches[] = $this->getMatch($topic->id(), $topic->label()); + } + } + } + + return new JsonResponse($matches); + } + + /** + * Retrieves suggestions for module autocomplete. + * + * The autocomplete suggestions search for matches by module displayed and + * machine name, and they are returned in a JSON response for use in an edit + * form field. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * A JSON response containing the autocomplete suggestions. + */ + public function moduleAutocomplete(Request $request) { + $matches = []; + if ($input = $request->query->get('q')) { + $input = Tags::explode($input); + $input = Unicode::strtolower(array_pop($input)); + + // Return only first 10 matches. + $limit = 10; + foreach ($this->getActiveModules() as $name => $label) { + // Add as a match if the typed text matches machine name or displayed + // name. + if (stripos($name, $input) !== FALSE || stripos($label, $input) !== FALSE) { + $matches[] = $this->getMatch($name, $label); + if (!--$limit) { + break; + } + } + } + } + + return new JsonResponse($matches); + } + + /** + * Retrieves suggestions for theme autocomplete. + * + * The autocomplete suggestions search for matches by theme displayed and + * machine name, and they are returned in a JSON response for use in an edit + * form field. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * A JSON response containing the autocomplete suggestions. + */ + public function themeAutocomplete(Request $request) { + $matches = []; + if ($input = $request->query->get('q')) { + $input = Tags::explode($input); + $input = Unicode::strtolower(array_pop($input)); + // There is not an obvious way to query installed themes. So make + // a list of all themes, and filter it down to ones that match. + $installed = $this->themeHandler->listInfo(); + + // Return only first 10 matches. + $limit = 10; + foreach ($installed as $name => $extension) { + $label = $extension->info['name']; + // Add as a match if the typed text matches machine name or displayed + // name. + if (strpos($name, $input) !== FALSE || strpos($label, $input) !== FALSE) { + $matches[] = $this->getMatch($name, $label); + if (!--$limit) { + break; + } + } + } + } + + return new JsonResponse($matches); + } + + /** + * Builds structure to display in autocomplete dropdown. + * + * @param string $value + * Machine name part. + * @param string $label + * Human readable part. + * + * @return array + * An array of matched labels, in the format required by the Ajax + * autocomplete API (array('value' => $value, 'label' => $label)). + */ + protected function getMatch($value, $label) { + return [ + 'value' => $value, + 'label' => new HtmlEscapedText("$label ($value)"), + ]; + } + + /** + * Returns list of active modules' names. + * + * @return array + * An associative array of active modules' human names keyed by module name. + */ + protected function getActiveModules() { + $installed = array_keys($this->moduleHandler->getModuleList()); + $info = system_get_info('module'); + $modules = []; + foreach ($installed as $extension) { + $modules[$extension] = $info[$extension]['name']; + } + return $modules; + } + +} diff --git a/core/modules/config_help/src/Entity/HelpTopic.php b/core/modules/config_help/src/Entity/HelpTopic.php new file mode 100644 index 0000000..daa019a --- /dev/null +++ b/core/modules/config_help/src/Entity/HelpTopic.php @@ -0,0 +1,458 @@ +get('body') as $chunk) { + $body_text .= $chunk['prefix_tags'] . $chunk['text'] . $chunk['suffix_tags']; + } + return $body_text; + } + + /** + * {@inheritdoc} + */ + public function setBody($body) { + // Chunk the passed-in string and save it as an array. + $dom = Html::load($body); + if (!$dom) { + throw new ConfigValueException($this->t('Body HTML is malformed')); + } + $chunks = []; + $body_node = $dom->getElementsByTagName('body')->item(0); + if ($body_node) { + $chunks = $this->chunkDomNode($dom, $body_node); + } + return $this->set('body', $chunks); + } + + /** + * {@inheritdoc} + */ + public function getBodyFormat() { + return $this->get('body_format'); + } + + /** + * {@inheritdoc} + */ + public function setBodyFormat($format) { + return $this->set('body_format', $format); + } + + /** + * {@inheritdoc} + */ + public function isTopLevel() { + return $this->get('top_level'); + } + + /** + * {@inheritdoc} + */ + public function setTopLevel($top_level) { + return $this->set('top_level', $top_level); + } + + /** + * {@inheritdoc} + */ + public function isLocked() { + return $this->get('locked'); + } + + /** + * {@inheritdoc} + */ + public function setLocked($locked = TRUE) { + return $this->set('locked', $locked); + } + + /** + * {@inheritdoc} + */ + public function getRelated() { + return $this->get('related'); + } + + /** + * {@inheritdoc} + */ + public function setRelated(array $topics) { + return $this->set('related', $topics); + } + + /** + * {@inheritdoc} + */ + public function getListOn() { + return $this->get('list_on'); + } + + /** + * {@inheritdoc} + */ + public function setListOn(array $topics) { + return $this->set('list_on', $topics); + } + + /** + * {@inheritdoc} + */ + public function getEnforcedDependencies() { + return [ + 'module' => isset($this->dependencies['enforced']['module']) ? $this->dependencies['enforced']['module'] : [], + 'theme' => isset($this->dependencies['enforced']['theme']) ? $this->dependencies['enforced']['theme'] : [], + ]; + } + + /** + * {@inheritdoc} + */ + public function setEnforcedDependencies(array $dependencies) { + foreach (['module', 'theme'] as $key) { + if (empty($dependencies[$key])) { + unset($this->dependencies['enforced'][$key]); + } + else { + $this->dependencies['enforced'][$key] = $dependencies[$key]; + } + } + + return $this; + } + + /** + * Makes a cache tag from a help topic ID. + * + * @param string $id + * The ID to make a cache tag from. + * + * @return string + * The cache tag that + * \Drupal\Core\Config\Entity\ConfigEntityBase::getCacheTagsToInvalidate() + * would return for the given ID, which matches the config object's name. + */ + protected function makeCacheTag($id) { + return 'config:' . $this->getEntityType()->getConfigPrefix() . '.' . $id; + } + + /** + * {@inheritdoc} + */ + public function getCacheTagsToInvalidate() { + // Get the standard bare cache tags. + $tags = parent::getCacheTagsToInvalidate(); + + // In addition to the standard entity tags, add the tags for entities + // this topic is listed on, so that when we edit or delete this entity, + // those others will also have their render caches invalidated. + foreach ($this->list_on as $topic) { + $tags[] = $this->makeCacheTag($topic); + } + + return $tags; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + // Get the standard bare entity cache tags. + $tags = parent::getCacheTagsToInvalidate(); + + // In addition to the standard entity tags, add the tags for entities + // listed on this topic, so that if they are edited or deleted, this one + // will have its render cache invalidated. + foreach ($this->related as $topic) { + $tags[] = $this->makeCacheTag($topic); + } + + return $tags; + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // Invalidate cache tags for topics this one was previously listed on. + if (!$this->isNew()) { + $original = $storage->loadUnchanged($this->getOriginalId()); + $tags = []; + foreach ($original->list_on as $topic) { + $tags[] = $this->makeCacheTag($topic); + } + if (count($tags)) { + Cache::invalidateTags($tags); + } + } + } + + /** + * Splits a DOM node recursively into chunks. + * + * @param \DOMDocument $dom + * The DOM document this came from. + * @param \DOMNode $node + * DOM node to split into chunks. + * @param string $prefix + * (optional) Prefix tags to put on first chunk. + * @param string $suffix + * (optional) Suffix tags to put on last chunk. + * + * @return array + * Array of chunks from $node. Each chunk is an array with elements: + * - text: Text content of the chunk, which may contain HTML. + * - prefix_tags: HTML tags that go before the text content. + * - suffix_tags: HTML tags that go after the text content. + * The intent is that if you concatenate all the chunks' prefix_tags, + * text, and suffix_tags, you will end up with the original HTML. + */ + protected function chunkDomNode(\DOMDocument $dom, \DOMNode $node, $prefix = '', $suffix = '') { + + // These HTML tags generate an automatic chunk. + $chunk_tags = [ + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'dt', + 'dd', + 'code', + 'pre', + 'blockquote', + 'td', + 'th', + 'caption', + ]; + + $chunks = []; + foreach ($node->childNodes as $node) { + switch($node->nodeType) { + case XML_ELEMENT_NODE: + if (!$node->hasChildNodes()) { + // It's an empty element, so just add it to the prefix for the + // next chunk. + $prefix .= $dom->saveXML($node); + } + else { + $open_tag = '<' . $node->tagName; + if ($node->hasAttributes()) { + foreach ($node->attributes as $attr) { + $open_tag .= $dom->saveXML($attr); + } + } + $open_tag .= '>'; + $close_tag = 'tagName . '>'; + + if (in_array($node->tagName, $chunk_tags)) { + // Don't go deeper, just save everything this node contains + // as one chunk. + $text = ''; + foreach ($node->childNodes as $inner) { + $text .= $dom->saveXML($inner); + } + $chunks[] = [ + 'text' => $text, + 'prefix_tags' => $prefix . $open_tag, + 'suffix_tags' => $close_tag, + ]; + $prefix = ''; + } + else { + // Recursively generate chunks from this node's children. + $new_chunks = $this->chunkDomNode($dom, $node, $prefix . $open_tag, $close_tag); + $chunks = array_merge($chunks, $new_chunks); + $prefix = ''; + } + } + break; + + case XML_TEXT_NODE: + // Add text nodes as their own chunks. + $chunks[] = [ + 'text' => $dom->saveXML($node), + 'prefix_tags' => $prefix, + 'suffix_tags' => '', + ]; + $prefix = ''; + break; + + default: + // For nodes that are anything except elements or text, just add + // them to the prefix we are working on. These are things like HTML + // comments, for example. + $prefix .= $dom->saveXML($node); + break; + } + } + + // If we have prefix left over, add it as a chunk. + if ($prefix) { + $chunks[] = [ + 'text' => '', + 'prefix_tags' => $prefix, + 'suffix_tags' => '', + ]; + } + + // If we have a suffix left over, add it to the last chunk. + if ($suffix) { + if (!$chunks) { + $chunks[] = [ + 'text' => '', + 'prefix_tags' => '', + 'suffix_tags' => '', + ]; + } + $chunks[count($chunks) - 1]['suffix_tags'] .= $suffix; + } + + return $chunks; + } + +} diff --git a/core/modules/config_help/src/Form/HelpLockForm.php b/core/modules/config_help/src/Form/HelpLockForm.php new file mode 100644 index 0000000..efa0094 --- /dev/null +++ b/core/modules/config_help/src/Form/HelpLockForm.php @@ -0,0 +1,53 @@ +t('Are you sure you want to lock the topic %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Lock'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.help_topic.collection'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->setLocked(); + $this->entity->save(); + drupal_set_message(t('Locked help topic %name.', ['%name' => $this->entity->label()])); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return '' . $this->t('Locked topics cannot be edited or deleted until they are unlocked.') . ''; + } + +} diff --git a/core/modules/config_help/src/Form/HelpTopicForm.php b/core/modules/config_help/src/Form/HelpTopicForm.php new file mode 100644 index 0000000..7c72e38 --- /dev/null +++ b/core/modules/config_help/src/Form/HelpTopicForm.php @@ -0,0 +1,279 @@ +helpStorage = $entity_type_manager->getStorage('help_topic'); + $this->moduleHandler = $module_handler; + $this->themeHandler = $theme_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('module_handler'), + $container->get('theme_handler') + ); + } + + /** + * Checks for an existing help topic. + * + * @param string $id + * The entity ID. + * + * @return bool + * TRUE if this topic already exists, FALSE otherwise. + */ + public function exists($id) { + // Use load() method to leverage entity cache. + return (bool) $this->helpStorage->load($id); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + /** @var \Drupal\config_help\HelpTopicInterface $entity */ + $entity = $this->entity; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Title'), + '#maxlength' => 100, + '#default_value' => $entity->label(), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $entity->id(), + '#machine_name' => [ + 'exists' => [$this, 'exists'], + 'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores.'), + ], + ]; + + $form['langcode'] = [ + '#type' => 'language_select', + '#default_value' => $entity->language()->getId(), + '#title' => $this->t('Language'), + '#languages' => LanguageInterface::STATE_ALL, + ]; + + $form['body'] = [ + '#type' => 'text_format', + '#title' => $this->t('Body'), + '#default_value' => $entity->getBody(), + '#format' => $entity->getBodyFormat(), + '#description' => $this->t('You can use tokens like [route:url:ROUTE_NAME] and [help_topic:url:MACHINE_NAME] to insert URLs to administrative page routes and to other help topics into the text.'), + ]; + // The filter module will deny access if the format is set to '', so unset + // to instead use the first format that the user can access as the default. + if (!$form['body']['#format']) { + unset($form['body']['#format']); + } + + $form['relationships'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Relationships and hierarchy'), + ]; + + $form['relationships']['top_level'] = [ + '#type' => 'checkbox', + '#default_value' => $entity->isTopLevel(), + '#title' => $this->t('Top-level topic'), + '#description' => $this->t('Check box if this topic should be displayed on the Help page topics list'), + ]; + + $form['relationships']['related'] = [ + '#title' => $this->t('Topics to list here'), + '#description' => $this->t("Topics to list in this topic's Related topics section. Comma-separated list of machine names."), + '#type' => 'textfield', + '#maxlength' => 5000, + '#default_value' => Tags::implode($entity->getRelated()), + '#autocomplete_route_name' => 'config_help.topic_autocomplete', + ]; + + $form['relationships']['list_on'] = [ + '#title' => $this->t('List this topic on'), + '#description' => $this->t("Topics that should have this topic listed in their Related topics sections. Comma-separated list of machine names."), + '#type' => 'textfield', + '#maxlength' => 5000, + '#default_value' => Tags::implode($entity->getListOn()), + '#autocomplete_route_name' => 'config_help.topic_autocomplete', + ]; + + $dependencies = $entity->getEnforcedDependencies(); + + $form['dependencies'] = [ + '#type' => 'details', + '#open' => FALSE, + '#title' => $this->t('Dependencies'), + '#description' => $this->t('Primarily for use by module and theme developers, for topics they plan to export to distribute with their modules or themes.'), + ]; + + $form['dependencies']['modules'] = [ + '#title' => $this->t('Module dependencies'), + '#description' => $this->t('Comma-separated list of machine names of modules this help topic depends on.'), + '#type' => 'textfield', + '#maxlength' => 5000, + '#default_value' => Tags::implode($dependencies['module']), + '#autocomplete_route_name' => 'config_help.module_autocomplete', + ]; + + $form['dependencies']['themes'] = [ + '#title' => $this->t('Theme dependencies'), + '#description' => $this->t('Comma-separated list of machine names of themes this help topic depends on.'), + '#type' => 'textfield', + '#maxlength' => 5000, + '#default_value' => Tags::implode($dependencies['theme']), + '#autocomplete_route_name' => 'config_help.theme_autocomplete', + ]; + + $form['#entity_builders'][] = '::copyTopicFieldsToEntity'; + + return parent::form($form, $form_state); + } + + /** + * Copies the topics and dependencies 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) { + $body = $form_state->getValue('body'); + $entity + ->setRelated(Tags::explode($form_state->getValue('related'))) + ->setListOn(Tags::explode($form_state->getValue('list_on'))) + ->setEnforcedDependencies([ + 'module' => Tags::explode($form_state->getValue('modules')), + 'theme' => Tags::explode($form_state->getValue('themes')), + ]) + ->setBody($body['value']) + ->setBodyFormat($body['format']); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + // Make sure that the HTML in the body can at least be loaded/parsed. + if (!Html::load($form_state->getValue('body')['value'])) { + $form_state->setErrorByName('body', $this->t('Body HTML is malformed')); + } + + // Make sure that the two reference fields only contain machine names of + // actual help topics. + foreach (['related', 'list_on'] as $field) { + $list = Tags::explode($form_state->getValue($field)); + $existing = array_keys($this->helpStorage->loadMultiple($list)); + $missing = array_diff($list, $existing); + if ($missing) { + $form_state->setErrorByName($field, $this->t('Must be a comma-separated list of existing topic machine names (%problem)', [ + '%problem' => Tags::implode($missing), + ])); + } + } + + $list = Tags::explode($form_state->getValue('modules')); + $missing = []; + foreach ($list as $module) { + if (!$this->moduleHandler->moduleExists($module)) { + $missing[] = $module; + } + } + if ($missing) { + $form_state->setErrorByName('modules', $this->t('Must be a comma-separated list of installed module machine names (%problem)', ['%problem' => Tags::implode($missing)])); + } + + $list = Tags::explode($form_state->getValue('themes')); + $missing = []; + foreach ($list as $theme) { + if (!$this->themeHandler->themeExists($theme)) { + $missing[] = $theme; + } + } + if ($missing) { + $form_state->setErrorByName('themes', $this->t('Must be a comma-separated list of installed theme machine names (%problem)', ['%problem' => Tags::implode($missing)])); + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $form_state->setRedirect('entity.help_topic.collection'); + + $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/config_help/src/Form/HelpUnlockForm.php b/core/modules/config_help/src/Form/HelpUnlockForm.php new file mode 100644 index 0000000..c12e793 --- /dev/null +++ b/core/modules/config_help/src/Form/HelpUnlockForm.php @@ -0,0 +1,53 @@ +t('Are you sure you want to unlock the topic %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Unlock'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.help_topic.collection'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->setLocked(FALSE); + $this->entity->save(); + drupal_set_message(t('Unlocked help topic %name.', ['%name' => $this->entity->label()])); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return '' . $this->t('Locked topics are typically provided by modules, themes, or installation profiles. They cannot be edited or deleted until they are unlocked. Unlocking and editing or deleting topics provided by a module, theme, or installation profile is not recommended.') . ''; + } + +} diff --git a/core/modules/config_help/src/HelpAccessControlHandler.php b/core/modules/config_help/src/HelpAccessControlHandler.php new file mode 100644 index 0000000..0774ab0 --- /dev/null +++ b/core/modules/config_help/src/HelpAccessControlHandler.php @@ -0,0 +1,52 @@ +isLocked() && $operation == 'unlock') { + return AccessResult::forbidden()->addCacheableDependency($entity); + } + + // Cannot lock if it is already locked. + if ($entity->isLocked() && $operation == 'lock') { + return AccessResult::forbidden()->addCacheableDependency($entity); + } + + // Deny all operations except unlock if it is currently locked. They need + // to unlock first. + if ($entity->isLocked() && $operation != 'unlock') { + return AccessResult::forbidden()->addCacheableDependency($entity); + } + + // For lock/unlock, use the locking permission. Note that we've already + // checked something on the entity, so make sure to add cache dependency. + if ($operation == 'unlock' || $operation == 'lock') { + return AccessResult::allowedIfHasPermission($account, 'administer help topic locking')->addCacheableDependency($entity); + } + + // For all remaining operations, use the generic administer permission. + // Note that we've already checked something on the entity, so make sure to + // add cache dependency. + return AccessResult::allowedIfHasPermission($account, 'administer help topics')->addCacheableDependency($entity); + } + +} diff --git a/core/modules/config_help/src/HelpBreadcrumbBuilder.php b/core/modules/config_help/src/HelpBreadcrumbBuilder.php new file mode 100644 index 0000000..df0ec47 --- /dev/null +++ b/core/modules/config_help/src/HelpBreadcrumbBuilder.php @@ -0,0 +1,49 @@ +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) { + $breadcrumb = new Breadcrumb(); + $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '')); + $breadcrumb->addLink(Link::createFromRoute($this->t('Administration'), 'system.admin')); + $breadcrumb->addLink(Link::createFromRoute($this->t('Help'), 'help.main')); + $breadcrumb->addCacheContexts(['route.name']); + + return $breadcrumb; + } + +} diff --git a/core/modules/config_help/src/HelpListBuilder.php b/core/modules/config_help/src/HelpListBuilder.php new file mode 100644 index 0000000..cb68c4c --- /dev/null +++ b/core/modules/config_help/src/HelpListBuilder.php @@ -0,0 +1,102 @@ + $this->t('Title'), + 'id' => $this->t('Machine name'), + 'top_level' => [ + 'data' => $this->t('Top level'), + 'class' => [RESPONSIVE_PRIORITY_MEDIUM], + ], + 'locked' => [ + 'data' => $this->t('Locked'), + 'class' => [RESPONSIVE_PRIORITY_MEDIUM], + ], + ]; + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row = []; + + $row['label']['data'] = [ + '#type' => 'link', + '#title' => $this->getLabel($entity), + '#url' => $entity->urlInfo('canonical'), + ]; + + $row['id'] = $entity->id(); + + $row['top_level'] = ($entity->isTopLevel()) ? $this->t('Yes') : $this->t('No'); + $row['locked'] = ($entity->isLocked()) ? $this->t('Yes') : $this->t('No'); + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + protected function getEntityIds() { + // Override the default method to sort by label, since the paged table + // is eventually sorted by label within each page. + $query = $this->getStorage()->getQuery() + ->sort($this->entityType->getKey('label')); + + if ($this->limit) { + $query->pager($this->limit); + } + + return $query->execute(); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + + // The default operations from the parent are edit and delete. Add + // view, lock, and unlock (if allowed). + if ($entity->access('view')) { + $operations['view'] = [ + 'title' => $this->t('View'), + 'weight' => 20, + 'url' => $entity->urlInfo('canonical'), + ]; + } + if ($entity->access('lock')) { + $operations['lock'] = [ + 'title' => $this->t('Lock'), + 'weight' => 30, + 'url' => $entity->urlInfo('lock-form'), + ]; + } + if ($entity->access('unlock')) { + $operations['unlock'] = [ + 'title' => $this->t('Unlock'), + 'weight' => 30, + 'url' => $entity->urlInfo('unlock-form'), + ]; + } + + return $operations; + } + +} diff --git a/core/modules/config_help/src/HelpTopicInterface.php b/core/modules/config_help/src/HelpTopicInterface.php new file mode 100644 index 0000000..80b669d --- /dev/null +++ b/core/modules/config_help/src/HelpTopicInterface.php @@ -0,0 +1,140 @@ +setDefaults([ + '_entity_form' => 'help_topic.unlock', + '_title' => 'Unlock help topic', + ]) + ->setRequirement('_entity_access', 'help_topic.unlock'); + $collection->add('entity.help_topic.unlock_form', $route); + + $route = (new Route('/admin/config/development/help/manage/{help_topic}/lock')) + ->setDefaults([ + '_entity_form' => 'help_topic.lock', + '_title' => 'Lock help topic', + ]) + ->setRequirement('_entity_access', 'help_topic.lock'); + $collection->add('entity.help_topic.lock_form', $route); + + return $collection; + } + + /** + * {@inheritdoc} + */ + protected function getCanonicalRoute(EntityTypeInterface $entity_type) { + $route = (new Route('/admin/help-topic/{help_topic}')) + ->setDefaults([ + '_entity_view' => 'help_topic.full', + '_title' => 'Help', + ]) + ->setRequirement('_entity_access', 'help_topic.view') + ->setOption('parameters', [ + 'help_topic' => [ + 'type' => 'entity:help_topic', + // Force load in current interface language. + 'with_config_overrides' => TRUE, + ], + ]); + + return $route; + } + +} diff --git a/core/modules/config_help/src/HelpViewBuilder.php b/core/modules/config_help/src/HelpViewBuilder.php new file mode 100644 index 0000000..c7a928a --- /dev/null +++ b/core/modules/config_help/src/HelpViewBuilder.php @@ -0,0 +1,132 @@ +entityTypeId = $entity_type->id(); + $this->entityType = $entity_type; + // Parent class is using deprecated entity manager. + $this->entityManager = $entity_manager; + $this->helpStorage = $entity_manager->getStorage('help_topic'); + $this->languageManager = $language_manager; + $this->token = $token; + $this->themeRegistry = $theme_registry; + } + + /** + * {@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('theme.registry') + ); + } + + /** + * {@inheritdoc} + */ + public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) { + $output = []; + + /** @var \Drupal\config_help\HelpTopicInterface[] $entities */ + foreach ($entities as $entity_id => $help_topic) { + // Get the cache information and other build defaults. + $build = $this->getBuildDefaults($help_topic, $view_mode); + $build['#langcode'] = $langcode; + $build['#title'] = $help_topic->label(); + + // Add in the body, with token replacement. + $bubbleable_metadata = new BubbleableMetadata(); + $build['#body'] = [ + '#type' => 'processed_text', + '#text' => $this->token->replace($help_topic->getBody(), [], [], $bubbleable_metadata), + '#format' => $help_topic->getBodyFormat(), + ]; + $bubbleable_metadata->applyTo($build['#body']); + + // Figure out which topics to list as related, including topics this + // entity lists as related, plus topics that have said "Add me to this + // topic's related list" using the list on field. + $related = $help_topic->getRelated() + + $this->helpStorage->getQuery() + ->condition('list_on.*', $help_topic->id()) + ->execute(); + + $links = []; + + foreach ($related as $other_id) { + if ($other_id != $help_topic->id()) { + $topic = $this->helpStorage->load($other_id); + if ($topic) { + $links[$other_id] = [ + 'title' => $topic->label(), + 'url' => $topic->urlInfo('canonical'), + ]; + } + } + } + + if (count($links)) { + ksort($links); + $build['#related'] = [ + '#theme' => 'links', + '#heading' => [ + 'text' => $this->t('Related topics'), + 'level' => 'h2', + ], + '#links' => $links, + ]; + } + + $output[$entity_id] = $build; + } + + return $output; + } + +} diff --git a/core/modules/config_help/src/Plugin/HelpSection/ConfigHelpSection.php b/core/modules/config_help/src/Plugin/HelpSection/ConfigHelpSection.php new file mode 100644 index 0000000..f6a4dc8 --- /dev/null +++ b/core/modules/config_help/src/Plugin/HelpSection/ConfigHelpSection.php @@ -0,0 +1,96 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + // The list of topics depends on the list cache tag for the topic entity. + return $this->entityTypeManager->getDefinition('help_topic')->getListCacheTags(); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + // The links are checked for user access, so we need the user permissions + // context. + return ['user.permissions']; + } + + /** + * {@inheritdoc} + */ + public function listTopics() { + /** @var \Drupal\Core\Entity\EntityStorageInterface $tour_storage */ + $help_storage = $this->entityTypeManager->getStorage('help_topic'); + /** @var \Drupal\config_help\Entity\HelpTopic[] $entities */ + $entities = $help_storage->loadMultiple(); + uasort($entities, [HelpTopic::class, 'sort']); + + $topics = []; + foreach ($entities as $entity) { + if ($entity->isTopLevel() && $entity->access('view')) { + $topics[$entity->id()] = $entity->toLink(); + } + } + + return $topics; + } + +} diff --git a/core/modules/config_help/templates/help-topic.html.twig b/core/modules/config_help/templates/help-topic.html.twig new file mode 100644 index 0000000..49c8f25 --- /dev/null +++ b/core/modules/config_help/templates/help-topic.html.twig @@ -0,0 +1,16 @@ +{# +/** + * @file + * Default theme implementation to display a help topic. + * + * Available variables: + * - body: The body of the topic. + * - related: List of related topic links. + * + * @ingroup themeable + */ +#} +
      + {{ body }} + {{ related }} +
      diff --git a/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test.yml b/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test.yml new file mode 100644 index 0000000..9ea5642 --- /dev/null +++ b/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - config_help_test + theme: { } +id: help_test +label: 'ABC Help Test module' +top_level: true +locked: false +related: + - config_help + - help_test_linked +list_on: { } +body: + - + text: '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.' + prefix_tags: '

      ' + suffix_tags: '

      ' +body_format: help diff --git a/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_additional.yml b/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_additional.yml new file mode 100644 index 0000000..6bc550a --- /dev/null +++ b/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_additional.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - config_help_test + theme: { } +id: help_test_additional +label: 'Additional topic' +top_level: false +locked: false +related: { } +list_on: + - help_test +body: + - + text: 'This topic should get listed automatically on the Help test topic.' + prefix_tags: '

      ' + suffix_tags: '

      ' +body_format: help diff --git a/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_linked.yml b/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_linked.yml new file mode 100644 index 0000000..43ba6ee --- /dev/null +++ b/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_linked.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - config_help_test + theme: { } +id: help_test_linked +label: 'Linked topic' +top_level: false +locked: false +related: { } +list_on: { } +body: + - + text: 'This topic is not supposed to be top-level.' + prefix_tags: '

      ' + suffix_tags: '

      ' +body_format: help diff --git a/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_locked.yml b/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_locked.yml new file mode 100644 index 0000000..2a608fb --- /dev/null +++ b/core/modules/config_help/tests/modules/config_help_test/config/optional/config_help.topic.help_test_locked.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - config_help_test + theme: { } +id: help_test_locked +label: 'Locked topic' +top_level: false +locked: true +related: { } +list_on: { } +body: + - + text: 'This topic is supposed to be locked to editing.' + prefix_tags: '

      ' + suffix_tags: '

      ' +body_format: help diff --git a/core/modules/config_help/tests/modules/config_help_test/config_help_test.info.yml b/core/modules/config_help/tests/modules/config_help_test/config_help_test.info.yml new file mode 100644 index 0000000..155c43d --- /dev/null +++ b/core/modules/config_help/tests/modules/config_help_test/config_help_test.info.yml @@ -0,0 +1,7 @@ +# The name of this module is deliberately different from its machine +# name to test the presented order of help topics. +name: 'ABC Help Test' +type: module +description: 'Support module for help testing.' +package: Testing +core: 8.x diff --git a/core/modules/config_help/tests/modules/config_help_test/config_help_test.module b/core/modules/config_help/tests/modules/config_help_test/config_help_test.module new file mode 100644 index 0000000..2cc5143 --- /dev/null +++ b/core/modules/config_help/tests/modules/config_help_test/config_help_test.module @@ -0,0 +1,18 @@ +adminUser = $this->createUser([ + 'access administration pages', + 'administer help topics', + 'use text format help', + 'view help topics', + 'administer help topic locking', + ]); + $this->nonLockingUser = $this->createUser([ + 'access administration pages', + 'administer help topics', + 'use text format help', + 'view help topics', + ]); + $this->nonAdminUser = $this->createUser(['access administration pages']); + + // Make sure page title, help, and local tasks are showing. + $this->placeBlock('local_tasks_block'); + $this->placeBlock('local_actions_block'); + $this->placeBlock('page_title_block'); + $this->placeBlock('help_block'); + } + + /** + * Logs in users, tests help admin pages. + */ + public function testHelpAdmin() { + $this->drupalLogin($this->nonLockingUser); + $this->verifyHelpLockingAdmin(403); + + $this->drupalLogin($this->adminUser); + $this->verifyHelpLockingAdmin(); + $this->verifyHelpAdmin(); + + $this->drupalLogin($this->nonAdminUser); + $this->verifyHelpAdmin(403); + } + + /** + * Verifies the logged in user has the correct access to locking admin. + * + * @param int $response + * (optional) The HTTP response code to test for. If it's 200 (default), + * the test verifies the user has access; if it's not, it verifies they + * are denied access. Note: Generic admin access for help topics is assumed + * but not verified in this method. + */ + protected function verifyHelpLockingAdmin($response = 200) { + $this->drupalGet('admin/config/development/help'); + + // Verify that a user without locking permissions cannot lock or unlock + // any topic, and that a user with the permission can get to the forms, + // but only to lock an unlocked topic and vice versa. + $pages = [ + 'admin/config/development/help/manage/help_test/lock' => TRUE, + 'admin/config/development/help/manage/help_test_locked/lock' => FALSE, + 'admin/config/development/help/manage/help_test/unlock' => FALSE, + 'admin/config/development/help/manage/help_test_locked/unlock' => TRUE, + ]; + foreach ($pages as $page => $allowed) { + $this->drupalGet($page); + $session = $this->assertSession(); + if (!$allowed) { + $session->statusCodeEquals(403); + } + else { + $session->statusCodeEquals($response); + } + } + + // Return at this point if testing for 403. + if ($response == 403) { + return; + } + + // Verify that locked pages cannot be edited or deleted. Editing and + // deleting of unlocked pages is tested elsewhere. Note that the URL + // for editing has no suffix. + foreach (['', '/delete'] as $action) { + $this->drupalGet('admin/config/development/help/manage/help_test_locked' . $action); + $session = $this->assertSession(); + $session->statusCodeEquals(403); + } + + // Unlock the page, and verify it can then be edited/deleted, but not + // unlocked. + $this->drupalGet('admin/config/development/help/manage/help_test_locked/unlock'); + $session = $this->assertSession(); + $session->pageTextContains('Locked topics are typically provided by modules'); + $session->pageTextContains('Are you sure you want to unlock the topic'); + $session->pageTextContains('Locked topic'); + $this->drupalPostForm(NULL, [], 'Unlock'); + $session = $this->assertSession(); + $session->pageTextContains('Unlocked help topic'); + // The '' action is actually edit. + foreach (['', '/delete', '/lock'] as $action) { + $this->drupalGet('admin/config/development/help/manage/help_test_locked' . $action); + $session = $this->assertSession(); + $session->statusCodeEquals(200); + } + $this->drupalGet('admin/config/development/help/manage/help_test_locked/unlock'); + $session = $this->assertSession(); + $session->statusCodeEquals(403); + + // Lock it up again, and verify again. + $this->drupalGet('admin/config/development/help/manage/help_test_locked/lock'); + $session = $this->assertSession(); + $session->pageTextContains('Locked topics cannot be edited or deleted until they are unlocked'); + $session->pageTextContains('Are you sure you want to lock the topic'); + $session->pageTextContains('Locked topic'); + $this->drupalPostForm(NULL, [], 'Lock'); + $session = $this->assertSession(); + $session->pageTextContains('Locked help topic'); + // The '' action is actually edit. + foreach (['', '/delete', '/lock'] as $action) { + $this->drupalGet('admin/config/development/help/manage/help_test_locked' . $action); + $session = $this->assertSession(); + $session->statusCodeEquals(403); + } + $this->drupalGet('admin/config/development/help/manage/help_test_locked/unlock'); + $session = $this->assertSession(); + $session->statusCodeEquals(200); + } + + /** + * Verifies the logged in user has the correct access to help admin. + * + * @param int $response + * (optional) The HTTP response code to test for. If it's 200 (default), + * the test verifies the user has access; if it's not, it verifies they + * are denied access. + */ + protected function verifyHelpAdmin($response = 200) { + // Verify admin links. + foreach ([ + 'admin/config', + 'admin/config/development', + 'admin/index', + ] as $page) { + $this->drupalGet($page); + $session = $this->assertSession(); + if ($response == 200) { + $session->pageTextContains('Add, delete, and edit help topics'); + $session->linkExists('Help topics'); + } + else { + $session->pageTextNotContains('Add, delete, and edit help topics'); + $session->linkNotExists('Help topics'); + } + } + + // Verify CRUD and listing page. + $this->drupalGet('admin/config/development/help'); + $session = $this->assertSession(); + $session->statusCodeEquals($response); + if ($response == 200) { + $session->linkExists('Add new help topic'); + $session->pageTextContains('Help topics'); + $session->pageTextContains('Title'); + $session->pageTextContains('Machine name'); + $session->pageTextContains('Operations'); + } + + $this->drupalGet('admin/config/development/help/add'); + $session = $this->assertSession(); + $session->statusCodeEquals($response); + + // Verify autocomplete page. + $this->drupalGet('config-help/autocomplete-topic'); + $session = $this->assertSession(); + $session->statusCodeEquals($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'; + $id = 'foo'; + $this->drupalPostForm('admin/config/development/help/add', [ + 'label' => $title, + 'id' => $id, + 'top_level' => TRUE, + 'body[value]' => $body, + ], 'Save'); + $session = $this->assertSession(); + $session->pageTextContains('Help topic added'); + + // Click to view the topic and verify the edit link works too. + $this->clickLink($title); + $session = $this->assertSession(); + $session->pageTextContains($title); + $session->pageTextContains($body); + + $this->clickLink('Edit'); + $new_title = 'Foo longer topic'; + $new_id = 'foo2'; + $this->drupalPostForm(NULL, [ + 'label' => $new_title, + 'id' => $new_id, + ], 'Save'); + $session = $this->assertSession(); + $session->pageTextContains('Help topic updated'); + $session->linkExists($new_title); + + // Test a few autocomplete values. + $autocompletes = [ + // ID of a topic we just added. + $new_id => [$new_title, $new_id], + // Title word of a topic we just edited. + 'longer' => [$new_title, $new_id], + 'help_test' => [ + 'Additional topic', + 'help_test_additional', + 'Help Test module', + ], + ]; + + foreach ($autocompletes as $query => $texts) { + $this->drupalGet('config-help/autocomplete-topic', ['query' => ['q' => $query]]); + $session = $this->assertSession(); + $session->statusCodeEquals($response); + foreach ($texts as $text) { + $session->responseContains($text); + } + } + + // Verify the link is on the Help page. + $this->drupalGet('admin/help'); + $session = $this->assertSession(); + $session->linkExists($new_title); + + // Test deleting. + $this->drupalGet('admin/config/development/help/manage/' . $new_id . '/delete'); + $session = $this->assertSession(); + $session->pageTextContains('This action cannot be undone.'); + $session->pageTextContains('Are you sure you want to delete the help topic'); + $session->pageTextContains($new_title); + $this->drupalPostForm(NULL, [], 'Delete'); + $session = $this->assertSession(); + $session->pageTextContains('The help topic'); + $session->pageTextContains('has been deleted.'); + $session->pageTextContains($new_title); + // Verfiy we are back on the admin page and there is no longer a link + // to the topic we just deleted. + $session->linkExists('Add new help topic'); + $session = $this->assertSession(); + $session->pageTextContains('Help topics'); + $session->pageTextContains('Title'); + $session->pageTextContains('Machine name'); + $session->pageTextContains('Operations'); + $session->linkNotExists($new_title); + // Verify the topic is not on admin/help any more either. + $this->drupalGet('admin/help'); + $session = $this->assertSession(); + $session->linkNotExists($new_title); + + // Test form validation. + $this->drupalGet('admin/config/development/help/add'); + $this->drupalPostForm(NULL, [ + 'label' => $title, + 'id' => 'foobar', + 'top_level' => TRUE, + 'body[value]' => $body, + 'related' => 'invalid.text', + ], 'Save'); + $session = $this->assertSession(); + $session->pageTextNotContains('Help topic added'); + $session->pageTextContains('Must be a comma-separated list of existing topic machine names'); + + // Fix form and add dependency field. + $this->drupalPostForm(NULL, [ + 'related' => '', + 'modules' => 'color', + ], 'Save'); + $session = $this->assertSession(); + $session->pageTextContains('Help topic added'); + $session->linkExists($title); + $this->drupalGet('admin/help'); + $session = $this->assertSession(); + $session->linkExists($title); + + // Disable the color module and verify dependent topics go away. + $this->container->get('module_installer')->uninstall(['color']); + $this->drupalGet('admin/help'); + $session = $this->assertSession(); + $session->linkExists('Building a help system'); + $session->linkNotExists($title); + } + + /** + * Tests tabs on topic view page. + */ + public function testTabsVisible() { + // Install config translation module to test translation tab. + $this->container->get('module_installer') + ->install(['config_translation', 'locale', 'language']); + ConfigurableLanguage::createFromLangcode('es')->save(); + + $user = $this->createUser([ + 'access administration pages', + 'administer help topics', + 'use text format help', + 'view help topics', + 'administer help topic locking', + 'translate configuration', + ]); + $this->drupalLogin($user); + foreach (['View', 'Edit', 'Delete', 'Translate help topic'] as $label) { + $this->drupalGet('admin/help-topic/help_test'); + $this->clickLink($label); + $session = $this->assertSession(); + $session->statusCodeEquals(200); + } + } + + /** + * Tests autocomplete for topics, modules & themes. + */ + public function testAutocomplete() { + $path = 'config-help/autocomplete-'; + $cases = [ + 'topic' => [ + '|' => '[]', + 'Writing good' => '[{"value":"config_help_writing","label":"Writing good help (config_help_writing)"}]', + 'config_' => '[{"value":"config_help","label":"Building a help system (config_help)"},{"value":"config_basic","label":"Changing basic site settings (config_basic)"},{"value":"config_error","label":"Configuring error responses, including 403\/404 pages (config_error)"},{"value":"config_help_form","label":"Help topic editing form (config_help_form)"},{"value":"config_help_writing","label":"Writing good help (config_help_writing)"}]', + ], + 'module' => [ + '|' => '[]', + 'ABC' => '[{"value":"config_help_test","label":"ABC Help Test (config_help_test)"}]', + 'config' => '[{"value":"config_help","label":"Configurable Help (config_help)"},{"value":"config_help_test","label":"ABC Help Test (config_help_test)"}]', + ], + 'theme' => [ + '|' => '[]', + 'Stab' => '[{"value":"stable","label":"Stable (stable)"}]', + 'a' => '[{"value":"stable","label":"Stable (stable)"},{"value":"classy","label":"Classy (classy)"}]', + ], + ]; + // Test that non-admin user has no access to autocomplete routes. + $this->drupalLogin($this->nonAdminUser); + foreach (array_keys($cases) as $part) { + $this->drupalGet($path . $part); + $session = $this->assertSession(); + $session->statusCodeEquals(403); + } + // Test output of autocomplete. + $this->drupalLogin($this->adminUser); + foreach ($cases as $part => $test) { + foreach ($test as $query => $expected) { + $this->drupalGet($path . $part, ['query' => ['q' => $query]]); + $session = $this->assertSession(); + $session->statusCodeEquals(200); + $this->assertEquals($expected, $this->getSession()->getPage()->getContent()); + } + } + // Make sure results are limited. + $this->drupalGet($path . 'topic', ['query' => ['q' => 'o']]); + $matches = Json::decode($this->getSession()->getPage()->getContent()); + $this->assertEquals(10, count($matches)); + $this->container->get('module_installer') + ->install(['contextual', 'dblog', 'hal', 'field', 'link']); + system_rebuild_module_data(); + $this->drupalGet($path . 'module', ['query' => ['q' => 'l']]); + $matches = Json::decode($this->getSession()->getPage()->getContent()); + $this->assertEquals(10, count($matches)); + } + +} diff --git a/core/modules/config_help/tests/src/Functional/HelpTopicTest.php b/core/modules/config_help/tests/src/Functional/HelpTopicTest.php new file mode 100644 index 0000000..b690837 --- /dev/null +++ b/core/modules/config_help/tests/src/Functional/HelpTopicTest.php @@ -0,0 +1,337 @@ +install(['seven']); + \Drupal::service('config.factory')->getEditable('system.theme')->set('admin', 'seven')->save(); + + // Place various blocks. + $settings = [ + 'theme' => 'seven', + 'region' => 'help', + ]; + $this->placeBlock('help_block', $settings); + $this->placeBlock('local_tasks_block', $settings); + $this->placeBlock('local_actions_block', $settings); + $this->placeBlock('page_title_block', $settings); + + // Create users. + $this->adminUser = $this->createUser([ + 'access administration pages', + 'view the administration theme', + 'administer permissions', + 'administer help topics', + 'view help topics', + ]); + $this->anyUser = $this->createUser([]); + } + + /** + * Tests the main help page and individual pages for topics. + */ + public function testHelp() { + // Log in the regular user. + $this->drupalLogin($this->anyUser); + $this->verifyHelp(403, FALSE); + + // Log in the admin user. + $this->drupalLogin($this->adminUser); + $this->verifyHelp(); + $this->verifyHelpLinks(); + + // Verify that help topics text appears on admin/help. + $this->drupalGet('admin/help'); + $session = $this->assertSession(); + $session->responseContains('

      Configured topics

      '); + $session->pageTextContains('Configured topics can be provided by modules, themes'); + + // Verify the cache tag for the list of topics is present, as well as + // the cache context for user permissions. + $this->assertCacheTag('config:help_topic_list'); + $this->assertCacheContext('user.permissions'); + + // Verify links for for configurable topics, and order. + $page_text = $this->getTextContent(); + $start = strpos($page_text, 'Configured topics'); + $pos = $start; + foreach ($this->getTopicList() as $info) { + $name = $info['name']; + $session->linkExists($name); + $new_pos = strpos($page_text, $name, $start); + $this->assertTrue($new_pos > $pos, 'Order of ' . $name . ' is correct on page'); + $pos = $new_pos; + } + + // Uninstall the test module and verify the topics are gone, after + // reloading page. + $this->container->get('module_installer')->uninstall(['config_help_test']); + $this->drupalGet('admin/help'); + $session = $this->assertSession(); + $session->linkNotExists('ABC Help Test module'); + $session->linkNotExists('ABC Help Test'); + } + + /** + * Tests export and import of help topics, and topic create from array. + */ + public function testTopicExportImport() { + // Create a help topic entity. + $values = [ + 'id' => 'foo', + 'label' => 'Foo', + 'body' => [ + [ + 'text' => 'Greetings', + 'prefix_tags' => '

      ', + 'suffix_tags' => '

      ', + ], + [ + 'text' => 'Hello, world!', + 'prefix_tags' => '

      ', + 'suffix_tags' => '

      ', + ], + ], + 'top_level' => TRUE, + 'locked' => TRUE, + 'related' => ['config_help'], + 'list_on' => ['config_help_form'], + ]; + // Set the dependecies after creation, because it's an odd bit of the + // configuration. + $dependencies = [ + 'module' => ['config_help', 'filter'], + 'theme' => ['classy', 'bartik'], + ]; + + /** @var \Drupal\config_help\HelpTopicInterface $foo */ + $foo = HelpTopic::create($values); + $foo->setEnforcedDependencies($dependencies); + + // Export and import the topic. + $foo_export = Yaml::encode($foo->toArray()); + $bar = HelpTopic::create(Yaml::decode($foo_export)); + + // Verify that everything is OK. + $to_check = [ + // This is an array of method name => component in $values, except + // dependencies and body, which are checked separately. + 'id' => 'id', + 'label' => 'label', + 'getBody' => FALSE, + 'isTopLevel' => 'top_level', + 'isLocked' => 'locked', + 'getRelated' => 'related', + 'getListOn' => 'list_on', + 'getEnforcedDependencies' => FALSE, + ]; + + // Verify that the initial create got the right values, and that after + // export/import, the values are the same. + foreach ($to_check as $method => $component) { + if ($component) { + $this->assertEqual(call_user_func([$foo, $method]), $values[$component], 'Data for ' . $component . ' is the same as method ' . $method); + } + $this->assertEqual(call_user_func([$foo, $method]), call_user_func([$bar, $method]), 'Method ' . $method . ' is the same before and after export/import'); + } + + // Check that the getEnforcedDepenencies() method works correctly. The + // getBody() and setBody() methods are checked in a different test method. + $this->assertEqual($foo->getEnforcedDependencies(), $dependencies, 'Data for dependencies is the same as method getEnforcedDependencies'); + + } + + /** + * Tests the help topic body get and set functions. + */ + public function testTopicBodyGetSet() { + $topic = HelpTopic::create(); + + $body = '' . + '

      A paragraph with attributes

      ' . + '

      A heading

      ' . + '

      A sub-heading

      ' . + '
      • Bullet 1
      • Bullet 2
      ' . + '
      1. Number 1
      2. Number 2
      ' . + '
      ' . + '
      Col 1Col 2
      Data 1Data 2
      ' . + '
      Item 1
      Definition 1
      Item 2
      Definition 2
      '; + + // Make sure after set/get, the body is unchanged. + $topic->setBody($body); + $body_out = $topic->getBody($body); + $this->assertEqual($body, $body_out); + + // Make sure that if we strip out tags, all remaining text is in the 'text' + // parts of the chunked body array. + $body_plain = strip_tags($body); + $body_chunked = $topic->toArray()['body']; + $chunked_text = ''; + foreach ($body_chunked as $item) { + $chunked_text .= $item['text']; + } + $this->assertEqual($body_plain, $chunked_text); + } + + /** + * Verifies the logged in user has access to various help links and pages. + * + * @param int $response + * (optional) The HTTP response code to test for. If it's 200 (default), + * the test verifies the user sees the help; if it's not, it verifies they + * are denied access. + * @param bool $check_tags + * (optional) TRUE (default) to verify that the cache tags are on the page, + * even though the response is not a 200. Cache tags should be there except + * for users who don't even have 'view help topics' permission. + */ + protected function verifyHelp($response = 200, $check_tags = TRUE) { + // Verify access to configurable help topic pages. + foreach ($this->getTopicList() as $topic => $info) { + // View help topic page. + $this->drupalGet('admin/help-topic/' . $topic); + $session = $this->assertSession(); + $session->statusCodeEquals($response); + if ($response == 200) { + $name = $info['name']; + $session->titleEquals($name . ' | Drupal'); + $session->responseContains('

      ' . $name . '

      '); + // Check the cache tags. + foreach ($info['cache_tags'] as $tag) { + $this->assertCacheTag($tag); + } + } + elseif ($check_tags) { + // Check that the cache tags are there. + foreach ($info['cache_tags'] as $tag) { + $this->assertCacheTag($tag); + } + } + } + } + + /** + * 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 = [ + 'link to the Help module topic' => 'Building a help system', + 'link to the help admin page' => 'Add new help topic', + 'Building a help system' => 'Building a help system', + 'Linked topic' => 'This topic is not supposed to be top-level', + 'Additional topic' => 'This topic should get listed automatically', + ]; + foreach ($links as $link_text => $page_text) { + $this->drupalGet($page); + $this->clickLink($link_text); + $session = $this->assertSession(); + $session->pageTextContains($page_text); + } + + // Verify that the non-top-level topics do not appear on the Help page. + $this->drupalGet('admin/help'); + $session = $this->assertSession(); + $session->linkNotExists('Linked topic'); + $session->linkNotExists('Additional topic'); + } + + /** + * Gets a list of topic IDs to test. + * + * @return array + * A list of topics to test, in the order in which they should appear. The + * keys are the machine names of the topics. The values are arrays with the + * following elements: + * - name: Displayed name. + * - cache_tags: Cache tags to verify are present on the topic display page. + */ + protected function getTopicList() { + return [ + 'help_test' => [ + 'name' => 'ABC Help Test module', + 'cache_tags' => [ + 'config:config_help.topic.help_test', + 'config:config_help.topic.config_help', + 'config:config_help.topic.help_test_linked', + ], + ], + 'config_help' => [ + 'name' => 'Building a help system', + 'cache_tags' => [ + 'config:config_help.topic.config_help', + ], + ], + ]; + } + + /** + * Asserts that a given cache context exists. + * + * This was part of the Simpletest base class, but doesn't exist in + * BrowserTestBase. + */ + protected function assertCacheContext($expected_cache_context) { + $contexts = explode(' ', $this->getSession()->getResponseHeader('X-Drupal-Cache-Contexts')); + $this->assertTrue(in_array($expected_cache_context, $contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header."); + } + + /** + * Asserts that a given cache tag exists. + * + * This was part of the Simpletest base class, but doesn't exist in + * BrowserTestBase except in a legacy trait. + */ + protected function assertCacheTag($expected_cache_tag) { + $tags = explode(' ', $this->getSession()->getResponseHeader('X-Drupal-Cache-Tags')); + $this->assertTrue(in_array($expected_cache_tag, $tags), "'" . $expected_cache_tag . "' is present in the X-Drupal-Cache-Tags header."); + } + +} diff --git a/core/modules/config_help/tests/src/Functional/HelpTopicTranslateTest.php b/core/modules/config_help/tests/src/Functional/HelpTopicTranslateTest.php new file mode 100644 index 0000000..7784372 --- /dev/null +++ b/core/modules/config_help/tests/src/Functional/HelpTopicTranslateTest.php @@ -0,0 +1,152 @@ +adminUser = $this->createUser([ + 'access administration pages', + 'administer help topics', + 'view help topics', + 'use text format help', + 'translate configuration', + 'administer languages', + 'administer site configuration', + ]); + + // Add a language. + ConfigurableLanguage::createFromLangcode('es')->save(); + } + + /** + * Logs in users, tests help translation. + */ + public function testHelpTranslation() { + $this->drupalLogin($this->adminUser); + + // Verify that the help topics admin page has translate links. + $this->drupalGet('admin/config/development/help'); + $session = $this->assertSession(); + $session->linkExists('Translate'); + + // Translate a topic. + $es_body = 'This is the fake Spanish body'; + $es_title = 'This is the fake Spanish title'; + $this->drupalGet('admin/config/development/help/manage/help_test/translate'); + $this->clickLink('Add'); + $this->drupalPostForm(NULL, [ + 'translation[config_names][config_help.topic.help_test][label]' => $es_title, + 'translation[config_names][config_help.topic.help_test][body][0][text]' => $es_body, + ], 'Save translation'); + + // Visit the page in English and verify. + $this->drupalGet('admin/help-topic/help_test'); + $session = $this->assertSession(); + $session->pageTextContains('ABC Help Test module'); + $session->pageTextContains('This is a test.'); + $session->pageTextNotContains($es_title); + $session->pageTextNotContains($es_body); + + // Visit the page in Spanish and verify. + $this->drupalGet('es/admin/help-topic/help_test'); + $session = $this->assertSession(); + $session->pageTextNotContains('ABC Help Test module'); + $session->pageTextNotContains('This is a test.'); + $session->pageTextContains($es_title); + $session->pageTextContains($es_body); + + // Add a new topic sourced in Spanish. + $second_en_title = 'Second Test'; + $second_es_title = 'Segunda Prueba'; + $second_en_body = 'Second body'; + $second_es_body = 'Segunda cuerpo'; + + $this->drupalPostForm('admin/config/development/help/add', [ + 'langcode' => 'es', + 'label' => $second_es_title, + 'id' => 'foo', + 'top_level' => TRUE, + 'body[value]' => $second_es_body, + ], 'Save'); + + // Translate it into English. + $this->drupalGet('admin/config/development/help/manage/foo/translate'); + $this->clickLink('Add'); + $this->drupalPostForm(NULL, [ + 'translation[config_names][config_help.topic.foo][label]' => $second_en_title, + 'translation[config_names][config_help.topic.foo][body][0][text]' => $second_en_body, + ], 'Save translation'); + + // Visit the page in English and verify. + $this->drupalGet('admin/help-topic/foo'); + $session = $this->assertSession(); + $session->pageTextContains($second_en_title); + $session->pageTextContains($second_en_body); + $session->pageTextNotContains($second_es_title); + $session->pageTextNotContains($second_es_body); + + // Visit the page in Spanish and verify. + $this->drupalGet('es/admin/help-topic/foo'); + $session = $this->assertSession(); + $session->pageTextNotContains($second_en_title); + $session->pageTextNotContains($second_en_body); + $session->pageTextContains($second_es_title); + $session->pageTextContains($second_es_body); + + // Visit the help landing page and verify correct title links are shown + // in both languages. + $this->drupalGet('admin/help'); + $session = $this->assertSession(); + $session->linkExists('ABC Help Test module'); + $session->linkNotExists($es_title); + $session->linkExists($second_en_title); + $session->linkNotExists($second_es_title); + + $this->drupalGet('es/admin/help'); + $session = $this->assertSession(); + $session->linkNotExists('ABC Help Test module'); + $session->linkExists($es_title); + $session->linkNotExists($second_en_title); + $session->linkExists($second_es_title); + } + +} diff --git a/core/modules/config_help/tests/src/Kernel/HelpTopicTokensTest.php b/core/modules/config_help/tests/src/Kernel/HelpTopicTokensTest.php new file mode 100644 index 0000000..d8c32e4 --- /dev/null +++ b/core/modules/config_help/tests/src/Kernel/HelpTopicTokensTest.php @@ -0,0 +1,64 @@ +installSchema('system', ['router', 'sequences']); + $this->installConfig(['config_help']); + \Drupal::service('router.builder')->rebuild(); + } + + /** + * Tests that help topic tokens work. + */ + public function testHelpTopicTokens() { + // Verify a URL token for a help topic. + $bubbleable_metadata = new BubbleableMetadata(); + $text = 'This should Link to help topic'; + $replaced = \Drupal::token()->replace($text, [], [], $bubbleable_metadata); + $this->assertTrue(strpos($replaced, 'assertTrue(in_array('config:config_help.topic.config_help', $bubbleable_metadata->getCacheTags()), 'Cache tag for the linked topic was added'); + + $text = 'This should Not link to help topic'; + $replaced = \Drupal::token()->replace($text); + $this->assertTrue(strpos($replaced, '[help_topic:url:nonexistent]') !== FALSE, 'Nonexistent help topic did not get replaced'); + } + + /** + * Tests that 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:url:system.nonexistent]') !== FALSE, 'Nonexistent route was not replaced'); + } + +}