Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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