diff --git a/bootstrap.drush.inc b/bootstrap.drush.inc index 1d94fa18896d1cc6a8adf5656fe04bf2bd48f660..d87978f1fe431da4be628e2c438b2159e44351c6 100644 --- a/bootstrap.drush.inc +++ b/bootstrap.drush.inc @@ -69,48 +69,56 @@ function _drush_bootstrap_generate_docs_settings(Theme $bootstrap) { $output[] = '```'; // Determine the groups. - $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($setting->getGroups(), 0, 2, FALSE); + $_groups = array_filter(array_slice($setting->getGroups(), 0, 2, FALSE)); if (!$_groups) { continue; } - $groups[implode(' > ', $_groups)][] = $setting->getPluginDefinition(); + $groups[array_keys($_groups)[0]][implode(' > ', $_groups)][] = $setting->getPluginDefinition(); } // Generate a table of each group's settings. - foreach ($groups as $group => $settings) { - $output[] = ''; - $output[] = '---'; - $output[] = ''; - $output[] = "### $group"; - $output[] = ''; - $output[] = ''; - $output[] = ' '; - $output[] = ' '; - $output[] = ' '; - $output[] = ' '; - $output[] = ' '; - $output[] = ' '; - $output[] = ' '; - foreach ($settings as $definition) { - $output[] = ' '; - $output[] = ' '; - $output[] = ' '; - $output[] = ' '; + foreach ($groups as $subgroups) { + foreach ($subgroups as $group => $settings) { + $output[] = ''; + $output[] = '---'; + $output[] = ''; + $output[] = "### $group"; + $output[] = ''; + $output[] = '
Setting nameDescription and default value
'; - $output[] = $definition['id']; - $output[] = ' '; - $output[] = '
'; - $output[] = str_replace('"e;', '"', wordwrap($definition['description'])); - $output[] = '
'; - $output[] = '
';
-      $output[] = wordwrap(Yaml::encode([$definition['id'] => $definition['defaultValue']]));
-      $output[] = '
'; - $output[] = '
'; + $output[] = ' '; + $output[] = ' '; + $output[] = ' '; + $output[] = ' '; + $output[] = ' '; + $output[] = ' '; + $output[] = ' '; + foreach ($settings as $definition) { + $output[] = ' '; + $output[] = ' '; + $output[] = ' '; + $output[] = ' '; + } + $output[] = ' '; + $output[] = '
Setting nameDescription and default value
'; + $output[] = $definition['id']; + $output[] = ' '; + $output[] = '
'; + $output[] = str_replace('"e;', '"', wordwrap($definition['description'])); + $output[] = '
'; + $output[] = '
';
+        $output[] = wordwrap(Yaml::encode([$definition['id'] => $definition['defaultValue']]));
+        $output[] = '
'; + $output[] = '
'; } - $output[] = ' '; - $output[] = ''; } // Ensure we have link references at the bottom. diff --git a/deprecated.php b/deprecated.php index 11ebc04880e91c338abb8ea2ec8547c8361eccab..2d7ca5b3e960f41cf03062d02c820b91ad74ca24 100644 --- a/deprecated.php +++ b/deprecated.php @@ -671,8 +671,8 @@ function _bootstrap_remove_class($class, array &$element, $property = 'attribute * @endcode * * @see \Drupal\bootstrap\Plugin\ProviderManager - * @see \Drupal\bootstrap\Theme::getProviders() - * @see \Drupal\bootstrap\Theme::getProvider() + * @see \Drupal\bootstrap\Theme::getCdnProviders() + * @see \Drupal\bootstrap\Theme::getCdnProvider() */ function bootstrap_cdn_provider($provider = NULL, $reset = FALSE) { Bootstrap::deprecated(); @@ -746,7 +746,20 @@ function bootstrap_element_smart_description(array &$element, array &$target = N * * // After. * use Drupal\bootstrap\Plugin\ProviderManager; - * $assets = ProviderManager::load($theme, $provider)->getAssets($type); + * $original_type = $type; + * $config = \Drupal::config('system.performance'); + * $cdnAssets = ProviderManager::load($theme, $provider)->getCdnAssets(); + * $data = []; + * $types = !isset($type) ? ['css', 'js'] : (array) $type; + * foreach ($types as $type) { + * if ($config->get("$type.preprocess") && !empty($cdnAssets['min'][$type])) { + * $data[$type] = $cdnAssets['min'][$type]; + * } + * elseif (!empty($data[$type])) { + * $data[$type] = $cdnAssets[$type]; + * } + * } + * $assets = is_string($original_type) ? $data[$original_type] : $data; * @endcode * * @see \Drupal\bootstrap\Plugin\Provider\Custom::getAssets() @@ -757,7 +770,20 @@ function bootstrap_element_smart_description(array &$element, array &$target = N */ function bootstrap_get_cdn_assets($type = NULL, $provider = NULL, $theme = NULL) { Bootstrap::deprecated(); - return ProviderManager::load($theme, $provider)->getAssets($type); + $original_type = $type; + $assets = []; + $config = \Drupal::config('system.performance'); + $cdnAssets = ProviderManager::load($theme, $provider)->getCdnAssets(); + $types = !isset($type) ? ['css', 'js'] : (array) $type; + foreach ($types as $type) { + if ($config->get("$type.preprocess") && !empty($cdnAssets['min'][$type])) { + $assets[$type] = $cdnAssets['min'][$type]; + } + elseif (!empty($data[$type])) { + $assets[$type] = $cdnAssets[$type]; + } + } + return is_string($original_type) ? $assets[$original_type] : $assets; } /** diff --git a/docs/Sub-Theming.md b/docs/Sub-Theming.md index 699fd1e8b9578f2315b98944d20f496ab7e4be91..bb98eacd1ca60ff358e72231e2cb90ff94ab1dc9 100644 --- a/docs/Sub-Theming.md +++ b/docs/Sub-Theming.md @@ -18,11 +18,24 @@ can override CSS, templates, and theme processing. #### Choose a Starterkit {#starterkit} - @link sub_theming_cdn CDN Starterkit @endlink - uses the "out-of-the-box" - CSS and JavaScript files served by the [jsDelivr CDN]. + CSS and JavaScript files served by a CDN Provider (like [jsDelivr]). - @link sub_theming_less Less Starterkit @endlink - uses the [Bootstrap Framework] [Less] source files and a local [Less] preprocessor. - @link sub_theming_sass Sass Starterkit @endlink - uses the [Bootstrap Framework] [Sass] source files and a local [Sass] preprocessor. + +{.alert.alert-info} **Note** Using the "CDN Starterkit" is the preferred method +for loading Bootstrap CSS and JS on simpler sites that do not use a site-wide +CDN. Using a CDN Provider for loading Bootstrap, however, does mean that it +depends on a third-party service. There is no obligation or commitment made by +this project or these third-party CDN services that guarantees up-time or +quality of service. If you need to customize Bootstrap, you must choose one of +the Less or Sass Starterkits, compile the source code locally, and disable the +"CDN Provider" theme setting. Alternatively, you may also choose to enable a +site-wide CDN implementation for performance reasons. + +{.alert.alert-warning} **Warning** All locally compiled versions of Bootstrap +will be superseded by any enabled "CDN Provider"; **do not use both**. Once you've selected one of the above starterkits, here's how to install it: @@ -62,6 +75,6 @@ to customize. [Drupal Bootstrap]: https://www.drupal.org/project/bootstrap [Bootstrap Framework]: https://getbootstrap.com/docs/3.4/ -[jsDelivr CDN]: http://www.jsdelivr.com +[jsDelivr]: http://www.jsdelivr.com [Less]: http://lesscss.org [Sass]: http://sass-lang.com diff --git a/docs/Theme-Settings.md b/docs/Theme-Settings.md index d772dca40a992719565191b6b028439b4598eced..648955dd0d0cc305be3672690e70ed8832061630 100644 --- a/docs/Theme-Settings.md +++ b/docs/Theme-Settings.md @@ -13,55 +13,7 @@ SETTING_NAME: SETTING_VALUE --- -### Advanced - - - - - - - - - - - - - - - - - - -
Setting nameDescription and default value
-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. -WARNING: Suppressing these messages does -not "fix" the problem and you will inevitably encounter issues -when they are removed in future updates. Only use this setting in extreme -and necessary circumstances. -
-

-suppress_deprecated_warnings: 0
-
-
-
- ---- - -### Advanced > CDN (Content Delivery Network) +### General > Buttons @@ -73,107 +25,42 @@ suppress_deprecated_warnings: 0 - - - - - - - - - - - - - - - - @@ -183,7 +70,7 @@ cdn_jsdelivr_theme: bootstrap --- -### Components > Breadcrumbs +### General > Container
-cdn_provider - -
-Choose between jsdelivr or a custom cdn source. -
-

-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 +button_colorize
-Additionally, you can provide the minimized version of the file. It will be -used instead if site aggregation is enabled. +Adds classes to buttons based on their text value.

-cdn_custom_js_min:
-'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js'
+button_colorize: 1
 
 
-cdn_jsdelivr_version +button_iconize
-Choose the Bootstrap version from jsdelivr +Adds icons to buttons based on the text value

-cdn_jsdelivr_version: 3.4.1
+button_iconize: 1
 
 
-cdn_jsdelivr_theme +button_size
-Choose the example Bootstrap Theme provided by Bootstrap or one of the -Bootswatch themes. +Defines the Bootstrap Buttons specific size

-cdn_jsdelivr_theme: bootstrap
+button_size: ''
 
 
@@ -195,44 +82,15 @@ cdn_jsdelivr_theme: bootstrap - - - - - - - - @@ -242,7 +100,7 @@ breadcrumb_title: 1 --- -### Components > Navbar +### General > Forms
-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 +fluid_container
-If your site has a module dedicated to handling breadcrumbs already, ensure -this setting is disabled. +Uses the .container-fluid class instead of +.container.

-breadcrumb_title: 1
+fluid_container: 0
 
 
@@ -254,124 +112,80 @@ breadcrumb_title: 1 - - - - - -
-navbar_inverse - -
-Select if you want the inverse navbar style. -
-

-navbar_inverse: 0
-
-
-
-navbar_position +forms_has_error_value_toggle
-Determines where the navbar is positioned on the page. +If an element has a .has-error class attached to it, enabling +this will automatically remove that class when a value is entered.

-navbar_position: ''
+forms_has_error_value_toggle: 1
 
 
- ---- - -### Components > Region Wells - - - - - - - - - - -
Setting nameDescription and default value
-region_wells +forms_required_has_error
-Enable the .well, .well-sm or -.well-lg classes for specified regions. +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.

-region_wells:
-  navigation: ''
-  navigation_collapsible: ''
-  header: ''
-  highlighted: ''
-  help: ''
-  content: ''
-  sidebar_first: ''
-  sidebar_second: well
-  footer: ''
+forms_required_has_error: 0
 
 
- ---- - -### General > Buttons - - - - - - - - - @@ -381,7 +195,7 @@ button_size: '' --- -### General > Container +### General > Images
Setting nameDescription and default value
-button_colorize +forms_smart_descriptions
-Adds classes to buttons based on their text value. +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.

-button_colorize: 1
+forms_smart_descriptions: 1
 
 
-button_iconize +forms_smart_descriptions_allowed_tags
-Adds icons to buttons based on the text value +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.

-button_iconize: 1
+forms_smart_descriptions_allowed_tags: 'b, code, em, i, kbd, span, strong'
 
 
-button_size +forms_smart_descriptions_limit
-Defines the Bootstrap Buttons specific size +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.

-button_size: ''
+forms_smart_descriptions_limit: '250'
 
 
@@ -393,15 +207,32 @@ button_size: '' + + + + @@ -411,7 +242,7 @@ fluid_container: 0 --- -### General > Forms +### General > Tables
-fluid_container +image_responsive
-Uses the .container-fluid class instead of -.container. +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.

-fluid_container: 0
+image_responsive: 1
+
+
+
+image_shape + +
+Add classes to an <img> element to easily style images +in any project. +
+

+image_shape: ''
 
 
@@ -423,80 +254,75 @@ fluid_container: 0 @@ -506,7 +332,7 @@ forms_smart_descriptions_limit: '250' --- -### General > Images +### Components > Breadcrumbs
-forms_has_error_value_toggle +table_bordered
-If an element has a .has-error class attached to it, enabling -this will automatically remove that class when a value is entered. +Add borders on all sides of the table and cells.

-forms_has_error_value_toggle: 1
+table_bordered: 0
 
 
-forms_required_has_error +table_condensed
-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. +Make tables more compact by cutting cell padding in half.

-forms_required_has_error: 0
+table_condensed: 0
 
 
-forms_smart_descriptions +table_hover
-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. +Enable a hover state on table rows.

-forms_smart_descriptions: 1
+table_hover: 1
 
 
-forms_smart_descriptions_allowed_tags +table_striped
-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. +Add zebra-striping to any table row within the <tbody>.

-forms_smart_descriptions_allowed_tags: 'b, code, em, i, kbd, span, strong'
+table_striped: 1
 
 
-forms_smart_descriptions_limit +table_responsive
-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. +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.

-forms_smart_descriptions_limit: '250'
+table_responsive: -1
 
 
@@ -518,32 +344,44 @@ forms_smart_descriptions_limit: '250' + + + + @@ -553,7 +391,7 @@ image_shape: '' --- -### General > Tables +### Components > Navbar
-image_responsive +breadcrumb
-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. +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.

-image_responsive: 1
+breadcrumb_home: 0
 
 
-image_shape +breadcrumb_title
-Add classes to an <img> element to easily style images -in any project. +If your site has a module dedicated to handling breadcrumbs already, ensure +this setting is disabled.

-image_shape: ''
+breadcrumb_title: 1
 
 
@@ -565,75 +403,67 @@ image_shape: '' - - - +
-table_bordered +navbar_inverse
-Add borders on all sides of the table and cells. +Select if you want the inverse navbar style.

-table_bordered: 0
+navbar_inverse: 0
 
 
-table_condensed +navbar_position
-Make tables more compact by cutting cell padding in half. +Determines where the navbar is positioned on the page.

-table_condensed: 0
+navbar_position: ''
 
 
-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
+---
 
-
- - +### Components > Region Wells + + + + + + + + + @@ -1130,5 +960,175 @@ tooltip_trigger: hover
Setting nameDescription and default value
-table_responsive +region_wells
-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. +Enable the .well, .well-sm or +.well-lg classes for specified regions.

-table_responsive: -1
+region_wells:
+  navigation: ''
+  navigation_collapsible: ''
+  header: ''
+  highlighted: ''
+  help: ''
+  content: ''
+  sidebar_first: ''
+  sidebar_second: well
+  footer: ''
 
 
+--- + +### CDN (Content Delivery Network) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Setting nameDescription and default value
+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 Bootstrap Theme provided by Bootstrap or one of the +Bootswatch themes. +
+

+cdn_jsdelivr_theme: bootstrap
+
+
+
+ +--- + +### Advanced + + + + + + + + + + + + + + + + + + +
Setting nameDescription and default value
+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. +WARNING: Suppressing these messages does +not "fix" the problem and you will inevitably encounter issues +when they are removed in future updates. Only use this setting in extreme +and necessary circumstances. +
+

+suppress_deprecated_warnings: 0
+
+
+
+ [Drupal Bootstrap]: https://www.drupal.org/project/bootstrap [Bootstrap Framework]: https://getbootstrap.com/docs/3.4/ diff --git a/js/theme-settings.js b/js/theme-settings.js index 12c72ccb685909a90131dafaa77b8a833d52f07d..3b6d017bbdc098296f66da00b3a40f84f9dc582d 100644 --- a/js/theme-settings.js +++ b/js/theme-settings.js @@ -11,7 +11,7 @@ var $context = $(context); // General. - $context.find('#edit-general').drupalSetSummary(function () { + $context.find('[data-drupal-selector="edit-general"]').drupalSetSummary(function () { var summary = []; // Buttons. var size = $context.find('select[name="button_size"] :selected'); @@ -42,7 +42,7 @@ }); // Components. - $context.find('#edit-components').drupalSetSummary(function () { + $context.find('[data-drupal-selector="edit-components"]').drupalSetSummary(function () { var summary = []; // Breadcrumbs. var breadcrumb = parseInt($context.find('select[name="breadcrumb"]').val(), 10); @@ -112,7 +112,7 @@ }); }); - $context.find('#edit-javascript').drupalSetSummary(function () { + $context.find('[data-drupal-selector="edit-javascript"]').drupalSetSummary(function () { var summary = []; if ($context.find('input[name="modal_enabled"]').is(':checked')) { if ($jQueryUiBridge.is(':checked')) { @@ -131,13 +131,13 @@ return summary.join(', '); }); - // Advanced. - $context.find('#edit-advanced').drupalSetSummary(function () { + // CDN. + $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 && cdnProvider.length) { - summary.push(Drupal.t('CDN provider: %provider', { '%provider': $cdnProvider.text() })); + if ($cdnProvider.length) { + summary.push(Drupal.t('Provider: %provider', { '%provider': $cdnProvider.text() })); // jsDelivr CDN. if (cdnProvider === 'jsdelivr') { @@ -153,6 +153,21 @@ } return summary.join(', '); }); + + + // Advanced. + $context.find('[data-drupal-selector="edit-advanced"]').drupalSetSummary(function () { + var summary = []; + var deprecations = []; + if ($context.find('input[name="include_deprecated"]').is(':checked')) { + deprecations.push(Drupal.t('Included')); + } + deprecations.push($context.find('input[name="suppress_deprecated_warnings"]').is(':checked') ? Drupal.t('Warnings Suppressed') : Drupal.t('Warnings Shown')); + summary.push(Drupal.t('Deprecations: @value', { + '@value': deprecations.join(', '), + })); + return summary.join(', '); + }); } }; diff --git a/src/Annotation/BootstrapProvider.php b/src/Annotation/BootstrapProvider.php index 06e132debebcd77e638ebd5b52c74ffdae4c8c40..a8fae6a58d0cea81dc715dae30d6a1f2f3e9a5a5 100644 --- a/src/Annotation/BootstrapProvider.php +++ b/src/Annotation/BootstrapProvider.php @@ -11,8 +11,8 @@ use Drupal\Component\Annotation\Plugin; * * @see \Drupal\bootstrap\Plugin\ProviderInterface * @see \Drupal\bootstrap\Plugin\ProviderManager - * @see \Drupal\bootstrap\Theme::getProviders() - * @see \Drupal\bootstrap\Theme::getProvider() + * @see \Drupal\bootstrap\Theme::getCdnProviders() + * @see \Drupal\bootstrap\Theme::getCdnProvider() * @see plugin_api * * @Annotation diff --git a/src/Bootstrap.php b/src/Bootstrap.php index cb370303e93d4719e711a9455709665637c8e283..42d3b387522d008b672096d9afa13492dc0aa1c9 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -5,13 +5,19 @@ namespace Drupal\bootstrap; use Drupal\bootstrap\Plugin\AlterManager; use Drupal\bootstrap\Plugin\FormManager; use Drupal\bootstrap\Plugin\PreprocessManager; +use Drupal\bootstrap\Utility\Crypt; use Drupal\bootstrap\Utility\Element; use Drupal\bootstrap\Utility\Unicode; -use Drupal\Component\Utility\Html; +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 GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Psr7\Request; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; /** * The primary class for the Drupal Bootstrap base theme. @@ -92,6 +98,13 @@ class Bootstrap { */ const PROJECT_DOCUMENTATION = 'https://drupal-bootstrap.org'; + /** + * The project API search URL. + * + * @var string + */ + const PROJECT_API_SEARCH_URL = self::PROJECT_DOCUMENTATION . '/api/bootstrap/' . self::PROJECT_BRANCH . '/search/@query'; + /** * The Drupal Bootstrap project page. * @@ -261,11 +274,13 @@ class Bootstrap { * @param string $query * The query to search for. * - * @return string + * @return \Drupal\Component\Render\FormattableMarkup * The complete URL to the documentation site. */ public static function apiSearchUrl($query = '') { - return self::PROJECT_DOCUMENTATION . '/api/bootstrap/' . self::PROJECT_BRANCH . '/search/' . Html::escape($query); + return new FormattableMarkup(self::PROJECT_API_SEARCH_URL, [ + '@query' => $query, + ]); } /** @@ -362,6 +377,7 @@ class Bootstrap { // Danger class. t('Delete')->render() => 'danger', t('Remove')->render() => 'danger', + t('Reset')->render() => 'danger', t('Uninstall')->render() => 'danger', // Success class. @@ -417,29 +433,40 @@ class Bootstrap { /** * Logs and displays a warning about a deprecated function/method being used. * + * @param string $caller + * Optional. The function or Class::method that should be shown as + * deprecated. If not set, it will be extrapolated automatically from + * the backtrace. This is primarily used when this method is being invoked + * from inside another method that isn't technically deprecated but has to + * 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. */ - public static function deprecated($show_message = NULL) { + public static function deprecated($caller = NULL, $show_message = NULL) { $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); // Extrapolate the caller. - $caller = $backtrace[1]; - $class = ''; - if (isset($caller['class'])) { - $parts = explode('\\', $caller['class']); - $class = array_pop($parts) . '::'; + if (!isset($caller) && !empty($backtrace[1]) && ($info = $backtrace[1])) { + $caller = (!empty($info['class']) ? $info['class'] . '::' : '') . $info['function']; + } + + // Remove class namespace. + $method = FALSE; + if (is_string($caller) && strpos($caller, '::') !== FALSE && ($parts = explode('\\', $caller))) { + $method = TRUE; + $caller = array_pop($parts); } - $message = t('The following function(s) or method(s) have been deprecated, please check the logs for a more detailed backtrace on where these are being invoked. Click on the function or method link to search the documentation site for a possible replacement or solution: @title', [ - ':url' => self::apiSearchUrl($class . $caller['function']), - '@title' => ($class ? $caller['class'] . '::' : '') . $caller['function'] . '()', + $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) && !self::getTheme()->getSetting('suppress_deprecated_warnings', FALSE))) { + if ($show_message || (!isset($show_message) && !static::getTheme()->getSetting('suppress_deprecated_warnings', FALSE))) { drupal_set_message($message, 'warning'); } @@ -686,6 +713,7 @@ class Bootstrap { t('Cancel')->render() => 'remove', t('Delete')->render() => 'trash', t('Remove')->render() => 'trash', + t('Reset')->render() => 'trash', t('Search')->render() => 'search', t('Upload')->render() => 'upload', t('Preview')->render() => 'eye-open', @@ -1184,6 +1212,81 @@ class Bootstrap { } } + /** + * Retrieves a response from a URI, using cached response if available. + * + * @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 \Symfony\Component\HttpFoundation\Response + * A Response object. + */ + public static function cachedRequest($uri, array $options = [], &$exception = NULL) { + $options += [ + 'method' => 'GET', + 'headers' => [ + 'User-Agent' => 'Drupal Bootstrap ' . static::PROJECT_BRANCH . ' (' . static::PROJECT_PAGE . ')', + ], + ]; + + $cache = \Drupal::keyValueExpirable('theme:' . static::getTheme()->getName() . ':http'); + $key = 'request-' . Crypt::hashBase64(serialize(['uri' => $uri] + $options)); + $response = $cache->get($key); + + if (!isset($response)) { + /** @var \GuzzleHttp\Client $client */ + $client = \Drupal::service('http_client_factory')->fromOptions($options); + $request = new Request($options['method'], $uri, $options['headers']); + + 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()); + } + + // Only cache if a maximum age has been detected. + if ($response->getStatusCode() == 200 && ($maxAge = $response->getMaxAge())) { + $cache->setWithExpire($key, $response, $maxAge); + } + } + + return $response; + } + + /** + * Retrieves JSON from a URI. + * + * @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 \Symfony\Component\HttpFoundation\JsonResponse + * A JsonResponse object. + */ + public static function requestJson($uri, array $options = [], &$exception = NULL) { + $r = static::cachedRequest($uri, $options, $exception); + $json = Json::decode($r->getContent() ?: '[]') ?: []; + $response = new JsonResponse($json, $r->getStatusCode(), $r->headers->all()); + $response->json = $json; + return $response; + } + /** * Ensures a value is typecast to a string, rendering an array if necessary. * diff --git a/src/Plugin/Alter/LibraryInfo.php b/src/Plugin/Alter/LibraryInfo.php index 844cd3f212722afa9fd394c342bada0b05e9693c..0256495fc40a7a6f10b697d639b7985d68fd3639 100644 --- a/src/Plugin/Alter/LibraryInfo.php +++ b/src/Plugin/Alter/LibraryInfo.php @@ -36,8 +36,8 @@ class LibraryInfo extends PluginBase implements AlterInterface { } // Alter the framework library based on currently set CDN provider. - if ($provider = $this->theme->getProvider()) { - $provider->alterFrameworkLibrary($libraries['framework']); + if ($cdnProvider = $this->theme->getCdnProvider()) { + $cdnProvider->alterFrameworkLibrary($libraries['framework']); } } // Core replacements. diff --git a/src/Plugin/Form/SystemThemeSettings.php b/src/Plugin/Form/SystemThemeSettings.php index 1e2c66a67c53a458951aa0458583bbdb422b21ff..c6f5cd462eabf47682fe0e4f21f88a7f023187f5 100644 --- a/src/Plugin/Form/SystemThemeSettings.php +++ b/src/Plugin/Form/SystemThemeSettings.php @@ -72,6 +72,7 @@ class SystemThemeSettings extends FormBase implements FormInterface { 'general' => t('General'), 'components' => t('Components'), 'javascript' => t('JavaScript'), + 'cdn' => t('CDN'), 'advanced' => t('Advanced'), ]; foreach ($groups as $group => $title) { @@ -80,9 +81,66 @@ class SystemThemeSettings extends FormBase implements FormInterface { '#title' => $title, '#group' => 'bootstrap', ]; + + // Show a button to reset cached HTTP requests. + if ($group === 'advanced') { + $cache = \Drupal::keyValueExpirable('theme:' . $this->theme->getName() . ':http'); + $count = count($cache->getAll()); + $form[$group]['reset_http_request_cache'] = [ + '#type' => 'item', + '#title' => $this->t('Cached HTTP requests: @count', ['@count' => $count]), + '#weight' => 100, + '#smart_description' => FALSE, + '#description' => $this->t('All HTTP requests initiated through the base-theme are cached if there is a "max-age" response header present. These cached requests will persist through cache rebuilds and only expire once the the "max-age" has been reached. If you believe a CDN Provider is not retrieving data properly, you can manually reset this cache here.'), + '#description_display' => 'before', + '#prefix' => '
', + '#suffix' => '
', + ]; + + $form[$group]['reset_http_request_cache']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Reset HTTP Request Cache'), + '#prefix' => '
', + '#suffix' => '
', + '#submit' => [ + [get_class($this), 'submitResetHttpRequestCache'], + ], + '#ajax' => [ + 'callback' => [get_class($this), 'ajaxResetHttpRequestCache'], + 'wrapper' => 'reset-http-request-cache', + ], + ]; + } } } + /** + * Submit callback for resetting the cached HTTP requests. + * + * @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 submitResetHttpRequestCache(array $form, FormStateInterface $form_state) { + $form_state->setRebuild(); + $theme = SystemThemeSettings::getTheme(Element::create($form), $form_state); + $cache = \Drupal::keyValueExpirable('theme:' . $theme->getName() . ':http'); + $cache->deleteAll(); + } + + /** + * AJAX callback for reloading the cached HTTP request markup. + * + * @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 ajaxResetHttpRequestCache(array $form, FormStateInterface $form_state) { + return $form['advanced']['reset_http_request_cache']; + } + /** * Retrieves the currently selected theme on the settings form. * diff --git a/src/Plugin/Provider/Broken.php b/src/Plugin/Provider/Broken.php index 351e73f07def9877e545d68aef50829eb5c7f548..7ca63a6d5c4cb4d40549345f798c4efb8ccb5e12 100644 --- a/src/Plugin/Provider/Broken.php +++ b/src/Plugin/Provider/Broken.php @@ -26,8 +26,8 @@ class Broken extends PluginBase implements ProviderInterface { /** * {@inheritdoc} */ - public function getAssets($types = NULL) { - return []; + public function getCacheTtl() { + return static::CACHE_TTL; } /** @@ -37,6 +37,20 @@ class Broken extends PluginBase implements ProviderInterface { return []; } + /** + * {@inheritdoc} + */ + public function getCdnExceptions($reset = TRUE) { + return []; + } + + /** + * {@inheritdoc} + */ + public function getCdnTheme() { + return NULL; + } + /** * {@inheritdoc} */ @@ -44,6 +58,13 @@ class Broken extends PluginBase implements ProviderInterface { return []; } + /** + * {@inheritdoc} + */ + public function getCdnVersion() { + return NULL; + } + /** * {@inheritdoc} */ @@ -68,28 +89,40 @@ class Broken extends PluginBase implements ProviderInterface { /** * {@inheritdoc} */ - public function getCdnTheme() { - return NULL; + public function resetCache() { + // Intentionally left empty. } + /**************************************************************************** + * + * Deprecated methods + * + ***************************************************************************/ + /** * {@inheritdoc} + * + * @deprecated in 8.x-3.18, will be removed in a future release. */ - public function getThemes() { - return []; + public function getApi() { + return NULL; } /** * {@inheritdoc} + * + * @deprecated in 8.x-3.18, will be removed in a future release. */ - public function getCdnVersion() { - return NULL; + public function getAssets($types = NULL) { + return []; } /** * {@inheritdoc} + * + * @deprecated in 8.x-3.18, will be removed in a future release. */ - public function getVersions() { + public function getThemes() { return []; } @@ -98,8 +131,8 @@ class Broken extends PluginBase implements ProviderInterface { * * @deprecated in 8.x-3.18, will be removed in a future release. */ - public function getApi() { - return NULL; + public function getVersions() { + return []; } /** diff --git a/src/Plugin/Provider/JsDelivr.php b/src/Plugin/Provider/JsDelivr.php index d85003b7d1c06762c14250c1cda56a1d99e566d5..bb6fe266d6708088d75169fe4c1b4c49d1b899a5 100644 --- a/src/Plugin/Provider/JsDelivr.php +++ b/src/Plugin/Provider/JsDelivr.php @@ -39,19 +39,12 @@ class JsDelivr extends ProviderBase { protected $latestVersion = []; /** - * A list of themes, keyed by NPM package name. + * A list of themes, keyed by version. * * @var array[] */ protected $themes = []; - /** - * A list of versions, keyed by NPM package name. - * - * @var array[] - */ - protected $versions = []; - /** * {@inheritdoc} */ @@ -80,7 +73,7 @@ class JsDelivr extends ProviderBase { $version = $this->getCdnVersion(); } if (!isset($this->themes[$version])) { - $this->themes[$version] = $this->cacheGet('themes.' . Unicode::escapeDelimiter($version), [], function ($themes) use ($version) { + $this->themes[$version] = $this->cacheGet('themes', Unicode::escapeDelimiter($version), [], function ($themes) use ($version) { foreach (['bootstrap', 'bootswatch'] as $package) { $mappedVersion = $this->mapVersion($version, $package); $files = $this->requestApiV1($package, $mappedVersion); @@ -95,10 +88,10 @@ class JsDelivr extends ProviderBase { /** * {@inheritdoc} */ - public function getCdnVersions($package = 'bootstrap') { - if (!isset($this->versions[$package])) { - $this->versions[$package] = $this->cacheGet("versions.$package", [], function ($versions) use ($package) { - $json = $this->requestApiV1($package) + ['versions' => []]; + public function getCdnVersions() { + if (!isset($this->versions)) { + $this->versions = $this->cacheGet('versions', 'bootstrap', [], function ($versions) { + $json = $this->requestApiV1('bootstrap') + ['versions' => []]; foreach ($json['versions'] as $version) { // Skip irrelevant versions. if (!preg_match('/^' . substr(Bootstrap::FRAMEWORK_VERSION, 0, 1) . '\.\d+\.\d+$/', $version)) { @@ -109,7 +102,7 @@ class JsDelivr extends ProviderBase { return $versions; }); } - return $this->versions[$package]; + return $this->versions; } /** @@ -270,30 +263,35 @@ class JsDelivr extends ProviderBase { * The JSON data from the API. */ protected function requestApiV1($package, $version = NULL) { - $url = static::BASE_API_URL . "/$package"; + $uri = static::BASE_API_URL . "/$package"; + $options = [ +// 'collection' => $this->getCacheId(), + ]; // If no version was passed, then all versions are returned. if (!$version) { - return $this->requestJson($url); + $response = Bootstrap::requestJson($uri, $options); + // If bootstrap JSON could not be returned, provide defaults. + if (!$response->json && $this->cdnExceptions && $package === 'bootstrap') { + $response->json = ['versions' => [Bootstrap::FRAMEWORK_VERSION]]; + } + return $response->json; } - $json = $this->requestJson("$url@$version/flat"); + $response = Bootstrap::requestJson("$uri@$version/flat", $options); // If bootstrap JSON could not be returned, provide defaults. - if (!$json && $package === 'bootstrap') { - $version = Bootstrap::FRAMEWORK_VERSION; + if (!$response->json && $this->cdnExceptions && $package === 'bootstrap') { return [ - 'css' => [static::BASE_CDN_URL . "/$package@$version/dist/css/bootstrap.css"], - 'js' => [static::BASE_CDN_URL . "/$package@$version/dist/js/bootstrap.js"], - 'min' => [ - 'css' => [static::BASE_CDN_URL . "/$package@$version/dist/css/bootstrap.min.css"], - 'js' => [static::BASE_CDN_URL . "/$package@$version/dist/js/bootstrap.min.js"], - ], + '/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); + return $this->parseFiles($response->json); } /** diff --git a/src/Plugin/Provider/ProviderBase.php b/src/Plugin/Provider/ProviderBase.php index b02614a92cf34bec149816e8fe3d9b0c003bd7f3..736bdc3361a1f2e8e208158142b408bcb58dedc0 100644 --- a/src/Plugin/Provider/ProviderBase.php +++ b/src/Plugin/Provider/ProviderBase.php @@ -10,8 +10,6 @@ use Drupal\bootstrap\Utility\Crypt; use Drupal\bootstrap\Utility\Unicode; use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\NestedArray; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Psr7\Request; /** * CDN provider base class. @@ -29,6 +27,22 @@ class ProviderBase extends PluginBase implements ProviderInterface { */ protected $assets = []; + /** + * The cache backend used for caching various CDN provider tasks. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + */ + protected $cache; + + /** + * The amount, in seconds, CDN Provider data should be cached. + * + * @var int + * + * @see \Drupal\bootstrap\Plugin\Provider\ProviderInterface::CACHE_TTL + */ + protected $cacheTtl; + /** * The currently set CDN assets. * @@ -37,11 +51,11 @@ class ProviderBase extends PluginBase implements ProviderInterface { protected $cdnAssets; /** - * The cache backend used for caching various CDN provider tasks. + * A list of currently set Exception objects. * - * @var \Drupal\Core\Cache\CacheBackendInterface + * @var \Drupal\bootstrap\Plugin\Provider\ProviderException[] */ - protected $cache; + protected $cdnExceptions = []; /** * The versions supplied by the CDN provider. @@ -51,86 +65,80 @@ class ProviderBase extends PluginBase implements ProviderInterface { protected $versions; /** - * {@inheritdoc} + * Adds a new CDN Provider exception. + * + * @param string|\Exception $message + * The exception message. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->cache = \Drupal::cache('discovery'); + 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); + } } /** * {@inheritdoc} */ public function alterFrameworkLibrary(array &$framework, $min = NULL) { - // Attempt to retrieve CDN assets from a sort of permanent cached in the - // theme settings. This is primarily used to avoid unnecessary API requests - // and speed up the process during a cache rebuild. Theme settings are used - // as they persist through cache rebuilds. In order to prevent stale data, - // a hash is used based on current CDN settings and this "permacache" is - // reset at least once a week regardless. + // 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. // @see https://www.drupal.org/project/bootstrap/issues/3031415 - $cdnCache = $this->theme->getSetting('cdn_cache', []); - $requestTime = \Drupal::time()->getRequestTime(); - - // Reset cache if expired. - if (isset($cdnCache['expire']) && (empty($cdnCache['expire']) || $requestTime > $cdnCache['expire'])) { - $cdnCache = []; - } + $cdn = [ + 'ttl' => $this->getCacheTtl(), + '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(), + ]; - // Set expiration date (1 week by default). - if (!isset($cdnCache['expire'])) { - $cdnCache['expire'] = $requestTime + $this->theme->getSetting('cdn_cache_expire', 604800); - } + // Construct a key based on hashed CDN Provider values. + $key = 'library-' . Crypt::hashBase64(serialize($cdn)); + + // Retrieve the assets from the cache. + $assets = $cdn['ttl'] > 0 ? $this->getCache()->get($key) : NULL; + + // Rebuild assets if they're not set. + if (!isset($assets)) { + $cdnAssets = $this->getCdnAssets($cdn['version'], $cdn['theme']); + + // Iterate over each type. + $assets = []; + 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; + } + } + } - $cdnVersion = $this->getCdnVersion(); - $cdnTheme = $this->getCdnTheme(); - - // Cache not found. - $cdnHash = Crypt::hashBase64("{$this->pluginId}:$cdnTheme:$cdnVersion"); - if (!isset($cdnCache[$cdnHash])) { - // Retrieve assets and reset cache (should only cache one at a time). - $cdnCache = [ - 'expire' => $cdnCache['expire'], - $cdnHash => $this->getCdnAssets($cdnVersion, $cdnTheme), - ]; - $this->theme->setSetting('cdn_cache', $cdnCache); + // Cache the assets. + $this->getCache()->setWithExpire($key, $assets, $cdn['ttl']); } // Immediately return if there are no theme CDN assets to use. - if (empty($cdnCache[$cdnHash])) { + if (empty($assets)) { return; } - // Retrieve the system performance config. - if (!isset($min)) { - $config = \Drupal::config('system.performance'); - $min = [ - 'css' => $config->get('css.preprocess'), - 'js' => $config->get('js.preprocess'), - ]; - } - else { - $min = ['css' => !!$min, 'js' => !!$min]; - } - - // Iterate over each type. - $assets = []; - foreach (['css', 'js'] as $type) { - $files = !empty($min[$type]) && isset($cdnCache[$cdnHash]['min'][$type]) ? $cdnCache[$cdnHash]['min'][$type] : (isset($cdnCache[$cdnHash][$type]) ? $cdnCache[$cdnHash][$type] : []); - foreach ($files as $asset) { - $data = ['data' => $asset, 'type' => 'external']; - // CSS library assets use "SMACSS" categorization, assign it to "base". - if ($type === 'css') { - $assets[$type]['base'][$asset] = $data; - } - else { - $assets[$type][$asset] = $data; - } - } - } - // Override the framework version with the CDN version that is being used. - $framework['version'] = $cdnVersion; + $framework['version'] = $cdn['version']; // Merge the assets into the library info. $framework = NestedArray::mergeDeepArray([$assets, $framework], TRUE); @@ -150,22 +158,31 @@ class ProviderBase extends PluginBase implements ProviderInterface { /** * Retrieves a value from the CDN provider cache. * + * @param string $name + * The name of the cache item to retrieve. * @param string $key - * The name of the item to retrieve. Note: this can be in the form of dot - * notation if the value is nested in an array. + * 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 $name will be returned. * @param mixed $default * Optional. The default value to return if $key is not set. * @param callable $builder * Optional. If provided, a builder will be invoked when there is no cache - * currently set. + * currently set. The return value of the build will be used to set the + * cached value, provided there are no CDN Provider exceptions generated. + * If there are, but you still need the cache to be set, reset them prior + * to returning from the builder callback. * * @return mixed * The cached value if it's set or the value supplied to $default if not. */ - protected function cacheGet($key, $default = NULL, callable $builder = NULL) { - $cid = $this->getCacheId(); - $cache = $this->cache->get($cid); - $data = $cache && isset($cache->data) && is_array($cache->data) ? $cache->data : []; + protected function cacheGet($name, $key = NULL, $default = NULL, callable $builder = NULL) { + $data = $this->getCache()->get($name, []); + + if (!isset($key)) { + return $data; + } + $parts = Unicode::splitDelimiter($key); $value = NestedArray::getValue($data, $parts, $key_exists); @@ -176,7 +193,12 @@ class ProviderBase extends PluginBase implements ProviderInterface { $value = $default; } NestedArray::setValue($data, $parts, $value); - $this->cache->set($cid, $data); + + // Only set the cache if no CDN Provider exceptions were thrown. + if (!$this->cdnExceptions) { + $this->getCache()->setWithExpire($name, $data, $this->getCacheTtl()); + } + return $value; } @@ -184,28 +206,33 @@ class ProviderBase extends PluginBase implements ProviderInterface { } /** - * Sets a value in the CDN provider cache. + * {@inheritdoc} + */ + protected function discoverCdnAssets($version, $theme) { + return $this->getAssets(); + } + + /** + * Retrieves the cache instance. * - * @param string $key - * The name of the item to set. Note: this can be in the form of dot - * notation if the value is nested in an array. - * @param mixed $value - * Optional. The value to set. - */ - protected function cacheSet($key, $value = NULL) { - $cid = $this->getCacheId(); - $cache = $this->cache->get($cid); - $data = $cache && isset($cache->data) && is_array($cache->data) ? $cache->data : []; - $parts = Unicode::splitDelimiter($key); - NestedArray::setValue($data, $parts, $value); - $this->cache->set($cid, $data); + * @return \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + * An expirable key/value storage instance. + */ + protected function getCache() { + if (!isset($this->cache)) { + $this->cache = \Drupal::keyValueExpirable($this->getCacheId()); + } + return $this->cache; } /** * {@inheritdoc} */ - protected function discoverCdnAssets($version, $theme) { - return $this->getAssets(); + public function getCacheTtl() { + if (!isset($this->cacheTtl)) { + $this->cacheTtl = (int) $this->theme->getSetting('cdn_cache_ttl', static::CACHE_TTL); + } + return $this->cacheTtl; } /** @@ -215,7 +242,7 @@ class ProviderBase extends PluginBase implements ProviderInterface { * The CDN provider cache identifier. */ protected function getCacheId() { - return "theme:{$this->theme->getName()}:provider:{$this->getPluginId()}"; + return "theme:{$this->theme->getName()}:cdn:{$this->getPluginId()}"; } /** @@ -230,12 +257,12 @@ class ProviderBase extends PluginBase implements ProviderInterface { } if (!isset($this->cdnAssets)) { - $this->cdnAssets = $this->cacheGet('cdn.assets', []); + $this->cdnAssets = $this->cacheGet('assets'); } if (!isset($this->cdnAssets[$version][$theme])) { $escapedVersion = Unicode::escapeDelimiter($version); - $this->cdnAssets[$version][$theme] = $this->cacheGet("cdn.assets.$escapedVersion.$theme", [], function () use ($version, $theme) { + $this->cdnAssets[$version][$theme] = $this->cacheGet('assets', "$escapedVersion.$theme", [], function () use ($version, $theme) { return $this->discoverCdnAssets($version, $theme); }); } @@ -243,6 +270,17 @@ class ProviderBase extends PluginBase implements ProviderInterface { return $this->cdnAssets[$version][$theme]; } + /** + * {@inheritdoc} + */ + public function getCdnExceptions($reset = TRUE) { + $exceptions = $this->cdnExceptions; + if ($reset) { + $this->cdnExceptions = []; + } + return $exceptions; + } + /** * {@inheritdoc} */ @@ -333,58 +371,15 @@ class ProviderBase extends PluginBase implements ProviderInterface { /** * {@inheritdoc} */ - public function hasError() { - return $this->pluginDefinition['error']; - } - - /** - * {@inheritdoc} - */ - public function isImported() { - return $this->pluginDefinition['imported']; - } - - /** - * Retrieves JSON from a URI. - * - * @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 array - * The requested JSON array. - */ - protected function requestJson($uri, array $options = [], &$exception = NULL) { - $json = []; - - $options += [ - 'method' => 'GET', - 'headers' => [ - 'User-Agent' => 'Drupal Bootstrap 8.x-3.x (https://www.drupal.org/project/bootstrap)', - ], - ]; - - /** @var \GuzzleHttp\Client $client */ - $client = \Drupal::service('http_client_factory')->fromOptions($options); - $request = new Request($options['method'], $uri); - try { - $response = $client->send($request, $options); - if ($response->getStatusCode() == 200) { - $contents = $response->getBody(TRUE)->getContents(); - $json = Json::decode($contents) ?: []; - } + public function resetCache() { + $this->getCache()->deleteAll(); + + // Invalidate library info if this provider is the one currently used. + if (($provider = $this->theme->getCdnProvider()) && $provider->getPluginId() === $this->pluginId) { + /** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */ + $invalidator = \Drupal::service('cache_tags.invalidator'); + $invalidator->invalidateTags(['library_info']); } - catch (GuzzleException $e) { - $exception = $e; - } - catch (\Exception $e) { - $exception = $e; - } - - return $json; } /**************************************************************************** @@ -399,6 +394,7 @@ class ProviderBase extends PluginBase implements ProviderInterface { * @deprecated in 8.x-3.18, will be removed in a future release. */ public function getApi() { + Bootstrap::deprecated(); return $this->pluginDefinition['api']; } @@ -412,6 +408,26 @@ class ProviderBase extends PluginBase implements ProviderInterface { return $this->assets; } + /** + * {@inheritdoc} + * + * @deprecated in 8.x-3.18, will be removed in a future release. + */ + public function hasError() { + Bootstrap::deprecated(); + return $this->pluginDefinition['error']; + } + + /** + * {@inheritdoc} + * + * @deprecated in 8.x-3.18, will be removed in a future release. + */ + public function isImported() { + Bootstrap::deprecated(); + return $this->pluginDefinition['imported']; + } + /** * {@inheritdoc} * @@ -440,7 +456,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 = $this->requestJson($api); + $json = Bootstrap::requestJson($api); } if (!isset($json)) { @@ -457,6 +473,8 @@ class ProviderBase extends PluginBase implements ProviderInterface { * * @deprecated in 8.x-3.18, will be removed in a future release. */ - public function processApi(array $json, array &$definition) {} + public function processApi(array $json, array &$definition) { + Bootstrap::deprecated(); + } } diff --git a/src/Plugin/Provider/ProviderException.php b/src/Plugin/Provider/ProviderException.php new file mode 100644 index 0000000000000000000000000000000000000000..504a86bc3f616ccabe92e26ab2df8cf8def4028c --- /dev/null +++ b/src/Plugin/Provider/ProviderException.php @@ -0,0 +1,44 @@ +provider = $provider; + } + + /** + * Retrieves the CDN Provider instance. + * + * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface + * The CDN Provider instance. + */ + public function getProvider() { + return $this->provider; + } + +} diff --git a/src/Plugin/Provider/ProviderInterface.php b/src/Plugin/Provider/ProviderInterface.php index de55dd4b4a357fed22da460da830d7a6e2e9769f..373e42c1fb4b592cfa0c55c5d1ab65f426ba64cc 100644 --- a/src/Plugin/Provider/ProviderInterface.php +++ b/src/Plugin/Provider/ProviderInterface.php @@ -12,6 +12,13 @@ use Drupal\Component\Plugin\PluginInspectionInterface; */ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspectionInterface { + /** + * The default CDN Provider cache time-to-live (TTL) value (one week). + * + * @var int + */ + const CACHE_TTL = 604800; + /** * Alters the framework library. * @@ -23,6 +30,14 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect */ public function alterFrameworkLibrary(array &$framework, $min = NULL); + /** + * Retrieves the cache time-to-live (TTL) value. + * + * @return int + * The cache expire value, in seconds. + */ + public function getCacheTtl(); + /** * Retrieves the assets from the CDN, if any. * @@ -60,6 +75,21 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect */ public function getLabel(); + /** + * Retrieves any CDN ProviderException objects triggered during discovery. + * + * Note: this is primarily used as a way to communicate in the UI that + * the discovery of the CDN Provider's assets failed. + * + * @param bool $reset + * Flag indicating whether to remove the Exceptions once they have been + * retrieved. + * + * @return \Drupal\bootstrap\Plugin\Provider\ProviderException[] + * An array of CDN ProviderException objects, if any. + */ + public function getCdnExceptions($reset = TRUE); + /** * Retrieves the currently set CDN provider theme. * @@ -98,6 +128,11 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect */ public function getCdnVersions(); + /** + * Removes any cached data the CDN Provider may have. + */ + public function resetCache(); + /**************************************************************************** * * Deprecated methods @@ -166,7 +201,9 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect * TRUE or FALSE * * @deprecated in 8.x-3.18, will be removed in a future release. There is no - * replacement for this functionality. + * 1:1 replacement for this functionality. + * + * @see \Drupal\bootstrap\Plugin\Provider\ProviderInterface::getCdnExceptions() */ public function hasError(); diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php index e8437add3cc9c72dbaadaf1542aaf98a29e9a435..0c87147224c91e8be0bec26018221aa13ce065e9 100644 --- a/src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php +++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php @@ -2,8 +2,6 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn; -use Drupal\bootstrap\Plugin\Setting\SettingBase; - /** * The "cdn_custom_css" theme setting. * @@ -18,19 +16,10 @@ use Drupal\bootstrap\Plugin\Setting\SettingBase; * defaultValue = "https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.css", * description = @Translation("It is best to use https protocols here as it will allow more flexibility if the need ever arises."), * groups = { - * "advanced" = @Translation("Advanced"), * "cdn" = @Translation("CDN (Content Delivery Network)"), + * "cdn_provider" = false, * "custom" = false, * }, * ) */ -class CdnCustomCss extends SettingBase { - - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return ['library_info']; - } - -} +class CdnCustomCss extends CdnProviderBase {} diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php index 5f61c3b3dca6d3f3d5203de6f9077acc906e9825..6894481c6b737d1f52d3bf59aef06706bf63b988 100644 --- a/src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php +++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php @@ -2,8 +2,6 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn; -use Drupal\bootstrap\Plugin\Setting\SettingBase; - /** * The "cdn_custom_css_min" theme setting. * @@ -18,19 +16,10 @@ use Drupal\bootstrap\Plugin\Setting\SettingBase; * defaultValue = "https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css", * description = @Translation("Additionally, you can provide the minimized version of the file. It will be used instead if site aggregation is enabled."), * groups = { - * "advanced" = @Translation("Advanced"), * "cdn" = @Translation("CDN (Content Delivery Network)"), + * "cdn_provider" = false, * "custom" = false, * }, * ) */ -class CdnCustomCssMin extends SettingBase { - - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return ['library_info']; - } - -} +class CdnCustomCssMin extends CdnProviderBase {} diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php index c88936c788b4284ade0d0b31d096df3b75ceb10a..8b5e93168422d17abce91a6fa2ccbe84566149b5 100644 --- a/src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php +++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php @@ -2,8 +2,6 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn; -use Drupal\bootstrap\Plugin\Setting\SettingBase; - /** * The "cdn_custom_js" theme setting. * @@ -18,19 +16,10 @@ use Drupal\bootstrap\Plugin\Setting\SettingBase; * defaultValue = "https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.js", * description = @Translation("It is best to use https protocols here as it will allow more flexibility if the need ever arises."), * groups = { - * "advanced" = @Translation("Advanced"), * "cdn" = @Translation("CDN (Content Delivery Network)"), + * "cdn_provider" = false, * "custom" = false, * }, * ) */ -class CdnCustomJs extends SettingBase { - - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return ['library_info']; - } - -} +class CdnCustomJs extends CdnProviderBase {} diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php b/src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php index c571d54cda49f051e1daf763f34c4eb2d9ab7541..ccf49fabcc802a007b1079326b9b18806fb1a171 100644 --- a/src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php +++ b/src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php @@ -2,8 +2,6 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn; -use Drupal\bootstrap\Plugin\Setting\SettingBase; - /** * The "cdn_custom_js_min" theme setting. * @@ -18,19 +16,10 @@ use Drupal\bootstrap\Plugin\Setting\SettingBase; * defaultValue = "https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js", * description = @Translation("Additionally, you can provide the minimized version of the file. It will be used instead if site aggregation is enabled."), * groups = { - * "advanced" = @Translation("Advanced"), * "cdn" = @Translation("CDN (Content Delivery Network)"), + * "cdn_provider" = false, * "custom" = false, * }, * ) */ -class CdnCustomJsMin extends SettingBase { - - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return ['library_info']; - } - -} +class CdnCustomJsMin extends CdnProviderBase {} diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php b/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php index ae70749b7ed44a9d822293fb330a00f45e7d008f..76d9eb1991adf0eed40dedbaad0ebb47b88625a2 100644 --- a/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php +++ b/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php @@ -20,34 +20,43 @@ use Drupal\Core\Form\FormStateInterface; * empty_option = @Translation("Bootstrap (default)"), * empty_value = "bootstrap", * groups = { - * "advanced" = @Translation("Advanced"), * "cdn" = @Translation("CDN (Content Delivery Network)"), + * "cdn_provider" = false, * "jsdelivr" = false, * }, * ) */ -class CdnJsdelivrTheme extends CdnProvider { +class CdnJsdelivrTheme extends CdnProviderBase { /** * {@inheritdoc} */ - public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) { - $setting = $this->getSettingElement($form, $form_state); + public function buildCdnProviderElement(Element $setting, FormStateInterface $form_state) { $version = $form_state->getValue('cdn_jsdelivr_version', $this->theme->getSetting('cdn_jsdelivr_version')); $themes = $this->provider->getCdnThemes($version); - $setting->setProperty('suffix', '
'); - $setting->setProperty('description', t('Choose the example Bootstrap Theme provided by Bootstrap or one of the many, many Bootswatch themes!', [ - ':bootswatch' => 'https://bootswatch.com', - ':bootstrap_theme' => 'https://getbootstrap.com/docs/3.4/examples/theme/', - ])); - $options = []; foreach ($themes as $theme => $data) { $options[$theme] = $data['title']; } $setting->setProperty('options', $options); + $setting->setProperty('suffix', '
'); + + if ($this->provider->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->provider->getLabel(), + ])); + } + else { + $setting->setProperty('description', t('Choose the example Bootstrap Theme provided by Bootstrap or one of the many, many Bootswatch themes!', [ + ':bootswatch' => 'https://bootswatch.com', + ':bootstrap_theme' => 'https://getbootstrap.com/docs/3.4/examples/theme/', + ])); + } + + // Check for any CDN failure(s). + $this->checkCdnExceptions(); } } diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php b/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php index d15b25ce20c112e54d64488da20825884d6cb0b0..a372787db18f3d0de3d8c94359c852d2c5bf8f45 100644 --- a/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php +++ b/src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php @@ -21,48 +21,38 @@ use Drupal\Core\Form\FormStateInterface; * description = @Translation("Choose the Bootstrap version from jsdelivr"), * defaultValue = @BootstrapConstant("Drupal\bootstrap\Bootstrap::FRAMEWORK_VERSION"), * groups = { - * "advanced" = @Translation("Advanced"), * "cdn" = @Translation("CDN (Content Delivery Network)"), + * "cdn_provider" = false, * "jsdelivr" = false, * }, * ) */ -class CdnJsdelivrVersion extends CdnProvider { +class CdnJsdelivrVersion extends CdnProviderBase { /** * {@inheritdoc} */ - public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) { - // Add autoload fix to make sure AJAX callbacks work. - static::formAutoloadFix($form_state); - + public function buildCdnProviderElement(Element $setting, FormStateInterface $form_state) { $plugin_id = Html::cleanCssIdentifier($this->provider->getPluginId()); - $setting = $this->getSettingElement($form, $form_state); - $versions = $this->provider->getCdnVersions(); - - $setting->setProperty('options', $versions); + $setting->setProperty('options', $this->provider->getCdnVersions()); $setting->setProperty('ajax', [ - 'callback' => [get_class($this), 'ajaxCallback'], + 'callback' => [get_class($this), 'ajaxProviderCallback'], 'wrapper' => 'cdn-provider-' . $plugin_id, ]); - if (!$this->provider->hasError() && !$this->provider->isImported()) { - $setting->setProperty('description', t('These versions are automatically populated by the @provider API upon cache clear and newer versions may appear over time. It is highly recommended the version that the site was built with stays at that version. Until a newer version has been properly tested for updatability by the site maintainer, you should not arbitrarily "update" just because there is a newer version. This can cause many inconsistencies and undesired effects with an existing site.', [ + if ($this->provider->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->provider->getLabel(), + ])); + } + else { + $setting->setProperty('description', t('These versions are automatically populated by the @provider API. While newer versions may appear over time, it is highly recommended the version that the site was built with stays at that version. Until a newer version has been properly tested for updatability by the site maintainer, you should not arbitrarily "update" just because there is a newer version. This can cause many inconsistencies and undesired effects with an existing site.', [ '@provider' => $this->provider->getLabel(), ])); } - } - /** - * AJAX callback for reloading CDN provider elements. - * - * @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 ajaxCallback(array $form, FormStateInterface $form_state) { - return $form['advanced']['cdn'][$form_state->getValue('cdn_provider', Bootstrap::getTheme()->getSetting('cdn_provider'))]; + // Check for any CDN failure(s). + $this->checkCdnExceptions(); } } diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnProvider.php b/src/Plugin/Setting/Advanced/Cdn/CdnProvider.php index a0741dbd18a74e0876c235f50e91da102e42f5b1..fa7b0f45adc8b977cd4c7d5cfce721a2259be821 100644 --- a/src/Plugin/Setting/Advanced/Cdn/CdnProvider.php +++ b/src/Plugin/Setting/Advanced/Cdn/CdnProvider.php @@ -3,10 +3,9 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced\Cdn; use Drupal\bootstrap\Bootstrap; +use Drupal\bootstrap\Plugin\Form\SystemThemeSettings; 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; @@ -21,88 +20,68 @@ use Drupal\Core\Form\FormStateInterface; * id = "cdn_provider", * type = "select", * title = @Translation("CDN Provider"), - * description = @Translation("Choose between jsdelivr or a custom cdn source."), + * description = @Translation("Choose the CDN Provider used to load Bootstrap resources."), * defaultValue = "jsdelivr", * empty_value = "", * weight = -1, * groups = { - * "advanced" = @Translation("Advanced"), * "cdn" = @Translation("CDN (Content Delivery Network)"), + * "cdn_provider" = false, * }, * options = { }, * ) */ -class CdnProvider extends SettingBase { - - use FormAutoloadFixTrait; - - /** - * The current provider. - * - * @var \Drupal\bootstrap\Plugin\Provider\ProviderInterface - */ - protected $provider; - - /** - * 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->provider = $this->providerManager->get(isset($plugin_definition['cdn_provider']) ? $plugin_definition['cdn_provider'] : NULL); - } +class CdnProvider extends CdnProviderBase { /** * {@inheritdoc} */ public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) { - // Add autoload fix to make sure AJAX callbacks work. - static::formAutoloadFix($form_state); + parent::alterFormElement($form, $form_state); // Retrieve the provider from form values or the setting. $default_provider = $form_state->getValue('cdn_provider', $this->theme->getSetting('cdn_provider')); + // Wrap the default group so it can be replaced via AJAX. $group = $this->getGroupElement($form, $form_state); - $description_label = $this->t('NOTE'); - $description = $this->t('Using one of the "CDN Provider" options below is the preferred method for loading Bootstrap CSS and JS on simpler sites that do not use a site-wide CDN. Using a "CDN Provider" for loading Bootstrap, however, does mean that it depends on a third-party service. There is no obligation or commitment by these third-parties that guarantees any up-time or service quality. If you need to customize Bootstrap and have chosen to compile the source code locally (served from this site), you must disable the "CDN Provider" option below by choosing "- None -" and alternatively enable a site-wide CDN implementation. All local (served from this site) versions of Bootstrap will be superseded by any enabled "CDN Provider" below. Do not do both.'); - $group->setProperty('description', '
' . $description_label . ': ' . $description . '
'); - $group->setProperty('open', !!$default_provider); + $group->setProperty('prefix', '
'); + $group->setProperty('suffix', '
'); // Intercept possible manual import of API data via AJAX callback. $this->importProviderData($form_state); - $options = []; - foreach ($this->theme->getProviders() as $plugin_id => $provider) { - // Skip the broken provider. - if ($plugin_id === '_broken') { - continue; - } - $options[$plugin_id] = $provider->getLabel(); - $this->createProviderGroup($group, $provider); - } - // Override the options with the provider manager discovery. $setting = $this->getSettingElement($form, $form_state); - $setting->setProperty('options', $options); + $setting->setProperty('empty_option', $this->t('None (compile locally)')); + $providers = $this->theme->getCdnProviders(); + $setting->setProperty('options', array_map(function (ProviderInterface $provider) { + return $provider->getLabel(); + }, $providers)); + + $setting->setProperty('ajax', [ + 'callback' => [get_class($this), 'ajaxProvidersCallback'], + 'wrapper' => 'cdn-providers', + ]); + + if (isset($providers[$default_provider])) { + $provider = $providers[$default_provider]; + $this->createProviderGroup($group, $provider); + } } /** - * AJAX callback for reloading CDN provider elements. + * 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 ajaxCallback(array $form, FormStateInterface $form_state) { - return $form['advanced']['cdn'][$form_state->getValue('cdn_provider', Bootstrap::getTheme()->getSetting('cdn_provider'))]; + public static function submitResetProviderCache(array $form, FormStateInterface $form_state) { + $form_state->setRebuild(); + $theme = SystemThemeSettings::getTheme(Element::create($form), $form_state); + $provider = ProviderManager::load($theme, $form_state->getValue('cdn_provider', $theme->getSetting('cdn_provider'))); + $provider->resetCache(); } /** @@ -113,19 +92,14 @@ class CdnProvider extends SettingBase { * @param \Drupal\bootstrap\Plugin\Provider\ProviderInterface $provider * The provider instance. */ - private function createProviderGroup(Element $group, ProviderInterface $provider) { + protected function createProviderGroup(Element $group, ProviderInterface $provider) { $plugin_id = Html::cleanCssIdentifier($provider->getPluginId()); // Create the provider container. $group->$plugin_id = [ '#type' => 'container', - '#prefix' => '
', + '#prefix' => '
', '#suffix' => '
', - '#states' => [ - 'visible' => [ - ':input[name="cdn_provider"]' => ['value' => $plugin_id], - ], - ], ]; // Add in the provider description. @@ -136,9 +110,27 @@ class CdnProvider extends SettingBase { ]; } + // Add a CDN Provider cache reset button. + if ($provider->getPluginId() !== 'custom' && ($reset = $this->buildResetProviderCache($provider))) { + $group->$plugin_id->reset = $reset; + } + + // To avoid triggering unnecessary deprecation messages, extract these + // values from the provider definition directly. + // @todo Remove when the deprecated functionality is removed. + $definition = $provider->getPluginDefinition(); + $hasError = !empty($definition['error']); + $isImported = !empty($definition['imported']); + // Indicate there was an error retrieving the provider's API data. - if ($provider->hasError() || $provider->isImported()) { - if ($provider->hasError()) { + if ($hasError || $isImported) { + if ($isImported) { + Bootstrap::deprecated('\Drupal\bootstrap\Plugin\Provider\ProviderInterface::isImported'); + } + if ($hasError) { + // Now a deprecation message can be shown as the provider clearly is + // using the outdated "process definition" method of providing assets. + Bootstrap::deprecated('\Drupal\bootstrap\Plugin\Provider\ProviderInterface::hasError'); $description_label = $this->t('ERROR'); $description = $this->t('Unable to reach or parse the data provided by the @title API. Ensure the server this website is hosted on is able to initiate HTTP requests. If the request consistently fails, it is likely that there are certain PHP functions that have been disabled by the hosting provider for security reasons. It is possible to manually copy and paste the contents of the following URL into the "Imported @title data" section below.

:provider_api.', [ '@title' => $provider->getLabel(), @@ -177,20 +169,13 @@ class CdnProvider extends SettingBase { } } - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return ['library_info']; - } - /** * 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. */ - private function importProviderData(FormStateInterface $form_state) { + 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); diff --git a/src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php b/src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php new file mode 100644 index 0000000000000000000000000000000000000000..3aacd5575aaf767a6c342e10e05d6d637accb289 --- /dev/null +++ b/src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php @@ -0,0 +1,164 @@ +providerManager = new ProviderManager($this->theme); + $this->provider = $this->providerManager->get(isset($plugin_definition['cdn_provider']) ? $plugin_definition['cdn_provider'] : NULL); + } + + /** + * {@inheritdoc} + */ + 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->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. + $default_provider = $form_state->getValue('cdn_provider', $this->theme->getSetting('cdn_provider')); + if ($default_provider !== $this->provider->getPluginId()) { + return; + } + $setting = $this->getSettingElement($form, $form_state); + $this->buildCdnProviderElement($setting, $form_state); + } + + /** + * Builds the setting element for the CDN Provider. + * + * @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. + */ + public function buildCdnProviderElement(Element $setting, FormStateInterface $form_state) { + // Allow settings to build more. + } + + /** + * 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) { + /** @var \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter */ + $dateFormatter = \Drupal::service('date.formatter'); + $reset = Element::createStandalone([ + '#type' => 'item', + '#weight' => 100, + '#description' => $this->t('All @provider data is cached using a time-based expiration method so it can persist through numerous cache rebuilds. If you believe data is not being retrieved from the API properly, you can manually reset the cache here. Otherwise it will invalidate and be rebuilt automatically after %duration.', [ + '@provider' => $provider->getLabel(), + '%duration' => $dateFormatter->formatInterval($provider->getCacheTtl()), + ]), + ]); + + $reset->submit = Element::createStandalone([ + '#type' => 'submit', + '#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; + } + + /** + * AJAX callback for reloading CDN providers. + * + * @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 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. + */ + public static function ajaxProviderCallback(array $form, FormStateInterface $form_state) { + return $form['cdn']['cdn_provider'][$form_state->getValue('cdn_provider', Bootstrap::getTheme()->getSetting('cdn_provider'))]; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return ['library_info']; + } + + /** + * Handles any CDN Provider exceptions that may have been thrown. + */ + protected function checkCdnExceptions() { + if ($exceptions = $this->provider->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->provider->getLabel(), + ]), 'error'); + foreach ($exceptions as $exception) { + watchdog_exception('bootstrap', $exception); + } + } + } + +} diff --git a/src/Plugin/Setting/Advanced/SuppressDeprecatedWarnings.php b/src/Plugin/Setting/Advanced/SuppressDeprecatedWarnings.php index c61311d70d4a3ecb23bcd6000c09c7dcbd8f9b79..6e0c53e2edbd5fc33039e49c2bb3e774704a936d 100644 --- a/src/Plugin/Setting/Advanced/SuppressDeprecatedWarnings.php +++ b/src/Plugin/Setting/Advanced/SuppressDeprecatedWarnings.php @@ -3,8 +3,6 @@ namespace Drupal\bootstrap\Plugin\Setting\Advanced; use Drupal\bootstrap\Plugin\Setting\SettingBase; -use Drupal\bootstrap\Utility\Element; -use Drupal\Core\Form\FormStateInterface; /** * The "suppress_deprecated_warnings" theme setting. @@ -23,18 +21,4 @@ use Drupal\Core\Form\FormStateInterface; * }, * ) */ -class SuppressDeprecatedWarnings extends SettingBase { - - /** - * {@inheritdoc} - */ - public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) { - $setting = $this->getSettingElement($form, $form_state); - $setting->setProperty('states', [ - 'visible' => [ - ':input[name="include_deprecated"]' => ['checked' => TRUE], - ], - ]); - } - -} +class SuppressDeprecatedWarnings extends SettingBase {} diff --git a/src/Theme.php b/src/Theme.php index 3f68d68f4b9f3e24bc9661d4d55834c66548c79b..58d912730220f21c95d8e71dc75e5007c133beb2 100644 --- a/src/Theme.php +++ b/src/Theme.php @@ -63,6 +63,13 @@ class Theme { */ protected $bootstrap; + /** + * A list of available CDN Provider instances. + * + * @var \Drupal\bootstrap\Plugin\Provider\ProviderInterface[] + */ + protected $cdnProviders; + /** * Flag indicating if the theme is in "development" mode. * @@ -400,6 +407,42 @@ class Theme { return $cache[$name]; } + /** + * 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. + */ + public function getCdnProvider() { + $provider = $this->getSetting('cdn_provider'); + $providers = $this->getCdnProviders(); + return isset($providers[$provider]) ? $providers[$provider] : NULL; + } + + /** + * Retrieves all available CDN Provider instances. + * + * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface[] + * All CDN Provider instances. + */ + public function getCdnProviders() { + if (!isset($this->cdnProviders)) { + $this->cdnProviders = []; + + // 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; + } + $this->cdnProviders[$provider] = $provider_manager->get($provider, ['theme' => $this]); + } + } + } + return $this->cdnProviders; + } + /** * Retrieves the theme info. * @@ -467,44 +510,6 @@ class Theme { return $pending; } - /** - * Retrieves the CDN provider. - * - * @param string $provider - * Optional. A CDN provider name. If not set, defaults to the CDN - * provider set in the theme settings. - * - * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface|false - * A provider instance or FALSE if no provider is set. - */ - public function getProvider($provider = NULL) { - $instance = ProviderManager::load($this, $provider); - return $instance instanceof Broken || !$this->isBootstrap() ? FALSE : $instance; - } - - /** - * Retrieves all CDN providers. - * - * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface[] - * All provider instances. - */ - 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; - } - /** * Retrieves a theme setting. * @@ -559,19 +564,6 @@ class Theme { return $settings; } - /** - * Retrieves the theme's setting plugin instances. - * - * @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. - */ - public function getSettingPlugins() { - Bootstrap::deprecated(); - return $this->getSettingPlugin(); - } - /** * Retrieves the theme's cache from the database. * @@ -760,4 +752,65 @@ class Theme { return (string) $theme === $this->getName() || in_array($theme, array_keys(self::getAncestry())); } + /**************************************************************************** + * + * Deprecated methods + * + ***************************************************************************/ + + /** + * Retrieves the CDN provider. + * + * @param string $provider + * Optional. A CDN provider name. If not set, defaults to the CDN + * provider set in the theme settings. + * + * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface|false + * A provider instance or FALSE if no provider is set. + * + * @deprecated in 8.x-3.18, will be removed in a future release. + */ + public function getProvider($provider = NULL) { + $instance = ProviderManager::load($this, $provider); + return $instance instanceof Broken || !$this->isBootstrap() ? FALSE : $instance; + } + + /** + * Retrieves all CDN providers. + * + * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface[] + * All provider instances. + * + * @deprecated in 8.x-3.18, will be removed in a future release. + */ + 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; + } + + /** + * Retrieves the theme's setting plugin instances. + * + * @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. + */ + public function getSettingPlugins() { + Bootstrap::deprecated(); + return $this->getSettingPlugin(); + } + }