diff --git a/bootstrap.drush.inc b/bootstrap.drush.inc
deleted file mode 100644
index fdd9b5cdc04ee5c05067c4066e36a13f173e0d1d..0000000000000000000000000000000000000000
--- a/bootstrap.drush.inc
+++ /dev/null
@@ -1,126 +0,0 @@
- dt('Generates markdown documentation for the Drupal based code.'),
- 'arguments' => [
- 'type' => 'The specific type of documentation to generate, defaults to "all". Can be: "all", "settings".',
- ],
- 'aliases' => ['bs-docs'],
- ];
- return $items;
-}
-
-/**
- * Generates markdown documentation.
- *
- * @param string $type
- * The type of documentation.
- */
-function drush_bootstrap_generate_docs($type = 'all') {
- $types = $type === 'all' ? ['settings'] : [$type];
- foreach ($types as $type) {
- $function = "_drush_bootstrap_generate_docs_$type";
- if (function_exists($function)) {
- $ret = $function(Bootstrap::getTheme('bootstrap'));
- if ($ret) {
- drush_log('Successfully generated documentation for: ' . $type, 'success');
- }
- else {
- drush_log('Unable to generate documentation for: ' . $type, 'error');
- }
- }
- else {
- drush_log('Invalid documentation type: ' . $type, 'error');
- }
- }
-}
-
-/**
- * Generates settings documentation.
- *
- * @param \Drupal\bootstrap\Theme $bootstrap
- * The theme instance of the Drupal Bootstrap base theme.
- */
-function _drush_bootstrap_generate_docs_settings(Theme $bootstrap) {
- $filename = realpath($bootstrap->getPath() . '/docs/Theme-Settings.md');
- $marker_start = "";
- $marker_end = "\n";
- $contents = @file_get_contents($filename) ?: '';
- $parts = @preg_split('/' . preg_quote($marker_start, '/') . '|' . preg_quote($marker_end, '/') . '/', $contents) ?: [];
- $start = isset($parts[0]) ? [trim($parts[0])] : [];
- $end = isset($parts[2]) ? [trim($parts[2])] : [];
-
- // Determine the groups.
- $groups = [
- 'general' => [],
- 'components' => [],
- 'javascript' => [],
- 'cdn' => [],
- 'advanced' => [],
- ];
- foreach ($bootstrap->getSettingPlugin() as $setting) {
- // Only get the first two groups (we don't need 3rd, or more, levels).
- $_groups = array_slice(array_filter($setting->getGroups()), 0, 2, FALSE);
- if (!$_groups) {
- continue;
- }
- $groups[array_keys($_groups)[0]][implode(' > ', $_groups)][] = $setting->getPluginDefinition();
- }
-
- // Generate a table of each group's settings.
- $lines = [$marker_start];
- foreach ($groups as $subgroups) {
- foreach ($subgroups as $group => $settings) {
- $lines[] = '';
- $lines[] = '---';
- $lines[] = '';
- $lines[] = "### $group";
- $lines[] = '';
- $lines[] = '
diff --git a/docs/Theme-Settings.md b/docs/Theme-Settings.md
index 813a0d525308d99a8d93dc230aaf0843c73d9c64..9b73694eb9263769f18b44c0745233bf366f7d12 100644
--- a/docs/Theme-Settings.md
+++ b/docs/Theme-Settings.md
@@ -82,34 +82,34 @@ $theme->setSetting('my_setting', 'a new value');
-
-
-button_colorize
- |
-
- Adds classes to buttons based on their text value.
- button_colorize: 1
- |
-
-
-
-button_iconize
- |
-
- Adds icons to buttons based on the text value
- button_iconize: 1
- |
-
-
-
-button_size
- |
-
- Defines the Bootstrap Buttons specific size
- button_size: ''
- |
-
-
+
+
+ button_colorize
+ |
+
+ Adds classes to buttons based on their text value.
+ button_colorize: 1
+ |
+
+
+
+ button_iconize
+ |
+
+ Adds icons to buttons based on the text value
+ button_iconize: 1
+ |
+
+
+
+ button_size
+ |
+
+ Defines the Bootstrap Buttons specific size
+ button_size: ''
+ |
+
+
---
@@ -124,16 +124,16 @@ button_size
-
-
-fluid_container
- |
-
- Uses the .container-fluid class instead of .container .
- fluid_container: 0
- |
-
-
+
+
+ fluid_container
+ |
+
+ Uses the .container-fluid class instead of .container .
+ fluid_container: 0
+ |
+
+
---
@@ -148,52 +148,52 @@ fluid_container
-
-
-forms_has_error_value_toggle
- |
-
- If an element has a .has-error class attached to it, enabling this will automatically remove that class when a value is entered.
- forms_has_error_value_toggle: 1
- |
-
-
-
-forms_required_has_error
- |
-
- If an element in a form is required, enabling this will always display the element with a .has-error class. This turns the element red and helps in usability for determining which form elements are required to submit the form.
- forms_required_has_error: 0
- |
-
-
-
-forms_smart_descriptions
- |
-
- Convert descriptions into tooltips (must be enabled) automatically based on certain criteria. This helps reduce the, sometimes unnecessary, amount of noise on a page full of form elements.
- forms_smart_descriptions: 1
- |
-
-
-
-forms_smart_descriptions_allowed_tags
- |
-
- Prevents descriptions from becoming tooltips by checking for HTML not in the list above (i.e. links). Separate by commas. To disable this filtering criteria, leave an empty value.
- forms_smart_descriptions_allowed_tags: 'b, code, em, i, kbd, span, strong'
- |
-
-
-
-forms_smart_descriptions_limit
- |
-
- Prevents descriptions from becoming tooltips by checking the character length of the description (HTML is not counted towards this limit). To disable this filtering criteria, leave an empty value.
- forms_smart_descriptions_limit: '250'
- |
-
-
+
+
+ forms_has_error_value_toggle
+ |
+
+ If an element has a .has-error class attached to it, enabling this will automatically remove that class when a value is entered.
+ forms_has_error_value_toggle: 1
+ |
+
+
+
+ forms_required_has_error
+ |
+
+ If an element in a form is required, enabling this will always display the element with a .has-error class. This turns the element red and helps in usability for determining which form elements are required to submit the form.
+ forms_required_has_error: 0
+ |
+
+
+
+ forms_smart_descriptions
+ |
+
+ Convert descriptions into tooltips (must be enabled) automatically based on certain criteria. This helps reduce the, sometimes unnecessary, amount of noise on a page full of form elements.
+ forms_smart_descriptions: 1
+ |
+
+
+
+ forms_smart_descriptions_allowed_tags
+ |
+
+ Prevents descriptions from becoming tooltips by checking for HTML not in the list above (i.e. links). Separate by commas. To disable this filtering criteria, leave an empty value.
+ forms_smart_descriptions_allowed_tags: 'b, code, em, i, kbd, span, strong'
+ |
+
+
+
+ forms_smart_descriptions_limit
+ |
+
+ Prevents descriptions from becoming tooltips by checking the character length of the description (HTML is not counted towards this limit). To disable this filtering criteria, leave an empty value.
+ forms_smart_descriptions_limit: '250'
+ |
+
+
---
@@ -208,25 +208,25 @@ forms_smart_descriptions_limit
-
-
-image_responsive
- |
-
- Images in Bootstrap 3 can be made responsive-friendly via the addition of the .img-responsive class. This applies max-width: 100%; and height: auto; to the image so that it scales nicely to the parent element.
- image_responsive: 1
- |
-
-
-
-image_shape
- |
-
- Add classes to an <img> element to easily style images in any project.
- image_shape: ''
- |
-
-
+
+
+ image_responsive
+ |
+
+ Images in Bootstrap 3 can be made responsive-friendly via the addition of the .img-responsive class. This applies max-width: 100%; and height: auto; to the image so that it scales nicely to the parent element.
+ image_responsive: 1
+ |
+
+
+
+ image_shape
+ |
+
+ Add classes to an <img> element to easily style images in any project.
+ image_shape: ''
+ |
+
+
---
@@ -241,52 +241,52 @@ image_shape
-
-
-table_bordered
- |
-
- Add borders on all sides of the table and cells.
- table_bordered: 0
- |
-
-
-
-table_condensed
- |
-
- Make tables more compact by cutting cell padding in half.
- table_condensed: 0
- |
-
-
-
-table_hover
- |
-
- Enable a hover state on table rows.
- table_hover: 1
- |
-
-
-
-table_striped
- |
-
- Add zebra-striping to any table row within the <tbody> .
- table_striped: 1
- |
-
-
-
-table_responsive
- |
-
- Wraps tables with .table-responsive to make them horizontally scroll when viewing them on devices under 768px. When viewing on devices larger than 768px, you will not see a difference in the presentational aspect of these tables. The Automatic option will only apply this setting for front-end facing tables, not the tables in administrative areas.
- table_responsive: -1
- |
-
-
+
+
+ table_bordered
+ |
+
+ Add borders on all sides of the table and cells.
+ table_bordered: 0
+ |
+
+
+
+ table_condensed
+ |
+
+ Make tables more compact by cutting cell padding in half.
+ table_condensed: 0
+ |
+
+
+
+ table_hover
+ |
+
+ Enable a hover state on table rows.
+ table_hover: 1
+ |
+
+
+
+ table_striped
+ |
+
+ Add zebra-striping to any table row within the <tbody> .
+ table_striped: 1
+ |
+
+
+
+ table_responsive
+ |
+
+ Wraps tables with .table-responsive to make them horizontally scroll when viewing them on devices under 768px. When viewing on devices larger than 768px, you will not see a difference in the presentational aspect of these tables. The Automatic option will only apply this setting for front-end facing tables, not the tables in administrative areas.
+ table_responsive: -1
+ |
+
+
---
@@ -301,34 +301,34 @@ table_responsive
-
-
-breadcrumb
- |
-
- Show or hide the Breadcrumbs
- breadcrumb: '1'
- |
-
-
-
-breadcrumb_home
- |
-
- If your site has a module dedicated to handling breadcrumbs already, ensure this setting is enabled.
- breadcrumb_home: 0
- |
-
-
-
-breadcrumb_title
- |
-
- If your site has a module dedicated to handling breadcrumbs already, ensure this setting is disabled.
- breadcrumb_title: 1
- |
-
-
+
+
+ breadcrumb
+ |
+
+ Show or hide the Breadcrumbs
+ breadcrumb: '1'
+ |
+
+
+
+ breadcrumb_home
+ |
+
+ If your site has a module dedicated to handling breadcrumbs already, ensure this setting is enabled.
+ breadcrumb_home: 0
+ |
+
+
+
+ breadcrumb_title
+ |
+
+ If your site has a module dedicated to handling breadcrumbs already, ensure this setting is disabled.
+ breadcrumb_title: 1
+ |
+
+
---
@@ -343,25 +343,25 @@ breadcrumb_title
-
-
-navbar_inverse
- |
-
- Select if you want the inverse navbar style.
- navbar_inverse: 0
- |
-
-
-
-navbar_position
- |
-
- Determines where the navbar is positioned on the page.
- navbar_position: ''
- |
-
-
+
+
+ navbar_inverse
+ |
+
+ Select if you want the inverse navbar style.
+ navbar_inverse: 0
+ |
+
+
+
+ navbar_position
+ |
+
+ Determines where the navbar is positioned on the page.
+ navbar_position: ''
+ |
+
+
---
@@ -376,13 +376,13 @@ navbar_position
-
-
-region_wells
- |
-
- Enable the .well , .well-sm or .well-lg classes for specified regions.
- region_wells:
+
+
+ region_wells
+ |
+
+ Enable the .well , .well-sm or .well-lg classes for specified regions.
+ region_wells:
navigation: ''
navigation_collapsible: ''
header: ''
@@ -392,9 +392,9 @@ region_wells
sidebar_first: ''
sidebar_second: well
footer: ''
- |
-
-
|
+
+
+
---
@@ -409,87 +409,88 @@ region_wells
-
-
-modal_enabled
- |
-
- modal_enabled: 1
- |
-
-
-
-modal_jquery_ui_bridge
- |
-
- Enabling this replaces the core/jquery.ui.dialog dependency in the core/drupal.dialog library with a jQuery UI Dialog widget bridge. This bridge adds support to Bootstrap Modals so that it may interpret jQuery UI Dialog functionality.
- modal_jquery_ui_bridge: 1
- |
-
-
-
-modal_animation
- |
-
- Apply a CSS fade transition to modals.
- modal_animation: 1
- |
-
-
-
-modal_backdrop
- |
-
- Includes a modal-backdrop element. Alternatively, specify static for a backdrop which doesn't close the modal on click.
- modal_backdrop: 'true'
- |
-
-
-
-modal_focus_input
- |
-
- Enabling this focuses on the first available and visible input found in the modal after it's opened. If no element is found, the close button (if visible) is focused instead.
- modal_focus_input: 1
- |
-
-
-
-modal_keyboard
- |
-
- Closes the modal when escape key is pressed.
- modal_keyboard: 1
- |
-
-
-
-modal_select_text
- |
-
- Enabling this selects the text of the first available and visible input found after it has been focused.
- modal_select_text: 1
- |
-
-
-
-modal_show
- |
-
- Shows the modal when initialized.
- modal_show: 1
- |
-
-
-
-modal_size
- |
-
- Defines the modal size between the default, modal-sm and modal-lg .
- modal_size: ''
- |
-
-
+
+
+ modal_enabled
+ |
+
+
+ modal_enabled: 1
+ |
+
+
+
+ modal_jquery_ui_bridge
+ |
+
+ Enabling this replaces the core/jquery.ui.dialog dependency in the core/drupal.dialog library with a jQuery UI Dialog widget bridge. This bridge adds support to Bootstrap Modals so that it may interpret jQuery UI Dialog functionality.
+ modal_jquery_ui_bridge: 1
+ |
+
+
+
+ modal_animation
+ |
+
+ Apply a CSS fade transition to modals.
+ modal_animation: 1
+ |
+
+
+
+ modal_backdrop
+ |
+
+ Includes a modal-backdrop element. Alternatively, specify static for a backdrop which doesn't close the modal on click.
+ modal_backdrop: 'true'
+ |
+
+
+
+ modal_focus_input
+ |
+
+ Enabling this focuses on the first available and visible input found in the modal after it's opened. If no element is found, the close button (if visible) is focused instead.
+ modal_focus_input: 1
+ |
+
+
+
+ modal_keyboard
+ |
+
+ Closes the modal when escape key is pressed.
+ modal_keyboard: 1
+ |
+
+
+
+ modal_select_text
+ |
+
+ Enabling this selects the text of the first available and visible input found after it has been focused.
+ modal_select_text: 1
+ |
+
+
+
+ modal_show
+ |
+
+ Shows the modal when initialized.
+ modal_show: 1
+ |
+
+
+
+ modal_size
+ |
+
+ Defines the modal size between the default, modal-sm and modal-lg .
+ modal_size: ''
+ |
+
+
---
@@ -504,115 +505,106 @@ modal_size
-
-
-popover_enabled
- |
-
- Elements that have the data-toggle="popover" attribute set will automatically initialize the popover upon page load. WARNING: This feature can sometimes impact performance. Disable if pages appear to hang after initial load.
- popover_enabled: 1
- |
-
-
-
-popover_animation
- |
-
- Apply a CSS fade transition to the popover.
- popover_animation: 1
- |
-
-
-
-popover_auto_close
- |
-
- If enabled, the active popover will automatically close when it loses focus, when a click occurs anywhere in the DOM (outside the popover), the escape key (ESC) is pressed or when another popover is opened.
- popover_auto_close: 1
- |
-
-
-
-popover_container
- |
-
- Appends the popover to a specific element. Example: body . This option is particularly useful in that it allows you to position the popover in the flow of the document near the triggering element - which will prevent the popover from floating away from the triggering element during a window resize.
- popover_container: body
- |
-
-
-
-popover_content
- |
-
- Default content value if data-content or data-target attributes are not present.
- popover_content: ''
- |
-
-
-
-popover_delay
- |
-
- The amount of time to delay showing and hiding the popover (in milliseconds). Does not apply to manual trigger type.
- popover_delay: '0'
- |
-
-
-
-popover_html
- |
-
- Insert HTML into the popover. If false, jQuery's text method will be used to insert content into the DOM. Use text if you're worried about XSS attacks.
- popover_html: 0
- |
-
-
-
-popover_placement
- |
-
- Where to position the popover. When auto is specified, it will dynamically reorient the popover. For example, if placement is auto left , the popover will display to the left when possible, otherwise it will display right.
- popover_placement: right
- |
-
-
-
-popover_selector
- |
-
- If a selector is provided, tooltip objects will be delegated to the specified targets. In practice, this is used to enable dynamic HTML content to have popovers added.
- popover_selector: ''
- |
-
-
-
-popover_title
- |
-
- Default title value if title attribute isn't present.
- popover_title: ''
- |
-
-
-
-popover_trigger
- |
-
- How a popover is triggered.
- popover_trigger: click
- |
-
-
-
-popover_trigger_autoclose
- |
-
- Will automatically close the current popover if a click occurs anywhere else other than the popover element.
- popover_trigger_autoclose: 1
- |
-
-
+
+
+ popover_enabled
+ |
+
+ Elements that have the data-toggle="popover" attribute set will automatically initialize the popover upon page load. WARNING: This feature can sometimes impact performance. Disable if pages appear to hang after load.
+ popover_enabled: 1
+ |
+
+
+
+ popover_animation
+ |
+
+ Apply a CSS fade transition to the popover.
+ popover_animation: 1
+ |
+
+
+
+ popover_auto_close
+ |
+
+ If enabled, the active popover will automatically close when it loses focus, when a click occurs anywhere in the DOM (outside the popover), the escape key (ESC) is pressed or when another popover is opened.
+ popover_auto_close: 1
+ |
+
+
+
+ popover_container
+ |
+
+ Appends the popover to a specific element. Example: body . This option is particularly useful in that it allows you to position the popover in the flow of the document near the triggering element - which will prevent the popover from floating away from the triggering element during a window resize.
+ popover_container: body
+ |
+
+
+
+ popover_content
+ |
+
+ Default content value if data-content or data-target attributes are not present.
+ popover_content: ''
+ |
+
+
+
+ popover_delay
+ |
+
+ The amount of time to delay showing and hiding the popover (in milliseconds). Does not apply to manual trigger type.
+ popover_delay: '0'
+ |
+
+
+
+ popover_html
+ |
+
+ Insert HTML into the popover. If false, jQuery's text method will be used to insert content into the DOM. Use text if you're worried about XSS attacks.
+ popover_html: 0
+ |
+
+
+
+ popover_placement
+ |
+
+ Where to position the popover. When auto is specified, it will dynamically reorient the popover. For example, if placement is auto left , the popover will display to the left when possible, otherwise it will display right.
+ popover_placement: right
+ |
+
+
+
+ popover_selector
+ |
+
+ If a selector is provided, tooltip objects will be delegated to the specified targets. In practice, this is used to enable dynamic HTML content to have popovers added.
+ popover_selector: ''
+ |
+
+
+
+ popover_title
+ |
+
+ Default title value if title attribute isn't present.
+ popover_title: ''
+ |
+
+
+
+ popover_trigger
+ |
+
+ How a popover is triggered.
+ popover_trigger: click
+ |
+
+
---
@@ -627,79 +619,79 @@ popover_trigger_autoclose
-
-
-tooltip_enabled
- |
-
- Elements that have the data-toggle="tooltip" attribute set will automatically initialize the tooltip upon page load. WARNING: This feature can sometimes impact performance. Disable if pages appear to "hang" after initial load.
- tooltip_enabled: 1
- |
-
-
-
-tooltip_animation
- |
-
- Apply a CSS fade transition to the tooltip.
- tooltip_animation: 1
- |
-
-
-
-tooltip_container
- |
-
- Appends the tooltip to a specific element. Example: body .
- tooltip_container: body
- |
-
-
-
-tooltip_delay
- |
-
- The amount of time to delay showing and hiding the tooltip (in milliseconds). Does not apply to manual trigger type.
- tooltip_delay: '0'
- |
-
-
-
-tooltip_html
- |
-
- Insert HTML into the tooltip. If false, jQuery's text method will be used to insert content into the DOM. Use text if you're worried about XSS attacks.
- tooltip_html: 0
- |
-
-
-
-tooltip_placement
- |
-
- Where to position the tooltip. When auto is specified, it will dynamically reorient the tooltip. For example, if placement is auto left , the tooltip will display to the left when possible, otherwise it will display right.
- tooltip_placement: 'auto left'
- |
-
-
-
-tooltip_selector
- |
-
- If a selector is provided, tooltip objects will be delegated to the specified targets.
- tooltip_selector: ''
- |
-
-
-
-tooltip_trigger
- |
-
- How a tooltip is triggered.
- tooltip_trigger: hover
- |
-
-
+
+
+ tooltip_enabled
+ |
+
+ Elements that have the data-toggle="tooltip" attribute set will automatically initialize the tooltip upon page load. WARNING: This feature can sometimes impact performance. Disable if pages appear to "hang" after load.
+ tooltip_enabled: 1
+ |
+
+
+
+ tooltip_animation
+ |
+
+ Apply a CSS fade transition to the tooltip.
+ tooltip_animation: 1
+ |
+
+
+
+ tooltip_container
+ |
+
+ Appends the tooltip to a specific element. Example: body .
+ tooltip_container: body
+ |
+
+
+
+ tooltip_delay
+ |
+
+ The amount of time to delay showing and hiding the tooltip (in milliseconds). Does not apply to manual trigger type.
+ tooltip_delay: '0'
+ |
+
+
+
+ tooltip_html
+ |
+
+ Insert HTML into the tooltip. If false, jQuery's text method will be used to insert content into the DOM. Use text if you're worried about XSS attacks.
+ tooltip_html: 0
+ |
+
+
+
+ tooltip_placement
+ |
+
+ Where to position the tooltip. When auto is specified, it will dynamically reorient the tooltip. For example, if placement is auto left , the tooltip will display to the left when possible, otherwise it will display right.
+ tooltip_placement: 'auto left'
+ |
+
+
+
+ tooltip_selector
+ |
+
+ If a selector is provided, tooltip objects will be delegated to the specified targets.
+ tooltip_selector: ''
+ |
+
+
+
+ tooltip_trigger
+ |
+
+ How a tooltip is triggered.
+ tooltip_trigger: hover
+ |
+
+
---
@@ -714,70 +706,34 @@ tooltip_trigger
-
-
-cdn_provider
- |
-
- Choose the CDN Provider used to load Bootstrap resources.
- cdn_provider: jsdelivr
- |
-
-
-
-cdn_custom_css
- |
-
- It is best to use https protocols here as it will allow more flexibility if the need ever arises.
- cdn_custom_css: 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.css'
- |
-
-
-
-cdn_custom_css_min
- |
-
- Additionally, you can provide the minimized version of the file. It will be used instead if site aggregation is enabled.
- cdn_custom_css_min: 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css'
- |
-
-
-
-cdn_custom_js
- |
-
- It is best to use https protocols here as it will allow more flexibility if the need ever arises.
- cdn_custom_js: 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.js'
- |
-
-
-
-cdn_custom_js_min
- |
-
- Additionally, you can provide the minimized version of the file. It will be used instead if site aggregation is enabled.
- cdn_custom_js_min: 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js'
- |
-
-
-
-cdn_jsdelivr_version
- |
-
- Choose the Bootstrap version from jsdelivr
- cdn_jsdelivr_version: 3.4.1
- |
-
-
-
-cdn_jsdelivr_theme
- |
-
- Choose the Example Theme provided by Bootstrap or one of the Bootswatch themes.
- cdn_jsdelivr_theme: bootstrap
- |
-
-
+
+
+ cdn_provider
+ |
+
+ Choose the CDN Provider used to load Bootstrap resources.
+ cdn_provider: jsdelivr
+ |
+
+
+
+ cdn_version
+ |
+
+ Choose a version provided by the CDN Provider.
+ cdn_version: 3.4.1
+ |
+
+
+
+ cdn_theme
+ |
+
+ Choose a theme provided by the CDN Provider.
+
+ |
+
+
---
@@ -792,43 +748,70 @@ cdn_jsdelivr_theme
-
-
-cdn_cache_ttl_versions
- |
-
- The length of time to cache the CDN verions before requesting them from the API again.
- cdn_cache_ttl_versions: 604800
- |
-
-
-
-cdn_cache_ttl_themes
- |
-
- The length of time to cache the CDN themes (if applicable) before requesting them from the API again.
- cdn_cache_ttl_themes: 2630000
- |
-
-
-
-cdn_cache_ttl_assets
- |
-
- The length of time to cache the parsing and processing of CDN assets before rebuilding them again. Note: any change to CDN values automatically triggers a new build.
- cdn_cache_ttl_assets: -1
- |
-
-
-
-cdn_cache_ttl_library
- |
-
- The length of time to cache the theme's library alterations before rebuilding them again. Note: any change to CDN values automatically triggers a new build.
- cdn_cache_ttl_library: -1
- |
-
-
+
+
+ cdn_cache_ttl_versions
+ |
+
+ The length of time to cache the CDN verions before requesting them from the API again.
+ cdn_cache_ttl_versions: 604800
+ |
+
+
+
+ cdn_cache_ttl_themes
+ |
+
+ The length of time to cache the CDN themes (if applicable) before requesting them from the API again.
+ cdn_cache_ttl_themes: 604800
+ |
+
+
+
+ cdn_cache_ttl_assets
+ |
+
+ The length of time to cache the parsing and processing of CDN assets before rebuilding them again. Note: any change to CDN values automatically triggers a new build.
+ cdn_cache_ttl_assets: -1
+ |
+
+
+
+ cdn_cache_ttl_library
+ |
+
+ The length of time to cache the theme's library alterations before rebuilding them again. Note: any change to CDN values automatically triggers a new build.
+ cdn_cache_ttl_library: -1
+ |
+
+
+
+
+---
+
+### CDN (Content Delivery Network) > Custom URLs
+
+
+
+
+ Setting name |
+ Description and default value |
+
+
+
+
+
+ cdn_custom
+ |
+
+ One complete URL per line. All URLs are validated and parsed to determine available version(s) and/or theme(s). A URL can be any file ending in .css or .js (with matching response MIME type). Minified URLs can also be supplied and the will be used automatically.
+ cdn_custom: "https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.css
+https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css
+https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.js
+https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js"
+ |
+
+
---
@@ -843,24 +826,123 @@ cdn_cache_ttl_library
+
+
+ include_deprecated
+ |
+
+ Enabling this setting will include any deprecated.php file found in your theme or base themes.
+ include_deprecated: 0
+ |
+
+
+
+ suppress_deprecated_warnings
+ |
+
+ Enable this setting if you wish to suppress deprecated warning messages.
+ suppress_deprecated_warnings: 0
+ |
+
+
+
+
+---
+
+### Deprecated
+
+
+
-
-include_deprecated
- |
-
- Enabling this setting will include any deprecated.php file found in your theme or base themes.
- include_deprecated: 0
- |
-
-
-
-suppress_deprecated_warnings
- |
-
- Enable this setting if you wish to suppress deprecated warning messages.
- suppress_deprecated_warnings: 0
- |
+ Setting name |
+ Description and default value |
+
+
+
+
+ popover_trigger_autoclose
+ |
+
+ Will automatically close the current popover if a click occurs anywhere else other than the popover element.
+ popover_trigger_autoclose: 1
+
+ Deprecated since 8.x-3.14 - Replaced with new setting. Will be removed in a future release. (see: popover_auto_close)
+
+ |
+
+
+
+ cdn_jsdelivr_version
+ |
+
+ Choose the Bootstrap version from jsdelivr
+ cdn_jsdelivr_version: 3.4.1
+
+ Deprecated since 8.x-3.18 - Replaced with new setting. Will be removed in a future release. (see: cdn_version)
+
+ |
+
+
+
+ cdn_jsdelivr_theme
+ |
+
+ Choose the Example Theme provided by Bootstrap or one of the Bootswatch themes.
+ cdn_jsdelivr_theme: bootstrap
+
+ Deprecated since 8.x-3.18 - Replaced with new setting. Will be removed in a future release. (see: cdn_theme)
+
+ |
+
+
+
+ cdn_custom_css
+ |
+
+ It is best to use https protocols here as it will allow more flexibility if the need ever arises.
+ cdn_custom_css: 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.css'
+
+ Deprecated since 8.x-3.18 - Replaced with new setting. Will be removed in a future release. (see: cdn_custom)
+
+ |
+
+
+
+ cdn_custom_css_min
+ |
+
+ Additionally, you can provide the minimized version of the file. It will be used instead if site aggregation is enabled.
+ cdn_custom_css_min: 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css'
+
+ Deprecated since 8.x-3.18 - Replaced with new setting. Will be removed in a future release. (see: cdn_custom)
+
+ |
+
+
+
+ cdn_custom_js
+ |
+
+ It is best to use https protocols here as it will allow more flexibility if the need ever arises.
+ cdn_custom_js: 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.js'
+
+ Deprecated since 8.x-3.18 - Replaced with new setting. Will be removed in a future release. (see: cdn_custom)
+
+ |
+
+
+
+ cdn_custom_js_min
+ |
+
+ Additionally, you can provide the minimized version of the file. It will be used instead if site aggregation is enabled.
+ cdn_custom_js_min: 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js'
+
+ Deprecated since 8.x-3.18 - Replaced with new setting. Will be removed in a future release. (see: cdn_custom)
+
+ |
+
diff --git a/docs/plugins/Provider.md b/docs/plugins/Provider.md
index 5da6e50d06b854f055d3029751dd5ec403539c97..f3a9c0efc480becb6195d624960891bd283afea1 100644
--- a/docs/plugins/Provider.md
+++ b/docs/plugins/Provider.md
@@ -3,7 +3,88 @@
# @BootstrapProvider
-This plugin is a little too complex to explain (for now). If you would like to
-help expand this documentation, please [create an issue](https://www.drupal.org/node/add/project-issue/bootstrap).
+- [Create a plugin](#create)
+- [Rebuild the cache](#rebuild)
-See the existing classes below on examples of how to implement your own.
+---
+
+## Create a plugin {#create}
+
+We'll use the `\Drupal\bootstrap\Plugin\Provider\JsDelivr` CDN Provider as an
+example of how to create a quick custom CDN provider using its API URLs.
+
+Replace all following instances of `THEMENAME` with the actual machine name of
+your sub-theme.
+
+You may also feel free to replace the provided URLs with your own. Most of the
+popular CDN API output can be easily parsed, however you may need to provide
+addition parsing in your custom CDN Provider if you're not getting the desired
+results.
+
+If you're truly interested in implementing a CDN Provider, it is highly
+recommended that you read the accompanying PHP based documentation on the
+classes and methods responsible for actually retrieving, parsing and caching
+the data from the CDN's API.
+
+Create a file at `./THEMENAME/src/Plugin/Provider/MyCdn.php` with the
+following contents:
+
+```php
+
+```
+
+## Rebuild the cache {#rebuild}
+
+Once you have saved, you must rebuild your cache for this new plugin to be
+discovered. This must happen anytime you make a change to the actual file name
+or the information inside the `@BootstrapProvider` annotation.
+
+To rebuild your cache, navigate to `admin/config/development/performance` and
+click the `Clear all caches` button. Or if you prefer, run `drush cr` from the
+command line.
+
+VoilĂ ! After this, you should have a fully functional `@BootstrapProvider`
+plugin!
diff --git a/docs/theme-settings.twig b/docs/theme-settings.twig
new file mode 100644
index 0000000000000000000000000000000000000000..8b44b454c01b58b44697dcae4f210ca46b6e1306
--- /dev/null
+++ b/docs/theme-settings.twig
@@ -0,0 +1,59 @@
+
+{% for heading, settings in groups %}
+
+---
+
+### {{ heading|raw }}
+
+
+
+
+ {{ 'Setting name'|t }} |
+ {{ 'Description and default value'|t }} |
+
+
+
+ {% for id, setting in settings %}
+
+ {{- id -}}
+ |
+
+ {{- setting.description -}}
+ {{- setting.defaultValue -}}
+ |
+
+ {% endfor %}
+
+
+{% endfor %}
+{% if deprecated %}
+
+---
+
+### {{ 'Deprecated'|t }}
+
+
+
+
+ {{ 'Setting name'|t }} |
+ {{ 'Description and default value'|t }} |
+
+
+
+ {% for id, setting in deprecated %}
+
+ {{- id -}}
+ |
+
+ {{- setting.description -}}
+ {{- setting.defaultValue -}}
+
+ {{ 'Deprecated since @version'|t({'@version': setting.deprecated.version }) }} - {{ setting.deprecated.reason }} ({{ 'see: @replacement'|t({'@replacement': setting.deprecated.replacement}) }})
+
+ |
+
+ {% endfor -%}
+
+
+{% endif %}
+
diff --git a/js/theme-settings.js b/js/theme-settings.js
index b5a16f3e32bf03abbd1a14747cfc85e66defc79d..d008a2e173646ee1a3165fb8af897eed65880d1a 100644
--- a/js/theme-settings.js
+++ b/js/theme-settings.js
@@ -135,18 +135,22 @@
$context.find('[data-drupal-selector="edit-cdn"]').drupalSetSummary(function () {
var summary = [];
var $cdnProvider = $context.find('select[name="cdn_provider"] :selected');
- var cdnProvider = $cdnProvider.val();
if ($cdnProvider.length) {
var provider = $cdnProvider.text();
- var $version = $context.find('select[name="cdn_' + cdnProvider + '_version"] :selected');
+ var $version = $context.find('select[name="cdn_version"] :selected');
if ($version.length && $version.val().length) {
provider += ' - ' + $version.text();
- var $theme = $context.find('select[name="cdn_' + cdnProvider + '_theme"] :selected');
+ var $theme = $context.find('select[name="cdn_theme"] :selected');
if ($theme.length) {
provider += ' (' + $theme.text() + ')';
}
}
+ else if ($cdnProvider.val() === 'custom') {
+ var $urls = $context.find('textarea[name="cdn_custom"]');
+ var urls = ($urls.val() + '').split(/\r\n|\n/).filter(Boolean);
+ provider += ' (' + Drupal.formatPlural(urls.length, '1 URL', '@count URLs') + ')';
+ }
summary.push(provider);
}
@@ -197,10 +201,11 @@
}
},
complete: function () {
- $preview.parent().find('select[name="cdn_jsdelivr_theme"]').bind('change', function () {
+ $preview.parent().find('select[name="cdn_theme"]').bind('change', function () {
$preview.find('.bootswatch-preview').addClass('visually-hidden');
- if ($(this).val().length) {
- $preview.find('#bootstrap-theme-preview-' + $(this).val()).removeClass('visually-hidden');
+ var theme = $(this).val();
+ if (theme && theme.length) {
+ $preview.find('#bootstrap-theme-preview-' + theme).removeClass('visually-hidden');
}
}).change();
}
diff --git a/scripts/bootstrap.php b/scripts/bootstrap.php
new file mode 100644
index 0000000000000000000000000000000000000000..cbb2cb8c494c1679abe2bf41139c17a249ac3fb1
--- /dev/null
+++ b/scripts/bootstrap.php
@@ -0,0 +1,58 @@
+getAppRoot());
+
+// Initialize settings, this requires reflection since its a protected method.
+$request = Request::createFromGlobals();
+$initializeSettings = new \ReflectionMethod($kernel, 'initializeSettings');
+$initializeSettings->setAccessible(TRUE);
+$initializeSettings->invokeArgs($kernel, [$request]);
+
+// Boot the kernel.
+$kernel->boot();
+$kernel->preHandle($request);
+
+// Due to a core bug, the theme handler has to be invoked to register theme
+// namespaces with the autoloader.
+// @todo Remove once installed_extensions makes its way into core.
+// @see https://www.drupal.org/project/drupal/issues/2941757
+$container = $kernel->getContainer();
+if (!$container->has('installed_extensions')) {
+ $container->get('theme_handler')->listInfo();
+}
+
+return $kernel;
diff --git a/scripts/gen-theme-setting-docs.php b/scripts/gen-theme-setting-docs.php
new file mode 100755
index 0000000000000000000000000000000000000000..9a3ce159c23641e8252b738e997fa270c9507f4d
--- /dev/null
+++ b/scripts/gen-theme-setting-docs.php
@@ -0,0 +1,88 @@
+#!/usr/bin/env php
+getSettingPlugin(NULL, TRUE), function (SettingInterface $setting) {
+ return !!$setting->getGroups();
+});
+
+// Populate the variables with settings.
+$variables = ['groups' => []];
+$deprecatedSettings = [];
+$replacementPairs = [
+ '"' => '"',
+ '\n' => "\n",
+];
+foreach ($settings as $id => $setting) {
+ $defaultValue = $setting->getDefaultValue();
+ $deprecated = FALSE;
+ if ($setting instanceof DeprecatedSettingInterface) {
+ $newSetting = $setting->getDeprecatedReplacementSetting()->getPluginId();
+ $deprecated = [
+ 'reason' => new FormattableMarkup($setting->getDeprecatedReason(), []),
+ 'replacement' => new FormattableMarkup('@setting', [
+ '@anchor' => Html::cleanCssIdentifier($newSetting),
+ '@setting' => $newSetting,
+ ]),
+ 'version' => new FormattableMarkup($setting->getDeprecatedVersion(), []),
+ ];
+ }
+ $data = [
+ 'id' => $id,
+ 'description' => new FormattableMarkup(strtr($setting->getDescription(), $replacementPairs), []),
+ 'defaultValue' => $defaultValue !== NULL ? new FormattableMarkup(strtr(trim(Yaml::encode([$id => $defaultValue])), $replacementPairs), []) : NULL,
+ 'deprecated' => $deprecated,
+ ];
+
+ // Defer adding deprecated settings.
+ if ($deprecated) {
+ $deprecatedSettings[$id] = $data;
+ }
+ else {
+ // Only get the first two groups (we don't need 3rd, or more, levels).
+ $header = implode(' > ', array_slice(array_filter($setting->getGroups()), 0, 2, FALSE));
+ $variables['groups'][$header][$id] = $data;
+ }
+}
+
+// Add Deprecated settings last (special table).
+if ($deprecatedSettings) {
+ $variables['deprecated'] = $deprecatedSettings;
+}
+
+$docsPath = "{$bootstrap->getPath()}/docs";
+
+// Render the settings.
+$output = Bootstrap::renderCustomTemplate("{$docsPath}/theme-settings.twig", $variables);
+
+// Save the generated output to the appropriate file.
+$result = Bootstrap::putContents("{$docsPath}/Theme-Settings.md", $output, '', '');
+
+if ($result) {
+ echo 'Successfully generated theme documentation!';
+ exit(0);
+}
+
+echo 'Unable to generate theme documentation!';
+exit(1);
diff --git a/src/Bootstrap.php b/src/Bootstrap.php
index 2f288c9e61df0cacb2f9e52fc042860722537bd9..ce8c475bc345dca64cc73ada2e5d2c91b868cb21 100644
--- a/src/Bootstrap.php
+++ b/src/Bootstrap.php
@@ -9,14 +9,16 @@ use Drupal\bootstrap\Utility\Crypt;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Unicode;
use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Markup;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\user\Entity\User;
+use Drupal\user\UserInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
-use Symfony\Component\HttpFoundation\Response;
/**
* The primary class for the Drupal Bootstrap base theme.
@@ -101,8 +103,10 @@ class Bootstrap {
* The project API search URL.
*
* @var string
+ *
+ * @todo Enable constant once PHP 5.5 is no longer supported.
*/
- const PROJECT_API_SEARCH_URL = self::PROJECT_DOCUMENTATION . '/api/bootstrap/' . self::PROJECT_BRANCH . '/search/@query';
+// const PROJECT_API_SEARCH_URL = self::PROJECT_DOCUMENTATION . '/api/bootstrap/' . self::PROJECT_BRANCH . '/search/@query';
/**
* The Drupal Bootstrap project page.
@@ -111,6 +115,34 @@ class Bootstrap {
*/
const PROJECT_PAGE = 'https://www.drupal.org/project/bootstrap';
+ /**
+ * The Messenger service, if it exists.
+ *
+ * @var \Drupal\Core\Messenger\MessengerInterface
+ */
+ protected static $messenger;
+
+ /**
+ * The Renderer service.
+ *
+ * @var \Drupal\Core\Render\Renderer
+ */
+ protected static $renderer;
+
+ /**
+ * The Theme Registry service.
+ *
+ * @var \Drupal\Core\Theme\Registry
+ */
+ protected static $themeRegistry;
+
+ /**
+ * The Twig service.
+ *
+ * @var \Drupal\Core\Template\TwigEnvironment
+ */
+ protected static $twig;
+
/**
* Adds a callback to an array.
*
@@ -277,7 +309,8 @@ class Bootstrap {
* The complete URL to the documentation site.
*/
public static function apiSearchUrl($query = '') {
- return new FormattableMarkup(self::PROJECT_API_SEARCH_URL, [
+ // @todo Move to a constant once PHP 5.5 is no longer supported.
+ return new FormattableMarkup(self::PROJECT_DOCUMENTATION . '/api/bootstrap/' . self::PROJECT_BRANCH . '/search/@query', [
'@query' => $query,
]);
}
@@ -318,6 +351,67 @@ class Bootstrap {
return static::getTheme('bootstrap')->getPath() . '/autoload-fix.php';
}
+ public static function checkUrlIsReachable($url, array $options = [], &$exception = NULL) {
+ $options['method'] = 'HEAD';
+ $options['ttl'] = 0;
+ return static::request($url, $options, $exception);
+ }
+
+ /**
+ * Retrieves a response from a URL, using cached response if available.
+ *
+ * @param string $url
+ * The URL to retrieve.
+ * @param array $options
+ * The options to pass to the HTTP client.
+ * @param \Exception|null $exception
+ * The exception thrown if there was an error, passed by reference.
+ *
+ * @return \Drupal\bootstrap\SerializedResponse
+ * A Response object.
+ */
+ public static function request($url, array $options = [], &$exception = NULL) {
+ $options += [
+ 'method' => 'GET',
+ 'headers' => [
+ 'User-Agent' => 'Drupal Bootstrap ' . static::PROJECT_BRANCH . ' (' . static::PROJECT_PAGE . ')',
+ ],
+ ];
+
+ // Determine if a custom TTL value was set.
+ $ttl = isset($options['ttl']) ? $options['ttl'] : NULL;
+ unset($options['ttl']);
+
+ $cache = \Drupal::keyValueExpirable('theme:' . static::getTheme()->getName() . ':http');
+ $hash = Crypt::generateBase64HashIdentifier($options, ['request', $url]);
+ $response = $cache->get($hash);
+
+ if (!isset($response)) {
+ /** @var \GuzzleHttp\Client $client */
+ $client = \Drupal::service('http_client_factory')->fromOptions($options);
+ $request = new Request($options['method'], $url, $options['headers']);
+
+ try {
+ $response = SerializedResponse::createFromGuzzleResponse($client->send($request, $options), $request);
+ }
+ catch (GuzzleException $e) {
+ $exception = $e;
+ $response = SerializedResponse::createFromException($e, $request);
+ }
+ catch (\Exception $e) {
+ $exception = $e;
+ $response = SerializedResponse::createFromException($e, $request);
+ }
+
+ // Only cache if a maximum age has been detected.
+ if ($response->getStatusCode() == 200 && ($maxAge = isset($ttl) ? $ttl : $response->getMaxAge())) {
+ $cache->setWithExpire($hash, $response, $maxAge);
+ }
+ }
+
+ return $response;
+ }
+
/**
* Matches a Bootstrap class based on a string value.
*
@@ -440,11 +534,14 @@ class Bootstrap {
* support deprecated functionality.
* @param bool $show_message
* Flag indicating whether to show a message to the user. If TRUE, it will
- * force showing the message. If FALSE, it will only log the message. If
- * not set, showing the message will be determined by whether the current
- * theme has suppressed showing deprecated warnings.
+ * force show the message. If FALSE, it will only log the message. If not
+ * set, the message will be shown based on whether the current user is an
+ * administrator and if the theme has suppressed deprecated warnings.
+ * @param \Drupal\Core\StringTranslation\TranslatableMarkup $message
+ * Optional. The message to show/log. If not set, it will be determined
+ * automatically based on the caller.
*/
- public static function deprecated($caller = NULL, $show_message = NULL) {
+ public static function deprecated($caller = NULL, $show_message = NULL, TranslatableMarkup $message = NULL) {
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// Extrapolate the caller.
@@ -459,14 +556,16 @@ class Bootstrap {
$caller = array_pop($parts);
}
- $message = t('The following @type has been deprecated: @title. Please check the logs for a more detailed backtrace on where it is being invoked.', [
- '@type' => $method ? 'method' : 'function',
- ':url' => static::apiSearchUrl($caller),
- '@title' => $caller,
- ]);
+ if (!isset($message)) {
+ $message = t('The following @type has been deprecated: @title. Please check the logs for a more detailed backtrace on where it is being invoked.', [
+ '@type' => $method ? 'method' : 'function',
+ ':url' => static::apiSearchUrl($caller),
+ '@title' => $caller,
+ ]);
+ }
- if ($show_message || (!isset($show_message) && !static::getTheme()->getSetting('suppress_deprecated_warnings', FALSE))) {
- drupal_set_message($message, 'warning');
+ if ($show_message || (!isset($show_message) && static::isAdmin() && !static::getTheme()->getSetting('suppress_deprecated_warnings', FALSE))) {
+ static::message($message, 'warning');
}
// Log message and accompanying backtrace.
@@ -1101,6 +1200,26 @@ class Bootstrap {
}
}
+ /**
+ * Checks whether a user is an administrator.
+ *
+ * @param \Drupal\user\UserInterface $user
+ * Optional. A specific user to check. If not set, the currently logged in
+ * user will be used.
+ *
+ * @return bool
+ * TRUE or FALSE
+ */
+ public static function isAdmin(UserInterface $user = NULL) {
+ static $admins = [];
+ $user = $user ?: User::load(\Drupal::currentUser()->id());
+ $uid = (int) $user->id();
+ if (!isset($admins[$uid])) {
+ $admins[$uid] = $user->hasPermission('access administration pages');
+ }
+ return $admins[$uid];
+ }
+
/**
* Determines if the current path is the "front" page.
*
@@ -1134,6 +1253,45 @@ class Bootstrap {
return $is_front;
}
+ /**
+ * Wrapper to use new Messenger service or the legacy procedural function.
+ *
+ * This is to help support older installations without trigger deprecation
+ * notices for newer installations.
+ *
+ * @param string|\Drupal\Component\Render\MarkupInterface $message
+ * (optional) The translated message to be displayed to the user. For
+ * consistency with other messages, it should begin with a capital letter
+ * and end with a period.
+ * @param string $type
+ * (optional) The message's type. Defaults to 'status'. These values are
+ * supported:
+ * - 'status'
+ * - 'warning'
+ * - 'error'
+ * @param bool $repeat
+ * (optional) If this is FALSE and the message is already set, then the
+ * message won't be repeated. Defaults to FALSE.
+ *
+ * @see \Drupal\Core\Messenger\MessengerInterface
+ * @see drupal_set_message()
+ * @see https://www.drupal.org/node/2774931
+ *
+ * @deprecated in 8.x-3.18 and will be removed in a future release.
+ * Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead.
+ */
+ public static function message($message, $type = 'status', $repeat = FALSE) {
+ if (!isset(static::$messenger)) {
+ static::$messenger = \Drupal::hasService('messenger') ? \Drupal::service('messenger') : FALSE;
+ }
+ if (static::$messenger) {
+ static::$messenger->addMessage($message, $type, $repeat);
+ }
+ else {
+ drupal_set_message($message, $type, $repeat);
+ }
+ }
+
/**
* Preprocess theme hook variables.
*
@@ -1179,6 +1337,11 @@ class Bootstrap {
// Retrieve the preprocess manager for this theme.
$preprocess_manager = $preprocess_managers[$theme_name];
+ // Add a global "is_admin" variable back to all templates.
+ if (!isset($variables['is_admin'])) {
+ $variables['is_admin'] = static::isAdmin();
+ }
+
// Adds a global "is_front" variable back to all templates.
// @see https://www.drupal.org/node/2829585
if (!isset($variables['is_front'])) {
@@ -1212,78 +1375,132 @@ class Bootstrap {
}
/**
- * Retrieves a response from a URI, using cached response if available.
+ * Renders a custom Twig template not registered in the theme system.
*
- * @param string $uri
- * The URI to retrieve JSON from.
- * @param array $options
- * The options to pass to the HTTP client.
- * @param \Exception|null $exception
- * The exception thrown if there was an error, passed by reference.
+ * Note: any template ending in .html.twig will be registered with the theme
+ * system automatically (that is simply how it works). For HTML based
+ * standalone Twig templates, just use .twig (without the .html prefix). For
+ * other file types, you may still use a prefix for the IDE to recognize the
+ * file type (e.g. .css.twig).
*
- * @return \Symfony\Component\HttpFoundation\Response
- * A Response object.
+ * @param string $path
+ * The path to the template.
+ * @param array $variables
+ * The variables to pass to the template.
+ * @param \Drupal\Core\Render\RenderContext $renderContext
+ * Optional. A RenderContext object to pass to the renderer.
+ *
+ * @return \Drupal\Component\Render\MarkupInterface
+ * The rendered template.
+ *
+ * @throws \RuntimeException
+ * If $path does not exist.
+ * @throws \InvalidArgumentException
+ * If $path references a Twig template already registered in the theme
+ * system.
*/
- public static function cachedRequest($uri, array $options = [], &$exception = NULL) {
- $options += [
- 'method' => 'GET',
- 'headers' => [
- 'User-Agent' => 'Drupal Bootstrap ' . static::PROJECT_BRANCH . ' (' . static::PROJECT_PAGE . ')',
- ],
- ];
+ public static function renderCustomTemplate($path, array $variables = [], RenderContext $renderContext = NULL) {
+ $realpath = realpath($path);
+ if (!file_exists($realpath)) {
+ throw new \RuntimeException(sprintf('Template does not exist: %s', $realpath));
+ }
- // Determine if a custom TTL value was set.
- $ttl = isset($options['ttl']) ? $options['ttl'] : NULL;
- unset($options['ttl']);
+ // Ensure provided template isn't actually registered in the theme system.
+ $registry = static::themeRegistry()->get();
+ foreach ($registry as $hook => $info) {
+ // Only process template based theme hooks.
+ if (!isset($info['path']) || !isset($info['template'])) {
+ continue;
+ }
+ $registered = realpath($info['path'] . '/' . $info['template'] . '.html.twig');
+ if ($registered === $realpath) {
+ $basename = basename($path);
+ $example = "\n\n\$build = [\n '#theme' => '$hook',\n /* Other properties */\n];\n\Drupal::service('renderer')->renderPlain(\$build);\n\n";
+ throw new \InvalidArgumentException(sprintf('The template provided is not a standalone Twig template: "%s". This template is already registered in Drupal\'s Theme System as "%s". If this template is intended to be truly standalone, you can change the file extension from ".html.twig" to just ".twig". Otherwise, if this is a properly registered template in the Theme System, you should render it using Drupal\'s existing Render API and not this method: %s', $basename, $hook, $example));
+ }
+ }
- $cache = \Drupal::keyValueExpirable('theme:' . static::getTheme()->getName() . ':http');
- $key = 'request-' . Crypt::hashBase64(serialize(['uri' => $uri] + $options));
- $response = $cache->get($key);
+ $template = file_get_contents($realpath);
+ if (!isset($renderContext)) {
+ $renderContext = new RenderContext();
+ }
- if (!isset($response)) {
- /** @var \GuzzleHttp\Client $client */
- $client = \Drupal::service('http_client_factory')->fromOptions($options);
- $request = new Request($options['method'], $uri, $options['headers']);
+ // Render the template.
+ $output = static::renderer()->executeInRenderContext($renderContext, function () use ($template, $variables) {
+ return static::twig()->createTemplate($template)->render($variables);
+ });
- try {
- $r = $client->send($request, $options);
- // In order to actually cache the response, the contents must be
- // extracted from the stream before it's stored in the database.
- $response = new Response($r->getBody(TRUE)->getContents(), $r->getStatusCode(), $r->getHeaders());
- }
- catch (GuzzleException $e) {
- $exception = $e;
- $response = new Response($e->getCode() ?: 500, [], $e->getMessage());
- }
- catch (\Exception $e) {
- $exception = $e;
- $response = new Response($e->getCode() ?: 500, [], $e->getMessage());
- }
+ return Markup::create($output);
+ }
- // Only cache if a maximum age has been detected.
- if ($response->getStatusCode() == 200 && ($maxAge = isset($ttl) ? $ttl : $response->getMaxAge())) {
- $cache->setWithExpire($key, $response, $maxAge);
+ /**
+ * Helper function for writing data to the file system.
+ *
+ * Note: this is specifically designed with replacing chunks of existing
+ * data in mind.
+ *
+ * @param string $path
+ * The path to the file where the data will be written.
+ * @param string $data
+ * The data to write to $file.
+ * @param string $start
+ * Optional. A marker determining where to begin injecting $data.
+ * Note: this value is used within a regular expression.
+ * @param string $end
+ * Optional. A marker determining where to stop injecting $data. This is
+ * primarily useful for replacing a "chunk" of data within a file.
+ * Note: this value is used within a regular expression.
+ *
+ * @return bool
+ * TRUE if the file was successfully written, FALSE otherwise.
+ */
+ public static function putContents($path, $data, $start = NULL, $end = NULL) {
+ $realpath = realpath($path) ?: $path;
+
+ // Markers used, build regular expression to split any existing content.
+ if ($start || $end) {
+ $regExp = [];
+ if ($start) {
+ $regExp[] = preg_quote($start, '/');
}
+ if ($end) {
+ $regExp[] = preg_quote($end, '/');
+ }
+ $regExp = implode('|', $regExp);
+ $parts = @preg_split("/$regExp/", @file_get_contents($realpath) ?: '') ?: [];
+ $replaced = isset($parts[0]) ? trim($parts[0]) . "\n" : '';
+ $replaced .= "$data\n";
+ $replaced .= isset($parts[2]) ? trim($parts[2]) . "\n" : '';
+ $data = $replaced;
}
- return $response;
+ return !!file_put_contents($realpath, $data) !== FALSE;
}
/**
- * Retrieves JSON from a URI.
+ * Retrieves the Renderer service.
*
- * @param string $uri
- * The URI to retrieve JSON from.
- * @param array $options
- * The options to pass to the HTTP client.
- * @param \Exception|null $exception
- * The exception thrown if there was an error, passed by reference.
+ * @return \Drupal\Core\Render\Renderer
+ * The Renderer service.
+ */
+ public static function renderer() {
+ if (!isset(static::$renderer)) {
+ static::$renderer = \Drupal::service('renderer');
+ }
+ return static::$renderer;
+ }
+
+ /**
+ * Retrieves the Theme Registry service.
*
- * @return \Drupal\bootstrap\JsonResponse
- * A JsonResponse object.
+ * @return \Drupal\Core\Theme\Registry
+ * The Theme Registry service.
*/
- public static function requestJson($uri, array $options = [], &$exception = NULL) {
- return JsonResponse::createFromResponse(static::cachedRequest($uri, $options, $exception));
+ public static function themeRegistry() {
+ if (!isset(static::$themeRegistry)) {
+ static::$themeRegistry = \Drupal::service('theme.registry');
+ }
+ return static::$themeRegistry;
}
/**
@@ -1299,4 +1516,17 @@ class Bootstrap {
return (string) (Element::isRenderArray($value) ? Element::create($value)->renderPlain() : $value);
}
+ /**
+ * Retrieves the Twig service.
+ *
+ * @return \Drupal\Core\Template\TwigEnvironment
+ * The Twig service.
+ */
+ public static function twig() {
+ if (!isset(static::$twig)) {
+ static::$twig = \Drupal::service('twig');
+ }
+ return static::$twig;
+ }
+
}
diff --git a/src/DeprecatedInterface.php b/src/DeprecatedInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..45f9b3644f5b6ddf596aeb98ce793bf02d7d0a47
--- /dev/null
+++ b/src/DeprecatedInterface.php
@@ -0,0 +1,34 @@
+json = Json::decode($content ?: '[]') ?: [];
- }
-
- /**
- * Creates a new JsonResponse object from a Symfony Response object.
- *
- * @param \Symfony\Component\HttpFoundation\Response $response
- * A Symfony Response object.
- *
- * @return \Drupal\bootstrap\JsonResponse
- * A JsonResponse object.
- */
- public static function createFromResponse(Response $response) {
- return new static($response->getContent(), $response->getStatusCode(), $response->headers->all());
- }
-
- /**
- * Retrieves the JSON array.
- *
- * @return array
- * The JSON array.
- */
- public function getJson(): array {
- return $this->json;
- }
-
-}
diff --git a/src/Plugin/Alter/LibraryInfo.php b/src/Plugin/Alter/LibraryInfo.php
index 437b85dd4e3bf537f6f346b88a111caff30f720f..09dbe8a2c86359efa5121d2e39c115c0a5953507 100644
--- a/src/Plugin/Alter/LibraryInfo.php
+++ b/src/Plugin/Alter/LibraryInfo.php
@@ -36,9 +36,7 @@ class LibraryInfo extends PluginBase implements AlterInterface {
}
// Alter the framework library based on currently set CDN Provider.
- if ($cdnProvider = $this->theme->getCdnProvider()) {
- $cdnProvider->alterFrameworkLibrary($libraries['framework']);
- }
+ $this->theme->getCdnProvider()->alterFrameworkLibrary($libraries['framework']);
}
// Core replacements.
elseif ($extension === 'core') {
diff --git a/src/Plugin/Form/SystemThemeSettings.php b/src/Plugin/Form/SystemThemeSettings.php
index 8947289ac5565ad4077e3d54d238f4b0a4c35507..7b13c85d95044f3f0bacbb7b61404ce7a44730b9 100644
--- a/src/Plugin/Form/SystemThemeSettings.php
+++ b/src/Plugin/Form/SystemThemeSettings.php
@@ -31,6 +31,10 @@ class SystemThemeSettings extends FormBase implements FormInterface {
// Iterate over all setting plugins and add them to the form.
foreach ($theme->getSettingPlugin() as $setting) {
+ // Skip settings that shouldn't be created automatically.
+ if (!$setting->autoCreateFormElement()) {
+ continue;
+ }
$setting->alterForm($form->getArray(), $form_state);
}
}
@@ -198,6 +202,12 @@ class SystemThemeSettings extends FormBase implements FormInterface {
// Retrieve the submitted value.
$value = $form_state->getValue($name);
+ // Trim any new lines and convert to simple new line breaks.
+ $definition = $setting->getPluginDefinition();
+ if (isset($definition['type']) && $definition['type'] === 'textarea' && is_string($value)) {
+ $value = implode("\n", array_filter(array_map('trim', preg_split("/\r\n|\n/", $value))));
+ }
+
// Determine if the setting has a new value that overrides the original.
// Ignore the schemas "setting" because it's handled by UpdateManager.
if ($name !== 'schemas' && $settings->overridesValue($name, $value)) {
diff --git a/src/Plugin/Preprocess/ContainerHelpBlock.php b/src/Plugin/Preprocess/ContainerHelpBlock.php
new file mode 100644
index 0000000000000000000000000000000000000000..63b099383f1bfa00f31bc8e49d9abf45e52b2b36
--- /dev/null
+++ b/src/Plugin/Preprocess/ContainerHelpBlock.php
@@ -0,0 +1,23 @@
+addClass('help-block');
+ }
+
+}
diff --git a/src/Plugin/Provider/ApiProviderBase.php b/src/Plugin/Provider/ApiProviderBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..3215dd2021bcba4b2b7c7eb22201b9018ea175cc
--- /dev/null
+++ b/src/Plugin/Provider/ApiProviderBase.php
@@ -0,0 +1,367 @@
+supportsThemes()) {
+ $themes = $this->getCdnThemes($version);
+ return isset($themes[$theme]) ? $themes[$theme] : new CdnAssets();
+ }
+ return $this->requestApiAssets('bootstrap', $version, $this->getCacheTtl(static::CACHE_ASSETS));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function discoverCdnThemes($version) {
+ $assets = new CdnAssets();
+ foreach (['bootstrap', 'bootswatch'] as $library) {
+ $assets = $this->requestApiAssets($library, $version, $this->getCacheTtl(static::CACHE_THEMES), $assets);
+ }
+ return $assets->getThemes();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function discoverCdnVersions() {
+ return $this->requestApiVersions('bootstrap', $this->getCacheTtl(static::CACHE_VERSIONS));
+ }
+
+ /**
+ * Retrieves the URL to use for determining available versions from the API.
+ *
+ * @param string $library
+ * The library to request.
+ * @param string $version
+ * The version to request.
+ *
+ * @return string
+ * The API URL to use.
+ */
+ protected function getApiAssetsUrl($library, $version) {
+ return (string) new FormattableMarkup($this->getApiAssetsUrlTemplate(), [
+ '@library' => Markup::create($this->mapLibrary($library)),
+ '@version' => Markup::create($this->mapVersion($version, $library)),
+ ]);
+ }
+
+ /**
+ * Retrieves the API URL template to use when requesting a specific asset.
+ *
+ * Available placeholders (must be prepended with an at symbol, @):
+ * - library - The library to request.
+ * - version - The version to request.
+ *
+ * @return string
+ * The CDN URL template.
+ */
+ abstract protected function getApiAssetsUrlTemplate();
+
+ /**
+ * Retrieves the URL to use for determining available versions from the API.
+ *
+ * @param string $library
+ * The library to request.
+ *
+ * @return string
+ * The API URL to use.
+ */
+ protected function getApiVersionsUrl($library) {
+ return (string) new FormattableMarkup($this->getApiVersionsUrlTemplate(), [
+ '@library' => Markup::create($this->mapLibrary($library)),
+ ]);
+ }
+
+ /**
+ * Retrieves the API URL template to use for determining available versions.
+ *
+ * Available placeholders (must be prepended with an at symbol, @):
+ * - library - The specific library being requested.
+ *
+ * @return string
+ * The CDN URL template.
+ */
+ abstract protected function getApiVersionsUrlTemplate();
+
+ /**
+ * Retrieves a CDN URL based on provided variables.
+ *
+ * @param string $library
+ * The library to request.
+ * @param string $version
+ * The version to request.
+ * @param string $file
+ * The file to request.
+ * @param array $info
+ * Additional information about the file, if any.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAsset
+ * A CDN URL.
+ */
+ protected function getCdnUrl($library, $version, $file, array $info = []) {
+ $library = $this->mapLibrary($library);
+ $version = $this->mapVersion($version, $library);
+
+ // Check if the "file" is really a fully qualified URL.
+ if (UrlHelper::isExternal($file)) {
+ $url = $file;
+ }
+ // Otherwise, use the template.
+ else {
+ $url = (string) new FormattableMarkup($this->getCdnUrlTemplate(), [
+ '@library' => Markup::create($library),
+ '@version' => Markup::create($version),
+ '@file' => Markup::create(ltrim($file, '/')),
+ ]);
+ }
+
+ return new CdnAsset($url, $library, $version, $info);
+ }
+
+ /**
+ * Retrieves the CDN URL template to use.
+ *
+ * Available placeholders (must be prepended with an at symbol, @):
+ * - library - The library to request.
+ * - version - The version to request.
+ * - file - The file to request.
+ * - theme - The theme to request.
+ *
+ * @return string
+ * The CDN URL template.
+ */
+ abstract protected function getCdnUrlTemplate();
+
+ /**
+ * Checks whether a version is valid.
+ *
+ * @param string $version
+ * The version to check.
+ *
+ * @return bool
+ * TRUE or FALSE
+ *
+ * @todo Move regular expression to a constant once PHP 5.5 is no longer
+ * supported.
+ */
+ public static function isValidVersion($version) {
+ return !!is_string($version) && preg_match('/^' . Bootstrap::FRAMEWORK_VERSION[0] . '\.\d+\.\d+$/', $version);
+ }
+
+ /**
+ * Allows providers a way to map a library to a different library.
+ *
+ * @param string $library
+ * The library to map.
+ *
+ * @return string
+ * The mapped library.
+ */
+ protected function mapLibrary($library) {
+ return $library;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function mapVersion($version, $library = NULL) {
+ $mapped = [];
+
+ // While the Bootswatch project attempts to maintain version parity with
+ // Bootstrap, it doesn't always happen. This causes issues when the system
+ // expects a 1:1 version match between Bootstrap and Bootswatch.
+ // @see https://github.com/thomaspark/bootswatch/issues/892#ref-issue-410070082
+ if ($library === 'bootswatch') {
+ // This version is "broken" because of jsDelivr's API limit.
+ $mapped['3.4.1'] = '3.4.0';
+ // This version doesn't exist.
+ $mapped['3.1.1'] = '3.2.0';
+ }
+
+ return isset($mapped[$version]) ? $mapped[$version] : $version;
+ }
+
+ /**
+ * Parses assets provided by the API data.
+ *
+ * @param array $data
+ * The data to parse.
+ * @param string $library
+ * The base URL each one of the $files are relative to, this usually
+ * should also include the version path prefix as well.
+ * @param string $version
+ * A specific version to use.
+ * @param \Drupal\bootstrap\Plugin\Provider\CdnAssets $assets
+ * An existing CdnAssets object, if chaining multiple requests together.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAssets
+ * A CdnAssets object containing the necessary assets.
+ */
+ protected function parseAssets(array $data, $library, $version, CdnAssets $assets = NULL) {
+ if (!isset($assets)) {
+ $assets = new CdnAssets();
+ }
+
+ $files = [];
+ // Support APIs that have a dedicated "files" property.
+ if (isset($data['files'])) {
+ $files = $data['files'];
+ }
+ elseif (isset($data['assets'])) {
+ foreach ($data['assets'] as $asset) {
+ // Support APIs that clump all the assets together, regardless of their
+ // versions. Skip assets that don't match this version.
+ if (isset($asset['version']) && $asset['version'] !== $version) {
+ continue;
+ }
+ // Found the necessary files for the specified version.
+ if (!empty($asset['files'])) {
+ $files = $asset['files'];
+ break;
+ }
+ }
+ }
+ foreach ($files as $file) {
+ // Support APIs that simply use simple strings as files.
+ if (is_string($file) && CdnAsset::isFileValid($file)) {
+ $assets->append($this->getCdnUrl($library, $version, $file));
+ }
+ // Support APIs that put each file into its own array (metadata).
+ elseif (is_array($file)) {
+ // Support APIs that clump all the files together, regardless of their
+ // versions. Skip assets that don't match this version.
+ if (isset($file['version']) && $file['version'] !== $version) {
+ continue;
+ }
+ // Support multiple keys for the "file".
+ foreach (['filename', 'name', 'url', 'uri', 'path'] as $key) {
+ if (!empty($file[$key]) && CdnAsset::isFileValid($file[$key])) {
+ $assets->append($this->getCdnUrl($library, $version, $file[$key], $file));
+ break;
+ }
+ }
+ }
+ }
+
+ return $assets;
+ }
+
+ /**
+ * Parses available versions provided by the API data.
+ *
+ * @param array $data
+ * The data to parse.
+ *
+ * @return array
+ * An associative array of versions, keyed by version.
+ */
+ protected function parseVersions(array $data = []) {
+ $versions = [];
+
+ // Support APIs that have a dedicated "versions" property.
+ if (!empty($data['versions'])) {
+ foreach ($data['versions'] as $version) {
+ // Only extract valid versions.
+ if ($this->isValidVersion($version)) {
+ $versions[$version] = $version;
+ }
+ }
+ }
+ // Support APIs that have the version nested under individual assets.
+ elseif (!empty($data['assets'])) {
+ foreach ($data['assets'] as $asset) {
+ if (isset($asset['version']) && $this->isValidVersion($asset['version'])) {
+ $versions[$asset['version']] = $asset['version'];
+ }
+ }
+ }
+
+ return $versions;
+ }
+
+ /**
+ * Requests available assets from the CDN Provider API.
+ *
+ * @param string $library
+ * The library to request.
+ * @param string $version
+ * The version to request.
+ * @param int $ttl
+ * Optional. A specific TTL value to use for caching the HTTP request. If
+ * not set, it will default to whatever is returned by the HTTP request.
+ * @param \Drupal\bootstrap\Plugin\Provider\CdnAssets $assets
+ * An existing CdnAssets object, if chaining multiple requests together.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAssets
+ * The CdnAssets provided by the API.
+ */
+ protected function requestApiAssets($library, $version, $ttl = NULL, CdnAssets $assets = NULL) {
+ $url = $this->getApiAssetsUrl($library, $version);
+ $data = $this->request($url, ['ttl' => $ttl])->getData();
+
+ // If bootstrap data could not be returned, provide defaults.
+ if (!$data && $this->cdnExceptions && $library === 'bootstrap') {
+ $data = [
+ 'files' => [
+ '/dist/css/bootstrap.css',
+ '/dist/js/bootstrap.js',
+ '/dist/css/bootstrap.min.css',
+ '/dist/js/bootstrap.min.js',
+ ],
+ ];
+ }
+
+ // Parse the files from data.
+ return $this->parseAssets($data, $library, $version, $assets);
+ }
+
+ /**
+ * Requests available versions from the CDN Provider API.
+ *
+ * @param string $library
+ * The library to request versions for.
+ * @param int $ttl
+ * Optional. A specific TTL value to use for caching the HTTP request. If
+ * not set, it will default to whatever is returned by the HTTP request.
+ *
+ * @return array
+ * An associative array of versions, keyed by version.
+ */
+ public function requestApiVersions($library, $ttl = NULL) {
+ $url = $this->getApiVersionsUrl($library);
+ $data = $this->request($url, ['ttl' => $ttl])->getData();
+
+ // If bootstrap data could not be returned, provide defaults.
+ if (!$data && $this->cdnExceptions && $library === 'bootstrap') {
+ $data = ['versions' => [Bootstrap::FRAMEWORK_VERSION]];
+ }
+
+ return $this->parseVersions($data);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @deprecated in 8.x-3.18, will be removed in a future release.
+ */
+ public function processDefinition(array &$definition, $plugin_id) {
+ // Intentionally left blank so it doesn't trigger a deprecation warning.
+ }
+
+}
diff --git a/src/Plugin/Provider/BootstrapCdn.php b/src/Plugin/Provider/BootstrapCdn.php
new file mode 100644
index 0000000000000000000000000000000000000000..4d89c4f1ad7fa94171762f560259c14ef3dbe486
--- /dev/null
+++ b/src/Plugin/Provider/BootstrapCdn.php
@@ -0,0 +1,42 @@
+David Henzel and Justin Dorfman at MaxCDN. Today, BootstrapCDN is used by over 7.9 million sites delivering over 70 billion requests a month.", arguments = {
+ * ":DavidHenzel" = "https://twitter.com/DavidHenzel",
+ * ":built_with" = "https://trends.builtwith.com/cdn/BootstrapCDN",
+ * }),
+ * )
+ */
+class BootstrapCdn extends ApiProviderBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getApiAssetsUrlTemplate() {
+ return 'https://www.bootstrapcdn.com/api/v1/@library/@version';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getApiVersionsUrlTemplate() {
+ return 'https://www.bootstrapcdn.com/api/v1/@library';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getCdnUrlTemplate() {
+ return 'https://stackpath.bootstrapcdn.com/@library/@version/@file';
+ }
+
+}
diff --git a/src/Plugin/Provider/Broken.php b/src/Plugin/Provider/Broken.php
index 72f26481358053deb179b178c13fef35e01f924d..4f82e938532b6765a58d6b51764c0f1218b295a2 100644
--- a/src/Plugin/Provider/Broken.php
+++ b/src/Plugin/Provider/Broken.php
@@ -2,8 +2,6 @@
namespace Drupal\bootstrap\Plugin\Provider;
-use Drupal\bootstrap\Plugin\PluginBase;
-
/**
* Broken CDN Provider instance.
*
@@ -12,9 +10,10 @@ use Drupal\bootstrap\Plugin\PluginBase;
* @BootstrapProvider(
* id = "_broken",
* label = @Translation("Broken"),
+ * description = @Translation("Broken CDN Provider instance."),
* )
*/
-class Broken extends PluginBase implements ProviderInterface {
+class Broken extends ProviderBase {
/**
* {@inheritdoc}
@@ -34,7 +33,7 @@ class Broken extends PluginBase implements ProviderInterface {
* {@inheritdoc}
*/
public function getCdnAssets($version = NULL, $theme = NULL) {
- return [];
+ return new CdnAssets();
}
/**
@@ -55,7 +54,7 @@ class Broken extends PluginBase implements ProviderInterface {
* {@inheritdoc}
*/
public function getCdnThemes($version = NULL) {
- return [];
+ return new CdnAssets();
}
/**
@@ -75,22 +74,22 @@ class Broken extends PluginBase implements ProviderInterface {
/**
* {@inheritdoc}
*/
- public function getDescription() {
- return $this->t('Broken CDN Provider instance.');
+ public function resetCache() {
+ // Intentionally left empty.
}
/**
* {@inheritdoc}
*/
- public function getLabel() {
- return $this->t('Broken');
+ public function supportsThemes() {
+ return FALSE;
}
/**
* {@inheritdoc}
*/
- public function resetCache() {
- // Intentionally left empty.
+ public function supportsVersions() {
+ return FALSE;
}
/****************************************************************************
@@ -99,60 +98,6 @@ class Broken extends PluginBase implements ProviderInterface {
*
***************************************************************************/
- /**
- * {@inheritdoc}
- *
- * @deprecated in 8.x-3.18, will be removed in a future release.
- */
- public function getApi() {
- return NULL;
- }
-
- /**
- * {@inheritdoc}
- *
- * @deprecated in 8.x-3.18, will be removed in a future release.
- */
- public function getAssets($types = NULL) {
- return [];
- }
-
- /**
- * {@inheritdoc}
- *
- * @deprecated in 8.x-3.18, will be removed in a future release.
- */
- public function getThemes() {
- return [];
- }
-
- /**
- * {@inheritdoc}
- *
- * @deprecated in 8.x-3.18, will be removed in a future release.
- */
- public function getVersions() {
- return [];
- }
-
- /**
- * {@inheritdoc}
- *
- * @deprecated in 8.x-3.18, will be removed in a future release.
- */
- public function hasError() {
- return FALSE;
- }
-
- /**
- * {@inheritdoc}
- *
- * @deprecated in 8.x-3.18, will be removed in a future release.
- */
- public function isImported() {
- return FALSE;
- }
-
/**
* {@inheritdoc}
*
diff --git a/src/Plugin/Provider/CdnAsset.php b/src/Plugin/Provider/CdnAsset.php
new file mode 100644
index 0000000000000000000000000000000000000000..1115f721f91f3cbb2bdf4916034701d5b9a15a14
--- /dev/null
+++ b/src/Plugin/Provider/CdnAsset.php
@@ -0,0 +1,351 @@
+ [
+ 'cerulean',
+ 'cosmo',
+ 'cyborg',
+ 'darkly',
+ 'flatly',
+ 'journal',
+ 'lumen',
+ 'paper',
+ 'readable',
+ 'sandstone',
+ 'simplex',
+ 'slate',
+ 'spacelab',
+ 'superhero',
+ 'united',
+ 'yeti',
+ ],
+ 4 => [
+ 'cerulean',
+ 'cosmo',
+ 'cyborg',
+ 'darkly',
+ 'flatly',
+ 'journal',
+ 'litera',
+ 'lumen',
+ 'lux',
+ 'materia',
+ 'minty',
+ 'pulse',
+ 'sandstone',
+ 'simplex',
+ 'sketchy',
+ 'slate',
+ 'solar',
+ 'spacelab',
+ 'superhero',
+ 'united',
+ 'yeti',
+ ],
+ ];
+
+ /**
+ * A unique identifier.
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * Additional information supplied from the CDN API.
+ *
+ * @var array
+ */
+ protected $info;
+
+ /**
+ * A human readable label for the CDN Asset.
+ *
+ * @var \Drupal\Component\Render\MarkupInterface
+ */
+ protected $label;
+
+ /**
+ * The library this URL references.
+ *
+ * @var string
+ */
+ protected $library;
+
+ /**
+ * Flag indicating whether the URL is minified.
+ *
+ * @var bool
+ */
+ protected $minified;
+
+ /**
+ * The theme this URL references.
+ *
+ * @var string
+ */
+ protected $theme;
+
+ /**
+ * The type of resource, e.g. css or js.
+ *
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * The URL.
+ *
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * The version this URL references.
+ *
+ * @var string
+ */
+ protected $version;
+
+ /**
+ * CdnAsset constructor.
+ *
+ * @param string $url
+ * The absolute URL to to the CDN asset.
+ * @param string $library
+ * The specific library this asset is associated with, if known.
+ * @param string $version
+ * The specific version this asset is associated with, if known.
+ * @param array $info
+ * Additional information provided by the CDN.
+ */
+ public function __construct($url, $library = NULL, $version = NULL, array $info = []) {
+ // Extract the necessary data from the file.
+ list($path, $example, $minified, $type) = static::extractParts($url);
+
+ // Bootstrap's example theme.
+ if ($example) {
+ $theme = 'bootstrap_theme';
+ $label = $this->t('Example Theme');
+ $library = 'bootstrap';
+ }
+ // Core bootstrap library.
+ elseif ($path === 'css' || $path === 'js') {
+ $theme = 'bootstrap';
+ $label = $this->t('Default');
+ $library = 'bootstrap';
+ }
+ // Other (e.g. bootswatch theme).
+ else {
+ $bootswatchThemes = isset(static::$bootswatchThemes[Bootstrap::FRAMEWORK_VERSION[0]]) ? static::$bootswatchThemes[Bootstrap::FRAMEWORK_VERSION[0]] : [];
+ $theme = in_array($path, $bootswatchThemes) ? $path : 'bootstrap';
+ $label = new HtmlEscapedText(ucfirst($theme));
+ if (!isset($library)) {
+ $library = in_array($path, $bootswatchThemes) ? 'bootswatch' : 'unknown';
+ }
+ }
+
+ // If no version was provided, attempt to extract it.
+ // @todo Move regular expression to a constant once PHP 5.5 is no longer
+ // supported.
+ if (!isset($version) && preg_match('`(' . Bootstrap::FRAMEWORK_VERSION[0] . '\.\d+\.\d+)`', $url, $matches)) {
+ $version = $matches[1];
+ }
+
+ $this->id = Crypt::generateBase64HashIdentifier([
+ 'url' => $url,
+ 'info' => $info,
+ ], [$library, $version, $theme, basename($url)]);
+ $this->info = $info;
+ $this->label = $label;
+ $this->library = $library;
+ $this->minified = $minified;
+ $this->theme = $theme;
+ $this->type = $type;
+ $this->url = $url;
+ $this->version = $version;
+ }
+
+ /**
+ * Extracts the necessary parts of the URL.
+ *
+ * @param string $url
+ * The URL to parse.
+ *
+ * @return array
+ */
+ protected static function extractParts($url) {
+ preg_match(static::VALID_FILE_REGEXP, $url, $matches);
+ $path = isset($matches[1]) ? mb_strtolower(Html::escape($matches[1])) : NULL;
+ $example = isset($matches[2]) ? !!$matches[2] : FALSE;
+ $minified = isset($matches[3]) ? !!$matches[3] : FALSE;
+ $type = isset($matches[4]) ? mb_strtolower(Html::escape($matches[4])) : NULL;
+ return [$path, $example, $minified, $type];
+ }
+
+ /**
+ * Indicates whether the provided URL is valid.
+ *
+ * @param string $url
+ * The URL to check.
+ *
+ * @return bool
+ * TRUE or FALSE
+ */
+ public static function isFileValid($url) {
+ if (preg_match(static::INVALID_FILE_REGEXP, $url)) {
+ return FALSE;
+ }
+ list($path, $example, $minified, $type) = static::extractParts($url);
+ return $path && $type;
+ }
+
+ /**
+ * Retrieves the unique identifier for this asset.
+ *
+ * @return string
+ * The unique identifier.
+ */
+ public function getId() {
+ return $this->id;
+ }
+
+ /**
+ * Retrieves information provided by the CDN API, if available.
+ *
+ * @param string $key
+ * Optional. A specific key of the item to retrieve. Note: this can be in
+ * the form of dot notation if the value is nested in an array. If not
+ * provided, the entire contents of the info will be returned.
+ * @param mixed $default
+ * The default value to use if $key was provided and does not exist.
+ *
+ * @return mixed
+ * The specified information or the entire contents of the array if $key
+ * was not provided.
+ */
+ public function getInfo($key = NULL, $default = NULL) {
+ $info = $this->info ?: [];
+ if (isset($key)) {
+ $parts = Unicode::splitDelimiter($key);
+ $value = NestedArray::getValue($info, $parts, $exists);
+ return $exists ? $value : $default;
+ }
+ return $info;
+ }
+
+ /**
+ * Retrieves the human readable label.
+ *
+ * @return \Drupal\Component\Render\MarkupInterface
+ * The label.
+ */
+ public function getLabel() {
+ return $this->label;
+ }
+
+ /**
+ * Retrieves the library this CDN asset is associated with, if any.
+ *
+ * @return string
+ * The library.
+ */
+ public function getLibrary() {
+ return $this->library;
+ }
+
+ /**
+ * Retrieves the theme this CDN asset is associated with, if any.
+ *
+ * @return string
+ * The theme.
+ */
+ public function getTheme() {
+ return $this->theme;
+ }
+
+ /**
+ * Retrieves the type of CDN asset this is (e.g. css or js).
+ *
+ * @return string
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * Retrieves the absolute URL this CDN asset represents.
+ *
+ * @return string
+ */
+ public function getUrl() {
+ return $this->url;
+ }
+
+ /**
+ * Retrieves the version this CDN asset is associated with, if any.
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return $this->version;
+ }
+
+ /**
+ * Indicates whether the CDN asset is minified.
+ *
+ * @return bool
+ * TRUE or FALSE
+ */
+ public function isMinified() {
+ return $this->minified;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ return $this->getUrl();
+ }
+
+}
diff --git a/src/Plugin/Provider/CdnAssets.php b/src/Plugin/Provider/CdnAssets.php
new file mode 100644
index 0000000000000000000000000000000000000000..bbdb935c05c291134bfea2f676496a49b72d37f9
--- /dev/null
+++ b/src/Plugin/Provider/CdnAssets.php
@@ -0,0 +1,353 @@
+appendAssets($assets);
+ }
+
+ /**
+ * Retrieves all assets.
+ *
+ * @param bool|bool[] $minified
+ * Flag indicating whether only the minified asset should be retrieved.
+ * This can be an associative array where the key is the asset type and
+ * the value is a boolean indicating whether to use minified assets for
+ * that specific type. If not set, all assets are retrieved regardless
+ * if they are minified or not.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAsset[]
+ * An array of CdnAsset objects.
+ */
+ public function all($minified = NULL) {
+ $assets = [];
+ if (isset($minified) && !is_array($minified)) {
+ $minified = ['css' => !!$minified, 'js' => !!$minified];
+ }
+ foreach (['css', 'js'] as $type) {
+ $assets = array_merge($assets, $this->get($type, isset($minified[$type]) ? $minified[$type] : NULL));
+ }
+ return $assets;
+ }
+
+ /**
+ * Appends a CdnAsset object to the list.
+ *
+ * @param \Drupal\bootstrap\Plugin\Provider\CdnAsset $asset
+ * A CdnAsset object.
+ */
+ public function append(CdnAsset $asset) {
+ if (isset($this->assets[$asset->getId()])) {
+ $this->assets[$asset->getId()] = $asset;
+ }
+ else {
+ $this->assets = array_merge($this->assets, [$asset->getId() => $asset]);
+ }
+ }
+
+ /**
+ * Appends an array of CdnAsset objects to the list.
+ *
+ * @param \Drupal\bootstrap\Plugin\Provider\CdnAsset[] $assets
+ * An array of CdnAsset objects.
+ */
+ public function appendAssets(array $assets) {
+ foreach ($assets as $asset) {
+ $this->append($asset);
+ }
+ }
+
+ /**
+ * Retrieves specific types of assets.
+ *
+ * @param string $type
+ * The type of assets to retrieve (e.g. css or js).
+ * @param bool|bool[] $minified
+ * Flag indicating whether only the minified asset should be retrieved.
+ * This can be an associative array where the key is the asset type and
+ * the value is a boolean indicating whether to use minified assets for
+ * that specific type. If not set, all assets are retrieved regardless
+ * if they are minified or not.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAsset[]
+ * An array of CdnAsset objects.
+ */
+ public function get($type, $minified = NULL) {
+ // Filter by type.
+ $assets = array_filter($this->assets, function (CdnAsset $asset) use ($type) {
+ return $asset->getType() === $type;
+ });
+
+ // Filter assets by matching minification value.
+ if (isset($minified)) {
+ $assets = array_filter($assets, function (CdnAsset $asset) use ($minified) {
+ return $asset->isMinified() === $minified;
+ });
+ }
+
+ return $assets;
+ }
+
+ /**
+ * Retrieves the human readable label.
+ *
+ * Note: if the label isn't yet set, it will attempt to retrieve the label
+ * from the first available asset.
+ *
+ * @return \Drupal\Component\Render\MarkupInterface
+ * The label.
+ */
+ public function getLabel() {
+ if (!isset($this->label)) {
+ $asset = reset($this->assets);
+ $this->label = $asset ? $asset->getLabel() : NULL;
+ }
+ return $this->label;
+ }
+
+ /**
+ * Retrieves the library associated with these assets.
+ *
+ * Note: if the library isn't yet set, it will attempt to retrieve the library
+ * from the first available asset.
+ *
+ * @return \Drupal\Component\Render\MarkupInterface
+ * The library.
+ */
+ public function getLibrary() {
+ if (!isset($this->library)) {
+ $asset = reset($this->assets);
+ $this->library = $asset ? $asset->getLibrary() : NULL;
+ }
+ return $this->library;
+ }
+
+ /**
+ * Groups available assets by theme.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAssets[]
+ * A collection of newly created CdnAssets objects, keyed by theme name.
+ */
+ public function getThemes() {
+ /** @var \Drupal\bootstrap\Plugin\Provider\CdnAssets[] $themes */
+ $themes = [];
+ foreach ($this->assets as $asset) {
+ $theme = $asset->getTheme();
+ if (!isset($themes[$theme])) {
+ $themes[$theme] = (new static())
+ ->setLabel($asset->getLabel())
+ ->setLibrary($asset->getLibrary());
+ }
+ $themes[$theme]->append($asset);
+ }
+
+ // Sort the themes.
+ uksort($themes, [$this, 'sortThemes']);
+
+ // Post process the themes to fill in any missing assets.
+ $bootstrap = isset($themes['bootstrap']) ? $themes['bootstrap'] : new static();
+ foreach (array_keys($themes) as $theme) {
+ // The example Bootstrap theme are just overrides, it requires the main
+ // bootstrap library CSS to be loaded first.
+ if ($theme === 'bootstrap_theme') {
+ if ($css = $bootstrap->get('css', TRUE)) {
+ $themes['bootstrap_theme']->prependAssets($css);
+ }
+ if ($css = $bootstrap->get('css', FALSE)) {
+ $themes['bootstrap_theme']->prependAssets($css);
+ }
+ }
+
+ // Populate missing JavaScript.
+ if (!$themes[$theme]->get('js', TRUE)) {
+ if ($js = $bootstrap->get('js', FALSE)) {
+ $themes[$theme]->appendAssets($js);
+ }
+ if ($js = $bootstrap->get('js', TRUE)) {
+ $themes[$theme]->appendAssets($js);
+ }
+ }
+ }
+
+ return $themes;
+ }
+
+ /**
+ * Prepends a CdnAsset object to the list.
+ *
+ * @param \Drupal\bootstrap\Plugin\Provider\CdnAsset $asset
+ * A CdnAsset object.
+ */
+ public function prepend(CdnAsset $asset) {
+ if (isset($this->assets[$asset->getId()])) {
+ $this->assets[$asset->getId()] = $asset;
+ }
+ else {
+ $this->assets = array_merge([$asset->getId() => $asset], $this->assets);
+ }
+ }
+
+ /**
+ * Prepends an array of CdnAsset objects to the list.
+ *
+ * @param \Drupal\bootstrap\Plugin\Provider\CdnAsset[] $assets
+ * An array of CdnAsset objects.
+ */
+ public function prependAssets(array $assets) {
+ foreach (array_reverse($assets) as $asset) {
+ $this->prepend($asset);
+ }
+ }
+
+ /**
+ * Retrieves all the set CDN Asset objects, as an array.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAsset[]
+ * The CDN Asset objects.
+ */
+ public function toArray() {
+ return $this->assets;
+ }
+
+ /**
+ * Converts the CDN Assets into an array suitable for a Drupal library array.
+ *
+ * @param bool $minified
+ * Flag indicating whether to use minified assets.
+ *
+ * @return array
+ * An array structured for use in a Drupal library.
+ */
+ public function toLibraryArray($minified = NULL) {
+ $assets = $this->all($minified);
+ $library = [];
+
+ // Iterate over each type.
+ foreach ($assets as $asset) {
+ $url = (string) $asset;
+ $type = $asset->getType();
+ $data = ['data' => $url, 'type' => 'external'];
+
+ // Attempt to add a corresponding SRI attribute for the URL.
+ // @see https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+ foreach (['sha512', 'sha384', 'sha256', 'sha', 'hash', 'sri', 'integrity'] as $key) {
+ if ($integrity = $asset->getInfo($key)) {
+ // Parse the SRI integrity value to extract both the algorithm and
+ // hash. Note: this is needed as some APIs do not prepend the hash
+ // with the actual algorithm used. This is likely because the field,
+ // while a valid base64 encoded hash, isn't specifically intended for
+ // use as an SRI integrity attribute value.
+ list($algorithm, $hash) = Crypt::parseSriIntegrity($integrity);
+
+ // Ensure the algorithm and hash are valid.
+ if (Crypt::checkBase64HashAlgorithm($algorithm, $hash, TRUE)) {
+ $data['attributes'] = [
+ 'integrity' => "$algorithm-$hash",
+ 'crossorigin' => $asset->getInfo('crossorigin', 'anonymous'),
+ ];
+ }
+ break;
+ }
+ }
+
+ // CSS library assets use "SMACSS" categorization, assign to "base".
+ if ($type === 'css') {
+ $library[$type]['base'][$url] = $data;
+ }
+ else {
+ $library[$type][$url] = $data;
+ }
+ }
+
+ return $library;
+ }
+
+ /**
+ * Sets the label.
+ *
+ * @param \Drupal\Component\Render\MarkupInterface $label
+ * The label to set.
+ *
+ * @return static
+ */
+ public function setLabel(MarkupInterface $label) {
+ $this->label = $label;
+ return $this;
+ }
+
+ /**
+ * Sets the library associated with these assets.
+ *
+ * @param string $library
+ * The library to set.
+ *
+ * @return static
+ */
+ public function setLibrary($library) {
+ $this->library = $library;
+ return $this;
+ }
+
+ /**
+ * Sorts themes.
+ *
+ * @param string $a
+ * First theme to compare.
+ * @param string $b
+ * Second theme to compare.
+ *
+ * @return false|int|string
+ * The comparision value, similar to other comparison functions.
+ */
+ protected function sortThemes($a, $b) {
+ $order = ['bootstrap', 'bootstrap_theme'];
+ $aIndex = array_search($a, $order);
+ if ($aIndex === FALSE) {
+ $aIndex = 2;
+ }
+ $bIndex = array_search($b, $order);
+ if ($bIndex === FALSE) {
+ $bIndex = 2;
+ }
+ if ($aIndex !== $bIndex) {
+ return $aIndex - $bIndex;
+ }
+ return strnatcasecmp($a, $b);
+ }
+
+}
diff --git a/src/Plugin/Provider/CdnJs.php b/src/Plugin/Provider/CdnJs.php
new file mode 100644
index 0000000000000000000000000000000000000000..97ebba52efae878ee7748bc4bf6d82172240c461
--- /dev/null
+++ b/src/Plugin/Provider/CdnJs.php
@@ -0,0 +1,50 @@
+theme->getSetting('cdn_custom_' . $type)) {
- $assets[$type][] = $setting;
+ $themes = $this->getCdnThemes($version);
+ return isset($themes[$theme]) ? $themes[$theme] : new CdnAssets();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function discoverCdnThemes($version) {
+ return $this->parseAssets($this->getUrls())->getThemes();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function discoverCdnVersions() {
+ $assets = $this->parseAssets($this->getUrls());
+ $versions = [];
+ foreach ($assets->toArray() as $asset) {
+ if ($version = $asset->getVersion()) {
+ $versions[$version] = $version;
+ }
+ }
+ return $versions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getCdnAssetsCacheData($version = NULL, $theme = NULL) {
+ return parent::getCdnAssetsCacheData($version, $theme) + ['urls' => $this->getUrls()];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getCdnThemesCacheData($version = NULL) {
+ return parent::getCdnThemesCacheData($version) + ['urls' => $this->getUrls()];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getCdnVersionsCacheData() {
+ return parent::getCdnVersionsCacheData() + ['urls' => $this->getUrls()];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTtl($type) {
+ // Because these are static URLs provided by the user, they should just be
+ // cached forever.
+ return static::TTL_FOREVER;
+ }
+
+ /**
+ * Retrieves an array of URLs that should be used in the Custom CDN.
+ *
+ * @return array
+ * An array of URLs.
+ */
+ protected function getUrls() {
+ if (!isset($this->urls)) {
+ $urls = [];
+ $filtered = array_filter(explode("\n", $this->theme->getSetting('cdn_custom')));
+ foreach ($filtered as $url) {
+ try {
+ $urls[] = $this->validateUrl($url);
+ }
+ catch (\Exception $e) {
+ // Intentionally do nothing.
+ }
}
- if ($setting = $this->theme->getSetting('cdn_custom_' . $type . '_min')) {
- $assets['min'][$type][] = $setting;
+ $this->urls = $urls;
+ }
+ return $this->urls;
+ }
+
+ /**
+ * Validates a URL.
+ *
+ * @param string $url
+ * The URL to validate.
+ *
+ * @return string
+ * The passed $url.
+ */
+ public function validateUrl($url) {
+ if (!UrlHelper::isValid($url, TRUE)) {
+ throw new InvalidCdnUrlException(sprintf('Malformed: %s', $url));
+ }
+ $response = Bootstrap::checkUrlIsReachable($url, ['method' => 'option']);
+ if (($statusCode = $response->getStatusCode()) >= 400) {
+ throw new InvalidCdnUrlException(sprintf('(%d) %s: %s', $statusCode, Response::$statusTexts[$statusCode], $url), $statusCode);
+ }
+ if (!$response->validMimeExtension()) {
+ throw new InvalidCdnUrlException(sprintf('(%d) Mismatched MIME Type: %s [%s]', $statusCode, $url, $response->getMimeType()), $statusCode);
+ }
+ return $url;
+ }
+
+ /**
+ * Parses URLs and places them in an "assets" like array.
+ *
+ * @param string[] $urls
+ * An array of URLs to process.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAssets
+ * A CdnAssets object.
+ */
+ protected function parseAssets(array $urls) {
+ $assets = new CdnAssets();
+ foreach ($urls as $url) {
+ // Skip invalid assets.
+ if (!CdnAsset::isFileValid($url)) {
+ continue;
}
+ $assets->append(new CdnAsset($url));
}
return $assets;
}
diff --git a/src/Plugin/Provider/InvalidCdnUrlException.php b/src/Plugin/Provider/InvalidCdnUrlException.php
new file mode 100644
index 0000000000000000000000000000000000000000..04bab57e040bc0626a6940d5b99767f6f2d56616
--- /dev/null
+++ b/src/Plugin/Provider/InvalidCdnUrlException.php
@@ -0,0 +1,8 @@
+jsDelivr is a free multi-CDN infrastructure that uses MaxCDN, Cloudflare and many others to combine their powers for the good of the open source community... read more", arguments = {
+ * ":jsdelivr" = "https://www.jsdelivr.com",
+ * ":maxcdn" = "https://www.maxcdn.com",
+ * ":cloudflare" = "https://www.cloudflare.com",
+ * ":read_more" = "https://www.jsdelivr.com/about",
+ * }),
+ * weight = -1
* )
*/
-class JsDelivr extends ProviderBase {
-
- /**
- * The base API URL.
- *
- * @var string
- */
- const BASE_API_URL = 'https://data.jsdelivr.com/v1/package/npm';
-
- /**
- * The base CDN URL.
- *
- * @var string
- */
- const BASE_CDN_URL = 'https://cdn.jsdelivr.net/npm';
+class JsDelivr extends ApiProviderBase {
/**
* {@inheritdoc}
*/
- public function getDescription() {
- return $this->t('jsDelivr is a free multi-CDN infrastructure that uses MaxCDN, Cloudflare and many others to combine their powers for the good of the open source community... read more
', [
- ':jsdelivr' => 'https://www.jsdelivr.com',
- ':jsdelivr_about' => 'https://www.jsdelivr.com/about',
- ':maxcdn' => 'https://www.maxcdn.com',
- ':cloudflare' => 'https://www.cloudflare.com',
- ]);
+ protected function getApiAssetsUrlTemplate() {
+ return 'https://data.jsdelivr.com/v1/package/npm/@library@@version/flat';
}
/**
* {@inheritdoc}
*/
- protected function discoverCdnAssets($version, $theme = NULL) {
- $themes = $this->getCdnThemes($version);
- return isset($themes[$theme]) ? $themes[$theme] : [];
- }
-
- /**
- * {@inheritdoc}
- */
- protected function discoverCdnThemes($version) {
- $themes = [];
- foreach (['bootstrap', 'bootswatch'] as $package) {
- $mappedVersion = $this->mapVersion($version, $package);
- $files = $this->requestApiV1($package, $mappedVersion, $this->getCacheTtl(static::CACHE_THEMES));
- $themes = $this->parseThemes($files, $package, $mappedVersion, $themes);
- }
- return $themes;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function discoverCdnVersions() {
- $versions = [];
- $json = $this->requestApiV1('bootstrap', NULL, $this->getCacheTtl(static::CACHE_VERSIONS)) + ['versions' => []];
- foreach ($json['versions'] as $version) {
- // Skip irrelevant versions.
- if (!preg_match('/^' . substr(Bootstrap::FRAMEWORK_VERSION, 0, 1) . '\.\d+\.\d+$/', $version)) {
- continue;
- }
- $versions[$version] = $version;
- }
- return $versions;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function mapVersion($version, $package = NULL) {
- // While the Bootswatch project attempts to maintain version parity with
- // Bootstrap, it doesn't always happen. This causes issues when the system
- // expects a 1:1 version match between Bootstrap and Bootswatch.
- // @see https://github.com/thomaspark/bootswatch/issues/892#ref-issue-410070082
- if ($package === 'bootswatch') {
- switch ($version) {
- // This version is "broken" because of jsDelivr's API limit.
- case '3.4.1':
- $version = '3.4.0';
- break;
-
- // This version doesn't exist.
- case '3.1.1':
- $version = '3.2.0';
- break;
- }
- }
- return $version;
- }
-
- /**
- * Parses JSON from the API and retrieves valid files.
- *
- * @param array $json
- * The JSON data to parse.
- *
- * @return array
- * An array of files parsed from provided JSON data.
- */
- protected function parseFiles(array $json) {
- // Immediately return if malformed.
- if (!isset($json['files']) || !is_array($json['files'])) {
- return [];
- }
-
- $files = [];
- foreach ($json['files'] as $file) {
- // Skip old bootswatch file structure.
- if (preg_match('`^/2|/bower_components`', $file['name'], $matches)) {
- continue;
- }
- preg_match('`([^/]*)/bootstrap(-theme)?(\.min)?\.(js|css)$`', $file['name'], $matches);
- if (!empty($matches[1]) && !empty($matches[4])) {
- $files[] = $file['name'];
- }
- }
- return $files;
- }
-
- /**
- * Extracts assets from files provided by the jsDelivr API.
- *
- * This will place the raw files into proper "css", "js" and "min" arrays
- * (if they exist) and prepends them with a base URL provided.
- *
- * @param array $files
- * An array of files to process.
- * @param string $package
- * The base URL each one of the $files are relative to, this usually
- * should also include the version path prefix as well.
- * @param string $version
- * A specific version to use.
- * @param array $themes
- * An existing array of themes. This is primarily used when building a
- * complete list of themes.
- *
- * @return array
- * An associative array containing the following keys, if there were
- * matching files found:
- * - css
- * - js
- * - min:
- * - css
- * - js
- */
- protected function parseThemes(array $files, $package, $version, array $themes = []) {
- $baseUrl = static::BASE_CDN_URL . "/$package@$version";
- foreach ($files as $file) {
- preg_match('`([^/]*)/bootstrap(-theme)?(\.min)?\.(js|css)$`', $file, $matches);
- if (!empty($matches[1]) && !empty($matches[4])) {
- $path = $matches[1];
- $min = $matches[3];
- $filetype = $matches[4];
-
- // Determine the "theme" name.
- if ($path === 'css' || $path === 'js') {
- $theme = 'bootstrap';
- $title = (string) $this->t('Default');
- }
- else {
- $theme = $path;
- $title = ucfirst($path);
- }
- if ($matches[2]) {
- $theme = 'bootstrap_theme';
- $title = (string) $this->t('Example Theme');
- }
-
- $themes[$theme]['title'] = $title;
- if ($min) {
- $themes[$theme]['min'][$filetype][] = "$baseUrl/" . ltrim($file, '/');
- }
- else {
- $themes[$theme][$filetype][] = "$baseUrl/" . ltrim($file, '/');
- }
- }
- }
-
- // Post process the themes to fill in any missing assets.
- foreach (array_keys($themes) as $theme) {
- // Some themes do not have a non-minified version, clone them to the
- // "normal" css/js arrays to ensure that the theme still loads if
- // aggregation (minification) is disabled.
- foreach (['css', 'js'] as $type) {
- if (!isset($themes[$theme][$type]) && isset($themes[$theme]['min'][$type])) {
- $themes[$theme][$type] = $themes[$theme]['min'][$type];
- }
- }
-
- // Prepend the main Bootstrap styles before the Bootstrap theme.
- if ($theme === 'bootstrap_theme') {
- if (isset($themes['bootstrap']['css'])) {
- $themes[$theme]['css'] = array_unique(array_merge($themes['bootstrap']['css'], isset($themes[$theme]['css']) ? $themes[$theme]['css'] : []));
- }
- if (isset($themes['bootstrap']['min']['css'])) {
- $themes[$theme]['min']['css'] = array_unique(array_merge($themes['bootstrap']['min']['css'], isset($themes[$theme]['min']['css']) ? $themes[$theme]['min']['css'] : []));
- }
- }
-
- // Populate missing JavaScript.
- if (!isset($themes[$theme]['js']) && isset($themes['bootstrap']['js'])) {
- $themes[$theme]['js'] = $themes['bootstrap']['js'];
- }
- if (!isset($themes[$theme]['min']['js']) && isset($themes['bootstrap']['min']['js'])) {
- $themes[$theme]['min']['js'] = $themes['bootstrap']['min']['js'];
- }
- }
-
- return $themes;
- }
-
- /**
- * Requests JSON from jsDelivr's API V1.
- *
- * @param string $package
- * The NPM package being requested.
- * @param string $version
- * A specific version of $package to request. If not provided, a list of
- * available versions will be returned.
- * @param int $ttl
- * Optional. A specific TTL value to use for caching the HTTP request. If
- * not set, it will default to whatever is returned by the HTTP request.
- *
- * @return array
- * The JSON data from the API.
- */
- protected function requestApiV1($package, $version = NULL, $ttl = NULL) {
- $uri = static::BASE_API_URL . "/$package";
- $options = [];
-
- if (isset($ttl)) {
- $options['ttl'] = $ttl;
- }
-
- // If no version was passed, then all versions are returned.
- if (!$version) {
- $response = $this->requestJson($uri, $options);
- $json = $response->getJson();
-
- // If bootstrap JSON could not be returned, provide defaults.
- if (!$json && $this->cdnExceptions && $package === 'bootstrap') {
- $json = ['versions' => [Bootstrap::FRAMEWORK_VERSION]];
- }
-
- return $json;
- }
-
- $response = $this->requestJson("$uri@$version/flat", $options);
- $json = $response->getJson();
-
- // If bootstrap JSON could not be returned, provide defaults.
- if (!$json && $this->cdnExceptions && $package === 'bootstrap') {
- return [
- '/dist/css/bootstrap.css',
- '/dist/js/bootstrap.js',
- '/dist/css/bootstrap.min.css',
- '/dist/js/bootstrap.min.js',
- ];
- }
-
- // Parse the files from JSON.
- return $this->parseFiles($json);
+ protected function getApiVersionsUrlTemplate() {
+ return 'https://data.jsdelivr.com/v1/package/npm/@library';
}
/**
* {@inheritdoc}
- *
- * @deprecated in 8.x-3.18, will be removed in a future release.
*/
- public function processDefinition(array &$definition, $plugin_id) {
- // Intentionally left blank so it doesn't trigger a deprecation warning.
+ protected function getCdnUrlTemplate() {
+ return 'https://cdn.jsdelivr.net/npm/@library@@version/@file';
}
}
diff --git a/src/Plugin/Provider/ProviderBase.php b/src/Plugin/Provider/ProviderBase.php
index b91bdd4933b7cf312ee7df470db9fedef2fb57c4..2bdce9dfd024fd313ef74834ea0e813e41f8f713 100644
--- a/src/Plugin/Provider/ProviderBase.php
+++ b/src/Plugin/Provider/ProviderBase.php
@@ -5,7 +5,6 @@ namespace Drupal\bootstrap\Plugin\Provider;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\PluginBase;
use Drupal\bootstrap\Plugin\ProviderManager;
-use Drupal\bootstrap\Theme;
use Drupal\bootstrap\Utility\Crypt;
use Drupal\bootstrap\Utility\Unicode;
use Drupal\Component\Serialization\Json;
@@ -51,9 +50,9 @@ class ProviderBase extends PluginBase implements ProviderInterface {
protected $cacheTtl = [];
/**
- * The currently set CDN assets.
+ * The currently set CDN assets, keyed by a hash identifier.
*
- * @var array
+ * @var \Drupal\bootstrap\Plugin\Provider\CdnAssets[]
*/
protected $cdnAssets;
@@ -81,62 +80,34 @@ class ProviderBase extends PluginBase implements ProviderInterface {
/**
* Adds a new CDN Provider exception.
*
- * @param string|\Exception $message
+ * @param \Throwable $exception
* The exception message.
*/
- protected function addCdnException($message) {
- if ($message instanceof \Throwable) {
- $this->cdnExceptions[] = new ProviderException($this, $message->getMessage(), $message->getCode(), $message);
- }
- else {
- $this->cdnExceptions[] = new ProviderException($this, $message);
- }
+ protected function addCdnException(\Throwable $exception) {
+ $this->cdnExceptions[] = new ProviderException($this, $exception->getMessage(), $exception->getCode(), $exception);
}
/**
* {@inheritdoc}
+ *
+ * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnAssetsCacheData()
*/
- public function alterFrameworkLibrary(array &$framework, $min = NULL) {
+ public function alterFrameworkLibrary(array &$framework) {
// Attempt to retrieve cached CDN assets from the database. This is
// primarily used to avoid unnecessary API requests and speed up the
// process during a cache rebuild. The "keyvalue.expirable" service is
// used as it persists through cache rebuilds. In order to prevent stale
- // data, a hash is used constructed of various CDN and preprocess settings.
- // The cache is rebuilt after it has expired, one week by default, based on
- // the "cdn_cache_ttl" theme setting.
+ // data, a hash is used constructed of various data relating to the CDN.
+ // The cache is rebuilt if and when it has expired.
// @see https://www.drupal.org/project/bootstrap/issues/3031415
- $cdn = [
- 'ttl' => $this->getCacheTtl(static::CACHE_LIBRARY),
- 'min' => [
- 'css' => !!(isset($min) ? $min : \Drupal::config('system.performance')->get('css.preprocess')),
- 'js' => !!(isset($min) ? $min : \Drupal::config('system.performance')->get('js.preprocess')),
- ],
- 'provider' => $this->pluginId,
- 'theme' => $this->getCdnTheme(),
- 'version' => $this->getCdnVersion(),
- ];
-
- // Construct a hash identifier based on the above CDN Provider values.
- $hash = Crypt::hashBase64(serialize($cdn));
+ $data = $this->getCdnAssetsCacheData();
+ $hash = Crypt::generateBase64HashIdentifier($data);
// Retrieve the cached value or build it if necessary.
- $assets = $this->cacheGet('library', $hash, [], function ($assets) use ($cdn) {
- // Iterate over each type.
- $cdnAssets = $this->getCdnAssets($cdn['version'], $cdn['theme']);
- foreach (['css', 'js'] as $type) {
- $files = !empty($cdn['min'][$type]) && isset($cdnAssets['min'][$type]) ? $cdnAssets['min'][$type] : (isset($cdnAssets[$type]) ? $cdnAssets[$type] : []);
- foreach ($files as $asset) {
- $data = ['data' => $asset, 'type' => 'external'];
- // CSS library assets use "SMACSS" categorization, assign to "base".
- if ($type === 'css') {
- $assets[$type]['base'][$asset] = $data;
- }
- else {
- $assets[$type][$asset] = $data;
- }
- }
- }
- return $assets;
+ $assets = $this->cacheGet('library', $hash, [], function () use ($data) {
+ $version = isset($data['version']) ? $data['version'] : NULL;
+ $theme = isset($data['theme']) ? $data['theme'] : NULL;
+ return $this->getCdnAssets($version, $theme)->toLibraryArray($data['min']);
});
// Immediately return if there are no theme CDN assets to use.
@@ -145,7 +116,9 @@ class ProviderBase extends PluginBase implements ProviderInterface {
}
// Override the framework version with the CDN version that is being used.
- $framework['version'] = $cdn['version'];
+ if (isset($data['version'])) {
+ $framework['version'] = $data['version'];
+ }
// Merge the assets into the library info.
$framework = NestedArray::mergeDeepArray([$assets, $framework], TRUE);
@@ -233,17 +206,28 @@ class ProviderBase extends PluginBase implements ProviderInterface {
* @param string $theme
* A specific set of themed assets to return, if any.
*
- * @return array
- * An associative array containing the following keys, if there were
- * matching files found:
- * - css
- * - js
- * - min:
- * - css
- * - js
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAssets
+ * A CdnAssets object.
*/
protected function discoverCdnAssets($version, $theme = NULL) {
- return $this->getAssets();
+ $assets = [];
+
+ // Convert the deprecated array structure into a proper CdnAssets object.
+ $data = $this->getAssets();
+ foreach (['css', 'js'] as $type) {
+ if (isset($data[$type])) {
+ foreach ($data[$type] as $file) {
+ $assets[] = new CdnAsset($file, NULL, $version);
+ }
+ }
+ if (isset($data['min'][$type])) {
+ foreach ($data['min'][$type] as $file) {
+ $assets[] = new CdnAsset($file, NULL, $version);
+ }
+ }
+ }
+
+ return new CdnAssets($assets);
}
/**
@@ -255,9 +239,10 @@ class ProviderBase extends PluginBase implements ProviderInterface {
* @param string $version
* A specific version of themes to retrieve.
*
- * @return array
- * An array of themes. If the CDN Provider does not support any it should
- * return an empty array.
+ * @return array|false
+ * An associative array of theme data, similar to what is returned in
+ * \Drupal\bootstrap\Plugin\Provider\ProviderBase::discoverCdnAssets(), but
+ * keyed by the theme name.
*/
protected function discoverCdnThemes($version) {
return [];
@@ -269,40 +254,13 @@ class ProviderBase extends PluginBase implements ProviderInterface {
* CDN Providers should sub-class this method to make requests and/or process
* any necessary data.
*
- * @return array
- * An array of versions. If the CDN Provider does not support any it should
- * return an empty array.
+ * @return array|false
+ * An associative array of versions, also keyed by the version.
*/
protected function discoverCdnVersions() {
return [];
}
- /**
- * Retrieves a permanent key/value storage instance.
- *
- * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
- * A permanent key/value storage instance.
- */
- protected function getKeyValue() {
- if (!isset($this->keyValue)) {
- $this->keyValue = \Drupal::keyValue($this->getCacheId());
- }
- return $this->keyValue;
- }
-
- /**
- * Retrieves a expirable key/value storage instance.
- *
- * @return \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
- * An expirable key/value storage instance.
- */
- protected function getKeyValueExpirable() {
- if (!isset($this->keyValueExpirable)) {
- $this->keyValueExpirable = \Drupal::keyValueExpirable($this->getCacheId());
- }
- return $this->keyValueExpirable;
- }
-
/**
* {@inheritdoc}
*/
@@ -327,25 +285,52 @@ class ProviderBase extends PluginBase implements ProviderInterface {
* {@inheritdoc}
*/
public function getCdnAssets($version = NULL, $theme = NULL) {
- if (!isset($version)) {
- $version = $this->getCdnVersion();
- }
- if (!isset($theme)) {
- $theme = $this->getCdnTheme();
- }
-
if (!isset($this->cdnAssets)) {
$this->cdnAssets = $this->cacheGet('assets');
}
- if (!isset($this->cdnAssets[$version][$theme])) {
- $escapedVersion = Unicode::escapeDelimiter($version);
- $this->cdnAssets[$version][$theme] = $this->cacheGet('assets', "$escapedVersion.$theme", [], function () use ($version, $theme) {
- return $this->discoverCdnAssets($version, $theme);
+ $data = $this->getCdnAssetsCacheData($version, $theme);
+ $hash = Crypt::generateBase64HashIdentifier($data);
+ if (!isset($this->cdnAssets[$hash])) {
+ $this->cdnAssets[$hash] = $this->cacheGet('assets', $hash, [], function () use ($data) {
+ return $this->discoverCdnAssets($data['version'], $data['theme']);
});
}
- return $this->cdnAssets[$version][$theme];
+ return $this->cdnAssets[$hash];
+ }
+
+ /**
+ * Retrieves the data used to create a hash for CDN Assets.
+ *
+ * @param string $version
+ * Optional. A specific version to use.
+ * @param string $theme
+ * Optional. A specific theme to use.
+ *
+ * @return array
+ * An array of components that will be serialized and hashed.
+ *
+ * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnAssets()
+ * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::alterFrameworkLibrary()
+ */
+ protected function getCdnAssetsCacheData($version = NULL, $theme = NULL) {
+ if (!isset($version) && $this->supportsVersions()) {
+ $version = $this->getCdnVersion();
+ }
+ if (!isset($theme) && $this->supportsThemes()) {
+ $theme = $this->getCdnTheme();
+ }
+ return [
+ 'ttl' => $this->getCacheTtl(static::CACHE_LIBRARY),
+ 'min' => [
+ 'css' => !!\Drupal::config('system.performance')->get('css.preprocess'),
+ 'js' => !!\Drupal::config('system.performance')->get('js.preprocess'),
+ ],
+ 'provider' => $this->pluginId,
+ 'version' => $version,
+ 'theme' => $theme,
+ ];
}
/**
@@ -363,43 +348,92 @@ class ProviderBase extends PluginBase implements ProviderInterface {
* {@inheritdoc}
*/
public function getCdnTheme() {
- return $this->theme->getSetting("cdn_{$this->getPluginId()}_theme") ?: 'bootstrap';
+ return $this->supportsThemes() ? $this->theme->getSetting('cdn_theme', 'bootstrap') : NULL;
}
/**
* {@inheritdoc}
*/
public function getCdnThemes($version = NULL) {
- if (!isset($version)) {
- $version = $this->getCdnVersion();
+ // Immediately return if the CDN Provider does not support themes.
+ if (!$this->supportsThemes()) {
+ return [];
}
- if (!isset($this->themes[$version])) {
- $this->themes[$version] = $this->cacheGet('themes', Unicode::escapeDelimiter($version), [], function () use ($version) {
- return $this->discoverCdnThemes($version);
+
+ $data = $this->getCdnThemesCacheData($version);
+ $hash = Crypt::generateBase64HashIdentifier($data);
+ if (!isset($this->themes[$hash])) {
+ $this->themes[$hash] = $this->cacheGet('themes', $hash, [], function () use ($data) {
+ return $this->discoverCdnThemes($data['version']);
});
}
- return $this->themes[$version];
+
+ return $this->themes[$hash];
+ }
+
+ /**
+ * Retrieves the data used to create a hash for CDN Themes.
+ *
+ * @param string $version
+ * Optional. A specific version to use. If not set, the
+ * currently set CDN version of the active theme will be used.
+ *
+ * @return array
+ * An array of components that will be serialized and hashed.
+ *
+ * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnThemes()
+ */
+ protected function getCdnThemesCacheData($version = NULL) {
+ if (!isset($version) && $this->supportsVersions()) {
+ $version = $this->getCdnVersion();
+ }
+ return [
+ 'ttl' => $this->getCacheTtl(static::CACHE_THEMES),
+ 'provider' => $this->pluginId,
+ 'version' => $version,
+ ];
}
/**
* {@inheritdoc}
*/
- public function getCdnVersion(Theme $theme = NULL) {
- return $this->theme->getSetting("cdn_{$this->getPluginId()}_version") ?: Bootstrap::FRAMEWORK_VERSION;
+ public function getCdnVersion() {
+ return $this->supportsVersions() ? $this->theme->getSetting('cdn_version', Bootstrap::FRAMEWORK_VERSION) : NULL;
}
/**
* {@inheritdoc}
*/
public function getCdnVersions() {
+ // Immediately return if the CDN Provider does not support versions.
+ if (!$this->supportsVersions()) {
+ return [];
+ }
+
if (!isset($this->versions)) {
- $this->versions = $this->cacheGet('versions', 'bootstrap', [], function () {
+ $hash = Crypt::generateBase64HashIdentifier($this->getCdnVersionsCacheData());
+ $this->versions = $this->cacheGet('versions', $hash, [], function () {
return $this->discoverCdnVersions();
});
}
return $this->versions;
}
+ /**
+ * Retrieves the data used to create a hash for CDN Versions.
+ *
+ * @return array
+ * An array of components that will be serialized and hashed.
+ *
+ * @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnVersions()
+ */
+ protected function getCdnVersionsCacheData() {
+ return [
+ 'ttl' => $this->getCacheTtl(static::CACHE_THEMES),
+ 'provider' => $this->pluginId,
+ ];
+ }
+
/**
* {@inheritdoc}
*/
@@ -407,6 +441,32 @@ class ProviderBase extends PluginBase implements ProviderInterface {
return $this->pluginDefinition['description'];
}
+ /**
+ * Retrieves a permanent key/value storage instance.
+ *
+ * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+ * A permanent key/value storage instance.
+ */
+ protected function getKeyValue() {
+ if (!isset($this->keyValue)) {
+ $this->keyValue = \Drupal::keyValue($this->getCacheId());
+ }
+ return $this->keyValue;
+ }
+
+ /**
+ * Retrieves a expirable key/value storage instance.
+ *
+ * @return \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
+ * An expirable key/value storage instance.
+ */
+ protected function getKeyValueExpirable() {
+ if (!isset($this->keyValueExpirable)) {
+ $this->keyValueExpirable = \Drupal::keyValueExpirable($this->getCacheId());
+ }
+ return $this->keyValueExpirable;
+ }
+
/**
* {@inheritdoc}
*/
@@ -419,17 +479,23 @@ class ProviderBase extends PluginBase implements ProviderInterface {
*
* @return string|null
* THe Drupal overrides CSS file.
+ *
+ * @todo This should really be a part of the CDN asset discovery phase.
+ *
+ * @see https://www.drupal.org/project/bootstrap/issues/2852156
*/
protected function getOverrides() {
- $version = $this->getCdnVersion();
+ $overrides = NULL;
+ $version = $this->getCdnVersion() ?: Bootstrap::FRAMEWORK_VERSION;
$theme = $this->getCdnTheme();
$theme = !$theme || $theme === '_default' || $theme === 'bootstrap' || $theme === 'bootstrap_theme' ? '' : "-$theme";
foreach ($this->theme->getAncestry(TRUE) as $ancestor) {
- $overrides = $ancestor->getPath() . "/css/{$version}/overrides{$theme}.min.css";
- if (file_exists($overrides)) {
- return $overrides;
+ $file = $ancestor->getPath() . "/css/{$version}/overrides{$theme}.min.css";
+ if (file_exists($file)) {
+ $overrides = $file;
}
}
+ return $overrides;
}
/**
@@ -460,18 +526,18 @@ class ProviderBase extends PluginBase implements ProviderInterface {
}
/**
- * Retrieves JSON from a URI.
+ * Initiates an HTTP request.
*
- * @param string $uri
- * The URI to retrieve JSON from.
+ * @param string $url
+ * The URL to retrieve.
* @param array $options
* The options to pass to the HTTP client.
*
- * @return \Drupal\bootstrap\JsonResponse
- * A JsonResponse object.
+ * @return \Drupal\bootstrap\SerializedResponse
+ * A SerializedResponse object.
*/
- protected function requestJson($uri, array $options = []) {
- $response = Bootstrap::requestJson($uri, $options, $exception);
+ protected function request($url, array $options = []) {
+ $response = Bootstrap::request($url, $options, $exception);
if ($exception) {
$this->addCdnException($exception);
}
@@ -482,16 +548,67 @@ class ProviderBase extends PluginBase implements ProviderInterface {
* {@inheritdoc}
*/
public function resetCache() {
+ $this->getKeyValue()->deleteAll();
$this->getKeyValueExpirable()->deleteAll();
// Invalidate library info if this provider is the one currently used.
- if (($provider = $this->theme->getCdnProvider()) && $provider->getPluginId() === $this->pluginId) {
+ if ($this->theme->getCdnProvider()->getPluginId() === $this->pluginId) {
/** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */
$invalidator = \Drupal::service('cache_tags.invalidator');
$invalidator->invalidateTags(['library_info']);
}
}
+ /**
+ * Sets CDN Provider exceptions, replacing any existing exceptions.
+ *
+ * @param \Throwable[] $exceptions
+ * The Exceptions to set.
+ *
+ * @return static
+ */
+ protected function setCdnExceptions(array $exceptions) {
+ $this->cdnExceptions = [];
+ foreach ($exceptions as $exception) {
+ $this->addCdnException($exception);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsThemes() {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsVersions() {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackCdnExceptions(callable $callable) {
+ // Retrieve existing exceptions.
+ $existing = $this->getCdnExceptions();
+
+ // Execute the callable.
+ $callable($this);
+
+ // Retrieve any newly generated exceptions.
+ $new = $this->getCdnExceptions();
+
+ // Merge the existing and newly generated exceptions and set them.
+ $this->setCdnExceptions(array_merge($existing, $new));
+
+ // Return the newly generated exceptions.
+ return $new;
+ }
+
/****************************************************************************
*
* Deprecated methods
@@ -566,7 +683,7 @@ class ProviderBase extends PluginBase implements ProviderInterface {
// Otherwise, attempt to request API data if the provider has specified
// an "api" URL to use.
else {
- $json = Bootstrap::requestJson($api);
+ $json = Bootstrap::request($api)->getData();
}
if (!isset($json)) {
diff --git a/src/Plugin/Provider/ProviderException.php b/src/Plugin/Provider/ProviderException.php
index 504a86bc3f616ccabe92e26ab2df8cf8def4028c..24624f2e8cd589f5bb68b12fb7bea364e57cbb87 100644
--- a/src/Plugin/Provider/ProviderException.php
+++ b/src/Plugin/Provider/ProviderException.php
@@ -26,7 +26,7 @@ class ProviderException extends \RuntimeException {
* @param \Throwable $previous
* A previous exception.
*/
- public function __construct(ProviderInterface $provider, $message = "", int $code = 0, \Throwable $previous = NULL) {
+ public function __construct(ProviderInterface $provider, $message = "", $code = 0, \Throwable $previous = NULL) {
parent::__construct($message, $code, $previous);
$this->provider = $provider;
}
diff --git a/src/Plugin/Provider/ProviderInterface.php b/src/Plugin/Provider/ProviderInterface.php
index 65026f533cf8737041c7561627d18fa694e9be65..10f52f08a158a7b34d189cd9fe152442336d1bcc 100644
--- a/src/Plugin/Provider/ProviderInterface.php
+++ b/src/Plugin/Provider/ProviderInterface.php
@@ -101,11 +101,8 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
*
* @param array $framework
* The framework library, passed by reference.
- * @param bool $min
- * Optional. Flag determining whether to use minified resources. If not set,
- * this will automatically be determined based on system configuration.
*/
- public function alterFrameworkLibrary(array &$framework, $min = NULL);
+ public function alterFrameworkLibrary(array &$framework);
/**
* Retrieves the cache time-to-live (TTL) value.
@@ -133,14 +130,8 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
* Optional. A specific set of themed assets to return, if any. If not set,
* the setting stored in the active theme will be used.
*
- * @return array
- * An associative array containing the following keys, if there were
- * matching files found:
- * - css
- * - js
- * - min:
- * - css
- * - js
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAssets
+ * A CdnAssets object.
*/
public function getCdnAssets($version = NULL, $theme = NULL);
@@ -174,9 +165,10 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
* Optional. A specific version of themes to retrieve. If not set, the
* currently set CDN version of the active theme will be used.
*
- * @return array
- * An array of themes. If the CDN Provider does not support any it will
- * just be an empty array.
+ * @return \Drupal\bootstrap\Plugin\Provider\CdnAssets[]
+ * An associative array of CDN assets, similar to what is returned in
+ * \Drupal\bootstrap\Plugin\Provider\ProviderBase::getCdnAssets(), but
+ * keyed by individual theme names.
*/
public function getCdnThemes($version = NULL);
@@ -191,9 +183,8 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
/**
* Retrieves the versions supported by the CDN Provider.
*
- * @return array
- * An array of versions. If the CDN Provider does not support any it will
- * just be an empty array.
+ * @return array|false
+ * An associative array of versions, also keyed by the version.
*/
public function getCdnVersions();
@@ -218,6 +209,33 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
*/
public function resetCache();
+ /**
+ * Indicates whether the CDN Provider supports themes.
+ *
+ * @return bool
+ * TRUE or FALSE
+ */
+ public function supportsThemes();
+
+ /**
+ * Indicates whether the CDN Provider supports versions.
+ *
+ * @return bool
+ * TRUE or FALSE
+ */
+ public function supportsVersions();
+
+ /**
+ * Tracks any newly generated CDN exceptions generated during a callable.
+ *
+ * @param callable $callable
+ * The callback to execute.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\ProviderException[]
+ * An array of newly generated ProviderException objects, if any.
+ */
+ public function trackCdnExceptions(callable $callable);
+
/****************************************************************************
*
* Deprecated methods
diff --git a/src/Plugin/ProviderManager.php b/src/Plugin/ProviderManager.php
index 3fb247bd841382c30f0115b92a6f85a04d28b6e5..3e84e6cd81d442c98db4a58abcdc844e7b7a7e26 100644
--- a/src/Plugin/ProviderManager.php
+++ b/src/Plugin/ProviderManager.php
@@ -19,6 +19,13 @@ class ProviderManager extends PluginManager implements FallbackPluginManagerInte
*/
const FILE_PATH = 'public://bootstrap/provider';
+ /**
+ * The Broken CDN Provider.
+ *
+ * @var \Drupal\bootstrap\Plugin\Provider\Broken
+ */
+ protected static $broken;
+
/**
* Constructs a new \Drupal\bootstrap\Plugin\ProviderManager object.
*
@@ -63,6 +70,19 @@ class ProviderManager extends PluginManager implements FallbackPluginManagerInte
$provider->processDefinition($definition, $plugin_id);
}
+ /**
+ * Returns the Broken CDN Provider instance.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\Broken
+ * The Broken CDN Provider.
+ */
+ public static function broken() {
+ if (!isset(static::$broken)) {
+ static::$broken = (new static(Bootstrap::getTheme()))->get('_broken');
+ }
+ return static::$broken;
+ }
+
/**
* Loads a CDN Provider.
*
@@ -80,7 +100,7 @@ class ProviderManager extends PluginManager implements FallbackPluginManagerInte
*/
public static function load($theme = NULL, $provider = NULL, array $configuration = []) {
$theme = Bootstrap::getTheme($theme);
- return (new static($theme))->get($provider ?: $theme->getSetting('cdn_provider'), $configuration + ['theme' => $theme]);
+ return (new static($theme))->get(isset($provider) ? $provider : $theme->getSetting('cdn_provider'), $configuration + ['theme' => $theme]);
}
}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlAssets.php b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlAssets.php
index cd8e501279879f691b69fa7eb2601acd46c21015..5fcf010ea4bf5103c8038cc915b7d6ecab606d08 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlAssets.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlAssets.php
@@ -2,6 +2,9 @@
namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
+use Drupal\bootstrap\Plugin\Provider\ProviderInterface;
+use Drupal\Core\Form\FormStateInterface;
+
/**
* Due to BC reasons, this class cannot be moved.
*
@@ -27,4 +30,13 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* },
* )
*/
-class CdnCacheTtlAssets extends CdnCacheTtlBase {}
+class CdnCacheTtlAssets extends CdnCacheTtlBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getSettingValue(FormStateInterface $form_state) {
+ return $this->getProvider()->getCacheTtl(ProviderInterface::CACHE_ASSETS);
+ }
+
+}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlBase.php b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlBase.php
index d0a554986d1fb12b915d2e724bafbb8b6062dd5a..546b05c381eda4fbe36a9f8c6506d6c728b0b9c2 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlBase.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlBase.php
@@ -17,7 +17,7 @@ use Drupal\Core\Form\FormStateInterface;
*
* @ingroup plugins_setting
*/
-class CdnCacheTtlBase extends CdnProviderBase {
+abstract class CdnCacheTtlBase extends CdnProviderBase {
/**
* The DateFormatter service.
@@ -39,17 +39,24 @@ class CdnCacheTtlBase extends CdnProviderBase {
public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
$setting = $this->getSettingElement($form, $form_state);
$setting->setProperty('options', $this->getTtlOptions());
- $setting->setProperty('access', $this->getSettingAccess());
+
+ // @todo This really shouldn't be here, but there isn't a great way of
+ // setting this from the provider.
+ if ($this->provider->getPluginId() === 'custom') {
+ $setting->setProperty('disabled', TRUE);
+ $setting->setProperty('description', '');
+ $group = $this->getGroupElement($form, $form_state);
+ $group->setProperty('description', $this->t('All caching is forced to "Forever" when using the "Custom" CDN Provider. This is because the provided Custom URLs above are used as part of the cache identifier. Anytime the above Custom URLs are modified, all of the caches are rebuilt automatically.'));
+ }
}
/**
- * Retrieves the access value for the setting.
- *
- * @return bool
- * TRUE or FALSE
+ * {@inheritdoc}
*/
- protected function getSettingAccess() {
- return TRUE;
+ public function autoCreateFormElement() {
+ // Don't auto create these; they are created as part of CDN Provider.
+ // @see \Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnProvider::alterFormElement()
+ return FALSE;
}
/**
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlLibrary.php b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlLibrary.php
index 307e67de065c30403362308f628deeef6b4922d1..71ba0e57cd3f8e940199fab461b541cfb91fa0c4 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlLibrary.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlLibrary.php
@@ -2,6 +2,9 @@
namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
+use Drupal\bootstrap\Plugin\Provider\ProviderInterface;
+use Drupal\Core\Form\FormStateInterface;
+
/**
* Due to BC reasons, this class cannot be moved.
*
@@ -27,4 +30,13 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* },
* )
*/
-class CdnCacheTtlLibrary extends CdnCacheTtlBase {}
+class CdnCacheTtlLibrary extends CdnCacheTtlBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getSettingValue(FormStateInterface $form_state) {
+ return $this->getProvider()->getCacheTtl(ProviderInterface::CACHE_LIBRARY);
+ }
+
+}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlThemes.php b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlThemes.php
index ee70a66d6b231b8f7cc2e8b00b1780adc66ae7ce..e09d6b63692f8505ddaf3a887d5d9775802d8da0 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlThemes.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlThemes.php
@@ -2,6 +2,10 @@
namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
+use Drupal\bootstrap\Plugin\Provider\ProviderInterface;
+use Drupal\Core\Access\AccessResultAllowed;
+use Drupal\Core\Form\FormStateInterface;
+
/**
* Due to BC reasons, this class cannot be moved.
*
@@ -19,7 +23,7 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* weight = 2,
* title = @Translation("Available Themes"),
* description = @Translation("The length of time to cache the CDN themes (if applicable) before requesting them from the API again."),
- * defaultValue = \Drupal\bootstrap\Plugin\Provider\ProviderInterface::TTL_ONE_MONTH,
+ * defaultValue = \Drupal\bootstrap\Plugin\Provider\ProviderInterface::TTL_ONE_WEEK,
* groups = {
* "cdn" = @Translation("CDN (Content Delivery Network)"),
* "cdn_provider" = false,
@@ -32,8 +36,15 @@ class CdnCacheTtlThemes extends CdnCacheTtlBase {
/**
* {@inheritdoc}
*/
- protected function getSettingAccess() {
- return !!$this->activeProvider->getCdnThemes();
+ public function access() {
+ return parent::access()->andIf(AccessResultAllowed::allowedIf($this->getProvider()->supportsThemes()));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getSettingValue(FormStateInterface $form_state) {
+ return $this->getProvider()->getCacheTtl(ProviderInterface::CACHE_THEMES);
}
}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlVersions.php b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlVersions.php
index 5ae6777e6cf69a37b0707977dbd2e2f60c3c0f4c..fc88684f819893e2ad2f279d278f32ba14728d93 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlVersions.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCacheTtlVersions.php
@@ -2,6 +2,10 @@
namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
+use Drupal\bootstrap\Plugin\Provider\ProviderInterface;
+use Drupal\Core\Access\AccessResultAllowed;
+use Drupal\Core\Form\FormStateInterface;
+
/**
* Due to BC reasons, this class cannot be moved.
*
@@ -32,8 +36,15 @@ class CdnCacheTtlVersions extends CdnCacheTtlBase {
/**
* {@inheritdoc}
*/
- protected function getSettingAccess() {
- return !!$this->activeProvider->getCdnVersions();
+ public function access() {
+ return parent::access()->andIf(AccessResultAllowed::allowedIf($this->getProvider()->supportsVersions()));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getSettingValue(FormStateInterface $form_state) {
+ return $this->getProvider()->getCacheTtl(ProviderInterface::CACHE_VERSIONS);
}
}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustom.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustom.php
new file mode 100644
index 0000000000000000000000000000000000000000..538d369326aba38d5773e9a0baf03e8f00c4c08f
--- /dev/null
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustom.php
@@ -0,0 +1,138 @@
+.css or .js
(with matching response MIME type). Minified URLs can also be supplied and the will be used automatically."),
+ * groups = {
+ * "cdn" = @Translation("CDN (Content Delivery Network)"),
+ * "cdn_provider" = false,
+ * "custom" = @Translation("Custom URLs"),
+ * },
+ * )
+ */
+class CdnCustom extends CdnProviderBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access() {
+ return parent::access()->andIf(AccessResultAllowed::allowedIf($this->getProvider()->getPluginId() === 'custom'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
+ $group = $this->getGroupElement($form, $form_state);
+ $group->setProperty('weight', 99);
+ $group->access($this->access());
+
+ $setting = $this->getSettingElement($form, $form_state);
+ $setting->setProperty('smart_description', FALSE);
+
+ $rows = count(array_filter(array_map('trim', preg_split("/\r\n|\n/", $form_state->getValue('cdn_custom', '')))));
+ $setting->setProperty('rows', $rows > 20 ? 20 : $rows);
+
+ $group->apply = $this->setCdnProvidersAjax(Element::createStandalone([
+ '#weight' => 100,
+ '#type' => 'submit',
+ '#value' => $this->t('Apply'),
+ '#submit' => [
+ [get_class($this), 'submitApplyCss'],
+ ],
+ ]));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefaultValue() {
+ return implode("\n", [
+ 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.css',
+ 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css',
+ 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.js',
+ 'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js',
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function processDeprecatedValues(array $values, array $deprecated) {
+ // Merge the deprecated settings together to form a new line for each value.
+ // @todo Remove deprecated setting support in a future release.
+ return implode("\n", $values) ?: NULL;
+ }
+
+ /**
+ * Submit callback for resetting CDN Provider cache.
+ *
+ * @param array $form
+ * Nested array of form elements that comprise the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public static function submitApplyCss(array $form, FormStateInterface $form_state) {
+ $theme = SystemThemeSettings::getTheme(Element::create($form, $form_state), $form_state);
+ $theme->setSetting('cdn_custom', $form_state->getValue('cdn_custom'));
+ $form_state->setRebuild();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function validateFormElement(Element $form, FormStateInterface $form_state) {
+ // Immediately return if this isn't the currently selected CDN Provider.
+ if ($form_state->getValue('cdn_provider') !== 'custom') {
+ return;
+ }
+
+ $theme = SystemThemeSettings::getTheme($form, $form_state);
+
+ /** @var \Drupal\bootstrap\Plugin\Provider\Custom $provider */
+ $provider = ProviderManager::load($theme, 'custom');
+
+ $urls = array_filter(array_map('trim', preg_split("/\r\n|\n/", $form_state->getValue('cdn_custom', ''))));
+
+ $invalid = [];
+ foreach ($urls as $url) {
+ try {
+ $provider->validateUrl($url);
+ }
+ catch (\Exception $e) {
+ $invalid[] = $e->getMessage();
+ }
+ }
+ if ($invalid) {
+ $form_state->setErrorByName('cdn_custom', t('Invalid Custom URLs: ', [
+ '@invalid' => new FormattableMarkup(implode('- ', $invalid), []),
+ ]));
+
+ // Unfortunately, any errors set during validation prevents the form
+ // rebuilding functionality from working. This has to be changed here.
+ $form->cdn->cdn_provider->custom->setProperty('open', TRUE);
+ }
+ }
+
+}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php
index f648dd976ed6e46a0bfbe954a6dbc1b031ad6ff6..a14f8a9f3c6d95a1b83cff882a38b2b06a5b2bfd 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php
@@ -2,6 +2,8 @@
namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
+
/**
* Due to BC reasons, this class cannot be moved.
*
@@ -11,10 +13,7 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
/**
* The "cdn_custom_css" theme setting.
*
- * @ingroup plugins_setting
- *
* @BootstrapSetting(
- * cdn_provider = "custom",
* id = "cdn_custom_css",
* type = "textfield",
* weight = 1,
@@ -27,5 +26,40 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* "custom" = false,
* },
* )
+ *
+ * @deprecated since 8.x-3.18. Replaced with new setting. Will be removed in a
+ * future release.
+ *
+ * @see \Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnCustom
*/
-class CdnCustomCss extends CdnProviderBase {}
+class CdnCustomCss extends CdnProviderBase implements DeprecatedSettingInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReason() {
+ return $this->t('Replaced with new setting. Will be removed in a future release.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacement() {
+ return '\Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnCustom';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacementSetting() {
+ return $this->theme->getSettingPlugin('cdn_custom');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedVersion() {
+ return '8.x-3.18';
+ }
+
+}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php
index d7c12ed7d60715409713126391a561eadc5d62ed..c10adfa8ca854074bdf8d8fa97f4c3d3fba4aac2 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php
@@ -2,6 +2,8 @@
namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
+
/**
* Due to BC reasons, this class cannot be moved.
*
@@ -11,10 +13,7 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
/**
* The "cdn_custom_css_min" theme setting.
*
- * @ingroup plugins_setting
- *
* @BootstrapSetting(
- * cdn_provider = "custom",
* id = "cdn_custom_css_min",
* type = "textfield",
* weight = 2,
@@ -27,5 +26,40 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* "custom" = false,
* },
* )
+ *
+ * @deprecated since 8.x-3.18. Replaced with new setting. Will be removed in a
+ * future release.
+ *
+ * @see \Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnCustom
*/
-class CdnCustomCssMin extends CdnProviderBase {}
+class CdnCustomCssMin extends CdnProviderBase implements DeprecatedSettingInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReason() {
+ return $this->t('Replaced with new setting. Will be removed in a future release.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacement() {
+ return '\Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnCustom';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacementSetting() {
+ return $this->theme->getSettingPlugin('cdn_custom');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedVersion() {
+ return '8.x-3.18';
+ }
+
+}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php
index 543cad23ddb6866ed812cc062838afba9e52ec72..43b1d2e4296bbdd13b0d1b2baddfe525fb629343 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php
@@ -2,6 +2,8 @@
namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
+
/**
* Due to BC reasons, this class cannot be moved.
*
@@ -11,10 +13,7 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
/**
* The "cdn_custom_js" theme setting.
*
- * @ingroup plugins_setting
- *
* @BootstrapSetting(
- * cdn_provider = "custom",
* id = "cdn_custom_js",
* type = "textfield",
* weight = 3,
@@ -27,5 +26,40 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* "custom" = false,
* },
* )
+ *
+ * @deprecated since 8.x-3.18. Replaced with new setting. Will be removed in a
+ * future release.
+ *
+ * @see \Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnCustom
*/
-class CdnCustomJs extends CdnProviderBase {}
+class CdnCustomJs extends CdnProviderBase implements DeprecatedSettingInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReason() {
+ return $this->t('Replaced with new setting. Will be removed in a future release.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacement() {
+ return '\Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnCustom';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacementSetting() {
+ return $this->theme->getSettingPlugin('cdn_custom');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedVersion() {
+ return '8.x-3.18';
+ }
+
+}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php
index e3500c652a90a6068c58b2275ef6a89bb946f921..cd4659157c40e033060748317d6ee59a4efbb767 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php
@@ -2,6 +2,8 @@
namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
+
/**
* Due to BC reasons, this class cannot be moved.
*
@@ -11,10 +13,7 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
/**
* The "cdn_custom_js_min" theme setting.
*
- * @ingroup plugins_setting
- *
* @BootstrapSetting(
- * cdn_provider = "custom",
* id = "cdn_custom_js_min",
* type = "textfield",
* weight = 4,
@@ -27,5 +26,40 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* "custom" = false,
* },
* )
+ *
+ * @deprecated since 8.x-3.18. Replaced with new setting. Will be removed in a
+ * future release.
+ *
+ * @see \Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnCustom
*/
-class CdnCustomJsMin extends CdnProviderBase {}
+class CdnCustomJsMin extends CdnProviderBase implements DeprecatedSettingInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReason() {
+ return $this->t('Replaced with new setting. Will be removed in a future release.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacement() {
+ return '\Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnCustom';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacementSetting() {
+ return $this->theme->getSettingPlugin('cdn_custom');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedVersion() {
+ return '8.x-3.18';
+ }
+
+}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php b/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php
index 6ca92bf78081e3c8da633c305a877cc0fb84d6c1..20259e06275f26ffd0d7dcdc34b38f6200e8e5af 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php
@@ -8,14 +8,11 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* @todo Move namespace up one.
*/
-use Drupal\bootstrap\Utility\Element;
-use Drupal\Core\Form\FormStateInterface;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
/**
* The "cdn_jsdelivr_theme" theme setting.
*
- * @ingroup plugins_setting
- *
* @BootstrapSetting(
* cdn_provider = "jsdelivr",
* id = "cdn_jsdelivr_theme",
@@ -31,38 +28,40 @@ use Drupal\Core\Form\FormStateInterface;
* "jsdelivr" = false,
* },
* )
+ *
+ * @deprecated since 8.x-3.18. Replaced with new setting. Will be removed in a
+ * future release.
+ *
+ * @see \Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnTheme
*/
-class CdnJsdelivrTheme extends CdnProviderBase {
+class CdnJsdelivrTheme extends CdnProviderBase implements DeprecatedSettingInterface {
/**
* {@inheritdoc}
*/
- public function buildCdnProviderElement(Element $setting, FormStateInterface $form_state) {
- $version = $form_state->getValue('cdn_jsdelivr_version', $this->theme->getSetting('cdn_jsdelivr_version'));
- $themes = $this->settingProvider->getCdnThemes($version);
-
- $options = [];
- foreach ($themes as $theme => $data) {
- $options[$theme] = $data['title'];
- }
+ public function getDeprecatedReason() {
+ return $this->t('Replaced with new setting. Will be removed in a future release.');
+ }
- $setting->setProperty('options', $options);
- $setting->setProperty('suffix', '');
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacement() {
+ return '\Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnTheme';
+ }
- if ($this->settingProvider->getCdnExceptions(FALSE)) {
- $setting->setProperty('description', t('Unable to parse the @provider API to determine themes. This theme is simply the default CSS supplied by the framework.', [
- '@provider' => $this->settingProvider->getLabel(),
- ]));
- }
- else {
- $setting->setProperty('description', t('Choose the Example Theme provided by Bootstrap or one of the many, many Bootswatch themes!', [
- ':bootswatch' => 'https://bootswatch.com/3/',
- ':bootstrap_theme' => 'https://getbootstrap.com/docs/3.4/examples/theme/',
- ]));
- }
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacementSetting() {
+ return $this->theme->getSettingPlugin('cdn_theme');
+ }
- // Check for any CDN failure(s).
- $this->checkCdnExceptions();
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedVersion() {
+ return '8.x-3.18';
}
}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php b/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php
index 771a1417ad7c22984fe7c1814a3e7a038b10cac1..1baa8a5389f14b939379191a5177df9c0aebccef 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php
@@ -8,15 +8,11 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
* @todo Move namespace up one.
*/
-use Drupal\bootstrap\Utility\Element;
-use Drupal\Component\Utility\Html;
-use Drupal\Core\Form\FormStateInterface;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
/**
* The "cdn_jsdelivr_version" theme setting.
*
- * @ingroup plugins_setting
- *
* @BootstrapSetting(
* cdn_provider = "jsdelivr",
* id = "cdn_jsdelivr_version",
@@ -31,34 +27,40 @@ use Drupal\Core\Form\FormStateInterface;
* "jsdelivr" = false,
* },
* )
+ *
+ * @deprecated since 8.x-3.18. Replaced with new setting. Will be removed in a
+ * future release.
+ *
+ * @see \Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnVersion
*/
-class CdnJsdelivrVersion extends CdnProviderBase {
+class CdnJsdelivrVersion extends CdnProviderBase implements DeprecatedSettingInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReason() {
+ return $this->t('Replaced with new setting. Will be removed in a future release.');
+ }
/**
* {@inheritdoc}
*/
- public function buildCdnProviderElement(Element $setting, FormStateInterface $form_state) {
- $plugin_id = Html::cleanCssIdentifier($this->settingProvider->getPluginId());
- $setting->setProperty('options', $this->settingProvider->getCdnVersions());
- $setting->setProperty('ajax', [
- 'callback' => [get_class($this), 'ajaxProviderCallback'],
- 'wrapper' => 'cdn-provider-' . $plugin_id,
- ]);
+ public function getDeprecatedReplacement() {
+ return '\Drupal\bootstrap\Plugin\Setting\Advanced\Cdn\CdnVersion';
+ }
- $setting->setProperty('smart_description', FALSE);
- if ($this->settingProvider->getCdnExceptions(FALSE)) {
- $setting->setProperty('description', t('Unable to parse the @provider API to determine versions. This version is the default version supplied by the base theme.', [
- '@provider' => $this->settingProvider->getLabel(),
- ]));
- }
- else {
- $setting->setProperty('description', t('These versions are automatically populated by the @provider API.', [
- '@provider' => $this->settingProvider->getLabel(),
- ]));
- }
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacementSetting() {
+ return $this->theme->getSettingPlugin('cdn_version');
+ }
- // Check for any CDN failure(s).
- $this->checkCdnExceptions();
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedVersion() {
+ return '8.x-3.18';
}
}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnProvider.php b/src/Plugin/Setting/Advanced/Cdn/CdnProvider.php
index b7ad7b7a16863cce33ec594384552215450a7d91..f817ccd007708ce360c03b2bc16383b42286cf60 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnProvider.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnProvider.php
@@ -9,13 +9,18 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
*/
use Drupal\bootstrap\Bootstrap;
+use Drupal\bootstrap\Plugin\Form\FormInterface;
use Drupal\bootstrap\Plugin\Form\SystemThemeSettings;
use Drupal\bootstrap\Plugin\Provider\Broken;
use Drupal\bootstrap\Plugin\Provider\ProviderInterface;
use Drupal\bootstrap\Plugin\ProviderManager;
use Drupal\bootstrap\Utility\Element;
use Drupal\Component\Utility\Html;
+use Drupal\Core\Access\AccessResultAllowed;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Render\Markup;
+use Drupal\Core\Url;
/**
* The "cdn_provider" theme setting.
@@ -29,6 +34,7 @@ use Drupal\Core\Form\FormStateInterface;
* title = @Translation("CDN Provider"),
* description = @Translation("Choose the CDN Provider used to load Bootstrap resources."),
* defaultValue = "jsdelivr",
+ * empty_option = @Translation("None (compile locally)"),
* empty_value = "",
* weight = -1,
* groups = {
@@ -43,48 +49,99 @@ class CdnProvider extends CdnProviderBase {
/**
* {@inheritdoc}
*/
- public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
- parent::alterFormElement($form, $form_state);
+ public function alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL) {
+ parent::alterForm($form, $form_state, $form_id);
+
+ // Allow the provider to participate.
+ if ($this->provider instanceof FormInterface) {
+ $this->provider->alterForm($form, $form_state);
+ }
+ }
+ /**
+ * {@inheritdoc}
+ */
+ public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
// Wrap the default group so it can be replaced via AJAX.
$group = $this->getGroupElement($form, $form_state);
$group->setProperty('prefix', '
');
$group->setProperty('suffix', '
');
- // Intercept possible manual import of API data via AJAX callback.
- $this->importProviderData($form_state);
-
- // Override the options with the provider manager discovery.
- $setting = $this->getSettingElement($form, $form_state);
- $setting->setProperty('empty_option', $this->t('None (compile locally)'));
- $providers = $this->theme->getCdnProviders();
+ // Set available CDN Providers.
+ $setting = $this->setCdnProvidersAjax($this->getSettingElement($form, $form_state));
$setting->setProperty('options', array_map(function (ProviderInterface $provider) {
return $provider->getLabel();
- }, $providers));
+ }, $this->theme->getCdnProviders()));
- $setting->setProperty('ajax', [
- 'callback' => [get_class($this), 'ajaxProvidersCallback'],
- 'wrapper' => 'cdn-providers',
- ]);
+ // Add the CDN Provider description.
+ $provider = $this->getProvider();
+ $description = $provider->getDescription();
+ $group->description = [
+ '#access' => AccessResultAllowed::allowedIf(!empty((string) $description) && !($provider instanceof Broken)),
+ '#type' => 'container',
+ '#theme_wrappers' => ['container__help_block'],
+ 0 => ['#markup' => $description],
+ ];
+ // Add CDN Provider cache reset functionality.
$group->cache = [
+ '#access' => AccessResultAllowed::allowedIf(!($provider instanceof Broken)),
'#type' => 'details',
'#title' => $this->t('Advanced Cache'),
'#description' => $this->t('All @provider data is intelligently and automatically cached using the various settings below. This allows the @provider data to persist through cache rebuilds. This data will invalidate and rebuild automatically, however a manual reset can be invoked below.', [
- '@provider' => $this->activeProvider->getPluginId() === 'custom' ? $this->t('CDN Provider') : $this->activeProvider->getLabel(),
+ '@provider' => $provider->getPluginId() === 'custom' ? $this->t('CDN Provider') : $provider->getLabel(),
]),
'#weight' => 1000,
];
- if (!($this->activeProvider instanceof Broken)) {
- // Add a CDN Provider cache reset button.
- if ($reset = $this->buildResetProviderCache($this->activeProvider)) {
- $group->cache->reset = $reset;
+ $ttl_settings = [
+ 'cdn_cache_ttl_versions',
+ 'cdn_cache_ttl_themes',
+ 'cdn_cache_ttl_assets',
+ 'cdn_cache_ttl_library',
+ ];
+
+ // Because these settings are used for all providers, any current value set
+ // in the input array is a result of a provider switch via AJAX. Go ahead
+ // and unset the value from the current form state and then add the setting
+ // to the form.
+ $input = $form_state->getUserInput();
+ $values = $form_state->getValues();
+ foreach ($ttl_settings as $ttl_setting) {
+ if (!empty($input['_triggering_element_name']) && $input['_triggering_element_name'] === 'cdn_provider') {
+ unset($input[$ttl_setting], $values[$ttl_setting]);
}
- $this->createProviderGroup($group, $this->activeProvider);
+ $this->theme->getSettingPlugin($ttl_setting)->alterForm($form->getArray(), $form_state);
}
- else {
- $group->cache['#access'] = FALSE;
+ $form_state->setUserInput($input);
+ $form_state->setValues($values);
+
+ $group->cache->reset = $this->setCdnProvidersAjax(Element::createStandalone([
+ '#weight' => 100,
+ '#type' => 'submit',
+ '#description' => $this->t('Note: this will not reset any cached HTTP requests; see the "Advanced" section.'),
+ '#value' => $this->t('Reset @provider Cache', [
+ '@provider' => $provider->getLabel(),
+ ]),
+ '#submit' => [
+ [get_class($this), 'submitResetProviderCache'],
+ ],
+ ]));
+
+ // Intercept possible manual import of API data via AJAX callback.
+ // @todo Import functionality is deprecated, remove in a future release.
+ $this->importProviderData($group, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm($form, $form_state);
+ $theme = SystemThemeSettings::getTheme(Element::create($form), $form_state);
+ $provider = ProviderManager::load($theme, $form_state->getValue('cdn_provider'));
+ if ($provider instanceof FormInterface) {
+ $provider->submitForm($form, $form_state);
}
}
@@ -104,34 +161,88 @@ class CdnProvider extends CdnProviderBase {
}
/**
- * Creates the necessary containers for each provider.
+ * {@inheritdoc}
+ */
+ public static function validateFormElement(Element $form, FormStateInterface $form_state) {
+ parent::validateFormElement($form, $form_state);
+ $theme = SystemThemeSettings::getTheme($form, $form_state);
+ $provider = ProviderManager::load($theme, $form_state->getValue('cdn_provider'));
+
+ // Validate the provider.
+ if (!($provider instanceof Broken)) {
+ $cdnVersion = $form_state->getValue('cdn_version', $theme->getSetting('cdn_version', Bootstrap::FRAMEWORK_VERSION));
+ $cdnTheme = $form_state->getValue('cdn_theme', $theme->getSetting('cdn_theme'));
+ $assets = $provider->getCdnAssets($cdnVersion, $cdnTheme);
+
+ // Now validate that each asset is reachable.
+ $unreachable = [];
+ foreach ($assets->toArray() as $asset) {
+ $url = $asset->getUrl();
+ if (!Bootstrap::checkUrlIsReachable($url)) {
+ $unreachable[] = Link::fromTextAndUrl($url, Url::fromUri($url)->setOption('attributes', ['target' => '_blank']));
+ }
+ }
+
+ if ($unreachable) {
+ $form_state->setErrorByName('cdn_provider', t('Unable to reach the following @provider assets: - @unreachable
', [
+ '@provider' => $provider->getLabel(),
+ '@unreachable' => Markup::create(implode('
- ', $unreachable)),
+ ]));
+ return;
+ }
+
+ // Check for exceptions (API HTTP request errors).
+ if (static::checkCdnExceptions($provider)) {
+ $form_state->setErrorByName('cdn_provider', t('Unable to use @provider assets. Please choose a different CDN Provider.', [
+ '@provider' => $provider->getLabel(),
+ ]));
+ return;
+ }
+ }
+
+ if ($provider instanceof FormInterface) {
+ $provider->validateFormElement($form, $form_state);
+ }
+ }
+
+ /**
+ * Imports data for a provider that was manually uploaded in theme settings.
*
* @param \Drupal\bootstrap\Utility\Element $group
- * The group element instance.
- * @param \Drupal\bootstrap\Plugin\Provider\ProviderInterface $provider
- * The provider instance.
+ * The setting group Element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @todo Import functionality is deprecated, remove in a future release.
*/
- protected function createProviderGroup(Element $group, ProviderInterface $provider) {
- $plugin_id = Html::cleanCssIdentifier($provider->getPluginId());
+ protected function importProviderData(Element $group, FormStateInterface $form_state) {
+ if ($form_state->getValue('clicked_button') === t('Save provider data')->render()) {
+ $provider_path = ProviderManager::FILE_PATH;
+ file_prepare_directory($provider_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
- // Create the provider container.
- $group->$plugin_id = [
- '#type' => 'container',
- '#prefix' => '
',
- '#suffix' => '
',
- ];
+ $provider = $form_state->getValue('cdn_provider', $this->theme->getSetting('cdn_provider'));
+ $file = "$provider_path/$provider.json";
- // Add in the provider description.
- if ($description = $provider->getDescription()) {
- $group->$plugin_id->description = [
- '#markup' => '' . $description . '
',
- '#weight' => -99,
- ];
+ if ($import_data = $form_state->getValue('cdn_provider_import_data', FALSE)) {
+ file_unmanaged_save_data($import_data, $file, FILE_EXISTS_REPLACE);
+ }
+ elseif ($file && file_exists($file)) {
+ file_unmanaged_delete($file);
+ }
+
+ // Clear the cached definitions so they can get rebuilt.
+ $providerManager = new ProviderManager($this->theme);
+ $providerManager->clearCachedDefinitions();
+ $form_state->setRebuild();
+ return;
}
+ $provider = $this->getProvider();
+ $plugin_id = Html::cleanCssIdentifier($provider->getPluginId());
+
// To avoid triggering unnecessary deprecation messages, extract these
// values from the provider definition directly.
- // @todo Remove when the deprecated functionality is removed.
+ // @todo Import functionality is deprecated, remove in a future release.
$definition = $provider->getPluginDefinition();
$hasError = !empty($definition['error']);
$isImported = !empty($definition['imported']);
@@ -150,13 +261,13 @@ class CdnProvider extends CdnProviderBase {
'@title' => $provider->getLabel(),
':provider_api' => $provider->getApi(),
]);
- $group->$plugin_id->error = [
+ $group->error = [
'#markup' => '' . $description_label . ': ' . $description . '
',
'#weight' => -20,
];
}
- $group->$plugin_id->import = [
+ $group->import = [
'#type' => 'details',
'#title' => t('Imported @title data', ['@title' => $provider->getLabel()]),
'#description' => t('The provider will attempt to parse the data entered here each time it is saved. If no data has been entered, any saved files associated with this provider will be removed and the provider will again attempt to request the API data normally through the following URL: :provider_api.', [
@@ -166,78 +277,16 @@ class CdnProvider extends CdnProviderBase {
'#open' => FALSE,
];
- $group->$plugin_id->import->cdn_provider_import_data = [
+ $group->import->cdn_provider_import_data = [
'#type' => 'textarea',
'#default_value' => file_exists(ProviderManager::FILE_PATH . '/' . $plugin_id . '.json') ? file_get_contents(ProviderManager::FILE_PATH . '/' . $plugin_id . '.json') : NULL,
];
- $group->$plugin_id->import->submit = [
+ $group->import->submit = $this->setCdnProvidersAjax([
'#type' => 'submit',
'#value' => t('Save provider data'),
'#executes_submit_callback' => FALSE,
- '#ajax' => [
- 'callback' => [get_class($this), 'ajaxCallback'],
- 'wrapper' => 'cdn-provider-' . $plugin_id,
- ],
- ];
- }
- }
-
- /**
- * Builds a reset button for the cache provider.
- *
- * @param \Drupal\bootstrap\Plugin\Provider\ProviderInterface $provider
- * A CDN Provider instance.
- *
- * @return \Drupal\bootstrap\Utility\Element
- * The reset element.
- */
- protected function buildResetProviderCache(ProviderInterface $provider) {
- $reset = Element::createStandalone([
- '#type' => 'item',
- '#weight' => 100,
- ]);
-
- $reset->submit = Element::createStandalone([
- '#type' => 'submit',
- '#description' => $this->t('Note: this will not reset any cached HTTP requests; see the "Advanced" section.'),
- '#value' => $this->t('Reset @provider Cache', [
- '@provider' => $provider->getLabel(),
- ]),
- '#submit' => [
- [get_class($this), 'submitResetProviderCache'],
- ],
- '#ajax' => [
- 'callback' => [get_class($this), 'ajaxProvidersCallback'],
- 'wrapper' => 'cdn-providers',
- ],
- ]);
- return $reset;
- }
-
- /**
- * Imports data for a provider that was manually uploaded in theme settings.
- *
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
- */
- protected function importProviderData(FormStateInterface $form_state) {
- if ($form_state->getValue('clicked_button') === t('Save provider data')->render()) {
- $provider_path = ProviderManager::FILE_PATH;
- file_prepare_directory($provider_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
-
- $provider = $form_state->getValue('cdn_provider', $this->theme->getSetting('cdn_provider'));
- $file = "$provider_path/$provider.json";
-
- if ($import_data = $form_state->getValue('cdn_provider_import_data', FALSE)) {
- file_unmanaged_save_data($import_data, $file, FILE_EXISTS_REPLACE);
- }
- elseif ($file && file_exists($file)) {
- file_unmanaged_delete($file);
- }
-
- // Clear the cached definitions so they can get rebuilt.
- $this->providerManager->clearCachedDefinitions();
+ ]);
}
}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php b/src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php
index 30c321257c2fd3c775f2e4b587f6c6701e6f9370..94eda0ea92c30a8822687dafc6aa3679421d689c 100644
--- a/src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php
@@ -9,10 +9,12 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn;
*/
use Drupal\bootstrap\Bootstrap;
+use Drupal\bootstrap\Plugin\Provider\ProviderInterface;
use Drupal\bootstrap\Plugin\ProviderManager;
use Drupal\bootstrap\Plugin\Setting\SettingBase;
use Drupal\bootstrap\Traits\FormAutoloadFixTrait;
use Drupal\bootstrap\Utility\Element;
+use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
@@ -31,30 +33,7 @@ abstract class CdnProviderBase extends SettingBase {
*
* @var \Drupal\bootstrap\Plugin\Provider\ProviderInterface
*/
- protected $activeProvider;
-
- /**
- * The setting provider.
- *
- * @var \Drupal\bootstrap\Plugin\Provider\ProviderInterface
- */
- protected $settingProvider;
-
- /**
- * The current provider manager instance.
- *
- * @var \Drupal\bootstrap\Plugin\ProviderManager
- */
- protected $providerManager;
-
- /**
- * {@inheritdoc}
- */
- public function __construct(array $configuration, $plugin_id, $plugin_definition) {
- parent::__construct($configuration, $plugin_id, $plugin_definition);
- $this->providerManager = new ProviderManager($this->theme);
- $this->settingProvider = $this->providerManager->get(isset($plugin_definition['cdn_provider']) ? $plugin_definition['cdn_provider'] : NULL);
- }
+ protected $provider;
/**
* {@inheritdoc}
@@ -62,32 +41,42 @@ abstract class CdnProviderBase extends SettingBase {
public function alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL) {
// Add autoload fix to make sure AJAX callbacks work.
static::formAutoloadFix($form_state);
- $this->activeProvider = $this->providerManager->get($form_state->getValue('cdn_provider', $this->theme->getSetting('cdn_provider')));
- $this->alterFormElement(Element::create($form), $form_state);
- }
- /**
- * {@inheritdoc}
- */
- public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
- // Immediately return if it's not the provider that should be configured.
- if ($this->activeProvider->getPluginId() !== $this->settingProvider->getPluginId()) {
- return;
- }
- $setting = $this->getSettingElement($form, $form_state);
- $this->buildCdnProviderElement($setting, $form_state);
+ // Attempt to extract the active provider from submitted values. Note: in
+ // some cases, it needs to be extracted from the raw input if the values
+ // haven't yet been populated.
+ $input = $form_state->getUserInput();
+ $provider = $form_state->getValue('cdn_provider', isset($input['cdn_provider']) ? Html::escape($input['cdn_provider']) : NULL);
+ $this->provider = ProviderManager::load($this->theme, $provider);
+
+ // Invoke the original alter.
+ parent::alterForm($form, $form_state, $form_id);
}
/**
- * Builds the setting element for the CDN Provider.
+ * Handles any CDN Provider exceptions that may have been thrown.
*
- * @param \Drupal\bootstrap\Utility\Element $setting
- * The Element object that comprises the setting.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
+ * @param \Drupal\bootstrap\Plugin\Provider\ProviderInterface $provider
+ * A CDN Provider to check.
+ * @param bool $reset
+ * Flag indicating whether to remove the Exceptions once they have been
+ * retrieved.
+ *
+ * @return bool
+ * TRUE if there are exceptions, FALSE otherwise.
*/
- public function buildCdnProviderElement(Element $setting, FormStateInterface $form_state) {
- // Allow settings to build more.
+ protected static function checkCdnExceptions(ProviderInterface $provider, $reset = TRUE) {
+ $exceptions = $provider->getCdnExceptions($reset);
+ if ($exceptions) {
+ Bootstrap::message(t('Unable to parse @provider data. Check the logs for more details. If your issues are network related, consider using the "custom" CDN Provider instead to statically set the URLs that should be used.', [
+ ':logs' => Url::fromRoute('dblog.overview')->toString(),
+ '@provider' => $provider->getLabel(),
+ ]), 'error');
+ foreach ($exceptions as $exception) {
+ watchdog_exception('bootstrap', $exception);
+ }
+ }
+ return !!$exceptions;
}
/**
@@ -97,21 +86,12 @@ abstract class CdnProviderBase extends SettingBase {
* Nested array of form elements that comprise the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
- */
- public static function ajaxProvidersCallback(array $form, FormStateInterface $form_state) {
- return $form['cdn']['cdn_provider'];
- }
-
- /**
- * AJAX callback for reloading a specific CDN Provider.
*
- * @param array $form
- * Nested array of form elements that comprise the form.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
+ * @return array
+ * The form element to render.
*/
- public static function ajaxProviderCallback(array $form, FormStateInterface $form_state) {
- return $form['cdn']['cdn_provider'][$form_state->getValue('cdn_provider', Bootstrap::getTheme()->getSetting('cdn_provider'))];
+ public static function ajaxCdnProvidersCallback(array $form, FormStateInterface $form_state) {
+ return $form['cdn']['cdn_provider'];
}
/**
@@ -122,18 +102,32 @@ abstract class CdnProviderBase extends SettingBase {
}
/**
- * Handles any CDN Provider exceptions that may have been thrown.
+ * Retrieves the active CDN Provider.
+ *
+ * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface
+ * A CDN Provider.
*/
- protected function checkCdnExceptions() {
- if ($exceptions = $this->settingProvider->getCdnExceptions()) {
- drupal_set_message($this->t('Unable to parse @provider CDN data. Check the logs for more details. If your issues are network related, consider using the "custom" CDN Provider instead to statically set the URLs that should be used.', [
- ':logs' => Url::fromRoute('dblog.overview')->toString(),
- '@provider' => $this->settingProvider->getLabel(),
- ]), 'error');
- foreach ($exceptions as $exception) {
- watchdog_exception($this->theme->getName(), $exception);
- }
+ protected function getProvider() {
+ if (!isset($this->provider)) {
+ $this->provider = $this->theme->getCdnProvider();
}
+ return $this->provider;
+ }
+
+ /**
+ * Sets the #ajax property to rebuild the entire CDN Providers container.
+ *
+ * @param \Drupal\bootstrap\Utility\Element|array $element
+ * An Element to modify.
+ *
+ * @return \Drupal\bootstrap\Utility\Element
+ * The Element passed.
+ */
+ protected function setCdnProvidersAjax($element) {
+ return Element::create($element)->setProperty('ajax', [
+ 'callback' => [get_class($this), 'ajaxCdnProvidersCallback'],
+ 'wrapper' => 'cdn-providers',
+ ]);
}
}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnTheme.php b/src/Plugin/Setting/Advanced/Cdn/CdnTheme.php
new file mode 100644
index 0000000000000000000000000000000000000000..2be763767a464ee96f0059369557a7f78f14d3e9
--- /dev/null
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnTheme.php
@@ -0,0 +1,76 @@
+getSettingElement($form, $form_state);
+ $setting->setProperty('suffix', '');
+
+ // Immediately return if the provider doesn't support themes.
+ $provider = $this->getProvider();
+ if (!$provider->supportsThemes()) {
+ $setting->access(FALSE);
+ return;
+ }
+
+ $version = $form_state->getValue('cdn_version', $this->theme->getSetting('cdn_version'));
+
+ $exceptions = $provider->trackCdnExceptions(function () use ($provider, $setting, $version) {
+ $options = [];
+ $themes = $provider->getCdnThemes($version);
+ foreach ($themes as $theme => $assets) {
+ $options[ucfirst($assets->getLibrary())][$theme] = $assets->getLabel();
+ }
+
+ $setting->setProperty('options', $options);
+ });
+
+ // Check for any CDN failure(s).
+ if ($exceptions) {
+ $setting->setError($this->t('Unable to parse the @provider API to determine available themes.', [
+ '@provider' => $provider->getLabel(),
+ ]));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function processDeprecatedValues(array $values, array $deprecated) {
+ // @todo Remove deprecated setting support in a future release.
+ $deprecated = "cdn_{$this->getProvider()->getPluginId()}_theme";
+ return isset($values[$deprecated]) ? $values[$deprecated] : NULL;
+ }
+
+}
diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnVersion.php b/src/Plugin/Setting/Advanced/Cdn/CdnVersion.php
new file mode 100644
index 0000000000000000000000000000000000000000..698f22078d353cba7e21845888c0051cc5bfddae
--- /dev/null
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnVersion.php
@@ -0,0 +1,72 @@
+setCdnProvidersAjax($this->getSettingElement($form, $form_state));
+
+ // Immediately return if the provider doesn't support versions.
+ $provider = $this->getProvider();
+ if (!$provider->supportsVersions()) {
+ $setting->access(FALSE);
+ return;
+ }
+
+ $setting->setProperty('description', $this->t('These versions are automatically populated by the @provider API.', [
+ '@provider' => $provider->getLabel(),
+ ]));
+
+ $exceptions = $provider->trackCdnExceptions(function () use ($provider, $setting) {
+ $setting->setProperty('options', $provider->getCdnVersions());
+ });
+
+ // Check for tracked CDN exceptions.
+ if ($exceptions) {
+ $setting->setError($this->t('Unable to parse the @provider API to determine available versions.', [
+ '@provider' => $provider->getLabel(),
+ ]));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function processDeprecatedValues(array $values, array $deprecated) {
+ // @todo Remove deprecated setting support in a future release.
+ $deprecated = "cdn_{$this->getProvider()->getPluginId()}_version";
+ return isset($values[$deprecated]) ? $values[$deprecated] : NULL;
+ }
+
+}
diff --git a/src/Plugin/Setting/DeprecatedSettingInterface.php b/src/Plugin/Setting/DeprecatedSettingInterface.php
index 6c0ae9a010e91118d0f9bab99f930cd4691bb6fc..72e7bedf49b17fc96796b800f794c610674ba8c3 100644
--- a/src/Plugin/Setting/DeprecatedSettingInterface.php
+++ b/src/Plugin/Setting/DeprecatedSettingInterface.php
@@ -2,8 +2,19 @@
namespace Drupal\bootstrap\Plugin\Setting;
+use Drupal\bootstrap\DeprecatedInterface;
+
/**
- * Interface DeprecatedSettingInterface.
+ * Interface DeprecatedInterface.
*/
-interface DeprecatedSettingInterface {
+interface DeprecatedSettingInterface extends DeprecatedInterface, SettingInterface {
+
+ /**
+ * The setting that replaces the deprecated setting.
+ *
+ * @return \Drupal\bootstrap\Plugin\Setting\SettingInterface
+ * The replacement setting.
+ */
+ public function getDeprecatedReplacementSetting();
+
}
diff --git a/src/Plugin/Setting/JavaScript/Popovers/PopoverEnabled.php b/src/Plugin/Setting/JavaScript/Popovers/PopoverEnabled.php
index 642d5a04f81db39cd9b1a85e1d8f8e645af551ab..186e66b2b0096ad8a44e9df3760977602a107645 100644
--- a/src/Plugin/Setting/JavaScript/Popovers/PopoverEnabled.php
+++ b/src/Plugin/Setting/JavaScript/Popovers/PopoverEnabled.php
@@ -15,7 +15,7 @@ use Drupal\Core\Form\FormStateInterface;
* id = "popover_enabled",
* type = "checkbox",
* title = @Translation("Enable Bootstrap Popovers"),
- * description = @Translation("Elements that have the data-toggle="popover"
attribute set will automatically initialize the popover upon page load. WARNING: This feature can sometimes impact performance. Disable if pages appear to hang after initial load."),
+ * description = @Translation("Elements that have the data-toggle="popover"
attribute set will automatically initialize the popover upon page load. WARNING: This feature can sometimes impact performance. Disable if pages appear to hang after load.
"),
* defaultValue = 1,
* weight = -1,
* groups = {
diff --git a/src/Plugin/Setting/JavaScript/Popovers/PopoverTriggerAutoclose.php b/src/Plugin/Setting/JavaScript/Popovers/PopoverTriggerAutoclose.php
index 8154772477e53ba54bfe7ed9130cf1ae76969683..7b7e768b1b53a9d9ddc14fd29d1d51a537f9da31 100644
--- a/src/Plugin/Setting/JavaScript/Popovers/PopoverTriggerAutoclose.php
+++ b/src/Plugin/Setting/JavaScript/Popovers/PopoverTriggerAutoclose.php
@@ -27,4 +27,33 @@ use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
* @see \Drupal\bootstrap\Plugin\Setting\JavaScript\Popovers\PopoverAutoClose
*/
class PopoverTriggerAutoclose extends PopoverAutoClose implements DeprecatedSettingInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReason() {
+ return $this->t('Replaced with new setting. Will be removed in a future release.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacement() {
+ return '\Drupal\bootstrap\Plugin\Setting\JavaScript\Popovers\PopoverAutoClose';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedReplacementSetting() {
+ return $this->theme->getSettingPlugin('popover_auto_close');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeprecatedVersion() {
+ return '8.x-3.14';
+ }
+
}
diff --git a/src/Plugin/Setting/JavaScript/Tooltips/TooltipEnabled.php b/src/Plugin/Setting/JavaScript/Tooltips/TooltipEnabled.php
index 23450c20d751e16a67a4d56ffa2795413f3d99dd..a0003397cf66be56354e79c928d3bb0e87bdcccc 100644
--- a/src/Plugin/Setting/JavaScript/Tooltips/TooltipEnabled.php
+++ b/src/Plugin/Setting/JavaScript/Tooltips/TooltipEnabled.php
@@ -15,7 +15,7 @@ use Drupal\Core\Form\FormStateInterface;
* id = "tooltip_enabled",
* type = "checkbox",
* title = @Translation("Enable Bootstrap Tooltips"),
- * description = @Translation("Elements that have the data-toggle="tooltip"
attribute set will automatically initialize the tooltip upon page load. WARNING: This feature can sometimes impact performance. Disable if pages appear to "hang" after initial load."),
+ * description = @Translation("Elements that have the data-toggle="tooltip"
attribute set will automatically initialize the tooltip upon page load. WARNING: This feature can sometimes impact performance. Disable if pages appear to "hang" after load.
"),
* defaultValue = 1,
* weight = -1,
* groups = {
diff --git a/src/Plugin/Setting/Schemas.php b/src/Plugin/Setting/Schemas.php
index bdf82580183f0887b86a276c483dca11100f89cf..3b517791da7e0d291551f19bbf261da56718477a 100644
--- a/src/Plugin/Setting/Schemas.php
+++ b/src/Plugin/Setting/Schemas.php
@@ -200,7 +200,7 @@ class Schemas extends SettingBase {
'#items' => $results['success'],
'#context' => ['type' => 'success'],
]);
- drupal_set_message(new FormattableMarkup('@message' . $list->renderPlain(), [
+ Bootstrap::message(new FormattableMarkup('@message' . $list->renderPlain(), [
'@message' => t('Successfully completed the following theme updates:'),
]));
}
@@ -212,7 +212,7 @@ class Schemas extends SettingBase {
'#items' => $results['errors'],
'#context' => ['type' => 'errors'],
]);
- drupal_set_message(new FormattableMarkup('@message' . $list->renderPlain(), [
+ Bootstrap::message(new FormattableMarkup('@message' . $list->renderPlain(), [
'@message' => t('The following theme updates could not be completed:'),
]), 'error');
}
diff --git a/src/Plugin/Setting/SettingBase.php b/src/Plugin/Setting/SettingBase.php
index c368451d5bff75c28482d5663a20ece9e8a425bc..8860c487e7655546aa5bc62cfa7bb63bbdbde3e2 100644
--- a/src/Plugin/Setting/SettingBase.php
+++ b/src/Plugin/Setting/SettingBase.php
@@ -5,6 +5,8 @@ namespace Drupal\bootstrap\Plugin\Setting;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\PluginBase;
use Drupal\bootstrap\Utility\Element;
+use Drupal\Core\Access\AccessResultAllowed;
+use Drupal\Core\Access\AccessResultForbidden;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
@@ -15,11 +17,24 @@ use Drupal\Core\Url;
*/
class SettingBase extends PluginBase implements SettingInterface {
+ public static $autoUserInterface = TRUE;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access() {
+ // Hide the setting if is been deprecated.
+ if ($this instanceof DeprecatedSettingInterface) {
+ return AccessResultForbidden::forbidden();
+ }
+ return AccessResultAllowed::allowed();
+ }
+
/**
* {@inheritdoc}
*/
public function alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL) {
- $this->alterFormElement(Element::create($form), $form_state);
+ $this->alterFormElement(Element::create($form, $form_state), $form_state);
}
/**
@@ -29,6 +44,13 @@ class SettingBase extends PluginBase implements SettingInterface {
$this->getSettingElement($form, $form_state);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function autoCreateFormElement() {
+ return !($this instanceof DeprecatedSettingInterface);
+ }
+
/**
* {@inheritdoc}
*/
@@ -75,6 +97,13 @@ class SettingBase extends PluginBase implements SettingInterface {
return isset($this->pluginDefinition['defaultValue']) ? $this->pluginDefinition['defaultValue'] : NULL;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return isset($this->pluginDefinition['description']) ? $this->pluginDefinition['description'] : NULL;
+ }
+
/**
* {@inheritdoc}
*
@@ -82,7 +111,7 @@ class SettingBase extends PluginBase implements SettingInterface {
*/
public function getGroup(array &$form, FormStateInterface $form_state) {
Bootstrap::deprecated();
- return $this->getGroupElement(Element::create($form), $form_state);
+ return $this->getGroupElement(Element::create($form, $form_state), $form_state);
}
/**
@@ -100,7 +129,7 @@ class SettingBase extends PluginBase implements SettingInterface {
else {
$group->$key = ['#type' => 'container'];
}
- $group = Element::create($group->$key->getArray());
+ $group = Element::create($group->$key->getArray(), $form_state);
if ($first) {
$group->setProperty('group', 'bootstrap');
}
@@ -109,7 +138,7 @@ class SettingBase extends PluginBase implements SettingInterface {
}
}
else {
- $group = Element::create($group->$key->getArray());
+ $group = Element::create($group->$key->getArray(), $form_state);
}
$first = FALSE;
}
@@ -130,7 +159,7 @@ class SettingBase extends PluginBase implements SettingInterface {
*/
public function getElement(array &$form, FormStateInterface $form_state) {
Bootstrap::deprecated();
- return $this->getSettingElement(Element::create($form), $form_state);
+ return $this->getSettingElement(Element::create($form, $form_state), $form_state);
}
/**
@@ -153,8 +182,17 @@ class SettingBase extends PluginBase implements SettingInterface {
$group->$plugin_id->setProperty($name, $value);
}
+ // Get the default value.
+ $default_value = $this->getSettingValue($form_state);
+
+ // Convert value from an array into a newline separated value.
+ // @todo Remove once settings have proper config schemas in place.
+ // @see https://www.drupal.org/project/bootstrap/issues/2883714
+ if ($group->$plugin_id->getProperty('type') === 'textarea' && is_array($default_value)) {
+ $default_value = implode("\n", $default_value);
+ }
+
// Set default value from the stored form state value or theme setting.
- $default_value = $form_state->getValue($plugin_id, $this->theme->getSetting($plugin_id));
$group->$plugin_id->setProperty('default_value', $default_value);
// Append additional "see" link references to the description.
@@ -168,7 +206,7 @@ class SettingBase extends PluginBase implements SettingInterface {
'#attributes' => [
'target' => '_blank',
],
- ]);
+ ], $form_state);
$links[] = (string) $link->renderPlain();
}
if (!empty($links)) {
@@ -179,14 +217,26 @@ class SettingBase extends PluginBase implements SettingInterface {
}
}
- // Hide the setting if is been deprecated.
- if ($this instanceof DeprecatedSettingInterface) {
- $group->$plugin_id->access(FALSE);
- }
+ // Set accessibility.
+ $group->$plugin_id->access($this->access());
return $group->$plugin_id;
}
+ /**
+ * Retrieves the setting value used to populate the form.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ *
+ * @return mixed
+ * The setting value.
+ */
+ protected function getSettingValue(FormStateInterface $form_state) {
+ $plugin_id = $this->getPluginId();
+ return $form_state->getValue($plugin_id, $this->theme->getSetting($plugin_id));
+ }
+
/**
* {@inheritdoc}
*/
@@ -194,11 +244,20 @@ class SettingBase extends PluginBase implements SettingInterface {
return !empty($this->pluginDefinition['title']) ? $this->pluginDefinition['title'] : NULL;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function processDeprecatedValues(array $values, array $deprecated) {
+ // Most deprecated settings will be a 1:1 map. Anything more complex than
+ // this should be handled by the newer replacement setting itself.
+ return !empty($values) ? reset($values) : NULL;
+ }
+
/**
* {@inheritdoc}
*/
public static function submitForm(array &$form, FormStateInterface $form_state) {
- static::submitFormElement(Element::create($form), $form_state);
+ static::submitFormElement(Element::create($form, $form_state), $form_state);
}
/**
@@ -210,7 +269,7 @@ class SettingBase extends PluginBase implements SettingInterface {
* {@inheritdoc}
*/
public static function validateForm(array &$form, FormStateInterface $form_state) {
- static::validateFormElement(Element::create($form), $form_state);
+ static::validateFormElement(Element::create($form, $form_state), $form_state);
}
/**
diff --git a/src/Plugin/Setting/SettingInterface.php b/src/Plugin/Setting/SettingInterface.php
index d41626549c8b600daa9a547d660d243e4f4ba078..9da31bf13b7c88476895c2f88044a57f5334f143 100644
--- a/src/Plugin/Setting/SettingInterface.php
+++ b/src/Plugin/Setting/SettingInterface.php
@@ -14,6 +14,22 @@ use Drupal\Core\Form\FormStateInterface;
*/
interface SettingInterface extends PluginInspectionInterface, FormInterface {
+ /**
+ * Indicates whether a setting is accessible.
+ *
+ * @return \Drupal\Core\Access\AccessResultInterface
+ * The value to supply to the setting's #access property.
+ */
+ public function access();
+
+ /**
+ * Indicates whether a form element should be created automatically.
+ *
+ * @return bool
+ * TRUE or FALSE
+ */
+ public function autoCreateFormElement();
+
/**
* Determines whether a theme setting should added to drupalSettings.
*
@@ -45,6 +61,14 @@ interface SettingInterface extends PluginInspectionInterface, FormInterface {
*/
public function getDefaultValue();
+ /**
+ * Retrieves the setting's description, if any.
+ *
+ * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+ * The setting description.
+ */
+ public function getDescription();
+
/**
* Retrieves the group form element the setting belongs to.
*
@@ -125,4 +149,18 @@ interface SettingInterface extends PluginInspectionInterface, FormInterface {
*/
public function getTitle();
+ /**
+ * Retrieves the value from other deprecated settings.
+ *
+ * @param array $values
+ * An array of values, keyed by deprecated setting name.
+ * @param \Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface[] $deprecated
+ * An array of deprecated Setting objects indicating this setting replaced
+ * theirs, keyed by deprecated setting name.
+ *
+ * @return string
+ * The setting's deprecated value.
+ */
+ public function processDeprecatedValues(array $values, array $deprecated);
+
}
diff --git a/src/Plugin/SettingManager.php b/src/Plugin/SettingManager.php
index 7e5d97cde8b2d634415a969c428490d15d58b16e..63b3c5e90220d752a5b282f11bb0a5d7c930c26d 100644
--- a/src/Plugin/SettingManager.php
+++ b/src/Plugin/SettingManager.php
@@ -3,7 +3,7 @@
namespace Drupal\bootstrap\Plugin;
use Drupal\bootstrap\Theme;
-use Drupal\Component\Utility\SortArray;
+use Drupal\bootstrap\Utility\SortArray;
/**
* Manages discovery and instantiation of Bootstrap theme settings.
@@ -12,6 +12,19 @@ use Drupal\Component\Utility\SortArray;
*/
class SettingManager extends PluginManager {
+ /**
+ * Provides the order of top-level groups.
+ *
+ * @var string[]
+ */
+ protected static $groupOrder = [
+ 'general',
+ 'components',
+ 'javascript',
+ 'cdn',
+ 'advanced',
+ ];
+
/**
* Constructs a new \Drupal\bootstrap\Plugin\SettingManager object.
*
@@ -29,40 +42,71 @@ class SettingManager extends PluginManager {
public function getDefinitions($sorted = TRUE) {
$definitions = parent::getDefinitions(FALSE);
if ($sorted) {
- $groups = [];
- foreach ($definitions as $plugin_id => $definition) {
- $key = !empty($definition['groups']) ? implode(':', array_keys($definition['groups'])) : '_default';
- $groups[$key][$plugin_id] = $definition;
- }
- ksort($groups);
- $definitions = [];
- foreach ($groups as $settings) {
- uasort($settings, [$this, 'sort']);
- $definitions = array_merge($definitions, $settings);
-
- }
+ uasort($definitions, [$this, 'sort']);
}
return $definitions;
}
/**
- * Sorts a structured array by either a set 'weight' property or by the ID.
+ * Sorts the setting plugin definitions.
+ *
+ * Sorts setting plugin definitions in the following order:
+ * - First by top level group.
+ * - Then by sub-groups.
+ * - Then by weight.
+ * - Then by identifier.
*
* @param array $a
- * First item for comparison.
+ * First plugin definition for comparison.
* @param array $b
- * Second item for comparison.
+ * Second plugin definition for comparison.
*
* @return int
- * The comparison result for uasort().
+ * The comparison result.
*/
public static function sort(array $a, array $b) {
- if (isset($a['weight']) || isset($b['weight'])) {
- return SortArray::sortByWeightElement($a, $b);
+ $aIndex = static::getTopLevelGroupIndex($a);
+ $bIndex = static::getTopLevelGroupIndex($b);
+
+ // Top level group isn't the same, sort by index.
+ if ($aIndex !== $bIndex) {
+ return $aIndex - $bIndex;
+ }
+
+ // Next sort by all groups (sub-groups).
+ $result = SortArray::sortByKeyString($a, $b, 'groups');
+
+ // Groups are the same.
+ if ($result === 0) {
+ // Sort by weight.
+ $result = SortArray::sortByWeightElement($a, $b);
+
+ // Weights are the same.
+ if ($result === 0) {
+ // Sort by plugin identifier.
+ $result = SortArray::sortByKeyString($a, $b, 'id');
+ }
}
- else {
- return SortArray::sortByKeyString($a, $b, 'id');
+
+ return $result;
+ }
+
+ /**
+ * Retrieves the index of the top level group.
+ *
+ * @param array $definition
+ * A plugin definition.
+ *
+ * @return int
+ * The array index of the top level group.
+ */
+ public static function getTopLevelGroupIndex(array $definition) {
+ $groups = !empty($definition['groups']) ? array_keys($definition['groups']) : [];
+ $index = array_search(reset($groups), static::$groupOrder);
+ if ($index === FALSE) {
+ $index = -1;
}
+ return $index;
}
}
diff --git a/src/SerializedResponse.php b/src/SerializedResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..2464a9c3be14bc76ce561bc74f01b2b074bc7d7b
--- /dev/null
+++ b/src/SerializedResponse.php
@@ -0,0 +1,226 @@
+ [
+ 'text/css',
+ ],
+ 'js' => [
+ 'application/javascript',
+ 'application/x-javascript',
+ 'text/javascript',
+ ],
+ 'json' => [
+ 'application/hal+json',
+ 'application/json',
+ 'application/vnd.api+json',
+ 'application/x-json',
+ 'text/json',
+ ],
+ 'yaml' => [
+ 'application/x-yaml',
+ 'application/yaml',
+ 'text/yaml',
+ 'text/yml',
+ ],
+ 'yml' => [
+ 'application/x-yaml',
+ 'application/yaml',
+ 'text/yaml',
+ 'text/yml',
+ ],
+ ];
+
+ /**
+ * A map of formats, keyed by MIME type.
+ *
+ * @var array
+ */
+ protected static $mimeFormatMap = [
+ 'application/hal+json' => 'json',
+ 'application/json' => 'json',
+ 'application/vnd.api+json' => 'json',
+ 'application/x-json' => 'json',
+ 'application/x-yaml' => 'yaml',
+ 'application/yaml' => 'yaml',
+ 'text/json' => 'json',
+ 'text/yaml' => 'yaml',
+ 'text/yml' => 'yaml',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($content = '', $status = 200, array $headers = [], Request $request = NULL) {
+ parent::__construct($content, $status, $headers);
+ $this->request = $request;
+
+ // Attempt to determine the format, based on the response content type.
+ $contentType = $this->getMimeType();
+ if (isset(static::$mimeFormatMap[$contentType])) {
+ $this->format = static::$mimeFormatMap[$contentType];
+ }
+ elseif (($extension = $this->getExtension()) && isset(static::$mimeFormatMap["text/$extension"])) {
+ $this->format = static::$mimeFormatMap["text/$extension"];
+ }
+
+ if (($serializer = static::getSerializer()) && ($data = $serializer->decode($content))) {
+ $this->data = $data;
+ $this->content = NULL;
+ }
+ }
+
+ /**
+ * Creates a new SerializedResponse object from a Guzzle Response object.
+ *
+ * @param \GuzzleHttp\Psr7\Response $response
+ * A Guzzle Response object.
+ * @param \GuzzleHttp\Psr7\Request $request
+ * Optional. The Guzzle Request object associated with the response.
+ *
+ * @return static
+ */
+ public static function createFromGuzzleResponse(GuzzleResponse $response, GuzzleRequest $request = NULL) {
+ // In order to actually cache any request or response body contents, they
+ // must be extracted from the stream before it's stored in the database.
+ return new static($response->getBody(TRUE)->getContents(), $response->getStatusCode(), $response->getHeaders(), static::createRequestFromGuzzleRequest($request));
+ }
+
+ /**
+ * Creates a new SerializedResponse object from an Exception object.
+ *
+ * @param \Exception $exception
+ * The exception thrown.
+ * @param \GuzzleHttp\Psr7\Request $request
+ * Optional. The Guzzle Request object associated with the response.
+ *
+ * @return static
+ */
+ public static function createFromException(\Exception $exception, GuzzleRequest $request = NULL) {
+ return new static($exception->getMessage(), $exception->getCode() ?: 500, [], static::createRequestFromGuzzleRequest($request));
+ }
+
+ /**
+ * Creates a Symfony Request object from a Guzzle Request object.
+ *
+ * @param \GuzzleHttp\Psr7\Request $request
+ * The Guzzle Request object.
+ *
+ * @return \Symfony\Component\HttpFoundation\Request
+ * A Symfony Request object.
+ */
+ protected static function createRequestFromGuzzleRequest(GuzzleRequest $request) {
+ return Request::create($request->getUri(), $request->getMethod(), ['headers' => $request->getHeaders()], [], [], [], $request->getBody(TRUE)->getContents());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent() {
+ if (!isset($this->content) && ($serializer = $this->getSerializer()) && ($data = $this->getData())) {
+ return $serializer->encode($data);
+ }
+ return $this->content;
+ }
+
+ /**
+ * Retrieves the file extension from the request URI, if any.
+ *
+ * @return string
+ * The extension.
+ */
+ public function getExtension() {
+ return $this->request ? pathinfo($this->request->getPathInfo(), PATHINFO_EXTENSION) : '';
+ }
+
+ /**
+ * Retrieves the MIME type from the response Content-Type header.
+ *
+ * @return string
+ * The MIME type.
+ */
+ public function getMimeType() {
+ $types = explode(';', $this->headers->get('Content-Type', ''));
+ return reset($types) ?: NULL;
+ }
+
+ /**
+ * Retrieves a format specific Serialization service.
+ *
+ * @return \Drupal\Component\Serialization\SerializationInterface|false
+ * A format specific Serialization service.
+ */
+ protected function getSerializer() {
+ if (!isset(static::$serializer)) {
+ static::$serializer = $this->format && \Drupal::hasService("serialization.{$this->format}") ? \Drupal::service("serialization.{$this->format}") : FALSE;
+ }
+ return static::$serializer;
+ }
+
+ /**
+ * Retrieves the data array.
+ *
+ * @return array
+ * The data array.
+ */
+ public function getData() {
+ return $this->data;
+ }
+
+ /**
+ * Ensures the MIME type matches the request file extension.
+ *
+ * @return bool
+ * TRUE or FALSE
+ */
+ public function validMimeExtension() {
+ $extension = $this->getExtension();
+ $mimeType = $this->getMimeType();
+ return isset(static::$mimeExtensionMap[$extension]) && in_array($mimeType, static::$mimeExtensionMap[$extension]);
+ }
+
+}
diff --git a/src/Theme.php b/src/Theme.php
index 28a8dd624fac54cae5d5d1d6797b2842ef1c4090..b6398a9eb1e2c8913c8beb023bc3abd496b978d8 100644
--- a/src/Theme.php
+++ b/src/Theme.php
@@ -2,7 +2,6 @@
namespace Drupal\bootstrap;
-use Drupal\bootstrap\Plugin\Provider\Broken;
use Drupal\bootstrap\Plugin\ProviderManager;
use Drupal\bootstrap\Plugin\SettingManager;
use Drupal\bootstrap\Plugin\UpdateManager;
@@ -117,6 +116,13 @@ class Theme {
*/
protected $name;
+ /**
+ * An array of Setting instances.
+ *
+ * @var \Drupal\bootstrap\Plugin\Setting\SettingInterface[]
+ */
+ protected $settings;
+
/**
* The current theme Extension object.
*
@@ -342,7 +348,7 @@ class Theme {
// Generate a unique hash for all parameters passed as a change in any of
// them could potentially return different results.
- $hash = Crypt::generateHash($mask, $path, $options);
+ $hash = Crypt::generateBase64HashIdentifier($options, [$mask, $path]);
if (!$files->has($hash)) {
$files->set($hash, file_scan_directory($path, $mask, $options));
@@ -410,13 +416,13 @@ class Theme {
/**
* Retrieves the set CDN Provider instance for the theme.
*
- * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface|null
- * A CDN Provider instance, NULL if CDN Provider is not set.
+ * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface
+ * A CDN Provider instance.
*/
public function getCdnProvider() {
$provider = $this->getSetting('cdn_provider');
$providers = $this->getCdnProviders();
- return isset($providers[$provider]) ? $providers[$provider] : NULL;
+ return isset($providers[$provider]) ? $providers[$provider] : ProviderManager::broken();
}
/**
@@ -538,30 +544,38 @@ class Theme {
*
* @param string $name
* Optional. The name of a specific setting plugin instance to return.
+ * @param bool $rebuild
+ * Flag indicating whether to reset any cached definitions and rebuild
+ * the settings.
*
* @return \Drupal\bootstrap\Plugin\Setting\SettingInterface|\Drupal\bootstrap\Plugin\Setting\SettingInterface[]|null
* If $name was provided, it will either return a specific setting plugin
* instance or NULL if not set. If $name was omitted it will return an array
* of setting plugin instances, keyed by their name.
*/
- public function getSettingPlugin($name = NULL) {
- $settings = [];
-
- // Only continue if the theme is Bootstrap based.
- if ($this->isBootstrap()) {
- $setting_manager = new SettingManager($this);
- foreach (array_keys($setting_manager->getDefinitions()) as $setting) {
- $settings[$setting] = $setting_manager->createInstance($setting);
+ public function getSettingPlugin($name = NULL, $rebuild = FALSE) {
+ if (!isset($this->settings) || $rebuild) {
+ $settings = [];
+ if ($this->isBootstrap()) {
+ $setting_manager = new SettingManager($this);
+ if ($rebuild) {
+ $setting_manager->clearCachedDefinitions();
+ }
+ $plugin_ids = array_keys($setting_manager->getDefinitions());
+ foreach ($plugin_ids as $plugin_id) {
+ $settings[$plugin_id] = $setting_manager->createInstance($plugin_id);
+ }
}
+ $this->settings = $settings;
}
// Return a specific setting plugin.
if (isset($name)) {
- return isset($settings[$name]) ? $settings[$name] : NULL;
+ return isset($this->settings[$name]) ? $this->settings[$name] : NULL;
}
// Return all setting plugins.
- return $settings;
+ return $this->settings;
}
/**
@@ -646,7 +660,7 @@ class Theme {
if (!isset($includes[$include])) {
$includes[$include] = !!@include_once $include;
if (!$includes[$include]) {
- drupal_set_message(t('Could not include file: @include', ['@include' => $include]), 'error');
+ Bootstrap::message(t('Could not include file: @include', ['@include' => $include]), 'error');
}
}
return $includes[$include];
@@ -769,10 +783,17 @@ class Theme {
* A provider instance or FALSE if no provider is set.
*
* @deprecated in 8.x-3.18, will be removed in a future release.
+ *
+ * @see \Drupal\bootstrap\Theme::getCdnProvider()
+ * @see \Drupal\bootstrap\Plugin\ProviderManager::load()
*/
public function getProvider($provider = NULL) {
- $instance = ProviderManager::load($this, $provider);
- return $instance instanceof Broken || !$this->isBootstrap() ? FALSE : $instance;
+ $provider = $provider ?: $this->getSetting('cdn_provider');
+ $providers = $this->getProviders();
+ if (!isset($providers[$provider])) {
+ return FALSE;
+ }
+ return $providers[$provider];
}
/**
@@ -782,22 +803,11 @@ class Theme {
* All provider instances.
*
* @deprecated in 8.x-3.18, will be removed in a future release.
+ *
+ * @see \Drupal\bootstrap\Theme::getCdnProviders()
*/
public function getProviders() {
- $providers = [];
-
- // Only continue if the theme is Bootstrap based.
- if ($this->isBootstrap()) {
- $provider_manager = new ProviderManager($this);
- foreach (array_keys($provider_manager->getDefinitions()) as $provider) {
- if ($provider === 'none' || $provider === '_broken') {
- continue;
- }
- $providers[$provider] = $provider_manager->get($provider, ['theme' => $this]);
- }
- }
-
- return $providers;
+ return !$this->isBootstrap() ? [] : $this->getCdnProviders();
}
/**
@@ -806,7 +816,9 @@ class Theme {
* @return \Drupal\bootstrap\Plugin\Setting\SettingInterface[]
* An associative array of setting objects, keyed by their name.
*
- * @deprecated Will be removed in a future release. Use \Drupal\bootstrap\Theme::getSettingPlugin instead.
+ * @deprecated in 8.x-3.1, will be removed in a future release.
+ *
+ * @see \Drupal\bootstrap\Theme::getSettingPlugin()
*/
public function getSettingPlugins() {
Bootstrap::deprecated();
diff --git a/src/ThemeSettings.php b/src/ThemeSettings.php
index e2777718703ae4414c93d29d2a6efeac4f05cca9..eabc24204bae2c49b5565a5549db5acc5a3f57df 100644
--- a/src/ThemeSettings.php
+++ b/src/ThemeSettings.php
@@ -2,6 +2,8 @@
namespace Drupal\bootstrap;
+use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
+use Drupal\bootstrap\Plugin\Setting\SettingInterface;
use Drupal\Core\Theme\ThemeSettings as CoreThemeSettings;
use Drupal\Component\Utility\DiffArray;
use Drupal\Component\Utility\NestedArray;
@@ -25,6 +27,13 @@ class ThemeSettings extends Config {
*/
protected $defaults;
+ /**
+ * A list of deprecated settings, keyed by the newer setting name.
+ *
+ * @var array
+ */
+ protected $deprecated;
+
/**
* The current theme object.
*
@@ -32,6 +41,13 @@ class ThemeSettings extends Config {
*/
protected $theme;
+ /**
+ * A list of available Setting plugins.
+ *
+ * @var \Drupal\bootstrap\Plugin\Setting\SettingInterface[]
+ */
+ protected $settings;
+
/**
* {@inheritdoc}
*/
@@ -39,6 +55,24 @@ class ThemeSettings extends Config {
parent::__construct($theme->getName() . '.settings', \Drupal::service('config.storage'), \Drupal::service('event_dispatcher'), \Drupal::service('config.typed'));
$this->theme = $theme;
+ // Retrieve the available settings.
+ $this->settings = $theme->getSettingPlugin();
+
+ // Filter out the deprecated settings.
+ /** @var \Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface[] $deprecated */
+ $deprecated = array_filter($this->settings, function ($setting) {
+ return $setting instanceof DeprecatedSettingInterface;
+ });
+
+ $this->deprecated = [];
+ foreach ($deprecated as $deprecatedName => $deprecatedSetting) {
+ $key = $deprecatedSetting->getDeprecatedReplacementSetting()->getPluginId();
+ if (!isset($this->deprecated[$key])) {
+ $this->deprecated[$key] = [];
+ }
+ $this->deprecated[$key][$deprecatedName] = $deprecatedSetting;
+ }
+
// Retrieve cache.
$cache = $theme->getCache('settings');
@@ -53,8 +87,12 @@ class ThemeSettings extends Config {
$this->defaults = \Drupal::config('system.theme.global')->get();
// Retrieve the theme setting plugin discovery defaults (code).
- foreach ($theme->getSettingPlugin() as $name => $setting) {
- $this->defaults[$name] = $setting->getDefaultValue();
+ foreach ($this->settings as $name => $deprecatedSetting) {
+ // Deprecated settings shouldn't provide default values, only set values.
+ if ($deprecatedSetting instanceof DeprecatedSettingInterface) {
+ continue;
+ }
+ $this->defaults[$name] = $deprecatedSetting->getDefaultValue();
}
// Retrieve the theme ancestry.
@@ -92,6 +130,9 @@ class ThemeSettings extends Config {
}
else {
$value = parent::get($key);
+ if (!isset($value)) {
+ $value = $this->getDeprecatedValue($key);
+ }
if (!isset($value)) {
$value = $this->getOriginal($key);
}
@@ -99,6 +140,67 @@ class ThemeSettings extends Config {
return $value;
}
+ /**
+ * Retrieves a deprecated value from other setting(s).
+ *
+ * @param string $key
+ * The name of the setting to retrieve a deprecated value for.
+ *
+ * @return mixed
+ * A value from deprecated setting(s) or NULL if there is no deprecated
+ * value currently in use.
+ */
+ protected function getDeprecatedValue($key) {
+ $value = NULL;
+
+ // Immediately return if no deprecated settings to extract a values from.
+ if (!isset($this->settings[$key]) || empty($this->deprecated[$key])) {
+ return $value;
+ }
+
+ $setting = $this->settings[$key];
+ $deprecatedSettings = $this->deprecated[$key];
+
+ $values = [];
+ foreach (array_keys($deprecatedSettings) as $deprecatedName) {
+ $deprecatedValue = $this->get($deprecatedName);
+ if (isset($deprecatedValue)) {
+ $values[$deprecatedName] = $deprecatedValue;
+ }
+ }
+
+ // Immediately return if there are no deprecated values to process.
+ if (!$values) {
+ return $value;
+ }
+
+ // Let the new setting handle how it should process the deprecated values.
+ $value = $setting->processDeprecatedValues($values, $deprecatedSettings);
+
+ // Deprecated value(s) found, show a message and then migrate them.
+ if (isset($value)) {
+ if (count($values) > 1) {
+ Bootstrap::deprecated(NULL, NULL, t('The following theme settings have been deprecated and should no longer be used: "%deprecated". The values have been converted automatically for use with the supported theme setting "%name" instead. The configuration for the site should be exported to accommodate this change.', [
+ '%deprecated' => implode('", "', array_keys($deprecatedSettings)),
+ '%name' => $key,
+ ]));
+ }
+ else {
+ Bootstrap::deprecated(NULL, NULL, t('The following theme setting has been deprecated and should no longer be used: "%deprecated". The value has been converted automatically for use with the supported theme setting "%name" instead. The configuration for the site should be exported to accommodate this change.', [
+ '%deprecated' => array_keys($deprecatedSettings)[0],
+ '%name' => $key,
+ ]));
+ }
+ $this->set($key, $value);
+ foreach (array_keys($values) as $setting) {
+ $this->clear($setting);
+ }
+ $this->save();
+ }
+
+ return $value;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Utility/Crypt.php b/src/Utility/Crypt.php
index 6b57fc59441294464e93bcde3f352f1124d5de47..54d0e4b9e3d41ebc239c5cc1bd42e4698a58f99b 100644
--- a/src/Utility/Crypt.php
+++ b/src/Utility/Crypt.php
@@ -2,6 +2,7 @@
namespace Drupal\bootstrap\Utility;
+use Drupal\bootstrap\Bootstrap;
use Drupal\Component\Utility\Crypt as CoreCrypt;
/**
@@ -11,6 +12,164 @@ use Drupal\Component\Utility\Crypt as CoreCrypt;
*/
class Crypt extends CoreCrypt {
+ /**
+ * The regular expression used to match an SRI integrity value.
+ *
+ * @var string
+ */
+ const SRI_INTEGRITY_REGEXP = '/^(sha(?:256|384|512))-(.*)$/';
+
+ /**
+ * The length of each algorithm's digest, keyed by algorithm name.
+ *
+ * @var int[]
+ *
+ * @todo Move to a constant once PHP 5.5 is no longer supported.
+ */
+ protected static $algorithmDigestLengths = [
+ 'md5' => 32,
+ 'sha1' => 40,
+ 'sha224' => 56,
+ 'sha256' => 64,
+ 'sha384' => 96,
+ 'sha512' => 128,
+ ];
+
+ /**
+ * The valid SRI Integrity algorithms supported by current browsers.
+ *
+ * @var string[]
+ *
+ * @todo Move to a constant once PHP 5.5 is no longer supported.
+ */
+ protected static $validSriIntegrityAlgorithms = [
+ 'sha256',
+ 'sha384',
+ 'sha512',
+ ];
+
+ /**
+ * Ensures the base64 encoded hash matches the algorithm's digest length.
+ *
+ * @param string $algorithm
+ * The algorithm output length to check.
+ * @param string $hash
+ * The base64 encoded hash to check.
+ * @param bool $sriIntegrity
+ * Flag indicating whether this is a hash intended for use as an SRI
+ * integrity value.
+ *
+ * @return bool
+ * TRUE if the digest length from decoding the base64 hash matches what
+ * the algorithm length is supposed to be; FALSE otherwise.
+ */
+ public static function checkBase64HashAlgorithm($algorithm, $hash, $sriIntegrity = FALSE) {
+ // Immediately return if values aren't provided or an unsupported algorithm.
+ if (!$algorithm || !$hash || !isset(static::$algorithmDigestLengths[$algorithm])) {
+ return FALSE;
+ }
+
+ // Check if this is an SRI algorithm supported by a browser.
+ if ($sriIntegrity && !in_array($algorithm, static::$validSriIntegrityAlgorithms)) {
+ return FALSE;
+ }
+
+ // Ensure the provided hash matches the length of the algorithm provided.
+ return !!preg_match('/^([a-f0-9]{' . static::$algorithmDigestLengths[$algorithm] . '})$/', static::decodeHashBase64($hash));
+ }
+
+ /**
+ * Decodes a base64 encoded hash back into its raw digest value.
+ *
+ * Note: this will also decode binary digests into a proper hexadecimal value.
+ *
+ * @param string $hash
+ * The base64 encoded hash to decode.
+ *
+ * @return string|false
+ * The decoded digest value or FALSE if unable to decode it.
+ */
+ public static function decodeHashBase64($hash) {
+ $digest = base64_decode($hash);
+
+ // Check if digest is binary and convert to hex, if needed.
+ if ($digest && preg_match('/[^a-f0-9]*/', $digest) && (!extension_loaded('ctype') || !ctype_print($digest))) {
+ $digest = bin2hex($digest);
+ }
+
+ return $digest;
+ }
+
+ /**
+ * Determines the algorithm used for a base64 encoded hash.
+ *
+ * @param string $hash
+ * The base64 encoded hash to check.
+ *
+ * @return string|false
+ * The algorithm used or FALSE if unable to determine it.
+ */
+ public static function determineHashBase64Algorithm($hash) {
+ $digest = static::decodeHashBase64($hash);
+ $length = strlen($digest);
+ return array_search($length, static::$algorithmDigestLengths, TRUE);
+ }
+
+ /**
+ * Generates a unique identifier by serializing and hashing an array of data.
+ *
+ * @param array $data
+ * The data to serialize and hash.
+ * @param string|string[] $prefix
+ * The value(s) to use to prefix the identifier, separated by colons (:).
+ * @param string $delimiter
+ * The delimiter to use when joining the prefix and hash.
+ *
+ * @return string
+ * The uniquely generated identifier.
+ */
+ public static function generateBase64HashIdentifier(array $data, $prefix = NULL, $delimiter = ':') {
+ $prefix = Unicode::castToString($prefix, $delimiter);
+ $hash = self::hashBase64(serialize(array_merge([$prefix], $data)));
+ return $prefix ? $prefix . $delimiter . $hash : $hash;
+ }
+
+ /**
+ * Parses a SRI integrity value to separate the algorithm from the hash.
+ *
+ * @param string $integrity
+ * An integrity value beginning with a prefix indicating a particular hash
+ * algorithm (currently the allowed prefixes are sha256, sha384, and
+ * sha512), followed by a dash, and ending with the actual base64 encoded
+ * hash.
+ *
+ * @return array
+ * An indexed array containing intended for use with list():
+ * - algorithm - (string) The provided or matched algorithm.
+ * - hash - (string) The base64 encoded hash.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+ */
+ public static function parseSriIntegrity($integrity) {
+ // Extract the algorithm and base64 encoded hash.
+ preg_match(Crypt::SRI_INTEGRITY_REGEXP, $integrity, $matches);
+ $algorithm = !empty($matches[1]) ? $matches[1] : FALSE;
+ $hash = !empty($matches[2]) ? $matches[2] : $integrity;
+
+ // Attempt to determine the algorithm used if one wasn't prepended.
+ if (!$algorithm) {
+ $algorithm = static::determineHashBase64Algorithm($hash);
+ }
+
+ return [$algorithm, $hash];
+ }
+
+ /****************************************************************************
+ *
+ * Deprecated methods
+ *
+ ***************************************************************************/
+
/**
* Generates a unique hash name.
*
@@ -19,18 +178,15 @@ class Crypt extends CoreCrypt {
*
* @return string
* The generated hash identifier.
+ *
+ * @deprecated since 8.x-3.18. Will be removed in a future release.
+ *
+ * @see \Drupal\bootstrap\Utility\Crypt::generateBase64HashIdentifier()
*/
public static function generateHash() {
+ Bootstrap::deprecated();
$args = func_get_args();
- $hash = '';
- if (is_string($args[0])) {
- $hash = $args[0] . ':';
- }
- elseif (is_array($args[0])) {
- $hash = implode(':', $args[0]) . ':';
- }
- $hash .= self::hashBase64(serialize($args));
- return $hash;
+ return static::generateBase64HashIdentifier($args, $args[0]);
}
}
diff --git a/src/Utility/Element.php b/src/Utility/Element.php
index 9fad8b887b30227e7cd93cb6059176b3a4beee86..06cb647ee27d6b7f7b4efec40595626aee9ff458 100644
--- a/src/Utility/Element.php
+++ b/src/Utility/Element.php
@@ -71,7 +71,7 @@ class Element extends DrupalAttributes {
if (CoreElement::property($key)) {
throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Please use \Drupal\bootstrap\Utility\Element::getProperty instead.');
}
- $instance = new self($this->offsetGet($key, []));
+ $instance = new self($this->offsetGet($key, []), $this->formState);
return $instance;
}
@@ -92,7 +92,7 @@ class Element extends DrupalAttributes {
if (CoreElement::property($key)) {
throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Use \Drupal\bootstrap\Utility\Element::setProperty instead.');
}
- $this->offsetSet($key, ($value instanceof Element ? $value->getArray() : $value));
+ $this->offsetSet($key, $value instanceof Element ? $value->getArray() : $value);
}
/**
@@ -154,7 +154,7 @@ class Element extends DrupalAttributes {
* @param mixed $value
* The value of the property to set.
*
- * @return $this
+ * @return static
*/
public function appendProperty($name, $value) {
$property = &$this->getProperty($name);
@@ -218,7 +218,7 @@ class Element extends DrupalAttributes {
* @param bool $override
* Flag determining whether or not to override any existing set class.
*
- * @return $this
+ * @return static
*/
public function colorize($override = TRUE) {
$button = $this->isButton();
@@ -516,7 +516,7 @@ class Element extends DrupalAttributes {
* are identical except for the leading '#', then an attribute name value is
* sufficient and no property name needs to be specified.
*
- * @return $this
+ * @return static
*/
public function map(array $map) {
CoreElement::setAttributes($this->array, $map);
@@ -531,7 +531,7 @@ class Element extends DrupalAttributes {
* @param mixed $value
* The value of the property to set.
*
- * @return $this
+ * @return static
*/
public function prependProperty($name, $value) {
$property = &$this->getProperty($name);
@@ -611,7 +611,7 @@ class Element extends DrupalAttributes {
* Flag indicating if the passed $class should be forcibly set. Setting
* this to FALSE allows any existing set class to persist.
*
- * @return $this
+ * @return static
*/
public function setButtonSize($class = NULL, $override = TRUE) {
// Immediately return if element is not a button.
@@ -659,18 +659,44 @@ class Element extends DrupalAttributes {
*
* @param string $message
* (optional) The error message to present to the user.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Optional. The current state of the form. If not provided, it will attempt
+ * to use the form state passed when constructing the element.
*
- * @return $this
+ * @return static
*
* @throws \BadMethodCallException
* When the element instance was not constructed with a valid form state
* object.
*/
- public function setError($message = '') {
- if (!$this->formState) {
- throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
+ public function setError($message = '', FormStateInterface $form_state = NULL) {
+ if (!isset($form_state)) {
+ if (!$this->formState) {
+ throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
+ }
+ $form_state = $this->formState;
+ }
+
+ // Form errors cannot be set after validation has already completed.
+ if (!$form_state->isValidationComplete() && isset($this->array['#parents'])) {
+ $form_state->setError($this->array, $message);
+ }
+ else {
+ Bootstrap::message($message, 'error');
}
- $this->formState->setError($this->array, $message);
+ return $this;
+ }
+
+ /**
+ * Sets the current form state for the element.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Optional. The current state of the form.
+ *
+ * @return static
+ */
+ public function setFormState(FormStateInterface $form_state = NULL) {
+ $this->formState = $form_state;
return $this;
}
@@ -680,7 +706,7 @@ class Element extends DrupalAttributes {
* @param array $icon
* An icon render array.
*
- * @return $this
+ * @return static
*
* @see \Drupal\bootstrap\Bootstrap::glyphicon()
*/
@@ -705,7 +731,7 @@ class Element extends DrupalAttributes {
* @param bool $recurse
* Flag indicating wither to set the same property on child elements.
*
- * @return $this
+ * @return static
*/
public function setProperty($name, $value, $recurse = FALSE) {
$this->array["#$name"] = $value instanceof Element ? $value->getArray() : $value;
@@ -729,7 +755,7 @@ class Element extends DrupalAttributes {
* @param int $length
* The length of characters to determine if description is "simple".
*
- * @return $this
+ * @return static
*/
public function smartDescription(&$target_element = NULL, $input_only = TRUE, $length = NULL) {
static $theme;
@@ -856,7 +882,7 @@ class Element extends DrupalAttributes {
* @param string $name
* The name of the property to unset.
*
- * @return $this
+ * @return static
*/
public function unsetProperty($name) {
unset($this->array["#$name"]);
diff --git a/src/Utility/SortArray.php b/src/Utility/SortArray.php
new file mode 100644
index 0000000000000000000000000000000000000000..c31030cfd74afe78cad329da3e46fbb57076e912
--- /dev/null
+++ b/src/Utility/SortArray.php
@@ -0,0 +1,23 @@
+__toString() ?: '');
+ }
+ if (is_array($value)) {
+ foreach ($value as $key => $item) {
+ $value[$key] = static::castToString($item, $delimiter);
+ }
+ return implode($delimiter, array_filter($value));
+ }
+ // Handle scalar values.
+ if (isset($value) && is_scalar($value) && !is_bool($value)) {
+ return (string) $value;
+ }
+ return '';
+ }
+
/**
* Extracts the hook name from a function name.
*