5 static $jsAdded = false;
7 /* The descriptor is an array of arrays.
9 'fieldname' => array( 'section' => 'section/subsection',
15 static $typeMappings = array(
16 'text' => 'HTMLTextField',
17 'select' => 'HTMLSelectField',
18 'radio' => 'HTMLRadioField',
19 'multiselect' => 'HTMLMultiSelectField',
20 'check' => 'HTMLCheckField',
21 'toggle' => 'HTMLCheckField',
22 'int' => 'HTMLIntField',
23 'float' => 'HTMLFloatField',
24 'info' => 'HTMLInfoField',
25 'selectorother' => 'HTMLSelectOrOtherField',
28 function __construct( $descriptor, $messagePrefix ) {
29 $this->mMessagePrefix
= $messagePrefix;
31 // Expand out into a tree.
32 $loadedDescriptor = array();
33 $this->mFlatFields
= array();
35 foreach( $descriptor as $fieldname => $info ) {
37 if ( isset( $info['section'] ) )
38 $section = $info['section'];
40 $info['name'] = $fieldname;
42 $field = $this->loadInputFromParameters( $info );
43 $field->mParent
= $this;
45 $setSection =& $loadedDescriptor;
47 $sectionParts = explode( '/', $section );
49 while( count( $sectionParts ) ) {
50 $newName = array_shift( $sectionParts );
52 if ( !isset( $setSection[$newName] ) ) {
53 $setSection[$newName] = array();
56 $setSection =& $setSection[$newName];
60 $setSection[$fieldname] = $field;
61 $this->mFlatFields
[$fieldname] = $field;
64 $this->mFieldTree
= $loadedDescriptor;
66 $this->mShowReset
= true;
69 static function addJS() {
70 if( self
::$jsAdded ) return;
72 global $wgOut, $wgStylePath;
74 $wgOut->addScriptFile( "$wgStylePath/common/htmlform.js" );
77 static function loadInputFromParameters( $descriptor ) {
78 if ( isset( $descriptor['class'] ) ) {
79 $class = $descriptor['class'];
80 } elseif ( isset( $descriptor['type'] ) ) {
81 $class = self
::$typeMappings[$descriptor['type']];
82 $descriptor['class'] = $class;
86 throw new MWException( "Descriptor with no class: " . print_r( $descriptor, true ) );
89 $obj = new $class( $descriptor );
99 // Load data from the request.
103 global $wgUser, $wgRequest;
104 $editToken = $wgRequest->getVal( 'wpEditToken' );
107 if ( $wgUser->matchEditToken( $editToken ) )
108 $result = $this->trySubmit();
110 if( $result === true )
114 $this->displayForm( $result );
118 * TRUE == Successful submission
119 * FALSE == No submission attempted
120 * Anything else == Error to display.
122 function trySubmit() {
123 // Check for validation
124 foreach( $this->mFlatFields
as $fieldname => $field ) {
125 if ( !empty( $field->mParams
['nodata'] ) ) continue;
126 if ( $field->validate( $this->mFieldData
[$fieldname],
127 $this->mFieldData
) !== true ) {
128 return isset( $this->mValidationErrorMessage
) ?
129 $this->mValidationErrorMessage
: array( 'htmlform-invalid-input' );
133 $callback = $this->mSubmitCallback
;
135 $data = $this->filterDataForSubmit( $this->mFieldData
);
137 $res = call_user_func( $callback, $data );
142 function setSubmitCallback( $cb ) {
143 $this->mSubmitCallback
= $cb;
146 function setValidationErrorMessage( $msg ) {
147 $this->mValidationErrorMessage
= $msg;
150 function setIntro( $msg ) {
151 $this->mIntro
= $msg;
154 function displayForm( $submitResult ) {
157 if ( $submitResult !== false ) {
158 $this->displayErrors( $submitResult );
161 if ( isset( $this->mIntro
) ) {
162 $wgOut->addHTML( $this->mIntro
);
165 $html = $this->getBody();
168 $html .= $this->getHiddenFields();
171 $html .= $this->getButtons();
173 $html = $this->wrapForm( $html );
175 $wgOut->addHTML( $html );
178 function wrapForm( $html ) {
182 'action' => $this->getTitle()->getFullURL(),
189 function getHiddenFields() {
193 $html .= Xml
::hidden( 'wpEditToken', $wgUser->editToken() ) . "\n";
194 $html .= Xml
::hidden( 'title', $this->getTitle() ) . "\n";
199 function getButtons() {
204 if ( isset( $this->mSubmitID
) )
205 $attribs['id'] = $this->mSubmitID
;
207 $attribs['class'] = 'mw-htmlform-submit';
209 $html .= Xml
::submitButton( $this->getSubmitText(), $attribs ) . "\n";
211 if( $this->mShowReset
) {
212 $html .= Xml
::element(
216 'value' => wfMsg( 'htmlform-reset' )
225 return $this->displaySection( $this->mFieldTree
);
228 function displayErrors( $errors ) {
229 if ( is_array( $errors ) ) {
230 $errorstr = $this->formatErrors( $errors );
235 $errorstr = Xml
::tags( 'div', array( 'class' => 'error' ), $errorstr );
238 $wgOut->addHTML( $errorstr );
241 static function formatErrors( $errors ) {
243 foreach ( $errors as $error ) {
244 if( is_array( $error ) ) {
245 $msg = array_shift( $error );
250 $errorstr .= Xml
::tags(
253 wfMsgExt( $msg, array( 'parseinline' ), $error )
257 $errorstr = Xml
::tags( 'ul', null, $errorstr );
262 function setSubmitText( $t ) {
263 $this->mSubmitText
= $t;
266 function getSubmitText() {
267 return isset( $this->mSubmitText
) ?
$this->mSubmitText
: wfMsg( 'htmlform-submit' );
270 function setSubmitID( $t ) {
271 $this->mSubmitID
= $t;
274 function setMessagePrefix( $p ) {
275 $this->mMessagePrefix
= $p;
278 function setTitle( $t ) {
282 function getTitle() {
283 return $this->mTitle
;
286 function displaySection( $fields ) {
288 $subsectionHtml = '';
289 $hasLeftColumn = false;
291 foreach( $fields as $key => $value ) {
292 if ( is_object( $value ) ) {
293 $v = empty( $value->mParams
['nodata'] )
294 ?
$this->mFieldData
[$key]
295 : $value->getDefault();
296 $tableHtml .= $value->getTableRow( $v );
298 if( $value->getLabel() != ' ' )
299 $hasLeftColumn = true;
300 } elseif ( is_array( $value ) ) {
301 $section = $this->displaySection( $value );
302 $legend = wfMsg( "{$this->mMessagePrefix}-$key" );
303 $subsectionHtml .= Xml
::fieldset( $legend, $section ) . "\n";
308 if( !$hasLeftColumn ) // Avoid strange spacing when no labels exist
309 $classes[] = 'mw-htmlform-nolabel';
310 $classes = implode( ' ', $classes );
312 $tableHtml = "<table class='$classes'><tbody>\n$tableHtml\n</tbody></table>\n";
314 return $subsectionHtml . "\n" . $tableHtml;
317 function loadData() {
320 $fieldData = array();
322 foreach( $this->mFlatFields
as $fieldname => $field ) {
323 if ( !empty( $field->mParams
['nodata'] ) ) continue;
324 if ( !empty( $field->mParams
['disabled'] ) ) {
325 $fieldData[$fieldname] = $field->getDefault();
327 $fieldData[$fieldname] = $field->loadDataFromRequest( $wgRequest );
332 foreach( $fieldData as $name => &$value ) {
333 $field = $this->mFlatFields
[$name];
334 $value = $field->filter( $value, $this->mFlatFields
);
337 $this->mFieldData
= $fieldData;
340 function importData( $fieldData ) {
342 foreach( $fieldData as $name => &$value ) {
343 $field = $this->mFlatFields
[$name];
344 $value = $field->filter( $value, $this->mFlatFields
);
347 foreach( $this->mFlatFields
as $fieldname => $field ) {
348 if ( !isset( $fieldData[$fieldname] ) )
349 $fieldData[$fieldname] = $field->getDefault();
352 $this->mFieldData
= $fieldData;
355 function suppressReset( $suppressReset = true ) {
356 $this->mShowReset
= !$suppressReset;
359 function filterDataForSubmit( $data ) {
364 abstract class HTMLFormField
{
365 abstract function getInputHTML( $value );
367 function validate( $value, $alldata ) {
368 if ( isset( $this->mValidationCallback
) ) {
369 return call_user_func( $this->mValidationCallback
, $value, $alldata );
375 function filter( $value, $alldata ) {
376 if( isset( $this->mFilterCallback
) ) {
377 $value = call_user_func( $this->mFilterCallback
, $value, $alldata );
383 function loadDataFromRequest( $request ) {
384 if( $request->getCheck( $this->mName
) ) {
385 return $request->getText( $this->mName
);
387 return $this->getDefault();
391 function __construct( $params ) {
392 $this->mParams
= $params;
394 if( isset( $params['label-message'] ) ) {
395 $msgInfo = $params['label-message'];
397 if ( is_array( $msgInfo ) ) {
398 $msg = array_shift( $msgInfo );
404 $this->mLabel
= wfMsgExt( $msg, 'parseinline', $msgInfo );
405 } elseif ( isset( $params['label'] ) ) {
406 $this->mLabel
= $params['label'];
409 if ( isset( $params['name'] ) ) {
410 $name = $params['name'];
411 $validName = Sanitizer
::escapeId( $name );
412 if( $name != $validName ) {
413 throw new MWException("Invalid name '$name' passed to " . __METHOD__
);
415 $this->mName
= 'wp'.$name;
416 $this->mID
= 'mw-input-'.$name;
419 if ( isset( $params['default'] ) ) {
420 $this->mDefault
= $params['default'];
423 if ( isset( $params['id'] ) ) {
425 $validId = Sanitizer
::escapeId( $id );
426 if( $id != $validId ) {
427 throw new MWException("Invalid id '$id' passed to " . __METHOD__
);
432 if ( isset( $params['validation-callback'] ) ) {
433 $this->mValidationCallback
= $params['validation-callback'];
436 if ( isset( $params['filter-callback'] ) ) {
437 $this->mFilterCallback
= $params['filter-callback'];
441 function getTableRow( $value ) {
442 // Check for invalid data.
445 $errors = $this->validate( $value, $this->mParent
->mFieldData
);
446 if ( $errors === true ||
!$wgRequest->wasPosted() ) {
449 $errors = Xml
::tags( 'span', array( 'class' => 'error' ), $errors );
454 $html .= Xml
::tags( 'td', array( 'class' => 'mw-label' ),
455 Xml
::tags( 'label', array( 'for' => $this->mID
), $this->getLabel() )
457 $html .= Xml
::tags( 'td', array( 'class' => 'mw-input' ),
458 $this->getInputHTML( $value ) ."\n$errors" );
460 $fieldType = get_class( $this );
462 $html = Xml
::tags( 'tr', array( 'class' => "mw-htmlform-field-$fieldType" ),
466 if ( isset( $this->mParams
['help-message'] ) ) {
467 $msg = $this->mParams
['help-message'];
468 $helptext = wfMsgExt( $msg, 'parseinline' );
469 if ( wfEmptyMsg( $msg, $helptext ) ) {
473 } elseif ( isset( $this->mParams
['help'] ) ) {
474 $helptext = $this->mParams
['help'];
477 if ( !is_null( $helptext ) ) {
478 $row = Xml
::tags( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ),
480 $row = Xml
::tags( 'tr', null, $row );
487 function getLabel() {
488 return $this->mLabel
;
491 function getDefault() {
492 if ( isset( $this->mDefault
) ) {
493 return $this->mDefault
;
499 static function flattenOptions( $options ) {
502 foreach( $options as $key => $value ) {
503 if ( is_array( $value ) ) {
504 $flatOpts = array_merge( $flatOpts, self
::flattenOptions( $value ) );
506 $flatOpts[] = $value;
514 class HTMLTextField
extends HTMLFormField
{
517 return isset( $this->mParams
['size'] ) ?
$this->mParams
['size'] : 45;
520 function getInputHTML( $value ) {
521 $attribs = array( 'id' => $this->mID
);
523 if ( isset( $this->mParams
['maxlength'] ) ) {
524 $attribs['maxlength'] = $this->mParams
['maxlength'];
527 if( !empty( $this->mParams
['disabled'] ) ) {
528 $attribs['disabled'] = 'disabled';
541 class HTMLFloatField
extends HTMLTextField
{
543 return isset( $this->mParams
['size'] ) ?
$this->mParams
['size'] : 20;
546 function validate( $value, $alldata ) {
547 $p = parent
::validate( $value, $alldata );
549 if ( $p !== true ) return $p;
551 if ( floatval( $value ) != $value ) {
552 return wfMsgExt( 'htmlform-float-invalid', 'parse' );
557 # The "int" part of these message names is rather confusing. They make
558 # equal sense for all numbers.
559 if ( isset( $this->mParams
['min'] ) ) {
560 $min = $this->mParams
['min'];
562 return wfMsgExt( 'htmlform-int-toolow', 'parse', array( $min ) );
565 if ( isset( $this->mParams
['max'] ) ) {
566 $max = $this->mParams
['max'];
568 return wfMsgExt( 'htmlform-int-toohigh', 'parse', array( $max ) );
575 class HTMLIntField
extends HTMLFloatField
{
576 function validate( $value, $alldata ) {
577 $p = parent
::validate( $value, $alldata );
579 if ( $p !== true ) return $p;
581 if ( intval( $value ) != $value ) {
582 return wfMsgExt( 'htmlform-int-invalid', 'parse' );
589 class HTMLCheckField
extends HTMLFormField
{
590 function getInputHTML( $value ) {
591 if ( !empty( $this->mParams
['invert'] ) )
594 $attr = array( 'id' => $this->mID
);
595 if( !empty( $this->mParams
['disabled'] ) ) {
596 $attr['disabled'] = 'disabled';
599 return Xml
::check( $this->mName
, $value, $attr ) . ' ' .
600 Xml
::tags( 'label', array( 'for' => $this->mID
), $this->mLabel
);
603 function getLabel() {
604 return ' '; // In the right-hand column.
607 function loadDataFromRequest( $request ) {
609 if ( isset( $this->mParams
['invert'] ) && $this->mParams
['invert'] ) {
613 // GetCheck won't work like we want for checks.
614 if( $request->getCheck( 'wpEditToken' ) ) {
615 // XOR has the following truth table, which is what we want
616 // INVERT VALUE | OUTPUT
619 // false false | false
621 return $request->getBool( $this->mName
) xor $invert;
623 return $this->getDefault();
628 class HTMLSelectField
extends HTMLFormField
{
630 function validate( $value, $alldata ) {
631 $p = parent
::validate( $value, $alldata );
632 if( $p !== true ) return $p;
634 $validOptions = HTMLFormField
::flattenOptions( $this->mParams
['options'] );
635 if ( in_array( $value, $validOptions ) )
638 return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
641 function getInputHTML( $value ) {
642 $select = new XmlSelect( $this->mName
, $this->mID
, strval( $value ) );
644 // If one of the options' 'name' is int(0), it is automatically selected.
645 // because PHP sucks and things int(0) == 'some string'.
646 // Working around this by forcing all of them to strings.
647 $options = array_map( 'strval', $this->mParams
['options'] );
649 if( !empty( $this->mParams
['disabled'] ) ) {
650 $select->setAttribute( 'disabled', 'disabled' );
653 $select->addOptions( $options );
655 return $select->getHTML();
659 class HTMLSelectOrOtherField
extends HTMLTextField
{
660 static $jsAdded = false;
662 function __construct( $params ) {
663 if( !in_array( 'other', $params['options'] ) ) {
664 $params['options'][wfMsg( 'htmlform-selectorother-other' )] = 'other';
667 parent
::__construct( $params );
670 static function forceToStringRecursive( $array ) {
671 if ( is_array($array) ) {
672 return array_map( array( __CLASS__
, 'forceToStringRecursive' ), $array);
674 return strval($array);
678 function getInputHTML( $value ) {
679 $valInSelect = false;
681 if( $value !== false )
682 $valInSelect = in_array( $value,
683 HTMLFormField
::flattenOptions( $this->mParams
['options'] ) );
685 $selected = $valInSelect ?
$value : 'other';
687 $opts = self
::forceToStringRecursive( $this->mParams
['options'] );
689 $select = new XmlSelect( $this->mName
, $this->mID
, $selected );
690 $select->addOptions( $opts );
692 $select->setAttribute( 'class', 'mw-htmlform-select-or-other' );
694 $tbAttribs = array( 'id' => $this->mID
. '-other' );
695 if( !empty( $this->mParams
['disabled'] ) ) {
696 $select->setAttribute( 'disabled', 'disabled' );
697 $tbAttribs['disabled'] = 'disabled';
700 $select = $select->getHTML();
702 if ( isset( $this->mParams
['maxlength'] ) ) {
703 $tbAttribs['maxlength'] = $this->mParams
['maxlength'];
706 $textbox = Xml
::input( $this->mName
. '-other',
708 $valInSelect ?
'' : $value,
711 return "$select<br/>\n$textbox";
714 function loadDataFromRequest( $request ) {
715 if( $request->getCheck( $this->mName
) ) {
716 $val = $request->getText( $this->mName
);
718 if( $val == 'other' ) {
719 $val = $request->getText( $this->mName
. '-other' );
724 return $this->getDefault();
729 class HTMLMultiSelectField
extends HTMLFormField
{
730 function validate( $value, $alldata ) {
731 $p = parent
::validate( $value, $alldata );
732 if( $p !== true ) return $p;
734 if( !is_array( $value ) ) return false;
736 // If all options are valid, array_intersect of the valid options and the provided
737 // options will return the provided options.
738 $validOptions = HTMLFormField
::flattenOptions( $this->mParams
['options'] );
740 $validValues = array_intersect( $value, $validOptions );
741 if ( count( $validValues ) == count( $value ) )
744 return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
747 function getInputHTML( $value ) {
748 $html = $this->formatOptions( $this->mParams
['options'], $value );
753 function formatOptions( $options, $value ) {
757 if ( !empty( $this->mParams
['disabled'] ) ) {
758 $attribs['disabled'] = 'disabled';
761 foreach( $options as $label => $info ) {
762 if( is_array( $info ) ) {
763 $html .= Xml
::tags( 'h1', null, $label ) . "\n";
764 $html .= $this->formatOptions( $info, $value );
766 $thisAttribs = array( 'id' => $this->mID
. "-$info", 'value' => $info );
768 $checkbox = Xml
::check( $this->mName
. '[]', in_array( $info, $value ),
769 $attribs +
$thisAttribs );
770 $checkbox .= ' ' . Xml
::tags( 'label', array( 'for' => $this->mID
. "-$info" ), $label );
772 $html .= $checkbox . '<br />';
779 function loadDataFromRequest( $request ) {
780 // won't work with getCheck
781 if( $request->getCheck( 'wpEditToken' ) ) {
782 $arr = $request->getArray( $this->mName
);
789 return $this->getDefault();
793 function getDefault() {
794 if ( isset( $this->mDefault
) ) {
795 return $this->mDefault
;
802 class HTMLRadioField
extends HTMLFormField
{
803 function validate( $value, $alldata ) {
804 $p = parent
::validate( $value, $alldata );
805 if( $p !== true ) return $p;
807 if( !is_string( $value ) && !is_int( $value ) )
810 $validOptions = HTMLFormField
::flattenOptions( $this->mParams
['options'] );
812 if ( in_array( $value, $validOptions ) )
815 return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
818 function getInputHTML( $value ) {
819 $html = $this->formatOptions( $this->mParams
['options'], $value );
824 function formatOptions( $options, $value ) {
828 if ( !empty( $this->mParams
['disabled'] ) ) {
829 $attribs['disabled'] = 'disabled';
832 foreach( $options as $label => $info ) {
833 if( is_array( $info ) ) {
834 $html .= Xml
::tags( 'h1', null, $label ) . "\n";
835 $html .= $this->formatOptions( $info, $value );
837 $id = Sanitizer
::escapeId( $this->mID
. "-$info" );
838 $html .= Xml
::radio( $this->mName
, $info, $info == $value,
839 $attribs +
array( 'id' => $id ) );
841 Xml
::tags( 'label', array( 'for' => $id ), $label );
851 class HTMLInfoField
extends HTMLFormField
{
852 function __construct( $info ) {
853 $info['nodata'] = true;
855 parent
::__construct( $info );
858 function getInputHTML( $value ) {
859 return !empty( $this->mParams
['raw'] ) ?
$value : htmlspecialchars( $value );
862 function getTableRow( $value ) {
863 if ( !empty( $this->mParams
['rawrow'] ) ) {
867 return parent
::getTableRow( $value );