-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
PmCommands.php
389 lines (347 loc) · 15.5 KB
/
PmCommands.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<?php
namespace Drush\Drupal\Commands\pm;
use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\MissingDependencyException;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drush\Commands\DrushCommands;
use Drush\Drush;
use Drush\Exceptions\UserAbortException;
use Drush\Utils\StringUtils;
class PmCommands extends DrushCommands
{
protected $configFactory;
protected $moduleInstaller;
protected $moduleHandler;
protected $themeHandler;
protected $extensionListModule;
public function __construct(ConfigFactoryInterface $configFactory, ModuleInstallerInterface $moduleInstaller, ModuleHandlerInterface $moduleHandler, ThemeHandlerInterface $themeHandler, ModuleExtensionList $extensionListModule)
{
parent::__construct();
$this->configFactory = $configFactory;
$this->moduleInstaller = $moduleInstaller;
$this->moduleHandler = $moduleHandler;
$this->themeHandler = $themeHandler;
$this->extensionListModule = $extensionListModule;
}
public function getConfigFactory(): ConfigFactoryInterface
{
return $this->configFactory;
}
public function getModuleInstaller(): ModuleInstallerInterface
{
return $this->moduleInstaller;
}
public function getModuleHandler(): ModuleHandlerInterface
{
return $this->moduleHandler;
}
public function getThemeHandler(): ThemeHandlerInterface
{
return $this->themeHandler;
}
public function getExtensionListModule(): ModuleExtensionList
{
return $this->extensionListModule;
}
/**
* Enable one or more modules.
*
* @command pm:install
* @param $modules A comma delimited list of modules.
* @aliases in, install, pm-install, en, pm-enable, pm:enable
* @bootstrap root
*
* @usage drush pm:install --simulate content_moderation
* Display what modules would be installed but don't install them.
*/
public function install(array $modules): void
{
$modules = StringUtils::csvToArray($modules);
$todo = $this->addInstallDependencies($modules);
$todo_str = ['!list' => implode(', ', $todo)];
if (empty($todo)) {
$this->logger()->notice(dt('Already enabled: !list', ['!list' => implode(', ', $modules)]));
return;
} elseif (Drush::simulate()) {
$this->output()->writeln(dt('The following module(s) will be enabled: !list', $todo_str));
return;
} elseif (array_values($todo) !== $modules) {
$this->output()->writeln(dt('The following module(s) will be enabled: !list', $todo_str));
if (!$this->io()->confirm(dt('Do you want to continue?'))) {
throw new UserAbortException();
}
}
if (!$this->getModuleInstaller()->install($modules, true)) {
throw new \Exception('Unable to install modules.');
}
if (batch_get()) {
drush_backend_batch_process();
}
$this->logger()->success(dt('Successfully enabled: !list', $todo_str));
}
/**
* Run requirements checks on the module installation.
*
* @hook validate pm:install
*
* @throws UserAbortException
* @throws MissingDependencyException
*
* @see \drupal_check_module()
*/
public function validateEnableModules(CommandData $commandData): void
{
$modules = $commandData->input()->getArgument('modules');
$modules = StringUtils::csvToArray($modules);
$modules = $this->addInstallDependencies($modules);
if (empty($modules)) {
return;
}
require_once DRUSH_DRUPAL_CORE . '/includes/install.inc';
$error = false;
foreach ($modules as $module) {
// Note: we can't just call the API ($moduleHandler->loadInclude($module, 'install')),
// because the API ignores modules that haven't been installed yet. We have
// to do it the same way the `function drupal_check_module($module)` does.
$module_list = \Drupal::service('extension.list.module');
$file = DRUPAL_ROOT . '/' . $module_list->getPath($module) . "/$module.install";
if (is_file($file)) {
require_once $file;
}
// Once we've loaded the module, we can invoke its requirements hook.
$requirements = $this->getModuleHandler()->invoke($module, 'requirements', ['install']);
if (is_array($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) {
$error = true;
$reasons = [];
foreach ($requirements as $id => $requirement) {
if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) {
$message = $requirement['description'];
if (isset($requirement['value']) && $requirement['value']) {
$message = dt('@requirements_message (Currently using @item version @version)', ['@requirements_message' => $requirement['description'], '@item' => $requirement['title'], '@version' => $requirement['value']]);
}
$reasons[$id] = $message;
}
}
$this->logger()->error(sprintf("Unable to install module '%s' due to unmet requirement(s):%s", $module, "\n - " . implode("\n - ", $reasons)));
}
}
if ($error) {
// Allow the user to bypass the install requirements.
if (!$this->io()->confirm(dt('The module install requirements failed. Do you wish to continue?'), false)) {
throw new UserAbortException();
}
}
}
/**
* Uninstall one or more modules and their dependent modules.
*
* @command pm:uninstall
* @param $modules A comma delimited list of modules.
* @aliases un,pmu,pm-uninstall
*
* @usage drush pm:uninstall --simulate field_ui
* Display what modules would be uninstalled but don't uninstall them.
*/
public function uninstall(array $modules): void
{
$modules = StringUtils::csvToArray($modules);
$installed_modules = array_filter($modules, function ($module) {
return $this->getModuleHandler()->moduleExists($module);
});
if ($installed_modules === []) {
throw new \Exception(dt('The following module(s) are not installed: !list. No modules to uninstall.', ['!list' => implode(', ', $modules)]));
}
if ($installed_modules !== $modules) {
$this->logger()->warning(dt('The following module(s) are not installed and will not be uninstalled: !list', ['!list' => implode(', ', array_diff($modules, $installed_modules))]));
}
$list = $this->addUninstallDependencies($installed_modules);
if (Drush::simulate()) {
$this->output()->writeln(dt('The following extensions will be uninstalled: !list', ['!list' => implode(', ', $list)]));
return;
}
if (array_values($list) !== $modules) {
$this->output()->writeln(dt('The following extensions will be uninstalled: !list', ['!list' => implode(', ', $list)]));
if (!$this->io()->confirm(dt('Do you want to continue?'))) {
throw new UserAbortException();
}
}
if (!$this->getModuleInstaller()->uninstall($modules, true)) {
throw new \Exception('Unable to uninstall modules.');
}
$this->logger()->success(dt('Successfully uninstalled: !list', ['!list' => implode(', ', $list)]));
}
/**
* @hook validate pm-uninstall
*/
public function validateUninstall(CommandData $commandData): void
{
if ($modules = $commandData->input()->getArgument('modules')) {
$modules = StringUtils::csvToArray($modules);
if ($validation_reasons = $this->getModuleInstaller()->validateUninstall($modules)) {
foreach ($validation_reasons as $module => $list) {
foreach ($list as $markup) {
$reasons[$module] = "$module: " . (string) $markup;
}
}
throw new \Exception(implode("/n", $reasons));
}
}
}
/**
* Show a list of available extensions (modules and themes).
*
* @command pm:list
* @option type Only show extensions having a given type. Choices: module, theme.
* @option status Only show extensions having a given status. Choices: enabled or disabled.
* @option core Only show extensions that are in Drupal core.
* @option no-core Only show extensions that are not provided by Drupal core.
* @option package Only show extensions having a given project packages (e.g. Development).
* @field-labels
* package: Package
* project: Project
* display_name: Name
* name: Name
* type: Type
* path: Path
* status: Status
* version: Version
* @default-fields package,display_name,status,version
* @aliases pml,pm-list
* @filter-default-field display_name
*/
public function pmList($options = ['format' => 'table', 'type' => 'module,theme', 'status' => 'enabled,disabled', 'package' => self::REQ, 'core' => false, 'no-core' => false]): RowsOfFields
{
$rows = [];
$modules = $this->getExtensionListModule()->getList();
$themes = $this->getThemeHandler()->rebuildThemeData();
$both = array_merge($modules, $themes);
$package_filter = StringUtils::csvToArray(strtolower((string) $options['package']));
$type_filter = StringUtils::csvToArray(strtolower($options['type']));
$status_filter = StringUtils::csvToArray(strtolower($options['status']));
foreach ($both as $key => $extension) {
// Fill in placeholder values as needed.
$extension->info += ['package' => ''];
// Filter out test modules/themes.
if (strpos($extension->getPath(), 'tests')) {
continue;
}
$status = $this->extensionStatus($extension);
if (!in_array($extension->getType(), $type_filter)) {
unset($modules[$key]);
continue;
}
if (!in_array($status, $status_filter)) {
unset($modules[$key]);
continue;
}
// Filter out core if --no-core specified.
if ($options['no-core']) {
if ($extension->origin == 'core') {
unset($modules[$key]);
continue;
}
}
// Filter out non-core if --core specified.
if ($options['core']) {
if ($extension->origin != 'core') {
unset($modules[$key]);
continue;
}
}
// Filter by package.
if (!empty($package_filter)) {
if (!in_array(strtolower($extension->info['package']), $package_filter)) {
unset($modules[$key]);
continue;
}
}
$row = [
'package' => $extension->info['package'],
'project' => isset($extension->info['project']) ? $extension->info['project'] : '',
'display_name' => $extension->info['name'] . ' (' . $extension->getName() . ')',
'name' => $extension->getName(),
'type' => $extension->getType(),
'path' => $extension->getPath(),
'status' => ucfirst($status),
// Suppress notice when version is not present.
'version' => @$extension->info['version'],
];
$rows[$key] = $row;
}
return new RowsOfFields($rows);
}
/**
* Calculate an extension status based on current status and schema version.
*
* @param $extension
* Object of a single extension info.
*
* @return
* String describing extension status. Values: enabled|disabled.
*/
public function extensionStatus($extension): string
{
return $extension->status == 1 ? 'enabled' : 'disabled';
}
public function addInstallDependencies($modules): array
{
$module_data = $this->getExtensionListModule()->reset()->getList();
$module_list = array_combine($modules, $modules);
if ($missing_modules = array_diff_key($module_list, $module_data)) {
// One or more of the given modules doesn't exist.
throw new MissingDependencyException(sprintf('Unable to install modules %s due to missing modules %s.', implode(', ', $module_list), implode(', ', $missing_modules)));
}
$extension_config = $this->getConfigFactory()->getEditable('core.extension');
$installed_modules = $extension_config->get('module') ?: [];
// Copied from \Drupal\Core\Extension\ModuleInstaller::install
// Add dependencies to the list. The new modules will be processed as
// the while loop continues.
foreach (array_keys($module_list) as $module) {
foreach (array_keys($module_data[$module]->requires) as $dependency) {
if (!isset($module_data[$dependency])) {
// The dependency does not exist.
throw new MissingDependencyException("Unable to install modules: module '$module' is missing its dependency module $dependency.");
}
// Skip already installed modules.
if (!isset($module_list[$dependency]) && !isset($installed_modules[$dependency])) {
$module_list[$dependency] = $dependency;
}
}
}
// Remove already installed modules.
$todo = array_diff_key($module_list, $installed_modules);
return $todo;
}
public function addUninstallDependencies($modules)
{
// Get all module data so we can find dependencies and sort.
$module_data = $this->getExtensionListModule()->reset()->getList();
$module_list = array_combine($modules, $modules);
if ($diff = array_diff_key($module_list, $module_data)) {
throw new \Exception(dt('A specified extension does not exist: !diff', ['!diff' => implode(',', $diff)]));
}
$extension_config = $this->getConfigFactory()->getEditable('core.extension');
$installed_modules = $extension_config->get('module') ?: [];
// Add dependent modules to the list. The new modules will be processed as
// the while loop continues.
$profile = \Drupal::installProfile();
foreach (array_keys($module_list) as $module) {
foreach (array_keys($module_data[$module]->required_by) as $dependent) {
if (!isset($module_data[$dependent])) {
// The dependent module does not exist.
return false;
}
// Skip already uninstalled modules.
if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
$module_list[$dependent] = $dependent;
}
}
}
return $module_list;
}
}