Merge "HTMLForm: throw a warning when no callback is set"
[lhc/web/wiklou.git] / includes / HTMLForm.php
1 <?php
2 /**
3 * HTML form generation and submission handling.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23 /**
24 * Object handling generic submission, CSRF protection, layout and
25 * other logic for UI forms. in a reusable manner.
26 *
27 * In order to generate the form, the HTMLForm object takes an array
28 * structure detailing the form fields available. Each element of the
29 * array is a basic property-list, including the type of field, the
30 * label it is to be given in the form, callbacks for validation and
31 * 'filtering', and other pertinent information.
32 *
33 * Field types are implemented as subclasses of the generic HTMLFormField
34 * object, and typically implement at least getInputHTML, which generates
35 * the HTML for the input field to be placed in the table.
36 *
37 * The constructor input is an associative array of $fieldname => $info,
38 * where $info is an Associative Array with any of the following:
39 *
40 * 'class' -- the subclass of HTMLFormField that will be used
41 * to create the object. *NOT* the CSS class!
42 * 'type' -- roughly translates into the <select> type attribute.
43 * if 'class' is not specified, this is used as a map
44 * through HTMLForm::$typeMappings to get the class name.
45 * 'default' -- default value when the form is displayed
46 * 'id' -- HTML id attribute
47 * 'cssclass' -- CSS class
48 * 'options' -- varies according to the specific object.
49 * 'label-message' -- message key for a message to use as the label.
50 * can be an array of msg key and then parameters to
51 * the message.
52 * 'label' -- alternatively, a raw text message. Overridden by
53 * label-message
54 * 'help' -- message text for a message to use as a help text.
55 * 'help-message' -- message key for a message to use as a help text.
56 * can be an array of msg key and then parameters to
57 * the message.
58 * Overwrites 'help-messages' and 'help'.
59 * 'help-messages' -- array of message key. As above, each item can
60 * be an array of msg key and then parameters.
61 * Overwrites 'help'.
62 * 'required' -- passed through to the object, indicating that it
63 * is a required field.
64 * 'size' -- the length of text fields
65 * 'filter-callback -- a function name to give you the chance to
66 * massage the inputted value before it's processed.
67 * @see HTMLForm::filter()
68 * 'validation-callback' -- a function name to give you the chance
69 * to impose extra validation on the field input.
70 * @see HTMLForm::validate()
71 * 'name' -- By default, the 'name' attribute of the input field
72 * is "wp{$fieldname}". If you want a different name
73 * (eg one without the "wp" prefix), specify it here and
74 * it will be used without modification.
75 *
76 * TODO: Document 'section' / 'subsection' stuff
77 */
78 class HTMLForm extends ContextSource {
79
80 // A mapping of 'type' inputs onto standard HTMLFormField subclasses
81 static $typeMappings = array(
82 'text' => 'HTMLTextField',
83 'textarea' => 'HTMLTextAreaField',
84 'select' => 'HTMLSelectField',
85 'radio' => 'HTMLRadioField',
86 'multiselect' => 'HTMLMultiSelectField',
87 'check' => 'HTMLCheckField',
88 'toggle' => 'HTMLCheckField',
89 'int' => 'HTMLIntField',
90 'float' => 'HTMLFloatField',
91 'info' => 'HTMLInfoField',
92 'selectorother' => 'HTMLSelectOrOtherField',
93 'selectandother' => 'HTMLSelectAndOtherField',
94 'submit' => 'HTMLSubmitField',
95 'hidden' => 'HTMLHiddenField',
96 'edittools' => 'HTMLEditTools',
97
98 // HTMLTextField will output the correct type="" attribute automagically.
99 // There are about four zillion other HTML5 input types, like url, but
100 // we don't use those at the moment, so no point in adding all of them.
101 'email' => 'HTMLTextField',
102 'password' => 'HTMLTextField',
103 );
104
105 protected $mMessagePrefix;
106
107 /** @var HTMLFormField[] */
108 protected $mFlatFields;
109
110 protected $mFieldTree;
111 protected $mShowReset = false;
112 public $mFieldData;
113
114 protected $mSubmitCallback;
115 protected $mValidationErrorMessage;
116
117 protected $mPre = '';
118 protected $mHeader = '';
119 protected $mFooter = '';
120 protected $mSectionHeaders = array();
121 protected $mSectionFooters = array();
122 protected $mPost = '';
123 protected $mId;
124
125 protected $mSubmitID;
126 protected $mSubmitName;
127 protected $mSubmitText;
128 protected $mSubmitTooltip;
129
130 protected $mTitle;
131 protected $mMethod = 'post';
132
133 /**
134 * Form action URL. false means we will use the URL to set Title
135 * @since 1.19
136 * @var bool|string
137 */
138 protected $mAction = false;
139
140 protected $mUseMultipart = false;
141 protected $mHiddenFields = array();
142 protected $mButtons = array();
143
144 protected $mWrapperLegend = false;
145
146 /**
147 * If true, sections that contain both fields and subsections will
148 * render their subsections before their fields.
149 *
150 * Subclasses may set this to false to render subsections after fields
151 * instead.
152 */
153 protected $mSubSectionBeforeFields = true;
154
155 /**
156 * Build a new HTMLForm from an array of field attributes
157 * @param $descriptor Array of Field constructs, as described above
158 * @param $context IContextSource available since 1.18, will become compulsory in 1.18.
159 * Obviates the need to call $form->setTitle()
160 * @param $messagePrefix String a prefix to go in front of default messages
161 */
162 public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) {
163 if( $context instanceof IContextSource ){
164 $this->setContext( $context );
165 $this->mTitle = false; // We don't need them to set a title
166 $this->mMessagePrefix = $messagePrefix;
167 } else {
168 // B/C since 1.18
169 if( is_string( $context ) && $messagePrefix === '' ){
170 // it's actually $messagePrefix
171 $this->mMessagePrefix = $context;
172 }
173 }
174
175 // Expand out into a tree.
176 $loadedDescriptor = array();
177 $this->mFlatFields = array();
178
179 foreach ( $descriptor as $fieldname => $info ) {
180 $section = isset( $info['section'] )
181 ? $info['section']
182 : '';
183
184 if ( isset( $info['type'] ) && $info['type'] == 'file' ) {
185 $this->mUseMultipart = true;
186 }
187
188 $field = self::loadInputFromParameters( $fieldname, $info );
189 $field->mParent = $this;
190
191 $setSection =& $loadedDescriptor;
192 if ( $section ) {
193 $sectionParts = explode( '/', $section );
194
195 while ( count( $sectionParts ) ) {
196 $newName = array_shift( $sectionParts );
197
198 if ( !isset( $setSection[$newName] ) ) {
199 $setSection[$newName] = array();
200 }
201
202 $setSection =& $setSection[$newName];
203 }
204 }
205
206 $setSection[$fieldname] = $field;
207 $this->mFlatFields[$fieldname] = $field;
208 }
209
210 $this->mFieldTree = $loadedDescriptor;
211 }
212
213 /**
214 * Add the HTMLForm-specific JavaScript, if it hasn't been
215 * done already.
216 * @deprecated since 1.18 load modules with ResourceLoader instead
217 */
218 static function addJS() { wfDeprecated( __METHOD__, '1.18' ); }
219
220 /**
221 * Initialise a new Object for the field
222 * @param $fieldname string
223 * @param $descriptor string input Descriptor, as described above
224 * @return HTMLFormField subclass
225 */
226 static function loadInputFromParameters( $fieldname, $descriptor ) {
227 if ( isset( $descriptor['class'] ) ) {
228 $class = $descriptor['class'];
229 } elseif ( isset( $descriptor['type'] ) ) {
230 $class = self::$typeMappings[$descriptor['type']];
231 $descriptor['class'] = $class;
232 } else {
233 $class = null;
234 }
235
236 if ( !$class ) {
237 throw new MWException( "Descriptor with no class: " . print_r( $descriptor, true ) );
238 }
239
240 $descriptor['fieldname'] = $fieldname;
241
242 # TODO
243 # This will throw a fatal error whenever someone try to use
244 # 'class' to feed a CSS class instead of 'cssclass'. Would be
245 # great to avoid the fatal error and show a nice error.
246 $obj = new $class( $descriptor );
247
248 return $obj;
249 }
250
251 /**
252 * Prepare form for submission
253 */
254 function prepareForm() {
255 # Check if we have the info we need
256 if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) {
257 throw new MWException( "You must call setTitle() on an HTMLForm" );
258 }
259
260 # Load data from the request.
261 $this->loadData();
262 }
263
264 /**
265 * Try submitting, with edit token check first
266 * @return Status|boolean
267 */
268 function tryAuthorizedSubmit() {
269 $result = false;
270
271 $submit = false;
272 if ( $this->getMethod() != 'post' ) {
273 $submit = true; // no session check needed
274 } elseif ( $this->getRequest()->wasPosted() ) {
275 $editToken = $this->getRequest()->getVal( 'wpEditToken' );
276 if ( $this->getUser()->isLoggedIn() || $editToken != null ) {
277 // Session tokens for logged-out users have no security value.
278 // However, if the user gave one, check it in order to give a nice
279 // "session expired" error instead of "permission denied" or such.
280 $submit = $this->getUser()->matchEditToken( $editToken );
281 } else {
282 $submit = true;
283 }
284 }
285
286 if ( $submit ) {
287 $result = $this->trySubmit();
288 }
289
290 return $result;
291 }
292
293 /**
294 * The here's-one-I-made-earlier option: do the submission if
295 * posted, or display the form with or without funky validation
296 * errors
297 * @return Bool or Status whether submission was successful.
298 */
299 function show() {
300 $this->prepareForm();
301
302 $result = $this->tryAuthorizedSubmit();
303 if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
304 return $result;
305 }
306
307 $this->displayForm( $result );
308 return false;
309 }
310
311 /**
312 * Validate all the fields, and call the submision callback
313 * function if everything is kosher.
314 * @return Mixed Bool true == Successful submission, Bool false
315 * == No submission attempted, anything else == Error to
316 * display.
317 */
318 function trySubmit() {
319 # Check for validation
320 foreach ( $this->mFlatFields as $fieldname => $field ) {
321 if ( !empty( $field->mParams['nodata'] ) ) {
322 continue;
323 }
324 if ( $field->validate(
325 $this->mFieldData[$fieldname],
326 $this->mFieldData )
327 !== true
328 ) {
329 return isset( $this->mValidationErrorMessage )
330 ? $this->mValidationErrorMessage
331 : array( 'htmlform-invalid-input' );
332 }
333 }
334
335 $callback = $this->mSubmitCallback;
336 if ( !is_callable( $callback ) ) {
337 throw new MWException( 'HTMLForm: no submit callback provided. Use setSubmitCallback() to set one.' );
338 }
339
340 $data = $this->filterDataForSubmit( $this->mFieldData );
341
342 $res = call_user_func( $callback, $data, $this );
343
344 return $res;
345 }
346
347 /**
348 * Set a callback to a function to do something with the form
349 * once it's been successfully validated.
350 * @param $cb String function name. The function will be passed
351 * the output from HTMLForm::filterDataForSubmit, and must
352 * return Bool true on success, Bool false if no submission
353 * was attempted, or String HTML output to display on error.
354 */
355 function setSubmitCallback( $cb ) {
356 $this->mSubmitCallback = $cb;
357 }
358
359 /**
360 * Set a message to display on a validation error.
361 * @param $msg Mixed String or Array of valid inputs to wfMsgExt()
362 * (so each entry can be either a String or Array)
363 */
364 function setValidationErrorMessage( $msg ) {
365 $this->mValidationErrorMessage = $msg;
366 }
367
368 /**
369 * Set the introductory message, overwriting any existing message.
370 * @param $msg String complete text of message to display
371 */
372 function setIntro( $msg ) {
373 $this->setPreText( $msg );
374 }
375
376 /**
377 * Set the introductory message, overwriting any existing message.
378 * @since 1.19
379 * @param $msg String complete text of message to display
380 */
381 function setPreText( $msg ) { $this->mPre = $msg; }
382
383 /**
384 * Add introductory text.
385 * @param $msg String complete text of message to display
386 */
387 function addPreText( $msg ) { $this->mPre .= $msg; }
388
389 /**
390 * Add header text, inside the form.
391 * @param $msg String complete text of message to display
392 * @param $section string The section to add the header to
393 */
394 function addHeaderText( $msg, $section = null ) {
395 if ( is_null( $section ) ) {
396 $this->mHeader .= $msg;
397 } else {
398 if ( !isset( $this->mSectionHeaders[$section] ) ) {
399 $this->mSectionHeaders[$section] = '';
400 }
401 $this->mSectionHeaders[$section] .= $msg;
402 }
403 }
404
405 /**
406 * Set header text, inside the form.
407 * @since 1.19
408 * @param $msg String complete text of message to display
409 * @param $section The section to add the header to
410 */
411 function setHeaderText( $msg, $section = null ) {
412 if ( is_null( $section ) ) {
413 $this->mHeader = $msg;
414 } else {
415 $this->mSectionHeaders[$section] = $msg;
416 }
417 }
418
419 /**
420 * Add footer text, inside the form.
421 * @param $msg String complete text of message to display
422 * @param $section string The section to add the footer text to
423 */
424 function addFooterText( $msg, $section = null ) {
425 if ( is_null( $section ) ) {
426 $this->mFooter .= $msg;
427 } else {
428 if ( !isset( $this->mSectionFooters[$section] ) ) {
429 $this->mSectionFooters[$section] = '';
430 }
431 $this->mSectionFooters[$section] .= $msg;
432 }
433 }
434
435 /**
436 * Set footer text, inside the form.
437 * @since 1.19
438 * @param $msg String complete text of message to display
439 * @param $section string The section to add the footer text to
440 */
441 function setFooterText( $msg, $section = null ) {
442 if ( is_null( $section ) ) {
443 $this->mFooter = $msg;
444 } else {
445 $this->mSectionFooters[$section] = $msg;
446 }
447 }
448
449 /**
450 * Add text to the end of the display.
451 * @param $msg String complete text of message to display
452 */
453 function addPostText( $msg ) { $this->mPost .= $msg; }
454
455 /**
456 * Set text at the end of the display.
457 * @param $msg String complete text of message to display
458 */
459 function setPostText( $msg ) { $this->mPost = $msg; }
460
461 /**
462 * Add a hidden field to the output
463 * @param $name String field name. This will be used exactly as entered
464 * @param $value String field value
465 * @param $attribs Array
466 */
467 public function addHiddenField( $name, $value, $attribs = array() ) {
468 $attribs += array( 'name' => $name );
469 $this->mHiddenFields[] = array( $value, $attribs );
470 }
471
472 public function addButton( $name, $value, $id = null, $attribs = null ) {
473 $this->mButtons[] = compact( 'name', 'value', 'id', 'attribs' );
474 }
475
476 /**
477 * Display the form (sending to $wgOut), with an appropriate error
478 * message or stack of messages, and any validation errors, etc.
479 * @param $submitResult Mixed output from HTMLForm::trySubmit()
480 */
481 function displayForm( $submitResult ) {
482 $this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
483 }
484
485 /**
486 * Returns the raw HTML generated by the form
487 * @param $submitResult Mixed output from HTMLForm::trySubmit()
488 * @return string
489 */
490 function getHTML( $submitResult ) {
491 # For good measure (it is the default)
492 $this->getOutput()->preventClickjacking();
493 $this->getOutput()->addModules( 'mediawiki.htmlform' );
494
495 $html = ''
496 . $this->getErrors( $submitResult )
497 . $this->mHeader
498 . $this->getBody()
499 . $this->getHiddenFields()
500 . $this->getButtons()
501 . $this->mFooter
502 ;
503
504 $html = $this->wrapForm( $html );
505
506 return '' . $this->mPre . $html . $this->mPost;
507 }
508
509 /**
510 * Wrap the form innards in an actual <form> element
511 * @param $html String HTML contents to wrap.
512 * @return String wrapped HTML.
513 */
514 function wrapForm( $html ) {
515
516 # Include a <fieldset> wrapper for style, if requested.
517 if ( $this->mWrapperLegend !== false ) {
518 $html = Xml::fieldset( $this->mWrapperLegend, $html );
519 }
520 # Use multipart/form-data
521 $encType = $this->mUseMultipart
522 ? 'multipart/form-data'
523 : 'application/x-www-form-urlencoded';
524 # Attributes
525 $attribs = array(
526 'action' => $this->mAction === false ? $this->getTitle()->getFullURL() : $this->mAction,
527 'method' => $this->mMethod,
528 'class' => 'visualClear',
529 'enctype' => $encType,
530 );
531 if ( !empty( $this->mId ) ) {
532 $attribs['id'] = $this->mId;
533 }
534
535 return Html::rawElement( 'form', $attribs, $html );
536 }
537
538 /**
539 * Get the hidden fields that should go inside the form.
540 * @return String HTML.
541 */
542 function getHiddenFields() {
543 global $wgArticlePath;
544
545 $html = '';
546 if( $this->getMethod() == 'post' ){
547 $html .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken(), array( 'id' => 'wpEditToken' ) ) . "\n";
548 $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
549 }
550
551 if ( strpos( $wgArticlePath, '?' ) !== false && $this->getMethod() == 'get' ) {
552 $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
553 }
554
555 foreach ( $this->mHiddenFields as $data ) {
556 list( $value, $attribs ) = $data;
557 $html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
558 }
559
560 return $html;
561 }
562
563 /**
564 * Get the submit and (potentially) reset buttons.
565 * @return String HTML.
566 */
567 function getButtons() {
568 $html = '';
569 $attribs = array();
570
571 if ( isset( $this->mSubmitID ) ) {
572 $attribs['id'] = $this->mSubmitID;
573 }
574
575 if ( isset( $this->mSubmitName ) ) {
576 $attribs['name'] = $this->mSubmitName;
577 }
578
579 if ( isset( $this->mSubmitTooltip ) ) {
580 $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
581 }
582
583 $attribs['class'] = 'mw-htmlform-submit';
584
585 $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
586
587 if ( $this->mShowReset ) {
588 $html .= Html::element(
589 'input',
590 array(
591 'type' => 'reset',
592 'value' => wfMsg( 'htmlform-reset' )
593 )
594 ) . "\n";
595 }
596
597 foreach ( $this->mButtons as $button ) {
598 $attrs = array(
599 'type' => 'submit',
600 'name' => $button['name'],
601 'value' => $button['value']
602 );
603
604 if ( $button['attribs'] ) {
605 $attrs += $button['attribs'];
606 }
607
608 if ( isset( $button['id'] ) ) {
609 $attrs['id'] = $button['id'];
610 }
611
612 $html .= Html::element( 'input', $attrs );
613 }
614
615 return $html;
616 }
617
618 /**
619 * Get the whole body of the form.
620 * @return String
621 */
622 function getBody() {
623 return $this->displaySection( $this->mFieldTree );
624 }
625
626 /**
627 * Format and display an error message stack.
628 * @param $errors String|Array|Status
629 * @return String
630 */
631 function getErrors( $errors ) {
632 if ( $errors instanceof Status ) {
633 if ( $errors->isOK() ) {
634 $errorstr = '';
635 } else {
636 $errorstr = $this->getOutput()->parse( $errors->getWikiText() );
637 }
638 } elseif ( is_array( $errors ) ) {
639 $errorstr = $this->formatErrors( $errors );
640 } else {
641 $errorstr = $errors;
642 }
643
644 return $errorstr
645 ? Html::rawElement( 'div', array( 'class' => 'error' ), $errorstr )
646 : '';
647 }
648
649 /**
650 * Format a stack of error messages into a single HTML string
651 * @param $errors Array of message keys/values
652 * @return String HTML, a <ul> list of errors
653 */
654 public static function formatErrors( $errors ) {
655 $errorstr = '';
656
657 foreach ( $errors as $error ) {
658 if ( is_array( $error ) ) {
659 $msg = array_shift( $error );
660 } else {
661 $msg = $error;
662 $error = array();
663 }
664
665 $errorstr .= Html::rawElement(
666 'li',
667 array(),
668 wfMsgExt( $msg, array( 'parseinline' ), $error )
669 );
670 }
671
672 $errorstr = Html::rawElement( 'ul', array(), $errorstr );
673
674 return $errorstr;
675 }
676
677 /**
678 * Set the text for the submit button
679 * @param $t String plaintext.
680 */
681 function setSubmitText( $t ) {
682 $this->mSubmitText = $t;
683 }
684
685 /**
686 * Set the text for the submit button to a message
687 * @since 1.19
688 * @param $msg String message key
689 */
690 public function setSubmitTextMsg( $msg ) {
691 return $this->setSubmitText( $this->msg( $msg )->text() );
692 }
693
694 /**
695 * Get the text for the submit button, either customised or a default.
696 * @return string
697 */
698 function getSubmitText() {
699 return $this->mSubmitText
700 ? $this->mSubmitText
701 : wfMsg( 'htmlform-submit' );
702 }
703
704 public function setSubmitName( $name ) {
705 $this->mSubmitName = $name;
706 }
707
708 public function setSubmitTooltip( $name ) {
709 $this->mSubmitTooltip = $name;
710 }
711
712 /**
713 * Set the id for the submit button.
714 * @param $t String.
715 * @todo FIXME: Integrity of $t is *not* validated
716 */
717 function setSubmitID( $t ) {
718 $this->mSubmitID = $t;
719 }
720
721 public function setId( $id ) {
722 $this->mId = $id;
723 }
724 /**
725 * Prompt the whole form to be wrapped in a <fieldset>, with
726 * this text as its <legend> element.
727 * @param $legend String HTML to go inside the <legend> element.
728 * Will be escaped
729 */
730 public function setWrapperLegend( $legend ) { $this->mWrapperLegend = $legend; }
731
732 /**
733 * Prompt the whole form to be wrapped in a <fieldset>, with
734 * this message as its <legend> element.
735 * @since 1.19
736 * @param $msg String message key
737 */
738 public function setWrapperLegendMsg( $msg ) {
739 return $this->setWrapperLegend( $this->msg( $msg )->escaped() );
740 }
741
742 /**
743 * Set the prefix for various default messages
744 * TODO: currently only used for the <fieldset> legend on forms
745 * with multiple sections; should be used elsewhre?
746 * @param $p String
747 */
748 function setMessagePrefix( $p ) {
749 $this->mMessagePrefix = $p;
750 }
751
752 /**
753 * Set the title for form submission
754 * @param $t Title of page the form is on/should be posted to
755 */
756 function setTitle( $t ) {
757 $this->mTitle = $t;
758 }
759
760 /**
761 * Get the title
762 * @return Title
763 */
764 function getTitle() {
765 return $this->mTitle === false
766 ? $this->getContext()->getTitle()
767 : $this->mTitle;
768 }
769
770 /**
771 * Set the method used to submit the form
772 * @param $method String
773 */
774 public function setMethod( $method = 'post' ) {
775 $this->mMethod = $method;
776 }
777
778 public function getMethod() {
779 return $this->mMethod;
780 }
781
782 /**
783 * TODO: Document
784 * @param $fields array[]|HTMLFormField[] array of fields (either arrays or objects)
785 * @param $sectionName string ID attribute of the <table> tag for this section, ignored if empty
786 * @param $fieldsetIDPrefix string ID prefix for the <fieldset> tag of each subsection, ignored if empty
787 * @return String
788 */
789 function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) {
790 $tableHtml = '';
791 $subsectionHtml = '';
792 $hasLeftColumn = false;
793
794 foreach ( $fields as $key => $value ) {
795 if ( is_object( $value ) ) {
796 $v = empty( $value->mParams['nodata'] )
797 ? $this->mFieldData[$key]
798 : $value->getDefault();
799 $tableHtml .= $value->getTableRow( $v );
800
801 if ( $value->getLabel() != '&#160;' ) {
802 $hasLeftColumn = true;
803 }
804 } elseif ( is_array( $value ) ) {
805 $section = $this->displaySection( $value, $key );
806 $legend = $this->getLegend( $key );
807 if ( isset( $this->mSectionHeaders[$key] ) ) {
808 $section = $this->mSectionHeaders[$key] . $section;
809 }
810 if ( isset( $this->mSectionFooters[$key] ) ) {
811 $section .= $this->mSectionFooters[$key];
812 }
813 $attributes = array();
814 if ( $fieldsetIDPrefix ) {
815 $attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
816 }
817 $subsectionHtml .= Xml::fieldset( $legend, $section, $attributes ) . "\n";
818 }
819 }
820
821 $classes = array();
822
823 if ( !$hasLeftColumn ) { // Avoid strange spacing when no labels exist
824 $classes[] = 'mw-htmlform-nolabel';
825 }
826
827 $attribs = array(
828 'class' => implode( ' ', $classes ),
829 );
830
831 if ( $sectionName ) {
832 $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" );
833 }
834
835 $tableHtml = Html::rawElement( 'table', $attribs,
836 Html::rawElement( 'tbody', array(), "\n$tableHtml\n" ) ) . "\n";
837
838 if ( $this->mSubSectionBeforeFields ) {
839 return $subsectionHtml . "\n" . $tableHtml;
840 } else {
841 return $tableHtml . "\n" . $subsectionHtml;
842 }
843 }
844
845 /**
846 * Construct the form fields from the Descriptor array
847 */
848 function loadData() {
849 $fieldData = array();
850
851 foreach ( $this->mFlatFields as $fieldname => $field ) {
852 if ( !empty( $field->mParams['nodata'] ) ) {
853 continue;
854 } elseif ( !empty( $field->mParams['disabled'] ) ) {
855 $fieldData[$fieldname] = $field->getDefault();
856 } else {
857 $fieldData[$fieldname] = $field->loadDataFromRequest( $this->getRequest() );
858 }
859 }
860
861 # Filter data.
862 foreach ( $fieldData as $name => &$value ) {
863 $field = $this->mFlatFields[$name];
864 $value = $field->filter( $value, $this->mFlatFields );
865 }
866
867 $this->mFieldData = $fieldData;
868 }
869
870 /**
871 * Stop a reset button being shown for this form
872 * @param $suppressReset Bool set to false to re-enable the
873 * button again
874 */
875 function suppressReset( $suppressReset = true ) {
876 $this->mShowReset = !$suppressReset;
877 }
878
879 /**
880 * Overload this if you want to apply special filtration routines
881 * to the form as a whole, after it's submitted but before it's
882 * processed.
883 * @param $data
884 * @return
885 */
886 function filterDataForSubmit( $data ) {
887 return $data;
888 }
889
890 /**
891 * Get a string to go in the <legend> of a section fieldset. Override this if you
892 * want something more complicated
893 * @param $key String
894 * @return String
895 */
896 public function getLegend( $key ) {
897 return wfMsg( "{$this->mMessagePrefix}-$key" );
898 }
899
900 /**
901 * Set the value for the action attribute of the form.
902 * When set to false (which is the default state), the set title is used.
903 *
904 * @since 1.19
905 *
906 * @param string|bool $action
907 */
908 public function setAction( $action ) {
909 $this->mAction = $action;
910 }
911
912 }
913
914 /**
915 * The parent class to generate form fields. Any field type should
916 * be a subclass of this.
917 */
918 abstract class HTMLFormField {
919
920 protected $mValidationCallback;
921 protected $mFilterCallback;
922 protected $mName;
923 public $mParams;
924 protected $mLabel; # String label. Set on construction
925 protected $mID;
926 protected $mClass = '';
927 protected $mDefault;
928
929 /**
930 * @var HTMLForm
931 */
932 public $mParent;
933
934 /**
935 * This function must be implemented to return the HTML to generate
936 * the input object itself. It should not implement the surrounding
937 * table cells/rows, or labels/help messages.
938 * @param $value String the value to set the input to; eg a default
939 * text for a text input.
940 * @return String valid HTML.
941 */
942 abstract function getInputHTML( $value );
943
944 /**
945 * Override this function to add specific validation checks on the
946 * field input. Don't forget to call parent::validate() to ensure
947 * that the user-defined callback mValidationCallback is still run
948 * @param $value String the value the field was submitted with
949 * @param $alldata Array the data collected from the form
950 * @return Mixed Bool true on success, or String error to display.
951 */
952 function validate( $value, $alldata ) {
953 if ( isset( $this->mParams['required'] ) && $value === '' ) {
954 return wfMsgExt( 'htmlform-required', 'parseinline' );
955 }
956
957 if ( isset( $this->mValidationCallback ) ) {
958 return call_user_func( $this->mValidationCallback, $value, $alldata, $this->mParent );
959 }
960
961 return true;
962 }
963
964 function filter( $value, $alldata ) {
965 if ( isset( $this->mFilterCallback ) ) {
966 $value = call_user_func( $this->mFilterCallback, $value, $alldata, $this->mParent );
967 }
968
969 return $value;
970 }
971
972 /**
973 * Should this field have a label, or is there no input element with the
974 * appropriate id for the label to point to?
975 *
976 * @return bool True to output a label, false to suppress
977 */
978 protected function needsLabel() {
979 return true;
980 }
981
982 /**
983 * Get the value that this input has been set to from a posted form,
984 * or the input's default value if it has not been set.
985 * @param $request WebRequest
986 * @return String the value
987 */
988 function loadDataFromRequest( $request ) {
989 if ( $request->getCheck( $this->mName ) ) {
990 return $request->getText( $this->mName );
991 } else {
992 return $this->getDefault();
993 }
994 }
995
996 /**
997 * Initialise the object
998 * @param $params array Associative Array. See HTMLForm doc for syntax.
999 */
1000 function __construct( $params ) {
1001 $this->mParams = $params;
1002
1003 # Generate the label from a message, if possible
1004 if ( isset( $params['label-message'] ) ) {
1005 $msgInfo = $params['label-message'];
1006
1007 if ( is_array( $msgInfo ) ) {
1008 $msg = array_shift( $msgInfo );
1009 } else {
1010 $msg = $msgInfo;
1011 $msgInfo = array();
1012 }
1013
1014 $this->mLabel = wfMsgExt( $msg, 'parseinline', $msgInfo );
1015 } elseif ( isset( $params['label'] ) ) {
1016 $this->mLabel = $params['label'];
1017 }
1018
1019 $this->mName = "wp{$params['fieldname']}";
1020 if ( isset( $params['name'] ) ) {
1021 $this->mName = $params['name'];
1022 }
1023
1024 $validName = Sanitizer::escapeId( $this->mName );
1025 if ( $this->mName != $validName && !isset( $params['nodata'] ) ) {
1026 throw new MWException( "Invalid name '{$this->mName}' passed to " . __METHOD__ );
1027 }
1028
1029 $this->mID = "mw-input-{$this->mName}";
1030
1031 if ( isset( $params['default'] ) ) {
1032 $this->mDefault = $params['default'];
1033 }
1034
1035 if ( isset( $params['id'] ) ) {
1036 $id = $params['id'];
1037 $validId = Sanitizer::escapeId( $id );
1038
1039 if ( $id != $validId ) {
1040 throw new MWException( "Invalid id '$id' passed to " . __METHOD__ );
1041 }
1042
1043 $this->mID = $id;
1044 }
1045
1046 if ( isset( $params['cssclass'] ) ) {
1047 $this->mClass = $params['cssclass'];
1048 }
1049
1050 if ( isset( $params['validation-callback'] ) ) {
1051 $this->mValidationCallback = $params['validation-callback'];
1052 }
1053
1054 if ( isset( $params['filter-callback'] ) ) {
1055 $this->mFilterCallback = $params['filter-callback'];
1056 }
1057
1058 if ( isset( $params['flatlist'] ) ){
1059 $this->mClass .= ' mw-htmlform-flatlist';
1060 }
1061 }
1062
1063 /**
1064 * Get the complete table row for the input, including help text,
1065 * labels, and whatever.
1066 * @param $value String the value to set the input to.
1067 * @return String complete HTML table row.
1068 */
1069 function getTableRow( $value ) {
1070 # Check for invalid data.
1071
1072 $errors = $this->validate( $value, $this->mParent->mFieldData );
1073
1074 $cellAttributes = array();
1075 $verticalLabel = false;
1076
1077 if ( !empty($this->mParams['vertical-label']) ) {
1078 $cellAttributes['colspan'] = 2;
1079 $verticalLabel = true;
1080 }
1081
1082 if ( $errors === true || ( !$this->mParent->getRequest()->wasPosted() && ( $this->mParent->getMethod() == 'post' ) ) ) {
1083 $errors = '';
1084 $errorClass = '';
1085 } else {
1086 $errors = self::formatErrors( $errors );
1087 $errorClass = 'mw-htmlform-invalid-input';
1088 }
1089
1090 $label = $this->getLabelHtml( $cellAttributes );
1091 $field = Html::rawElement(
1092 'td',
1093 array( 'class' => 'mw-input' ) + $cellAttributes,
1094 $this->getInputHTML( $value ) . "\n$errors"
1095 );
1096
1097 $fieldType = get_class( $this );
1098
1099 if ( $verticalLabel ) {
1100 $html = Html::rawElement( 'tr',
1101 array( 'class' => 'mw-htmlform-vertical-label' ), $label );
1102 $html .= Html::rawElement( 'tr',
1103 array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ),
1104 $field );
1105 } else {
1106 $html = Html::rawElement( 'tr',
1107 array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ),
1108 $label . $field );
1109 }
1110
1111 $helptext = null;
1112
1113 if ( isset( $this->mParams['help-message'] ) ) {
1114 $this->mParams['help-messages'] = array( $this->mParams['help-message'] );
1115 }
1116
1117 if ( isset( $this->mParams['help-messages'] ) ) {
1118 foreach( $this->mParams['help-messages'] as $name ) {
1119 $helpMessage = (array)$name;
1120 $msg = wfMessage( array_shift( $helpMessage ), $helpMessage );
1121
1122 if( $msg->exists() ) {
1123 if( is_null( $helptext ) ) {
1124 $helptext = '';
1125 } else {
1126 $helptext .= wfMessage( 'word-separator' )->escaped(); // some space
1127 }
1128 $helptext .= $msg->parse(); // Append message
1129 }
1130 }
1131 }
1132 elseif ( isset( $this->mParams['help'] ) ) {
1133 $helptext = $this->mParams['help'];
1134 }
1135
1136 if ( !is_null( $helptext ) ) {
1137 $row = Html::rawElement(
1138 'td',
1139 array( 'colspan' => 2, 'class' => 'htmlform-tip' ),
1140 $helptext
1141 );
1142 $row = Html::rawElement( 'tr', array(), $row );
1143 $html .= "$row\n";
1144 }
1145
1146 return $html;
1147 }
1148
1149 function getLabel() {
1150 return $this->mLabel;
1151 }
1152 function getLabelHtml( $cellAttributes = array() ) {
1153 # Don't output a for= attribute for labels with no associated input.
1154 # Kind of hacky here, possibly we don't want these to be <label>s at all.
1155 $for = array();
1156
1157 if ( $this->needsLabel() ) {
1158 $for['for'] = $this->mID;
1159 }
1160
1161 return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes,
1162 Html::rawElement( 'label', $for, $this->getLabel() )
1163 );
1164 }
1165
1166 function getDefault() {
1167 if ( isset( $this->mDefault ) ) {
1168 return $this->mDefault;
1169 } else {
1170 return null;
1171 }
1172 }
1173
1174 /**
1175 * Returns the attributes required for the tooltip and accesskey.
1176 *
1177 * @return array Attributes
1178 */
1179 public function getTooltipAndAccessKey() {
1180 if ( empty( $this->mParams['tooltip'] ) ) {
1181 return array();
1182 }
1183 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
1184 }
1185
1186 /**
1187 * flatten an array of options to a single array, for instance,
1188 * a set of <options> inside <optgroups>.
1189 * @param $options array Associative Array with values either Strings
1190 * or Arrays
1191 * @return Array flattened input
1192 */
1193 public static function flattenOptions( $options ) {
1194 $flatOpts = array();
1195
1196 foreach ( $options as $value ) {
1197 if ( is_array( $value ) ) {
1198 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1199 } else {
1200 $flatOpts[] = $value;
1201 }
1202 }
1203
1204 return $flatOpts;
1205 }
1206
1207 /**
1208 * Formats one or more errors as accepted by field validation-callback.
1209 * @param $errors String|Message|Array of strings or Message instances
1210 * @return String html
1211 * @since 1.18
1212 */
1213 protected static function formatErrors( $errors ) {
1214 if ( is_array( $errors ) && count( $errors ) === 1 ) {
1215 $errors = array_shift( $errors );
1216 }
1217
1218 if ( is_array( $errors ) ) {
1219 $lines = array();
1220 foreach ( $errors as $error ) {
1221 if ( $error instanceof Message ) {
1222 $lines[] = Html::rawElement( 'li', array(), $error->parse() );
1223 } else {
1224 $lines[] = Html::rawElement( 'li', array(), $error );
1225 }
1226 }
1227 return Html::rawElement( 'ul', array( 'class' => 'error' ), implode( "\n", $lines ) );
1228 } else {
1229 if ( $errors instanceof Message ) {
1230 $errors = $errors->parse();
1231 }
1232 return Html::rawElement( 'span', array( 'class' => 'error' ), $errors );
1233 }
1234 }
1235 }
1236
1237 class HTMLTextField extends HTMLFormField {
1238 function getSize() {
1239 return isset( $this->mParams['size'] )
1240 ? $this->mParams['size']
1241 : 45;
1242 }
1243
1244 function getInputHTML( $value ) {
1245 $attribs = array(
1246 'id' => $this->mID,
1247 'name' => $this->mName,
1248 'size' => $this->getSize(),
1249 'value' => $value,
1250 ) + $this->getTooltipAndAccessKey();
1251
1252 if ( $this->mClass !== '' ) {
1253 $attribs['class'] = $this->mClass;
1254 }
1255
1256 if ( isset( $this->mParams['maxlength'] ) ) {
1257 $attribs['maxlength'] = $this->mParams['maxlength'];
1258 }
1259
1260 if ( !empty( $this->mParams['disabled'] ) ) {
1261 $attribs['disabled'] = 'disabled';
1262 }
1263
1264 # TODO: Enforce pattern, step, required, readonly on the server side as
1265 # well
1266 foreach ( array( 'min', 'max', 'pattern', 'title', 'step',
1267 'placeholder' ) as $param ) {
1268 if ( isset( $this->mParams[$param] ) ) {
1269 $attribs[$param] = $this->mParams[$param];
1270 }
1271 }
1272
1273 foreach ( array( 'required', 'autofocus', 'multiple', 'readonly' ) as $param ) {
1274 if ( isset( $this->mParams[$param] ) ) {
1275 $attribs[$param] = '';
1276 }
1277 }
1278
1279 # Implement tiny differences between some field variants
1280 # here, rather than creating a new class for each one which
1281 # is essentially just a clone of this one.
1282 if ( isset( $this->mParams['type'] ) ) {
1283 switch ( $this->mParams['type'] ) {
1284 case 'email':
1285 $attribs['type'] = 'email';
1286 break;
1287 case 'int':
1288 $attribs['type'] = 'number';
1289 break;
1290 case 'float':
1291 $attribs['type'] = 'number';
1292 $attribs['step'] = 'any';
1293 break;
1294 # Pass through
1295 case 'password':
1296 case 'file':
1297 $attribs['type'] = $this->mParams['type'];
1298 break;
1299 }
1300 }
1301
1302 return Html::element( 'input', $attribs );
1303 }
1304 }
1305 class HTMLTextAreaField extends HTMLFormField {
1306 function getCols() {
1307 return isset( $this->mParams['cols'] )
1308 ? $this->mParams['cols']
1309 : 80;
1310 }
1311
1312 function getRows() {
1313 return isset( $this->mParams['rows'] )
1314 ? $this->mParams['rows']
1315 : 25;
1316 }
1317
1318 function getInputHTML( $value ) {
1319 $attribs = array(
1320 'id' => $this->mID,
1321 'name' => $this->mName,
1322 'cols' => $this->getCols(),
1323 'rows' => $this->getRows(),
1324 ) + $this->getTooltipAndAccessKey();
1325
1326 if ( $this->mClass !== '' ) {
1327 $attribs['class'] = $this->mClass;
1328 }
1329
1330 if ( !empty( $this->mParams['disabled'] ) ) {
1331 $attribs['disabled'] = 'disabled';
1332 }
1333
1334 if ( !empty( $this->mParams['readonly'] ) ) {
1335 $attribs['readonly'] = 'readonly';
1336 }
1337
1338 if ( isset( $this->mParams['placeholder'] ) ) {
1339 $attribs['placeholder'] = $this->mParams['placeholder'];
1340 }
1341
1342 foreach ( array( 'required', 'autofocus' ) as $param ) {
1343 if ( isset( $this->mParams[$param] ) ) {
1344 $attribs[$param] = '';
1345 }
1346 }
1347
1348 return Html::element( 'textarea', $attribs, $value );
1349 }
1350 }
1351
1352 /**
1353 * A field that will contain a numeric value
1354 */
1355 class HTMLFloatField extends HTMLTextField {
1356 function getSize() {
1357 return isset( $this->mParams['size'] )
1358 ? $this->mParams['size']
1359 : 20;
1360 }
1361
1362 function validate( $value, $alldata ) {
1363 $p = parent::validate( $value, $alldata );
1364
1365 if ( $p !== true ) {
1366 return $p;
1367 }
1368
1369 $value = trim( $value );
1370
1371 # http://dev.w3.org/html5/spec/common-microsyntaxes.html#real-numbers
1372 # with the addition that a leading '+' sign is ok.
1373 if ( !preg_match( '/^((\+|\-)?\d+(\.\d+)?(E(\+|\-)?\d+)?)?$/i', $value ) ) {
1374 return wfMsgExt( 'htmlform-float-invalid', 'parse' );
1375 }
1376
1377 # The "int" part of these message names is rather confusing.
1378 # They make equal sense for all numbers.
1379 if ( isset( $this->mParams['min'] ) ) {
1380 $min = $this->mParams['min'];
1381
1382 if ( $min > $value ) {
1383 return wfMsgExt( 'htmlform-int-toolow', 'parse', array( $min ) );
1384 }
1385 }
1386
1387 if ( isset( $this->mParams['max'] ) ) {
1388 $max = $this->mParams['max'];
1389
1390 if ( $max < $value ) {
1391 return wfMsgExt( 'htmlform-int-toohigh', 'parse', array( $max ) );
1392 }
1393 }
1394
1395 return true;
1396 }
1397 }
1398
1399 /**
1400 * A field that must contain a number
1401 */
1402 class HTMLIntField extends HTMLFloatField {
1403 function validate( $value, $alldata ) {
1404 $p = parent::validate( $value, $alldata );
1405
1406 if ( $p !== true ) {
1407 return $p;
1408 }
1409
1410 # http://dev.w3.org/html5/spec/common-microsyntaxes.html#signed-integers
1411 # with the addition that a leading '+' sign is ok. Note that leading zeros
1412 # are fine, and will be left in the input, which is useful for things like
1413 # phone numbers when you know that they are integers (the HTML5 type=tel
1414 # input does not require its value to be numeric). If you want a tidier
1415 # value to, eg, save in the DB, clean it up with intval().
1416 if ( !preg_match( '/^((\+|\-)?\d+)?$/', trim( $value ) )
1417 ) {
1418 return wfMsgExt( 'htmlform-int-invalid', 'parse' );
1419 }
1420
1421 return true;
1422 }
1423 }
1424
1425 /**
1426 * A checkbox field
1427 */
1428 class HTMLCheckField extends HTMLFormField {
1429 function getInputHTML( $value ) {
1430 if ( !empty( $this->mParams['invert'] ) ) {
1431 $value = !$value;
1432 }
1433
1434 $attr = $this->getTooltipAndAccessKey();
1435 $attr['id'] = $this->mID;
1436
1437 if ( !empty( $this->mParams['disabled'] ) ) {
1438 $attr['disabled'] = 'disabled';
1439 }
1440
1441 if ( $this->mClass !== '' ) {
1442 $attr['class'] = $this->mClass;
1443 }
1444
1445 return Xml::check( $this->mName, $value, $attr ) . '&#160;' .
1446 Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
1447 }
1448
1449 /**
1450 * For a checkbox, the label goes on the right hand side, and is
1451 * added in getInputHTML(), rather than HTMLFormField::getRow()
1452 * @return String
1453 */
1454 function getLabel() {
1455 return '&#160;';
1456 }
1457
1458 /**
1459 * @param $request WebRequest
1460 * @return String
1461 */
1462 function loadDataFromRequest( $request ) {
1463 $invert = false;
1464 if ( isset( $this->mParams['invert'] ) && $this->mParams['invert'] ) {
1465 $invert = true;
1466 }
1467
1468 // GetCheck won't work like we want for checks.
1469 // Fetch the value in either one of the two following case:
1470 // - we have a valid token (form got posted or GET forged by the user)
1471 // - checkbox name has a value (false or true), ie is not null
1472 if ( $request->getCheck( 'wpEditToken' ) || $request->getVal( $this->mName )!== null ) {
1473 // XOR has the following truth table, which is what we want
1474 // INVERT VALUE | OUTPUT
1475 // true true | false
1476 // false true | true
1477 // false false | false
1478 // true false | true
1479 return $request->getBool( $this->mName ) xor $invert;
1480 } else {
1481 return $this->getDefault();
1482 }
1483 }
1484 }
1485
1486 /**
1487 * A select dropdown field. Basically a wrapper for Xmlselect class
1488 */
1489 class HTMLSelectField extends HTMLFormField {
1490 function validate( $value, $alldata ) {
1491 $p = parent::validate( $value, $alldata );
1492
1493 if ( $p !== true ) {
1494 return $p;
1495 }
1496
1497 $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
1498
1499 if ( in_array( $value, $validOptions ) )
1500 return true;
1501 else
1502 return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
1503 }
1504
1505 function getInputHTML( $value ) {
1506 $select = new XmlSelect( $this->mName, $this->mID, strval( $value ) );
1507
1508 # If one of the options' 'name' is int(0), it is automatically selected.
1509 # because PHP sucks and thinks int(0) == 'some string'.
1510 # Working around this by forcing all of them to strings.
1511 foreach( $this->mParams['options'] as &$opt ){
1512 if( is_int( $opt ) ){
1513 $opt = strval( $opt );
1514 }
1515 }
1516 unset( $opt ); # PHP keeps $opt around as a reference, which is a bit scary
1517
1518 if ( !empty( $this->mParams['disabled'] ) ) {
1519 $select->setAttribute( 'disabled', 'disabled' );
1520 }
1521
1522 if ( $this->mClass !== '' ) {
1523 $select->setAttribute( 'class', $this->mClass );
1524 }
1525
1526 $select->addOptions( $this->mParams['options'] );
1527
1528 return $select->getHTML();
1529 }
1530 }
1531
1532 /**
1533 * Select dropdown field, with an additional "other" textbox.
1534 */
1535 class HTMLSelectOrOtherField extends HTMLTextField {
1536 static $jsAdded = false;
1537
1538 function __construct( $params ) {
1539 if ( !in_array( 'other', $params['options'], true ) ) {
1540 $msg = isset( $params['other'] ) ? $params['other'] : wfMsg( 'htmlform-selectorother-other' );
1541 $params['options'][$msg] = 'other';
1542 }
1543
1544 parent::__construct( $params );
1545 }
1546
1547 static function forceToStringRecursive( $array ) {
1548 if ( is_array( $array ) ) {
1549 return array_map( array( __CLASS__, 'forceToStringRecursive' ), $array );
1550 } else {
1551 return strval( $array );
1552 }
1553 }
1554
1555 function getInputHTML( $value ) {
1556 $valInSelect = false;
1557
1558 if ( $value !== false ) {
1559 $valInSelect = in_array(
1560 $value,
1561 HTMLFormField::flattenOptions( $this->mParams['options'] )
1562 );
1563 }
1564
1565 $selected = $valInSelect ? $value : 'other';
1566
1567 $opts = self::forceToStringRecursive( $this->mParams['options'] );
1568
1569 $select = new XmlSelect( $this->mName, $this->mID, $selected );
1570 $select->addOptions( $opts );
1571
1572 $select->setAttribute( 'class', 'mw-htmlform-select-or-other' );
1573
1574 $tbAttribs = array( 'id' => $this->mID . '-other', 'size' => $this->getSize() );
1575
1576 if ( !empty( $this->mParams['disabled'] ) ) {
1577 $select->setAttribute( 'disabled', 'disabled' );
1578 $tbAttribs['disabled'] = 'disabled';
1579 }
1580
1581 $select = $select->getHTML();
1582
1583 if ( isset( $this->mParams['maxlength'] ) ) {
1584 $tbAttribs['maxlength'] = $this->mParams['maxlength'];
1585 }
1586
1587 if ( $this->mClass !== '' ) {
1588 $tbAttribs['class'] = $this->mClass;
1589 }
1590
1591 $textbox = Html::input(
1592 $this->mName . '-other',
1593 $valInSelect ? '' : $value,
1594 'text',
1595 $tbAttribs
1596 );
1597
1598 return "$select<br />\n$textbox";
1599 }
1600
1601 /**
1602 * @param $request WebRequest
1603 * @return String
1604 */
1605 function loadDataFromRequest( $request ) {
1606 if ( $request->getCheck( $this->mName ) ) {
1607 $val = $request->getText( $this->mName );
1608
1609 if ( $val == 'other' ) {
1610 $val = $request->getText( $this->mName . '-other' );
1611 }
1612
1613 return $val;
1614 } else {
1615 return $this->getDefault();
1616 }
1617 }
1618 }
1619
1620 /**
1621 * Multi-select field
1622 */
1623 class HTMLMultiSelectField extends HTMLFormField {
1624
1625 function validate( $value, $alldata ) {
1626 $p = parent::validate( $value, $alldata );
1627
1628 if ( $p !== true ) {
1629 return $p;
1630 }
1631
1632 if ( !is_array( $value ) ) {
1633 return false;
1634 }
1635
1636 # If all options are valid, array_intersect of the valid options
1637 # and the provided options will return the provided options.
1638 $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
1639
1640 $validValues = array_intersect( $value, $validOptions );
1641 if ( count( $validValues ) == count( $value ) ) {
1642 return true;
1643 } else {
1644 return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
1645 }
1646 }
1647
1648 function getInputHTML( $value ) {
1649 $html = $this->formatOptions( $this->mParams['options'], $value );
1650
1651 return $html;
1652 }
1653
1654 function formatOptions( $options, $value ) {
1655 $html = '';
1656
1657 $attribs = array();
1658
1659 if ( !empty( $this->mParams['disabled'] ) ) {
1660 $attribs['disabled'] = 'disabled';
1661 }
1662
1663 foreach ( $options as $label => $info ) {
1664 if ( is_array( $info ) ) {
1665 $html .= Html::rawElement( 'h1', array(), $label ) . "\n";
1666 $html .= $this->formatOptions( $info, $value );
1667 } else {
1668 $thisAttribs = array( 'id' => "{$this->mID}-$info", 'value' => $info );
1669
1670 $checkbox = Xml::check(
1671 $this->mName . '[]',
1672 in_array( $info, $value, true ),
1673 $attribs + $thisAttribs );
1674 $checkbox .= '&#160;' . Html::rawElement( 'label', array( 'for' => "{$this->mID}-$info" ), $label );
1675
1676 $html .= ' ' . Html::rawElement( 'div', array( 'class' => 'mw-htmlform-flatlist-item' ), $checkbox );
1677 }
1678 }
1679
1680 return $html;
1681 }
1682
1683 /**
1684 * @param $request WebRequest
1685 * @return String
1686 */
1687 function loadDataFromRequest( $request ) {
1688 if ( $this->mParent->getMethod() == 'post' ) {
1689 if( $request->wasPosted() ){
1690 # Checkboxes are just not added to the request arrays if they're not checked,
1691 # so it's perfectly possible for there not to be an entry at all
1692 return $request->getArray( $this->mName, array() );
1693 } else {
1694 # That's ok, the user has not yet submitted the form, so show the defaults
1695 return $this->getDefault();
1696 }
1697 } else {
1698 # This is the impossible case: if we look at $_GET and see no data for our
1699 # field, is it because the user has not yet submitted the form, or that they
1700 # have submitted it with all the options unchecked? We will have to assume the
1701 # latter, which basically means that you can't specify 'positive' defaults
1702 # for GET forms.
1703 # @todo FIXME...
1704 return $request->getArray( $this->mName, array() );
1705 }
1706 }
1707
1708 function getDefault() {
1709 if ( isset( $this->mDefault ) ) {
1710 return $this->mDefault;
1711 } else {
1712 return array();
1713 }
1714 }
1715
1716 protected function needsLabel() {
1717 return false;
1718 }
1719 }
1720
1721 /**
1722 * Double field with a dropdown list constructed from a system message in the format
1723 * * Optgroup header
1724 * ** <option value>
1725 * * New Optgroup header
1726 * Plus a text field underneath for an additional reason. The 'value' of the field is
1727 * ""<select>: <extra reason>"", or "<extra reason>" if nothing has been selected in the
1728 * select dropdown.
1729 * @todo FIXME: If made 'required', only the text field should be compulsory.
1730 */
1731 class HTMLSelectAndOtherField extends HTMLSelectField {
1732
1733 function __construct( $params ) {
1734 if ( array_key_exists( 'other', $params ) ) {
1735 } elseif( array_key_exists( 'other-message', $params ) ){
1736 $params['other'] = wfMessage( $params['other-message'] )->plain();
1737 } else {
1738 $params['other'] = null;
1739 }
1740
1741 if ( array_key_exists( 'options', $params ) ) {
1742 # Options array already specified
1743 } elseif( array_key_exists( 'options-message', $params ) ){
1744 # Generate options array from a system message
1745 $params['options'] = self::parseMessage(
1746 wfMessage( $params['options-message'] )->inContentLanguage()->plain(),
1747 $params['other']
1748 );
1749 } else {
1750 # Sulk
1751 throw new MWException( 'HTMLSelectAndOtherField called without any options' );
1752 }
1753 $this->mFlatOptions = self::flattenOptions( $params['options'] );
1754
1755 parent::__construct( $params );
1756 }
1757
1758 /**
1759 * Build a drop-down box from a textual list.
1760 * @param $string String message text
1761 * @param $otherName String name of "other reason" option
1762 * @return Array
1763 * TODO: this is copied from Xml::listDropDown(), deprecate/avoid duplication?
1764 */
1765 public static function parseMessage( $string, $otherName=null ) {
1766 if( $otherName === null ){
1767 $otherName = wfMessage( 'htmlform-selectorother-other' )->plain();
1768 }
1769
1770 $optgroup = false;
1771 $options = array( $otherName => 'other' );
1772
1773 foreach ( explode( "\n", $string ) as $option ) {
1774 $value = trim( $option );
1775 if ( $value == '' ) {
1776 continue;
1777 } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) {
1778 # A new group is starting...
1779 $value = trim( substr( $value, 1 ) );
1780 $optgroup = $value;
1781 } elseif ( substr( $value, 0, 2) == '**' ) {
1782 # groupmember
1783 $opt = trim( substr( $value, 2 ) );
1784 if( $optgroup === false ){
1785 $options[$opt] = $opt;
1786 } else {
1787 $options[$optgroup][$opt] = $opt;
1788 }
1789 } else {
1790 # groupless reason list
1791 $optgroup = false;
1792 $options[$option] = $option;
1793 }
1794 }
1795
1796 return $options;
1797 }
1798
1799 function getInputHTML( $value ) {
1800 $select = parent::getInputHTML( $value[1] );
1801
1802 $textAttribs = array(
1803 'id' => $this->mID . '-other',
1804 'size' => $this->getSize(),
1805 );
1806
1807 if ( $this->mClass !== '' ) {
1808 $textAttribs['class'] = $this->mClass;
1809 }
1810
1811 foreach ( array( 'required', 'autofocus', 'multiple', 'disabled' ) as $param ) {
1812 if ( isset( $this->mParams[$param] ) ) {
1813 $textAttribs[$param] = '';
1814 }
1815 }
1816
1817 $textbox = Html::input(
1818 $this->mName . '-other',
1819 $value[2],
1820 'text',
1821 $textAttribs
1822 );
1823
1824 return "$select<br />\n$textbox";
1825 }
1826
1827 /**
1828 * @param $request WebRequest
1829 * @return Array( <overall message>, <select value>, <text field value> )
1830 */
1831 function loadDataFromRequest( $request ) {
1832 if ( $request->getCheck( $this->mName ) ) {
1833
1834 $list = $request->getText( $this->mName );
1835 $text = $request->getText( $this->mName . '-other' );
1836
1837 if ( $list == 'other' ) {
1838 $final = $text;
1839 } elseif( !in_array( $list, $this->mFlatOptions ) ){
1840 # User has spoofed the select form to give an option which wasn't
1841 # in the original offer. Sulk...
1842 $final = $text;
1843 } elseif( $text == '' ) {
1844 $final = $list;
1845 } else {
1846 $final = $list . wfMsgForContent( 'colon-separator' ) . $text;
1847 }
1848
1849 } else {
1850 $final = $this->getDefault();
1851
1852 $list = 'other';
1853 $text = $final;
1854 foreach ( $this->mFlatOptions as $option ) {
1855 $match = $option . wfMsgForContent( 'colon-separator' );
1856 if( strpos( $text, $match ) === 0 ) {
1857 $list = $option;
1858 $text = substr( $text, strlen( $match ) );
1859 break;
1860 }
1861 }
1862 }
1863 return array( $final, $list, $text );
1864 }
1865
1866 function getSize() {
1867 return isset( $this->mParams['size'] )
1868 ? $this->mParams['size']
1869 : 45;
1870 }
1871
1872 function validate( $value, $alldata ) {
1873 # HTMLSelectField forces $value to be one of the options in the select
1874 # field, which is not useful here. But we do want the validation further up
1875 # the chain
1876 $p = parent::validate( $value[1], $alldata );
1877
1878 if ( $p !== true ) {
1879 return $p;
1880 }
1881
1882 if( isset( $this->mParams['required'] ) && $value[1] === '' ){
1883 return wfMsgExt( 'htmlform-required', 'parseinline' );
1884 }
1885
1886 return true;
1887 }
1888 }
1889
1890 /**
1891 * Radio checkbox fields.
1892 */
1893 class HTMLRadioField extends HTMLFormField {
1894
1895
1896 function validate( $value, $alldata ) {
1897 $p = parent::validate( $value, $alldata );
1898
1899 if ( $p !== true ) {
1900 return $p;
1901 }
1902
1903 if ( !is_string( $value ) && !is_int( $value ) ) {
1904 return false;
1905 }
1906
1907 $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
1908
1909 if ( in_array( $value, $validOptions ) ) {
1910 return true;
1911 } else {
1912 return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
1913 }
1914 }
1915
1916 /**
1917 * This returns a block of all the radio options, in one cell.
1918 * @see includes/HTMLFormField#getInputHTML()
1919 * @param $value String
1920 * @return String
1921 */
1922 function getInputHTML( $value ) {
1923 $html = $this->formatOptions( $this->mParams['options'], $value );
1924
1925 return $html;
1926 }
1927
1928 function formatOptions( $options, $value ) {
1929 $html = '';
1930
1931 $attribs = array();
1932 if ( !empty( $this->mParams['disabled'] ) ) {
1933 $attribs['disabled'] = 'disabled';
1934 }
1935
1936 # TODO: should this produce an unordered list perhaps?
1937 foreach ( $options as $label => $info ) {
1938 if ( is_array( $info ) ) {
1939 $html .= Html::rawElement( 'h1', array(), $label ) . "\n";
1940 $html .= $this->formatOptions( $info, $value );
1941 } else {
1942 $id = Sanitizer::escapeId( $this->mID . "-$info" );
1943 $radio = Xml::radio(
1944 $this->mName,
1945 $info,
1946 $info == $value,
1947 $attribs + array( 'id' => $id )
1948 );
1949 $radio .= '&#160;' .
1950 Html::rawElement( 'label', array( 'for' => $id ), $label );
1951
1952 $html .= ' ' . Html::rawElement( 'div', array( 'class' => 'mw-htmlform-flatlist-item' ), $radio );
1953 }
1954 }
1955
1956 return $html;
1957 }
1958
1959 protected function needsLabel() {
1960 return false;
1961 }
1962 }
1963
1964 /**
1965 * An information field (text blob), not a proper input.
1966 */
1967 class HTMLInfoField extends HTMLFormField {
1968 function __construct( $info ) {
1969 $info['nodata'] = true;
1970
1971 parent::__construct( $info );
1972 }
1973
1974 function getInputHTML( $value ) {
1975 return !empty( $this->mParams['raw'] ) ? $value : htmlspecialchars( $value );
1976 }
1977
1978 function getTableRow( $value ) {
1979 if ( !empty( $this->mParams['rawrow'] ) ) {
1980 return $value;
1981 }
1982
1983 return parent::getTableRow( $value );
1984 }
1985
1986 protected function needsLabel() {
1987 return false;
1988 }
1989 }
1990
1991 class HTMLHiddenField extends HTMLFormField {
1992 public function __construct( $params ) {
1993 parent::__construct( $params );
1994
1995 # Per HTML5 spec, hidden fields cannot be 'required'
1996 # http://dev.w3.org/html5/spec/states-of-the-type-attribute.html#hidden-state
1997 unset( $this->mParams['required'] );
1998 }
1999
2000 public function getTableRow( $value ) {
2001 $params = array();
2002 if ( $this->mID ) {
2003 $params['id'] = $this->mID;
2004 }
2005
2006 $this->mParent->addHiddenField(
2007 $this->mName,
2008 $this->mDefault,
2009 $params
2010 );
2011
2012 return '';
2013 }
2014
2015 public function getInputHTML( $value ) { return ''; }
2016 }
2017
2018 /**
2019 * Add a submit button inline in the form (as opposed to
2020 * HTMLForm::addButton(), which will add it at the end).
2021 */
2022 class HTMLSubmitField extends HTMLFormField {
2023
2024 function __construct( $info ) {
2025 $info['nodata'] = true;
2026 parent::__construct( $info );
2027 }
2028
2029 function getInputHTML( $value ) {
2030 return Xml::submitButton(
2031 $value,
2032 array(
2033 'class' => 'mw-htmlform-submit ' . $this->mClass,
2034 'name' => $this->mName,
2035 'id' => $this->mID,
2036 )
2037 );
2038 }
2039
2040 protected function needsLabel() {
2041 return false;
2042 }
2043
2044 /**
2045 * Button cannot be invalid
2046 * @param $value String
2047 * @param $alldata Array
2048 * @return Bool
2049 */
2050 public function validate( $value, $alldata ){
2051 return true;
2052 }
2053 }
2054
2055 class HTMLEditTools extends HTMLFormField {
2056 public function getInputHTML( $value ) {
2057 return '';
2058 }
2059
2060 public function getTableRow( $value ) {
2061 if ( empty( $this->mParams['message'] ) ) {
2062 $msg = wfMessage( 'edittools' );
2063 } else {
2064 $msg = wfMessage( $this->mParams['message'] );
2065 if ( $msg->isDisabled() ) {
2066 $msg = wfMessage( 'edittools' );
2067 }
2068 }
2069 $msg->inContentLanguage();
2070
2071
2072 return '<tr><td></td><td class="mw-input">'
2073 . '<div class="mw-editTools">'
2074 . $msg->parseAsBlock()
2075 . "</div></td></tr>\n";
2076 }
2077 }