bootstrap3-1.0.1/src/Plugin/Provider/ProviderBase.php
src/Plugin/Provider/ProviderBase.php
<?php
namespace Drupal\bootstrap3\Plugin\Provider;
use Drupal\bootstrap3\Bootstrap;
use Drupal\bootstrap3\Plugin\PluginBase;
use Drupal\bootstrap3\Plugin\ProviderManager;
use Drupal\bootstrap3\Utility\Crypt;
use Drupal\bootstrap3\Utility\Unicode;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
/**
* CDN Provider base class.
*
* @ingroup plugins_provider
*/
class ProviderBase extends PluginBase implements ProviderInterface {
/**
* The currently set assets.
*
* @var array
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $assets = [];
/**
* The cache backend used for storing various permanent CDN Provider data.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValue;
/**
* The cache backend used for storing various expirable CDN Provider data.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
*/
protected $keyValueExpirable;
/**
* The cache TTL values, in seconds, keyed by type.
*
* @var int[]
*
* @see \Drupal\bootstrap3\Plugin\Provider\ProviderInterface
*/
protected $cacheTtl = [];
/**
* The currently set CDN assets, keyed by a hash identifier.
*
* @var \Drupal\bootstrap3\Plugin\Provider\CdnAssets[]
*/
protected $cdnAssets;
/**
* A list of currently set Exception objects.
*
* @var \Drupal\bootstrap3\Plugin\Provider\ProviderException[]
*/
protected $cdnExceptions = [];
/**
* The versions supplied by the CDN Provider.
*
* @var array
*/
protected $versions;
/**
* The themes supplied by the CDN Provider, keyed by version.
*
* @var array[]
*/
protected $themes = [];
/**
* Adds a new CDN Provider exception.
*
* @param \Throwable $exception
* The exception message.
*/
protected function addCdnException(\Throwable $exception) {
$this->cdnExceptions[] = new ProviderException($this, $exception->getMessage(), $exception->getCode(), $exception);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap3\Plugin\Provider\ProviderBase::getCdnAssetsCacheData()
*/
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 data relating to the CDN.
// The cache is rebuilt if and when it has expired.
// @see https://www.drupal.org/project/bootstrap/issues/3031415
$data = $this->getCdnAssetsCacheData();
$hash = Crypt::generateBase64HashIdentifier($data);
// Retrieve the cached value or build it if necessary.
$framework = $this->cacheGet('library', $hash, [], function () use ($framework, $data) {
$version = $data['version'] ?? NULL;
$theme = $data['theme'] ?? NULL;
$assets = $this->getCdnAssets($version, $theme)->toLibraryArray($data['min']);
// Immediately return if there are no theme CDN assets to use.
if (empty($assets)) {
return $framework;
}
// Override the framework version with the CDN version that is being used.
if (isset($data['version'])) {
$framework['version'] = $data['version'];
}
// @todo Provide a UI setting for this?
$styles = [];
if ($this->theme->getSetting('cdn_styles', TRUE)) {
$stylesProvider = ProviderManager::load($this->theme, 'drupal_bootstrap_styles');
$styles = $stylesProvider->getCdnAssets($version, $theme)->toLibraryArray($data['min']);
}
// Merge the assets with the existing library info and return it.
return NestedArray::mergeDeepArray([$assets, $styles, $framework], TRUE);
});
}
/**
* Retrieves a value from the CDN Provider cache.
*
* @param string $type
* The type of cache item to retrieve.
* @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 $name will be returned.
* @param mixed $default
* Optional. The default value to return if $key is not set.
* @param callable|null $builder
* Optional. If provided, a builder will be invoked when there is no cache
* currently set. The return value of the build will be used to set the
* cached value, provided there are no CDN Provider exceptions generated.
* If there are, but you still need the cache to be set, reset them prior
* to returning from the builder callback.
*
* @return mixed
* The cached value if it's set or the value supplied to $default if not.
*/
protected function cacheGet($type, $key = NULL, $default = NULL, ?callable $builder = NULL) {
$ttl = $this->getCacheTtl($type);
$never = $ttl === static::TTL_NEVER;
$forever = $ttl === static::TTL_FOREVER;
$cache = $forever ? $this->getKeyValue() : $this->getKeyValueExpirable();
$data = $cache->get($type, []);
if (!isset($key)) {
return $data;
}
$parts = Unicode::splitDelimiter($key);
$value = NestedArray::getValue($data, $parts, $key_exists);
// Build the cache.
if (!$key_exists && $builder) {
$value = $builder($default);
if (!isset($value)) {
$value = $default;
}
NestedArray::setValue($data, $parts, $value);
// Only set the cache if no CDN Provider exceptions were thrown.
if (!$this->cdnExceptions && !$never) {
if ($forever) {
$cache->set($type, $data);
}
else {
$cache->setWithExpire($type, $data, $ttl);
}
}
return $value;
}
return $key_exists ? $value : $default;
}
/**
* Discovers the assets supported by the CDN Provider.
*
* CDN Providers should sub-class this method to make requests and/or process
* any necessary data.
*
* @param string $version
* The version of assets to return.
* @param string $theme
* A specific set of themed assets to return, if any.
*
* @return \Drupal\bootstrap3\Plugin\Provider\CdnAssets
* A CdnAssets object.
*/
protected function discoverCdnAssets($version, $theme = NULL) {
$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);
}
/**
* Discovers the themes supported by the CDN Provider.
*
* CDN Providers should sub-class this method to make requests and/or process
* any necessary data.
*
* @param string $version
* A specific version of themes to retrieve.
*
* @return array|false
* An associative array of theme data, similar to what is returned in
* \Drupal\bootstrap3\Plugin\Provider\ProviderBase::discoverCdnAssets(), but
* keyed by the theme name.
*/
protected function discoverCdnThemes($version) {
return [];
}
/**
* Discovers the versions supported by the CDN Provider.
*
* CDN Providers should sub-class this method to make requests and/or process
* any necessary data.
*
* @return array|false
* An associative array of versions, also keyed by the version.
*/
protected function discoverCdnVersions() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTtl($type) {
if (!isset($this->cacheTtl[$type])) {
$this->cacheTtl[$type] = (int) $this->theme->getSetting("cdn_cache_ttl_$type", static::TTL_NEVER);
// If TTL is -1, the set a far reaching date from now.
if ($this->cacheTtl[$type] === static::TTL_FOREVER) {
$this->cacheTtl[$type] = static::TTL_ONE_YEAR * 10;
}
}
return $this->cacheTtl[$type];
}
/**
* Retrieves the unique cache identifier for the CDN Provider.
*
* @return string
* The CDN Provider cache identifier.
*/
protected function getCacheId() {
return "theme:{$this->theme->getName()}:cdn:{$this->getPluginId()}";
}
/**
* {@inheritdoc}
*/
public function getCdnAssets($version = NULL, $theme = NULL) {
if (!isset($this->cdnAssets)) {
$this->cdnAssets = $this->cacheGet('assets');
}
$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[$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\bootstrap3\Plugin\Provider\ProviderBase::getCdnAssets()
* @see \Drupal\bootstrap3\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,
];
}
/**
* {@inheritdoc}
*/
public function getCdnExceptions($reset = TRUE) {
$exceptions = $this->cdnExceptions;
if ($reset) {
$this->cdnExceptions = [];
}
return $exceptions;
}
/**
* {@inheritdoc}
*/
public function getCdnTheme() {
return $this->supportsThemes() ? $this->theme->getSetting('cdn_theme', 'bootstrap') : NULL;
}
/**
* {@inheritdoc}
*/
public function getCdnThemes($version = NULL) {
// Immediately return if the CDN Provider does not support themes.
if (!$this->supportsThemes()) {
return [];
}
$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[$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\bootstrap3\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() {
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)) {
$hash = Crypt::generateBase64HashIdentifier($this->getCdnVersionsCacheData());
$this->versions = $this->cacheGet('versions', $hash, [], function () {
return $this->discoverCdnVersions();
});
}
return $this->versions;
}
/**
* Retrieves the data used to create a hash for CDN Versions.
*
* @return array
* An array of components that will be serialized and hashed.
*
* @see \Drupal\bootstrap3\Plugin\Provider\ProviderBase::getCdnVersions()
*/
protected function getCdnVersionsCacheData() {
return [
'ttl' => $this->getCacheTtl(static::CACHE_THEMES),
'provider' => $this->pluginId,
];
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->pluginDefinition['description'];
}
/**
* Retrieves a permanent key/value storage instance.
*
* @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
* A permanent key/value storage instance.
*/
protected function getKeyValue() {
if (!isset($this->keyValue)) {
$this->keyValue = \Drupal::keyValue($this->getCacheId());
}
return $this->keyValue;
}
/**
* Retrieves a expirable key/value storage instance.
*
* @return \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
* An expirable key/value storage instance.
*/
protected function getKeyValueExpirable() {
if (!isset($this->keyValueExpirable)) {
$this->keyValueExpirable = \Drupal::keyValueExpirable($this->getCacheId());
}
return $this->keyValueExpirable;
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->pluginDefinition['label'] ?: $this->getPluginId();
}
/**
* {@inheritdoc}
*/
public function getThemes() {
return $this->pluginDefinition['themes'];
}
/**
* {@inheritdoc}
*/
public function getVersions() {
return $this->pluginDefinition['versions'];
}
/**
* Allows providers a way to map a version to a different version.
*
* @param string $version
* The version to map.
*
* @return string
* The mapped version.
*/
protected function mapVersion($version) {
return $version;
}
/**
* Initiates an HTTP request.
*
* @param string $url
* The URL to retrieve.
* @param array $options
* The options to pass to the HTTP client.
*
* @return \Drupal\bootstrap3\SerializedResponse
* A SerializedResponse object.
*/
protected function request($url, array $options = []) {
$response = Bootstrap::request($url, $options, $exception);
if ($exception) {
$this->addCdnException($exception);
}
return $response;
}
/**
* {@inheritdoc}
*/
public function resetCache() {
$this->getKeyValue()->deleteAll();
$this->getKeyValueExpirable()->deleteAll();
// Invalidate library info if this provider is the one currently used.
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.
***************************************************************************/
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function getApi() {
Bootstrap::deprecated();
return $this->pluginDefinition['api'];
}
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function getAssets($types = NULL) {
Bootstrap::deprecated();
return $this->assets;
}
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function hasError() {
Bootstrap::deprecated();
return $this->pluginDefinition['error'];
}
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function isImported() {
Bootstrap::deprecated();
return $this->pluginDefinition['imported'];
}
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function processDefinition(array &$definition, $plugin_id) {
// Due to code recursion and the need to keep this code in place for BC
// reasons, this deprecated message should only be logged and not shown.
Bootstrap::deprecated(FALSE);
// Process API data.
if ($api = $this->getApi()) {
$provider_path = ProviderManager::FILE_PATH;
// FILE_CREATE_DIRECTORY = 1 | FILE_MODIFY_PERMISSIONS = 2.
$options = 1 | 2;
if ($fileSystem = Bootstrap::fileSystem('prepareDirectory')) {
$fileSystem->prepareDirectory($provider_path, $options);
}
else {
\Drupal::service('file_system')->prepareDirectory($provider_path, $options);
}
// Use manually imported API data, if it exists.
if (file_exists("$provider_path/$plugin_id.json") && ($imported_data = file_get_contents("$provider_path/$plugin_id.json"))) {
$definition['imported'] = TRUE;
try {
$json = Json::decode($imported_data);
}
catch (\Exception $e) {
// Intentionally left blank.
}
}
// Otherwise, attempt to request API data if the provider has specified
// an "api" URL to use.
else {
$json = Bootstrap::request($api)->getData();
}
if (!isset($json)) {
$json = [];
$definition['error'] = TRUE;
}
$this->processApi($json, $definition);
}
}
/**
* {@inheritdoc}
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
public function processApi(array $json, array &$definition) {
Bootstrap::deprecated();
}
}
