[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
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 != ' ' && $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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |