Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / themes / contrib / bootstrap / src / Theme.php
1 <?php
2
3 namespace Drupal\bootstrap;
4
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;
14 use Drupal\Core\Url;
15
16 /**
17  * Defines a theme object.
18  *
19  * @ingroup utility
20  */
21 class Theme {
22
23   /**
24    * Ignores the following directories during file scans of a theme.
25    *
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
30    */
31   const IGNORE_DEFAULT = -1;
32
33   /**
34    * Ignores the directories "assets", "css", "images" and "js".
35    */
36   const IGNORE_ASSETS = 0x1;
37
38   /**
39    * Ignores the directories "config", "lib" and "src".
40    */
41   const IGNORE_CORE = 0x2;
42
43   /**
44    * Ignores the directories "docs" and "documentation".
45    */
46   const IGNORE_DOCS = 0x4;
47
48   /**
49    * Ignores "bower_components", "grunt", "node_modules" and "starterkits".
50    */
51   const IGNORE_DEV = 0x8;
52
53   /**
54    * Ignores the directories "templates" and "theme".
55    */
56   const IGNORE_TEMPLATES = 0x16;
57
58   /**
59    * Flag indicating if the theme is Bootstrap based.
60    *
61    * @var bool
62    */
63   protected $bootstrap;
64
65   /**
66    * Flag indicating if the theme is in "development" mode.
67    *
68    * @var bool
69    *
70    * This property can only be set via `settings.local.php`:
71    *
72    * @code
73    * $settings['theme.dev'] = TRUE;
74    * @endcode
75    */
76   protected $dev;
77
78   /**
79    * The current theme info.
80    *
81    * @var array
82    */
83   protected $info;
84
85   /**
86    * A URL for where a livereload instance is listening, if set.
87    *
88    * @var string
89    *
90    * This property can only be set via `settings.local.php`:
91    *
92    * @code
93    * // Enable default value: //127.0.0.1:35729/livereload.js.
94    * $settings['theme.livereload'] = TRUE;
95    *
96    * // Or, set just the port number: //127.0.0.1:12345/livereload.js.
97    * $settings['theme.livereload'] = 12345;
98    *
99    * // Or, Set an explicit URL.
100    * $settings['theme.livereload'] = '//127.0.0.1:35729/livereload.js';
101    * @endcode
102    */
103   protected $livereload;
104
105   /**
106    * The theme machine name.
107    *
108    * @var string
109    */
110   protected $name;
111
112   /**
113    * The current theme Extension object.
114    *
115    * @var \Drupal\Core\Extension\Extension
116    */
117   protected $theme;
118
119   /**
120    * An array of installed themes.
121    *
122    * @var array
123    */
124   protected $themes;
125
126   /**
127    * Theme handler object.
128    *
129    * @var \Drupal\Core\Extension\ThemeHandlerInterface
130    */
131   protected $themeHandler;
132
133   /**
134    * The update plugin manager.
135    *
136    * @var \Drupal\bootstrap\Plugin\UpdateManager
137    */
138   protected $updateManager;
139
140   /**
141    * Theme constructor.
142    *
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.
147    */
148   public function __construct(Extension $theme, ThemeHandlerInterface $theme_handler) {
149     // Determine if "development mode" is set.
150     $this->dev = !!Settings::get('theme.dev');
151
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';
158       }
159       // If an integer, assume it's a port.
160       elseif (is_int($livereload)) {
161         $livereload = "//127.0.0.1:$livereload/livereload.js";
162       }
163       // If it's scalar, attempt to parse the URL.
164       elseif (is_scalar($livereload)) {
165         try {
166           $livereload = Url::fromUri($livereload)->toString();
167         }
168         catch (\Exception $e) {
169           $livereload = '';
170         }
171       }
172
173       // Typecast livereload URL to a string.
174       $this->livereload = "$livereload" ?: '';
175     }
176
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');
183
184     // Only install the theme if it's Bootstrap based and there are no schemas
185     // currently set.
186     if ($this->isBootstrap() && !$this->getSetting('schemas')) {
187       try {
188         $this->install();
189       }
190       catch (\Exception $e) {
191         // Intentionally left blank.
192         // @see https://www.drupal.org/node/2697075
193       }
194     }
195   }
196
197   /**
198    * Serialization method.
199    */
200   public function __sleep() {
201     // Only store the theme name.
202     return ['name'];
203   }
204
205   /**
206    * Unserialize method.
207    */
208   public function __wakeup() {
209     $theme_handler = Bootstrap::getThemeHandler();
210     $theme = $theme_handler->getTheme($this->name);
211     $this->__construct($theme, $theme_handler);
212   }
213
214   /**
215    * Returns the theme machine name.
216    *
217    * @return string
218    *   Theme machine name.
219    */
220   public function __toString() {
221     return $this->getName();
222   }
223
224   /**
225    * Retrieves the theme's settings array appropriate for drupalSettings.
226    *
227    * @return array
228    *   The theme settings for drupalSettings.
229    */
230   public function drupalSettings() {
231     // Immediately return if theme is not Bootstrap based.
232     if (!$this->isBootstrap()) {
233       return [];
234     }
235
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;
242         }
243       }
244       $cache->setMultiple($drupal_settings);
245     }
246
247     $drupal_settings = array_intersect_key($this->settings()->get(), $drupal_settings);
248
249     // Indicate that theme is in dev mode.
250     if ($this->isDev()) {
251       $drupal_settings['dev'] = TRUE;
252     }
253
254     return $drupal_settings;
255   }
256
257   /**
258    * Wrapper for the core file_scan_directory() function.
259    *
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.
263    *
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.
280    *
281    * @return array
282    *   An associative array (keyed on the chosen key) of objects with 'uri',
283    *   'filename', and 'name' members corresponding to the matching files.
284    *
285    * @see file_scan_directory()
286    */
287   public function fileScan($mask, $subdir = NULL, array $options = []) {
288     $path = $this->getPath();
289
290     // Append addition sub-directories to the path if they were provided.
291     if (isset($subdir)) {
292       $path .= '/' . $subdir;
293     }
294
295     // Default ignore flags.
296     $options += [
297       'ignore_flags' => self::IGNORE_DEFAULT,
298     ];
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;
302     }
303
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'];
309       }
310       if ($flags & self::IGNORE_CORE) {
311         $ignore_directories += ['config', 'lib', 'src'];
312       }
313       if ($flags & self::IGNORE_DOCS) {
314         $ignore_directories += ['docs', 'documentation'];
315       }
316       if ($flags & self::IGNORE_DEV) {
317         $ignore_directories += [
318           'bower_components',
319           'grunt',
320           'node_modules',
321           'starterkits',
322         ];
323       }
324       if ($flags & self::IGNORE_TEMPLATES) {
325         $ignore_directories += ['templates', 'theme'];
326       }
327       if (!empty($ignore_directories)) {
328         $options['nomask'] = '/^' . implode('|', $ignore_directories) . '$/';
329       }
330     }
331
332     // Retrieve cache.
333     $files = $this->getCache('files');
334
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);
338
339     if (!$files->has($hash)) {
340       $files->set($hash, file_scan_directory($path, $mask, $options));
341     }
342     return $files->get($hash, []);
343   }
344
345   /**
346    * Retrieves the full base/sub-theme ancestry of a theme.
347    *
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.
351    *
352    * @return \Drupal\bootstrap\Theme[]
353    *   An associative array of \Drupal\bootstrap objects (theme), keyed
354    *   by machine name.
355    */
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);
360     }
361     $ancestry[$this->getName()] = $this;
362     return $reverse ? array_reverse($ancestry) : $ancestry;
363   }
364
365   /**
366    * Retrieves an individual item from a theme's cache in the database.
367    *
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
372    *   cached storage.
373    * @param mixed $default
374    *   Optional. The default value to use if $name does not exist.
375    *
376    * @return mixed|\Drupal\bootstrap\Utility\StorageItem
377    *   The cached value for $name.
378    */
379   public function getCache($name, array $context = [], $default = []) {
380     static $cache = [];
381
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());
385
386     // Join context together with ":" and use it as the name.
387     $name = implode(':', $context);
388
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);
395       }
396       $cache[$name] = $value;
397     }
398
399     return $cache[$name];
400   }
401
402   /**
403    * Retrieves the theme info.
404    *
405    * @param string $property
406    *   A specific property entry from the theme's info array to return.
407    *
408    * @return array
409    *   The entire theme info or a specific item if $property was passed.
410    */
411   public function getInfo($property = NULL) {
412     if (isset($property)) {
413       return isset($this->info[$property]) ? $this->info[$property] : NULL;
414     }
415     return $this->info;
416   }
417
418   /**
419    * Returns the machine name of the theme.
420    *
421    * @return string
422    *   The machine name of the theme.
423    */
424   public function getName() {
425     return $this->theme->getName();
426   }
427
428   /**
429    * Returns the relative path of the theme.
430    *
431    * @return string
432    *   The relative path of the theme.
433    */
434   public function getPath() {
435     return $this->theme->getPath();
436   }
437
438   /**
439    * Retrieves pending updates for the theme.
440    *
441    * @return \Drupal\bootstrap\Plugin\Update\UpdateInterface[]
442    *   An array of update plugin objects.
443    */
444   public function getPendingUpdates() {
445     $pending = [];
446
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);
456         }
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;
461           }
462         }
463       }
464     }
465
466     return $pending;
467   }
468
469   /**
470    * Retrieves the CDN provider.
471    *
472    * @param string $provider
473    *   A CDN provider name. Defaults to the provider set in the theme settings.
474    *
475    * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface|false
476    *   A provider instance or FALSE if there is no provider.
477    */
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]);
485       }
486     }
487     return FALSE;
488   }
489
490   /**
491    * Retrieves all CDN providers.
492    *
493    * @return \Drupal\bootstrap\Plugin\Provider\ProviderInterface[]
494    *   All provider instances.
495    */
496   public function getProviders() {
497     $providers = [];
498
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') {
504           continue;
505         }
506         $providers[$provider] = $provider_manager->createInstance($provider, ['theme' => $this]);
507       }
508     }
509
510     return $providers;
511   }
512
513   /**
514    * Retrieves a theme setting.
515    *
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).
524    *
525    * @return mixed
526    *   The value of the requested setting, NULL if the setting does not exist
527    *   and no $default value was provided.
528    *
529    * @see theme_get_setting()
530    */
531   public function getSetting($name, $default = NULL) {
532     $value = $this->settings()->get($name);
533     return !isset($value) ? $default : $value;
534   }
535
536   /**
537    * Retrieves a theme's setting plugin instance(s).
538    *
539    * @param string $name
540    *   Optional. The name of a specific setting plugin instance to return.
541    *
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.
546    */
547   public function getSettingPlugin($name = NULL) {
548     $settings = [];
549
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);
555       }
556     }
557
558     // Return a specific setting plugin.
559     if (isset($name)) {
560       return isset($settings[$name]) ? $settings[$name] : NULL;
561     }
562
563     // Return all setting plugins.
564     return $settings;
565   }
566
567   /**
568    * Retrieves the theme's setting plugin instances.
569    *
570    * @return \Drupal\bootstrap\Plugin\Setting\SettingInterface[]
571    *   An associative array of setting objects, keyed by their name.
572    *
573    * @deprecated Will be removed in a future release. Use \Drupal\bootstrap\Theme::getSettingPlugin instead.
574    */
575   public function getSettingPlugins() {
576     Bootstrap::deprecated();
577     return $this->getSettingPlugin();
578   }
579
580   /**
581    * Retrieves the theme's cache from the database.
582    *
583    * @return \Drupal\bootstrap\Utility\Storage
584    *   The cache object.
585    */
586   public function getStorage() {
587     static $cache = [];
588     $theme = $this->getName();
589     if (!isset($cache[$theme])) {
590       $cache[$theme] = new Storage($theme);
591     }
592     return $cache[$theme];
593   }
594
595   /**
596    * Retrieves the human-readable title of the theme.
597    *
598    * @return string
599    *   The theme title or machine name as a fallback.
600    */
601   public function getTitle() {
602     return $this->getInfo('name') ?: $this->getName();
603   }
604
605   /**
606    * Retrieves the update plugin manager for the theme.
607    *
608    * @return \Drupal\bootstrap\Plugin\UpdateManager|false
609    *   The Update plugin manager or FALSE if theme is not Bootstrap based.
610    */
611   public function getUpdateManager() {
612     // Immediately return if theme is not Bootstrap based.
613     if (!$this->isBootstrap()) {
614       return FALSE;
615     }
616
617     if (!$this->updateManager) {
618       $this->updateManager = new UpdateManager($this);
619     }
620     return $this->updateManager;
621   }
622
623   /**
624    * Determines whether or not if the theme has Bootstrap Framework Glyphicons.
625    */
626   public function hasGlyphicons() {
627     $glyphicons = $this->getCache('glyphicons');
628     if (!$glyphicons->has($this->getName())) {
629       $exists = FALSE;
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])) {
632           $exists = TRUE;
633           break;
634         }
635       }
636       $glyphicons->set($this->getName(), $exists);
637     }
638     return $glyphicons->get($this->getName(), FALSE);
639   }
640
641   /**
642    * Includes a file from the theme.
643    *
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.
649    *
650    * @return bool
651    *   TRUE if the file exists and is included successfully, FALSE otherwise.
652    */
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');
663       }
664     }
665     return $includes[$include];
666   }
667
668   /**
669    * Installs a Bootstrap based theme.
670    */
671   protected function install() {
672     // Immediately return if theme is not Bootstrap based.
673     if (!$this->isBootstrap()) {
674       return;
675     }
676
677     $schemas = [];
678     foreach ($this->getAncestry() as $ancestor) {
679       $schemas[$ancestor->getName()] = $ancestor->getUpdateManager()->getLatestSchema();
680     }
681     $this->setSetting('schemas', $schemas);
682   }
683
684   /**
685    * Indicates whether the theme is bootstrap based.
686    *
687    * @return bool
688    *   TRUE or FALSE
689    */
690   public function isBootstrap() {
691     return $this->bootstrap;
692   }
693
694   /**
695    * Indicates whether the theme is in "development mode".
696    *
697    * @return bool
698    *   TRUE or FALSE
699    *
700    * @see \Drupal\bootstrap\Theme::dev
701    */
702   public function isDev() {
703     return $this->dev;
704   }
705
706   /**
707    * Returns the livereload URL set, if any.
708    *
709    * @see \Drupal\bootstrap\Theme::livereload
710    *
711    * @return string
712    *   The livereload URL.
713    */
714   public function livereloadUrl() {
715     return $this->livereload;
716   }
717
718   /**
719    * Removes a theme setting.
720    *
721    * @param string $name
722    *   Name of the theme setting to remove.
723    */
724   public function removeSetting($name) {
725     $this->settings()->clear($name)->save();
726   }
727
728   /**
729    * Sets a value for a theme setting.
730    *
731    * @param string $name
732    *   Name of the theme setting.
733    * @param mixed $value
734    *   Value to associate with the theme setting.
735    */
736   public function setSetting($name, $value) {
737     $this->settings()->set($name, $value)->save();
738   }
739
740   /**
741    * Retrieves the theme settings instance.
742    *
743    * @return \Drupal\bootstrap\ThemeSettings
744    *   All settings.
745    */
746   public function settings() {
747     static $themes = [];
748     $name = $this->getName();
749     if (!isset($themes[$name])) {
750       $themes[$name] = new ThemeSettings($this);
751     }
752     return $themes[$name];
753   }
754
755   /**
756    * Determines whether or not a theme is a sub-theme of another.
757    *
758    * @param string|\Drupal\bootstrap\Theme $theme
759    *   The name or theme Extension object to check.
760    *
761    * @return bool
762    *   TRUE or FALSE
763    */
764   public function subthemeOf($theme) {
765     return (string) $theme === $this->getName() || in_array($theme, array_keys(self::getAncestry()));
766   }
767
768 }