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