1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Drupal\Driver; |
4
|
|
|
|
5
|
|
|
use Drupal\Component\Utility\Random; |
6
|
|
|
use Drupal\Driver\Exception\BootstrapException; |
7
|
|
|
|
8
|
|
|
use Symfony\Component\Process\Process; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Implements DriverInterface. |
12
|
|
|
*/ |
13
|
|
|
class DrushDriver extends BaseDriver { |
14
|
|
|
/** |
15
|
|
|
* Store a drush alias for tests requiring shell access. |
16
|
|
|
* |
17
|
|
|
* @var string |
18
|
|
|
*/ |
19
|
|
|
public $alias; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Stores the root path to a Drupal installation. |
23
|
|
|
* |
24
|
|
|
* This is an alternative to using drush aliases. |
25
|
|
|
* |
26
|
|
|
* @var string |
27
|
|
|
*/ |
28
|
|
|
public $root; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Store the path to drush binary. |
32
|
|
|
* |
33
|
|
|
* @var string |
34
|
|
|
*/ |
35
|
|
|
public $binary; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Track bootstrapping. |
39
|
|
|
* |
40
|
|
|
* @var bool |
41
|
|
|
*/ |
42
|
|
|
private $bootstrapped = FALSE; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Random generator. |
46
|
|
|
* |
47
|
|
|
* @var \Drupal\Component\Utility\Random |
48
|
|
|
*/ |
49
|
|
|
private $random; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Global arguments or options for drush commands. |
53
|
|
|
* |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
private $arguments = ''; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Tracks legacy drush. |
60
|
|
|
* |
61
|
|
|
* @var bool |
62
|
|
|
*/ |
63
|
|
|
protected static $isLegacyDrush; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Set drush alias or root path. |
67
|
|
|
* |
68
|
|
|
* @param string $alias |
69
|
|
|
* A drush alias. |
70
|
|
|
* @param string $root_path |
71
|
|
|
* The root path of the Drupal install. This is an alternative to using |
72
|
|
|
* aliases. |
73
|
|
|
* @param string $binary |
74
|
|
|
* The path to the drush binary. |
75
|
|
|
* @param \Drupal\Component\Utility\Random $random |
76
|
|
|
* Random generator. |
77
|
|
|
* |
78
|
|
|
* @throws \Drupal\Driver\Exception\BootstrapException |
79
|
|
|
* Thrown when a required parameter is missing. |
80
|
|
|
*/ |
81
|
|
|
public function __construct($alias = NULL, $root_path = NULL, $binary = 'drush', Random $random = NULL) { |
82
|
|
|
if (!empty($alias)) { |
83
|
|
|
// Trim off the '@' symbol if it has been added. |
84
|
|
|
$alias = ltrim($alias, '@'); |
85
|
|
|
|
86
|
|
|
$this->alias = $alias; |
87
|
|
|
} |
88
|
|
|
elseif (!empty($root_path)) { |
89
|
|
|
$this->root = realpath($root_path); |
90
|
|
|
} |
91
|
|
|
else { |
92
|
|
|
throw new BootstrapException('A drush alias or root path is required.'); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
$this->binary = $binary; |
96
|
|
|
|
97
|
|
|
if (!isset($random)) { |
98
|
|
|
$random = new Random(); |
99
|
|
|
} |
100
|
|
|
$this->random = $random; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* {@inheritdoc} |
105
|
|
|
*/ |
106
|
|
|
public function getRandom() { |
107
|
|
|
return $this->random; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* {@inheritdoc} |
112
|
|
|
*/ |
113
|
|
|
public function bootstrap() { |
114
|
|
|
// Check that the given alias works. |
115
|
|
|
// @todo check that this is a functioning alias. |
116
|
|
|
// See http://drupal.org/node/1615450 |
117
|
|
|
if (!isset($this->alias) && !isset($this->root)) { |
118
|
|
|
throw new BootstrapException('A drush alias or root path is required.'); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
// Determine if drush version is legacy. |
122
|
|
|
if (!isset(self::$isLegacyDrush)) { |
123
|
|
|
self::$isLegacyDrush = $this->isLegacyDrush(); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
$this->bootstrapped = TRUE; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Determine if drush is a legacy version. |
131
|
|
|
* |
132
|
|
|
* @return bool |
133
|
|
|
* Returns TRUE if drush is older than drush 9. |
134
|
|
|
*/ |
135
|
|
|
protected function isLegacyDrush() { |
136
|
|
|
try { |
137
|
|
|
// Try for a drush 9 version. |
138
|
|
|
$version = trim($this->drush('version', [], ['format' => 'string'])); |
139
|
|
|
return version_compare($version, '9', '<='); |
140
|
|
|
} |
141
|
|
|
catch (\RuntimeException $e) { |
142
|
|
|
// The version of drush is old enough that only `--version` was available, |
143
|
|
|
// so this is a legacy version. |
144
|
|
|
return TRUE; |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* {@inheritdoc} |
150
|
|
|
*/ |
151
|
|
|
public function isBootstrapped() { |
152
|
|
|
return $this->bootstrapped; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* {@inheritdoc} |
157
|
|
|
*/ |
158
|
|
|
public function userCreate(\stdClass $user) { |
159
|
|
|
$arguments = array( |
160
|
|
|
sprintf('"%s"', $user->name), |
161
|
|
|
); |
162
|
|
|
$options = array( |
163
|
|
|
'password' => $user->pass, |
164
|
|
|
'mail' => $user->mail, |
165
|
|
|
); |
166
|
|
|
$result = $this->drush('user-create', $arguments, $options); |
167
|
|
|
if ($uid = $this->parseUserId($result)) { |
168
|
|
|
$user->uid = $uid; |
169
|
|
|
} |
170
|
|
|
if (isset($user->roles) && is_array($user->roles)) { |
171
|
|
|
foreach ($user->roles as $role) { |
172
|
|
|
$this->userAddRole($user, $role); |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Parse user id from drush user-information output. |
179
|
|
|
*/ |
180
|
|
|
protected function parseUserId($info) { |
181
|
|
|
// Find the row containing "User ID : xxx". |
182
|
|
|
preg_match('/User ID\s+:\s+\d+/', $info, $matches); |
183
|
|
|
if (!empty($matches)) { |
184
|
|
|
// Extract the ID from the row. |
185
|
|
|
list(, $uid) = explode(':', $matches[0]); |
186
|
|
|
return (int) $uid; |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* {@inheritdoc} |
192
|
|
|
*/ |
193
|
|
|
public function userDelete(\stdClass $user) { |
194
|
|
|
$arguments = array(sprintf('"%s"', $user->name)); |
195
|
|
|
$options = array( |
196
|
|
|
'yes' => NULL, |
197
|
|
|
'delete-content' => NULL, |
198
|
|
|
); |
199
|
|
|
$this->drush('user-cancel', $arguments, $options); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* {@inheritdoc} |
204
|
|
|
*/ |
205
|
|
|
public function userAddRole(\stdClass $user, $role) { |
206
|
|
|
$arguments = array( |
207
|
|
|
sprintf('"%s"', $role), |
208
|
|
|
sprintf('"%s"', $user->name), |
209
|
|
|
); |
210
|
|
|
$this->drush('user-add-role', $arguments); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* {@inheritdoc} |
215
|
|
|
*/ |
216
|
|
|
public function fetchWatchdog($count = 10, $type = NULL, $severity = NULL) { |
217
|
|
|
$options = array( |
218
|
|
|
'count' => $count, |
219
|
|
|
'type' => $type, |
220
|
|
|
'severity' => $severity, |
221
|
|
|
); |
222
|
|
|
return $this->drush('watchdog-show', array(), $options); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* {@inheritdoc} |
227
|
|
|
*/ |
228
|
|
|
public function clearCache($type = 'all') { |
229
|
|
|
if (self::$isLegacyDrush) { |
230
|
|
|
$type = array($type); |
231
|
|
|
return $this->drush('cache-clear', $type, array()); |
232
|
|
|
} |
233
|
|
|
if (($type == 'all') || ($type == 'drush')) { |
234
|
|
|
$this->drush('cache-clear', array('drush'), array()); |
235
|
|
|
if ($type == 'drush') { |
236
|
|
|
return; |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
return $this->drush('cache:rebuild'); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* {@inheritdoc} |
244
|
|
|
*/ |
245
|
|
|
public function clearStaticCaches() { |
246
|
|
|
// The drush driver does each operation as a separate request; |
247
|
|
|
// therefore, 'clearStaticCaches' can be a no-op. |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Decodes JSON object returned by Drush. |
252
|
|
|
* |
253
|
|
|
* It will clean up any junk that may have appeared before or after the |
254
|
|
|
* JSON object. This can happen with remote Drush aliases. |
255
|
|
|
* |
256
|
|
|
* @param string $output |
257
|
|
|
* The output from Drush. |
258
|
|
|
* |
259
|
|
|
* @return object |
260
|
|
|
* The decoded JSON object. |
261
|
|
|
*/ |
262
|
|
|
protected function decodeJsonObject($output) { |
263
|
|
|
// Remove anything before the first '{'. |
264
|
|
|
$output = preg_replace('/^[^\{]*/', '', $output); |
265
|
|
|
// Remove anything after the last '}'. |
266
|
|
|
$output = preg_replace('/[^\}]*$/s', '', $output); |
267
|
|
|
return json_decode($output); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* {@inheritdoc} |
272
|
|
|
*/ |
273
|
|
View Code Duplication |
public function createEntity($entity_type, \StdClass $entity) { |
|
|
|
|
274
|
|
|
$options = array( |
275
|
|
|
'entity_type' => $entity_type, |
276
|
|
|
'entity' => $entity, |
277
|
|
|
); |
278
|
|
|
$result = $this->drush('behat', array('create-entity', escapeshellarg(json_encode($options))), array()); |
279
|
|
|
return $this->decodeJsonObject($result); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* {@inheritdoc} |
284
|
|
|
*/ |
285
|
|
View Code Duplication |
public function entityDelete($entity_type, \StdClass $entity) { |
|
|
|
|
286
|
|
|
$options = array( |
287
|
|
|
'entity_type' => $entity_type, |
288
|
|
|
'entity' => $entity, |
289
|
|
|
); |
290
|
|
|
$this->drush('behat', array('delete-entity', escapeshellarg(json_encode($options))), array()); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* {@inheritdoc} |
295
|
|
|
*/ |
296
|
|
|
public function createNode($node) { |
297
|
|
|
// Look up author by name. |
298
|
|
|
if (isset($node->author)) { |
299
|
|
|
$user_info = $this->drush('user-information', array(sprintf('"%s"', $node->author))); |
300
|
|
|
if ($uid = $this->parseUserId($user_info)) { |
301
|
|
|
$node->uid = $uid; |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
$result = $this->drush('behat', array('create-node', escapeshellarg(json_encode($node))), array()); |
305
|
|
|
return $this->decodeJsonObject($result); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* {@inheritdoc} |
310
|
|
|
*/ |
311
|
|
|
public function nodeDelete($node) { |
312
|
|
|
$this->drush('behat', array('delete-node', escapeshellarg(json_encode($node))), array()); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* {@inheritdoc} |
317
|
|
|
*/ |
318
|
|
|
public function createTerm(\stdClass $term) { |
319
|
|
|
$result = $this->drush('behat', array('create-term', escapeshellarg(json_encode($term))), array()); |
320
|
|
|
return $this->decodeJsonObject($result); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* {@inheritdoc} |
325
|
|
|
*/ |
326
|
|
|
public function termDelete(\stdClass $term) { |
327
|
|
|
$this->drush('behat', array('delete-term', escapeshellarg(json_encode($term))), array()); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* {@inheritdoc} |
332
|
|
|
*/ |
333
|
|
|
public function isField($entity_type, $field_name) { |
334
|
|
|
// If the Behat Drush Endpoint is not installed on the site-under-test, |
335
|
|
|
// then the drush() method will throw an exception. In this instance, we |
336
|
|
|
// want to treat all potential fields as non-fields. This allows the |
337
|
|
|
// Drush Driver to work with certain built-in Drush capabilities (e.g. |
338
|
|
|
// creating users) even if the Behat Drush Endpoint is not available. |
339
|
|
|
try { |
340
|
|
|
$value = array($entity_type, $field_name); |
341
|
|
|
$arguments = array('is-field', escapeshellarg(json_encode($value))); |
342
|
|
|
$result = $this->drush('behat', $arguments, array()); |
343
|
|
|
return strpos($result, "true\n") !== FALSE; |
344
|
|
|
} |
345
|
|
|
catch (\Exception $e) { |
346
|
|
|
return FALSE; |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Sets common drush arguments or options. |
352
|
|
|
* |
353
|
|
|
* @param string $arguments |
354
|
|
|
* Global arguments to add to every drush command. |
355
|
|
|
*/ |
356
|
|
|
public function setArguments($arguments) { |
357
|
|
|
$this->arguments = $arguments; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Get common drush arguments. |
362
|
|
|
*/ |
363
|
|
|
public function getArguments() { |
364
|
|
|
return $this->arguments; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Parse arguments into a string. |
369
|
|
|
* |
370
|
|
|
* @param array $arguments |
371
|
|
|
* An array of argument/option names to values. |
372
|
|
|
* |
373
|
|
|
* @return string |
374
|
|
|
* The parsed arguments. |
375
|
|
|
*/ |
376
|
|
|
protected static function parseArguments(array $arguments) { |
377
|
|
|
$string = ''; |
378
|
|
|
foreach ($arguments as $name => $value) { |
379
|
|
|
if (is_null($value)) { |
380
|
|
|
$string .= ' --' . $name; |
381
|
|
|
} |
382
|
|
|
else { |
383
|
|
|
$string .= ' --' . $name . '=' . $value; |
384
|
|
|
} |
385
|
|
|
} |
386
|
|
|
return $string; |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* Execute a drush command. |
391
|
|
|
*/ |
392
|
|
|
public function drush($command, array $arguments = array(), array $options = array()) { |
393
|
|
|
$arguments = implode(' ', $arguments); |
394
|
|
|
|
395
|
|
|
// Disable colored output from drush. |
396
|
|
|
if (isset(static::$isLegacyDrush) && static::$isLegacyDrush) { |
397
|
|
|
$options['nocolor'] = TRUE; |
398
|
|
|
} |
399
|
|
|
else { |
400
|
|
|
$options['no-ansi'] = NULL; |
401
|
|
|
} |
402
|
|
|
$string_options = $this->parseArguments($options); |
403
|
|
|
|
404
|
|
|
$alias = isset($this->alias) ? "@{$this->alias}" : '--root=' . $this->root; |
405
|
|
|
|
406
|
|
|
// Add any global arguments. |
407
|
|
|
$global = $this->getArguments(); |
408
|
|
|
|
409
|
|
|
$process = new Process("{$this->binary} {$alias} {$string_options} {$global} {$command} {$arguments}"); |
410
|
|
|
$process->setTimeout(3600); |
411
|
|
|
$process->run(); |
412
|
|
|
|
413
|
|
|
if (!$process->isSuccessful()) { |
414
|
|
|
throw new \RuntimeException($process->getErrorOutput()); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
// Some drush commands write to standard error output (for example enable |
418
|
|
|
// use drush_log which default to _drush_print_log) instead of returning a |
419
|
|
|
// string (drush status use drush_print_pipe). |
420
|
|
|
if (!$process->getOutput()) { |
421
|
|
|
return $process->getErrorOutput(); |
422
|
|
|
} |
423
|
|
|
else { |
424
|
|
|
return $process->getOutput(); |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* {@inheritdoc} |
431
|
|
|
*/ |
432
|
|
|
public function processBatch() { |
433
|
|
|
// Do nothing. Drush should internally handle any needs for processing |
434
|
|
|
// batch ops. |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* {@inheritdoc} |
439
|
|
|
*/ |
440
|
|
|
public function runCron() { |
441
|
|
|
$this->drush('cron'); |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* Run Drush commands dynamically from a DrupalContext. |
446
|
|
|
*/ |
447
|
|
|
public function __call($name, $arguments) { |
448
|
|
|
return $this->drush($name, $arguments); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
} |
452
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.