3 namespace Drupal\bootstrap;
5 use Drupal\bootstrap\Plugin\ProviderManager;
6 use Drupal\bootstrap\Plugin\SettingManager;
7 use Drupal\bootstrap\Plugin\UpdateManager;
8 use Drupal\bootstrap\Utility\Crypt;
9 use Drupal\bootstrap\Utility\Storage;
10 use Drupal\bootstrap\Utility\StorageItem;
11 use Drupal\Core\Extension\Extension;
12 use Drupal\Core\Extension\ThemeHandlerInterface;
13 use Drupal\Core\Site\Settings;
17 * Defines a theme object.
24 * Ignores the following directories during file scans of a theme.
26 * @see \Drupal\bootstrap\Theme::IGNORE_ASSETS
27 * @see \Drupal\bootstrap\Theme::IGNORE_CORE
28 * @see \Drupal\bootstrap\Theme::IGNORE_DOCS
29 * @see \Drupal\bootstrap\Theme::IGNORE_DEV
31 const IGNORE_DEFAULT = -1;
34 * Ignores the directories "assets", "css", "images" and "js".
36 const IGNORE_ASSETS = 0x1;
39 * Ignores the directories "config", "lib" and "src".
41 const IGNORE_CORE = 0x2;
44 * Ignores the directories "docs" and "documentation".
46 const IGNORE_DOCS = 0x4;
49 * Ignores "bower_components", "grunt", "node_modules" and "starterkits".
51 const IGNORE_DEV = 0x8;
54 * Ignores the directories "templates" and "theme".
56 const IGNORE_TEMPLATES = 0x16;
59 * Flag indicating if the theme is Bootstrap based.
66 * Flag indicating if the theme is in "development" mode.
70 * This property can only be set via `settings.local.php`:
73 * $settings['theme.dev'] = TRUE;
79 * The current theme info.
86 * A URL for where a livereload instance is listening, if set.
90 * This property can only be set via `settings.local.php`:
93 * // Enable default value: //127.0.0.1:35729/livereload.js.
94 * $settings['theme.livereload'] = TRUE;
96 * // Or, set just the port number: //127.0.0.1:12345/livereload.js.
97 * $settings['theme.livereload'] = 12345;
99 * // Or, Set an explicit URL.
100 * $settings['theme.livereload'] = '//127.0.0.1:35729/livereload.js';
103 protected $livereload;
106 * The theme machine name.
113 * The current theme Extension object.
115 * @var \Drupal\Core\Extension\Extension
120 * An array of installed themes.
127 * Theme handler object.
129 * @var \Drupal\Core\Extension\ThemeHandlerInterface
131 protected $themeHandler;
134 * The update plugin manager.
136 * @var \Drupal\bootstrap\Plugin\UpdateManager
138 protected $updateManager;
143 * @param \Drupal\Core\Extension\Extension $theme
144 * A theme \Drupal\Core\Extension\Extension object.
145 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
146 * The theme handler object.
148 public function __construct(Extension $theme, ThemeHandlerInterface $theme_handler) {
149 // Determine if "development mode" is set.
150 $this->dev = !!Settings::get('theme.dev');
152 // Determine the URL for livereload, if set.
153 $this->livereload = '';
154 if ($livereload = Settings::get('theme.livereload')) {
155 // If TRUE, then set the port to the default used by grunt-contrib-watch.
156 if ($livereload === TRUE) {
157 $livereload = '//127.0.0.1:35729/livereload.js';
159 // If an integer, assume it's a port.
160 elseif (is_int($livereload)) {
161 $livereload = "//127.0.0.1:$livereload/livereload.js";
163 // If it's scalar, attempt to parse the URL.
164 elseif (is_scalar($livereload)) {
166 $livereload = Url::fromUri($livereload)->toString();
168 catch (\Exception $e) {
173 // Typecast livereload URL to a string.
174 $this->livereload = "$livereload" ?: '';
177 $this->name = $theme->getName();
178 $this->theme = $theme;
179 $this->themeHandler = $theme_handler;
180 $this->themes = $this->themeHandler->listInfo();
181 $this->info = isset($this->themes[$this->name]->info) ? $this->themes[$this->name]->info : [];
182 $this->bootstrap = $this->subthemeOf('bootstrap');
184 // Only install the theme if it's Bootstrap based and there are no schemas
186 if ($this->isBootstrap() && !$this->getSetting('schemas')) {
190 catch (\Exception $e) {
191 // Intentionally left blank.
192 // @see https://www.drupal.org/node/2697075
198 * Serialization method.
200 public function __sleep() {
201 // Only store the theme name.
206 * Unserialize method.
208 public function __wakeup() {
209 $theme_handler = Bootstrap::getThemeHandler();
210 $theme = $theme_handler->getTheme($this->name);
211 $this->__construct($theme, $theme_handler);
215 * Returns the theme machine name.
218 * Theme machine name.
220 public function __toString() {
221 return $this->getName();
225 * Retrieves the theme's settings array appropriate for drupalSettings.
228 * The theme settings for drupalSettings.
230 public function drupalSettings() {
231 // Immediately return if theme is not Bootstrap based.
232 if (!$this->isBootstrap()) {
236 $cache = $this->getCache('drupalSettings');
237 $drupal_settings = $cache->getAll();
238 if (!$drupal_settings) {
239 foreach ($this->getSettingPlugin() as $name => $setting) {
240 if ($setting->drupalSettings()) {
241 $drupal_settings[$name] = TRUE;
244 $cache->setMultiple($drupal_settings);
247 $drupal_settings = array_intersect_key($this->settings()->get(), $drupal_settings);
249 // Indicate that theme is in dev mode.
250 if ($this->isDev()) {
251 $drupal_settings['dev'] = TRUE;
254 return $drupal_settings;
258 * Wrapper for the core file_scan_directory() function.
260 * Finds all files that match a given mask in the given directories and then
261 * caches the results. A general site cache clear will force new scans to be
262 * initiated for already cached directories.
264 * @param string $mask
265 * The preg_match() regular expression of the files to find.
266 * @param string $subdir
267 * Sub-directory in the theme to start the scan, without trailing slash. If
268 * not set, the base path of the current theme will be used.
269 * @param array $options
270 * Options to pass, see file_scan_directory() for addition options:
271 * - ignore_flags: (int|FALSE) A bitmask to indicate which directories (if
272 * any) should be skipped during the scan. Must also not contain a
273 * "nomask" property in $options. Value can be any of the following:
274 * - \Drupal\bootstrap::IGNORE_CORE
275 * - \Drupal\bootstrap::IGNORE_ASSETS
276 * - \Drupal\bootstrap::IGNORE_DOCS
277 * - \Drupal\bootstrap::IGNORE_DEV
278 * - \Drupal\bootstrap::IGNORE_THEME
279 * Pass FALSE to iterate over all directories in $dir.
282 * An associative array (keyed on the chosen key) of objects with 'uri',
283 * 'filename', and 'name' members corresponding to the matching files.
285 * @see file_scan_directory()
287 public function fileScan($mask, $subdir = NULL, array $options = []) {
288 $path = $this->getPath();
290 // Append addition sub-directories to the path if they were provided.
291 if (isset($subdir)) {
292 $path .= '/' . $subdir;
295 // Default ignore flags.
297 'ignore_flags' => self::IGNORE_DEFAULT,
299 $flags = $options['ignore_flags'];
300 if ($flags === self::IGNORE_DEFAULT) {
301 $flags = self::IGNORE_CORE | self::IGNORE_ASSETS | self::IGNORE_DOCS | self::IGNORE_DEV;
304 // Save effort by skipping directories that are flagged.
305 if (!isset($options['nomask']) && $flags) {
306 $ignore_directories = [];
307 if ($flags & self::IGNORE_ASSETS) {
308 $ignore_directories += ['assets', 'css', 'images', 'js'];
310 if ($flags & self::IGNORE_CORE) {
311 $ignore_directories += ['config', 'lib', 'src'];
313 if ($flags & self::IGNORE_DOCS) {
314 $ignore_directories += ['docs', 'documentation'];
316 if ($flags & self::IGNORE_DEV) {
317 $ignore_directories += [
324 if ($flags & self::IGNORE_TEMPLATES) {
325 $ignore_directories += ['templates', 'theme'];
327 if (!empty($ignore_directories)) {
328 $options['nomask'] = '/^' . implode('|', $ignore_directories) . '$/';
333 $files = $this->getCache('files');
335 // Generate a unique hash for all parameters passed as a change in any of
336 // them could potentially return different results.
337 $hash = Crypt::generateHash($mask, $path, $options);
339 if (!$files->has($hash)) {
340 $files->set($hash, file_scan_directory($path, $mask, $options));
342 return $files->get($hash, []);
346 * Retrieves the full base/sub-theme ancestry of a theme.
348 * @param bool $reverse
349 * Whether or not to return the array of themes in reverse order, where the
350 * active theme is the first entry.
352 * @return \Drupal\bootstrap\Theme[]
353 * An associative array of \Drupal\bootstrap objects (theme), keyed
356 public function getAncestry($reverse = FALSE) {
357 $ancestry = $this->themeHandler->getBaseThemes($this->themes, $this->getName());
358 foreach (array_keys($ancestry) as $name) {
359 $ancestry[$name] = Bootstrap::getTheme($name, $this->themeHandler);
361 $ancestry[$this->getName()] = $this;
362 return $reverse ? array_reverse($ancestry) : $ancestry;
366 * Retrieves an individual item from a theme's cache in the database.
368 * @param string $name
369 * The name of the item to retrieve from the theme cache.
370 * @param array $context
371 * Optional. An array of additional context to use for retrieving the
373 * @param mixed $default
374 * Optional. The default value to use if $name does not exist.
376 * @return mixed|\Drupal\bootstrap\Utility\StorageItem
377 * The cached value for $name.
379 public function getCache($name, array $context = [], $default = []) {
382 // Prepend the theme name as the first context item, followed by cache name.
383 array_unshift($context, $name);
384 array_unshift($context, $this->getName());
386 // Join context together with ":" and use it as the name.
387 $name = implode(':', $context);
389 if (!isset($cache[$name])) {
390 $storage = self::getStorage();
391 $value = $storage->get($name);
392 if (!isset($value)) {
393 $value = is_array($default) ? new StorageItem($default, $storage) : $default;
394 $storage->set($name, $value);
396 $cache[$name] = $value;
399 return $cache[$name];
403 * Retrieves the theme info.
405 * @param string $property
406 * A specific property entry from the theme's info array to return.
409 * The entire theme info or a specific item if $property was passed.
411 public function getInfo($property = NULL) {
412 if (isset($property)) {
413 return isset($this->info[$property]) ? $this->info[$property] : NULL;
419 * Returns the machine name of the theme.
422 * The machine name of the theme.
424 public function getName() {
425 return $this->theme->getName();
429 * Returns the relative path of the theme.
432 * The relative path of the theme.
434 public function getPath() {
435 return $this->theme->getPath();
439 * Retrieves pending updates for the theme.
441 * @return \Drupal\bootstrap\Plugin\Update\UpdateInterface[]
442 * An array of update plugin objects.
444 public function getPendingUpdates() {
447 // Only continue if the theme is Bootstrap based.
448 if ($this->isBootstrap()) {
449 $current_theme = $this->getName();
450 $schemas = $this->getSetting('schemas', []);
451 foreach ($this->getAncestry() as $ancestor) {
452 $ancestor_name = $ancestor->getName();
453 if (!isset($schemas[$ancestor_name])) {
454 $schemas[$ancestor_name] = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
455 $this->setSetting('schemas', $schemas);
457 $pending_updates = $ancestor->getUpdateManager()->getPendingUpdates($current_theme === $ancestor_name);
458 foreach ($pending_updates as $schema => $update) {
459 if ((int) $schema > (int) $schemas[$ancestor_name]) {
460 $pending[] = $update;
470 * Retrieves the CDN provider.
472 * @param string $provider
473 * A CDN provider name. Defaults to the provider set in the theme settings.
475 * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface|false
476 * A provider instance or FALSE if there is no provider.
478 public function getProvider($provider = NULL) {
479 // Only continue if the theme is Bootstrap based.
480 if ($this->isBootstrap()) {
481 $provider = $provider ?: $this->getSetting('cdn_provider');
482 $provider_manager = new ProviderManager($this);
483 if ($provider_manager->hasDefinition($provider)) {
484 return $provider_manager->createInstance($provider, ['theme' => $this]);
491 * Retrieves all CDN providers.
493 * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface[]
494 * All provider instances.
496 public function getProviders() {
499 // Only continue if the theme is Bootstrap based.
500 if ($this->isBootstrap()) {
501 $provider_manager = new ProviderManager($this);
502 foreach (array_keys($provider_manager->getDefinitions()) as $provider) {
503 if ($provider === 'none') {
506 $providers[$provider] = $provider_manager->createInstance($provider, ['theme' => $this]);
514 * Retrieves a theme setting.
516 * @param string $name
517 * The name of the setting to be retrieved.
518 * @param mixed $default
519 * A default value to provide if the setting is not found or if the plugin
520 * does not have a "defaultValue" annotation key/value pair. Typically,
521 * you will likely never need to use this unless in rare circumstances
522 * where the setting plugin exists but needs a default value not able to
523 * be set by conventional means (e.g. empty array).
526 * The value of the requested setting, NULL if the setting does not exist
527 * and no $default value was provided.
529 * @see theme_get_setting()
531 public function getSetting($name, $default = NULL) {
532 $value = $this->settings()->get($name);
533 return !isset($value) ? $default : $value;
537 * Retrieves a theme's setting plugin instance(s).
539 * @param string $name
540 * Optional. The name of a specific setting plugin instance to return.
542 * @return \Drupal\bootstrap\Plugin\Setting\SettingInterface|\Drupal\bootstrap\Plugin\Setting\SettingInterface[]|null
543 * If $name was provided, it will either return a specific setting plugin
544 * instance or NULL if not set. If $name was omitted it will return an array
545 * of setting plugin instances, keyed by their name.
547 public function getSettingPlugin($name = NULL) {
550 // Only continue if the theme is Bootstrap based.
551 if ($this->isBootstrap()) {
552 $setting_manager = new SettingManager($this);
553 foreach (array_keys($setting_manager->getDefinitions()) as $setting) {
554 $settings[$setting] = $setting_manager->createInstance($setting);
558 // Return a specific setting plugin.
560 return isset($settings[$name]) ? $settings[$name] : NULL;
563 // Return all setting plugins.
568 * Retrieves the theme's setting plugin instances.
570 * @return \Drupal\bootstrap\Plugin\Setting\SettingInterface[]
571 * An associative array of setting objects, keyed by their name.
573 * @deprecated Will be removed in a future release. Use \Drupal\bootstrap\Theme::getSettingPlugin instead.
575 public function getSettingPlugins() {
576 Bootstrap::deprecated();
577 return $this->getSettingPlugin();
581 * Retrieves the theme's cache from the database.
583 * @return \Drupal\bootstrap\Utility\Storage
586 public function getStorage() {
588 $theme = $this->getName();
589 if (!isset($cache[$theme])) {
590 $cache[$theme] = new Storage($theme);
592 return $cache[$theme];
596 * Retrieves the human-readable title of the theme.
599 * The theme title or machine name as a fallback.
601 public function getTitle() {
602 return $this->getInfo('name') ?: $this->getName();
606 * Retrieves the update plugin manager for the theme.
608 * @return \Drupal\bootstrap\Plugin\UpdateManager|false
609 * The Update plugin manager or FALSE if theme is not Bootstrap based.
611 public function getUpdateManager() {
612 // Immediately return if theme is not Bootstrap based.
613 if (!$this->isBootstrap()) {
617 if (!$this->updateManager) {
618 $this->updateManager = new UpdateManager($this);
620 return $this->updateManager;
624 * Determines whether or not if the theme has Bootstrap Framework Glyphicons.
626 public function hasGlyphicons() {
627 $glyphicons = $this->getCache('glyphicons');
628 if (!$glyphicons->has($this->getName())) {
630 foreach ($this->getAncestry(TRUE) as $ancestor) {
631 if ($ancestor->getSetting('cdn_provider') || $ancestor->fileScan('/glyphicons-halflings-regular\.(eot|svg|ttf|woff)$/', NULL, ['ignore_flags' => FALSE])) {
636 $glyphicons->set($this->getName(), $exists);
638 return $glyphicons->get($this->getName(), FALSE);
642 * Includes a file from the theme.
644 * @param string $file
645 * The file name, including the extension.
646 * @param string $path
647 * The path to the file in the theme. Defaults to: "includes". Set to FALSE
648 * or and empty string if the file resides in the theme's root directory.
651 * TRUE if the file exists and is included successfully, FALSE otherwise.
653 public function includeOnce($file, $path = 'includes') {
654 static $includes = [];
655 $file = preg_replace('`^/?' . $this->getPath() . '/?`', '', $file);
656 $file = strpos($file, '/') !== 0 ? $file = "/$file" : $file;
657 $path = is_string($path) && !empty($path) && strpos($path, '/') !== 0 ? $path = "/$path" : '';
658 $include = DRUPAL_ROOT . '/' . $this->getPath() . $path . $file;
659 if (!isset($includes[$include])) {
660 $includes[$include] = !!@include_once $include;
661 if (!$includes[$include]) {
662 drupal_set_message(t('Could not include file: @include', ['@include' => $include]), 'error');
665 return $includes[$include];
669 * Installs a Bootstrap based theme.
671 protected function install() {
672 // Immediately return if theme is not Bootstrap based.
673 if (!$this->isBootstrap()) {
678 foreach ($this->getAncestry() as $ancestor) {
679 $schemas[$ancestor->getName()] = $ancestor->getUpdateManager()->getLatestSchema();
681 $this->setSetting('schemas', $schemas);
685 * Indicates whether the theme is bootstrap based.
690 public function isBootstrap() {
691 return $this->bootstrap;
695 * Indicates whether the theme is in "development mode".
700 * @see \Drupal\bootstrap\Theme::dev
702 public function isDev() {
707 * Returns the livereload URL set, if any.
709 * @see \Drupal\bootstrap\Theme::livereload
712 * The livereload URL.
714 public function livereloadUrl() {
715 return $this->livereload;
719 * Removes a theme setting.
721 * @param string $name
722 * Name of the theme setting to remove.
724 public function removeSetting($name) {
725 $this->settings()->clear($name)->save();
729 * Sets a value for a theme setting.
731 * @param string $name
732 * Name of the theme setting.
733 * @param mixed $value
734 * Value to associate with the theme setting.
736 public function setSetting($name, $value) {
737 $this->settings()->set($name, $value)->save();
741 * Retrieves the theme settings instance.
743 * @return \Drupal\bootstrap\ThemeSettings
746 public function settings() {
748 $name = $this->getName();
749 if (!isset($themes[$name])) {
750 $themes[$name] = new ThemeSettings($this);
752 return $themes[$name];
756 * Determines whether or not a theme is a sub-theme of another.
758 * @param string|\Drupal\bootstrap\Theme $theme
759 * The name or theme Extension object to check.
764 public function subthemeOf($theme) {
765 return (string) $theme === $this->getName() || in_array($theme, array_keys(self::getAncestry()));