Security update for permissions_by_term
[yaffs-website] / vendor / drupal / drupal-extension / src / Drupal / DrupalExtension / Context / RawDrupalContext.php
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
9 use Drupal\DrupalDriverManager;
10
11 use Drupal\DrupalExtension\Hook\Scope\AfterLanguageEnableScope;
12 use Drupal\DrupalExtension\Hook\Scope\AfterNodeCreateScope;
13 use Drupal\DrupalExtension\Hook\Scope\AfterTermCreateScope;
14 use Drupal\DrupalExtension\Hook\Scope\AfterUserCreateScope;
15 use Drupal\DrupalExtension\Hook\Scope\BaseEntityScope;
16 use Drupal\DrupalExtension\Hook\Scope\BeforeLanguageEnableScope;
17 use Drupal\DrupalExtension\Hook\Scope\BeforeNodeCreateScope;
18 use Drupal\DrupalExtension\Hook\Scope\BeforeUserCreateScope;
19 use Drupal\DrupalExtension\Hook\Scope\BeforeTermCreateScope;
20
21
22 /**
23  * Provides the raw functionality for interacting with Drupal.
24  */
25 class RawDrupalContext extends RawMinkContext implements DrupalAwareInterface {
26
27   /**
28    * Drupal driver manager.
29    *
30    * @var \Drupal\DrupalDriverManager
31    */
32   private $drupal;
33
34   /**
35    * Test parameters.
36    *
37    * @var array
38    */
39   private $drupalParameters;
40
41   /**
42    * Event dispatcher object.
43    *
44    * @var \Behat\Testwork\Hook\HookDispatcher
45    */
46   protected $dispatcher;
47
48   /**
49    * Keep track of nodes so they can be cleaned up.
50    *
51    * @var array
52    */
53   protected $nodes = array();
54
55   /**
56    * Current authenticated user.
57    *
58    * A value of FALSE denotes an anonymous user.
59    *
60    * @var \stdClass|bool
61    */
62   public $user = FALSE;
63
64   /**
65    * Keep track of all users that are created so they can easily be removed.
66    *
67    * @var array
68    */
69   protected $users = array();
70
71   /**
72    * Keep track of all terms that are created so they can easily be removed.
73    *
74    * @var array
75    */
76   protected $terms = array();
77
78   /**
79    * Keep track of any roles that are created so they can easily be removed.
80    *
81    * @var array
82    */
83   protected $roles = array();
84
85   /**
86    * Keep track of any languages that are created so they can easily be removed.
87    *
88    * @var array
89    */
90   protected $languages = array();
91
92   /**
93    * {@inheritDoc}
94    */
95   public function setDrupal(DrupalDriverManager $drupal) {
96     $this->drupal = $drupal;
97   }
98
99   /**
100    * {@inheritDoc}
101    */
102   public function getDrupal() {
103     return $this->drupal;
104   }
105
106   /**
107    * {@inheritDoc}
108    */
109   public function setDispatcher(HookDispatcher $dispatcher) {
110     $this->dispatcher = $dispatcher;
111   }
112
113   /**
114    * Set parameters provided for Drupal.
115    */
116   public function setDrupalParameters(array $parameters) {
117     $this->drupalParameters = $parameters;
118   }
119
120   /**
121    * Returns a specific Drupal parameter.
122    *
123    * @param string $name
124    *   Parameter name.
125    *
126    * @return mixed
127    */
128   public function getDrupalParameter($name) {
129     return isset($this->drupalParameters[$name]) ? $this->drupalParameters[$name] : NULL;
130   }
131
132   /**
133    * Returns a specific Drupal text value.
134    *
135    * @param string $name
136    *   Text value name, such as 'log_out', which corresponds to the default 'Log
137    *   out' link text.
138    * @throws \Exception
139    * @return
140    */
141   public function getDrupalText($name) {
142     $text = $this->getDrupalParameter('text');
143     if (!isset($text[$name])) {
144       throw new \Exception(sprintf('No such Drupal string: %s', $name));
145     }
146     return $text[$name];
147   }
148
149   /**
150    * Returns a specific css selector.
151    *
152    * @param $name
153    *   string CSS selector name
154    */
155   public function getDrupalSelector($name) {
156     $text = $this->getDrupalParameter('selectors');
157     if (!isset($text[$name])) {
158       throw new \Exception(sprintf('No such selector configured: %s', $name));
159     }
160     return $text[$name];
161   }
162
163   /**
164    * Get active Drupal Driver.
165    *
166    * @return \Drupal\Driver\DrupalDriver
167    */
168   public function getDriver($name = NULL) {
169     return $this->getDrupal()->getDriver($name);
170   }
171
172   /**
173    * Get driver's random generator.
174    */
175   public function getRandom() {
176     return $this->getDriver()->getRandom();
177   }
178
179   /**
180    * Massage node values to match the expectations on different Drupal versions.
181    *
182    * @beforeNodeCreate
183    */
184   public static function alterNodeParameters(BeforeNodeCreateScope $scope) {
185     $node = $scope->getEntity();
186
187     // Get the Drupal API version if available. This is not available when
188     // using e.g. the BlackBoxDriver or DrushDriver.
189     $api_version = NULL;
190     $driver = $scope->getContext()->getDrupal()->getDriver();
191     if ($driver instanceof \Drupal\Driver\DrupalDriver) {
192       $api_version = $scope->getContext()->getDrupal()->getDriver()->version;
193     }
194
195     // On Drupal 8 the timestamps should be in UNIX time.
196     switch ($api_version) {
197       case 8:
198         foreach (array('changed', 'created', 'revision_timestamp') as $field) {
199           if (!empty($node->$field) && !is_numeric($node->$field)) {
200             $node->$field = strtotime($node->$field);
201           }
202         }
203       break;
204     }
205   }
206
207   /**
208    * Remove any created nodes.
209    *
210    * @AfterScenario
211    */
212   public function cleanNodes() {
213     // Remove any nodes that were created.
214     foreach ($this->nodes as $node) {
215       $this->getDriver()->nodeDelete($node);
216     }
217     $this->nodes = array();
218   }
219
220   /**
221    * Remove any created users.
222    *
223    * @AfterScenario
224    */
225   public function cleanUsers() {
226     // Remove any users that were created.
227     if (!empty($this->users)) {
228       foreach ($this->users as $user) {
229         $this->getDriver()->userDelete($user);
230       }
231       $this->getDriver()->processBatch();
232       $this->users = array();
233       $this->user = FALSE;
234       if ($this->loggedIn()) {
235         $this->logout();
236       }
237     }
238   }
239
240   /**
241    * Remove any created terms.
242    *
243    * @AfterScenario
244    */
245   public function cleanTerms() {
246     // Remove any terms that were created.
247     foreach ($this->terms as $term) {
248       $this->getDriver()->termDelete($term);
249     }
250     $this->terms = array();
251   }
252
253   /**
254    * Remove any created roles.
255    *
256    * @AfterScenario
257    */
258   public function cleanRoles() {
259     // Remove any roles that were created.
260     foreach ($this->roles as $rid) {
261       $this->getDriver()->roleDelete($rid);
262     }
263     $this->roles = array();
264   }
265
266   /**
267    * Remove any created languages.
268    *
269    * @AfterScenario
270    */
271   public function cleanLanguages() {
272     // Delete any languages that were created.
273     foreach ($this->languages as $language) {
274       $this->getDriver()->languageDelete($language);
275       unset($this->languages[$language->langcode]);
276     }
277   }
278
279   /**
280    * Clear static caches.
281    *
282    * @AfterScenario @api
283    */
284   public function clearStaticCaches() {
285     $this->getDriver()->clearStaticCaches();
286   }
287
288   /**
289    * Dispatch scope hooks.
290    *
291    * @param string $scope
292    *   The entity scope to dispatch.
293    * @param \stdClass $entity
294    *   The entity.
295    */
296   protected function dispatchHooks($scopeType, \stdClass $entity) {
297     $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
298     $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
299     $callResults = $this->dispatcher->dispatchScopeHooks($scope);
300
301     // The dispatcher suppresses exceptions, throw them here if there are any.
302     foreach ($callResults as $result) {
303       if ($result->hasException()) {
304         $exception = $result->getException();
305         throw $exception;
306       }
307     }
308   }
309
310   /**
311    * Create a node.
312    *
313    * @return object
314    *   The created node.
315    */
316   public function nodeCreate($node) {
317     $this->dispatchHooks('BeforeNodeCreateScope', $node);
318     $this->parseEntityFields('node', $node);
319     $saved = $this->getDriver()->createNode($node);
320     $this->dispatchHooks('AfterNodeCreateScope', $saved);
321     $this->nodes[] = $saved;
322     return $saved;
323   }
324
325   /**
326    * Parse multi-value fields. Possible formats:
327    *    A, B, C
328    *    A - B, C - D, E - F
329    *
330    * @param string $entity_type
331    *   The entity type.
332    * @param \stdClass $entity
333    *   An object containing the entity properties and fields as properties.
334    */
335   public function parseEntityFields($entity_type, \stdClass $entity) {
336     $multicolumn_field = '';
337     $multicolumn_fields = array();
338
339     foreach (clone $entity as $field => $field_value) {
340       // Reset the multicolumn field if the field name does not contain a column.
341       if (strpos($field, ':') === FALSE) {
342         $multicolumn_field = '';
343       }
344       // Start tracking a new multicolumn field if the field name contains a ':'
345       // which is preceded by at least 1 character.
346       elseif (strpos($field, ':', 1) !== FALSE) {
347         list($multicolumn_field, $multicolumn_column) = explode(':', $field);
348       }
349       // If a field name starts with a ':' but we are not yet tracking a
350       // multicolumn field we don't know to which field this belongs.
351       elseif (empty($multicolumn_field)) {
352         throw new \Exception('Field name missing for ' . $field);
353       }
354       // Update the column name if the field name starts with a ':' and we are
355       // already tracking a multicolumn field.
356       else {
357         $multicolumn_column = substr($field, 1);
358       }
359
360       $is_multicolumn = $multicolumn_field && $multicolumn_column;
361       $field_name = $multicolumn_field ?: $field;
362       if ($this->getDriver()->isField($entity_type, $field_name)) {
363         // Split up multiple values in multi-value fields.
364         $values = array();
365         foreach (explode(', ', $field_value) as $key => $value) {
366           $columns = $value;
367           // Split up field columns if the ' - ' separator is present.
368           if (strstr($value, ' - ') !== FALSE) {
369             $columns = array();
370             foreach (explode(' - ', $value) as $column) {
371               // Check if it is an inline named column.
372               if (!$is_multicolumn && strpos($column, ': ', 1) !== FALSE) {
373                 list ($key, $column) = explode(': ', $column);
374                 $columns[$key] = $column;
375               }
376               else {
377                 $columns[] = $column;
378               }
379             }
380           }
381           // Use the column name if we are tracking a multicolumn field.
382           if ($is_multicolumn) {
383             $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
384             unset($entity->$field);
385           }
386           else {
387             $values[] = $columns;
388           }
389         }
390         // Replace regular fields inline in the entity after parsing.
391         if (!$is_multicolumn) {
392           $entity->$field_name = $values;
393           // Don't specify any value if the step author has left it blank.
394           if ($field_value === '') {
395             unset($entity->$field_name);
396           }
397         }
398       }
399     }
400
401     // Add the multicolumn fields to the entity.
402     foreach ($multicolumn_fields as $field_name => $columns) {
403       // Don't specify any value if the step author has left it blank.
404       if (count(array_filter($columns, function ($var) {
405         return ($var !== '');
406       })) > 0) {
407         $entity->$field_name = $columns;
408       }
409     }
410   }
411
412   /**
413    * Create a user.
414    *
415    * @return object
416    *   The created user.
417    */
418   public function userCreate($user) {
419     $this->dispatchHooks('BeforeUserCreateScope', $user);
420     $this->parseEntityFields('user', $user);
421     $this->getDriver()->userCreate($user);
422     $this->dispatchHooks('AfterUserCreateScope', $user);
423     $this->users[$user->name] = $this->user = $user;
424     return $user;
425   }
426
427   /**
428    * Create a term.
429    *
430    * @return object
431    *   The created term.
432    */
433   public function termCreate($term) {
434     $this->dispatchHooks('BeforeTermCreateScope', $term);
435     $this->parseEntityFields('taxonomy_term', $term);
436     $saved = $this->getDriver()->createTerm($term);
437     $this->dispatchHooks('AfterTermCreateScope', $saved);
438     $this->terms[] = $saved;
439     return $saved;
440   }
441
442   /**
443    * Creates a language.
444    *
445    * @param \stdClass $language
446    *   An object with the following properties:
447    *   - langcode: the langcode of the language to create.
448    *
449    * @return object|FALSE
450    *   The created language, or FALSE if the language was already created.
451    */
452   public function languageCreate(\stdClass $language) {
453     $this->dispatchHooks('BeforeLanguageCreateScope', $language);
454     $language = $this->getDriver()->languageCreate($language);
455     if ($language) {
456       $this->dispatchHooks('AfterLanguageCreateScope', $language);
457       $this->languages[$language->langcode] = $language;
458     }
459     return $language;
460   }
461
462   /**
463    * Log-in the current user.
464    */
465   public function login() {
466     // Check if logged in.
467     if ($this->loggedIn()) {
468       $this->logout();
469     }
470
471     if (!$this->user) {
472       throw new \Exception('Tried to login without a user.');
473     }
474
475     $this->getSession()->visit($this->locatePath('/user'));
476     $element = $this->getSession()->getPage();
477     $element->fillField($this->getDrupalText('username_field'), $this->user->name);
478     $element->fillField($this->getDrupalText('password_field'), $this->user->pass);
479     $submit = $element->findButton($this->getDrupalText('log_in'));
480     if (empty($submit)) {
481       throw new \Exception(sprintf("No submit button at %s", $this->getSession()->getCurrentUrl()));
482     }
483
484     // Log in.
485     $submit->click();
486
487     if (!$this->loggedIn()) {
488       if (isset($this->user->role)) {
489         throw new \Exception(sprintf("Unable to determine if logged in because 'log_out' link cannot be found for user '%s' with role '%s'", $this->user->name, $this->user->role));
490       }
491       else {
492         throw new \Exception(sprintf("Unable to determine if logged in because 'log_out' link cannot be found for user '%s'", $this->user->name));
493       }
494     }
495   }
496
497   /**
498    * Logs the current user out.
499    */
500   public function logout() {
501     $this->getSession()->visit($this->locatePath('/user/logout'));
502   }
503
504   /**
505    * Determine if the a user is already logged in.
506    *
507    * @return boolean
508    *   Returns TRUE if a user is logged in for this session.
509    */
510   public function loggedIn() {
511     $session = $this->getSession();
512
513     // If the session has not been started yet, or no page has yet been loaded,
514     // then this is a brand new test session and the user is not logged in.
515     if (!$session->isStarted() || !$page = $session->getPage()) {
516       return FALSE;
517     }
518
519     // Look for a css selector to determine if a user is logged in.
520     // Default is the logged-in class on the body tag.
521     // Which should work with almost any theme.
522     try {
523       if ($page->has('css', $this->getDrupalSelector('logged_in_selector'))) {
524         return TRUE;
525       }
526     } catch (DriverException $e) {
527       // This test may fail if the driver did not load any site yet.
528     }
529
530     // Some themes do not add that class to the body, so lets check if the
531     // login form is displayed on /user/login.
532     $session->visit($this->locatePath('/user/login'));
533     if (!$page->has('css', $this->getDrupalSelector('login_form_selector'))) {
534       return TRUE;
535     }
536
537     $session->visit($this->locatePath('/'));
538
539     // As a last resort, if a logout link is found, we are logged in. While not
540     // perfect, this is how Drupal SimpleTests currently work as well.
541     return $page->findLink($this->getDrupalText('log_out'));
542   }
543
544   /**
545    * User with a given role is already logged in.
546    *
547    * @param string $role
548    *   A single role, or multiple comma-separated roles in a single string.
549    *
550    * @return boolean
551    *   Returns TRUE if the current logged in user has this role (or roles).
552    */
553   public function loggedInWithRole($role) {
554     return $this->loggedIn() && $this->user && isset($this->user->role) && $this->user->role == $role;
555   }
556
557 }