Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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                  case 'number':
 163                      return 'text';
 164                  case 'checkbox':
 165                      return 'checkbox';
 166                  case 'radio':
 167                      return 'radio';
 168                  default:
 169                      // Here we return false because all text-based
 170                      // fields should be included in the first switch case.
 171                      return false;
 172              }
 173  
 174          }
 175  
 176          if ($tagname == 'select') {
 177              // Select tag.
 178              return 'select';
 179          }
 180  
 181          if ($tagname == 'span') {
 182              if ($node->hasAttribute('data-inplaceeditable') && $node->getAttribute('data-inplaceeditable')) {
 183                  // Determine appropriate editable type of this field (text or select).
 184                  if ($node->getAttribute('data-type') == 'select') {
 185                      return 'inplaceeditable_select';
 186                  } else {
 187                      return 'inplaceeditable';
 188                  }
 189              }
 190          }
 191  
 192          if ($tagname == 'div') {
 193              if ($node->getAttribute('role') == 'combobox') {
 194                  return 'select_menu';
 195              }
 196          }
 197  
 198          // We can not provide a closer field type.
 199          return false;
 200      }
 201  
 202      /**
 203       * Detects when the field is a moodleform field type.
 204       *
 205       * Note that there are fields inside moodleforms that are not
 206       * moodleform element; this method can not detect this, this will
 207       * be managed by get_field_node_type, after failing to find the form
 208       * element element type.
 209       *
 210       * @param NodeElement $fieldnode
 211       * @return bool
 212       */
 213      protected static function is_moodleform_field(NodeElement $fieldnode) {
 214  
 215          // We already waited when getting the NodeElement and we don't want an exception if it's not part of a moodleform.
 216          $parentformfound = $fieldnode->find('xpath',
 217              "/ancestor::form[contains(concat(' ', normalize-space(@class), ' '), ' mform ')]"
 218          );
 219  
 220          return ($parentformfound != false);
 221      }
 222  
 223      /**
 224       * Get the DOMDocument and DOMElement for a NodeElement.
 225       *
 226       * @param NodeElement $fieldnode
 227       * @param Session $session
 228       * @return array
 229       */
 230      protected static function get_dom_elements_for_node(NodeElement $fieldnode, Session $session): array {
 231          $html = $session->getPage()->getContent();
 232  
 233          $document = new \DOMDocument();
 234  
 235          $previousinternalerrors = libxml_use_internal_errors(true);
 236          $document->loadHTML($html, LIBXML_HTML_NODEFDTD | LIBXML_BIGLINES);
 237          libxml_clear_errors();
 238          libxml_use_internal_errors($previousinternalerrors);
 239  
 240          $xpath = new \DOMXPath($document);
 241          $node = $xpath->query($fieldnode->getXpath())->item(0);
 242  
 243          return [
 244              'document' => $document,
 245              'node' => $node,
 246          ];
 247      }
 248  
 249      /**
 250       * Recursive method to find the field type.
 251       *
 252       * Depending on the field the felement class node is in a level or in another. We
 253       * look recursively for a parent node with a 'felement' class to find the field type.
 254       *
 255       * @param NodeElement $fieldnode The current node.
 256       * @param Session $session The behat browser session
 257       * @return null|string A text description of the node type, or null if one could not be accurately determined
 258       */
 259      protected static function get_field_node_type(NodeElement $fieldnode, Session $session): ?string {
 260          [
 261              'document' => $document,
 262              'node' => $node,
 263          ] = self::get_dom_elements_for_node($fieldnode, $session);
 264  
 265          return self::get_field_type($document, $node, $session);
 266      }
 267  
 268      /**
 269       * Get the field type from the specified DOMElement.
 270       *
 271       * @param \DOMDocument $document
 272       * @param \DOMElement $node
 273       * @param Session $session
 274       * @return null|string
 275       */
 276      protected static function get_field_type(\DOMDocument $document, \DOMElement $node, Session $session): ?string {
 277          $xpath = new \DOMXPath($document);
 278  
 279          if ($node->getAttribute('name') === 'availabilityconditionsjson') {
 280              // Special handling for availability field which requires custom JavaScript.
 281              return 'availability';
 282          }
 283  
 284          if ($node->nodeName == 'html') {
 285              // The top of the document has been reached.
 286              return null;
 287          }
 288  
 289          // If the type is explictly set on the element pointed to by the label - use it.
 290          $fieldtype = $node->getAttribute('data-fieldtype');
 291          if ($fieldtype) {
 292              return self::normalise_fieldtype($fieldtype);
 293          }
 294  
 295          if ($xpath->query('/ancestor::*[@data-passwordunmaskid]', $node)->count() !== 0) {
 296              // This element has a passwordunmaskid as a parent.
 297              return 'passwordunmask';
 298          }
 299  
 300          // Fetch the parentnode only once.
 301          $parentnode = $node->parentNode;
 302          if ($parentnode instanceof \DOMDocument) {
 303              return null;
 304          }
 305  
 306          // Check the parent fieldtype before we check classes.
 307          $fieldtype = $parentnode->getAttribute('data-fieldtype');
 308          if ($fieldtype) {
 309              return self::normalise_fieldtype($fieldtype);
 310          }
 311  
 312          // We look for a parent node with 'felement' class.
 313          if ($class = $parentnode->getAttribute('class')) {
 314              if (strstr($class, 'felement') != false) {
 315                  // Remove 'felement f' from class value.
 316                  return substr($class, 10);
 317              }
 318  
 319              // Stop propagation through the DOM, if it does not have a felement is not part of a moodle form.
 320              if (strstr($class, 'fcontainer') != false) {
 321                  return null;
 322              }
 323          }
 324  
 325          // Move up the tree.
 326          return self::get_field_type($document, $parentnode, $session);
 327      }
 328  
 329      /**
 330       * Normalise the field type.
 331       *
 332       * @param string $fieldtype
 333       * @return string
 334       */
 335      protected static function normalise_fieldtype(string $fieldtype): string {
 336          if ($fieldtype === 'tags') {
 337              return 'autocomplete';
 338          }
 339  
 340          return $fieldtype;
 341      }
 342  
 343      /**
 344       * @deprecated since Moodle 2.6 MDL-39634 - please do not use this function any more.
 345       */
 346      public static function get_field() {
 347          throw new coding_exception('behat_field_manager::get_field() can not be used any more, ' .
 348              'function behat_field_manager::get_form_field() instead');
 349      }
 350  
 351      /**
 352       * @deprecated since Moodle 2.6 MDL-39634 - please do not use this function any more.
 353       */
 354      protected static function get_node_type() {
 355          throw new coding_exception('behat_field_manager::get_node_type() can not be used any more, ' .
 356              'function behat_field_manager::get_field_node_type() instead');
 357      }
 358  }