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