4 * Contains \Drupal\bootstrap.
7 namespace Drupal\bootstrap;
9 use Drupal\bootstrap\Plugin\ProviderManager;
10 use Drupal\bootstrap\Plugin\SettingManager;
11 use Drupal\bootstrap\Plugin\UpdateManager;
12 use Drupal\bootstrap\Utility\Crypt;
13 use Drupal\bootstrap\Utility\Storage;
14 use Drupal\bootstrap\Utility\StorageItem;
15 use Drupal\Core\Extension\Extension;
16 use Drupal\Core\Extension\ThemeHandlerInterface;
17 use Drupal\Core\Site\Settings;
21 * Defines a theme object.
28 * Ignores the following directories during file scans of a theme.
30 * @see \Drupal\bootstrap\Theme::IGNORE_ASSETS
31 * @see \Drupal\bootstrap\Theme::IGNORE_CORE
32 * @see \Drupal\bootstrap\Theme::IGNORE_DOCS
33 * @see \Drupal\bootstrap\Theme::IGNORE_DEV
35 const IGNORE_DEFAULT = -1;
38 * Ignores the directories "assets", "css", "images" and "js".
40 const IGNORE_ASSETS = 0x1;
43 * Ignores the directories "config", "lib" and "src".
45 const IGNORE_CORE = 0x2;
48 * Ignores the directories "docs" and "documentation".
50 const IGNORE_DOCS = 0x4;
53 * Ignores "bower_components", "grunt", "node_modules" and "starterkits".
55 const IGNORE_DEV = 0x8;
58 * Ignores the directories "templates" and "theme".
60 const IGNORE_TEMPLATES = 0x16;
63 * Flag indicating if the theme is Bootstrap based.
70 * Flag indicating if the theme is in "development" mode.
72 * This property can only be set via `settings.local.php`:
75 * $settings['theme.dev'] = TRUE;
83 * The current theme info.
90 * A URL for where a livereload instance is listening, if set.
92 * This property can only be set via `settings.local.php`:
95 * // Enable default value: //127.0.0.1:35729/livereload.js.
96 * $settings['theme.livereload'] = TRUE;
98 * // Or, set just the port number: //127.0.0.1:12345/livereload.js.
99 * $settings['theme.livereload'] = 12345;
101 * // Or, Set an explicit URL.
102 * $settings['theme.livereload'] = '//127.0.0.1:35729/livereload.js';
107 protected $livereload;
110 * The theme machine name.
117 * The current theme Extension object.
119 * @var \Drupal\Core\Extension\Extension
124 * An array of installed themes.
131 * Theme handler object.
133 * @var \Drupal\Core\Extension\ThemeHandlerInterface
135 protected $themeHandler;
138 * The update plugin manager.
140 * @var \Drupal\bootstrap\Plugin\UpdateManager
142 protected $updateManager;
147 * @param \Drupal\Core\Extension\Extension $theme
148 * A theme \Drupal\Core\Extension\Extension object.
149 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
150 * The theme handler object.
152 public function __construct(Extension $theme, ThemeHandlerInterface $theme_handler) {
153 // Determine if "development mode" is set.
154 $this->dev = !!Settings::get('theme.dev');
156 // Determine the URL for livereload, if set.
157 $this->livereload = '';
158 if ($livereload = Settings::get('theme.livereload')) {
159 // If TRUE, then set the port to the default used by grunt-contrib-watch.
160 if ($livereload === TRUE) {
161 $livereload = '//127.0.0.1:35729/livereload.js';
163 // If an integer, assume it's a port.
164 else if (is_int($livereload)) {
165 $livereload = "//127.0.0.1:$livereload/livereload.js";
167 // If it's scalar, attempt to parse the URL.
168 elseif (is_scalar($livereload)) {
170 $livereload = Url::fromUri($livereload)->toString();
172 catch (\Exception $e) {
177 // Typecast livereload URL to a string.
178 $this->livereload = "$livereload" ?: '';
181 $this->name = $theme->getName();
182 $this->theme = $theme;
183 $this->themeHandler = $theme_handler;
184 $this->themes = $this->themeHandler->listInfo();
185 $this->info = isset($this->themes[$this->name]->info) ? $this->themes[$this->name]->info : [];
186 $this->bootstrap = $this->subthemeOf('bootstrap');
188 // Only install the theme if it's Bootstrap based and there are no schemas
190 if ($this->isBootstrap() && !$this->getSetting('schemas')) {
194 catch (\Exception $e) {
195 // Intentionally left blank.
196 // @see https://www.drupal.org/node/2697075
202 * Serialization method.
204 public function __sleep() {
205 // Only store the theme name.
210 * Unserialize method.
212 public function __wakeup() {
213 $theme_handler = Bootstrap::getThemeHandler();
214 $theme = $theme_handler->getTheme($this->name);
215 $this->__construct($theme, $theme_handler);
219 * Returns the theme machine name.
222 * Theme machine name.
224 public function __toString() {
225 return $this->getName();
229 * Retrieves the theme's settings array appropriate for drupalSettings.
232 * The theme settings for drupalSettings.
234 public function drupalSettings() {
235 // Immediately return if theme is not Bootstrap based.
236 if (!$this->isBootstrap()) {
240 $cache = $this->getCache('drupalSettings');
241 $drupal_settings = $cache->getAll();
242 if (!$drupal_settings) {
243 foreach ($this->getSettingPlugin() as $name => $setting) {
244 if ($setting->drupalSettings()) {
245 $drupal_settings[$name] = TRUE;
248 $cache->setMultiple($drupal_settings);
251 $drupal_settings = array_intersect_key($this->settings()->get(), $drupal_settings);
253 // Indicate that theme is in dev mode.
254 if ($this->isDev()) {
255 $drupal_settings['dev'] = TRUE;
258 return $drupal_settings;
262 * Wrapper for the core file_scan_directory() function.
264 * Finds all files that match a given mask in the given directories and then
265 * caches the results. A general site cache clear will force new scans to be
266 * initiated for already cached directories.
268 * @param string $mask
269 * The preg_match() regular expression of the files to find.
270 * @param string $subdir
271 * Sub-directory in the theme to start the scan, without trailing slash. If
272 * not set, the base path of the current theme will be used.
273 * @param array $options
274 * Options to pass, see file_scan_directory() for addition options:
275 * - ignore_flags: (int|FALSE) A bitmask to indicate which directories (if
276 * any) should be skipped during the scan. Must also not contain a
277 * "nomask" property in $options. Value can be any of the following:
278 * - \Drupal\bootstrap::IGNORE_CORE
279 * - \Drupal\bootstrap::IGNORE_ASSETS
280 * - \Drupal\bootstrap::IGNORE_DOCS
281 * - \Drupal\bootstrap::IGNORE_DEV
282 * - \Drupal\bootstrap::IGNORE_THEME
283 * Pass FALSE to iterate over all directories in $dir.
286 * An associative array (keyed on the chosen key) of objects with 'uri',
287 * 'filename', and 'name' members corresponding to the matching files.
289 * @see file_scan_directory()
291 public function fileScan($mask, $subdir = NULL, array $options = []) {
292 $path = $this->getPath();
294 // Append addition sub-directories to the path if they were provided.
295 if (isset($subdir)) {
296 $path .= '/' . $subdir;
299 // Default ignore flags.
301 'ignore_flags' => self::IGNORE_DEFAULT,
303 $flags = $options['ignore_flags'];
304 if ($flags === self::IGNORE_DEFAULT) {
305 $flags = self::IGNORE_CORE | self::IGNORE_ASSETS | self::IGNORE_DOCS | self::IGNORE_DEV;
308 // Save effort by skipping directories that are flagged.
309 if (!isset($options['nomask']) && $flags) {
310 $ignore_directories = [];
311 if ($flags & self::IGNORE_ASSETS) {
312 $ignore_directories += ['assets', 'css', 'images', 'js'];
314 if ($flags & self::IGNORE_CORE) {
315 $ignore_directories += ['config', 'lib', 'src'];
317 if ($flags & self::IGNORE_DOCS) {
318 $ignore_directories += ['docs', 'documentation'];
320 if ($flags & self::IGNORE_DEV) {
321 $ignore_directories += ['bower_components', 'grunt', 'node_modules', 'starterkits'];
323 if ($flags & self::IGNORE_TEMPLATES) {
324 $ignore_directories += ['templates', 'theme'];
326 if (!empty($ignore_directories)) {
327 $options['nomask'] = '/^' . implode('|', $ignore_directories) . '$/';
332 $files = $this->getCache('files');
334 // Generate a unique hash for all parameters passed as a change in any of
335 // them could potentially return different results.
336 $hash = Crypt::generateHash($mask, $path, $options);
338 if (!$files->has($hash)) {
339 $files->set($hash, file_scan_directory($path, $mask, $options));
341 return $files->get($hash, []);
345 * Retrieves the full base/sub-theme ancestry of a theme.
347 * @param bool $reverse
348 * Whether or not to return the array of themes in reverse order, where the
349 * active theme is the first entry.
351 * @return \Drupal\bootstrap\Theme[]
352 * An associative array of \Drupal\bootstrap objects (theme), keyed
355 public function getAncestry($reverse = FALSE) {
356 $ancestry = $this->themeHandler->getBaseThemes($this->themes, $this->getName());
357 foreach (array_keys($ancestry) as $name) {
358 $ancestry[$name] = Bootstrap::getTheme($name, $this->themeHandler);
360 $ancestry[$this->getName()] = $this;
361 return $reverse ? array_reverse($ancestry) : $ancestry;
365 * Retrieves an individual item from a theme's cache in the database.
367 * @param string $name
368 * The name of the item to retrieve from the theme cache.
369 * @param array $context
370 * Optional. An array of additional context to use for retrieving the
372 * @param mixed $default
373 * Optional. The default value to use if $name does not exist.
375 * @return mixed|\Drupal\bootstrap\Utility\StorageItem
376 * The cached value for $name.
378 public function getCache($name, array $context = [], $default = []) {
381 // Prepend the theme name as the first context item, followed by cache name.
382 array_unshift($context, $name);
383 array_unshift($context, $this->getName());
385 // Join context together with ":" and use it as the name.
386 $name = implode(':', $context);
388 if (!isset($cache[$name])) {
389 $storage = self::getStorage();
390 $value = $storage->get($name);
391 if (!isset($value)) {
392 $value = is_array($default) ? new StorageItem($default, $storage) : $default;
393 $storage->set($name, $value);
395 $cache[$name] = $value;
398 return $cache[$name];
402 * Retrieves the theme info.
404 * @param string $property
405 * A specific property entry from the theme's info array to return.
408 * The entire theme info or a specific item if $property was passed.
410 public function getInfo($property = NULL) {
411 if (isset($property)) {
412 return isset($this->info[$property]) ? $this->info[$property] : NULL;
418 * Returns the machine name of the theme.
421 * The machine name of the theme.
423 public function getName() {
424 return $this->theme->getName();
428 * Returns the relative path of the theme.
431 * The relative path of the theme.
433 public function getPath() {
434 return $this->theme->getPath();
438 * Retrieves pending updates for the theme.
440 * @return \Drupal\bootstrap\Plugin\Update\UpdateInterface[]
441 * An array of update plugin objects.
443 public function getPendingUpdates() {
446 // Only continue if the theme is Bootstrap based.
447 if ($this->isBootstrap()) {
448 $current_theme = $this->getName();
449 $schemas = $this->getSetting('schemas', []);
450 foreach ($this->getAncestry() as $ancestor) {
451 $ancestor_name = $ancestor->getName();
452 if (!isset($schemas[$ancestor_name])) {
453 $schemas[$ancestor_name] = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
454 $this->setSetting('schemas', $schemas);
456 $pending_updates = $ancestor->getUpdateManager()->getPendingUpdates($current_theme === $ancestor_name);
457 foreach ($pending_updates as $schema => $update) {
458 if ((int) $schema > (int) $schemas[$ancestor_name]) {
459 $pending[] = $update;
469 * Retrieves the CDN provider.
471 * @param string $provider
472 * A CDN provider name. Defaults to the provider set in the theme settings.
474 * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface|FALSE
475 * A provider instance or FALSE if there is no provider.
477 public function getProvider($provider = NULL) {
478 // Only continue if the theme is Bootstrap based.
479 if ($this->isBootstrap()) {
480 $provider = $provider ?: $this->getSetting('cdn_provider');
481 $provider_manager = new ProviderManager($this);
482 if ($provider_manager->hasDefinition($provider)) {
483 return $provider_manager->createInstance($provider, ['theme' => $this]);
490 * Retrieves all CDN providers.
492 * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface[]
493 * All provider instances.
495 public function getProviders() {
498 // Only continue if the theme is Bootstrap based.
499 if ($this->isBootstrap()) {
500 $provider_manager = new ProviderManager($this);
501 foreach (array_keys($provider_manager->getDefinitions()) as $provider) {
502 if ($provider === 'none') {
505 $providers[$provider] = $provider_manager->createInstance($provider, ['theme' => $this]);
513 * Retrieves a theme setting.
515 * @param string $name
516 * The name of the setting to be retrieved.
517 * @param mixed $default
518 * A default value to provide if the setting is not found or if the plugin
519 * does not have a "defaultValue" annotation key/value pair. Typically,
520 * you will likely never need to use this unless in rare circumstances
521 * where the setting plugin exists but needs a default value not able to
522 * be set by conventional means (e.g. empty array).
525 * The value of the requested setting, NULL if the setting does not exist
526 * and no $default value was provided.
528 * @see theme_get_setting()
530 public function getSetting($name, $default = NULL) {
531 $value = $this->settings()->get($name);
532 return !isset($value) ? $default : $value;
536 * Retrieves a theme's setting plugin instance(s).
538 * @param string $name
539 * Optional. The name of a specific setting plugin instance to return.
541 * @return \Drupal\bootstrap\Plugin\Setting\SettingInterface|\Drupal\bootstrap\Plugin\Setting\SettingInterface[]|NULL
542 * If $name was provided, it will either return a specific setting plugin
543 * instance or NULL if not set. If $name was omitted it will return an array
544 * of setting plugin instances, keyed by their name.
546 public function getSettingPlugin($name = NULL) {
549 // Only continue if the theme is Bootstrap based.
550 if ($this->isBootstrap()) {
551 $setting_manager = new SettingManager($this);
552 foreach (array_keys($setting_manager->getDefinitions()) as $setting) {
553 $settings[$setting] = $setting_manager->createInstance($setting);
557 // Return a specific setting plugin.
559 return isset($settings[$name]) ? $settings[$name] : NULL;
562 // Return all setting plugins.
567 * Retrieves the theme's setting plugin instances.
569 * @return \Drupal\bootstrap\Plugin\Setting\SettingInterface[]
570 * An associative array of setting objects, keyed by their name.
572 * @deprecated Will be removed in a future release. Use \Drupal\bootstrap\Theme::getSettingPlugin instead.
574 public function getSettingPlugins() {
575 Bootstrap::deprecated();
576 return $this->getSettingPlugin();
580 * Retrieves the theme's cache from the database.
582 * @return \Drupal\bootstrap\Utility\Storage
585 public function getStorage() {
587 $theme = $this->getName();
588 if (!isset($cache[$theme])) {
589 $cache[$theme] = new Storage($theme);
591 return $cache[$theme];
595 * Retrieves the human-readable title of the theme.
598 * The theme title or machine name as a fallback.
600 public function getTitle() {
601 return $this->getInfo('name') ?: $this->getName();
605 * Retrieves the update plugin manager for the theme.
607 * @return \Drupal\bootstrap\Plugin\UpdateManager|FALSE
608 * The Update plugin manager or FALSE if theme is not Bootstrap based.
610 public function getUpdateManager() {
611 // Immediately return if theme is not Bootstrap based.
612 if (!$this->isBootstrap()) {
616 if (!$this->updateManager) {
617 $this->updateManager = new UpdateManager($this);
619 return $this->updateManager;
623 * Determines whether or not if the theme has Bootstrap Framework Glyphicons.
625 public function hasGlyphicons() {
626 $glyphicons = $this->getCache('glyphicons');
627 if (!$glyphicons->has($this->getName())) {
629 foreach ($this->getAncestry(TRUE) as $ancestor) {
630 if ($ancestor->getSetting('cdn_provider') || $ancestor->fileScan('/glyphicons-halflings-regular\.(eot|svg|ttf|woff)$/', NULL, ['ignore_flags' => FALSE])) {
635 $glyphicons->set($this->getName(), $exists);
637 return $glyphicons->get($this->getName(), FALSE);
641 * Includes a file from the theme.
643 * @param string $file
644 * The file name, including the extension.
645 * @param string $path
646 * The path to the file in the theme. Defaults to: "includes". Set to FALSE
647 * or and empty string if the file resides in the theme's root directory.
650 * TRUE if the file exists and is included successfully, FALSE otherwise.
652 public function includeOnce($file, $path = 'includes') {
653 static $includes = [];
654 $file = preg_replace('`^/?' . $this->getPath() . '/?`', '', $file);
655 $file = strpos($file, '/') !== 0 ? $file = "/$file" : $file;
656 $path = is_string($path) && !empty($path) && strpos($path, '/') !== 0 ? $path = "/$path" : '';
657 $include = DRUPAL_ROOT . '/' . $this->getPath() . $path . $file;
658 if (!isset($includes[$include])) {
659 $includes[$include] = !!@include_once $include;
660 if (!$includes[$include]) {
661 drupal_set_message(t('Could not include file: @include', ['@include' => $include]), 'error');
664 return $includes[$include];
668 * Installs a Bootstrap based theme.
670 protected function install() {
671 // Immediately return if theme is not Bootstrap based.
672 if (!$this->isBootstrap()) {
677 foreach ($this->getAncestry() as $ancestor) {
678 $schemas[$ancestor->getName()] = $ancestor->getUpdateManager()->getLatestSchema();
680 $this->setSetting('schemas', $schemas);
684 * Indicates whether the theme is bootstrap based.
689 public function isBootstrap() {
690 return $this->bootstrap;
694 * Indicates whether the theme is in "development mode".
699 * @see \Drupal\bootstrap\Theme::dev
701 public function isDev() {
706 * Returns the livereload URL set, if any.
710 * @see \Drupal\bootstrap\Theme::livereload
712 public function livereloadUrl() {
713 return $this->livereload;
717 * Removes a theme setting.
719 * @param string $name
720 * Name of the theme setting to remove.
722 public function removeSetting($name) {
723 $this->settings()->clear($name)->save();
727 * Sets a value for a theme setting.
729 * @param string $name
730 * Name of the theme setting.
731 * @param mixed $value
732 * Value to associate with the theme setting.
734 public function setSetting($name, $value) {
735 $this->settings()->set($name, $value)->save();
739 * Retrieves the theme settings instance.
741 * @return \Drupal\bootstrap\ThemeSettings
744 public function settings() {
746 $name = $this->getName();
747 if (!isset($themes[$name])) {
748 $themes[$name] = new ThemeSettings($this, $this->themeHandler);
750 return $themes[$name];
754 * Determines whether or not a theme is a sub-theme of another.
756 * @param string|\Drupal\bootstrap\Theme $theme
757 * The name or theme Extension object to check.
762 public function subthemeOf($theme) {
763 return (string) $theme === $this->getName() || in_array($theme, array_keys(self::getAncestry()));