Completed
Pull Request — master (#392)
by
unknown
01:29
created

RawDrupalContext::getContext()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 15
rs 9.2
cc 4
eloc 8
nc 4
nop 1
1
<?php
2
3
namespace Drupal\DrupalExtension\Context;
4
5
use Behat\MinkExtension\Context\RawMinkContext;
6
use Behat\Mink\Exception\DriverException;
7
use Behat\Testwork\Hook\HookDispatcher;
8
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
9
10
use Drupal\DrupalDriverManager;
11
use Drupal\DrupalUserManagerInterface;
12
13
use Drupal\DrupalExtension\Hook\Scope\AfterLanguageEnableScope;
14
use Drupal\DrupalExtension\Hook\Scope\AfterNodeCreateScope;
15
use Drupal\DrupalExtension\Hook\Scope\AfterTermCreateScope;
16
use Drupal\DrupalExtension\Hook\Scope\AfterUserCreateScope;
17
use Drupal\DrupalExtension\Hook\Scope\BaseEntityScope;
18
use Drupal\DrupalExtension\Hook\Scope\BeforeLanguageEnableScope;
19
use Drupal\DrupalExtension\Hook\Scope\BeforeNodeCreateScope;
20
use Drupal\DrupalExtension\Hook\Scope\BeforeUserCreateScope;
21
use Drupal\DrupalExtension\Hook\Scope\BeforeTermCreateScope;
22
23
24
/**
25
 * Provides the raw functionality for interacting with Drupal.
26
 */
27
class RawDrupalContext extends RawMinkContext implements DrupalAwareInterface {
28
29
  /**
30
   * Drupal driver manager.
31
   *
32
   * @var \Drupal\DrupalDriverManager
33
   */
34
  private $drupal;
35
36
  /**
37
   * Test parameters.
38
   *
39
   * @var array
40
   */
41
  private $drupalParameters;
42
43
  /**
44
   * Event dispatcher object.
45
   *
46
   * @var \Behat\Testwork\Hook\HookDispatcher
47
   */
48
  protected $dispatcher;
49
50
  /**
51
   * Drupal user manager.
52
   *
53
   * @var \Drupal\DrupalUserManagerInterface
54
   */
55
  protected $userManager;
56
57
  /**
58
   * Keep track of nodes so they can be cleaned up.
59
   *
60
   * @var array
61
   */
62
  protected $nodes = array();
63
64
  /**
65
   * Keep track of all terms that are created so they can easily be removed.
66
   *
67
   * @var array
68
   */
69
  protected $terms = array();
70
71
  /**
72
   * Keep track of any roles that are created so they can easily be removed.
73
   *
74
   * @var array
75
   */
76
  protected $roles = array();
77
78
  /**
79
   * Keep track of any languages that are created so they can easily be removed.
80
   *
81
   * @var array
82
   */
83
  protected $languages = array();
84
85
  /**
86
   * {@inheritDoc}
87
   */
88
  public function setDrupal(DrupalDriverManager $drupal) {
89
    $this->drupal = $drupal;
90
  }
91
92
  /**
93
   * {@inheritDoc}
94
   */
95
  public function getDrupal() {
96
    return $this->drupal;
97
  }
98
99
  /**
100
   * {@inheritDoc}
101
   */
102
  public function setUserManager(DrupalUserManagerInterface $userManager) {
103
    $this->userManager = $userManager;
104
  }
105
106
  /**
107
   * {@inheritdoc}
108
   */
109
  public function getUserManager() {
110
    return $this->userManager;
111
  }
112
113
  /**
114
   * Magic setter.
115
   */
116
  public function __set($name, $value) {
117
    switch ($name) {
118
      case 'user':
119
        trigger_error('Interacting directly with the RawDrupalContext::$user property has been deprecated. Use RawDrupalContext::getUserManager->setCurrentUser() instead.', E_USER_DEPRECATED);
120
        // Set the user on the user manager service, so it is shared between all
121
        // contexts.
122
        $this->getUserManager()->setCurrentUser($value);
123
        break;
124
125
      case 'users':
126
        trigger_error('Interacting directly with the RawDrupalContext::$users property has been deprecated. Use RawDrupalContext::getUserManager->addUser() instead.', E_USER_DEPRECATED);
127
        // Set the user on the user manager service, so it is shared between all
128
        // contexts.
129
        if (empty($value)) {
130
          $this->getUserManager()->clearUsers();
131
        }
132
        else {
133
          foreach ($value as $user) {
134
            $this->getUserManager()->addUser($user);
135
          }
136
        }
137
        break;
138
    }
139
  }
140
141
  /**
142
   * Magic getter.
143
   */
144
  public function __get($name) {
145
    switch ($name) {
146
      case 'user':
147
        trigger_error('Interacting directly with the RawDrupalContext::$user property has been deprecated. Use RawDrupalContext::getUserManager->getCurrentUser() instead.', E_USER_DEPRECATED);
148
        // Returns the current user from the user manager service. This is shared
149
        // between all contexts.
150
        return $this->getUserManager()->getCurrentUser();
151
152
      case 'users':
153
        trigger_error('Interacting directly with the RawDrupalContext::$users property has been deprecated. Use RawDrupalContext::getUserManager->getUsers() instead.', E_USER_DEPRECATED);
154
        // Returns the current user from the user manager service. This is shared
155
        // between all contexts.
156
        return $this->getUserManager()->getUsers();
157
    }
158
  }
159
160
  /**
161
   * {@inheritdoc}
162
   */
163
  public function setDispatcher(HookDispatcher $dispatcher) {
164
    $this->dispatcher = $dispatcher;
165
  }
166
167
  /**
168
   * Set parameters provided for Drupal.
169
   */
170
  public function setDrupalParameters(array $parameters) {
171
    $this->drupalParameters = $parameters;
172
  }
173
174
  /**
175
   * Returns a specific Drupal parameter.
176
   *
177
   * @param string $name
178
   *   Parameter name.
179
   *
180
   * @return mixed
181
   */
182
  public function getDrupalParameter($name) {
183
    return isset($this->drupalParameters[$name]) ? $this->drupalParameters[$name] : NULL;
184
  }
185
186
  /**
187
   * Returns a specific Drupal text value.
188
   *
189
   * @param string $name
190
   *   Text value name, such as 'log_out', which corresponds to the default 'Log
191
   *   out' link text.
192
   * @throws \Exception
193
   * @return
194
   */
195 View Code Duplication
  public function getDrupalText($name) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
196
    $text = $this->getDrupalParameter('text');
197
    if (!isset($text[$name])) {
198
      throw new \Exception(sprintf('No such Drupal string: %s', $name));
199
    }
200
    return $text[$name];
201
  }
202
203
  /**
204
   * Returns a specific css selector.
205
   *
206
   * @param $name
207
   *   string CSS selector name
208
   */
209 View Code Duplication
  public function getDrupalSelector($name) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
210
    $text = $this->getDrupalParameter('selectors');
211
    if (!isset($text[$name])) {
212
      throw new \Exception(sprintf('No such selector configured: %s', $name));
213
    }
214
    return $text[$name];
215
  }
216
217
  /**
218
   * Get active Drupal Driver.
219
   *
220
   * @return \Drupal\Driver\DrupalDriver
221
   */
222
  public function getDriver($name = NULL) {
223
    return $this->getDrupal()->getDriver($name);
224
  }
225
226
  /**
227
   * Get driver's random generator.
228
   */
229
  public function getRandom() {
230
    return $this->getDriver()->getRandom();
231
  }
232
233
  /**
234
   * Massage node values to match the expectations on different Drupal versions.
235
   *
236
   * @beforeNodeCreate
237
   */
238
  public static function alterNodeParameters(BeforeNodeCreateScope $scope) {
239
    $node = $scope->getEntity();
240
241
    // Get the Drupal API version if available. This is not available when
242
    // using e.g. the BlackBoxDriver or DrushDriver.
243
    $api_version = NULL;
244
    $driver = $scope->getContext()->getDrupal()->getDriver();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\Context as the method getDrupal() does only exist in the following implementations of said interface: Drupal\DrupalExtension\Context\ConfigContext, Drupal\DrupalExtension\Context\DrupalContext, Drupal\DrupalExtension\C...xt\DrupalSubContextBase, Drupal\DrupalExtension\Context\DrushContext, Drupal\DrupalExtension\Context\MailContext, Drupal\DrupalExtension\Context\MessageContext, Drupal\DrupalExtension\Context\RawDrupalContext, FeatureContext, FooFoo.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
245
    if ($driver instanceof \Drupal\Driver\DrupalDriver) {
246
      $api_version = $scope->getContext()->getDrupal()->getDriver()->version;
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\Context as the method getDrupal() does only exist in the following implementations of said interface: Drupal\DrupalExtension\Context\ConfigContext, Drupal\DrupalExtension\Context\DrupalContext, Drupal\DrupalExtension\C...xt\DrupalSubContextBase, Drupal\DrupalExtension\Context\DrushContext, Drupal\DrupalExtension\Context\MailContext, Drupal\DrupalExtension\Context\MessageContext, Drupal\DrupalExtension\Context\RawDrupalContext, FeatureContext, FooFoo.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
247
    }
248
249
    // On Drupal 8 the timestamps should be in UNIX time.
250
    switch ($api_version) {
251
      case 8:
252
        foreach (array('changed', 'created', 'revision_timestamp') as $field) {
253
          if (!empty($node->$field) && !is_numeric($node->$field)) {
254
            $node->$field = strtotime($node->$field);
255
          }
256
        }
257
      break;
258
    }
259
  }
260
261
  /**
262
   * Remove any created nodes.
263
   *
264
   * @AfterScenario
265
   */
266
  public function cleanNodes() {
267
    // Remove any nodes that were created.
268
    foreach ($this->nodes as $node) {
269
      $this->getDriver()->nodeDelete($node);
270
    }
271
    $this->nodes = array();
272
  }
273
274
  /**
275
   * Remove any created users.
276
   *
277
   * @AfterScenario
278
   */
279
  public function cleanUsers() {
280
    // Remove any users that were created.
281
    if ($this->userManager->hasUsers()) {
282
      foreach ($this->userManager->getUsers() as $user) {
283
        $this->getDriver()->userDelete($user);
284
      }
285
      $this->getDriver()->processBatch();
286
      $this->userManager->clearUsers();
287
      if ($this->loggedIn()) {
288
        $this->logout();
289
      }
290
    }
291
  }
292
293
  /**
294
   * Remove any created terms.
295
   *
296
   * @AfterScenario
297
   */
298
  public function cleanTerms() {
299
    // Remove any terms that were created.
300
    foreach ($this->terms as $term) {
301
      $this->getDriver()->termDelete($term);
302
    }
303
    $this->terms = array();
304
  }
305
306
  /**
307
   * Remove any created roles.
308
   *
309
   * @AfterScenario
310
   */
311
  public function cleanRoles() {
312
    // Remove any roles that were created.
313
    foreach ($this->roles as $rid) {
314
      $this->getDriver()->roleDelete($rid);
315
    }
316
    $this->roles = array();
317
  }
318
319
  /**
320
   * Remove any created languages.
321
   *
322
   * @AfterScenario
323
   */
324
  public function cleanLanguages() {
325
    // Delete any languages that were created.
326
    foreach ($this->languages as $language) {
327
      $this->getDriver()->languageDelete($language);
328
      unset($this->languages[$language->langcode]);
329
    }
330
  }
331
332
  /**
333
   * Clear static caches.
334
   *
335
   * @AfterScenario @api
336
   */
337
  public function clearStaticCaches() {
338
    $this->getDriver()->clearStaticCaches();
339
  }
340
341
  /**
342
   * Dispatch scope hooks.
343
   *
344
   * @param string $scope
0 ignored issues
show
Bug introduced by
There is no parameter named $scope. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
345
   *   The entity scope to dispatch.
346
   * @param \stdClass $entity
347
   *   The entity.
348
   */
349
  protected function dispatchHooks($scopeType, \stdClass $entity) {
350
    $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
351
    $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
352
    $callResults = $this->dispatcher->dispatchScopeHooks($scope);
353
354
    // The dispatcher suppresses exceptions, throw them here if there are any.
355
    foreach ($callResults as $result) {
356
      if ($result->hasException()) {
357
        $exception = $result->getException();
358
        throw $exception;
359
      }
360
    }
361
  }
362
363
  /**
364
   * Create a node.
365
   *
366
   * @return object
367
   *   The created node.
368
   */
369 View Code Duplication
  public function nodeCreate($node) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
370
    $this->dispatchHooks('BeforeNodeCreateScope', $node);
371
    $this->parseEntityFields('node', $node);
372
    $saved = $this->getDriver()->createNode($node);
373
    $this->dispatchHooks('AfterNodeCreateScope', $saved);
374
    $this->nodes[] = $saved;
375
    return $saved;
376
  }
377
378
  /**
379
   * Parses the field values and turns them into the format expected by Drupal.
380
   *
381
   * Multiple values in a single field must be separated by commas. Wrap the
382
   * field value in double quotes in case it should contain a comma.
383
   *
384
   * Compound field properties are identified using a ':' operator, either in
385
   * the column heading or in the cell. If multiple properties are present in a
386
   * single cell, they must be separated using ' - ', and values should not
387
   * contain ':' or ' - '.
388
   *
389
   * Possible formats for the values:
390
   *   A
391
   *   A, B, "a value, containing a comma"
392
   *   A - B
393
   *   x: A - y: B
394
   *   A - B, C - D, "E - F"
395
   *   x: A - y: B,  x: C - y: D,  "x: E - y: F"
396
   *
397
   * See field_handlers.feature for examples of usage.
398
   *
399
   * @param string $entity_type
400
   *   The entity type.
401
   * @param \stdClass $entity
402
   *   An object containing the entity properties and fields as properties.
403
   *
404
   * @throws \Exception
405
   *   Thrown when a field name is invalid.
406
   */
407
  public function parseEntityFields($entity_type, \stdClass $entity) {
408
    $multicolumn_field = '';
409
    $multicolumn_fields = array();
410
411
    foreach (clone $entity as $field => $field_value) {
0 ignored issues
show
Bug introduced by
The expression clone $entity of type object<stdClass> is not traversable.
Loading history...
412
      // Reset the multicolumn field if the field name does not contain a column.
413
      if (strpos($field, ':') === FALSE) {
414
        $multicolumn_field = '';
415
      }
416
      // Start tracking a new multicolumn field if the field name contains a ':'
417
      // which is preceded by at least 1 character.
418
      elseif (strpos($field, ':', 1) !== FALSE) {
419
        list($multicolumn_field, $multicolumn_column) = explode(':', $field);
420
      }
421
      // If a field name starts with a ':' but we are not yet tracking a
422
      // multicolumn field we don't know to which field this belongs.
423
      elseif (empty($multicolumn_field)) {
424
        throw new \Exception('Field name missing for ' . $field);
425
      }
426
      // Update the column name if the field name starts with a ':' and we are
427
      // already tracking a multicolumn field.
428
      else {
429
        $multicolumn_column = substr($field, 1);
430
      }
431
432
      $is_multicolumn = $multicolumn_field && $multicolumn_column;
0 ignored issues
show
Bug introduced by
The variable $multicolumn_column does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
433
      $field_name = $multicolumn_field ?: $field;
434
      if ($this->getDriver()->isField($entity_type, $field_name)) {
435
        // Split up multiple values in multi-value fields.
436
        $values = array();
437
        foreach (str_getcsv($field_value) as $key => $value) {
438
          $value = trim($value);
439
          $columns = $value;
440
          // Split up field columns if the ' - ' separator is present.
441
          if (strstr($value, ' - ') !== FALSE) {
442
            $columns = array();
443
            foreach (explode(' - ', $value) as $column) {
444
              // Check if it is an inline named column.
445
              if (!$is_multicolumn && strpos($column, ': ', 1) !== FALSE) {
446
                list ($key, $column) = explode(': ', $column);
447
                $columns[$key] = $column;
448
              }
449
              else {
450
                $columns[] = $column;
451
              }
452
            }
453
          }
454
          // Use the column name if we are tracking a multicolumn field.
455
          if ($is_multicolumn) {
456
            $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
457
            unset($entity->$field);
458
          }
459
          else {
460
            $values[] = $columns;
461
          }
462
        }
463
        // Replace regular fields inline in the entity after parsing.
464
        if (!$is_multicolumn) {
465
          $entity->$field_name = $values;
466
          // Don't specify any value if the step author has left it blank.
467
          if ($field_value === '') {
468
            unset($entity->$field_name);
469
          }
470
        }
471
      }
472
    }
473
474
    // Add the multicolumn fields to the entity.
475
    foreach ($multicolumn_fields as $field_name => $columns) {
476
      // Don't specify any value if the step author has left it blank.
477
      if (count(array_filter($columns, function ($var) {
478
        return ($var !== '');
479
      })) > 0) {
480
        $entity->$field_name = $columns;
481
      }
482
    }
483
  }
484
485
  /**
486
   * Create a user.
487
   *
488
   * @return object
489
   *   The created user.
490
   */
491
  public function userCreate($user) {
492
    $this->dispatchHooks('BeforeUserCreateScope', $user);
493
    $this->parseEntityFields('user', $user);
494
    $this->getDriver()->userCreate($user);
495
    $this->dispatchHooks('AfterUserCreateScope', $user);
496
    $this->userManager->addUser($user);
497
    return $user;
498
  }
499
500
  /**
501
   * Create a term.
502
   *
503
   * @return object
504
   *   The created term.
505
   */
506 View Code Duplication
  public function termCreate($term) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
507
    $this->dispatchHooks('BeforeTermCreateScope', $term);
508
    $this->parseEntityFields('taxonomy_term', $term);
509
    $saved = $this->getDriver()->createTerm($term);
510
    $this->dispatchHooks('AfterTermCreateScope', $saved);
511
    $this->terms[] = $saved;
512
    return $saved;
513
  }
514
515
  /**
516
   * Creates a language.
517
   *
518
   * @param \stdClass $language
519
   *   An object with the following properties:
520
   *   - langcode: the langcode of the language to create.
521
   *
522
   * @return object|FALSE
523
   *   The created language, or FALSE if the language was already created.
524
   */
525
  public function languageCreate(\stdClass $language) {
526
    $this->dispatchHooks('BeforeLanguageCreateScope', $language);
527
    $language = $this->getDriver()->languageCreate($language);
528
    if ($language) {
529
      $this->dispatchHooks('AfterLanguageCreateScope', $language);
530
      $this->languages[$language->langcode] = $language;
531
    }
532
    return $language;
533
  }
534
535
  /**
536
   * Log-in the given user.
537
   *
538
   * @param \stdClass $user
539
   *   The user to log in.
540
   */
541
  public function login(\stdClass $user) {
542
    $manager = $this->getUserManager();
543
544
    // Check if logged in.
545
    if ($this->loggedIn()) {
546
      $this->logout();
547
    }
548
549
    $this->getSession()->visit($this->locatePath('/user'));
550
    $element = $this->getSession()->getPage();
551
    $element->fillField($this->getDrupalText('username_field'), $user->name);
552
    $element->fillField($this->getDrupalText('password_field'), $user->pass);
553
    $submit = $element->findButton($this->getDrupalText('log_in'));
554
    if (empty($submit)) {
555
      throw new \Exception(sprintf("No submit button at %s", $this->getSession()->getCurrentUrl()));
556
    }
557
558
    // Log in.
559
    $submit->click();
560
561
    if (!$this->loggedIn()) {
562
      if (isset($user->role)) {
563
        throw new \Exception(sprintf("Unable to determine if logged in because 'log_out' link cannot be found for user '%s' with role '%s'", $user->name, $user->role));
564
      }
565
      else {
566
        throw new \Exception(sprintf("Unable to determine if logged in because 'log_out' link cannot be found for user '%s'", $user->name));
567
      }
568
    }
569
570
    $manager->setCurrentUser($user);
571
  }
572
573
  /**
574
   * Logs the current user out.
575
   */
576
  public function logout() {
577
    $this->getSession()->visit($this->locatePath('/user/logout'));
578
    $this->getUserManager()->setCurrentUser(FALSE);
579
  }
580
581
  /**
582
   * Determine if the a user is already logged in.
583
   *
584
   * @return boolean
585
   *   Returns TRUE if a user is logged in for this session.
586
   */
587
  public function loggedIn() {
588
    $session = $this->getSession();
589
590
    // If the session has not been started yet, or no page has yet been loaded,
591
    // then this is a brand new test session and the user is not logged in.
592
    if (!$session->isStarted() || !$page = $session->getPage()) {
593
      return FALSE;
594
    }
595
596
    // Look for a css selector to determine if a user is logged in.
597
    // Default is the logged-in class on the body tag.
598
    // Which should work with almost any theme.
599
    try {
600
      if ($page->has('css', $this->getDrupalSelector('logged_in_selector'))) {
601
        return TRUE;
602
      }
603
    } catch (DriverException $e) {
604
      // This test may fail if the driver did not load any site yet.
605
    }
606
607
    // Some themes do not add that class to the body, so lets check if the
608
    // login form is displayed on /user/login.
609
    $session->visit($this->locatePath('/user/login'));
610
    if (!$page->has('css', $this->getDrupalSelector('login_form_selector'))) {
611
      return TRUE;
612
    }
613
614
    $session->visit($this->locatePath('/'));
615
616
    // As a last resort, if a logout link is found, we are logged in. While not
617
    // perfect, this is how Drupal SimpleTests currently work as well.
618
    if ($page->findLink($this->getDrupalText('log_out'))) {
619
      return TRUE;
620
    }
621
622
    // The user appears to be anonymous. Clear the current user from the user
623
    // manager so this reflects the actual situation.
624
    $this->getUserManager()->setCurrentUser(FALSE);
625
    return FALSE;
626
  }
627
628
  /**
629
   * User with a given role is already logged in.
630
   *
631
   * @param string $role
632
   *   A single role, or multiple comma-separated roles in a single string.
633
   *
634
   * @return boolean
635
   *   Returns TRUE if the current logged in user has this role (or roles).
636
   */
637
  public function loggedInWithRole($role) {
638
    return $this->loggedIn() && $this->getUserManager()->currentUserHasRole($role);
639
  }
640
641
  /**
642
   * Returns the Behat context that corresponds with the given class name.
643
   *
644
   * This is inspired by InitializedContextEnvironment::getContext() but also
645
   * returns subclasses of the given class name. This allows us to retrieve for
646
   * example DrupalContext even if it is overridden in a project.
647
   *
648
   * @param string $class
649
   *   A fully namespaced class name.
650
   * 
651
   * @return \Behat\Behat\Context\Context|false
652
   *   The requested context, or FALSE if the context is not registered.
653
   *
654
   * @throws \Exception
655
   *   Thrown when the environment is not yet initialized, meaning that contexts
656
   *   cannot yet be retrieved.
657
   */
658
  protected function getContext($class) {
659
    /** @var InitializedContextEnvironment $environment */
660
    $environment = $this->drupal->getEnvironment();
661
    if (!$environment instanceof InitializedContextEnvironment) {
662
      throw new \Exception('Cannot retrieve contexts when the environment is not yet initialized.');
663
    }
664
665
    foreach ($environment->getContexts() as $context) {
666
      if ($context instanceof $class) {
667
        return $context;
668
      }
669
    }
670
671
    return FALSE;
672
  }
673
674
}
675