Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like HTMLForm often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use HTMLForm, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 123 | class HTMLForm extends ContextSource { |
||
| 124 | // A mapping of 'type' inputs onto standard HTMLFormField subclasses |
||
| 125 | public static $typeMappings = [ |
||
| 126 | 'api' => 'HTMLApiField', |
||
| 127 | 'text' => 'HTMLTextField', |
||
| 128 | 'textwithbutton' => 'HTMLTextFieldWithButton', |
||
| 129 | 'textarea' => 'HTMLTextAreaField', |
||
| 130 | 'select' => 'HTMLSelectField', |
||
| 131 | 'combobox' => 'HTMLComboboxField', |
||
| 132 | 'radio' => 'HTMLRadioField', |
||
| 133 | 'multiselect' => 'HTMLMultiSelectField', |
||
| 134 | 'limitselect' => 'HTMLSelectLimitField', |
||
| 135 | 'check' => 'HTMLCheckField', |
||
| 136 | 'toggle' => 'HTMLCheckField', |
||
| 137 | 'int' => 'HTMLIntField', |
||
| 138 | 'float' => 'HTMLFloatField', |
||
| 139 | 'info' => 'HTMLInfoField', |
||
| 140 | 'selectorother' => 'HTMLSelectOrOtherField', |
||
| 141 | 'selectandother' => 'HTMLSelectAndOtherField', |
||
| 142 | 'namespaceselect' => 'HTMLSelectNamespace', |
||
| 143 | 'namespaceselectwithbutton' => 'HTMLSelectNamespaceWithButton', |
||
| 144 | 'tagfilter' => 'HTMLTagFilter', |
||
| 145 | 'submit' => 'HTMLSubmitField', |
||
| 146 | 'hidden' => 'HTMLHiddenField', |
||
| 147 | 'edittools' => 'HTMLEditTools', |
||
| 148 | 'checkmatrix' => 'HTMLCheckMatrix', |
||
| 149 | 'cloner' => 'HTMLFormFieldCloner', |
||
| 150 | 'autocompleteselect' => 'HTMLAutoCompleteSelectField', |
||
| 151 | // HTMLTextField will output the correct type="" attribute automagically. |
||
| 152 | // There are about four zillion other HTML5 input types, like range, but |
||
| 153 | // we don't use those at the moment, so no point in adding all of them. |
||
| 154 | 'email' => 'HTMLTextField', |
||
| 155 | 'password' => 'HTMLTextField', |
||
| 156 | 'url' => 'HTMLTextField', |
||
| 157 | 'title' => 'HTMLTitleTextField', |
||
| 158 | 'user' => 'HTMLUserTextField', |
||
| 159 | ]; |
||
| 160 | |||
| 161 | public $mFieldData; |
||
| 162 | |||
| 163 | protected $mMessagePrefix; |
||
| 164 | |||
| 165 | /** @var HTMLFormField[] */ |
||
| 166 | protected $mFlatFields; |
||
| 167 | |||
| 168 | protected $mFieldTree; |
||
| 169 | protected $mShowReset = false; |
||
| 170 | protected $mShowSubmit = true; |
||
| 171 | protected $mSubmitFlags = [ 'constructive', 'primary' ]; |
||
| 172 | |||
| 173 | protected $mSubmitCallback; |
||
| 174 | protected $mValidationErrorMessage; |
||
| 175 | |||
| 176 | protected $mPre = ''; |
||
| 177 | protected $mHeader = ''; |
||
| 178 | protected $mFooter = ''; |
||
| 179 | protected $mSectionHeaders = []; |
||
| 180 | protected $mSectionFooters = []; |
||
| 181 | protected $mPost = ''; |
||
| 182 | protected $mId; |
||
| 183 | protected $mName; |
||
| 184 | protected $mTableId = ''; |
||
| 185 | |||
| 186 | protected $mSubmitID; |
||
| 187 | protected $mSubmitName; |
||
| 188 | protected $mSubmitText; |
||
| 189 | protected $mSubmitTooltip; |
||
| 190 | |||
| 191 | protected $mTitle; |
||
| 192 | protected $mMethod = 'post'; |
||
| 193 | protected $mWasSubmitted = false; |
||
| 194 | |||
| 195 | /** |
||
| 196 | * Form action URL. false means we will use the URL to set Title |
||
| 197 | * @since 1.19 |
||
| 198 | * @var bool|string |
||
| 199 | */ |
||
| 200 | protected $mAction = false; |
||
| 201 | |||
| 202 | /** |
||
| 203 | * Form attribute autocomplete. false does not set the attribute |
||
| 204 | * @since 1.27 |
||
| 205 | * @var bool|string |
||
| 206 | */ |
||
| 207 | protected $mAutocomplete = false; |
||
| 208 | |||
| 209 | protected $mUseMultipart = false; |
||
| 210 | protected $mHiddenFields = []; |
||
| 211 | protected $mButtons = []; |
||
| 212 | |||
| 213 | protected $mWrapperLegend = false; |
||
| 214 | |||
| 215 | /** |
||
| 216 | * Salt for the edit token. |
||
| 217 | * @var string|array |
||
| 218 | */ |
||
| 219 | protected $mTokenSalt = ''; |
||
| 220 | |||
| 221 | /** |
||
| 222 | * If true, sections that contain both fields and subsections will |
||
| 223 | * render their subsections before their fields. |
||
| 224 | * |
||
| 225 | * Subclasses may set this to false to render subsections after fields |
||
| 226 | * instead. |
||
| 227 | */ |
||
| 228 | protected $mSubSectionBeforeFields = true; |
||
| 229 | |||
| 230 | /** |
||
| 231 | * Format in which to display form. For viable options, |
||
| 232 | * @see $availableDisplayFormats |
||
| 233 | * @var string |
||
| 234 | */ |
||
| 235 | protected $displayFormat = 'table'; |
||
| 236 | |||
| 237 | /** |
||
| 238 | * Available formats in which to display the form |
||
| 239 | * @var array |
||
| 240 | */ |
||
| 241 | protected $availableDisplayFormats = [ |
||
| 242 | 'table', |
||
| 243 | 'div', |
||
| 244 | 'raw', |
||
| 245 | 'inline', |
||
| 246 | ]; |
||
| 247 | |||
| 248 | /** |
||
| 249 | * Available formats in which to display the form |
||
| 250 | * @var array |
||
| 251 | */ |
||
| 252 | protected $availableSubclassDisplayFormats = [ |
||
| 253 | 'vform', |
||
| 254 | 'ooui', |
||
| 255 | ]; |
||
| 256 | |||
| 257 | /** |
||
| 258 | * Construct a HTMLForm object for given display type. May return a HTMLForm subclass. |
||
| 259 | * |
||
| 260 | * @param string $displayFormat |
||
| 261 | * @param mixed $arguments... Additional arguments to pass to the constructor. |
||
|
|
|||
| 262 | * @return HTMLForm |
||
| 263 | */ |
||
| 264 | public static function factory( $displayFormat/*, $arguments...*/ ) { |
||
| 265 | $arguments = func_get_args(); |
||
| 266 | array_shift( $arguments ); |
||
| 267 | |||
| 268 | switch ( $displayFormat ) { |
||
| 269 | case 'vform': |
||
| 270 | $reflector = new ReflectionClass( 'VFormHTMLForm' ); |
||
| 271 | return $reflector->newInstanceArgs( $arguments ); |
||
| 272 | case 'ooui': |
||
| 273 | $reflector = new ReflectionClass( 'OOUIHTMLForm' ); |
||
| 274 | return $reflector->newInstanceArgs( $arguments ); |
||
| 275 | default: |
||
| 276 | $reflector = new ReflectionClass( 'HTMLForm' ); |
||
| 277 | $form = $reflector->newInstanceArgs( $arguments ); |
||
| 278 | $form->setDisplayFormat( $displayFormat ); |
||
| 279 | return $form; |
||
| 280 | } |
||
| 281 | } |
||
| 282 | |||
| 283 | /** |
||
| 284 | * Build a new HTMLForm from an array of field attributes |
||
| 285 | * |
||
| 286 | * @param array $descriptor Array of Field constructs, as described above |
||
| 287 | * @param IContextSource $context Available since 1.18, will become compulsory in 1.18. |
||
| 288 | * Obviates the need to call $form->setTitle() |
||
| 289 | * @param string $messagePrefix A prefix to go in front of default messages |
||
| 290 | */ |
||
| 291 | public function __construct( $descriptor, /*IContextSource*/ $context = null, |
||
| 292 | $messagePrefix = '' |
||
| 293 | ) { |
||
| 294 | if ( $context instanceof IContextSource ) { |
||
| 295 | $this->setContext( $context ); |
||
| 296 | $this->mTitle = false; // We don't need them to set a title |
||
| 297 | $this->mMessagePrefix = $messagePrefix; |
||
| 298 | } elseif ( $context === null && $messagePrefix !== '' ) { |
||
| 299 | $this->mMessagePrefix = $messagePrefix; |
||
| 300 | } elseif ( is_string( $context ) && $messagePrefix === '' ) { |
||
| 301 | // B/C since 1.18 |
||
| 302 | // it's actually $messagePrefix |
||
| 303 | $this->mMessagePrefix = $context; |
||
| 304 | } |
||
| 305 | |||
| 306 | // Evil hack for mobile :( |
||
| 307 | if ( |
||
| 308 | !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) |
||
| 309 | && $this->displayFormat === 'table' |
||
| 310 | ) { |
||
| 311 | $this->displayFormat = 'div'; |
||
| 312 | } |
||
| 313 | |||
| 314 | // Expand out into a tree. |
||
| 315 | $loadedDescriptor = []; |
||
| 316 | $this->mFlatFields = []; |
||
| 317 | |||
| 318 | foreach ( $descriptor as $fieldname => $info ) { |
||
| 319 | $section = isset( $info['section'] ) |
||
| 320 | ? $info['section'] |
||
| 321 | : ''; |
||
| 322 | |||
| 323 | if ( isset( $info['type'] ) && $info['type'] === 'file' ) { |
||
| 324 | $this->mUseMultipart = true; |
||
| 325 | } |
||
| 326 | |||
| 327 | $field = static::loadInputFromParameters( $fieldname, $info, $this ); |
||
| 328 | |||
| 329 | $setSection =& $loadedDescriptor; |
||
| 330 | if ( $section ) { |
||
| 331 | $sectionParts = explode( '/', $section ); |
||
| 332 | |||
| 333 | while ( count( $sectionParts ) ) { |
||
| 334 | $newName = array_shift( $sectionParts ); |
||
| 335 | |||
| 336 | if ( !isset( $setSection[$newName] ) ) { |
||
| 337 | $setSection[$newName] = []; |
||
| 338 | } |
||
| 339 | |||
| 340 | $setSection =& $setSection[$newName]; |
||
| 341 | } |
||
| 342 | } |
||
| 343 | |||
| 344 | $setSection[$fieldname] = $field; |
||
| 345 | $this->mFlatFields[$fieldname] = $field; |
||
| 346 | } |
||
| 347 | |||
| 348 | $this->mFieldTree = $loadedDescriptor; |
||
| 349 | } |
||
| 350 | |||
| 351 | /** |
||
| 352 | * Set format in which to display the form |
||
| 353 | * |
||
| 354 | * @param string $format The name of the format to use, must be one of |
||
| 355 | * $this->availableDisplayFormats |
||
| 356 | * |
||
| 357 | * @throws MWException |
||
| 358 | * @since 1.20 |
||
| 359 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 360 | */ |
||
| 361 | public function setDisplayFormat( $format ) { |
||
| 362 | if ( |
||
| 363 | in_array( $format, $this->availableSubclassDisplayFormats, true ) || |
||
| 364 | in_array( $this->displayFormat, $this->availableSubclassDisplayFormats, true ) |
||
| 365 | ) { |
||
| 366 | throw new MWException( 'Cannot change display format after creation, ' . |
||
| 367 | 'use HTMLForm::factory() instead' ); |
||
| 368 | } |
||
| 369 | |||
| 370 | if ( !in_array( $format, $this->availableDisplayFormats, true ) ) { |
||
| 371 | throw new MWException( 'Display format must be one of ' . |
||
| 372 | print_r( $this->availableDisplayFormats, true ) ); |
||
| 373 | } |
||
| 374 | |||
| 375 | // Evil hack for mobile :( |
||
| 376 | if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) { |
||
| 377 | $format = 'div'; |
||
| 378 | } |
||
| 379 | |||
| 380 | $this->displayFormat = $format; |
||
| 381 | |||
| 382 | return $this; |
||
| 383 | } |
||
| 384 | |||
| 385 | /** |
||
| 386 | * Getter for displayFormat |
||
| 387 | * @since 1.20 |
||
| 388 | * @return string |
||
| 389 | */ |
||
| 390 | public function getDisplayFormat() { |
||
| 391 | return $this->displayFormat; |
||
| 392 | } |
||
| 393 | |||
| 394 | /** |
||
| 395 | * Test if displayFormat is 'vform' |
||
| 396 | * @since 1.22 |
||
| 397 | * @deprecated since 1.25 |
||
| 398 | * @return bool |
||
| 399 | */ |
||
| 400 | public function isVForm() { |
||
| 401 | wfDeprecated( __METHOD__, '1.25' ); |
||
| 402 | return false; |
||
| 403 | } |
||
| 404 | |||
| 405 | /** |
||
| 406 | * Get the HTMLFormField subclass for this descriptor. |
||
| 407 | * |
||
| 408 | * The descriptor can be passed either 'class' which is the name of |
||
| 409 | * a HTMLFormField subclass, or a shorter 'type' which is an alias. |
||
| 410 | * This makes sure the 'class' is always set, and also is returned by |
||
| 411 | * this function for ease. |
||
| 412 | * |
||
| 413 | * @since 1.23 |
||
| 414 | * |
||
| 415 | * @param string $fieldname Name of the field |
||
| 416 | * @param array $descriptor Input Descriptor, as described above |
||
| 417 | * |
||
| 418 | * @throws MWException |
||
| 419 | * @return string Name of a HTMLFormField subclass |
||
| 420 | */ |
||
| 421 | public static function getClassFromDescriptor( $fieldname, &$descriptor ) { |
||
| 422 | if ( isset( $descriptor['class'] ) ) { |
||
| 423 | $class = $descriptor['class']; |
||
| 424 | } elseif ( isset( $descriptor['type'] ) ) { |
||
| 425 | $class = static::$typeMappings[$descriptor['type']]; |
||
| 426 | $descriptor['class'] = $class; |
||
| 427 | } else { |
||
| 428 | $class = null; |
||
| 429 | } |
||
| 430 | |||
| 431 | if ( !$class ) { |
||
| 432 | throw new MWException( "Descriptor with no class for $fieldname: " |
||
| 433 | . print_r( $descriptor, true ) ); |
||
| 434 | } |
||
| 435 | |||
| 436 | return $class; |
||
| 437 | } |
||
| 438 | |||
| 439 | /** |
||
| 440 | * Initialise a new Object for the field |
||
| 441 | * |
||
| 442 | * @param string $fieldname Name of the field |
||
| 443 | * @param array $descriptor Input Descriptor, as described above |
||
| 444 | * @param HTMLForm|null $parent Parent instance of HTMLForm |
||
| 445 | * |
||
| 446 | * @throws MWException |
||
| 447 | * @return HTMLFormField Instance of a subclass of HTMLFormField |
||
| 448 | */ |
||
| 449 | public static function loadInputFromParameters( $fieldname, $descriptor, |
||
| 450 | HTMLForm $parent = null |
||
| 451 | ) { |
||
| 452 | $class = static::getClassFromDescriptor( $fieldname, $descriptor ); |
||
| 453 | |||
| 454 | $descriptor['fieldname'] = $fieldname; |
||
| 455 | if ( $parent ) { |
||
| 456 | $descriptor['parent'] = $parent; |
||
| 457 | } |
||
| 458 | |||
| 459 | # @todo This will throw a fatal error whenever someone try to use |
||
| 460 | # 'class' to feed a CSS class instead of 'cssclass'. Would be |
||
| 461 | # great to avoid the fatal error and show a nice error. |
||
| 462 | return new $class( $descriptor ); |
||
| 463 | } |
||
| 464 | |||
| 465 | /** |
||
| 466 | * Prepare form for submission. |
||
| 467 | * |
||
| 468 | * @attention When doing method chaining, that should be the very last |
||
| 469 | * method call before displayForm(). |
||
| 470 | * |
||
| 471 | * @throws MWException |
||
| 472 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 473 | */ |
||
| 474 | public function prepareForm() { |
||
| 475 | # Check if we have the info we need |
||
| 476 | if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) { |
||
| 477 | throw new MWException( 'You must call setTitle() on an HTMLForm' ); |
||
| 478 | } |
||
| 479 | |||
| 480 | # Load data from the request. |
||
| 481 | $this->loadData(); |
||
| 482 | |||
| 483 | return $this; |
||
| 484 | } |
||
| 485 | |||
| 486 | /** |
||
| 487 | * Try submitting, with edit token check first |
||
| 488 | * @return Status|bool |
||
| 489 | */ |
||
| 490 | public function tryAuthorizedSubmit() { |
||
| 491 | $result = false; |
||
| 492 | |||
| 493 | $submit = false; |
||
| 494 | if ( $this->getMethod() !== 'post' ) { |
||
| 495 | $submit = true; // no session check needed |
||
| 496 | } elseif ( $this->getRequest()->wasPosted() ) { |
||
| 497 | $editToken = $this->getRequest()->getVal( 'wpEditToken' ); |
||
| 498 | if ( $this->getUser()->isLoggedIn() || $editToken !== null ) { |
||
| 499 | // Session tokens for logged-out users have no security value. |
||
| 500 | // However, if the user gave one, check it in order to give a nice |
||
| 501 | // "session expired" error instead of "permission denied" or such. |
||
| 502 | $submit = $this->getUser()->matchEditToken( $editToken, $this->mTokenSalt ); |
||
| 503 | } else { |
||
| 504 | $submit = true; |
||
| 505 | } |
||
| 506 | } |
||
| 507 | |||
| 508 | if ( $submit ) { |
||
| 509 | $this->mWasSubmitted = true; |
||
| 510 | $result = $this->trySubmit(); |
||
| 511 | } |
||
| 512 | |||
| 513 | return $result; |
||
| 514 | } |
||
| 515 | |||
| 516 | /** |
||
| 517 | * The here's-one-I-made-earlier option: do the submission if |
||
| 518 | * posted, or display the form with or without funky validation |
||
| 519 | * errors |
||
| 520 | * @return bool|Status Whether submission was successful. |
||
| 521 | */ |
||
| 522 | public function show() { |
||
| 523 | $this->prepareForm(); |
||
| 524 | |||
| 525 | $result = $this->tryAuthorizedSubmit(); |
||
| 526 | if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) { |
||
| 527 | return $result; |
||
| 528 | } |
||
| 529 | |||
| 530 | $this->displayForm( $result ); |
||
| 531 | |||
| 532 | return false; |
||
| 533 | } |
||
| 534 | |||
| 535 | /** |
||
| 536 | * Same as self::show with the difference, that the form will be |
||
| 537 | * added to the output, no matter, if the validation was good or not. |
||
| 538 | * @return bool|Status Whether submission was successful. |
||
| 539 | */ |
||
| 540 | public function showAlways() { |
||
| 541 | $this->prepareForm(); |
||
| 542 | |||
| 543 | $result = $this->tryAuthorizedSubmit(); |
||
| 544 | |||
| 545 | $this->displayForm( $result ); |
||
| 546 | |||
| 547 | return $result; |
||
| 548 | } |
||
| 549 | |||
| 550 | /** |
||
| 551 | * Validate all the fields, and call the submission callback |
||
| 552 | * function if everything is kosher. |
||
| 553 | * @throws MWException |
||
| 554 | * @return bool|string|array|Status |
||
| 555 | * - Bool true or a good Status object indicates success, |
||
| 556 | * - Bool false indicates no submission was attempted, |
||
| 557 | * - Anything else indicates failure. The value may be a fatal Status |
||
| 558 | * object, an HTML string, or an array of arrays (message keys and |
||
| 559 | * params) or strings (message keys) |
||
| 560 | */ |
||
| 561 | public function trySubmit() { |
||
| 562 | $valid = true; |
||
| 563 | $hoistedErrors = []; |
||
| 564 | $hoistedErrors[] = isset( $this->mValidationErrorMessage ) |
||
| 565 | ? $this->mValidationErrorMessage |
||
| 566 | : [ 'htmlform-invalid-input' ]; |
||
| 567 | |||
| 568 | $this->mWasSubmitted = true; |
||
| 569 | |||
| 570 | # Check for cancelled submission |
||
| 571 | foreach ( $this->mFlatFields as $fieldname => $field ) { |
||
| 572 | if ( !array_key_exists( $fieldname, $this->mFieldData ) ) { |
||
| 573 | continue; |
||
| 574 | } |
||
| 575 | if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) { |
||
| 576 | $this->mWasSubmitted = false; |
||
| 577 | return false; |
||
| 578 | } |
||
| 579 | } |
||
| 580 | |||
| 581 | # Check for validation |
||
| 582 | foreach ( $this->mFlatFields as $fieldname => $field ) { |
||
| 583 | if ( !array_key_exists( $fieldname, $this->mFieldData ) ) { |
||
| 584 | continue; |
||
| 585 | } |
||
| 586 | if ( $field->isHidden( $this->mFieldData ) ) { |
||
| 587 | continue; |
||
| 588 | } |
||
| 589 | $res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData ); |
||
| 590 | if ( $res !== true ) { |
||
| 591 | $valid = false; |
||
| 592 | if ( $res !== false && !$field->canDisplayErrors() ) { |
||
| 593 | $hoistedErrors[] = [ 'rawmessage', $res ]; |
||
| 594 | } |
||
| 595 | } |
||
| 596 | } |
||
| 597 | |||
| 598 | if ( !$valid ) { |
||
| 599 | if ( count( $hoistedErrors ) === 1 ) { |
||
| 600 | $hoistedErrors = $hoistedErrors[0]; |
||
| 601 | } |
||
| 602 | return $hoistedErrors; |
||
| 603 | } |
||
| 604 | |||
| 605 | $callback = $this->mSubmitCallback; |
||
| 606 | if ( !is_callable( $callback ) ) { |
||
| 607 | throw new MWException( 'HTMLForm: no submit callback provided. Use ' . |
||
| 608 | 'setSubmitCallback() to set one.' ); |
||
| 609 | } |
||
| 610 | |||
| 611 | $data = $this->filterDataForSubmit( $this->mFieldData ); |
||
| 612 | |||
| 613 | $res = call_user_func( $callback, $data, $this ); |
||
| 614 | if ( $res === false ) { |
||
| 615 | $this->mWasSubmitted = false; |
||
| 616 | } |
||
| 617 | |||
| 618 | return $res; |
||
| 619 | } |
||
| 620 | |||
| 621 | /** |
||
| 622 | * Test whether the form was considered to have been submitted or not, i.e. |
||
| 623 | * whether the last call to tryAuthorizedSubmit or trySubmit returned |
||
| 624 | * non-false. |
||
| 625 | * |
||
| 626 | * This will return false until HTMLForm::tryAuthorizedSubmit or |
||
| 627 | * HTMLForm::trySubmit is called. |
||
| 628 | * |
||
| 629 | * @since 1.23 |
||
| 630 | * @return bool |
||
| 631 | */ |
||
| 632 | public function wasSubmitted() { |
||
| 633 | return $this->mWasSubmitted; |
||
| 634 | } |
||
| 635 | |||
| 636 | /** |
||
| 637 | * Set a callback to a function to do something with the form |
||
| 638 | * once it's been successfully validated. |
||
| 639 | * |
||
| 640 | * @param callable $cb The function will be passed the output from |
||
| 641 | * HTMLForm::filterDataForSubmit and this HTMLForm object, and must |
||
| 642 | * return as documented for HTMLForm::trySubmit |
||
| 643 | * |
||
| 644 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 645 | */ |
||
| 646 | public function setSubmitCallback( $cb ) { |
||
| 647 | $this->mSubmitCallback = $cb; |
||
| 648 | |||
| 649 | return $this; |
||
| 650 | } |
||
| 651 | |||
| 652 | /** |
||
| 653 | * Set a message to display on a validation error. |
||
| 654 | * |
||
| 655 | * @param string|array $msg String or Array of valid inputs to wfMessage() |
||
| 656 | * (so each entry can be either a String or Array) |
||
| 657 | * |
||
| 658 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 659 | */ |
||
| 660 | public function setValidationErrorMessage( $msg ) { |
||
| 661 | $this->mValidationErrorMessage = $msg; |
||
| 662 | |||
| 663 | return $this; |
||
| 664 | } |
||
| 665 | |||
| 666 | /** |
||
| 667 | * Set the introductory message, overwriting any existing message. |
||
| 668 | * |
||
| 669 | * @param string $msg Complete text of message to display |
||
| 670 | * |
||
| 671 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 672 | */ |
||
| 673 | public function setIntro( $msg ) { |
||
| 674 | $this->setPreText( $msg ); |
||
| 675 | |||
| 676 | return $this; |
||
| 677 | } |
||
| 678 | |||
| 679 | /** |
||
| 680 | * Set the introductory message HTML, overwriting any existing message. |
||
| 681 | * @since 1.19 |
||
| 682 | * |
||
| 683 | * @param string $msg Complete HTML of message to display |
||
| 684 | * |
||
| 685 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 686 | */ |
||
| 687 | public function setPreText( $msg ) { |
||
| 688 | $this->mPre = $msg; |
||
| 689 | |||
| 690 | return $this; |
||
| 691 | } |
||
| 692 | |||
| 693 | /** |
||
| 694 | * Add HTML to introductory message. |
||
| 695 | * |
||
| 696 | * @param string $msg Complete HTML of message to display |
||
| 697 | * |
||
| 698 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 699 | */ |
||
| 700 | public function addPreText( $msg ) { |
||
| 701 | $this->mPre .= $msg; |
||
| 702 | |||
| 703 | return $this; |
||
| 704 | } |
||
| 705 | |||
| 706 | /** |
||
| 707 | * Add HTML to the header, inside the form. |
||
| 708 | * |
||
| 709 | * @param string $msg Additional HTML to display in header |
||
| 710 | * @param string|null $section The section to add the header to |
||
| 711 | * |
||
| 712 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 713 | */ |
||
| 714 | View Code Duplication | public function addHeaderText( $msg, $section = null ) { |
|
| 715 | if ( $section === null ) { |
||
| 716 | $this->mHeader .= $msg; |
||
| 717 | } else { |
||
| 718 | if ( !isset( $this->mSectionHeaders[$section] ) ) { |
||
| 719 | $this->mSectionHeaders[$section] = ''; |
||
| 720 | } |
||
| 721 | $this->mSectionHeaders[$section] .= $msg; |
||
| 722 | } |
||
| 723 | |||
| 724 | return $this; |
||
| 725 | } |
||
| 726 | |||
| 727 | /** |
||
| 728 | * Set header text, inside the form. |
||
| 729 | * @since 1.19 |
||
| 730 | * |
||
| 731 | * @param string $msg Complete HTML of header to display |
||
| 732 | * @param string|null $section The section to add the header to |
||
| 733 | * |
||
| 734 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 735 | */ |
||
| 736 | public function setHeaderText( $msg, $section = null ) { |
||
| 737 | if ( $section === null ) { |
||
| 738 | $this->mHeader = $msg; |
||
| 739 | } else { |
||
| 740 | $this->mSectionHeaders[$section] = $msg; |
||
| 741 | } |
||
| 742 | |||
| 743 | return $this; |
||
| 744 | } |
||
| 745 | |||
| 746 | /** |
||
| 747 | * Get header text. |
||
| 748 | * |
||
| 749 | * @param string|null $section The section to get the header text for |
||
| 750 | * @since 1.26 |
||
| 751 | * @return string HTML |
||
| 752 | */ |
||
| 753 | public function getHeaderText( $section = null ) { |
||
| 754 | if ( $section === null ) { |
||
| 755 | return $this->mHeader; |
||
| 756 | } else { |
||
| 757 | return isset( $this->mSectionHeaders[$section] ) ? $this->mSectionHeaders[$section] : ''; |
||
| 758 | } |
||
| 759 | } |
||
| 760 | |||
| 761 | /** |
||
| 762 | * Add footer text, inside the form. |
||
| 763 | * |
||
| 764 | * @param string $msg Complete text of message to display |
||
| 765 | * @param string|null $section The section to add the footer text to |
||
| 766 | * |
||
| 767 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 768 | */ |
||
| 769 | View Code Duplication | public function addFooterText( $msg, $section = null ) { |
|
| 770 | if ( $section === null ) { |
||
| 771 | $this->mFooter .= $msg; |
||
| 772 | } else { |
||
| 773 | if ( !isset( $this->mSectionFooters[$section] ) ) { |
||
| 774 | $this->mSectionFooters[$section] = ''; |
||
| 775 | } |
||
| 776 | $this->mSectionFooters[$section] .= $msg; |
||
| 777 | } |
||
| 778 | |||
| 779 | return $this; |
||
| 780 | } |
||
| 781 | |||
| 782 | /** |
||
| 783 | * Set footer text, inside the form. |
||
| 784 | * @since 1.19 |
||
| 785 | * |
||
| 786 | * @param string $msg Complete text of message to display |
||
| 787 | * @param string|null $section The section to add the footer text to |
||
| 788 | * |
||
| 789 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 790 | */ |
||
| 791 | public function setFooterText( $msg, $section = null ) { |
||
| 792 | if ( $section === null ) { |
||
| 793 | $this->mFooter = $msg; |
||
| 794 | } else { |
||
| 795 | $this->mSectionFooters[$section] = $msg; |
||
| 796 | } |
||
| 797 | |||
| 798 | return $this; |
||
| 799 | } |
||
| 800 | |||
| 801 | /** |
||
| 802 | * Get footer text. |
||
| 803 | * |
||
| 804 | * @param string|null $section The section to get the footer text for |
||
| 805 | * @since 1.26 |
||
| 806 | * @return string |
||
| 807 | */ |
||
| 808 | public function getFooterText( $section = null ) { |
||
| 809 | if ( $section === null ) { |
||
| 810 | return $this->mFooter; |
||
| 811 | } else { |
||
| 812 | return isset( $this->mSectionFooters[$section] ) ? $this->mSectionFooters[$section] : ''; |
||
| 813 | } |
||
| 814 | } |
||
| 815 | |||
| 816 | /** |
||
| 817 | * Add text to the end of the display. |
||
| 818 | * |
||
| 819 | * @param string $msg Complete text of message to display |
||
| 820 | * |
||
| 821 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 822 | */ |
||
| 823 | public function addPostText( $msg ) { |
||
| 824 | $this->mPost .= $msg; |
||
| 825 | |||
| 826 | return $this; |
||
| 827 | } |
||
| 828 | |||
| 829 | /** |
||
| 830 | * Set text at the end of the display. |
||
| 831 | * |
||
| 832 | * @param string $msg Complete text of message to display |
||
| 833 | * |
||
| 834 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 835 | */ |
||
| 836 | public function setPostText( $msg ) { |
||
| 837 | $this->mPost = $msg; |
||
| 838 | |||
| 839 | return $this; |
||
| 840 | } |
||
| 841 | |||
| 842 | /** |
||
| 843 | * Add a hidden field to the output |
||
| 844 | * |
||
| 845 | * @param string $name Field name. This will be used exactly as entered |
||
| 846 | * @param string $value Field value |
||
| 847 | * @param array $attribs |
||
| 848 | * |
||
| 849 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 850 | */ |
||
| 851 | public function addHiddenField( $name, $value, array $attribs = [] ) { |
||
| 852 | $attribs += [ 'name' => $name ]; |
||
| 853 | $this->mHiddenFields[] = [ $value, $attribs ]; |
||
| 854 | |||
| 855 | return $this; |
||
| 856 | } |
||
| 857 | |||
| 858 | /** |
||
| 859 | * Add an array of hidden fields to the output |
||
| 860 | * |
||
| 861 | * @since 1.22 |
||
| 862 | * |
||
| 863 | * @param array $fields Associative array of fields to add; |
||
| 864 | * mapping names to their values |
||
| 865 | * |
||
| 866 | * @return HTMLForm $this for chaining calls |
||
| 867 | */ |
||
| 868 | public function addHiddenFields( array $fields ) { |
||
| 869 | foreach ( $fields as $name => $value ) { |
||
| 870 | $this->mHiddenFields[] = [ $value, [ 'name' => $name ] ]; |
||
| 871 | } |
||
| 872 | |||
| 873 | return $this; |
||
| 874 | } |
||
| 875 | |||
| 876 | /** |
||
| 877 | * Add a button to the form |
||
| 878 | * |
||
| 879 | * @since 1.27 takes an array as shown. Earlier versions accepted |
||
| 880 | * 'name', 'value', 'id', and 'attribs' as separate parameters in that |
||
| 881 | * order. |
||
| 882 | * @note Custom labels ('label', 'label-message', 'label-raw') are not |
||
| 883 | * supported for IE6 and IE7 due to bugs in those browsers. If detected, |
||
| 884 | * they will be served buttons using 'value' as the button label. |
||
| 885 | * @param array $data Data to define the button: |
||
| 886 | * - name: (string) Button name. |
||
| 887 | * - value: (string) Button value. |
||
| 888 | * - label-message: (string, optional) Button label message key to use |
||
| 889 | * instead of 'value'. Overrides 'label' and 'label-raw'. |
||
| 890 | * - label: (string, optional) Button label text to use instead of |
||
| 891 | * 'value'. Overrides 'label-raw'. |
||
| 892 | * - label-raw: (string, optional) Button label HTML to use instead of |
||
| 893 | * 'value'. |
||
| 894 | * - id: (string, optional) DOM id for the button. |
||
| 895 | * - attribs: (array, optional) Additional HTML attributes. |
||
| 896 | * - flags: (string|string[], optional) OOUI flags. |
||
| 897 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 898 | */ |
||
| 899 | public function addButton( $data ) { |
||
| 900 | if ( !is_array( $data ) ) { |
||
| 901 | $args = func_get_args(); |
||
| 902 | if ( count( $args ) < 2 || count( $args ) > 4 ) { |
||
| 903 | throw new InvalidArgumentException( |
||
| 904 | 'Incorrect number of arguments for deprecated calling style' |
||
| 905 | ); |
||
| 906 | } |
||
| 907 | $data = [ |
||
| 908 | 'name' => $args[0], |
||
| 909 | 'value' => $args[1], |
||
| 910 | 'id' => isset( $args[2] ) ? $args[2] : null, |
||
| 911 | 'attribs' => isset( $args[3] ) ? $args[3] : null, |
||
| 912 | ]; |
||
| 913 | } else { |
||
| 914 | if ( !isset( $data['name'] ) ) { |
||
| 915 | throw new InvalidArgumentException( 'A name is required' ); |
||
| 916 | } |
||
| 917 | if ( !isset( $data['value'] ) ) { |
||
| 918 | throw new InvalidArgumentException( 'A value is required' ); |
||
| 919 | } |
||
| 920 | } |
||
| 921 | $this->mButtons[] = $data + [ |
||
| 922 | 'id' => null, |
||
| 923 | 'attribs' => null, |
||
| 924 | 'flags' => null, |
||
| 925 | ]; |
||
| 926 | |||
| 927 | return $this; |
||
| 928 | } |
||
| 929 | |||
| 930 | /** |
||
| 931 | * Set the salt for the edit token. |
||
| 932 | * |
||
| 933 | * Only useful when the method is "post". |
||
| 934 | * |
||
| 935 | * @since 1.24 |
||
| 936 | * @param string|array $salt Salt to use |
||
| 937 | * @return HTMLForm $this For chaining calls |
||
| 938 | */ |
||
| 939 | public function setTokenSalt( $salt ) { |
||
| 940 | $this->mTokenSalt = $salt; |
||
| 941 | |||
| 942 | return $this; |
||
| 943 | } |
||
| 944 | |||
| 945 | /** |
||
| 946 | * Display the form (sending to the context's OutputPage object), with an |
||
| 947 | * appropriate error message or stack of messages, and any validation errors, etc. |
||
| 948 | * |
||
| 949 | * @attention You should call prepareForm() before calling this function. |
||
| 950 | * Moreover, when doing method chaining this should be the very last method |
||
| 951 | * call just after prepareForm(). |
||
| 952 | * |
||
| 953 | * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit() |
||
| 954 | * |
||
| 955 | * @return void Nothing, should be last call |
||
| 956 | */ |
||
| 957 | public function displayForm( $submitResult ) { |
||
| 958 | $this->getOutput()->addHTML( $this->getHTML( $submitResult ) ); |
||
| 959 | } |
||
| 960 | |||
| 961 | /** |
||
| 962 | * Returns the raw HTML generated by the form |
||
| 963 | * |
||
| 964 | * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit() |
||
| 965 | * |
||
| 966 | * @return string HTML |
||
| 967 | */ |
||
| 968 | public function getHTML( $submitResult ) { |
||
| 969 | # For good measure (it is the default) |
||
| 970 | $this->getOutput()->preventClickjacking(); |
||
| 971 | $this->getOutput()->addModules( 'mediawiki.htmlform' ); |
||
| 972 | $this->getOutput()->addModuleStyles( 'mediawiki.htmlform.styles' ); |
||
| 973 | |||
| 974 | $html = '' |
||
| 975 | . $this->getErrors( $submitResult ) |
||
| 976 | . $this->getHeaderText() |
||
| 977 | . $this->getBody() |
||
| 978 | . $this->getHiddenFields() |
||
| 979 | . $this->getButtons() |
||
| 980 | . $this->getFooterText(); |
||
| 981 | |||
| 982 | $html = $this->wrapForm( $html ); |
||
| 983 | |||
| 984 | return '' . $this->mPre . $html . $this->mPost; |
||
| 985 | } |
||
| 986 | |||
| 987 | /** |
||
| 988 | * Get HTML attributes for the `<form>` tag. |
||
| 989 | * @return array |
||
| 990 | */ |
||
| 991 | protected function getFormAttributes() { |
||
| 992 | # Use multipart/form-data |
||
| 993 | $encType = $this->mUseMultipart |
||
| 994 | ? 'multipart/form-data' |
||
| 995 | : 'application/x-www-form-urlencoded'; |
||
| 996 | # Attributes |
||
| 997 | $attribs = [ |
||
| 998 | 'action' => $this->getAction(), |
||
| 999 | 'method' => $this->getMethod(), |
||
| 1000 | 'enctype' => $encType, |
||
| 1001 | ]; |
||
| 1002 | if ( $this->mId ) { |
||
| 1003 | $attribs['id'] = $this->mId; |
||
| 1004 | } |
||
| 1005 | if ( $this->mAutocomplete ) { |
||
| 1006 | $attribs['autocomplete'] = $this->mAutocomplete; |
||
| 1007 | } |
||
| 1008 | if ( $this->mName ) { |
||
| 1009 | $attribs['name'] = $this->mName; |
||
| 1010 | } |
||
| 1011 | return $attribs; |
||
| 1012 | } |
||
| 1013 | |||
| 1014 | /** |
||
| 1015 | * Wrap the form innards in an actual "<form>" element |
||
| 1016 | * |
||
| 1017 | * @param string $html HTML contents to wrap. |
||
| 1018 | * |
||
| 1019 | * @return string Wrapped HTML. |
||
| 1020 | */ |
||
| 1021 | public function wrapForm( $html ) { |
||
| 1022 | # Include a <fieldset> wrapper for style, if requested. |
||
| 1023 | if ( $this->mWrapperLegend !== false ) { |
||
| 1024 | $legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false; |
||
| 1025 | $html = Xml::fieldset( $legend, $html ); |
||
| 1026 | } |
||
| 1027 | |||
| 1028 | return Html::rawElement( |
||
| 1029 | 'form', |
||
| 1030 | $this->getFormAttributes() + [ 'class' => 'visualClear' ], |
||
| 1031 | $html |
||
| 1032 | ); |
||
| 1033 | } |
||
| 1034 | |||
| 1035 | /** |
||
| 1036 | * Get the hidden fields that should go inside the form. |
||
| 1037 | * @return string HTML. |
||
| 1038 | */ |
||
| 1039 | public function getHiddenFields() { |
||
| 1040 | $html = ''; |
||
| 1041 | if ( $this->getMethod() === 'post' ) { |
||
| 1042 | $html .= Html::hidden( |
||
| 1043 | 'wpEditToken', |
||
| 1044 | $this->getUser()->getEditToken( $this->mTokenSalt ), |
||
| 1045 | [ 'id' => 'wpEditToken' ] |
||
| 1046 | ) . "\n"; |
||
| 1047 | $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; |
||
| 1048 | } |
||
| 1049 | |||
| 1050 | $articlePath = $this->getConfig()->get( 'ArticlePath' ); |
||
| 1051 | if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) { |
||
| 1052 | $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; |
||
| 1053 | } |
||
| 1054 | |||
| 1055 | foreach ( $this->mHiddenFields as $data ) { |
||
| 1056 | list( $value, $attribs ) = $data; |
||
| 1057 | $html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n"; |
||
| 1058 | } |
||
| 1059 | |||
| 1060 | return $html; |
||
| 1061 | } |
||
| 1062 | |||
| 1063 | /** |
||
| 1064 | * Get the submit and (potentially) reset buttons. |
||
| 1065 | * @return string HTML. |
||
| 1066 | */ |
||
| 1067 | public function getButtons() { |
||
| 1068 | $buttons = ''; |
||
| 1069 | $useMediaWikiUIEverywhere = $this->getConfig()->get( 'UseMediaWikiUIEverywhere' ); |
||
| 1070 | |||
| 1071 | if ( $this->mShowSubmit ) { |
||
| 1072 | $attribs = []; |
||
| 1073 | |||
| 1074 | if ( isset( $this->mSubmitID ) ) { |
||
| 1075 | $attribs['id'] = $this->mSubmitID; |
||
| 1076 | } |
||
| 1077 | |||
| 1078 | if ( isset( $this->mSubmitName ) ) { |
||
| 1079 | $attribs['name'] = $this->mSubmitName; |
||
| 1080 | } |
||
| 1081 | |||
| 1082 | if ( isset( $this->mSubmitTooltip ) ) { |
||
| 1083 | $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip ); |
||
| 1084 | } |
||
| 1085 | |||
| 1086 | $attribs['class'] = [ 'mw-htmlform-submit' ]; |
||
| 1087 | |||
| 1088 | if ( $useMediaWikiUIEverywhere ) { |
||
| 1089 | foreach ( $this->mSubmitFlags as $flag ) { |
||
| 1090 | $attribs['class'][] = 'mw-ui-' . $flag; |
||
| 1091 | } |
||
| 1092 | $attribs['class'][] = 'mw-ui-button'; |
||
| 1093 | } |
||
| 1094 | |||
| 1095 | $buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n"; |
||
| 1096 | } |
||
| 1097 | |||
| 1098 | View Code Duplication | if ( $this->mShowReset ) { |
|
| 1099 | $buttons .= Html::element( |
||
| 1100 | 'input', |
||
| 1101 | [ |
||
| 1102 | 'type' => 'reset', |
||
| 1103 | 'value' => $this->msg( 'htmlform-reset' )->text(), |
||
| 1104 | 'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null, |
||
| 1105 | ] |
||
| 1106 | ) . "\n"; |
||
| 1107 | } |
||
| 1108 | |||
| 1109 | // IE<8 has bugs with <button>, so we'll need to avoid them. |
||
| 1110 | $isBadIE = preg_match( '/MSIE [1-7]\./i', $this->getRequest()->getHeader( 'User-Agent' ) ); |
||
| 1111 | |||
| 1112 | foreach ( $this->mButtons as $button ) { |
||
| 1113 | $attrs = [ |
||
| 1114 | 'type' => 'submit', |
||
| 1115 | 'name' => $button['name'], |
||
| 1116 | 'value' => $button['value'] |
||
| 1117 | ]; |
||
| 1118 | |||
| 1119 | if ( isset( $button['label-message'] ) ) { |
||
| 1120 | $label = $this->getMessage( $button['label-message'] )->parse(); |
||
| 1121 | } elseif ( isset( $button['label'] ) ) { |
||
| 1122 | $label = htmlspecialchars( $button['label'] ); |
||
| 1123 | } elseif ( isset( $button['label-raw'] ) ) { |
||
| 1124 | $label = $button['label-raw']; |
||
| 1125 | } else { |
||
| 1126 | $label = htmlspecialchars( $button['value'] ); |
||
| 1127 | } |
||
| 1128 | |||
| 1129 | if ( $button['attribs'] ) { |
||
| 1130 | $attrs += $button['attribs']; |
||
| 1131 | } |
||
| 1132 | |||
| 1133 | if ( isset( $button['id'] ) ) { |
||
| 1134 | $attrs['id'] = $button['id']; |
||
| 1135 | } |
||
| 1136 | |||
| 1137 | if ( $useMediaWikiUIEverywhere ) { |
||
| 1138 | $attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : []; |
||
| 1139 | $attrs['class'][] = 'mw-ui-button'; |
||
| 1140 | } |
||
| 1141 | |||
| 1142 | if ( $isBadIE ) { |
||
| 1143 | $buttons .= Html::element( 'input', $attrs ) . "\n"; |
||
| 1144 | } else { |
||
| 1145 | $buttons .= Html::rawElement( 'button', $attrs, $label ) . "\n"; |
||
| 1146 | } |
||
| 1147 | } |
||
| 1148 | |||
| 1149 | if ( !$buttons ) { |
||
| 1150 | return ''; |
||
| 1151 | } |
||
| 1152 | |||
| 1153 | return Html::rawElement( 'span', |
||
| 1154 | [ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n"; |
||
| 1155 | } |
||
| 1156 | |||
| 1157 | /** |
||
| 1158 | * Get the whole body of the form. |
||
| 1159 | * @return string |
||
| 1160 | */ |
||
| 1161 | public function getBody() { |
||
| 1162 | return $this->displaySection( $this->mFieldTree, $this->mTableId ); |
||
| 1163 | } |
||
| 1164 | |||
| 1165 | /** |
||
| 1166 | * Format and display an error message stack. |
||
| 1167 | * |
||
| 1168 | * @param string|array|Status $errors |
||
| 1169 | * |
||
| 1170 | * @return string |
||
| 1171 | */ |
||
| 1172 | public function getErrors( $errors ) { |
||
| 1173 | if ( $errors instanceof Status ) { |
||
| 1174 | if ( $errors->isOK() ) { |
||
| 1175 | $errorstr = ''; |
||
| 1176 | } else { |
||
| 1177 | $errorstr = $this->getOutput()->parse( $errors->getWikiText() ); |
||
| 1178 | } |
||
| 1179 | } elseif ( is_array( $errors ) ) { |
||
| 1180 | $errorstr = $this->formatErrors( $errors ); |
||
| 1181 | } else { |
||
| 1182 | $errorstr = $errors; |
||
| 1183 | } |
||
| 1184 | |||
| 1185 | return $errorstr |
||
| 1186 | ? Html::rawElement( 'div', [ 'class' => 'error' ], $errorstr ) |
||
| 1187 | : ''; |
||
| 1188 | } |
||
| 1189 | |||
| 1190 | /** |
||
| 1191 | * Format a stack of error messages into a single HTML string |
||
| 1192 | * |
||
| 1193 | * @param array $errors Array of message keys/values |
||
| 1194 | * |
||
| 1195 | * @return string HTML, a "<ul>" list of errors |
||
| 1196 | */ |
||
| 1197 | public function formatErrors( $errors ) { |
||
| 1198 | $errorstr = ''; |
||
| 1199 | |||
| 1200 | foreach ( $errors as $error ) { |
||
| 1201 | $errorstr .= Html::rawElement( |
||
| 1202 | 'li', |
||
| 1203 | [], |
||
| 1204 | $this->getMessage( $error )->parse() |
||
| 1205 | ); |
||
| 1206 | } |
||
| 1207 | |||
| 1208 | $errorstr = Html::rawElement( 'ul', [], $errorstr ); |
||
| 1209 | |||
| 1210 | return $errorstr; |
||
| 1211 | } |
||
| 1212 | |||
| 1213 | /** |
||
| 1214 | * Set the text for the submit button |
||
| 1215 | * |
||
| 1216 | * @param string $t Plaintext |
||
| 1217 | * |
||
| 1218 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1219 | */ |
||
| 1220 | public function setSubmitText( $t ) { |
||
| 1221 | $this->mSubmitText = $t; |
||
| 1222 | |||
| 1223 | return $this; |
||
| 1224 | } |
||
| 1225 | |||
| 1226 | /** |
||
| 1227 | * Identify that the submit button in the form has a destructive action |
||
| 1228 | * @since 1.24 |
||
| 1229 | * |
||
| 1230 | * @return HTMLForm $this for chaining calls (since 1.28) |
||
| 1231 | */ |
||
| 1232 | public function setSubmitDestructive() { |
||
| 1237 | |||
| 1238 | /** |
||
| 1239 | * Identify that the submit button in the form has a progressive action |
||
| 1240 | * @since 1.25 |
||
| 1241 | * |
||
| 1242 | * @return HTMLForm $this for chaining calls (since 1.28) |
||
| 1243 | */ |
||
| 1244 | public function setSubmitProgressive() { |
||
| 1249 | |||
| 1250 | /** |
||
| 1251 | * Set the text for the submit button to a message |
||
| 1252 | * @since 1.19 |
||
| 1253 | * |
||
| 1254 | * @param string|Message $msg Message key or Message object |
||
| 1255 | * |
||
| 1256 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1257 | */ |
||
| 1258 | public function setSubmitTextMsg( $msg ) { |
||
| 1259 | if ( !$msg instanceof Message ) { |
||
| 1260 | $msg = $this->msg( $msg ); |
||
| 1261 | } |
||
| 1262 | $this->setSubmitText( $msg->text() ); |
||
| 1263 | |||
| 1264 | return $this; |
||
| 1265 | } |
||
| 1266 | |||
| 1267 | /** |
||
| 1268 | * Get the text for the submit button, either customised or a default. |
||
| 1269 | * @return string |
||
| 1270 | */ |
||
| 1271 | public function getSubmitText() { |
||
| 1272 | return $this->mSubmitText ?: $this->msg( 'htmlform-submit' )->text(); |
||
| 1273 | } |
||
| 1274 | |||
| 1275 | /** |
||
| 1276 | * @param string $name Submit button name |
||
| 1277 | * |
||
| 1278 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1279 | */ |
||
| 1280 | public function setSubmitName( $name ) { |
||
| 1281 | $this->mSubmitName = $name; |
||
| 1282 | |||
| 1283 | return $this; |
||
| 1284 | } |
||
| 1285 | |||
| 1286 | /** |
||
| 1287 | * @param string $name Tooltip for the submit button |
||
| 1288 | * |
||
| 1289 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1290 | */ |
||
| 1291 | public function setSubmitTooltip( $name ) { |
||
| 1292 | $this->mSubmitTooltip = $name; |
||
| 1296 | |||
| 1297 | /** |
||
| 1298 | * Set the id for the submit button. |
||
| 1299 | * |
||
| 1300 | * @param string $t |
||
| 1301 | * |
||
| 1302 | * @todo FIXME: Integrity of $t is *not* validated |
||
| 1303 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1304 | */ |
||
| 1305 | public function setSubmitID( $t ) { |
||
| 1310 | |||
| 1311 | /** |
||
| 1312 | * Stop a default submit button being shown for this form. This implies that an |
||
| 1313 | * alternate submit method must be provided manually. |
||
| 1314 | * |
||
| 1315 | * @since 1.22 |
||
| 1316 | * |
||
| 1317 | * @param bool $suppressSubmit Set to false to re-enable the button again |
||
| 1318 | * |
||
| 1319 | * @return HTMLForm $this for chaining calls |
||
| 1320 | */ |
||
| 1321 | public function suppressDefaultSubmit( $suppressSubmit = true ) { |
||
| 1326 | |||
| 1327 | /** |
||
| 1328 | * Set the id of the \<table\> or outermost \<div\> element. |
||
| 1329 | * |
||
| 1330 | * @since 1.22 |
||
| 1331 | * |
||
| 1332 | * @param string $id New value of the id attribute, or "" to remove |
||
| 1333 | * |
||
| 1334 | * @return HTMLForm $this for chaining calls |
||
| 1335 | */ |
||
| 1336 | public function setTableId( $id ) { |
||
| 1341 | |||
| 1342 | /** |
||
| 1343 | * @param string $id DOM id for the form |
||
| 1344 | * |
||
| 1345 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1346 | */ |
||
| 1347 | public function setId( $id ) { |
||
| 1352 | |||
| 1353 | /** |
||
| 1354 | * @param string $name 'name' attribute for the form |
||
| 1355 | * @return HTMLForm $this for chaining calls |
||
| 1356 | */ |
||
| 1357 | public function setName( $name ) { |
||
| 1362 | |||
| 1363 | /** |
||
| 1364 | * Prompt the whole form to be wrapped in a "<fieldset>", with |
||
| 1365 | * this text as its "<legend>" element. |
||
| 1366 | * |
||
| 1367 | * @param string|bool $legend If false, no wrapper or legend will be displayed. |
||
| 1368 | * If true, a wrapper will be displayed, but no legend. |
||
| 1369 | * If a string, a wrapper will be displayed with that string as a legend. |
||
| 1370 | * The string will be escaped before being output (this doesn't support HTML). |
||
| 1371 | * |
||
| 1372 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1373 | */ |
||
| 1374 | public function setWrapperLegend( $legend ) { |
||
| 1379 | |||
| 1380 | /** |
||
| 1381 | * Prompt the whole form to be wrapped in a "<fieldset>", with |
||
| 1382 | * this message as its "<legend>" element. |
||
| 1383 | * @since 1.19 |
||
| 1384 | * |
||
| 1385 | * @param string|Message $msg Message key or Message object |
||
| 1386 | * |
||
| 1387 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1388 | */ |
||
| 1389 | public function setWrapperLegendMsg( $msg ) { |
||
| 1397 | |||
| 1398 | /** |
||
| 1399 | * Set the prefix for various default messages |
||
| 1400 | * @todo Currently only used for the "<fieldset>" legend on forms |
||
| 1401 | * with multiple sections; should be used elsewhere? |
||
| 1402 | * |
||
| 1403 | * @param string $p |
||
| 1404 | * |
||
| 1405 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1406 | */ |
||
| 1407 | public function setMessagePrefix( $p ) { |
||
| 1412 | |||
| 1413 | /** |
||
| 1414 | * Set the title for form submission |
||
| 1415 | * |
||
| 1416 | * @param Title $t Title of page the form is on/should be posted to |
||
| 1417 | * |
||
| 1418 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1419 | */ |
||
| 1420 | public function setTitle( $t ) { |
||
| 1425 | |||
| 1426 | /** |
||
| 1427 | * Get the title |
||
| 1428 | * @return Title |
||
| 1429 | */ |
||
| 1430 | public function getTitle() { |
||
| 1435 | |||
| 1436 | /** |
||
| 1437 | * Set the method used to submit the form |
||
| 1438 | * |
||
| 1439 | * @param string $method |
||
| 1440 | * |
||
| 1441 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1442 | */ |
||
| 1443 | public function setMethod( $method = 'post' ) { |
||
| 1448 | |||
| 1449 | /** |
||
| 1450 | * @return string Always lowercase |
||
| 1451 | */ |
||
| 1452 | public function getMethod() { |
||
| 1455 | |||
| 1456 | /** |
||
| 1457 | * Wraps the given $section into an user-visible fieldset. |
||
| 1458 | * |
||
| 1459 | * @param string $legend Legend text for the fieldset |
||
| 1460 | * @param string $section The section content in plain Html |
||
| 1461 | * @param array $attributes Additional attributes for the fieldset |
||
| 1462 | * @return string The fieldset's Html |
||
| 1463 | */ |
||
| 1464 | protected function wrapFieldSetSection( $legend, $section, $attributes ) { |
||
| 1467 | |||
| 1468 | /** |
||
| 1469 | * @todo Document |
||
| 1470 | * |
||
| 1471 | * @param array[]|HTMLFormField[] $fields Array of fields (either arrays or |
||
| 1472 | * objects). |
||
| 1473 | * @param string $sectionName ID attribute of the "<table>" tag for this |
||
| 1474 | * section, ignored if empty. |
||
| 1475 | * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of |
||
| 1476 | * each subsection, ignored if empty. |
||
| 1477 | * @param bool &$hasUserVisibleFields Whether the section had user-visible fields. |
||
| 1478 | * @throws LogicException When called on uninitialized field data, e.g. When |
||
| 1479 | * HTMLForm::displayForm was called without calling HTMLForm::prepareForm |
||
| 1480 | * first. |
||
| 1481 | * |
||
| 1482 | * @return string |
||
| 1483 | */ |
||
| 1484 | public function displaySection( $fields, |
||
| 1567 | |||
| 1568 | /** |
||
| 1569 | * Put a form section together from the individual fields' HTML, merging it and wrapping. |
||
| 1570 | * @param array $fieldsHtml |
||
| 1571 | * @param string $sectionName |
||
| 1572 | * @param bool $anyFieldHasLabel |
||
| 1573 | * @return string HTML |
||
| 1574 | */ |
||
| 1575 | protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) { |
||
| 1607 | |||
| 1608 | /** |
||
| 1609 | * Construct the form fields from the Descriptor array |
||
| 1610 | */ |
||
| 1611 | public function loadData() { |
||
| 1633 | |||
| 1634 | /** |
||
| 1635 | * Stop a reset button being shown for this form |
||
| 1636 | * |
||
| 1637 | * @param bool $suppressReset Set to false to re-enable the button again |
||
| 1638 | * |
||
| 1639 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1640 | */ |
||
| 1641 | public function suppressReset( $suppressReset = true ) { |
||
| 1646 | |||
| 1647 | /** |
||
| 1648 | * Overload this if you want to apply special filtration routines |
||
| 1649 | * to the form as a whole, after it's submitted but before it's |
||
| 1650 | * processed. |
||
| 1651 | * |
||
| 1652 | * @param array $data |
||
| 1653 | * |
||
| 1654 | * @return array |
||
| 1655 | */ |
||
| 1656 | public function filterDataForSubmit( $data ) { |
||
| 1659 | |||
| 1660 | /** |
||
| 1661 | * Get a string to go in the "<legend>" of a section fieldset. |
||
| 1662 | * Override this if you want something more complicated. |
||
| 1663 | * |
||
| 1664 | * @param string $key |
||
| 1665 | * |
||
| 1666 | * @return string |
||
| 1667 | */ |
||
| 1668 | public function getLegend( $key ) { |
||
| 1671 | |||
| 1672 | /** |
||
| 1673 | * Set the value for the action attribute of the form. |
||
| 1674 | * When set to false (which is the default state), the set title is used. |
||
| 1675 | * |
||
| 1676 | * @since 1.19 |
||
| 1677 | * |
||
| 1678 | * @param string|bool $action |
||
| 1679 | * |
||
| 1680 | * @return HTMLForm $this for chaining calls (since 1.20) |
||
| 1681 | */ |
||
| 1682 | public function setAction( $action ) { |
||
| 1687 | |||
| 1688 | /** |
||
| 1689 | * Get the value for the action attribute of the form. |
||
| 1690 | * |
||
| 1691 | * @since 1.22 |
||
| 1692 | * |
||
| 1693 | * @return string |
||
| 1694 | */ |
||
| 1695 | public function getAction() { |
||
| 1713 | |||
| 1714 | /** |
||
| 1715 | * Set the value for the autocomplete attribute of the form. |
||
| 1716 | * When set to false (which is the default state), the attribute get not set. |
||
| 1717 | * |
||
| 1718 | * @since 1.27 |
||
| 1719 | * |
||
| 1720 | * @param string|bool $autocomplete |
||
| 1721 | * |
||
| 1722 | * @return HTMLForm $this for chaining calls |
||
| 1723 | */ |
||
| 1724 | public function setAutocomplete( $autocomplete ) { |
||
| 1729 | |||
| 1730 | /** |
||
| 1731 | * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a |
||
| 1732 | * name + parameters array) into a Message. |
||
| 1733 | * @param mixed $value |
||
| 1734 | * @return Message |
||
| 1735 | */ |
||
| 1736 | protected function getMessage( $value ) { |
||
| 1739 | } |
||
| 1740 |
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.
Consider the following example. The parameter
$italyis not defined by the methodfinale(...).The most likely cause is that the parameter was removed, but the annotation was not.