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