Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Extension / ThemeInstaller.php
1 <?php
2
3 namespace Drupal\Core\Extension;
4
5 use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Config\ConfigFactoryInterface;
8 use Drupal\Core\Config\ConfigInstallerInterface;
9 use Drupal\Core\Config\ConfigManagerInterface;
10 use Drupal\Core\Extension\Exception\UnknownExtensionException;
11 use Drupal\Core\Routing\RouteBuilderInterface;
12 use Drupal\Core\State\StateInterface;
13 use Psr\Log\LoggerInterface;
14
15 /**
16  * Manages theme installation/uninstallation.
17  */
18 class ThemeInstaller implements ThemeInstallerInterface {
19
20   /**
21    * @var \Drupal\Core\Extension\ThemeHandlerInterface
22    */
23   protected $themeHandler;
24
25   /**
26    * @var \Drupal\Core\Config\ConfigFactoryInterface
27    */
28   protected $configFactory;
29
30   /**
31    * @var \Drupal\Core\Config\ConfigInstallerInterface
32    */
33   protected $configInstaller;
34
35   /**
36    * @var \Drupal\Core\Extension\ModuleHandlerInterface
37    */
38   protected $moduleHandler;
39
40   /**
41    * @var \Drupal\Core\State\StateInterface
42    */
43   protected $state;
44
45   /**
46    * @var \Drupal\Core\Config\ConfigManagerInterface
47    */
48   protected $configManager;
49
50   /**
51    * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
52    */
53   protected $cssCollectionOptimizer;
54
55   /**
56    * @var \Drupal\Core\Routing\RouteBuilderInterface
57    */
58   protected $routeBuilder;
59
60   /**
61    * @var \Psr\Log\LoggerInterface
62    */
63   protected $logger;
64
65   /**
66    * Constructs a new ThemeInstaller.
67    *
68    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
69    *   The theme handler.
70    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
71    *   The config factory to get the installed themes.
72    * @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer
73    *   (optional) The config installer to install configuration. This optional
74    *   to allow the theme handler to work before Drupal is installed and has a
75    *   database.
76    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
77    *   The module handler to fire themes_installed/themes_uninstalled hooks.
78    * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
79    *   The config manager used to uninstall a theme.
80    * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
81    *   The CSS asset collection optimizer service.
82    * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
83    *   (optional) The route builder service to rebuild the routes if a theme is
84    *   installed.
85    * @param \Psr\Log\LoggerInterface $logger
86    *   A logger instance.
87    * @param \Drupal\Core\State\StateInterface $state
88    *   The state store.
89    */
90   public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state) {
91     $this->themeHandler = $theme_handler;
92     $this->configFactory = $config_factory;
93     $this->configInstaller = $config_installer;
94     $this->moduleHandler = $module_handler;
95     $this->configManager = $config_manager;
96     $this->cssCollectionOptimizer = $css_collection_optimizer;
97     $this->routeBuilder = $route_builder;
98     $this->logger = $logger;
99     $this->state = $state;
100   }
101
102   /**
103    * {@inheritdoc}
104    */
105   public function install(array $theme_list, $install_dependencies = TRUE) {
106     $extension_config = $this->configFactory->getEditable('core.extension');
107
108     $theme_data = $this->themeHandler->rebuildThemeData();
109
110     if ($install_dependencies) {
111       $theme_list = array_combine($theme_list, $theme_list);
112
113       if ($missing = array_diff_key($theme_list, $theme_data)) {
114         // One or more of the given themes doesn't exist.
115         throw new UnknownExtensionException('Unknown themes: ' . implode(', ', $missing) . '.');
116       }
117
118       // Only process themes that are not installed currently.
119       $installed_themes = $extension_config->get('theme') ?: [];
120       if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
121         // Nothing to do. All themes already installed.
122         return TRUE;
123       }
124
125       foreach ($theme_list as $theme => $value) {
126         // Add dependencies to the list. The new themes will be processed as
127         // the parent foreach loop continues.
128         foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
129           if (!isset($theme_data[$dependency])) {
130             // The dependency does not exist.
131             return FALSE;
132           }
133
134           // Skip already installed themes.
135           if (!isset($theme_list[$dependency]) && !isset($installed_themes[$dependency])) {
136             $theme_list[$dependency] = $dependency;
137           }
138         }
139       }
140
141       // Set the actual theme weights.
142       $theme_list = array_map(function ($theme) use ($theme_data) {
143         return $theme_data[$theme]->sort;
144       }, $theme_list);
145
146       // Sort the theme list by their weights (reverse).
147       arsort($theme_list);
148       $theme_list = array_keys($theme_list);
149     }
150     else {
151       $installed_themes = $extension_config->get('theme') ?: [];
152     }
153
154     $themes_installed = [];
155     foreach ($theme_list as $key) {
156       // Only process themes that are not already installed.
157       $installed = $extension_config->get("theme.$key") !== NULL;
158       if ($installed) {
159         continue;
160       }
161
162       // Throw an exception if the theme name is too long.
163       if (strlen($key) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
164         throw new ExtensionNameLengthException("Theme name $key is over the maximum allowed length of " . DRUPAL_EXTENSION_NAME_MAX_LENGTH . ' characters.');
165       }
166
167       // Validate default configuration of the theme. If there is existing
168       // configuration then stop installing.
169       $this->configInstaller->checkConfigurationToInstall('theme', $key);
170
171       // The value is not used; the weight is ignored for themes currently. Do
172       // not check schema when saving the configuration.
173       $extension_config
174         ->set("theme.$key", 0)
175         ->save(TRUE);
176
177       // Add the theme to the current list.
178       // @todo Remove all code that relies on $status property.
179       $theme_data[$key]->status = 1;
180       $this->themeHandler->addTheme($theme_data[$key]);
181
182       // Update the current theme data accordingly.
183       $current_theme_data = $this->state->get('system.theme.data', []);
184       $current_theme_data[$key] = $theme_data[$key];
185       $this->state->set('system.theme.data', $current_theme_data);
186
187       // Reset theme settings.
188       $theme_settings = &drupal_static('theme_get_setting');
189       unset($theme_settings[$key]);
190
191       // @todo Remove system_list().
192       $this->systemListReset();
193
194       // Only install default configuration if this theme has not been installed
195       // already.
196       if (!isset($installed_themes[$key])) {
197         // Install default configuration of the theme.
198         $this->configInstaller->installDefaultConfig('theme', $key);
199       }
200
201       $themes_installed[] = $key;
202
203       // Record the fact that it was installed.
204       $this->logger->info('%theme theme installed.', ['%theme' => $key]);
205     }
206
207     $this->cssCollectionOptimizer->deleteAll();
208     $this->resetSystem();
209
210     // Invoke hook_themes_installed() after the themes have been installed.
211     $this->moduleHandler->invokeAll('themes_installed', [$themes_installed]);
212
213     return !empty($themes_installed);
214   }
215
216   /**
217    * {@inheritdoc}
218    */
219   public function uninstall(array $theme_list) {
220     $extension_config = $this->configFactory->getEditable('core.extension');
221     $theme_config = $this->configFactory->getEditable('system.theme');
222     $list = $this->themeHandler->listInfo();
223     foreach ($theme_list as $key) {
224       if (!isset($list[$key])) {
225         throw new UnknownExtensionException("Unknown theme: $key.");
226       }
227       if ($key === $theme_config->get('default')) {
228         throw new \InvalidArgumentException("The current default theme $key cannot be uninstalled.");
229       }
230       if ($key === $theme_config->get('admin')) {
231         throw new \InvalidArgumentException("The current administration theme $key cannot be uninstalled.");
232       }
233       // Base themes cannot be uninstalled if sub themes are installed, and if
234       // they are not uninstalled at the same time.
235       // @todo https://www.drupal.org/node/474684 and
236       //   https://www.drupal.org/node/1297856 themes should leverage the module
237       //   dependency system.
238       if (!empty($list[$key]->sub_themes)) {
239         foreach ($list[$key]->sub_themes as $sub_key => $sub_label) {
240           if (isset($list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) {
241             throw new \InvalidArgumentException("The base theme $key cannot be uninstalled, because theme $sub_key depends on it.");
242           }
243         }
244       }
245     }
246
247     $this->cssCollectionOptimizer->deleteAll();
248     $current_theme_data = $this->state->get('system.theme.data', []);
249     foreach ($theme_list as $key) {
250       // The value is not used; the weight is ignored for themes currently.
251       $extension_config->clear("theme.$key");
252
253       // Update the current theme data accordingly.
254       unset($current_theme_data[$key]);
255
256       // Reset theme settings.
257       $theme_settings = &drupal_static('theme_get_setting');
258       unset($theme_settings[$key]);
259
260       // Remove all configuration belonging to the theme.
261       $this->configManager->uninstall('theme', $key);
262
263     }
264     // Don't check schema when uninstalling a theme since we are only clearing
265     // keys.
266     $extension_config->save(TRUE);
267     $this->state->set('system.theme.data', $current_theme_data);
268
269     // @todo Remove system_list().
270     $this->themeHandler->refreshInfo();
271     $this->resetSystem();
272
273     $this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]);
274   }
275
276   /**
277    * Resets some other systems like rebuilding the route information or caches.
278    */
279   protected function resetSystem() {
280     if ($this->routeBuilder) {
281       $this->routeBuilder->setRebuildNeeded();
282     }
283     $this->systemListReset();
284
285     // @todo It feels wrong to have the requirement to clear the local tasks
286     //   cache here.
287     Cache::invalidateTags(['local_task']);
288     $this->themeRegistryRebuild();
289   }
290
291   /**
292    * Wraps drupal_theme_rebuild().
293    */
294   protected function themeRegistryRebuild() {
295     drupal_theme_rebuild();
296   }
297
298   /**
299    * Wraps system_list_reset().
300    */
301   protected function systemListReset() {
302     system_list_reset();
303   }
304
305 }