Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Form fields helper.
  19   *
  20   * @package    core
  21   * @category   test
  22   * @copyright  2013 David MonllaĆ³
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
  27  
  28  use Behat\Mink\Session as Session,
  29      Behat\Mink\Element\NodeElement as NodeElement,
  30      Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
  31      Behat\MinkExtension\Context\RawMinkContext as RawMinkContext;
  32  
  33  /**
  34   * Helper to interact with form fields.
  35   *
  36   * @package    core
  37   * @category   test
  38   * @copyright  2013 David MonllaĆ³
  39   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class behat_field_manager {
  42  
  43      /**
  44       * Gets an instance of the form field from it's label
  45       *
  46       * @param string $label
  47       * @param RawMinkContext $context
  48       * @return behat_form_field
  49       */
  50      public static function get_form_field_from_label($label, RawMinkContext $context) {
  51          // There are moodle form elements that are not directly related with
  52          // a basic HTML form field, we should also take care of them.
  53          // The DOM node.
  54          $fieldnode = $context->find_field($label);
  55  
  56          // The behat field manager.
  57          $field = self::get_form_field($fieldnode, $context->getSession());
  58          return $field;
  59      }
  60  
  61      /**
  62       * Gets an instance of the form field.
  63       *
  64       * Not all the fields are part of a moodle form, in this
  65       * cases it fallsback to the generic form field. Also note
  66       * that this generic field type is using a generic setValue()
  67       * method from the Behat API, which is not always good to set
  68       * the value of form elements.
  69       *
  70       * @param NodeElement $fieldnode
  71       * @param Session $session The behat browser session
  72       * @return behat_form_field
  73       */
  74      public static function get_form_field(NodeElement $fieldnode, Session $session) {
  75  
  76          // Get the field type if is part of a moodleform.
  77          if (self::is_moodleform_field($fieldnode)) {
  78              $type = self::get_field_node_type($fieldnode, $session);
  79          }
  80  
  81          // If is not a moodleforms field use the base field type.
  82          if (empty($type)) {
  83              $type = 'field';
  84          }
  85  
  86          return self::get_field_instance($type, $fieldnode, $session);
  87      }
  88  
  89      /**
  90       * Returns the appropiate behat_form_field according to the provided type.
  91       *
  92       * It defaults to behat_form_field.
  93       *
  94       * @param string $type The field type (checkbox, date_selector, text...)
  95       * @param NodeElement $fieldnode
  96       * @param Session $session The behat session
  97       * @return behat_form_field
  98       */
  99      public static function get_field_instance($type, NodeElement $fieldnode, Session $session) {
 100          global $CFG;
 101  
 102          // If the field is not part of a moodleform, we should still try to find out
 103          // which field type are we dealing with.
 104          if ($type == 'field' && $guessedtype = self::guess_field_type($fieldnode, $session)) {
 105              $type = $guessedtype;
 106          }
 107  
 108          $classname = 'behat_form_' . $type;
 109  
 110          // Fallsback on the type guesser if nothing specific exists.
 111          $classpath = $CFG->libdir . '/behat/form_field/' . $classname . '.php';
 112          if (!file_exists($classpath)) {
 113              $classname = 'behat_form_field';
 114              $classpath = $CFG->libdir . '/behat/form_field/' . $classname . '.php';
 115          }
 116  
 117          // Returns the instance.
 118          require_once($classpath);
 119          return new $classname($session, $fieldnode);
 120      }
 121  
 122      /**
 123       * Guesses a basic field type and returns it.
 124       *
 125       * This method is intended to detect HTML form fields when no
 126       * moodleform-specific elements have been detected.
 127       *
 128       * @param NodeElement $fieldnode
 129       * @param Session $session
 130       * @return string|bool The field type or false.
 131       */
 132      public static function guess_field_type(NodeElement $fieldnode, Session $session) {
 133          [
 134              'document' => $document,
 135              'node' => $node,
 136          ] = self::get_dom_elements_for_node($fieldnode, $session);
 137  
 138          // If the type is explicitly set on the element pointed to by the label - use it.
 139          if ($fieldtype = $node->getAttribute('data-fieldtype')) {
 140              return self::normalise_fieldtype($fieldtype);
 141          }
 142  
 143          // Textareas are considered text based elements.
 144          $tagname = strtolower($node->nodeName);
 145          if ($tagname == 'textarea') {
 146              $xpath = new \DOMXPath($document);
 147  
 148              // If there is an iframe with $id + _ifr there a TinyMCE editor loaded.
 149              if ($xpath->query('//div[@id="' . $node->getAttribute('id') . 'editable"]')->count() !== 0) {
 150                  return 'editor';
 151              }
 152              return 'textarea';
 153  
 154          }
 155  
 156          if ($tagname == 'input') {
 157              switch ($node->getAttribute('type')) {
 158                  case 'text':
 159                  case 'password':
 160                  case 'email':
 161                  case 'file':
 162                      return 'text';
 163                  case 'checkbox':
 164                      return 'checkbox';
 165                      break;
 166                  case 'radio':
 167                      return 'radio';
 168                      break;
 169                  default:
 170                      // Here we return false because all text-based
 171                      // fields should be included in the first switch case.
 172                      return false;
 173              }
 174  
 175          }
 176  
 177          if ($tagname == 'select') {
 178              // Select tag.
 179              return 'select';
 180          }
 181  
 182          if ($tagname == 'span') {
 183              if ($node->hasAttribute('data-inplaceeditable') && $node->getAttribute('data-inplaceeditable')) {
 184                  // Determine appropriate editable type of this field (text or select).
 185                  if ($node->getAttribute('data-type') == 'select') {
 186                      return 'inplaceeditable_select';
 187                  } else {
 188                      return 'inplaceeditable';
 189                  }
 190              }
 191          }
 192  
 193          // We can not provide a closer field type.
 194          return false;
 195      }
 196  
 197      /**
 198       * Detects when the field is a moodleform field type.
 199       *
 200       * Note that there are fields inside moodleforms that are not
 201       * moodleform element; this method can not detect this, this will
 202       * be managed by get_field_node_type, after failing to find the form
 203       * element element type.
 204       *
 205       * @param NodeElement $fieldnode
 206       * @return bool
 207       */
 208      protected static function is_moodleform_field(NodeElement $fieldnode) {
 209  
 210          // We already waited when getting the NodeElement and we don't want an exception if it's not part of a moodleform.
 211          $parentformfound = $fieldnode->find('xpath',
 212              "/ancestor::form[contains(concat(' ', normalize-space(@class), ' '), ' mform ')]"
 213          );
 214  
 215          return ($parentformfound != false);
 216      }
 217  
 218      /**
 219       * Get the DOMDocument and DOMElement for a NodeElement.
 220       *
 221       * @param NodeElement $fieldnode
 222       * @param Session $session
 223       * @return array
 224       */
 225      protected static function get_dom_elements_for_node(NodeElement $fieldnode, Session $session): array {
 226          $html = $session->getPage()->getContent();
 227  
 228          $document = new \DOMDocument();
 229  
 230          $previousinternalerrors = libxml_use_internal_errors(true);
 231          $document->loadHTML($html, LIBXML_HTML_NODEFDTD | LIBXML_BIGLINES);
 232          libxml_clear_errors();
 233          libxml_use_internal_errors($previousinternalerrors);
 234  
 235          $xpath = new \DOMXPath($document);
 236          $node = $xpath->query($fieldnode->getXpath())->item(0);
 237  
 238          return [
 239              'document' => $document,
 240              'node' => $node,
 241          ];
 242      }
 243  
 244      /**
 245       * Recursive method to find the field type.
 246       *
 247       * Depending on the field the felement class node is in a level or in another. We
 248       * look recursively for a parent node with a 'felement' class to find the field type.
 249       *
 250       * @param NodeElement $fieldnode The current node.
 251       * @param Session $session The behat browser session
 252       * @return null|string A text description of the node type, or null if one could not be accurately determined
 253       */
 254      protected static function get_field_node_type(NodeElement $fieldnode, Session $session): ?string {
 255          [
 256              'document' => $document,
 257              'node' => $node,
 258          ] = self::get_dom_elements_for_node($fieldnode, $session);
 259  
 260          return self::get_field_type($document, $node, $session);
 261      }
 262  
 263      /**
 264       * Get the field type from the specified DOMElement.
 265       *
 266       * @param \DOMDocument $document
 267       * @param \DOMElement $node
 268       * @param Session $session
 269       * @return null|string
 270       */
 271      protected static function get_field_type(\DOMDocument $document, \DOMElement $node, Session $session): ?string {
 272          $xpath = new \DOMXPath($document);
 273  
 274          if ($node->getAttribute('name') === 'availabilityconditionsjson') {
 275              // Special handling for availability field which requires custom JavaScript.
 276              return 'availability';
 277          }
 278  
 279          if ($node->nodeName == 'html') {
 280              // The top of the document has been reached.
 281              return null;
 282          }
 283  
 284          // If the type is explictly set on the element pointed to by the label - use it.
 285          $fieldtype = $node->getAttribute('data-fieldtype');
 286          if ($fieldtype) {
 287              return self::normalise_fieldtype($fieldtype);
 288          }
 289  
 290          if ($xpath->query('/ancestor::*[@data-passwordunmaskid]', $node)->count() !== 0) {
 291              // This element has a passwordunmaskid as a parent.
 292              return 'passwordunmask';
 293          }
 294  
 295          // Fetch the parentnode only once.
 296          $parentnode = $node->parentNode;
 297          if ($parentnode instanceof \DOMDocument) {
 298              return null;
 299          }
 300  
 301          // Check the parent fieldtype before we check classes.
 302          $fieldtype = $parentnode->getAttribute('data-fieldtype');
 303          if ($fieldtype) {
 304              return self::normalise_fieldtype($fieldtype);
 305          }
 306  
 307          // We look for a parent node with 'felement' class.
 308          if ($class = $parentnode->getAttribute('class')) {
 309              if (strstr($class, 'felement') != false) {
 310                  // Remove 'felement f' from class value.
 311                  return substr($class, 10);
 312              }
 313  
 314              // Stop propagation through the DOM, if it does not have a felement is not part of a moodle form.
 315              if (strstr($class, 'fcontainer') != false) {
 316                  return null;
 317              }
 318          }
 319  
 320          // Move up the tree.
 321          return self::get_field_type($document, $parentnode, $session);
 322      }
 323  
 324      /**
 325       * Normalise the field type.
 326       *
 327       * @param string $fieldtype
 328       * @return string
 329       */
 330      protected static function normalise_fieldtype(string $fieldtype): string {
 331          if ($fieldtype === 'tags') {
 332              return 'autocomplete';
 333          }
 334  
 335          return $fieldtype;
 336      }
 337  
 338      /**
 339       * Gets an instance of the form field.
 340       *
 341       * Not all the fields are part of a moodle form, in this
 342       * cases it fallsback to the generic form field. Also note
 343       * that this generic field type is using a generic setValue()
 344       * method from the Behat API, which is not always good to set
 345       * the value of form elements.
 346       *
 347       * @deprecated since Moodle 2.6 MDL-39634 - please do not use this function any more.
 348       * @todo MDL-XXXXX This will be deleted in Moodle 2.8
 349       * @see behat_field_manager::get_form_field()
 350       * @param NodeElement $fieldnode
 351       * @param string $locator
 352       * @param Session $session The behat browser session
 353       * @return behat_form_field
 354       */
 355      public static function get_field(NodeElement $fieldnode, $locator, Session $session) {
 356          debugging('Function behat_field_manager::get_field() is deprecated, ' .
 357              'please use function behat_field_manager::get_form_field() instead', DEBUG_DEVELOPER);
 358  
 359          return self::get_form_field($fieldnode, $session);
 360      }
 361  
 362      /**
 363       * Recursive method to find the field type.
 364       *
 365       * Depending on the field the felement class node is in a level or in another. We
 366       * look recursively for a parent node with a 'felement' class to find the field type.
 367       *
 368       * @deprecated since Moodle 2.6 MDL-39634 - please do not use this function any more.
 369       * @todo MDL-XXXXX This will be deleted in Moodle 2.8
 370       * @see behat_field_manager::get_field_node_type()
 371       * @param NodeElement $fieldnode The current node.
 372       * @param string $locator
 373       * @param Session $session The behat browser session
 374       * @return mixed A NodeElement if we continue looking for the element type and String or false when we are done.
 375       */
 376      protected static function get_node_type(NodeElement $fieldnode, $locator, Session $session) {
 377          debugging('Function behat_field_manager::get_node_type() is deprecated, ' .
 378              'please use function behat_field_manager::get_field_node_type() instead', DEBUG_DEVELOPER);
 379  
 380          return self::get_field_node_type($fieldnode, $session);
 381      }
 382  }