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