[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/htmlform/ -> HTMLForm.php (source)

   1  <?php
   2  
   3  /**
   4   * HTML form generation and submission handling.
   5   *
   6   * This program is free software; you can redistribute it and/or modify
   7   * it under the terms of the GNU General Public License as published by
   8   * the Free Software Foundation; either version 2 of the License, or
   9   * (at your option) any later version.
  10   *
  11   * This program is distributed in the hope that it will be useful,
  12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14   * GNU General Public License for more details.
  15   *
  16   * You should have received a copy of the GNU General Public License along
  17   * with this program; if not, write to the Free Software Foundation, Inc.,
  18   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19   * http://www.gnu.org/copyleft/gpl.html
  20   *
  21   * @file
  22   */
  23  
  24  /**
  25   * Object handling generic submission, CSRF protection, layout and
  26   * other logic for UI forms. in a reusable manner.
  27   *
  28   * In order to generate the form, the HTMLForm object takes an array
  29   * structure detailing the form fields available. Each element of the
  30   * array is a basic property-list, including the type of field, the
  31   * label it is to be given in the form, callbacks for validation and
  32   * 'filtering', and other pertinent information.
  33   *
  34   * Field types are implemented as subclasses of the generic HTMLFormField
  35   * object, and typically implement at least getInputHTML, which generates
  36   * the HTML for the input field to be placed in the table.
  37   *
  38   * You can find extensive documentation on the www.mediawiki.org wiki:
  39   *  - https://www.mediawiki.org/wiki/HTMLForm
  40   *  - https://www.mediawiki.org/wiki/HTMLForm/tutorial
  41   *
  42   * The constructor input is an associative array of $fieldname => $info,
  43   * where $info is an Associative Array with any of the following:
  44   *
  45   *    'class'               -- the subclass of HTMLFormField that will be used
  46   *                             to create the object.  *NOT* the CSS class!
  47   *    'type'                -- roughly translates into the <select> type attribute.
  48   *                             if 'class' is not specified, this is used as a map
  49   *                             through HTMLForm::$typeMappings to get the class name.
  50   *    'default'             -- default value when the form is displayed
  51   *    'id'                  -- HTML id attribute
  52   *    'cssclass'            -- CSS class
  53   *    'csshelpclass'        -- CSS class used to style help text
  54   *    'options'             -- associative array mapping labels to values.
  55   *                             Some field types support multi-level arrays.
  56   *    'options-messages'    -- associative array mapping message keys to values.
  57   *                             Some field types support multi-level arrays.
  58   *    'options-message'     -- message key to be parsed to extract the list of
  59   *                             options (like 'ipbreason-dropdown').
  60   *    'label-message'       -- message key for a message to use as the label.
  61   *                             can be an array of msg key and then parameters to
  62   *                             the message.
  63   *    'label'               -- alternatively, a raw text message. Overridden by
  64   *                             label-message
  65   *    'help'                -- message text for a message to use as a help text.
  66   *    'help-message'        -- message key for a message to use as a help text.
  67   *                             can be an array of msg key and then parameters to
  68   *                             the message.
  69   *                             Overwrites 'help-messages' and 'help'.
  70   *    'help-messages'       -- array of message key. As above, each item can
  71   *                             be an array of msg key and then parameters.
  72   *                             Overwrites 'help'.
  73   *    'required'            -- passed through to the object, indicating that it
  74   *                             is a required field.
  75   *    'size'                -- the length of text fields
  76   *    'filter-callback      -- a function name to give you the chance to
  77   *                             massage the inputted value before it's processed.
  78   *                             @see HTMLForm::filter()
  79   *    'validation-callback' -- a function name to give you the chance
  80   *                             to impose extra validation on the field input.
  81   *                             @see HTMLForm::validate()
  82   *    'name'                -- By default, the 'name' attribute of the input field
  83   *                             is "wp{$fieldname}".  If you want a different name
  84   *                             (eg one without the "wp" prefix), specify it here and
  85   *                             it will be used without modification.
  86   *
  87   * Since 1.20, you can chain mutators to ease the form generation:
  88   * @par Example:
  89   * @code
  90   * $form = new HTMLForm( $someFields );
  91   * $form->setMethod( 'get' )
  92   *      ->setWrapperLegendMsg( 'message-key' )
  93   *      ->prepareForm()
  94   *      ->displayForm( '' );
  95   * @endcode
  96   * Note that you will have prepareForm and displayForm at the end. Other
  97   * methods call done after that would simply not be part of the form :(
  98   *
  99   * @todo Document 'section' / 'subsection' stuff
 100   */
 101  class HTMLForm extends ContextSource {
 102      // A mapping of 'type' inputs onto standard HTMLFormField subclasses
 103      public static $typeMappings = array(
 104          'api' => 'HTMLApiField',
 105          'text' => 'HTMLTextField',
 106          'textarea' => 'HTMLTextAreaField',
 107          'select' => 'HTMLSelectField',
 108          'radio' => 'HTMLRadioField',
 109          'multiselect' => 'HTMLMultiSelectField',
 110          'limitselect' => 'HTMLSelectLimitField',
 111          'check' => 'HTMLCheckField',
 112          'toggle' => 'HTMLCheckField',
 113          'int' => 'HTMLIntField',
 114          'float' => 'HTMLFloatField',
 115          'info' => 'HTMLInfoField',
 116          'selectorother' => 'HTMLSelectOrOtherField',
 117          'selectandother' => 'HTMLSelectAndOtherField',
 118          'submit' => 'HTMLSubmitField',
 119          'hidden' => 'HTMLHiddenField',
 120          'edittools' => 'HTMLEditTools',
 121          'checkmatrix' => 'HTMLCheckMatrix',
 122          'cloner' => 'HTMLFormFieldCloner',
 123          'autocompleteselect' => 'HTMLAutoCompleteSelectField',
 124          // HTMLTextField will output the correct type="" attribute automagically.
 125          // There are about four zillion other HTML5 input types, like range, but
 126          // we don't use those at the moment, so no point in adding all of them.
 127          'email' => 'HTMLTextField',
 128          'password' => 'HTMLTextField',
 129          'url' => 'HTMLTextField',
 130      );
 131  
 132      public $mFieldData;
 133  
 134      protected $mMessagePrefix;
 135  
 136      /** @var HTMLFormField[] */
 137      protected $mFlatFields;
 138  
 139      protected $mFieldTree;
 140      protected $mShowReset = false;
 141      protected $mShowSubmit = true;
 142      protected $mSubmitModifierClass = 'mw-ui-constructive';
 143  
 144      protected $mSubmitCallback;
 145      protected $mValidationErrorMessage;
 146  
 147      protected $mPre = '';
 148      protected $mHeader = '';
 149      protected $mFooter = '';
 150      protected $mSectionHeaders = array();
 151      protected $mSectionFooters = array();
 152      protected $mPost = '';
 153      protected $mId;
 154      protected $mTableId = '';
 155  
 156      protected $mSubmitID;
 157      protected $mSubmitName;
 158      protected $mSubmitText;
 159      protected $mSubmitTooltip;
 160  
 161      protected $mTitle;
 162      protected $mMethod = 'post';
 163      protected $mWasSubmitted = false;
 164  
 165      /**
 166       * Form action URL. false means we will use the URL to set Title
 167       * @since 1.19
 168       * @var bool|string
 169       */
 170      protected $mAction = false;
 171  
 172      protected $mUseMultipart = false;
 173      protected $mHiddenFields = array();
 174      protected $mButtons = array();
 175  
 176      protected $mWrapperLegend = false;
 177  
 178      /**
 179       * Salt for the edit token.
 180       * @var string|array
 181       */
 182      protected $mTokenSalt = '';
 183  
 184      /**
 185       * If true, sections that contain both fields and subsections will
 186       * render their subsections before their fields.
 187       *
 188       * Subclasses may set this to false to render subsections after fields
 189       * instead.
 190       */
 191      protected $mSubSectionBeforeFields = true;
 192  
 193      /**
 194       * Format in which to display form. For viable options,
 195       * @see $availableDisplayFormats
 196       * @var string
 197       */
 198      protected $displayFormat = 'table';
 199  
 200      /**
 201       * Available formats in which to display the form
 202       * @var array
 203       */
 204      protected $availableDisplayFormats = array(
 205          'table',
 206          'div',
 207          'raw',
 208          'vform',
 209      );
 210  
 211      /**
 212       * Build a new HTMLForm from an array of field attributes
 213       *
 214       * @param array $descriptor Array of Field constructs, as described above
 215       * @param IContextSource $context Available since 1.18, will become compulsory in 1.18.
 216       *     Obviates the need to call $form->setTitle()
 217       * @param string $messagePrefix A prefix to go in front of default messages
 218       */
 219  	public function __construct( $descriptor, /*IContextSource*/ $context = null,
 220          $messagePrefix = ''
 221      ) {
 222          if ( $context instanceof IContextSource ) {
 223              $this->setContext( $context );
 224              $this->mTitle = false; // We don't need them to set a title
 225              $this->mMessagePrefix = $messagePrefix;
 226          } elseif ( is_null( $context ) && $messagePrefix !== '' ) {
 227              $this->mMessagePrefix = $messagePrefix;
 228          } elseif ( is_string( $context ) && $messagePrefix === '' ) {
 229              // B/C since 1.18
 230              // it's actually $messagePrefix
 231              $this->mMessagePrefix = $context;
 232          }
 233  
 234          // Expand out into a tree.
 235          $loadedDescriptor = array();
 236          $this->mFlatFields = array();
 237  
 238          foreach ( $descriptor as $fieldname => $info ) {
 239              $section = isset( $info['section'] )
 240                  ? $info['section']
 241                  : '';
 242  
 243              if ( isset( $info['type'] ) && $info['type'] == 'file' ) {
 244                  $this->mUseMultipart = true;
 245              }
 246  
 247              $field = self::loadInputFromParameters( $fieldname, $info );
 248              // FIXME During field's construct, the parent form isn't available!
 249              // could add a 'parent' name-value to $info, could add a third parameter.
 250              $field->mParent = $this;
 251  
 252              // vform gets too much space if empty labels generate HTML.
 253              if ( $this->isVForm() ) {
 254                  $field->setShowEmptyLabel( false );
 255              }
 256  
 257              $setSection =& $loadedDescriptor;
 258              if ( $section ) {
 259                  $sectionParts = explode( '/', $section );
 260  
 261                  while ( count( $sectionParts ) ) {
 262                      $newName = array_shift( $sectionParts );
 263  
 264                      if ( !isset( $setSection[$newName] ) ) {
 265                          $setSection[$newName] = array();
 266                      }
 267  
 268                      $setSection =& $setSection[$newName];
 269                  }
 270              }
 271  
 272              $setSection[$fieldname] = $field;
 273              $this->mFlatFields[$fieldname] = $field;
 274          }
 275  
 276          $this->mFieldTree = $loadedDescriptor;
 277      }
 278  
 279      /**
 280       * Set format in which to display the form
 281       *
 282       * @param string $format The name of the format to use, must be one of
 283       *   $this->availableDisplayFormats
 284       *
 285       * @throws MWException
 286       * @since 1.20
 287       * @return HTMLForm $this for chaining calls (since 1.20)
 288       */
 289  	public function setDisplayFormat( $format ) {
 290          if ( !in_array( $format, $this->availableDisplayFormats ) ) {
 291              throw new MWException( 'Display format must be one of ' .
 292                  print_r( $this->availableDisplayFormats, true ) );
 293          }
 294          $this->displayFormat = $format;
 295  
 296          return $this;
 297      }
 298  
 299      /**
 300       * Getter for displayFormat
 301       * @since 1.20
 302       * @return string
 303       */
 304  	public function getDisplayFormat() {
 305          $format = $this->displayFormat;
 306          if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
 307              $format = 'div';
 308          }
 309          return $format;
 310      }
 311  
 312      /**
 313       * Test if displayFormat is 'vform'
 314       * @since 1.22
 315       * @return bool
 316       */
 317  	public function isVForm() {
 318          return $this->displayFormat === 'vform';
 319      }
 320  
 321      /**
 322       * Get the HTMLFormField subclass for this descriptor.
 323       *
 324       * The descriptor can be passed either 'class' which is the name of
 325       * a HTMLFormField subclass, or a shorter 'type' which is an alias.
 326       * This makes sure the 'class' is always set, and also is returned by
 327       * this function for ease.
 328       *
 329       * @since 1.23
 330       *
 331       * @param string $fieldname Name of the field
 332       * @param array $descriptor Input Descriptor, as described above
 333       *
 334       * @throws MWException
 335       * @return string Name of a HTMLFormField subclass
 336       */
 337  	public static function getClassFromDescriptor( $fieldname, &$descriptor ) {
 338          if ( isset( $descriptor['class'] ) ) {
 339              $class = $descriptor['class'];
 340          } elseif ( isset( $descriptor['type'] ) ) {
 341              $class = self::$typeMappings[$descriptor['type']];
 342              $descriptor['class'] = $class;
 343          } else {
 344              $class = null;
 345          }
 346  
 347          if ( !$class ) {
 348              throw new MWException( "Descriptor with no class for $fieldname: "
 349                  . print_r( $descriptor, true ) );
 350          }
 351  
 352          return $class;
 353      }
 354  
 355      /**
 356       * Initialise a new Object for the field
 357       *
 358       * @param string $fieldname Name of the field
 359       * @param array $descriptor Input Descriptor, as described above
 360       *
 361       * @throws MWException
 362       * @return HTMLFormField Instance of a subclass of HTMLFormField
 363       */
 364  	public static function loadInputFromParameters( $fieldname, $descriptor ) {
 365          $class = self::getClassFromDescriptor( $fieldname, $descriptor );
 366  
 367          $descriptor['fieldname'] = $fieldname;
 368  
 369          # @todo This will throw a fatal error whenever someone try to use
 370          # 'class' to feed a CSS class instead of 'cssclass'. Would be
 371          # great to avoid the fatal error and show a nice error.
 372          $obj = new $class( $descriptor );
 373  
 374          return $obj;
 375      }
 376  
 377      /**
 378       * Prepare form for submission.
 379       *
 380       * @attention When doing method chaining, that should be the very last
 381       * method call before displayForm().
 382       *
 383       * @throws MWException
 384       * @return HTMLForm $this for chaining calls (since 1.20)
 385       */
 386  	function prepareForm() {
 387          # Check if we have the info we need
 388          if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) {
 389              throw new MWException( "You must call setTitle() on an HTMLForm" );
 390          }
 391  
 392          # Load data from the request.
 393          $this->loadData();
 394  
 395          return $this;
 396      }
 397  
 398      /**
 399       * Try submitting, with edit token check first
 400       * @return Status|bool
 401       */
 402  	function tryAuthorizedSubmit() {
 403          $result = false;
 404  
 405          $submit = false;
 406          if ( $this->getMethod() != 'post' ) {
 407              $submit = true; // no session check needed
 408          } elseif ( $this->getRequest()->wasPosted() ) {
 409              $editToken = $this->getRequest()->getVal( 'wpEditToken' );
 410              if ( $this->getUser()->isLoggedIn() || $editToken != null ) {
 411                  // Session tokens for logged-out users have no security value.
 412                  // However, if the user gave one, check it in order to give a nice
 413                  // "session expired" error instead of "permission denied" or such.
 414                  $submit = $this->getUser()->matchEditToken( $editToken, $this->mTokenSalt );
 415              } else {
 416                  $submit = true;
 417              }
 418          }
 419  
 420          if ( $submit ) {
 421              $this->mWasSubmitted = true;
 422              $result = $this->trySubmit();
 423          }
 424  
 425          return $result;
 426      }
 427  
 428      /**
 429       * The here's-one-I-made-earlier option: do the submission if
 430       * posted, or display the form with or without funky validation
 431       * errors
 432       * @return bool|Status Whether submission was successful.
 433       */
 434  	function show() {
 435          $this->prepareForm();
 436  
 437          $result = $this->tryAuthorizedSubmit();
 438          if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
 439              return $result;
 440          }
 441  
 442          $this->displayForm( $result );
 443  
 444          return false;
 445      }
 446  
 447      /**
 448       * Validate all the fields, and call the submission callback
 449       * function if everything is kosher.
 450       * @throws MWException
 451       * @return bool|string|array|Status
 452       *     - Bool true or a good Status object indicates success,
 453       *     - Bool false indicates no submission was attempted,
 454       *     - Anything else indicates failure. The value may be a fatal Status
 455       *       object, an HTML string, or an array of arrays (message keys and
 456       *       params) or strings (message keys)
 457       */
 458  	function trySubmit() {
 459          $this->mWasSubmitted = true;
 460  
 461          # Check for cancelled submission
 462          foreach ( $this->mFlatFields as $fieldname => $field ) {
 463              if ( !empty( $field->mParams['nodata'] ) ) {
 464                  continue;
 465              }
 466              if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
 467                  $this->mWasSubmitted = false;
 468                  return false;
 469              }
 470          }
 471  
 472          # Check for validation
 473          foreach ( $this->mFlatFields as $fieldname => $field ) {
 474              if ( !empty( $field->mParams['nodata'] ) ) {
 475                  continue;
 476              }
 477              if ( $field->isHidden( $this->mFieldData ) ) {
 478                  continue;
 479              }
 480              if ( $field->validate(
 481                      $this->mFieldData[$fieldname],
 482                      $this->mFieldData )
 483                  !== true
 484              ) {
 485                  return isset( $this->mValidationErrorMessage )
 486                      ? $this->mValidationErrorMessage
 487                      : array( 'htmlform-invalid-input' );
 488              }
 489          }
 490  
 491          $callback = $this->mSubmitCallback;
 492          if ( !is_callable( $callback ) ) {
 493              throw new MWException( 'HTMLForm: no submit callback provided. Use ' .
 494                  'setSubmitCallback() to set one.' );
 495          }
 496  
 497          $data = $this->filterDataForSubmit( $this->mFieldData );
 498  
 499          $res = call_user_func( $callback, $data, $this );
 500          if ( $res === false ) {
 501              $this->mWasSubmitted = false;
 502          }
 503  
 504          return $res;
 505      }
 506  
 507      /**
 508       * Test whether the form was considered to have been submitted or not, i.e.
 509       * whether the last call to tryAuthorizedSubmit or trySubmit returned
 510       * non-false.
 511       *
 512       * This will return false until HTMLForm::tryAuthorizedSubmit or
 513       * HTMLForm::trySubmit is called.
 514       *
 515       * @since 1.23
 516       * @return bool
 517       */
 518  	function wasSubmitted() {
 519          return $this->mWasSubmitted;
 520      }
 521  
 522      /**
 523       * Set a callback to a function to do something with the form
 524       * once it's been successfully validated.
 525       *
 526       * @param callable $cb The function will be passed the output from
 527       *   HTMLForm::filterDataForSubmit and this HTMLForm object, and must
 528       *   return as documented for HTMLForm::trySubmit
 529       *
 530       * @return HTMLForm $this for chaining calls (since 1.20)
 531       */
 532  	function setSubmitCallback( $cb ) {
 533          $this->mSubmitCallback = $cb;
 534  
 535          return $this;
 536      }
 537  
 538      /**
 539       * Set a message to display on a validation error.
 540       *
 541       * @param string|array $msg String or Array of valid inputs to wfMessage()
 542       *     (so each entry can be either a String or Array)
 543       *
 544       * @return HTMLForm $this for chaining calls (since 1.20)
 545       */
 546  	function setValidationErrorMessage( $msg ) {
 547          $this->mValidationErrorMessage = $msg;
 548  
 549          return $this;
 550      }
 551  
 552      /**
 553       * Set the introductory message, overwriting any existing message.
 554       *
 555       * @param string $msg Complete text of message to display
 556       *
 557       * @return HTMLForm $this for chaining calls (since 1.20)
 558       */
 559  	function setIntro( $msg ) {
 560          $this->setPreText( $msg );
 561  
 562          return $this;
 563      }
 564  
 565      /**
 566       * Set the introductory message, overwriting any existing message.
 567       * @since 1.19
 568       *
 569       * @param string $msg Complete text of message to display
 570       *
 571       * @return HTMLForm $this for chaining calls (since 1.20)
 572       */
 573  	function setPreText( $msg ) {
 574          $this->mPre = $msg;
 575  
 576          return $this;
 577      }
 578  
 579      /**
 580       * Add introductory text.
 581       *
 582       * @param string $msg Complete text of message to display
 583       *
 584       * @return HTMLForm $this for chaining calls (since 1.20)
 585       */
 586  	function addPreText( $msg ) {
 587          $this->mPre .= $msg;
 588  
 589          return $this;
 590      }
 591  
 592      /**
 593       * Add header text, inside the form.
 594       *
 595       * @param string $msg Complete text of message to display
 596       * @param string|null $section The section to add the header to
 597       *
 598       * @return HTMLForm $this for chaining calls (since 1.20)
 599       */
 600  	function addHeaderText( $msg, $section = null ) {
 601          if ( is_null( $section ) ) {
 602              $this->mHeader .= $msg;
 603          } else {
 604              if ( !isset( $this->mSectionHeaders[$section] ) ) {
 605                  $this->mSectionHeaders[$section] = '';
 606              }
 607              $this->mSectionHeaders[$section] .= $msg;
 608          }
 609  
 610          return $this;
 611      }
 612  
 613      /**
 614       * Set header text, inside the form.
 615       * @since 1.19
 616       *
 617       * @param string $msg Complete text of message to display
 618       * @param string|null $section The section to add the header to
 619       *
 620       * @return HTMLForm $this for chaining calls (since 1.20)
 621       */
 622  	function setHeaderText( $msg, $section = null ) {
 623          if ( is_null( $section ) ) {
 624              $this->mHeader = $msg;
 625          } else {
 626              $this->mSectionHeaders[$section] = $msg;
 627          }
 628  
 629          return $this;
 630      }
 631  
 632      /**
 633       * Add footer text, inside the form.
 634       *
 635       * @param string $msg Complete text of message to display
 636       * @param string|null $section The section to add the footer text to
 637       *
 638       * @return HTMLForm $this for chaining calls (since 1.20)
 639       */
 640  	function addFooterText( $msg, $section = null ) {
 641          if ( is_null( $section ) ) {
 642              $this->mFooter .= $msg;
 643          } else {
 644              if ( !isset( $this->mSectionFooters[$section] ) ) {
 645                  $this->mSectionFooters[$section] = '';
 646              }
 647              $this->mSectionFooters[$section] .= $msg;
 648          }
 649  
 650          return $this;
 651      }
 652  
 653      /**
 654       * Set footer text, inside the form.
 655       * @since 1.19
 656       *
 657       * @param string $msg Complete text of message to display
 658       * @param string|null $section The section to add the footer text to
 659       *
 660       * @return HTMLForm $this for chaining calls (since 1.20)
 661       */
 662  	function setFooterText( $msg, $section = null ) {
 663          if ( is_null( $section ) ) {
 664              $this->mFooter = $msg;
 665          } else {
 666              $this->mSectionFooters[$section] = $msg;
 667          }
 668  
 669          return $this;
 670      }
 671  
 672      /**
 673       * Add text to the end of the display.
 674       *
 675       * @param string $msg Complete text of message to display
 676       *
 677       * @return HTMLForm $this for chaining calls (since 1.20)
 678       */
 679  	function addPostText( $msg ) {
 680          $this->mPost .= $msg;
 681  
 682          return $this;
 683      }
 684  
 685      /**
 686       * Set text at the end of the display.
 687       *
 688       * @param string $msg Complete text of message to display
 689       *
 690       * @return HTMLForm $this for chaining calls (since 1.20)
 691       */
 692  	function setPostText( $msg ) {
 693          $this->mPost = $msg;
 694  
 695          return $this;
 696      }
 697  
 698      /**
 699       * Add a hidden field to the output
 700       *
 701       * @param string $name Field name.  This will be used exactly as entered
 702       * @param string $value Field value
 703       * @param array $attribs
 704       *
 705       * @return HTMLForm $this for chaining calls (since 1.20)
 706       */
 707  	public function addHiddenField( $name, $value, $attribs = array() ) {
 708          $attribs += array( 'name' => $name );
 709          $this->mHiddenFields[] = array( $value, $attribs );
 710  
 711          return $this;
 712      }
 713  
 714      /**
 715       * Add an array of hidden fields to the output
 716       *
 717       * @since 1.22
 718       *
 719       * @param array $fields Associative array of fields to add;
 720       *        mapping names to their values
 721       *
 722       * @return HTMLForm $this for chaining calls
 723       */
 724  	public function addHiddenFields( array $fields ) {
 725          foreach ( $fields as $name => $value ) {
 726              $this->mHiddenFields[] = array( $value, array( 'name' => $name ) );
 727          }
 728  
 729          return $this;
 730      }
 731  
 732      /**
 733       * Add a button to the form
 734       *
 735       * @param string $name Field name.
 736       * @param string $value Field value
 737       * @param string $id DOM id for the button (default: null)
 738       * @param array $attribs
 739       *
 740       * @return HTMLForm $this for chaining calls (since 1.20)
 741       */
 742  	public function addButton( $name, $value, $id = null, $attribs = null ) {
 743          $this->mButtons[] = compact( 'name', 'value', 'id', 'attribs' );
 744  
 745          return $this;
 746      }
 747  
 748      /**
 749       * Set the salt for the edit token.
 750       *
 751       * Only useful when the method is "post".
 752       *
 753       * @since 1.24
 754       * @param string|array $salt Salt to use
 755       * @return HTMLForm $this For chaining calls
 756       */
 757  	public function setTokenSalt( $salt ) {
 758          $this->mTokenSalt = $salt;
 759  
 760          return $this;
 761      }
 762  
 763      /**
 764       * Display the form (sending to the context's OutputPage object), with an
 765       * appropriate error message or stack of messages, and any validation errors, etc.
 766       *
 767       * @attention You should call prepareForm() before calling this function.
 768       * Moreover, when doing method chaining this should be the very last method
 769       * call just after prepareForm().
 770       *
 771       * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
 772       *
 773       * @return void Nothing, should be last call
 774       */
 775  	function displayForm( $submitResult ) {
 776          $this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
 777      }
 778  
 779      /**
 780       * Returns the raw HTML generated by the form
 781       *
 782       * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
 783       *
 784       * @return string
 785       */
 786  	function getHTML( $submitResult ) {
 787          # For good measure (it is the default)
 788          $this->getOutput()->preventClickjacking();
 789          $this->getOutput()->addModules( 'mediawiki.htmlform' );
 790          if ( $this->isVForm() ) {
 791              $this->getOutput()->addModuleStyles( array(
 792                  'mediawiki.ui',
 793                  'mediawiki.ui.button',
 794              ) );
 795              // @todo Should vertical form set setWrapperLegend( false )
 796              // to hide ugly fieldsets?
 797          }
 798  
 799          $html = ''
 800              . $this->getErrors( $submitResult )
 801              . $this->mHeader
 802              . $this->getBody()
 803              . $this->getHiddenFields()
 804              . $this->getButtons()
 805              . $this->mFooter;
 806  
 807          $html = $this->wrapForm( $html );
 808  
 809          return '' . $this->mPre . $html . $this->mPost;
 810      }
 811  
 812      /**
 813       * Wrap the form innards in an actual "<form>" element
 814       *
 815       * @param string $html HTML contents to wrap.
 816       *
 817       * @return string Wrapped HTML.
 818       */
 819  	function wrapForm( $html ) {
 820  
 821          # Include a <fieldset> wrapper for style, if requested.
 822          if ( $this->mWrapperLegend !== false ) {
 823              $html = Xml::fieldset( $this->mWrapperLegend, $html );
 824          }
 825          # Use multipart/form-data
 826          $encType = $this->mUseMultipart
 827              ? 'multipart/form-data'
 828              : 'application/x-www-form-urlencoded';
 829          # Attributes
 830          $attribs = array(
 831              'action' => $this->getAction(),
 832              'method' => $this->getMethod(),
 833              'class' => array( 'visualClear' ),
 834              'enctype' => $encType,
 835          );
 836          if ( !empty( $this->mId ) ) {
 837              $attribs['id'] = $this->mId;
 838          }
 839  
 840          if ( $this->isVForm() ) {
 841              array_push( $attribs['class'], 'mw-ui-vform', 'mw-ui-container' );
 842          }
 843  
 844          return Html::rawElement( 'form', $attribs, $html );
 845      }
 846  
 847      /**
 848       * Get the hidden fields that should go inside the form.
 849       * @return string HTML.
 850       */
 851  	function getHiddenFields() {
 852          $html = '';
 853          if ( $this->getMethod() == 'post' ) {
 854              $html .= Html::hidden(
 855                  'wpEditToken',
 856                  $this->getUser()->getEditToken( $this->mTokenSalt ),
 857                  array( 'id' => 'wpEditToken' )
 858              ) . "\n";
 859              $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
 860          }
 861  
 862          $articlePath = $this->getConfig()->get( 'ArticlePath' );
 863          if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() == 'get' ) {
 864              $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
 865          }
 866  
 867          foreach ( $this->mHiddenFields as $data ) {
 868              list( $value, $attribs ) = $data;
 869              $html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
 870          }
 871  
 872          return $html;
 873      }
 874  
 875      /**
 876       * Get the submit and (potentially) reset buttons.
 877       * @return string HTML.
 878       */
 879  	function getButtons() {
 880          $buttons = '';
 881          $useMediaWikiUIEverywhere = $this->getConfig()->get( 'UseMediaWikiUIEverywhere' );
 882  
 883          if ( $this->mShowSubmit ) {
 884              $attribs = array();
 885  
 886              if ( isset( $this->mSubmitID ) ) {
 887                  $attribs['id'] = $this->mSubmitID;
 888              }
 889  
 890              if ( isset( $this->mSubmitName ) ) {
 891                  $attribs['name'] = $this->mSubmitName;
 892              }
 893  
 894              if ( isset( $this->mSubmitTooltip ) ) {
 895                  $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
 896              }
 897  
 898              $attribs['class'] = array( 'mw-htmlform-submit' );
 899  
 900              if ( $this->isVForm() || $useMediaWikiUIEverywhere ) {
 901                  array_push( $attribs['class'], 'mw-ui-button', $this->mSubmitModifierClass );
 902              }
 903  
 904              if ( $this->isVForm() ) {
 905                  // mw-ui-block is necessary because the buttons aren't necessarily in an
 906                  // immediate child div of the vform.
 907                  // @todo Let client specify if the primary submit button is progressive or destructive
 908                  array_push(
 909                      $attribs['class'],
 910                      'mw-ui-big',
 911                      'mw-ui-block'
 912                  );
 913              }
 914  
 915              $buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
 916          }
 917  
 918          if ( $this->mShowReset ) {
 919              $buttons .= Html::element(
 920                  'input',
 921                  array(
 922                      'type' => 'reset',
 923                      'value' => $this->msg( 'htmlform-reset' )->text()
 924                  )
 925              ) . "\n";
 926          }
 927  
 928          foreach ( $this->mButtons as $button ) {
 929              $attrs = array(
 930                  'type' => 'submit',
 931                  'name' => $button['name'],
 932                  'value' => $button['value']
 933              );
 934  
 935              if ( $button['attribs'] ) {
 936                  $attrs += $button['attribs'];
 937              }
 938  
 939              if ( isset( $button['id'] ) ) {
 940                  $attrs['id'] = $button['id'];
 941              }
 942  
 943              if ( $this->isVForm() || $useMediaWikiUIEverywhere ) {
 944                  if ( isset( $attrs['class'] ) ) {
 945                      $attrs['class'] .= ' mw-ui-button';
 946                  } else {
 947                      $attrs['class'] = 'mw-ui-button';
 948                  }
 949                  if ( $this->isVForm() ) {
 950                      $attrs['class'] .= ' mw-ui-big mw-ui-block';
 951                  }
 952              }
 953  
 954              $buttons .= Html::element( 'input', $attrs ) . "\n";
 955          }
 956  
 957          $html = Html::rawElement( 'span',
 958              array( 'class' => 'mw-htmlform-submit-buttons' ), "\n$buttons" ) . "\n";
 959  
 960          // Buttons are top-level form elements in table and div layouts,
 961          // but vform wants all elements inside divs to get spaced-out block
 962          // styling.
 963          if ( $this->mShowSubmit && $this->isVForm() ) {
 964              $html = Html::rawElement( 'div', null, "\n$html" ) . "\n";
 965          }
 966  
 967          return $html;
 968      }
 969  
 970      /**
 971       * Get the whole body of the form.
 972       * @return string
 973       */
 974  	function getBody() {
 975          return $this->displaySection( $this->mFieldTree, $this->mTableId );
 976      }
 977  
 978      /**
 979       * Format and display an error message stack.
 980       *
 981       * @param string|array|Status $errors
 982       *
 983       * @return string
 984       */
 985  	function getErrors( $errors ) {
 986          if ( $errors instanceof Status ) {
 987              if ( $errors->isOK() ) {
 988                  $errorstr = '';
 989              } else {
 990                  $errorstr = $this->getOutput()->parse( $errors->getWikiText() );
 991              }
 992          } elseif ( is_array( $errors ) ) {
 993              $errorstr = $this->formatErrors( $errors );
 994          } else {
 995              $errorstr = $errors;
 996          }
 997  
 998          return $errorstr
 999              ? Html::rawElement( 'div', array( 'class' => 'error' ), $errorstr )
1000              : '';
1001      }
1002  
1003      /**
1004       * Format a stack of error messages into a single HTML string
1005       *
1006       * @param array $errors Array of message keys/values
1007       *
1008       * @return string HTML, a "<ul>" list of errors
1009       */
1010  	public static function formatErrors( $errors ) {
1011          $errorstr = '';
1012  
1013          foreach ( $errors as $error ) {
1014              if ( is_array( $error ) ) {
1015                  $msg = array_shift( $error );
1016              } else {
1017                  $msg = $error;
1018                  $error = array();
1019              }
1020  
1021              $errorstr .= Html::rawElement(
1022                  'li',
1023                  array(),
1024                  wfMessage( $msg, $error )->parse()
1025              );
1026          }
1027  
1028          $errorstr = Html::rawElement( 'ul', array(), $errorstr );
1029  
1030          return $errorstr;
1031      }
1032  
1033      /**
1034       * Set the text for the submit button
1035       *
1036       * @param string $t Plaintext
1037       *
1038       * @return HTMLForm $this for chaining calls (since 1.20)
1039       */
1040  	function setSubmitText( $t ) {
1041          $this->mSubmitText = $t;
1042  
1043          return $this;
1044      }
1045  
1046      /**
1047       * Identify that the submit button in the form has a destructive action
1048       *
1049       */
1050  	public function setSubmitDestructive() {
1051          $this->mSubmitModifierClass = 'mw-ui-destructive';
1052      }
1053  
1054      /**
1055       * Set the text for the submit button to a message
1056       * @since 1.19
1057       *
1058       * @param string|Message $msg Message key or Message object
1059       *
1060       * @return HTMLForm $this for chaining calls (since 1.20)
1061       */
1062  	public function setSubmitTextMsg( $msg ) {
1063          if ( !$msg instanceof Message ) {
1064              $msg = $this->msg( $msg );
1065          }
1066          $this->setSubmitText( $msg->text() );
1067  
1068          return $this;
1069      }
1070  
1071      /**
1072       * Get the text for the submit button, either customised or a default.
1073       * @return string
1074       */
1075  	function getSubmitText() {
1076          return $this->mSubmitText
1077              ? $this->mSubmitText
1078              : $this->msg( 'htmlform-submit' )->text();
1079      }
1080  
1081      /**
1082       * @param string $name Submit button name
1083       *
1084       * @return HTMLForm $this for chaining calls (since 1.20)
1085       */
1086  	public function setSubmitName( $name ) {
1087          $this->mSubmitName = $name;
1088  
1089          return $this;
1090      }
1091  
1092      /**
1093       * @param string $name Tooltip for the submit button
1094       *
1095       * @return HTMLForm $this for chaining calls (since 1.20)
1096       */
1097  	public function setSubmitTooltip( $name ) {
1098          $this->mSubmitTooltip = $name;
1099  
1100          return $this;
1101      }
1102  
1103      /**
1104       * Set the id for the submit button.
1105       *
1106       * @param string $t
1107       *
1108       * @todo FIXME: Integrity of $t is *not* validated
1109       * @return HTMLForm $this for chaining calls (since 1.20)
1110       */
1111  	function setSubmitID( $t ) {
1112          $this->mSubmitID = $t;
1113  
1114          return $this;
1115      }
1116  
1117      /**
1118       * Stop a default submit button being shown for this form. This implies that an
1119       * alternate submit method must be provided manually.
1120       *
1121       * @since 1.22
1122       *
1123       * @param bool $suppressSubmit Set to false to re-enable the button again
1124       *
1125       * @return HTMLForm $this for chaining calls
1126       */
1127  	function suppressDefaultSubmit( $suppressSubmit = true ) {
1128          $this->mShowSubmit = !$suppressSubmit;
1129  
1130          return $this;
1131      }
1132  
1133      /**
1134       * Set the id of the \<table\> or outermost \<div\> element.
1135       *
1136       * @since 1.22
1137       *
1138       * @param string $id New value of the id attribute, or "" to remove
1139       *
1140       * @return HTMLForm $this for chaining calls
1141       */
1142  	public function setTableId( $id ) {
1143          $this->mTableId = $id;
1144  
1145          return $this;
1146      }
1147  
1148      /**
1149       * @param string $id DOM id for the form
1150       *
1151       * @return HTMLForm $this for chaining calls (since 1.20)
1152       */
1153  	public function setId( $id ) {
1154          $this->mId = $id;
1155  
1156          return $this;
1157      }
1158  
1159      /**
1160       * Prompt the whole form to be wrapped in a "<fieldset>", with
1161       * this text as its "<legend>" element.
1162       *
1163       * @param string|bool $legend HTML to go inside the "<legend>" element, or
1164       * false for no <legend>
1165       *     Will be escaped
1166       *
1167       * @return HTMLForm $this for chaining calls (since 1.20)
1168       */
1169  	public function setWrapperLegend( $legend ) {
1170          $this->mWrapperLegend = $legend;
1171  
1172          return $this;
1173      }
1174  
1175      /**
1176       * Prompt the whole form to be wrapped in a "<fieldset>", with
1177       * this message as its "<legend>" element.
1178       * @since 1.19
1179       *
1180       * @param string|Message $msg Message key or Message object
1181       *
1182       * @return HTMLForm $this for chaining calls (since 1.20)
1183       */
1184  	public function setWrapperLegendMsg( $msg ) {
1185          if ( !$msg instanceof Message ) {
1186              $msg = $this->msg( $msg );
1187          }
1188          $this->setWrapperLegend( $msg->text() );
1189  
1190          return $this;
1191      }
1192  
1193      /**
1194       * Set the prefix for various default messages
1195       * @todo Currently only used for the "<fieldset>" legend on forms
1196       * with multiple sections; should be used elsewhere?
1197       *
1198       * @param string $p
1199       *
1200       * @return HTMLForm $this for chaining calls (since 1.20)
1201       */
1202  	function setMessagePrefix( $p ) {
1203          $this->mMessagePrefix = $p;
1204  
1205          return $this;
1206      }
1207  
1208      /**
1209       * Set the title for form submission
1210       *
1211       * @param Title $t Title of page the form is on/should be posted to
1212       *
1213       * @return HTMLForm $this for chaining calls (since 1.20)
1214       */
1215  	function setTitle( $t ) {
1216          $this->mTitle = $t;
1217  
1218          return $this;
1219      }
1220  
1221      /**
1222       * Get the title
1223       * @return Title
1224       */
1225  	function getTitle() {
1226          return $this->mTitle === false
1227              ? $this->getContext()->getTitle()
1228              : $this->mTitle;
1229      }
1230  
1231      /**
1232       * Set the method used to submit the form
1233       *
1234       * @param string $method
1235       *
1236       * @return HTMLForm $this for chaining calls (since 1.20)
1237       */
1238  	public function setMethod( $method = 'post' ) {
1239          $this->mMethod = $method;
1240  
1241          return $this;
1242      }
1243  
1244  	public function getMethod() {
1245          return $this->mMethod;
1246      }
1247  
1248      /**
1249       * @todo Document
1250       *
1251       * @param array[]|HTMLFormField[] $fields Array of fields (either arrays or
1252       *   objects).
1253       * @param string $sectionName ID attribute of the "<table>" tag for this
1254       *   section, ignored if empty.
1255       * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of
1256       *   each subsection, ignored if empty.
1257       * @param bool &$hasUserVisibleFields Whether the section had user-visible fields.
1258       *
1259       * @return string
1260       */
1261  	public function displaySection( $fields,
1262          $sectionName = '',
1263          $fieldsetIDPrefix = '',
1264          &$hasUserVisibleFields = false ) {
1265          $displayFormat = $this->getDisplayFormat();
1266  
1267          $html = '';
1268          $subsectionHtml = '';
1269          $hasLabel = false;
1270  
1271          switch ( $displayFormat ) {
1272              case 'table':
1273                  $getFieldHtmlMethod = 'getTableRow';
1274                  break;
1275              case 'vform':
1276                  // Close enough to a div.
1277                  $getFieldHtmlMethod = 'getDiv';
1278                  break;
1279              case 'div':
1280                  $getFieldHtmlMethod = 'getDiv';
1281                  break;
1282              default:
1283                  $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat );
1284          }
1285  
1286          foreach ( $fields as $key => $value ) {
1287              if ( $value instanceof HTMLFormField ) {
1288                  $v = empty( $value->mParams['nodata'] )
1289                      ? $this->mFieldData[$key]
1290                      : $value->getDefault();
1291                  $html .= $value->$getFieldHtmlMethod( $v );
1292  
1293                  $labelValue = trim( $value->getLabel() );
1294                  if ( $labelValue != '&#160;' && $labelValue !== '' ) {
1295                      $hasLabel = true;
1296                  }
1297  
1298                  if ( get_class( $value ) !== 'HTMLHiddenField' &&
1299                      get_class( $value ) !== 'HTMLApiField'
1300                  ) {
1301                      $hasUserVisibleFields = true;
1302                  }
1303              } elseif ( is_array( $value ) ) {
1304                  $subsectionHasVisibleFields = false;
1305                  $section =
1306                      $this->displaySection( $value,
1307                          "mw-htmlform-$key",
1308                          "$fieldsetIDPrefix$key-",
1309                          $subsectionHasVisibleFields );
1310                  $legend = null;
1311  
1312                  if ( $subsectionHasVisibleFields === true ) {
1313                      // Display the section with various niceties.
1314                      $hasUserVisibleFields = true;
1315  
1316                      $legend = $this->getLegend( $key );
1317  
1318                      if ( isset( $this->mSectionHeaders[$key] ) ) {
1319                          $section = $this->mSectionHeaders[$key] . $section;
1320                      }
1321                      if ( isset( $this->mSectionFooters[$key] ) ) {
1322                          $section .= $this->mSectionFooters[$key];
1323                      }
1324  
1325                      $attributes = array();
1326                      if ( $fieldsetIDPrefix ) {
1327                          $attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
1328                      }
1329                      $subsectionHtml .= Xml::fieldset( $legend, $section, $attributes ) . "\n";
1330                  } else {
1331                      // Just return the inputs, nothing fancy.
1332                      $subsectionHtml .= $section;
1333                  }
1334              }
1335          }
1336  
1337          if ( $displayFormat !== 'raw' ) {
1338              $classes = array();
1339  
1340              if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
1341                  $classes[] = 'mw-htmlform-nolabel';
1342              }
1343  
1344              $attribs = array(
1345                  'class' => implode( ' ', $classes ),
1346              );
1347  
1348              if ( $sectionName ) {
1349                  $attribs['id'] = Sanitizer::escapeId( $sectionName );
1350              }
1351  
1352              if ( $displayFormat === 'table' ) {
1353                  $html = Html::rawElement( 'table',
1354                          $attribs,
1355                          Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n";
1356              } elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) {
1357                  $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
1358              }
1359          }
1360  
1361          if ( $this->mSubSectionBeforeFields ) {
1362              return $subsectionHtml . "\n" . $html;
1363          } else {
1364              return $html . "\n" . $subsectionHtml;
1365          }
1366      }
1367  
1368      /**
1369       * Construct the form fields from the Descriptor array
1370       */
1371  	function loadData() {
1372          $fieldData = array();
1373  
1374          foreach ( $this->mFlatFields as $fieldname => $field ) {
1375              if ( !empty( $field->mParams['nodata'] ) ) {
1376                  continue;
1377              } elseif ( !empty( $field->mParams['disabled'] ) ) {
1378                  $fieldData[$fieldname] = $field->getDefault();
1379              } else {
1380                  $fieldData[$fieldname] = $field->loadDataFromRequest( $this->getRequest() );
1381              }
1382          }
1383  
1384          # Filter data.
1385          foreach ( $fieldData as $name => &$value ) {
1386              $field = $this->mFlatFields[$name];
1387              $value = $field->filter( $value, $this->mFlatFields );
1388          }
1389  
1390          $this->mFieldData = $fieldData;
1391      }
1392  
1393      /**
1394       * Stop a reset button being shown for this form
1395       *
1396       * @param bool $suppressReset Set to false to re-enable the button again
1397       *
1398       * @return HTMLForm $this for chaining calls (since 1.20)
1399       */
1400  	function suppressReset( $suppressReset = true ) {
1401          $this->mShowReset = !$suppressReset;
1402  
1403          return $this;
1404      }
1405  
1406      /**
1407       * Overload this if you want to apply special filtration routines
1408       * to the form as a whole, after it's submitted but before it's
1409       * processed.
1410       *
1411       * @param array $data
1412       *
1413       * @return array
1414       */
1415  	function filterDataForSubmit( $data ) {
1416          return $data;
1417      }
1418  
1419      /**
1420       * Get a string to go in the "<legend>" of a section fieldset.
1421       * Override this if you want something more complicated.
1422       *
1423       * @param string $key
1424       *
1425       * @return string
1426       */
1427  	public function getLegend( $key ) {
1428          return $this->msg( "{$this->mMessagePrefix}-$key" )->text();
1429      }
1430  
1431      /**
1432       * Set the value for the action attribute of the form.
1433       * When set to false (which is the default state), the set title is used.
1434       *
1435       * @since 1.19
1436       *
1437       * @param string|bool $action
1438       *
1439       * @return HTMLForm $this for chaining calls (since 1.20)
1440       */
1441  	public function setAction( $action ) {
1442          $this->mAction = $action;
1443  
1444          return $this;
1445      }
1446  
1447      /**
1448       * Get the value for the action attribute of the form.
1449       *
1450       * @since 1.22
1451       *
1452       * @return string
1453       */
1454  	public function getAction() {
1455          // If an action is alredy provided, return it
1456          if ( $this->mAction !== false ) {
1457              return $this->mAction;
1458          }
1459  
1460          $articlePath = $this->getConfig()->get( 'ArticlePath' );
1461          // Check whether we are in GET mode and the ArticlePath contains a "?"
1462          // meaning that getLocalURL() would return something like "index.php?title=...".
1463          // As browser remove the query string before submitting GET forms,
1464          // it means that the title would be lost. In such case use wfScript() instead
1465          // and put title in an hidden field (see getHiddenFields()).
1466          if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1467              return wfScript();
1468          }
1469  
1470          return $this->getTitle()->getLocalURL();
1471      }
1472  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1