Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Extension / ThemeHandler.php
1 <?php
2
3 namespace Drupal\Core\Extension;
4
5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\State\StateInterface;
7
8 /**
9  * Default theme handler using the config system to store installation statuses.
10  */
11 class ThemeHandler implements ThemeHandlerInterface {
12
13   /**
14    * Contains the features enabled for themes by default.
15    *
16    * @var array
17    *
18    * @see _system_default_theme_features()
19    */
20   protected $defaultFeatures = [
21     'favicon',
22     'logo',
23     'node_user_picture',
24     'comment_user_picture',
25     'comment_user_verification',
26   ];
27
28   /**
29    * A list of all currently available themes.
30    *
31    * @var array
32    */
33   protected $list;
34
35   /**
36    * The config factory to get the installed themes.
37    *
38    * @var \Drupal\Core\Config\ConfigFactoryInterface
39    */
40   protected $configFactory;
41
42   /**
43    * The module handler to fire themes_installed/themes_uninstalled hooks.
44    *
45    * @var \Drupal\Core\Extension\ModuleHandlerInterface
46    */
47   protected $moduleHandler;
48
49   /**
50    * The state backend.
51    *
52    * @var \Drupal\Core\State\StateInterface
53    */
54   protected $state;
55
56   /**
57    * The config installer to install configuration.
58    *
59    * @var \Drupal\Core\Config\ConfigInstallerInterface
60    */
61   protected $configInstaller;
62
63   /**
64    * The info parser to parse the theme.info.yml files.
65    *
66    * @var \Drupal\Core\Extension\InfoParserInterface
67    */
68   protected $infoParser;
69
70   /**
71    * A logger instance.
72    *
73    * @var \Psr\Log\LoggerInterface
74    */
75   protected $logger;
76
77   /**
78    * The route builder to rebuild the routes if a theme is installed.
79    *
80    * @var \Drupal\Core\Routing\RouteBuilderInterface
81    */
82   protected $routeBuilder;
83
84   /**
85    * An extension discovery instance.
86    *
87    * @var \Drupal\Core\Extension\ExtensionDiscovery
88    */
89   protected $extensionDiscovery;
90
91   /**
92    * The CSS asset collection optimizer service.
93    *
94    * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
95    */
96   protected $cssCollectionOptimizer;
97
98   /**
99    * The config manager used to uninstall a theme.
100    *
101    * @var \Drupal\Core\Config\ConfigManagerInterface
102    */
103   protected $configManager;
104
105   /**
106    * The app root.
107    *
108    * @var string
109    */
110   protected $root;
111
112   /**
113    * Constructs a new ThemeHandler.
114    *
115    * @param string $root
116    *   The app root.
117    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
118    *   The config factory to get the installed themes.
119    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
120    *   The module handler to fire themes_installed/themes_uninstalled hooks.
121    * @param \Drupal\Core\State\StateInterface $state
122    *   The state store.
123    * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
124    *   The info parser to parse the theme.info.yml files.
125    * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
126    *   (optional) A extension discovery instance (for unit tests).
127    */
128   public function __construct($root, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser, ExtensionDiscovery $extension_discovery = NULL) {
129     $this->root = $root;
130     $this->configFactory = $config_factory;
131     $this->moduleHandler = $module_handler;
132     $this->state = $state;
133     $this->infoParser = $info_parser;
134     $this->extensionDiscovery = $extension_discovery;
135   }
136
137   /**
138    * {@inheritdoc}
139    */
140   public function getDefault() {
141     return $this->configFactory->get('system.theme')->get('default');
142   }
143
144   /**
145    * {@inheritdoc}
146    */
147   public function setDefault($name) {
148     $list = $this->listInfo();
149     if (!isset($list[$name])) {
150       throw new \InvalidArgumentException("$name theme is not installed.");
151     }
152     $this->configFactory->getEditable('system.theme')
153       ->set('default', $name)
154       ->save();
155     return $this;
156   }
157
158   /**
159    * {@inheritdoc}
160    */
161   public function install(array $theme_list, $install_dependencies = TRUE) {
162     // We keep the old install() method as BC layer but redirect directly to the
163     // theme installer.
164     return \Drupal::service('theme_installer')->install($theme_list, $install_dependencies);
165   }
166
167   /**
168    * {@inheritdoc}
169    */
170   public function uninstall(array $theme_list) {
171     // We keep the old uninstall() method as BC layer but redirect directly to
172     // the theme installer.
173     \Drupal::service('theme_installer')->uninstall($theme_list);
174   }
175
176   /**
177    * {@inheritdoc}
178    */
179   public function listInfo() {
180     if (!isset($this->list)) {
181       $this->list = [];
182       $themes = $this->systemThemeList();
183       // @todo Ensure that systemThemeList() does not contain an empty list
184       //   during the batch installer, see https://www.drupal.org/node/2322619.
185       if (empty($themes)) {
186         $this->refreshInfo();
187         $this->list = $this->list ?: [];
188         $themes = \Drupal::state()->get('system.theme.data', []);
189       }
190       foreach ($themes as $theme) {
191         $this->addTheme($theme);
192       }
193     }
194     return $this->list;
195   }
196
197   /**
198    * {@inheritdoc}
199    */
200   public function addTheme(Extension $theme) {
201     if (!empty($theme->info['libraries'])) {
202       foreach ($theme->info['libraries'] as $library => $name) {
203         $theme->libraries[$library] = $name;
204       }
205     }
206     if (isset($theme->info['engine'])) {
207       $theme->engine = $theme->info['engine'];
208     }
209     if (isset($theme->info['base theme'])) {
210       $theme->base_theme = $theme->info['base theme'];
211     }
212     $this->list[$theme->getName()] = $theme;
213   }
214
215   /**
216    * {@inheritdoc}
217    */
218   public function refreshInfo() {
219     $extension_config = $this->configFactory->get('core.extension');
220     $installed = $extension_config->get('theme');
221     // Only refresh the info if a theme has been installed. Modules are
222     // installed before themes by the installer and this method is called during
223     // module installation.
224     if (empty($installed) && empty($this->list)) {
225       return;
226     }
227
228     $this->reset();
229     // @todo Avoid re-scanning all themes by retaining the original (unaltered)
230     //   theme info somewhere.
231     $list = $this->rebuildThemeData();
232     foreach ($list as $name => $theme) {
233       if (isset($installed[$name])) {
234         $this->addTheme($theme);
235       }
236     }
237     $this->state->set('system.theme.data', $this->list);
238   }
239
240   /**
241    * {@inheritdoc}
242    */
243   public function reset() {
244     $this->systemListReset();
245     $this->list = NULL;
246   }
247
248   /**
249    * {@inheritdoc}
250    */
251   public function rebuildThemeData() {
252     $listing = $this->getExtensionDiscovery();
253     $themes = $listing->scan('theme');
254     $engines = $listing->scan('theme_engine');
255     $extension_config = $this->configFactory->get('core.extension');
256     $installed = $extension_config->get('theme') ?: [];
257
258     // Set defaults for theme info.
259     $defaults = [
260       'engine' => 'twig',
261       'base theme' => 'stable',
262       'regions' => [
263         'sidebar_first' => 'Left sidebar',
264         'sidebar_second' => 'Right sidebar',
265         'content' => 'Content',
266         'header' => 'Header',
267         'primary_menu' => 'Primary menu',
268         'secondary_menu' => 'Secondary menu',
269         'footer' => 'Footer',
270         'highlighted' => 'Highlighted',
271         'help' => 'Help',
272         'page_top' => 'Page top',
273         'page_bottom' => 'Page bottom',
274         'breadcrumb' => 'Breadcrumb',
275       ],
276       'description' => '',
277       'features' => $this->defaultFeatures,
278       'screenshot' => 'screenshot.png',
279       'php' => DRUPAL_MINIMUM_PHP,
280       'libraries' => [],
281     ];
282
283     $sub_themes = [];
284     $files_theme = [];
285     $files_theme_engine = [];
286     // Read info files for each theme.
287     foreach ($themes as $key => $theme) {
288       // @todo Remove all code that relies on the $status property.
289       $theme->status = (int) isset($installed[$key]);
290
291       $theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults;
292       // Remove the default Stable base theme when 'base theme: false' is set in
293       // a theme .info.yml file.
294       if ($theme->info['base theme'] === FALSE) {
295         unset($theme->info['base theme']);
296       }
297
298       // Add the info file modification time, so it becomes available for
299       // contributed modules to use for ordering theme lists.
300       $theme->info['mtime'] = $theme->getMTime();
301
302       // Invoke hook_system_info_alter() to give installed modules a chance to
303       // modify the data in the .info.yml files if necessary.
304       // @todo Remove $type argument, obsolete with $theme->getType().
305       $type = 'theme';
306       $this->moduleHandler->alter('system_info', $theme->info, $theme, $type);
307
308       if (!empty($theme->info['base theme'])) {
309         $sub_themes[] = $key;
310         // Add the base theme as a proper dependency.
311         $themes[$key]->info['dependencies'][] = $themes[$key]->info['base theme'];
312       }
313
314       // Defaults to 'twig' (see $defaults above).
315       $engine = $theme->info['engine'];
316       if (isset($engines[$engine])) {
317         $theme->owner = $engines[$engine]->getExtensionPathname();
318         $theme->prefix = $engines[$engine]->getName();
319         $files_theme_engine[$engine] = $engines[$engine]->getPathname();
320       }
321
322       // Prefix screenshot with theme path.
323       if (!empty($theme->info['screenshot'])) {
324         $theme->info['screenshot'] = $theme->getPath() . '/' . $theme->info['screenshot'];
325       }
326
327       $files_theme[$key] = $theme->getPathname();
328     }
329     // Build dependencies.
330     // @todo Move into a generic ExtensionHandler base class.
331     // @see https://www.drupal.org/node/2208429
332     $themes = $this->moduleHandler->buildModuleDependencies($themes);
333
334     // Store filenames to allow system_list() and drupal_get_filename() to
335     // retrieve them for themes and theme engines without having to scan the
336     // filesystem.
337     $this->state->set('system.theme.files', $files_theme);
338     $this->state->set('system.theme_engine.files', $files_theme_engine);
339
340     // After establishing the full list of available themes, fill in data for
341     // sub-themes.
342     foreach ($sub_themes as $key) {
343       $sub_theme = $themes[$key];
344       // The $base_themes property is optional; only set for sub themes.
345       // @see ThemeHandlerInterface::listInfo()
346       $sub_theme->base_themes = $this->getBaseThemes($themes, $key);
347       // empty() cannot be used here, since ThemeHandler::doGetBaseThemes() adds
348       // the key of a base theme with a value of NULL in case it is not found,
349       // in order to prevent needless iterations.
350       if (!current($sub_theme->base_themes)) {
351         continue;
352       }
353       // Determine the root base theme.
354       $root_key = key($sub_theme->base_themes);
355       // Build the list of sub-themes for each of the theme's base themes.
356       foreach (array_keys($sub_theme->base_themes) as $base_theme) {
357         $themes[$base_theme]->sub_themes[$key] = $sub_theme->info['name'];
358       }
359       // Add the theme engine info from the root base theme.
360       if (isset($themes[$root_key]->owner)) {
361         $sub_theme->info['engine'] = $themes[$root_key]->info['engine'];
362         $sub_theme->owner = $themes[$root_key]->owner;
363         $sub_theme->prefix = $themes[$root_key]->prefix;
364       }
365     }
366
367     return $themes;
368   }
369
370   /**
371    * {@inheritdoc}
372    */
373   public function getBaseThemes(array $themes, $theme) {
374     return $this->doGetBaseThemes($themes, $theme);
375   }
376
377   /**
378    * Finds the base themes for the specific theme.
379    *
380    * @param array $themes
381    *   An array of available themes.
382    * @param string $theme
383    *   The name of the theme whose base we are looking for.
384    * @param array $used_themes
385    *   (optional) A recursion parameter preventing endless loops. Defaults to
386    *   an empty array.
387    *
388    * @return array
389    *   An array of base themes.
390    */
391   protected function doGetBaseThemes(array $themes, $theme, $used_themes = []) {
392     if (!isset($themes[$theme]->info['base theme'])) {
393       return [];
394     }
395
396     $base_key = $themes[$theme]->info['base theme'];
397     // Does the base theme exist?
398     if (!isset($themes[$base_key])) {
399       return [$base_key => NULL];
400     }
401
402     $current_base_theme = [$base_key => $themes[$base_key]->info['name']];
403
404     // Is the base theme itself a child of another theme?
405     if (isset($themes[$base_key]->info['base theme'])) {
406       // Do we already know the base themes of this theme?
407       if (isset($themes[$base_key]->base_themes)) {
408         return $themes[$base_key]->base_themes + $current_base_theme;
409       }
410       // Prevent loops.
411       if (!empty($used_themes[$base_key])) {
412         return [$base_key => NULL];
413       }
414       $used_themes[$base_key] = TRUE;
415       return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
416     }
417     // If we get here, then this is our parent theme.
418     return $current_base_theme;
419   }
420
421   /**
422    * Returns an extension discovery object.
423    *
424    * @return \Drupal\Core\Extension\ExtensionDiscovery
425    *   The extension discovery object.
426    */
427   protected function getExtensionDiscovery() {
428     if (!isset($this->extensionDiscovery)) {
429       $this->extensionDiscovery = new ExtensionDiscovery($this->root);
430     }
431     return $this->extensionDiscovery;
432   }
433
434   /**
435    * {@inheritdoc}
436    */
437   public function getName($theme) {
438     $themes = $this->listInfo();
439     if (!isset($themes[$theme])) {
440       throw new \InvalidArgumentException("Requested the name of a non-existing theme $theme");
441     }
442     return $themes[$theme]->info['name'];
443   }
444
445   /**
446    * Wraps system_list_reset().
447    */
448   protected function systemListReset() {
449     system_list_reset();
450   }
451
452   /**
453    * Wraps system_list().
454    *
455    * @return array
456    *   A list of themes keyed by name.
457    */
458   protected function systemThemeList() {
459     return system_list('theme');
460   }
461
462   /**
463    * {@inheritdoc}
464    */
465   public function getThemeDirectories() {
466     $dirs = [];
467     foreach ($this->listInfo() as $name => $theme) {
468       $dirs[$name] = $this->root . '/' . $theme->getPath();
469     }
470     return $dirs;
471   }
472
473   /**
474    * {@inheritdoc}
475    */
476   public function themeExists($theme) {
477     $themes = $this->listInfo();
478     return isset($themes[$theme]);
479   }
480
481   /**
482    * {@inheritdoc}
483    */
484   public function getTheme($name) {
485     $themes = $this->listInfo();
486     if (isset($themes[$name])) {
487       return $themes[$name];
488     }
489     throw new \InvalidArgumentException(sprintf('The theme %s does not exist.', $name));
490   }
491
492   /**
493    * {@inheritdoc}
494    */
495   public function hasUi($name) {
496     $themes = $this->listInfo();
497     if (isset($themes[$name])) {
498       if (!empty($themes[$name]->info['hidden'])) {
499         $theme_config = $this->configFactory->get('system.theme');
500         return $name == $theme_config->get('default') || $name == $theme_config->get('admin');
501       }
502       return TRUE;
503     }
504     return FALSE;
505   }
506
507 }