95ce5f3e95101d9abc59b4d364f96493818e8c2f
[yaffs-website] / web / core / lib / Drupal / Core / Theme / ThemeInitialization.php
1 <?php
2
3 namespace Drupal\Core\Theme;
4
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Extension\Extension;
7 use Drupal\Core\Extension\ModuleHandlerInterface;
8 use Drupal\Core\Extension\ThemeHandlerInterface;
9
10 /**
11  * Provides the theme initialization logic.
12  */
13 class ThemeInitialization implements ThemeInitializationInterface {
14
15   /**
16    * The theme handler.
17    *
18    * @var \Drupal\Core\Extension\ThemeHandlerInterface
19    */
20   protected $themeHandler;
21
22   /**
23    * The cache backend to use for the active theme.
24    *
25    * @var \Drupal\Core\Cache\CacheBackendInterface
26    */
27   protected $cache;
28
29   /**
30    * The app root.
31    *
32    * @var string
33    */
34   protected $root;
35
36   /**
37    * The extensions that might be attaching assets.
38    *
39    * @var array
40    */
41   protected $extensions;
42
43   /**
44    * Constructs a new ThemeInitialization object.
45    *
46    * @param string $root
47    *   The app root.
48    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
49    *   The theme handler.
50    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
51    *   The cache backend.
52    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
53    *   The module handler to use to load modules.
54    */
55   public function __construct($root, ThemeHandlerInterface $theme_handler, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler) {
56     $this->root = $root;
57     $this->themeHandler = $theme_handler;
58     $this->cache = $cache;
59     $this->moduleHandler = $module_handler;
60   }
61
62   /**
63    * {@inheritdoc}
64    */
65   public function initTheme($theme_name) {
66     $active_theme = $this->getActiveThemeByName($theme_name);
67     $this->loadActiveTheme($active_theme);
68
69     return $active_theme;
70   }
71
72   /**
73    * {@inheritdoc}
74    */
75   public function getActiveThemeByName($theme_name) {
76     if ($cached = $this->cache->get('theme.active_theme.' . $theme_name)) {
77       return $cached->data;
78     }
79     $themes = $this->themeHandler->listInfo();
80
81     // If no theme could be negotiated, or if the negotiated theme is not within
82     // the list of installed themes, fall back to the default theme output of
83     // core and modules (like Stark, but without a theme extension at all). This
84     // is possible, because loadActiveTheme() always loads the Twig theme
85     // engine. This is desired, because missing or malformed theme configuration
86     // should not leave the application in a broken state. By falling back to
87     // default output, the user is able to reconfigure the theme through the UI.
88     // Lastly, tests are expected to operate with no theme by default, so as to
89     // only assert the original theme output of modules (unless a test manually
90     // installs a specific theme).
91     if (empty($themes) || !$theme_name || !isset($themes[$theme_name])) {
92       $theme_name = 'core';
93       // /core/core.info.yml does not actually exist, but is required because
94       // Extension expects a pathname.
95       $active_theme = $this->getActiveTheme(new Extension($this->root, 'theme', 'core/core.info.yml'));
96
97       // Early-return and do not set state, because the initialized $theme_name
98       // differs from the original $theme_name.
99       return $active_theme;
100     }
101
102     // Find all our ancestor themes and put them in an array.
103     $base_themes = [];
104     $ancestor = $theme_name;
105     while ($ancestor && isset($themes[$ancestor]->base_theme)) {
106       $ancestor = $themes[$ancestor]->base_theme;
107       if (!$this->themeHandler->themeExists($ancestor)) {
108         if ($ancestor == 'stable') {
109           // Themes that depend on Stable will be fixed by system_update_8014().
110           // There is no harm in not adding it as an ancestor since at worst
111           // some people might experience slight visual regressions on
112           // update.php.
113           continue;
114         }
115         throw new MissingThemeDependencyException(sprintf('Base theme %s has not been installed.', $ancestor), $ancestor);
116       }
117       $base_themes[] = $themes[$ancestor];
118     }
119
120     $active_theme = $this->getActiveTheme($themes[$theme_name], $base_themes);
121
122     $this->cache->set('theme.active_theme.' . $theme_name, $active_theme);
123     return $active_theme;
124   }
125
126   /**
127    * {@inheritdoc}
128    */
129   public function loadActiveTheme(ActiveTheme $active_theme) {
130     // Initialize the theme.
131     if ($theme_engine = $active_theme->getEngine()) {
132       // Include the engine.
133       include_once $this->root . '/' . $active_theme->getOwner();
134
135       if (function_exists($theme_engine . '_init')) {
136         foreach ($active_theme->getBaseThemes() as $base) {
137           call_user_func($theme_engine . '_init', $base->getExtension());
138         }
139         call_user_func($theme_engine . '_init', $active_theme->getExtension());
140       }
141     }
142     else {
143       // include non-engine theme files
144       foreach ($active_theme->getBaseThemes() as $base) {
145         // Include the theme file or the engine.
146         if ($base->getOwner()) {
147           include_once $this->root . '/' . $base->getOwner();
148         }
149       }
150       // and our theme gets one too.
151       if ($active_theme->getOwner()) {
152         include_once $this->root . '/' . $active_theme->getOwner();
153       }
154     }
155
156     // Always include Twig as the default theme engine.
157     include_once $this->root . '/core/themes/engines/twig/twig.engine';
158   }
159
160   /**
161    * {@inheritdoc}
162    */
163   public function getActiveTheme(Extension $theme, array $base_themes = []) {
164     $theme_path = $theme->getPath();
165
166     $values['path'] = $theme_path;
167     $values['name'] = $theme->getName();
168
169     // @todo Remove in Drupal 9.0.x.
170     $values['stylesheets_remove'] = $this->prepareStylesheetsRemove($theme, $base_themes);
171
172     // Prepare libraries overrides from this theme and ancestor themes. This
173     // allows child themes to easily remove CSS files from base themes and
174     // modules.
175     $values['libraries_override'] = [];
176
177     // Get libraries overrides declared by base themes.
178     foreach ($base_themes as $base) {
179       if (!empty($base->info['libraries-override'])) {
180         foreach ($base->info['libraries-override'] as $library => $override) {
181           $values['libraries_override'][$base->getPath()][$library] = $override;
182         }
183       }
184     }
185
186     // Add libraries overrides declared by this theme.
187     if (!empty($theme->info['libraries-override'])) {
188       foreach ($theme->info['libraries-override'] as $library => $override) {
189         $values['libraries_override'][$theme->getPath()][$library] = $override;
190       }
191     }
192
193     // Get libraries extensions declared by base themes.
194     foreach ($base_themes as $base) {
195       if (!empty($base->info['libraries-extend'])) {
196         foreach ($base->info['libraries-extend'] as $library => $extend) {
197           if (isset($values['libraries_extend'][$library])) {
198             // Merge if libraries-extend has already been defined for this
199             // library.
200             $values['libraries_extend'][$library] = array_merge($values['libraries_extend'][$library], $extend);
201           }
202           else {
203             $values['libraries_extend'][$library] = $extend;
204           }
205         }
206       }
207     }
208     // Add libraries extensions declared by this theme.
209     if (!empty($theme->info['libraries-extend'])) {
210       foreach ($theme->info['libraries-extend'] as $library => $extend) {
211         if (isset($values['libraries_extend'][$library])) {
212           // Merge if libraries-extend has already been defined for this
213           // library.
214           $values['libraries_extend'][$library] = array_merge($values['libraries_extend'][$library], $extend);
215         }
216         else {
217           $values['libraries_extend'][$library] = $extend;
218         }
219       }
220     }
221
222     // Do basically the same as the above for libraries
223     $values['libraries'] = [];
224
225     // Grab libraries from base theme
226     foreach ($base_themes as $base) {
227       if (!empty($base->libraries)) {
228         foreach ($base->libraries as $library) {
229           $values['libraries'][] = $library;
230         }
231       }
232     }
233
234     // Add libraries used by this theme.
235     if (!empty($theme->libraries)) {
236       foreach ($theme->libraries as $library) {
237         $values['libraries'][] = $library;
238       }
239     }
240
241     $values['engine'] = isset($theme->engine) ? $theme->engine : NULL;
242     $values['owner'] = isset($theme->owner) ? $theme->owner : NULL;
243     $values['extension'] = $theme;
244
245     $base_active_themes = [];
246     foreach ($base_themes as $base_theme) {
247       $base_active_themes[$base_theme->getName()] = $this->getActiveTheme($base_theme, array_slice($base_themes, 1));
248     }
249
250     $values['base_themes'] = $base_active_themes;
251     if (!empty($theme->info['regions'])) {
252       $values['regions'] = $theme->info['regions'];
253     }
254
255     return new ActiveTheme($values);
256   }
257
258   /**
259    * Gets all extensions.
260    *
261    * @return array
262    */
263   protected function getExtensions() {
264     if (!isset($this->extensions)) {
265       $this->extensions = array_merge($this->moduleHandler->getModuleList(), $this->themeHandler->listInfo());
266     }
267     return $this->extensions;
268   }
269
270   /**
271    * Gets CSS file where tokens have been resolved.
272    *
273    * @param string $css_file
274    *   CSS file which may contain tokens.
275    *
276    * @return string
277    *   CSS file where placeholders are replaced.
278    *
279    * @todo Remove in Drupal 9.0.x.
280    */
281   protected function resolveStyleSheetPlaceholders($css_file) {
282     $token_candidate = explode('/', $css_file)[0];
283     if (!preg_match('/@[A-z0-9_-]+/', $token_candidate)) {
284       return $css_file;
285     }
286
287     $token = substr($token_candidate, 1);
288
289     // Prime extensions.
290     $extensions = $this->getExtensions();
291     if (isset($extensions[$token])) {
292       return str_replace($token_candidate, $extensions[$token]->getPath(), $css_file);
293     }
294   }
295
296   /**
297    * Prepares stylesheets-remove specified in the *.info.yml file.
298    *
299    * @param \Drupal\Core\Extension\Extension $theme
300    *   The theme extension object.
301    * @param \Drupal\Core\Extension\Extension[] $base_themes
302    *   An array of base themes.
303    *
304    * @return string[]
305    *   The list of stylesheets-remove specified in the *.info.yml file.
306    *
307    * @todo Remove in Drupal 9.0.x.
308    */
309   protected function prepareStylesheetsRemove(Extension $theme, $base_themes) {
310     // Prepare stylesheets from this theme as well as all ancestor themes.
311     // We work it this way so that we can have child themes remove CSS files
312     // easily from parent.
313     $stylesheets_remove = [];
314     // Grab stylesheets from base theme.
315     foreach ($base_themes as $base) {
316       if (!empty($base->info['stylesheets-remove'])) {
317         foreach ($base->info['stylesheets-remove'] as $css_file) {
318           $css_file = $this->resolveStyleSheetPlaceholders($css_file);
319           $stylesheets_remove[$css_file] = $css_file;
320         }
321       }
322     }
323
324     // Add stylesheets used by this theme.
325     if (!empty($theme->info['stylesheets-remove'])) {
326       foreach ($theme->info['stylesheets-remove'] as $css_file) {
327         $css_file = $this->resolveStyleSheetPlaceholders($css_file);
328         $stylesheets_remove[$css_file] = $css_file;
329       }
330     }
331     return $stylesheets_remove;
332   }
333
334 }