summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMark Carver2019-02-17 00:08:07 (GMT)
committerMark Carver2019-02-17 00:08:07 (GMT)
commitfd18ce46842ca3540ca457e1e5c104529bb0fd19 (patch)
tree2c9b6018173822d2956c601e8c98c26750496e6b
parent6aa33213ffef8a3bbdf090679138cb02629a9172 (diff)
Issue #3031415 by markcarver: Overhaul CDN Providers API
-rw-r--r--bootstrap.drush.inc74
-rw-r--r--deprecated.php34
-rw-r--r--docs/Sub-Theming.md17
-rw-r--r--docs/Theme-Settings.md596
-rw-r--r--js/theme-settings.js29
-rw-r--r--src/Annotation/BootstrapProvider.php4
-rw-r--r--src/Bootstrap.php129
-rw-r--r--src/Plugin/Alter/LibraryInfo.php4
-rw-r--r--src/Plugin/Form/SystemThemeSettings.php58
-rw-r--r--src/Plugin/Provider/Broken.php55
-rw-r--r--src/Plugin/Provider/JsDelivr.php50
-rw-r--r--src/Plugin/Provider/ProviderBase.php314
-rw-r--r--src/Plugin/Provider/ProviderException.php44
-rw-r--r--src/Plugin/Provider/ProviderInterface.php39
-rw-r--r--src/Plugin/Setting/Advanced/Cdn/CdnCustomCss.php15
-rw-r--r--src/Plugin/Setting/Advanced/Cdn/CdnCustomCssMin.php15
-rw-r--r--src/Plugin/Setting/Advanced/Cdn/CdnCustomJs.php15
-rw-r--r--src/Plugin/Setting/Advanced/Cdn/CdnCustomJsMin.php15
-rw-r--r--src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrTheme.php29
-rw-r--r--src/Plugin/Setting/Advanced/Cdn/CdnJsdelivrVersion.php38
-rw-r--r--src/Plugin/Setting/Advanced/Cdn/CdnProvider.php119
-rw-r--r--src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php164
-rw-r--r--src/Plugin/Setting/Advanced/SuppressDeprecatedWarnings.php18
-rw-r--r--src/Theme.php155
24 files changed, 1262 insertions, 768 deletions
diff --git a/bootstrap.drush.inc b/bootstrap.drush.inc
index 1d94fa1..d87978f 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[] = '<table class="table table-striped table-responsive">';
- $output[] = ' <thead>';
- $output[] = ' <tr>';
- $output[] = ' <th class="col-xs-3">Setting name</th>';
- $output[] = ' <th>Description and default value</th>';
- $output[] = ' </tr>';
- $output[] = ' </thead>';
- $output[] = ' <tbody>';
- foreach ($settings as $definition) {
- $output[] = ' <tr>';
- $output[] = ' <td class="col-xs-3">';
- $output[] = $definition['id'];
- $output[] = ' </td>';
- $output[] = ' <td>';
- $output[] = ' <div class="help-block">';
- $output[] = str_replace('&quote;', '"', wordwrap($definition['description']));
- $output[] = ' </div>';
- $output[] = ' <pre class=" language-yaml"><code>';
- $output[] = wordwrap(Yaml::encode([$definition['id'] => $definition['defaultValue']]));
- $output[] = '</code></pre>';
- $output[] = ' </td>';
- $output[] = ' </tr>';
+ foreach ($groups as $subgroups) {
+ foreach ($subgroups as $group => $settings) {
+ $output[] = '';
+ $output[] = '---';
+ $output[] = '';
+ $output[] = "### $group";
+ $output[] = '';
+ $output[] = '<table class="table table-striped table-responsive">';
+ $output[] = ' <thead>';
+ $output[] = ' <tr>';
+ $output[] = ' <th class="col-xs-3">Setting name</th>';
+ $output[] = ' <th>Description and default value</th>';
+ $output[] = ' </tr>';
+ $output[] = ' </thead>';
+ $output[] = ' <tbody>';
+ foreach ($settings as $definition) {
+ $output[] = ' <tr>';
+ $output[] = ' <td class="col-xs-3">';
+ $output[] = $definition['id'];
+ $output[] = ' </td>';
+ $output[] = ' <td>';
+ $output[] = ' <div class="help-block">';
+ $output[] = str_replace('&quote;', '"', wordwrap($definition['description']));
+ $output[] = ' </div>';
+ $output[] = ' <pre class=" language-yaml"><code>';
+ $output[] = wordwrap(Yaml::encode([$definition['id'] => $definition['defaultValue']]));
+ $output[] = '</code></pre>';
+ $output[] = ' </td>';
+ $output[] = ' </tr>';
+ }
+ $output[] = ' </tbody>';
+ $output[] = '</table>';
}
- $output[] = ' </tbody>';
- $output[] = '</table>';
}
// Ensure we have link references at the bottom.
diff --git a/deprecated.php b/deprecated.php
index 11ebc04..2d7ca5b 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 699fd1e..bb98eac 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 d772dca..648955d 100644
--- a/docs/Theme-Settings.md
+++ b/docs/Theme-Settings.md
@@ -13,7 +13,7 @@ SETTING_NAME: SETTING_VALUE
---
-### Advanced
+### General > Buttons
<table class="table table-striped table-responsive">
<thead>
@@ -25,33 +25,42 @@ SETTING_NAME: SETTING_VALUE
<tbody>
<tr>
<td class="col-xs-3">
-include_deprecated
+button_colorize
</td>
<td>
<div class="help-block">
-Enabling this setting will include any <code>deprecated.php</code> file
-found in your theme or base themes.
+Adds classes to buttons based on their text value.
</div>
<pre class=" language-yaml"><code>
-include_deprecated: 0
+button_colorize: 1
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-suppress_deprecated_warnings
+button_iconize
</td>
<td>
<div class="help-block">
-Enable this setting if you wish to suppress deprecated warning messages.
-<strong class='error text-error'>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.</strong>
+Adds icons to buttons based on the text value
</div>
<pre class=" language-yaml"><code>
-suppress_deprecated_warnings: 0
+button_iconize: 1
+
+</code></pre>
+ </td>
+ </tr>
+ <tr>
+ <td class="col-xs-3">
+button_size
+ </td>
+ <td>
+ <div class="help-block">
+Defines the Bootstrap Buttons specific size
+ </div>
+ <pre class=" language-yaml"><code>
+button_size: ''
</code></pre>
</td>
@@ -61,7 +70,7 @@ suppress_deprecated_warnings: 0
---
-### Advanced > CDN (Content Delivery Network)
+### General > Container
<table class="table table-striped table-responsive">
<thead>
@@ -73,166 +82,110 @@ suppress_deprecated_warnings: 0
<tbody>
<tr>
<td class="col-xs-3">
-cdn_provider
- </td>
- <td>
- <div class="help-block">
-Choose between jsdelivr or a custom cdn source.
- </div>
- <pre class=" language-yaml"><code>
-cdn_provider: jsdelivr
-
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-cdn_custom_css
+fluid_container
</td>
<td>
<div class="help-block">
-It is best to use <code>https</code> protocols here as it will allow more
-flexibility if the need ever arises.
+Uses the <code>.container-fluid</code> class instead of
+<code>.container</code>.
</div>
<pre class=" language-yaml"><code>
-cdn_custom_css:
-'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.css'
+fluid_container: 0
</code></pre>
</td>
</tr>
- <tr>
- <td class="col-xs-3">
-cdn_custom_css_min
- </td>
- <td>
- <div class="help-block">
-Additionally, you can provide the minimized version of the file. It will be
-used instead if site aggregation is enabled.
- </div>
- <pre class=" language-yaml"><code>
-cdn_custom_css_min:
-'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css'
+ </tbody>
+</table>
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-cdn_custom_js
- </td>
- <td>
- <div class="help-block">
-It is best to use <code>https</code> protocols here as it will allow more
-flexibility if the need ever arises.
- </div>
- <pre class=" language-yaml"><code>
-cdn_custom_js:
-'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.js'
+---
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-cdn_custom_js_min
- </td>
- <td>
- <div class="help-block">
-Additionally, you can provide the minimized version of the file. It will be
-used instead if site aggregation is enabled.
- </div>
- <pre class=" language-yaml"><code>
-cdn_custom_js_min:
-'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js'
+### General > Forms
-</code></pre>
- </td>
- </tr>
+<table class="table table-striped table-responsive">
+ <thead>
+ <tr>
+ <th class="col-xs-3">Setting name</th>
+ <th>Description and default value</th>
+ </tr>
+ </thead>
+ <tbody>
<tr>
<td class="col-xs-3">
-cdn_jsdelivr_version
+forms_has_error_value_toggle
</td>
<td>
<div class="help-block">
-Choose the Bootstrap version from jsdelivr
+If an element has a <code>.has-error</code> class attached to it, enabling
+this will automatically remove that class when a value is entered.
</div>
<pre class=" language-yaml"><code>
-cdn_jsdelivr_version: 3.4.1
+forms_has_error_value_toggle: 1
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-cdn_jsdelivr_theme
+forms_required_has_error
</td>
<td>
<div class="help-block">
-Choose the example Bootstrap Theme provided by Bootstrap or one of the
-Bootswatch themes.
+If an element in a form is required, enabling this will always display the
+element with a <code>.has-error</code> class. This turns the element red
+and helps in usability for determining which form elements are required to
+submit the form.
</div>
<pre class=" language-yaml"><code>
-cdn_jsdelivr_theme: bootstrap
+forms_required_has_error: 0
</code></pre>
</td>
</tr>
- </tbody>
-</table>
-
----
-
-### Components > Breadcrumbs
-
-<table class="table table-striped table-responsive">
- <thead>
- <tr>
- <th class="col-xs-3">Setting name</th>
- <th>Description and default value</th>
- </tr>
- </thead>
- <tbody>
<tr>
<td class="col-xs-3">
-breadcrumb
+forms_smart_descriptions
</td>
<td>
<div class="help-block">
-Show or hide the Breadcrumbs
+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.
</div>
<pre class=" language-yaml"><code>
-breadcrumb: '1'
+forms_smart_descriptions: 1
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-breadcrumb_home
+forms_smart_descriptions_allowed_tags
</td>
<td>
<div class="help-block">
-If your site has a module dedicated to handling breadcrumbs already, ensure
-this setting is enabled.
+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.
</div>
<pre class=" language-yaml"><code>
-breadcrumb_home: 0
+forms_smart_descriptions_allowed_tags: 'b, code, em, i, kbd, span, strong'
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-breadcrumb_title
+forms_smart_descriptions_limit
</td>
<td>
<div class="help-block">
-If your site has a module dedicated to handling breadcrumbs already, ensure
-this setting is disabled.
+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.
</div>
<pre class=" language-yaml"><code>
-breadcrumb_title: 1
+forms_smart_descriptions_limit: '250'
</code></pre>
</td>
@@ -242,7 +195,7 @@ breadcrumb_title: 1
---
-### Components > Navbar
+### General > Images
<table class="table table-striped table-responsive">
<thead>
@@ -254,28 +207,32 @@ breadcrumb_title: 1
<tbody>
<tr>
<td class="col-xs-3">
-navbar_inverse
+image_responsive
</td>
<td>
<div class="help-block">
-Select if you want the inverse navbar style.
+Images in Bootstrap 3 can be made responsive-friendly via the addition of
+the <code>.img-responsive</code> class. This applies <code>max-width:
+100%;</code> and <code>height: auto;</code> to the image so that it scales
+nicely to the parent element.
</div>
<pre class=" language-yaml"><code>
-navbar_inverse: 0
+image_responsive: 1
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-navbar_position
+image_shape
</td>
<td>
<div class="help-block">
-Determines where the navbar is positioned on the page.
+Add classes to an <code>&lt;img&gt;</code> element to easily style images
+in any project.
</div>
<pre class=" language-yaml"><code>
-navbar_position: ''
+image_shape: ''
</code></pre>
</td>
@@ -285,7 +242,7 @@ navbar_position: ''
---
-### Components > Region Wells
+### General > Tables
<table class="table table-striped table-responsive">
<thead>
@@ -297,111 +254,75 @@ navbar_position: ''
<tbody>
<tr>
<td class="col-xs-3">
-region_wells
+table_bordered
</td>
<td>
<div class="help-block">
-Enable the <code>.well</code>, <code>.well-sm</code> or
-<code>.well-lg</code> classes for specified regions.
+Add borders on all sides of the table and cells.
</div>
<pre class=" language-yaml"><code>
-region_wells:
- navigation: ''
- navigation_collapsible: ''
- header: ''
- highlighted: ''
- help: ''
- content: ''
- sidebar_first: ''
- sidebar_second: well
- footer: ''
+table_bordered: 0
</code></pre>
</td>
</tr>
- </tbody>
-</table>
-
----
-
-### General > Buttons
-
-<table class="table table-striped table-responsive">
- <thead>
- <tr>
- <th class="col-xs-3">Setting name</th>
- <th>Description and default value</th>
- </tr>
- </thead>
- <tbody>
<tr>
<td class="col-xs-3">
-button_colorize
+table_condensed
</td>
<td>
<div class="help-block">
-Adds classes to buttons based on their text value.
+Make tables more compact by cutting cell padding in half.
</div>
<pre class=" language-yaml"><code>
-button_colorize: 1
+table_condensed: 0
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-button_iconize
+table_hover
</td>
<td>
<div class="help-block">
-Adds icons to buttons based on the text value
+Enable a hover state on table rows.
</div>
<pre class=" language-yaml"><code>
-button_iconize: 1
+table_hover: 1
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-button_size
+table_striped
</td>
<td>
<div class="help-block">
-Defines the Bootstrap Buttons specific size
+Add zebra-striping to any table row within the <code>&lt;tbody&gt;</code>.
</div>
<pre class=" language-yaml"><code>
-button_size: ''
+table_striped: 1
</code></pre>
</td>
</tr>
- </tbody>
-</table>
-
----
-
-### General > Container
-
-<table class="table table-striped table-responsive">
- <thead>
- <tr>
- <th class="col-xs-3">Setting name</th>
- <th>Description and default value</th>
- </tr>
- </thead>
- <tbody>
<tr>
<td class="col-xs-3">
-fluid_container
+table_responsive
</td>
<td>
<div class="help-block">
-Uses the <code>.container-fluid</code> class instead of
-<code>.container</code>.
+Wraps tables with <code>.table-responsive</code> 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 <code>Automatic</code> option will only apply
+this setting for front-end facing tables, not the tables in administrative
+areas.
</div>
<pre class=" language-yaml"><code>
-fluid_container: 0
+table_responsive: -1
</code></pre>
</td>
@@ -411,7 +332,7 @@ fluid_container: 0
---
-### General > Forms
+### Components > Breadcrumbs
<table class="table table-striped table-responsive">
<thead>
@@ -423,80 +344,44 @@ fluid_container: 0
<tbody>
<tr>
<td class="col-xs-3">
-forms_has_error_value_toggle
- </td>
- <td>
- <div class="help-block">
-If an element has a <code>.has-error</code> class attached to it, enabling
-this will automatically remove that class when a value is entered.
- </div>
- <pre class=" language-yaml"><code>
-forms_has_error_value_toggle: 1
-
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-forms_required_has_error
- </td>
- <td>
- <div class="help-block">
-If an element in a form is required, enabling this will always display the
-element with a <code>.has-error</code> class. This turns the element red
-and helps in usability for determining which form elements are required to
-submit the form.
- </div>
- <pre class=" language-yaml"><code>
-forms_required_has_error: 0
-
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-forms_smart_descriptions
+breadcrumb
</td>
<td>
<div class="help-block">
-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.
+Show or hide the Breadcrumbs
</div>
<pre class=" language-yaml"><code>
-forms_smart_descriptions: 1
+breadcrumb: '1'
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-forms_smart_descriptions_allowed_tags
+breadcrumb_home
</td>
<td>
<div class="help-block">
-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.
+If your site has a module dedicated to handling breadcrumbs already, ensure
+this setting is enabled.
</div>
<pre class=" language-yaml"><code>
-forms_smart_descriptions_allowed_tags: 'b, code, em, i, kbd, span, strong'
+breadcrumb_home: 0
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-forms_smart_descriptions_limit
+breadcrumb_title
</td>
<td>
<div class="help-block">
-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.
+If your site has a module dedicated to handling breadcrumbs already, ensure
+this setting is disabled.
</div>
<pre class=" language-yaml"><code>
-forms_smart_descriptions_limit: '250'
+breadcrumb_title: 1
</code></pre>
</td>
@@ -506,7 +391,7 @@ forms_smart_descriptions_limit: '250'
---
-### General > Images
+### Components > Navbar
<table class="table table-striped table-responsive">
<thead>
@@ -518,32 +403,28 @@ forms_smart_descriptions_limit: '250'
<tbody>
<tr>
<td class="col-xs-3">
-image_responsive
+navbar_inverse
</td>
<td>
<div class="help-block">
-Images in Bootstrap 3 can be made responsive-friendly via the addition of
-the <code>.img-responsive</code> class. This applies <code>max-width:
-100%;</code> and <code>height: auto;</code> to the image so that it scales
-nicely to the parent element.
+Select if you want the inverse navbar style.
</div>
<pre class=" language-yaml"><code>
-image_responsive: 1
+navbar_inverse: 0
</code></pre>
</td>
</tr>
<tr>
<td class="col-xs-3">
-image_shape
+navbar_position
</td>
<td>
<div class="help-block">
-Add classes to an <code>&lt;img&gt;</code> element to easily style images
-in any project.
+Determines where the navbar is positioned on the page.
</div>
<pre class=" language-yaml"><code>
-image_shape: ''
+navbar_position: ''
</code></pre>
</td>
@@ -553,7 +434,7 @@ image_shape: ''
---
-### General > Tables
+### Components > Region Wells
<table class="table table-striped table-responsive">
<thead>
@@ -565,75 +446,24 @@ image_shape: ''
<tbody>
<tr>
<td class="col-xs-3">
-table_bordered
- </td>
- <td>
- <div class="help-block">
-Add borders on all sides of the table and cells.
- </div>
- <pre class=" language-yaml"><code>
-table_bordered: 0
-
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-table_condensed
- </td>
- <td>
- <div class="help-block">
-Make tables more compact by cutting cell padding in half.
- </div>
- <pre class=" language-yaml"><code>
-table_condensed: 0
-
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-table_hover
- </td>
- <td>
- <div class="help-block">
-Enable a hover state on table rows.
- </div>
- <pre class=" language-yaml"><code>
-table_hover: 1
-
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-table_striped
- </td>
- <td>
- <div class="help-block">
-Add zebra-striping to any table row within the <code>&lt;tbody&gt;</code>.
- </div>
- <pre class=" language-yaml"><code>
-table_striped: 1
-
-</code></pre>
- </td>
- </tr>
- <tr>
- <td class="col-xs-3">
-table_responsive
+region_wells
</td>
<td>
<div class="help-block">
-Wraps tables with <code>.table-responsive</code> 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 <code>Automatic</code> option will only apply
-this setting for front-end facing tables, not the tables in administrative
-areas.
+Enable the <code>.well</code>, <code>.well-sm</code> or
+<code>.well-lg</code> classes for specified regions.
</div>
<pre class=" language-yaml"><code>
-table_responsive: -1
+region_wells:
+ navigation: ''
+ navigation_collapsible: ''
+ header: ''
+ highlighted: ''
+ help: ''
+ content: ''
+ sidebar_first: ''
+ sidebar_second: well
+ footer: ''
</code></pre>
</td>
@@ -1130,5 +960,175 @@ tooltip_trigger: hover
</tbody>
</table>
+---
+
+### CDN (Content Delivery Network)
+
+<table class="table table-striped table-responsive">
+ <thead>
+ <tr>
+ <th class="col-xs-3">Setting name</th>
+ <th>Description and default value</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="col-xs-3">
+cdn_provider
+ </td>
+ <td>
+ <div class="help-block">
+Choose the CDN Provider used to load Bootstrap resources.
+ </div>
+ <pre class=" language-yaml"><code>
+cdn_provider: jsdelivr
+
+</code></pre>
+ </td>
+ </tr>
+ <tr>
+ <td class="col-xs-3">
+cdn_custom_css
+ </td>
+ <td>
+ <div class="help-block">
+It is best to use <code>https</code> protocols here as it will allow more
+flexibility if the need ever arises.
+ </div>
+ <pre class=" language-yaml"><code>
+cdn_custom_css:
+'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.css'
+
+</code></pre>
+ </td>
+ </tr>
+ <tr>
+ <td class="col-xs-3">
+cdn_custom_css_min
+ </td>
+ <td>
+ <div class="help-block">
+Additionally, you can provide the minimized version of the file. It will be
+used instead if site aggregation is enabled.
+ </div>
+ <pre class=" language-yaml"><code>
+cdn_custom_css_min:
+'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css'
+
+</code></pre>
+ </td>
+ </tr>
+ <tr>
+ <td class="col-xs-3">
+cdn_custom_js
+ </td>
+ <td>
+ <div class="help-block">
+It is best to use <code>https</code> protocols here as it will allow more
+flexibility if the need ever arises.
+ </div>
+ <pre class=" language-yaml"><code>
+cdn_custom_js:
+'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.js'
+
+</code></pre>
+ </td>
+ </tr>
+ <tr>
+ <td class="col-xs-3">
+cdn_custom_js_min
+ </td>
+ <td>
+ <div class="help-block">
+Additionally, you can provide the minimized version of the file. It will be
+used instead if site aggregation is enabled.
+ </div>
+ <pre class=" language-yaml"><code>
+cdn_custom_js_min:
+'https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js'
+
+</code></pre>
+ </td>
+ </tr>
+ <tr>
+ <td class="col-xs-3">
+cdn_jsdelivr_version
+ </td>
+ <td>
+ <div class="help-block">
+Choose the Bootstrap version from jsdelivr
+ </div>
+ <pre class=" language-yaml"><code>
+cdn_jsdelivr_version: 3.4.1
+
+</code></pre>
+ </td>
+ </tr>
+ <tr>
+ <td class="col-xs-3">
+cdn_jsdelivr_theme
+ </td>
+ <td>
+ <div class="help-block">
+Choose the example Bootstrap Theme provided by Bootstrap or one of the
+Bootswatch themes.
+ </div>
+ <pre class=" language-yaml"><code>
+cdn_jsdelivr_theme: bootstrap
+
+</code></pre>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+---
+
+### Advanced
+
+<table class="table table-striped table-responsive">
+ <thead>
+ <tr>
+ <th class="col-xs-3">Setting name</th>
+ <th>Description and default value</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="col-xs-3">
+include_deprecated
+ </td>
+ <td>
+ <div class="help-block">
+Enabling this setting will include any <code>deprecated.php</code> file
+found in your theme or base themes.
+ </div>
+ <pre class=" language-yaml"><code>
+include_deprecated: 0
+
+</code></pre>
+ </td>
+ </tr>
+ <tr>
+ <td class="col-xs-3">
+suppress_deprecated_warnings
+ </td>
+ <td>
+ <div class="help-block">
+Enable this setting if you wish to suppress deprecated warning messages.
+<strong class='error text-error'>WARNING: Suppressing these messages does
+not &quot;fix&quot; the problem and you will inevitably encounter issues
+when they are removed in future updates. Only use this setting in extreme
+and necessary circumstances.</strong>
+ </div>
+ <pre class=" language-yaml"><code>
+suppress_deprecated_warnings: 0
+
+</code></pre>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
[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 12c72cc..3b6d017 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 06e132d..a8fae6a 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 cb37030..42d3b38 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.
@@ -93,6 +99,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.
*
* @var string
@@ -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: <a href=":url" target="_blank">@title</a>', [
- ':url' => self::apiSearchUrl($class . $caller['function']),
- '@title' => ($class ? $caller['class'] . '::' : '') . $caller['function'] . '()',
+ $message = t('The following @type has been deprecated: <a href=":url" target="_blank">@title</a>. 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',
@@ -1185,6 +1213,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.
*
* @param string|array $value
diff --git a/src/Plugin/Alter/LibraryInfo.php b/src/Plugin/Alter/LibraryInfo.php
index 844cd3f..0256495 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 1e2c66a..c6f5cd4 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,10 +81,67 @@ 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' => '<div id="reset-http-request-cache">',
+ '#suffix' => '</div>',
+ ];
+
+ $form[$group]['reset_http_request_cache']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Reset HTTP Request Cache'),
+ '#prefix' => '<div>',
+ '#suffix' => '</div>',
+ '#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.
*
* @param \Drupal\bootstrap\Utility\Element $form
diff --git a/src/Plugin/Provider/Broken.php b/src/Plugin/Provider/Broken.php
index 351e73f..7ca63a6 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;
}
/**
@@ -40,6 +40,20 @@ class Broken extends PluginBase implements ProviderInterface {
/**
* {@inheritdoc}
*/
+ public function getCdnExceptions($reset = TRUE) {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCdnTheme() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function getCdnThemes($version = NULL) {
return [];
}
@@ -47,6 +61,13 @@ class Broken extends PluginBase implements ProviderInterface {
/**
* {@inheritdoc}
*/
+ public function getCdnVersion() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function getCdnVersions() {
return [];
}
@@ -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 d85003b..bb6fe26 100644
--- a/src/Plugin/Provider/JsDelivr.php
+++ b/src/Plugin/Provider/JsDelivr.php
@@ -39,20 +39,13 @@ 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}
*/
public function getDescription() {
@@ -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 b02614a..736bdc3 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.
@@ -30,6 +28,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.
*
* @var array
@@ -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);
});
}
@@ -246,6 +273,17 @@ class ProviderBase extends PluginBase implements ProviderInterface {
/**
* {@inheritdoc}
*/
+ public function getCdnExceptions($reset = TRUE) {
+ $exceptions = $this->cdnExceptions;
+ if ($reset) {
+ $this->cdnExceptions = [];
+ }
+ return $exceptions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function getCdnTheme() {
return $this->theme->getSetting("cdn_{$this->getPluginId()}_theme") ?: 'bootstrap';
}
@@ -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'];
}
@@ -417,6 +413,26 @@ class ProviderBase extends PluginBase implements ProviderInterface {
*
* @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}
+ *
+ * @deprecated in 8.x-3.18, will be removed in a future release.
+ */
public function processDefinition(array &$definition, $plugin_id) {
// Due to code recursion and the need to keep this code in place for BC
// reasons, this deprecated message should only be logged and not shown.
@@ -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 0000000..504a86b
--- /dev/null
+++ b/src/Plugin/Provider/ProviderException.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\bootstrap\Plugin\Provider;
+
+/**
+ * Class ProviderException.
+ */
+class ProviderException extends \RuntimeException {
+
+ /**
+ * The CDN Provider that threw the exception.
+ *
+ * @var \Drupal\bootstrap\Plugin\Provider\ProviderInterface
+ */
+ protected $provider;
+
+ /**
+ * ProviderException constructor.
+ *
+ * @param \Drupal\bootstrap\Plugin\Provider\ProviderInterface $provider
+ * The CDN Provider that threw the exception.
+ * @param string $message
+ * The exception message.
+ * @param int $code
+ * The exception code.
+ * @param \Throwable $previous
+ * A previous exception.
+ */
+ public function __construct(ProviderInterface $provider, $message = "", int $code = 0, \Throwable $previous = NULL) {
+ parent::__construct($message, $code, $previous);
+ $this->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 de55dd4..373e42c 100644
--- a/src/Plugin/Provider/ProviderInterface.php
+++ b/src/Plugin/Provider/ProviderInterface.php
@@ -13,6 +13,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.
*
* @param array $framework
@@ -24,6 +31,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.
*
* @param string $version
@@ -61,6 +76,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.
*
* @return string
@@ -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 e8437ad..0c87147 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 <code>https</code> 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 5f61c3b..6894481 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 c88936c..8b5e931 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 <code>https</code> 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 c571d54..ccf49fa 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 ae70749..76d9eb1 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', '<div id="bootstrap-theme-preview"></div>');
- $setting->setProperty('description', t('Choose the example <a href=":bootstrap_theme" target="_blank">Bootstrap Theme</a> provided by Bootstrap or one of the many, many <a href=":bootswatch" target="_blank">Bootswatch</a> 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', '<div id="bootstrap-theme-preview"></div>');
+
+ 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 <a href=":bootstrap_theme" target="_blank">Bootstrap Theme</a> provided by Bootstrap or one of the many, many <a href=":bootswatch" target="_blank">Bootswatch</a> 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 d15b25c..a372787 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 a0741db..fa7b0f4 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. <strong>Do not do both</strong>.');
- $group->setProperty('description', '<div class="alert alert-info messages warning"><strong>' . $description_label . ':</strong> ' . $description . '</div>');
- $group->setProperty('open', !!$default_provider);
+ $group->setProperty('prefix', '<div id="cdn-providers">');
+ $group->setProperty('suffix', '</div>');
// 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' => '<div id="cdn-provider-' . $plugin_id . '">',
+ '#prefix' => '<div id="cdn-provider-' . $plugin_id . '" class="form-group">',
'#suffix' => '</div>',
- '#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.<br /><br /><a href=":provider_api" target="_blank">:provider_api</a>.', [
'@title' => $provider->getLabel(),
@@ -178,19 +170,12 @@ 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 0000000..3aacd55
--- /dev/null
+++ b/src/Plugin/Setting/Advanced/Cdn/CdnProviderBase.php
@@ -0,0 +1,164 @@
+<?php
+
+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\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * A base class for CDN Provider settings.
+ *
+ * @ingroup plugins_provider
+ * @ingroup plugins_setting
+ */
+abstract class CdnProviderBase 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);
+ }
+
+ /**
+ * {@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 <a href=":logs">logs</a> 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 c61311d..6e0c53e 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 3f68d68..58d9127 100644
--- a/src/Theme.php
+++ b/src/Theme.php
@@ -64,6 +64,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.
*
* @var bool
@@ -401,6 +408,42 @@ 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.
+ */
+ 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.
*
* @param string $property
@@ -468,44 +511,6 @@ class Theme {
}
/**
- * 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.
*
* @param string $name
@@ -560,19 +565,6 @@ class Theme {
}
/**
- * 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.
*
* @return \Drupal\bootstrap\Utility\Storage
@@ -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();
+ }
+
}