Issue #3031415 by markcarver: Overhaul CDN Providers API

parent 2f9ce16e
......@@ -78,7 +78,7 @@ function _drush_bootstrap_generate_docs_settings(Theme $bootstrap) {
];
foreach ($bootstrap->getSettingPlugin() as $setting) {
// Only get the first two groups (we don't need 3rd, or more, levels).
$_groups = array_filter(array_slice($setting->getGroups(), 0, 2, FALSE));
$_groups = array_slice(array_filter($setting->getGroups()), 0, 2, FALSE);
if (!$_groups) {
continue;
}
......
......@@ -714,7 +714,7 @@ cdn_jsdelivr_version
cdn_jsdelivr_theme
</td>
<td>
<div class="help-block">Choose the example Bootstrap Theme provided by Bootstrap or one of the Bootswatch themes.</div>
<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>
......@@ -723,6 +723,57 @@ cdn_jsdelivr_theme
---
### CDN (Content Delivery Network) > Advanced Cache
<table class="table table-striped table-responsive">
<thead>
<tr>
<th class="col-xs-3">Setting name</th>
<th>Description and default value</th>
</tr>
</thead>
<tbody>
<tr>
<td class="col-xs-3">
cdn_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>
</table>
---
### Advanced
<table class="table table-striped table-responsive">
......@@ -747,7 +798,7 @@ include_deprecated
suppress_deprecated_warnings
</td>
<td>
<div class="help-block">Enable this setting if you wish to suppress deprecated warning messages. <strong class='error text-error'>WARNING: Suppressing these messages does not "fix" the problem and you will inevitably encounter issues when they are removed in future updates. Only use this setting in extreme and necessary circumstances.</strong></div>
<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>
......
......@@ -35,7 +35,7 @@
$glyphicon.addClass('glyphicon-spin');
// Add any message as a tooltip to the glyphicon.
if (drupalSettings.bootstrap.tooltip_enabled) {
if ($.fn.tooltip && drupalSettings.bootstrap.tooltip_enabled) {
$glyphicon
.removeAttr('data-toggle')
.removeAttr('data-original-title')
......@@ -66,7 +66,7 @@
var $glyphicon = this.findGlyphicon(element);
if ($glyphicon[0]) {
$glyphicon.removeClass('glyphicon-spin');
if (drupalSettings.bootstrap.tooltip_enabled) {
if ($.fn.tooltip && drupalSettings.bootstrap.tooltip_enabled) {
$glyphicon
.removeAttr('data-toggle')
.removeAttr('data-original-title')
......
......@@ -137,19 +137,18 @@
var $cdnProvider = $context.find('select[name="cdn_provider"] :selected');
var cdnProvider = $cdnProvider.val();
if ($cdnProvider.length) {
summary.push(Drupal.t('Provider: %provider', { '%provider': $cdnProvider.text() }));
var provider = $cdnProvider.text();
// jsDelivr CDN.
if (cdnProvider === 'jsdelivr') {
var $jsDelivrVersion = $context.find('select[name="cdn_jsdelivr_version"] :selected');
if ($jsDelivrVersion.length && $jsDelivrVersion.val().length) {
summary.push($jsDelivrVersion.text());
}
var $jsDelivrTheme = $context.find('select[name="cdn_jsdelivr_theme"] :selected');
if ($jsDelivrTheme.length && $jsDelivrTheme.val() !== 'bootstrap') {
summary.push($jsDelivrTheme.text());
var $version = $context.find('select[name="cdn_' + cdnProvider + '_version"] :selected');
if ($version.length && $version.val().length) {
provider += ' - ' + $version.text();
var $theme = $context.find('select[name="cdn_' + cdnProvider + '_theme"] :selected');
if ($theme.length) {
provider += ' (' + $theme.text() + ')';
}
}
summary.push(provider);
}
return summary.join(', ');
});
......
......@@ -15,6 +15,7 @@ var Drupal = Drupal || {};
return {
DEFAULTS: {
animation: !!settings.tooltip_animation,
enabled: settings.tooltip_enabled,
html: !!settings.tooltip_html,
placement: settings.tooltip_placement,
selector: settings.tooltip_selector,
......@@ -32,6 +33,11 @@ var Drupal = Drupal || {};
*/
Drupal.behaviors.bootstrapTooltips = {
attach: function (context) {
// Immediately return if tooltips are not available.
if (!$.fn.tooltip || !$.fn.tooltip.Constructor.DEFAULTS.enabled) {
return;
}
var elements = $(context).find('[data-toggle="tooltip"]').toArray();
for (var i = 0; i < elements.length; i++) {
var $element = $(elements[i]);
......@@ -40,6 +46,11 @@ var Drupal = Drupal || {};
}
},
detach: function (context) {
// Immediately return if tooltips are not available.
if (!$.fn.tooltip || !$.fn.tooltip.Constructor.DEFAULTS.enabled) {
return;
}
// Destroy all tooltips.
$(context).find('[data-toggle="tooltip"]').tooltip('destroy');
}
......
......@@ -16,7 +16,6 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Markup;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
......@@ -1233,6 +1232,10 @@ class Bootstrap {
],
];
// 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');
$key = 'request-' . Crypt::hashBase64(serialize(['uri' => $uri] + $options));
$response = $cache->get($key);
......@@ -1258,7 +1261,7 @@ class Bootstrap {
}
// Only cache if a maximum age has been detected.
if ($response->getStatusCode() == 200 && ($maxAge = $response->getMaxAge())) {
if ($response->getStatusCode() == 200 && ($maxAge = isset($ttl) ? $ttl : $response->getMaxAge())) {
$cache->setWithExpire($key, $response, $maxAge);
}
}
......@@ -1276,15 +1279,11 @@ class Bootstrap {
* @param \Exception|null $exception
* The exception thrown if there was an error, passed by reference.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @return \Drupal\bootstrap\JsonResponse
* A JsonResponse object.
*/
public static function requestJson($uri, array $options = [], &$exception = NULL) {
$r = static::cachedRequest($uri, $options, $exception);
$json = Json::decode($r->getContent() ?: '[]') ?: [];
$response = new JsonResponse($json, $r->getStatusCode(), $r->headers->all());
$response->json = $json;
return $response;
return JsonResponse::createFromResponse(static::cachedRequest($uri, $options, $exception));
}
/**
......
<?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;
}
}
......@@ -35,7 +35,7 @@ class LibraryInfo extends PluginBase implements AlterInterface {
unset($libraries['livereload']['js']['livereload.js']);
}
// Alter the framework library based on currently set CDN provider.
// Alter the framework library based on currently set CDN Provider.
if ($cdnProvider = $this->theme->getCdnProvider()) {
$cdnProvider->alterFrameworkLibrary($libraries['framework']);
}
......
......@@ -91,7 +91,7 @@ class SystemThemeSettings extends FormBase implements FormInterface {
'#title' => $this->t('Cached HTTP requests: @count', ['@count' => $count]),
'#weight' => 100,
'#smart_description' => FALSE,
'#description' => $this->t('All HTTP requests initiated through the base-theme are cached if there is a "max-age" response header present. These cached requests will persist through cache rebuilds and only expire once the the "max-age" has been reached. If you believe a CDN Provider is not retrieving data properly, you can manually reset this cache here.'),
'#description' => $this->t('All external HTTP requests initiated by this theme are subject to caching. Cacheability is determined automatically based on a manually passed TTL value by the initiator or if there is a "max-age" response header present. These cached requests will persist through cache rebuilds and will only be requested again once they have expired. If you believe there is some request not being properly retrieved, you can manually reset this cache here.'),
'#description_display' => 'before',
'#prefix' => '<div id="reset-http-request-cache">',
'#suffix' => '</div>',
......@@ -100,6 +100,7 @@ class SystemThemeSettings extends FormBase implements FormInterface {
$form[$group]['reset_http_request_cache']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Reset HTTP Request Cache'),
'#description' => $this->t('Note: this will not reset any cached CDN data; see "Advanced Cache" in the "CDN" section.'),
'#prefix' => '<div>',
'#suffix' => '</div>',
'#submit' => [
......
......@@ -26,8 +26,8 @@ class Broken extends PluginBase implements ProviderInterface {
/**
* {@inheritdoc}
*/
public function getCacheTtl() {
return static::CACHE_TTL;
public function getCacheTtl($type) {
return static::TTL_NEVER;
}
/**
......
......@@ -3,13 +3,14 @@
namespace Drupal\bootstrap\Plugin\Provider;
/**
* The "custom" CDN provider plugin.
* The "custom" CDN Provider plugin.
*
* @ingroup plugins_provider
*
* @BootstrapProvider(
* id = "custom",
* label = @Translation("Custom"),
* description = @Translation("Allows the use of any CDN Provider by simply injecting any URLs set below.")
* )
*/
class Custom extends ProviderBase {
......@@ -17,7 +18,7 @@ class Custom extends ProviderBase {
/**
* {@inheritdoc}
*/
protected function discoverCdnAssets($version, $theme) {
protected function discoverCdnAssets($version, $theme = NULL) {
$assets = [];
foreach (['css', 'js'] as $type) {
if ($setting = $this->theme->getSetting('cdn_custom_' . $type)) {
......
......@@ -3,10 +3,9 @@
namespace Drupal\bootstrap\Plugin\Provider;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Utility\Unicode;
/**
* The "jsdelivr" CDN provider plugin.
* The "jsdelivr" CDN Provider plugin.
*
* @ingroup plugins_provider
*
......@@ -31,20 +30,6 @@ class JsDelivr extends ProviderBase {
*/
const BASE_CDN_URL = 'https://cdn.jsdelivr.net/npm';
/**
* A list of latest versions, keyed by NPM package name.
*
* @var string[]
*/
protected $latestVersion = [];
/**
* A list of themes, keyed by version.
*
* @var array[]
*/
protected $themes = [];
/**
* {@inheritdoc}
*/
......@@ -60,7 +45,7 @@ class JsDelivr extends ProviderBase {
/**
* {@inheritdoc}
*/
protected function discoverCdnAssets($version, $theme = 'bootstrap') {
protected function discoverCdnAssets($version, $theme = NULL) {
$themes = $this->getCdnThemes($version);
return isset($themes[$theme]) ? $themes[$theme] : [];
}
......@@ -68,41 +53,30 @@ class JsDelivr extends ProviderBase {
/**
* {@inheritdoc}
*/
public function getCdnThemes($version = NULL) {
if (!isset($version)) {
$version = $this->getCdnVersion();
}
if (!isset($this->themes[$version])) {
$this->themes[$version] = $this->cacheGet('themes', Unicode::escapeDelimiter($version), [], function ($themes) use ($version) {
foreach (['bootstrap', 'bootswatch'] as $package) {
$mappedVersion = $this->mapVersion($version, $package);
$files = $this->requestApiV1($package, $mappedVersion);
$themes = $this->parseThemes($files, $package, $mappedVersion, $themes);
}
return $themes;
});
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 $this->themes[$version];
return $themes;
}
/**
* {@inheritdoc}
*/
public function getCdnVersions() {
if (!isset($this->versions)) {
$this->versions = $this->cacheGet('versions', 'bootstrap', [], function ($versions) {
$json = $this->requestApiV1('bootstrap') + ['versions' => []];
foreach ($json['versions'] as $version) {
// Skip irrelevant versions.
if (!preg_match('/^' . substr(Bootstrap::FRAMEWORK_VERSION, 0, 1) . '\.\d+\.\d+$/', $version)) {
continue;
}
$versions[$version] = $version;
}
return $versions;
});
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 $this->versions;
return $versions;
}
/**
......@@ -196,7 +170,7 @@ class JsDelivr extends ProviderBase {
// Determine the "theme" name.
if ($path === 'css' || $path === 'js') {
$theme = 'bootstrap';
$title = (string) $this->t('Bootstrap');
$title = (string) $this->t('Default');
}
else {
$theme = $path;
......@@ -204,7 +178,7 @@ class JsDelivr extends ProviderBase {
}
if ($matches[2]) {
$theme = 'bootstrap_theme';
$title = (string) $this->t('Bootstrap Theme');
$title = (string) $this->t('Example Theme');
}
$themes[$theme]['title'] = $title;
......@@ -258,30 +232,39 @@ class JsDelivr extends ProviderBase {
* @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) {
protected function requestApiV1($package, $version = NULL, $ttl = NULL) {
$uri = static::BASE_API_URL . "/$package";
$options = [
// 'collection' => $this->getCacheId(),
];
$options = [];
if (isset($ttl)) {
$options['ttl'] = $ttl;
}
// If no version was passed, then all versions are returned.
if (!$version) {
$response = Bootstrap::requestJson($uri, $options);
$response = $this->requestJson($uri, $options);
$json = $response->getJson();
// If bootstrap JSON could not be returned, provide defaults.
if (!$response->json && $this->cdnExceptions && $package === 'bootstrap') {
$response->json = ['versions' => [Bootstrap::FRAMEWORK_VERSION]];
if (!$json && $this->cdnExceptions && $package === 'bootstrap') {
$json = ['versions' => [Bootstrap::FRAMEWORK_VERSION]];
}
return $response->json;
return $json;
}
$response = Bootstrap::requestJson("$uri@$version/flat", $options);
$response = $this->requestJson("$uri@$version/flat", $options);
$json = $response->getJson();
// If bootstrap JSON could not be returned, provide defaults.
if (!$response->json && $this->cdnExceptions && $package === 'bootstrap') {
if (!$json && $this->cdnExceptions && $package === 'bootstrap') {
return [
'/dist/css/bootstrap.css',
'/dist/js/bootstrap.js',
......@@ -291,7 +274,7 @@ class JsDelivr extends ProviderBase {
}
// Parse the files from JSON.
return $this->parseFiles($response->json);
return $this->parseFiles($json);
}
/**
......
This diff is collapsed.
......@@ -13,11 +13,88 @@ use Drupal\Component\Plugin\PluginInspectionInterface;
interface ProviderInterface extends PluginInspectionInterface, DerivativeInspectionInterface {
/**
* The default CDN Provider cache time-to-live (TTL) value (one week).
* Defines the "assets" cache type.
*
* @var string
*/
const CACHE_ASSETS = 'assets';
/**
* Defines the "library" cache type.
*
* @var string
*/
const CACHE_LIBRARY = 'library';
/**
* Defines the "themes" cache type.
*
* @var string
*/
const CACHE_THEMES = 'themes';
/**
* Defines the "versions" cache type.
*
* @var string
*/
const CACHE_VERSIONS = 'versions';
/**
* Defines the "forever" time-to-live (TTL) value.
*
* @var int
*/
const TTL_FOREVER = -1;
/**
* Defines the "never" time-to-live (TTL) value.
*
* @var int
*/
const TTL_NEVER = 0;
/**
* Defines the "one day" time-to-live (TTL) value.
*
* @var int
*/
const TTL_ONE_DAY = 86400;
/**
* Defines the "one week" time-to-live (TTL) value.
*
* @var int
*/
const TTL_ONE_WEEK = 604800;
/**
* Defines the "one month" time-to-live (TTL) value.
*
* @var int
*/
const TTL_ONE_MONTH = 2630000;
/**
* Defines the "three months" time-to-live (TTL) value.
*
* @var int
*/
const TTL_THREE_MONTHS = 7776000;
/**
* Defines the "six months" time-to-live (TTL) value.
*
* @var int
*/
const CACHE_TTL = 604800;
const TTL_SIX_MONTHS = 15780000;
/**
* Defines the "one year" time-to-live (TTL) value.
*
* @var int
*/
const TTL_ONE_YEAR = 31536000;
/**
* Alters the framework library.
......@@ -33,10 +110,18 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
/**
* Retrieves the cache time-to-live (TTL) value.
*
* @param string $type
* The type of cache TTL value. Can be one of the following types:
* - \Drupal\bootstrap\Plugin\Provider\ProviderInterface::CACHE_ASSETS
* - \Drupal\bootstrap\Plugin\Provider\ProviderInterface::CACHE_LIBRARY
* - \Drupal\bootstrap\Plugin\Provider\ProviderInterface::CACHE_THEMES
* - \Drupal\bootstrap\Plugin\Provider\ProviderInterface::CACHE_VERSIONS
* If an invalid type was specified, the resulting TTL value will be 0.
*
* @return int
* The cache expire value, in seconds.
* The cache TTL value, in seconds.
*/
public function getCacheTtl();
public function getCacheTtl($type);
/**
* Retrieves the assets from the CDN, if any.
......@@ -59,22 +144,6 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
*/
public function getCdnAssets($version = NULL, $theme = NULL);
/**
* Retrieves the provider description.
*
* @return string
* The provider description.
*/
public function getDescription();
/**
* Retrieves the provider human-readable label.
*
* @return string
* The provider human-readable label.
*/
public function getLabel();
/**
* Retrieves any CDN ProviderException objects triggered during discovery.
*
......@@ -91,43 +160,59 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
public function getCdnExceptions($reset = TRUE);
/**
* Retrieves the currently set CDN provider theme.
* Retrieves the currently set CDN Provider theme.
*
* @return string
* The currently set CDN provider theme.
* The currently set CDN Provider theme.
*/
public function getCdnTheme();
/**
* Retrieves the themes supported by the CDN provider.
* Retrieves the themes supported by the CDN Provider.
*
* @param string $version
* 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
* An array of themes. If the CDN Provider does not support any it will
* just be an empty array.
*/
public function getCdnThemes($version = NULL);
/**
* Retrieves the currently set CDN provider version.
* Retrieves the currently set CDN Provider version.
*
* @return string
* The currently set CDN provider version.
* The currently set CDN Provider version.
*/
public function getCdnVersion();
/**
* Retrieves the versions supported by the CDN provider.
* Retrieves the versions supported by the CDN Provider.
*
* @return array
* An array of versions. If the CDN provider does not support any it will
* An array of versions. If the CDN Provider does not support any it will
* just be an empty array.
*/
public function getCdnVersions();
/**
* Retrieves the provider description.
*
* @return string
* The provider description.
*/
public function getDescription();
/**
* Retrieves the provider human-readable label.
*
* @return string
* The provider human-readable label.
*/
public function getLabel();
/**
* Removes any cached data the CDN Provider may have.
*/
......@@ -169,10 +254,10 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
public function getAssets($types = NULL);
/**
* Retrieves the themes supported by the CDN provider.
* Retrieves the themes supported by the CDN Provider.
*
* @return array
* An array of themes. If the CDN provider does not support any it will
* An array of themes. If the CDN Provider does not support any it will
* just be an empty array.
*
* @deprecated in 8.x-3.18, will be removed in a future release.
......@@ -182,10 +267,10 @@ interface ProviderInterface extends PluginInspectionInterface, DerivativeInspect
public function getThemes();
/**
* Retrieves the versions supported by the CDN provider.
* Retrieves the versions supported by the CDN Provider.
*
* @return array
* An array of versions. If the CDN provider does not support any it will
* An array of versions. If the CDN Provider does not support any it will
* just be an empty array.
*
* @deprecated in 8.x-3.18, will be removed in a future release.
......
......@@ -7,13 +7,13 @@ use Drupal\bootstrap\Theme;